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 94a58cb6ef [python][ray] Preserve row kind in TableRead.to_ray() 
(#9693)
94a58cb6ef is described below

commit 94a58cb6efdbe72a38e87391a37368dbbf908832
Author: QuakeWang <[email protected]>
AuthorDate: Fri Sep 11 14:02:16 2026 +0800

    [python][ray] Preserve row kind in TableRead.to_ray() (#9693)
---
 .../pypaimon/read/datasource/ray_datasource.py     |  9 +++-
 .../pypaimon/read/datasource/split_provider.py     | 11 ++++-
 paimon-python/pypaimon/read/table_read.py          |  3 ++
 .../pypaimon/tests/ray_integration_test.py         | 54 ++++++++++++++++++++++
 .../pypaimon/tests/split_provider_test.py          |  6 +++
 5 files changed, 81 insertions(+), 2 deletions(-)

diff --git a/paimon-python/pypaimon/read/datasource/ray_datasource.py 
b/paimon-python/pypaimon/read/datasource/ray_datasource.py
index f8d5c7fafe..4b7e9459da 100644
--- a/paimon-python/pypaimon/read/datasource/ray_datasource.py
+++ b/paimon-python/pypaimon/read/datasource/ray_datasource.py
@@ -127,11 +127,15 @@ class RayDatasource(Datasource):
         nested_name_paths = self._split_provider.nested_name_paths()
         splits = self._split_provider.splits()
         limit = self._split_provider.limit()
+        include_row_kind = self._split_provider.include_row_kind()
         if not splits:
             return []
 
         if self._schema is None:
             self._schema = PyarrowFieldParser.from_paimon_schema(read_type)
+            if include_row_kind:
+                from pypaimon.read.table_read import TableRead
+                self._schema = TableRead._add_row_kind_to_schema(self._schema)
         schema = self._schema
 
         if parallelism > len(splits):
@@ -150,6 +154,7 @@ class RayDatasource(Datasource):
                 schema=schema,
                 limit=limit,
                 nested_name_paths=nested_name_paths,
+                include_row_kind=include_row_kind,
         ) -> Iterable[pyarrow.Table]:
             """Read function that will be executed by Ray workers."""
             from pypaimon.read.table_read import TableRead
@@ -159,7 +164,8 @@ class RayDatasource(Datasource):
             # columns and reads every projected leaf as NULL.
             worker_table_read = TableRead(
                 table, predicate, read_type, limit=limit,
-                nested_name_paths=nested_name_paths)
+                nested_name_paths=nested_name_paths,
+                include_row_kind=include_row_kind)
 
             batch_reader = worker_table_read.to_arrow_batch_reader(splits)
             has_data = False
@@ -187,6 +193,7 @@ class RayDatasource(Datasource):
             schema=schema,
             limit=limit,
             nested_name_paths=nested_name_paths,
+            include_row_kind=include_row_kind,
         )
 
         read_tasks = []
diff --git a/paimon-python/pypaimon/read/datasource/split_provider.py 
b/paimon-python/pypaimon/read/datasource/split_provider.py
index 6060ccce80..224a74fe54 100644
--- a/paimon-python/pypaimon/read/datasource/split_provider.py
+++ b/paimon-python/pypaimon/read/datasource/split_provider.py
@@ -67,6 +67,10 @@ class SplitProvider(ABC):
         """
         return None
 
+    def include_row_kind(self) -> bool:
+        """Whether Arrow output should include the row kind column."""
+        return False
+
     def nested_name_paths(self) -> Optional[List[List[str]]]:
         """Parallel name paths for a nested-leaf projection, or ``None``.
 
