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 8c182bf0db [python] Fix RecursionError when reading a shard or slice 
of a large file (#10113)
8c182bf0db is described below

commit 8c182bf0db768541ea987a7684addfa5378cb735
Author: jackylee <[email protected]>
AuthorDate: Thu Sep 24 13:09:28 2026 +0800

    [python] Fix RecursionError when reading a shard or slice of a large file 
(#10113)
---
 .../pypaimon/read/reader/shard_batch_reader.py     | 13 ++-
 .../pypaimon/tests/shard_batch_reader_test.py      | 94 ++++++++++++++++++++++
 2 files changed, 103 insertions(+), 4 deletions(-)

diff --git a/paimon-python/pypaimon/read/reader/shard_batch_reader.py 
b/paimon-python/pypaimon/read/reader/shard_batch_reader.py
index aa00814b82..445ff5f802 100644
--- a/paimon-python/pypaimon/read/reader/shard_batch_reader.py
+++ b/paimon-python/pypaimon/read/reader/shard_batch_reader.py
@@ -38,8 +38,14 @@ class ShardBatchReader(RecordBatchReader):
         if isinstance(self.reader.format_reader, FormatBlobReader):
             # For blob reader, pass begin_idx and end_idx parameters
             return self.reader.read_arrow_batch(start_idx=self.start_pos, 
end_idx=self.end_pos)
-        else:
-            # For non-blob reader (DataFileBatchReader), use standard 
read_arrow_batch
+
+        # For non-blob reader (DataFileBatchReader), use standard 
read_arrow_batch.
+        # Loop rather than recurse over skipped batches: a slice/shard whose 
range
+        # sits deep in a file (default parquet batch_size is 1024 rows) skips 
one
+        # batch per step, so recursing here overflows the stack 
(RecursionError)
+        # once the skipped count exceeds the interpreter limit. Mirrors the
+        # while-loop skip pattern in ConcatBatchReader / 
ApplyDeletionVectorReader.
+        while True:
             batch = self.reader.read_arrow_batch()
 
             if batch is None:
@@ -56,8 +62,7 @@ class ShardBatchReader(RecordBatchReader):
                 return batch.slice(self.start_pos - batch_begin, self.end_pos 
- self.start_pos)
             elif batch_begin < self.end_pos < self.current_pos:  # batch ends 
after the desired range
                 return batch.slice(0, self.end_pos - batch_begin)
-            else:  # batch is outside the desired range
-                return self.read_arrow_batch()
+            # else: batch is outside the desired range -> read the next one 
(loop)
 
     def close(self):
         self.reader.close()
diff --git a/paimon-python/pypaimon/tests/shard_batch_reader_test.py 
b/paimon-python/pypaimon/tests/shard_batch_reader_test.py
new file mode 100644
index 0000000000..7dc4badbc2
--- /dev/null
+++ b/paimon-python/pypaimon/tests/shard_batch_reader_test.py
@@ -0,0 +1,94 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import unittest
+
+import pyarrow as pa
+
+from pypaimon.read.reader.iface.record_batch_reader import RecordBatchReader
+from pypaimon.read.reader.shard_batch_reader import ShardBatchReader
+
+
+class _BatchReader(RecordBatchReader):
+    """A non-blob reader that replays an explicit list of arrow batches."""
+
+    format_reader = None  # not a FormatBlobReader -> ShardBatchReader takes 
the row-range path
+
+    def __init__(self, batches):
+        self._batches = iter(batches)
+
+    def read_arrow_batch(self):
+        return next(self._batches, None)
+
+    def close(self):
+        pass
+
+
+def _single_row_batches(count):
+    return [pa.record_batch([pa.array([i])], names=["id"]) for i in 
range(count)]
+
+
+def _read_all(reader):
+    got = []
+    while True:
+        batch = reader.read_arrow_batch()
+        if batch is None:
+            break
+        got.extend(batch.column("id").to_pylist())
+    return got
+
+
+class ShardBatchReaderTest(unittest.TestCase):
+
+    def test_slice_deep_into_file_does_not_recurse(self):
+        # A slice/shard whose range sits many batches into a file must not 
recurse
+        # once per skipped batch. With the default parquet batch_size of 1024 
rows a
+        # slice starting ~1M rows in skips >1000 batches; recursing there 
overflowed
+        # the stack with RecursionError. 2000 single-row batches reproduce 
that.
+        batch_count = 2000
+        reader = ShardBatchReader(
+            _BatchReader(_single_row_batches(batch_count)), batch_count - 1, 
batch_count)
+
+        batch = reader.read_arrow_batch()
+
+        self.assertIsNotNone(batch)
+        self.assertEqual(batch.column("id").to_pylist(), [batch_count - 1])
+        self.assertIsNone(reader.read_arrow_batch())
+
+    def test_slice_returns_only_rows_in_range(self):
+        # Semantics guard: with single-row batches, slice [2, 5) yields rows 
2, 3, 4
+        # and nothing else, so the loop refactor preserves the range filtering.
+        reader = ShardBatchReader(_BatchReader(_single_row_batches(8)), 2, 5)
+
+        self.assertEqual(_read_all(reader), [2, 3, 4])
+
+    def test_slice_straddling_batch_boundaries(self):
+        # Multi-row batches so the two slice() branches are exercised: the 
first
+        # batch straddles start_pos (2 in [0,4)) and the last straddles end_pos
+        # (9 in [8,12)); slice [2, 9) must yield exactly rows 2..8.
+        batches = [
+            pa.record_batch([pa.array([0, 1, 2, 3])], names=["id"]),
+            pa.record_batch([pa.array([4, 5, 6, 7])], names=["id"]),
+            pa.record_batch([pa.array([8, 9, 10, 11])], names=["id"]),
+        ]
+        reader = ShardBatchReader(_BatchReader(batches), 2, 9)
+
+        self.assertEqual(_read_all(reader), [2, 3, 4, 5, 6, 7, 8])
+
+
+if __name__ == "__main__":
+    unittest.main()

Reply via email to