| ################################################################################ |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| ################################################################################ |
| |
| """Merge function for the ``aggregation`` merge engine. |
| |
| Rows sharing a primary key are folded across each non-PK field by the |
| per-field :class:`FieldAggregator` configured in table options. |
| ``DeduplicateMergeFunction`` keeps only the latest row; |
| ``PartialUpdateMergeFunction`` lets later writes "fill in" fields the |
| earlier writes left null; ``AggregateMergeFunction`` runs an actual |
| aggregation (sum / max / min / last_value / ...) per column. |
| |
| This is the **core merge semantics only**. Retract on DELETE / |
| UPDATE_BEFORE rows (with ``aggregation.remove-record-on-delete`` and |
| ``fields.<field>.ignore-retract`` opt-ins) and ~14 additional |
| aggregators (``product`` / ``listagg`` / ``collect`` / ``merge_map`` / |
| ``nested_update`` / ``theta_sketch`` / ``hll_sketch`` / |
| ``roaring_bitmap_*``) are intentionally deferred. Non-INSERT row |
| kinds raise ``NotImplementedError`` at :meth:`add` time so we never |
| silently corrupt data with a half-implemented contract, and |
| out-of-scope aggregator identifiers / options are rejected up-front in |
| :mod:`pypaimon.read.merge_engine_support`. |
| """ |
| |
| from typing import Any, List, Optional |
| |
| from pypaimon.read.reader.aggregate import create_field_aggregator |
| from pypaimon.read.reader.aggregate.aggregators import ( |
| NAME_LAST_NON_NULL_VALUE, |
| NAME_LAST_VALUE, |
| NAME_PRIMARY_KEY, |
| ) |
| from pypaimon.read.reader.aggregate.field_aggregator import FieldAggregator |
| from pypaimon.schema.data_types import DataField |
| from pypaimon.table.row.key_value import KeyValue |
| from pypaimon.table.row.row_kind import RowKind |
| |
| |
| # --------------------------------------------------------------------------- |
| # Aggregator-list construction helpers. Live in this module (rather than |
| # in split_read.py) so they can be exercised directly by unit tests. |
| # --------------------------------------------------------------------------- |
| |
| |
| def resolve_agg_func_name(field_name, primary_keys, options_map, |
| sequence_fields=()): |
| """Pick the aggregator identifier for ``field_name`` using the same |
| precedence as Java ``AggregateMergeFunction.getAggFuncName``: |
| |
| 1. Sequence fields use ``last_value`` (no aggregation -- the |
| sequence column just carries the latest-by-sequence value). |
| 2. Primary-key columns use ``primary_key`` (identity). |
| 3. Otherwise, field-level ``fields.<f>.aggregate-function`` |
| overrides everything. |
| 4. Otherwise, the table-wide ``fields.default-aggregate-function``. |
| 5. Otherwise, the system default ``last_non_null_value``. |
| |
| Sequence fields take precedence over the table-wide |
| ``fields.default-aggregate-function``, matching Java: the value of a |
| ``sequence.field`` column must not be aggregated. An *explicit* |
| ``fields.<seq>.aggregate-function`` on a sequence column is rejected |
| up-front (see ``merge_engine_support.check_sequence_field_valid``), so |
| it never reaches this precedence. |
| """ |
| if field_name in sequence_fields: |
| return NAME_LAST_VALUE |
| if field_name in primary_keys: |
| return NAME_PRIMARY_KEY |
| return ( |
| options_map.get("fields.{}.aggregate-function".format(field_name)) |
| or options_map.get("fields.default-aggregate-function") |
| or NAME_LAST_NON_NULL_VALUE |
| ) |
| |
| |
| def build_field_aggregators( |
| value_fields: List[DataField], |
| primary_keys: List[str], |
| core_options, |
| ) -> List[FieldAggregator]: |
| """Build the per-column aggregator list parallel to ``value_fields``. |
| |
| Resolves the identifier for each field via :func:`resolve_agg_func_name` |
| and instantiates the aggregator through the registry. Type validation |
| for aggregators that care (``sum`` requires numeric, ``bool_or`` / |
| ``bool_and`` require boolean) runs inside the registered factory, so |
| misconfigured tables fail here rather than at first row. |
| """ |
| options_map = core_options.options.to_map() |
| pk_set = set(primary_keys) |
| sequence_fields = set(core_options.sequence_field()) |
| aggregators = [] |
| for field in value_fields: |
| agg_name = resolve_agg_func_name( |
| field.name, pk_set, options_map, sequence_fields) |
| aggregators.append( |
| create_field_aggregator( |
| field.type, field.name, agg_name, core_options |
| ) |
| ) |
| return aggregators |
| |
| |
| # --------------------------------------------------------------------------- |
| # Merge function |
| # --------------------------------------------------------------------------- |
| |
| |
| class AggregateMergeFunction: |
| """A MergeFunction where the key is the primary key (unique) and |
| each non-PK column is reduced across the rows for that key by its |
| configured :class:`FieldAggregator`. |
| |
| Follows the same ``MergeFunction`` protocol used by |
| :class:`SortMergeReaderWithMinHeap`: :meth:`reset` between groups |
| of same-key rows, :meth:`add` one row at a time (oldest to |
| newest), :meth:`get_result` after the group is exhausted. |
| """ |
| |
| def __init__(self, |
| key_arity: int, |
| value_arity: int, |
| field_aggregators: List[FieldAggregator]): |
| if len(field_aggregators) != value_arity: |
| raise ValueError( |
| "field_aggregators length {} does not match value_arity " |
| "{}".format(len(field_aggregators), value_arity) |
| ) |
| self._key_arity = key_arity |
| self._value_arity = value_arity |
| self._field_aggregators = field_aggregators |
| # Parallel to value indices. Reset at the start of every key |
| # group; updated in-place as ``add()`` calls feed rows in. |
| self._accumulators: List[Any] = [None] * value_arity |
| # Reference to the most recently added kv. Used only to |
| # propagate the key + sequence_number into the result row; we |
| # snapshot those values into a fresh tuple in ``get_result()`` |
| # so the result is not aliased to upstream's reused KeyValue. |
| self._latest_kv: Optional[KeyValue] = None |
| |
| def reset(self) -> None: |
| self._accumulators = [None] * self._value_arity |
| for agg in self._field_aggregators: |
| agg.reset() |
| self._latest_kv = None |
| |
| def add(self, kv: KeyValue) -> None: |
| row_kind_byte = kv.value_row_kind_byte |
| if not RowKind.is_add_byte(row_kind_byte): |
| # DELETE / UPDATE_BEFORE rows are not supported by this |
| # merge engine. Refuse them rather than silently swallow |
| # rows, which would let aggregations diverge from the |
| # underlying data. |
| raise NotImplementedError( |
| "AggregateMergeFunction received a {} row; the " |
| "aggregation merge engine does not yet implement " |
| "retract (DELETE / UPDATE_BEFORE) handling. Tables " |
| "producing such rows are not yet supported." |
| .format(RowKind(row_kind_byte).to_string()) |
| ) |
| |
| for i, agg in enumerate(self._field_aggregators): |
| input_val = kv.value.get_field(i) |
| self._accumulators[i] = agg.agg(self._accumulators[i], input_val) |
| self._latest_kv = kv |
| |
| def get_result(self) -> Optional[KeyValue]: |
| if self._latest_kv is None: |
| return None |
| |
| kv = self._latest_kv |
| # Snapshot the key as a fresh tuple — we cannot keep a reference |
| # to ``kv`` because upstream readers (e.g. KeyValueWrapReader) |
| # reuse a single KeyValue instance and mutate its underlying |
| # row_tuple between calls. Building a fresh tuple here means |
| # the result we return is decoupled from any subsequent |
| # iteration. |
| key_values = tuple( |
| kv.key.get_field(i) for i in range(self._key_arity) |
| ) |
| result_row = key_values + ( |
| kv.sequence_number, |
| RowKind.INSERT.value, |
| ) + tuple(self._accumulators) |
| |
| result = KeyValue(self._key_arity, self._value_arity) |
| result.replace(result_row) |
| return result |