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 eaf5958150 [python][ray] Fix count metadata for row-level 
authorization (#9681)
eaf5958150 is described below

commit eaf5958150a46e6094c9a2f38a980f529d1aa0f4
Author: QuakeWang <[email protected]>
AuthorDate: Tue Sep 8 22:10:48 2026 +0800

    [python][ray] Fix count metadata for row-level authorization (#9681)
---
 .../pypaimon/read/datasource/ray_datasource.py     | 15 ++---
 .../pypaimon/tests/ray_integration_test.py         | 66 ++++++++++++++++++++++
 2 files changed, 74 insertions(+), 7 deletions(-)

diff --git a/paimon-python/pypaimon/read/datasource/ray_datasource.py 
b/paimon-python/pypaimon/read/datasource/ray_datasource.py
index 7a2f726143..f8d5c7fafe 100644
--- a/paimon-python/pypaimon/read/datasource/ray_datasource.py
+++ b/paimon-python/pypaimon/read/datasource/ray_datasource.py
@@ -197,15 +197,16 @@ class RayDatasource(Datasource):
                 continue
 
             # Calculate metadata for this chunk
-            total_rows = 0
+            total_rows: Optional[int] = 0
             total_size = 0
 
             for split in chunk_splits:
-                if predicate is None:
-                    # Only estimate rows if no predicate (predicate filtering 
changes row count)
-                    merged = split.merged_row_count()
-                    row_count = merged if merged is not None else 
split.row_count
-                    if row_count > 0:
+                if predicate is None and total_rows is not None:
+                    row_count = split.merged_row_count()
+                    if row_count is None:
+                        # Physical rows cannot replace unknown counts after 
filtering or merging.
+                        total_rows = None
+                    else:
                         total_rows += row_count
                 if hasattr(split, 'file_size') and split.file_size > 0:
                     total_size += split.file_size
@@ -222,7 +223,7 @@ class RayDatasource(Datasource):
             elif predicate is not None:
                 num_rows = None  # Can't estimate with predicate filtering
             else:
-                num_rows = total_rows if total_rows > 0 else None
+                num_rows = total_rows
             size_bytes = total_size if total_size > 0 else None
 
             metadata_kwargs = {
diff --git a/paimon-python/pypaimon/tests/ray_integration_test.py 
b/paimon-python/pypaimon/tests/ray_integration_test.py
index 875a9a3c9e..df8c52ae81 100644
--- a/paimon-python/pypaimon/tests/ray_integration_test.py
+++ b/paimon-python/pypaimon/tests/ray_integration_test.py
@@ -15,6 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import json
 import os
 import shutil
 import tempfile
@@ -25,6 +26,8 @@ import pyarrow as pa
 import ray
 
 from pypaimon import CatalogFactory, Schema
+from pypaimon.catalog.catalog_environment import CatalogEnvironment
+from pypaimon.catalog.table_query_auth import TableQueryAuthResult
 
 
 class RayIntegrationTest(unittest.TestCase):
@@ -119,6 +122,69 @@ class RayIntegrationTest(unittest.TestCase):
         self.assertEqual(set(df.columns), {'id', 'name'})
         self.assertEqual(len(df), 2)
 
+    def test_read_paimon_count_with_query_auth(self):
+        from pypaimon.ray import read_paimon
+
+        identifier = self._create_and_populate_table(
+            'test_count_query_auth',
+            pa.schema([('id', pa.int64()), ('dept', pa.string()), ('part', 
pa.string())]),
+            {'id': list(range(100)),
+             'dept': ['eng' if i % 5 == 0 else 'sales' for i in range(100)],
+             'part': ['a'] * 50 + ['b'] * 50},
+            partition_keys=['part'],
+        )
+        for dept, expected_ids in [('eng', list(range(0, 100, 5))), 
('missing', [])]:
+            with self.subTest(dept=dept):
+                auth = TableQueryAuthResult([json.dumps({
+                    'kind': 'LEAF',
+                    'transform': {'name': 'FIELD_REF', 'fieldRef': {'name': 
'dept'}},
+                    'function': 'EQUAL',
+                    'literals': [dept],
+                })], None)
+                # Replace only the catalog authorization response; filtering 
runs in Ray workers.
+                with patch.object(CatalogEnvironment, 'table_query_auth',
+                                  return_value=lambda select: auth):
+                    ds = read_paimon(identifier, self.catalog_options, 
override_num_blocks=2)
+                    # Count before materialization exercises the datasource 
metadata path.
+                    self.assertEqual(ds.count(), len(expected_ids))
+                    self.assertEqual(sorted(row['id'] for row in 
ds.take_all()), expected_ids)
+
+    def test_ray_datasource_logical_row_count_metadata(self):
+        from pypaimon.read.datasource.ray_datasource import RayDatasource
+        from pypaimon.read.datasource.split_provider import 
PreResolvedSplitProvider
+        from pypaimon.read.query_auth_split import QueryAuthSplit
+        from pypaimon.read.split import DataSplit
+
+        identifier = self._create_and_populate_table(
+            'test_count_metadata', pa.schema([('id', pa.int64()), ('part', 
pa.string())]),
+            {'id': [1, 2, 3, 4], 'part': ['a', 'a', 'b', 'b']}, 
partition_keys=['part'],
+        )
+        table = 
CatalogFactory.create(self.catalog_options).get_table(identifier)
+        rb = table.new_read_builder()
+        splits = rb.new_scan().plan().splits()
+        self.assertEqual(len(splits), 2)
+        auth = TableQueryAuthResult([json.dumps({
+            'kind': 'LEAF',
+            'transform': {'name': 'FIELD_REF', 'fieldRef': {'name': 'id'}},
+            'function': 'EQUAL', 'literals': [1],
+        })], None)
+        masked = TableQueryAuthResult(None, {'id': json.dumps({'name': 
'NULL'})})
+        empty = DataSplit([], splits[0].partition, splits[0].bucket, 
raw_convertible=True)
+        cases = [
+            ('known', splits, 4),
+            ('masked', [QueryAuthSplit(s, masked) for s in splits], 4),
+            ('first_unknown', [QueryAuthSplit(splits[0], auth), splits[1]], 
None),
+            ('second_unknown', [splits[0], QueryAuthSplit(splits[1], auth)], 
None),
+            ('zero', [empty], 0),
+        ]
+        for name, task_splits, expected in cases:
+            with self.subTest(name=name):
+                datasource = RayDatasource(PreResolvedSplitProvider(
+                    table, task_splits, rb.read_type()))
+                tasks = datasource.get_read_tasks(1)
+                self.assertEqual(len(tasks), 1)
+                self.assertEqual(tasks[0].metadata.num_rows, expected)
+
     def test_read_paimon_with_nested_projection(self):
         """read_paimon() respects a nested-leaf projection.
 

Reply via email to