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 94d1ce28ac [python] Keep reading an Avro file after a fully-filtered 
batch (#10128)
94d1ce28ac is described below

commit 94d1ce28ac4daec374222bbe88086e8a141e9197
Author: jackylee <[email protected]>
AuthorDate: Thu Sep 24 10:57:13 2026 +0800

    [python] Keep reading an Avro file after a fully-filtered batch (#10128)
---
 .../pypaimon/read/reader/format_avro_reader.py     | 41 ++++++++++++----------
 .../pypaimon/tests/reader_append_only_test.py      | 33 +++++++++++++++++
 2 files changed, 55 insertions(+), 19 deletions(-)

diff --git a/paimon-python/pypaimon/read/reader/format_avro_reader.py 
b/paimon-python/pypaimon/read/reader/format_avro_reader.py
index a0e189fae6..57309fab1d 100644
--- a/paimon-python/pypaimon/read/reader/format_avro_reader.py
+++ b/paimon-python/pypaimon/read/reader/format_avro_reader.py
@@ -56,33 +56,36 @@ class FormatAvroReader(RecordBatchReader):
             nested_name_paths and any(len(p) > 1 for p in nested_name_paths))
 
     def read_arrow_batch(self) -> Optional[RecordBatch]:
-        pydict_data = {name: [] for name in self._fields}
-        records_in_batch = 0
+        while True:
+            pydict_data = {name: [] for name in self._fields}
+            records_in_batch = 0
 
-        for record in self._avro_reader:
-            if self._has_nested:
-                for col_name, path in zip(self._fields, 
self._nested_name_paths):
-                    pydict_data[col_name].append(_walk_avro_record(record, 
path))
-            else:
-                for col_name in self._fields:
-                    pydict_data[col_name].append(record.get(col_name))
-            records_in_batch += 1
-            if records_in_batch >= self._batch_size:
-                break
+            for record in self._avro_reader:
+                if self._has_nested:
+                    for col_name, path in zip(self._fields, 
self._nested_name_paths):
+                        pydict_data[col_name].append(_walk_avro_record(record, 
path))
+                else:
+                    for col_name in self._fields:
+                        pydict_data[col_name].append(record.get(col_name))
+                records_in_batch += 1
+                if records_in_batch >= self._batch_size:
+                    break
+
+            # No more records in the file: this is the only real end-of-input.
+            if records_in_batch == 0:
+                return None
+            if self._push_down_predicate is None:
+                return pa.RecordBatch.from_pydict(pydict_data, self._schema)
 
-        if records_in_batch == 0:
-            return None
-        if self._push_down_predicate is None:
-            return pa.RecordBatch.from_pydict(pydict_data, self._schema)
-        else:
             pa_batch = pa.Table.from_pydict(pydict_data, self._schema)
             dataset = ds.InMemoryDataset(pa_batch)
             scanner = dataset.scanner(filter=self._push_down_predicate)
             combine_chunks = scanner.to_table().combine_chunks()
             if combine_chunks.num_rows > 0:
                 return combine_chunks.to_batches()[0]
-            else:
-                return None
+            # This batch matched no rows but the file has more; keep reading 
rather
+            # than returning None, which the caller treats as end-of-input and 
would
+            # silently drop every remaining row.
 
     def close(self):
         if self._file:
diff --git a/paimon-python/pypaimon/tests/reader_append_only_test.py 
b/paimon-python/pypaimon/tests/reader_append_only_test.py
index 1c783bfe06..9137622a47 100644
--- a/paimon-python/pypaimon/tests/reader_append_only_test.py
+++ b/paimon-python/pypaimon/tests/reader_append_only_test.py
@@ -104,6 +104,39 @@ class AoReaderTest(unittest.TestCase):
         actual = self._read_test_table(read_builder).sort_by('user_id')
         self.assertEqual(actual, self.expected)
 
+    def test_avro_ao_reader_filter_keeps_rows_past_first_batch(self):
+        # A filtered Avro read must not stop when a full batch_size (1024) 
block
+        # matches nothing: the reader returned None there, which the caller 
reads as
+        # end-of-input, silently dropping every later matching row.
+        schema = Schema.from_pyarrow_schema(
+            self.pa_schema, partition_keys=['dt'], options={'file.format': 
'avro'})
+        self.catalog.create_table('default.test_avro_filter_batches', schema, 
False)
+        table = self.catalog.get_table('default.test_avro_filter_batches')
+
+        n = 3000
+        data = pa.Table.from_pydict({
+            'user_id': list(range(n)),
+            'item_id': [1000 + i for i in range(n)],
+            'behavior': ['a'] * n,
+            'dt': ['p1'] * n,
+        }, schema=self.pa_schema)
+        wb = table.new_batch_write_builder()
+        w, c = wb.new_write(), wb.new_commit()
+        w.write_arrow(data)
+        c.commit(w.prepare_commit())
+        w.close()
+        c.close()
+
+        pb = table.new_read_builder().new_predicate_builder()
+        # >= 2500 over 3000 rows fully filters the first two 1024-row batches, 
so the
+        # test distinguishes the loop from an implementation that only retries 
once.
+        read_builder = table.new_read_builder().with_filter(
+            pb.greater_or_equal('user_id', 2500))
+        result = self._read_test_table(read_builder)
+
+        self.assertEqual(
+            sorted(result.column('user_id').to_pylist()), list(range(2500, n)))
+
     def test_lance_ao_reader(self):
         schema = Schema.from_pyarrow_schema(self.pa_schema, 
partition_keys=['dt'], options={'file.format': 'lance'})
         self.catalog.create_table('default.test_append_only_lance', schema, 
False)

Reply via email to