This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new ec7f508740 [python] Build temporal group indexes with Arrow run-end 
encoding (#10466)
ec7f508740 is described below

commit ec7f5087404e46f7cd5fa7b257c544e4a4c93388
Author: zhigang <[email protected]>
AuthorDate: Fri Oct 9 21:54:09 2026 +0800

    [python] Build temporal group indexes with Arrow run-end encoding (#10466)
---
 .gitignore                                         |  1 +
 paimon-python/pypaimon/multimodal/temporal.py      | 24 +++++++++
 .../pypaimon/tests/multimodal_temporal_test.py     | 62 ++++++++++++++++++++++
 3 files changed, 87 insertions(+)

diff --git a/.gitignore b/.gitignore
index 9f1331ac3d..92b2b85097 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@ paimon-python/.idea/
 .claude
 .cursor
 .qoder
+.qwen
 
 ### Java / Maven ###
 target
diff --git a/paimon-python/pypaimon/multimodal/temporal.py 
b/paimon-python/pypaimon/multimodal/temporal.py
index c1052cd502..760fde10f3 100644
--- a/paimon-python/pypaimon/multimodal/temporal.py
+++ b/paimon-python/pypaimon/multimodal/temporal.py
@@ -390,6 +390,30 @@ class _AsOfJoinRight:
         self._row_ids = metadata[_ROW_ID].combine_chunks()
         self._index = {}
         group_columns = [metadata[name].combine_chunks() for name in self.by]
+        encode = getattr(pc, "run_end_encode", None)
+        if len(group_columns) == 1 and encode is not None:
+            column = group_columns[0]
+            data_type = column.type
+            # Only use types whose Arrow runs have Python key equality.
+            # In particular, run-end encoding distinguishes -0.0 from +0.0.
+            if (pa.types.is_integer(data_type)
+                    or pa.types.is_boolean(data_type)
+                    or pa.types.is_string(data_type)
+                    or pa.types.is_large_string(data_type)
+                    or pa.types.is_binary(data_type)
+                    or pa.types.is_large_binary(data_type)):
+                try:
+                    groups = encode(column, run_end_type=pa.int64())
+                except pa.ArrowNotImplementedError:
+                    pass
+                else:
+                    start = 0
+                    # Avoid Arrow scalar conversion per run for dense groups.
+                    ends = groups.run_ends.to_numpy().tolist()
+                    for key, end in zip(groups.values.to_pylist(), ends):
+                        self._index[(key,)] = (start, end)
+                        start = end
+                    return
         previous = None
         start = 0
         for position in range(len(metadata)):
diff --git a/paimon-python/pypaimon/tests/multimodal_temporal_test.py 
b/paimon-python/pypaimon/tests/multimodal_temporal_test.py
index f74b8fe245..69c650cf4b 100644
--- a/paimon-python/pypaimon/tests/multimodal_temporal_test.py
+++ b/paimon-python/pypaimon/tests/multimodal_temporal_test.py
@@ -122,6 +122,68 @@ class MultimodalTemporalTest(unittest.TestCase):
                 ).to_list()[0]
                 self.assertEqual(expected, row["value"])
 
+    def test_alignment_treats_signed_zero_as_one_group(self):
+        schema = {"group": pa.float64(), "event_time": pa.int64()}
+        anchors = self._table("zero_anchors", schema)
+        samples = self._table("zero_samples", {**schema, "value": pa.int32()})
+        anchors.add([{"group": 0.0, "event_time": time} for time in (1, 2)])
+        samples.add(
+            [
+                {"group": -0.0, "event_time": 1, "value": 7},
+                {"group": 0.0, "event_time": 2, "value": 8},
+            ]
+        )
+        rows = pmm.join_asof(
+            anchors.scan(),
+            samples.scan().select("value"),
+            on="event_time",
+            by="group",
+            tolerance=0,
+        ).to_list()
+        self.assertEqual({1: 7, 2: 8}, {row["event_time"]: row["value"] for 
row in rows})
+
+    def test_alignment_preserves_composite_group_boundaries(self):
+        schema = {"host": pa.string(), "sensor": pa.int32(), "event_time": 
pa.int64()}
+        anchors = self._table("composite_anchors", schema)
+        samples = self._table("composite_samples", {**schema, "value": 
pa.int32()})
+        keys = [("a", 1), ("a", 2), ("b", 1)]
+        anchors.add([{"host": host, "sensor": sensor, "event_time": 10} for 
host, sensor in keys])
+        samples.add(
+            [
+                {"host": host, "sensor": sensor, "event_time": 9, "value": 
index}
+                for index, (host, sensor) in enumerate(keys)
+            ]
+        )
+        rows = pmm.join_asof(
+            anchors.scan(),
+            samples.scan().select("value"),
+            on="event_time",
+            by=["host", "sensor"],
+        ).to_list()
+        self.assertEqual(dict(zip(keys, range(3))), {(row["host"], 
row["sensor"]): row["value"] for row in rows})
+
+    def test_alignment_without_run_end_kernel(self):
+        schema = {"group": pa.string(), "event_time": pa.int64()}
+        anchors = self._table("fallback_anchors", schema)
+        samples = self._table("fallback_samples", {**schema, "value": 
pa.int32()})
+        anchors.add([{"group": "a", "event_time": 1}])
+        samples.add(
+            [
+                {"group": "a", "event_time": 1, "value": 7},
+                {"group": "b", "event_time": 1, "value": 8},
+            ]
+        )
+        for kernel in (None, 
mock.Mock(side_effect=pa.ArrowNotImplementedError("No kernel"))):
+            with self.subTest(kernel=kernel), mock.patch.object(temporal.pc, 
"run_end_encode", kernel, create=True):
+                rows = pmm.join_asof(
+                    anchors.scan(),
+                    samples.scan().select("value"),
+                    on="event_time",
+                    by="group",
+                    tolerance=0,
+                ).to_list()
+                self.assertEqual([7], [row["value"] for row in rows])
+
     def test_asof_excludes_exact_matches_within_group(self):
         anchors = self._table("strict_anchors", {
             "episode_id": pa.int32(), "event_time": pa.int64(),

Reply via email to