blob: c525a7592c071c866e7cc5412074c0f62a3de2d3 [file]
# 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.
import heapq
from typing import Any, Callable, List, Optional
from pypaimon.read.reader.deduplicate_merge_function import \
DeduplicateMergeFunction
from pypaimon.read.reader.iface.record_iterator import RecordIterator
from pypaimon.read.reader.iface.record_reader import RecordReader
from pypaimon.schema.data_types import AtomicType, DataField, Keyword
from pypaimon.schema.table_schema import TableSchema
from pypaimon.table.row.internal_row import InternalRow
from pypaimon.table.row.key_value import KeyValue
class SortMergeReaderWithMinHeap(RecordReader):
"""SortMergeReader implemented with min-heap."""
def __init__(self, readers: List[RecordReader[KeyValue]], schema: TableSchema,
merge_function: Optional[Any] = None,
seq_comparator: Optional[Callable[[Any, Any], int]] = None):
self.next_batch_readers = list(readers)
# Default to dedupe so callers that don't pass a merge_function
# keep their old behaviour. The merge engine dispatch lives in
# ``MergeFileSplitRead.section_reader_supplier`` for the read
# path; tests or other ad-hoc callers can pass a different
# implementation here.
self.merge_function = merge_function if merge_function is not None else DeduplicateMergeFunction()
# Optional user-defined sequence comparator (``sequence.field``).
# When set, it breaks key-ties on the value row before the
# file-level sequence number, mirroring Java's
# ``SortMergeReaderWithMinHeap`` + ``UserDefinedSeqComparator``.
# Built by the caller, which knows the value-side schema.
self.seq_comparator = seq_comparator
if schema.partition_keys:
trimmed_primary_keys = [pk for pk in schema.primary_keys if pk not in schema.partition_keys]
if not trimmed_primary_keys:
raise ValueError(f"Primary key constraint {schema.primary_keys} same with partition fields")
else:
trimmed_primary_keys = schema.primary_keys
field_map = {field.name: field for field in schema.fields}
key_schema = [field_map[name] for name in trimmed_primary_keys if name in field_map]
self.key_comparator = builtin_key_comparator(key_schema)
self.min_heap = []
self.polled = []
def read_batch(self) -> Optional[RecordIterator]:
for reader in self.next_batch_readers:
while True:
iterator = reader.read_batch()
if iterator is None:
reader.close()
break
kv = iterator.next()
if kv is not None:
element = Element(kv, iterator, reader)
entry = HeapEntry(kv.key, element, self.key_comparator,
self.seq_comparator)
heapq.heappush(self.min_heap, entry)
break
self.next_batch_readers.clear()
if not self.min_heap:
return None
return SortMergeIterator(
self,
self.polled,
self.min_heap,
self.merge_function,
self.key_comparator,
self.seq_comparator,
)
def close(self):
for reader in self.next_batch_readers:
reader.close()
for entry in self.min_heap:
entry.element.reader.close()
for element in self.polled:
element.reader.close()
class SortMergeIterator(RecordIterator):
def __init__(self, reader, polled: List['Element'], min_heap, merge_function,
key_comparator, seq_comparator=None):
self.reader = reader
self.polled = polled
self.min_heap = min_heap
self.merge_function = merge_function
self.key_comparator = key_comparator
self.seq_comparator = seq_comparator
self.released = False
def next(self):
while True:
if not self._next_impl():
return None
result = self.merge_function.get_result()
if result is not None:
return result
def _next_impl(self):
for element in self.polled:
if element.update():
entry = HeapEntry(element.kv.key, element, self.key_comparator,
self.seq_comparator)
heapq.heappush(self.min_heap, entry)
self.polled.clear()
if not self.min_heap:
return False
self.merge_function.reset()
key = self.min_heap[0].key
while self.min_heap and self.key_comparator(key, self.min_heap[0].key) == 0:
entry = heapq.heappop(self.min_heap)
self.merge_function.add(entry.element.kv)
self.polled.append(entry.element)
return True
class Element:
def __init__(self, kv: KeyValue, iterator: RecordIterator[KeyValue], reader: RecordReader[KeyValue]):
self.kv = kv
self.iterator = iterator
self.reader = reader
def update(self) -> bool:
next_kv = self.iterator.next()
if next_kv is not None:
self.kv = next_kv
return True
self.iterator = self.reader.read_batch()
if self.iterator is None:
self.reader.close()
return False
next_kv_from_new_batch = self.iterator.next()
self.kv = next_kv_from_new_batch
return True
class HeapEntry:
def __init__(self, key: InternalRow, element: Element, key_comparator,
seq_comparator=None):
self.key = key
self.element = element
self.key_comparator = key_comparator
self.seq_comparator = seq_comparator
def __lt__(self, other):
# Heap order mirrors Java ``SortMergeReaderWithMinHeap``: user key
# -> user-defined sequence comparator (``sequence.field``) on the
# value row -> file-level sequence number.
result = self.key_comparator(self.key, other.key)
if result == 0 and self.seq_comparator is not None:
result = self.seq_comparator(
self.element.kv.value, other.element.kv.value)
if result == 0:
result = self.element.kv.sequence_number - other.element.kv.sequence_number
return result < 0
def _base_type_name(field: DataField) -> str:
"""Base type keyword of a field, stripping any ``(precision[, scale])``
parameters and the ``NOT NULL`` suffix. E.g. ``DECIMAL(10, 2)`` and
``TIMESTAMP(6)`` map to ``DECIMAL`` / ``TIMESTAMP``.
"""
return field.type.type.split('(')[0].split(' ')[0]
# Atomic type keywords pypaimon can order with Python's native comparison
# operators. VARIANT is atomic but has no ordering, so it is excluded --
# matching Java, which has no VARIANT sequence-field support.
_COMPARABLE_TYPE_NAMES = frozenset(
member.value for member in Keyword if member is not Keyword.VARIANT)
def is_comparable_seq_field(field: DataField) -> bool:
"""Whether ``field`` can serve as a ``sequence.field`` for pypaimon's
atomic comparator: it must be an ``AtomicType`` whose base type name is
orderable. Complex types (ARRAY / MAP / ROW / ...) and the atomic-but-
unorderable VARIANT both return ``False``. Used by the read-builder
guard to reject unsupported sequence fields up front.
"""
return (isinstance(field.type, AtomicType)
and _base_type_name(field) in _COMPARABLE_TYPE_NAMES)
def _row_field_comparator(
fields: List[DataField],
indices: List[int],
ascending: bool = True) -> Callable[[Any, Any], int]:
"""Build a comparator over two rows on the given ``indices`` (positions
in ``fields`` / the row's ``get_field``), compared left-to-right.
Shared by :func:`builtin_key_comparator` (all key fields, ascending) and
:func:`builtin_seq_comparator` (the configured sequence fields, with
sort-order). Comparability is precomputed once. ``None`` rows/values
always sort first, independent of ``ascending`` -- only the comparison
of two non-null values is reversed when ``ascending=False``. This
mirrors Java ``GenerateUtils.generateRowCompare`` built with
``nullIsLast=false`` (see ``CodeGeneratorImpl#getSortSpec``), where
descending order flips only the non-null value comparison and leaves
nulls sorting first.
"""
comparable_flags = [_base_type_name(fields[idx]) in _COMPARABLE_TYPE_NAMES for idx in indices]
sign = 1 if ascending else -1
def comparator(row1: InternalRow, row2: InternalRow) -> int:
if row1 is None and row2 is None:
return 0
if row1 is None:
return -1
if row2 is None:
return 1
for pos, idx in enumerate(indices):
val1 = row1.get_field(idx)
val2 = row2.get_field(idx)
if val1 is None and val2 is None:
continue
if val1 is None:
return -1
if val2 is None:
return 1
if not comparable_flags[pos]:
raise ValueError(f"Unsupported {fields[idx].type} comparison")
if val1 < val2:
return -sign
elif val1 > val2:
return sign
return 0
return comparator
def builtin_key_comparator(key_schema: List[DataField]) -> Callable[[Any, Any], int]:
return _row_field_comparator(key_schema, list(range(len(key_schema))))
def builtin_seq_comparator(
value_fields: List[DataField],
sequence_field_names: List[str],
ascending: bool) -> Optional[Callable[[Any, Any], int]]:
"""Build a comparator for the user-defined ``sequence.field`` option.
Compares two *value* rows (the value side of a ``KeyValue``) on the
configured sequence fields, in declaration order, returning a negative
/ zero / positive int. Mirrors Java ``UserDefinedSeqComparator``:
- ``sequence_field_names`` empty -> ``None`` (no comparator; the caller
falls back to the file-level sequence number).
- field names resolve to indices within the value row
(``value_fields`` is the value-side schema, == ``read_type``);
``get_field(idx)`` indexes the value ``OffsetRow``.
- multiple fields compared left-to-right.
- ``ascending=False`` reverses only the non-null value comparison for
each field; null ordering stays nulls-first regardless of sort order
(mirroring Java's ``nullIsLast=false``). The value rows here carry a
homogeneous sort order, so reversing the final non-null comparison is
equivalent to Java reversing each field.
A name that does not resolve raises ``ValueError`` -- the read path
injects missing sequence fields into the projection before this runs,
so a miss indicates a wiring bug rather than user error.
A sequence field whose type pypaimon cannot order raises
``NotImplementedError``: complex types (ARRAY / VECTOR / MAP / MULTISET /
ROW), which Java handles via ``RecordComparator``, and the atomic-but-
unorderable VARIANT. pypaimon only implements atomic-type comparison
here, so reject these explicitly rather than failing later with an
obscure error.
"""
if not sequence_field_names:
return None
name_to_index = {field.name: i for i, field in enumerate(value_fields)}
indices = []
for name in sequence_field_names:
if name not in name_to_index:
raise ValueError(
f"sequence.field '{name}' not found in value fields "
f"{[f.name for f in value_fields]}")
idx = name_to_index[name]
if not is_comparable_seq_field(value_fields[idx]):
raise NotImplementedError(
f"sequence.field '{name}' has unsupported type "
f"{value_fields[idx].type}; pypaimon only supports orderable "
f"atomic sequence-field types. Complex types (ARRAY / MAP / "
f"ROW etc., handled by Java via RecordComparator) and VARIANT "
f"are not supported -- open an issue to track support.")
indices.append(idx)
return _row_field_comparator(value_fields, indices, ascending)