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 0a6de28448 [python] Vectorize temporal alignment row ID fetching 
(#10843)
0a6de28448 is described below

commit 0a6de2844838f85d6dc655ccf1b4a63c4d2d9fbf
Author: XiaoHongbo <[email protected]>
AuthorDate: Wed Oct 28 15:15:44 2026 +0800

    [python] Vectorize temporal alignment row ID fetching (#10843)
---
 paimon-python/pypaimon/multimodal/temporal.py      | 42 ++++++++++++++--------
 .../pypaimon/tests/multimodal_temporal_test.py     | 40 +++++++++++++++++++++
 2 files changed, 68 insertions(+), 14 deletions(-)

diff --git a/paimon-python/pypaimon/multimodal/temporal.py 
b/paimon-python/pypaimon/multimodal/temporal.py
index ca2d299dbe..1affc09c5c 100644
--- a/paimon-python/pypaimon/multimodal/temporal.py
+++ b/paimon-python/pypaimon/multimodal/temporal.py
@@ -23,6 +23,7 @@ import json
 import math
 from numbers import Integral, Real
 
+import numpy as np
 import pyarrow as pa
 import pyarrow.compute as pc
 
@@ -1238,14 +1239,21 @@ class _RowIdFetcher:
         return Range.sort_and_merge_overlap(ranges, True)
 
     def fetch(self, row_ids):
-        if not row_ids:
+        if len(row_ids) == 0:
             return pa.Table.from_arrays(
                 [pa.array([], type=field.type) for field in self._schema],
                 schema=self._schema,
             )
 
-        wanted = Range.sort_and_merge_overlap(
-            [Range(row_id, row_id) for row_id in set(row_ids)], True)
+        requested = np.asarray(row_ids, dtype=np.int64)
+        unique = np.unique(requested)
+        boundaries = np.flatnonzero(unique[1:] - unique[:-1] != 1) + 1
+        starts = np.concatenate(([0], boundaries))
+        ends = np.concatenate((boundaries - 1, [len(unique) - 1]))
+        wanted = [
+            Range(int(unique[start]), int(unique[end]))
+            for start, end in zip(starts, ends)
+        ]
         selected_splits = []
         for split_index in self._find_splits(wanted):
             original = self._splits[split_index]
@@ -1265,21 +1273,27 @@ class _RowIdFetcher:
             selected_splits.append(indexed)
 
         arrow = self._project_fetch(self._read.to_arrow(selected_splits))
-        found = arrow[self._row_id_name].to_pylist()
-        positions = {}
-        for index, row_id in enumerate(found):
-            if row_id in positions:
-                raise RuntimeError(
-                    "Duplicate row id %r in aligned scan." % row_id)
-            positions[row_id] = index
-        missing = [row_id for row_id in row_ids if row_id not in positions]
-        if missing:
+        found = arrow[self._row_id_name]
+        if found.null_count:
+            raise RuntimeError("Null row id in aligned scan.")
+        found = found.to_numpy()
+        order = None
+        if len(found) > 1 and not np.all(found[1:] > found[:-1]):
+            order = np.argsort(found)
+            found = found[order]
+            if np.any(found[1:] == found[:-1]):
+                raise RuntimeError("Duplicate row id in aligned scan.")
+        positions = np.searchsorted(found, requested)
+        present = positions < len(found)
+        if len(found):
+            present &= found[np.minimum(positions, len(found) - 1)] == 
requested
+        if not np.all(present):
             raise RuntimeError(
                 "Aligned row ids disappeared from pinned snapshot: %r."
-                % missing
+                % requested[~present].tolist()
             )
         take = pa.array(
-            [positions[row_id] for row_id in row_ids], type=pa.int64())
+            positions if order is None else order[positions], type=pa.int64())
         visible = pa.Table.from_arrays(
             [arrow.column(index) for index in range(len(self._schema))],
             schema=self._schema,
diff --git a/paimon-python/pypaimon/tests/multimodal_temporal_test.py 
b/paimon-python/pypaimon/tests/multimodal_temporal_test.py
index 8c5199eeff..b65f5f7309 100644
--- a/paimon-python/pypaimon/tests/multimodal_temporal_test.py
+++ b/paimon-python/pypaimon/tests/multimodal_temporal_test.py
@@ -36,6 +36,46 @@ from pypaimon.read.table_scan import TableScan
 
 class MultimodalTemporalTest(unittest.TestCase):
 
+    def test_row_id_fetch_preserves_request_order(self):
+        for found in ([1, 2, 3, 9], [9, 2, 1, 3], [2**63 - 1, 0]):
+            for requested in (list(reversed(found)), [found[0]] * 3):
+                with self.subTest(found=found, requested=requested):
+                    fetcher = self._row_id_fetcher(found)
+                    result = fetcher.fetch(np.asarray(requested, 
dtype=np.int64))
+                    self.assertEqual(result["value"].to_pylist(), requested)
+
+    def test_row_id_fetch_keeps_exact_ranges(self):
+        fetcher = self._row_id_fetcher([1, 2, 4, 5, 9])
+        fetcher.fetch([9, 1, 2, 4, 5, 1])
+        fetcher._find_splits.assert_called_once_with([
+            temporal.Range(1, 2), temporal.Range(4, 5), temporal.Range(9, 9)])
+
+    def test_row_id_fetch_rejects_missing_and_duplicate_ids(self):
+        for found, requested, error in (
+                ([1, 1], [1], "Duplicate row id"),
+                ([1, 2], [3], "disappeared"),
+                ([], [1], "disappeared"),
+                ([None], [1], "Null row id")):
+            with self.subTest(found=found, requested=requested):
+                with self.assertRaisesRegex(RuntimeError, error):
+                    self._row_id_fetcher(found).fetch(requested)
+        self.assertEqual(
+            self._row_id_fetcher([]).fetch(np.array([], 
dtype=np.int64)).num_rows, 0)
+
+    @staticmethod
+    def _row_id_fetcher(ids):
+        fetcher = temporal._RowIdFetcher.__new__(temporal._RowIdFetcher)
+        fetcher._schema = pa.schema([("value", pa.int64())])
+        fetcher._row_id_name = temporal._ROW_ID
+        fetcher._name_paths = None
+        fetcher._find_splits = mock.Mock(return_value=[])
+        fetcher._read = mock.Mock()
+        fetcher._read.to_arrow.return_value = pa.table({
+            "value": pa.array(ids, type=pa.int64()),
+            temporal._ROW_ID: pa.array(ids, type=pa.int64()),
+        })
+        return fetcher
+
     def setUp(self):
         self.temp_dir = tempfile.mkdtemp(prefix="pypaimon_temporal_")
         self.conn = pmm.connect(options={

Reply via email to