@@ -212,13 +216,15 @@ class PreResolvedSplitProvider(SplitProvider):
     """
 
     def __init__(self, table, splits: List[Split], read_type, predicate=None,
-                 limit: Optional[int] = None, nested_name_paths=None):
+                 limit: Optional[int] = None, nested_name_paths=None,
+                 include_row_kind: bool = False):
         self._table = table
         self._splits = splits
         self._read_type = read_type
         self._predicate = predicate
         self._limit = limit
         self._nested_name_paths = nested_name_paths
+        self._include_row_kind = include_row_kind
 
     def table(self):
         return self._table
@@ -232,6 +238,9 @@ class PreResolvedSplitProvider(SplitProvider):
     def nested_name_paths(self) -> Optional[List[List[str]]]:
         return self._nested_name_paths
 
+    def include_row_kind(self) -> bool:
+        return self._include_row_kind
+
     def predicate(self):
         return self._predicate
 
diff --git a/paimon-python/pypaimon/read/table_read.py 
b/paimon-python/pypaimon/read/table_read.py
index 3472666ae5..51465486ed 100644
--- a/paimon-python/pypaimon/read/table_read.py
+++ b/paimon-python/pypaimon/read/table_read.py
@@ -666,6 +666,8 @@ class TableRead:
 
         if not splits:
             schema = PyarrowFieldParser.from_paimon_schema(self.read_type)
+            if self.include_row_kind:
+                schema = self._add_row_kind_to_schema(schema)
             empty_table = pyarrow.Table.from_arrays(
                 [pyarrow.array([], type=field.type) for field in schema],
                 schema=schema
@@ -686,6 +688,7 @@ class TableRead:
                 predicate=self.predicate,
                 limit=self.limit,
                 nested_name_paths=self.nested_name_paths,
+                include_row_kind=self.include_row_kind,
             )
         )
         ds = ray.data.read_datasource(
diff --git a/paimon-python/pypaimon/tests/ray_integration_test.py 
b/paimon-python/pypaimon/tests/ray_integration_test.py
index df8c52ae81..05aff4bc63 100644
--- a/paimon-python/pypaimon/tests/ray_integration_test.py
+++ b/paimon-python/pypaimon/tests/ray_integration_test.py
@@ -103,6 +103,60 @@ class RayIntegrationTest(unittest.TestCase):
         self.assertEqual(list(df['id']), [1, 2, 3])
         self.assertEqual(list(df['name']), ['a', 'b', 'c'])
 
+    def test_to_ray_preserves_row_kind(self):
+        from pypaimon.read.table_read import TableRead
+
+        pa_schema = pa.schema([('id', pa.int32()), ('name', pa.string())])
+        identifier = self._create_and_populate_table(
+            'test_to_ray_row_kind', pa_schema,
+            {'id': [1, 2], 'name': ['a', 'b']},
+        )
+        table = 
CatalogFactory.create(self.catalog_options).get_table(identifier)
+        rb = table.new_read_builder()
+        splits = rb.new_scan().plan().splits()
+        self.assertTrue(splits)
+        predicate = rb.new_predicate_builder().equal('id', 999)
+        cases = [('data', splits, None), ('no_splits', [], None),
+                 ('filtered_empty', splits, predicate)]
+
+        for include_row_kind in (False, True):
+            for name, task_splits, task_predicate in cases:
+                with self.subTest(include_row_kind=include_row_kind, 
case=name):
+                    read = TableRead(
+                        table, task_predicate, rb.read_type(),
+                        include_row_kind=include_row_kind)
+                    arrow = read.to_arrow(task_splits)
+                    expected_schema = pa_schema
+                    if include_row_kind:
+                        expected_schema = pa.schema(
+                            [pa.field('_row_kind', pa.string())] + 
list(pa_schema))
+                    self.assertEqual(arrow.schema, expected_schema)
+                    if name == 'data' and include_row_kind:
+                        
self.assertEqual(arrow.column('_row_kind').to_pylist(), ['+I', '+I'])
+
+                    with patch.object(ray.data, 'read_datasource',
+                                      wraps=ray.data.read_datasource) as 
read_datasource:
+                        ds = read.to_ray(task_splits, override_num_blocks=1)
+                    self.assertEqual(ds.schema().base_schema, expected_schema)
+                    materialized = ds.materialize()
+                    self.assertEqual(materialized.schema().base_schema, 
expected_schema)
+                    self.assertEqual(materialized.take_all(), 
arrow.to_pylist())
+
+                    if task_splits:
+                        datasource = read_datasource.call_args[0][0]
+                        tasks = datasource.get_read_tasks(1)
+                        self.assertEqual(len(tasks), 1)
+                        task = tasks[0]
+                        task_schema = (task.schema if hasattr(task, 'schema')
+                                       else task.metadata.schema)
+                        self.assertEqual(task_schema, expected_schema)
+                        task = 
ray.cloudpickle.loads(ray.cloudpickle.dumps(task))
+                        blocks = list(task())
+                        self.assertTrue(blocks)
+                        for block in blocks:
+                            self.assertEqual(block.schema, expected_schema)
+                        self.assertEqual(pa.concat_tables(blocks), arrow)
+
     def test_read_paimon_with_projection(self):
         """read_paimon() respects column projection."""
         from pypaimon.ray import read_paimon
diff --git a/paimon-python/pypaimon/tests/split_provider_test.py 
b/paimon-python/pypaimon/tests/split_provider_test.py
index 61e14005f5..7602a9978e 100644
--- a/paimon-python/pypaimon/tests/split_provider_test.py
+++ b/paimon-python/pypaimon/tests/split_provider_test.py
@@ -86,6 +86,7 @@ class SplitProviderTest(unittest.TestCase):
         self.assertIs(provider.splits(), splits)  # cached
         self.assertIsNotNone(provider.read_type())
         self.assertIsNone(provider.predicate())
+        self.assertFalse(provider.include_row_kind())
 
     def test_catalog_provider_propagates_projection(self):
         """``projection`` reaches ``ReadBuilder.with_projection`` (visible via 
read_type)."""
@@ -282,6 +283,11 @@ class SplitProviderTest(unittest.TestCase):
         self.assertIs(provider.read_type(), read_type)
         self.assertIsNone(provider.predicate())
 
+        self.assertFalse(provider.include_row_kind())
+        provider = PreResolvedSplitProvider(
+            table, splits, read_type, include_row_kind=True)
+        self.assertTrue(provider.include_row_kind())
+
 
 if __name__ == '__main__':
     unittest.main()

Reply via email to