JingsongLi commented on code in PR #8075:
URL: https://github.com/apache/paimon/pull/8075#discussion_r3339659385


##########
paimon-python/pypaimon/read/reader/sort_merge_reader.py:
##########
@@ -168,51 +179,119 @@ def update(self) -> bool:
 
 
 class HeapEntry:
-    def __init__(self, key: InternalRow, element: Element, key_comparator):
+    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:
-            return True
-        elif result > 0:
-            return False
-
-        return self.element.kv.sequence_number < 
other.element.kv.sequence_number
-
-
-def builtin_key_comparator(key_schema: List[DataField]) -> Callable[[Any, 
Any], int]:
-    # Precompute comparability flags to avoid repeated type checks
+        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]
+
+
+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; ``ascending=False``
+    reverses the overall result. ``None`` rows/values sort first under
+    ascending order (and the reversal flips that consistently).
+    """
     comparable_types = {member.value for member in Keyword if member is not 
Keyword.VARIANT}
-    comparable_flags = [field.type.type.split(' ')[0] in comparable_types for 
field in key_schema]
+    comparable_flags = [_base_type_name(fields[idx]) in comparable_types for 
idx in indices]
+    sign = 1 if ascending else -1
 
-    def comparator(key1: InternalRow, key2: InternalRow) -> int:
-        if key1 is None and key2 is None:
+    def comparator(row1: InternalRow, row2: InternalRow) -> int:
+        if row1 is None and row2 is None:
             return 0
-        if key1 is None:
-            return -1
-        if key2 is None:
-            return 1
-        for i, comparable in enumerate(comparable_flags):
-            val1 = key1.get_field(i)
-            val2 = key2.get_field(i)
+        if row1 is None:
+            return -sign
+        if row2 is None:
+            return sign
+        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
+                return -sign

Review Comment:
   This reverses null ordering when `sequence.field.sort-order=descending`. 
Java builds the sequence comparator with `nullIsLast=false` 
(`CodeGeneratorImpl#getSortSpec`), and `GenerateUtils.generateRowCompare` 
applies descending only to the non-null comparison result, so null sequence 
values still sort first even for descending order. Here `-sign` / `sign` makes 
nulls sort last when `ascending=False`, which can make a null-sequence row 
become the last row fed into the merge function and therefore the winner. Could 
we keep null ordering independent from `ascending` and only flip non-null value 
comparisons? A regression test for descending sequence fields with nulls would 
catch this.



##########
paimon-python/pypaimon/common/options/core_options.py:
##########
@@ -808,6 +833,23 @@ def changelog_producer(self, default=None):
     def merge_engine(self, default=None):
         return self.options.get(CoreOptions.MERGE_ENGINE, default)
 
+    def sequence_field(self) -> List[str]:
+        """User-defined sequence fields, in declaration order. Empty list
+        when ``sequence.field`` is unset. Mirrors Java
+        ``CoreOptions.sequenceField()``.
+        """
+        raw = self.options.get(CoreOptions.SEQUENCE_FIELD)
+        if not raw:
+            return []
+        return [name.strip() for name in raw.split(",") if name.strip()]

Review Comment:
   Java `CoreOptions.sequenceField()` trims each segment but does not drop 
empty ones (`Arrays.stream(s.split(',')).map(String::trim).collect(...)`), so a 
malformed value like `sequence.field='ts,,ts2'` leaves an empty field name and 
is rejected by schema validation. This comprehension filters empty segments 
out, so the same invalid config is silently accepted as `['ts', 'ts2']`. To 
stay aligned with Java, please preserve empty trimmed segments here and let 
`check_sequence_field_valid` reject the missing empty field.



##########
paimon-python/pypaimon/read/reader/sort_merge_reader.py:
##########
@@ -168,51 +179,119 @@ def update(self) -> bool:
 
 
 class HeapEntry:
-    def __init__(self, key: InternalRow, element: Element, key_comparator):
+    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:
-            return True
-        elif result > 0:
-            return False
-
-        return self.element.kv.sequence_number < 
other.element.kv.sequence_number
-
-
-def builtin_key_comparator(key_schema: List[DataField]) -> Callable[[Any, 
Any], int]:
-    # Precompute comparability flags to avoid repeated type checks
+        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]

Review Comment:
   This assumes every comparable sequence field is an `AtomicType` with 
`field.type.type`. Java's `UserDefinedSeqComparator` delegates to 
`RecordComparator`, which supports ARRAY, VECTOR, MAP, MULTISET, and ROW as 
well as atomic types, and the docs say `sequence.field` supports fields of all 
data types. For a Java-valid table using a complex sequence field, pypaimon 
will currently fail with an attribute error or `Unsupported ... comparison`. If 
full recursive comparison is too much for this PR, I think we should explicitly 
validate and reject unsupported complex sequence-field types with a clear 
`NotImplementedError` instead of presenting this as aligned with Java.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to