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 0f2c69716a [python] Close BLOB stream iterators on errors (#10059)
0f2c69716a is described below

commit 0f2c69716ae9df4c74972073d831713594ad44fc
Author: chaoyang <[email protected]>
AuthorDate: Tue Sep 22 10:44:23 2026 +0800

    [python] Close BLOB stream iterators on errors (#10059)
---
 paimon-python/pypaimon/multimodal/query.py         |  9 ++---
 .../pypaimon/tests/multimodal_table_test.py        | 41 ++++++++++++++++++++++
 2 files changed, 43 insertions(+), 7 deletions(-)

diff --git a/paimon-python/pypaimon/multimodal/query.py 
b/paimon-python/pypaimon/multimodal/query.py
index 17ea818eef..1d65eb4ffb 100644
--- a/paimon-python/pypaimon/multimodal/query.py
+++ b/paimon-python/pypaimon/multimodal/query.py
@@ -318,20 +318,15 @@ class ScanQuery:
         return self._iter_blobs(read_builder, file_io, blob_cols, parallelism)
 
     def _iter_blobs(self, read_builder, file_io, blob_cols, parallelism):
-        reader = read_builder.new_read().to_arrow_batch_reader(
-            read_builder.new_scan().plan().splits())
         map_blob_cols, array_blob_cols = self._nested_blob_columns()
-        try:
+        with read_builder.new_read()._to_managed_arrow_batch_reader(
+                read_builder.new_scan().plan().splits()) as reader:
             for batch in reader:
                 bodies = self._fetch_bodies(
                     file_io, batch.select(blob_cols).to_pydict(), blob_cols,
                     parallelism, map_blob_cols, array_blob_cols)
                 scalar = batch.select(self._scalar_columns(batch.schema.names))
                 yield scalar, bodies
-        finally:
-            # Close the reader even if the caller breaks out early.
-            if hasattr(reader, "close"):
-                reader.close()
 
     def _blob_descriptor_query_read_builder(self):
         from pypaimon.common.options.core_options import CoreOptions
diff --git a/paimon-python/pypaimon/tests/multimodal_table_test.py 
b/paimon-python/pypaimon/tests/multimodal_table_test.py
index b5e04a7238..71160f8a8d 100644
--- a/paimon-python/pypaimon/tests/multimodal_table_test.py
+++ b/paimon-python/pypaimon/tests/multimodal_table_test.py
@@ -1662,6 +1662,47 @@ class MultimodalTableTest(unittest.TestCase):
         self.assertEqual(
             [], list(obs.scan().where("clip = 'none'").stream_blobs("image")))
 
+    def test_scan_stream_blobs_closes_underlying_iterator(self):
+        from pypaimon.read.table_read import TableRead
+
+        table = self.conn.create_table(
+            "stream_cleanup", schema=_schema({
+                "id": pa.int32(), "image": pa.large_binary(),
+            }), options=dict(_PARQUET_OPTIONS, **{"read.batch-size": "1"}))
+        table.add([{"id": i, "image": b"body"} for i in range(3)])
+        original = TableRead._arrow_batch_generator
+        closed = []
+
+        def tracked_read(read, *args, **kwargs):
+            reader = original(read, *args, **kwargs)
+            try:
+                yield from reader
+            finally:
+                reader.close()
+                closed.append(True)
+
+        with patch.object(TableRead, "_try_native_batches", 
return_value=None), \
+                patch.object(TableRead, "_arrow_batch_generator", 
tracked_read):
+            stream = table.scan().stream_blobs("image")
+            next(stream)
+            stream.close()
+            self.assertEqual([True], closed)
+
+            for method in ("_fetch_bodies", "_scalar_columns"):
+                with self.subTest(method=method):
+                    closed.clear()
+                    query = table.scan()
+                    with patch.object(query, method, 
side_effect=ValueError("batch failed")):
+                        stream = query.stream_blobs("image")
+                        try:
+                            next(stream)
+                        except ValueError as error:
+                            self.assertEqual("batch failed", str(error))
+                            # Assert while the traceback still retains the 
reader's frame.
+                            self.assertEqual([True], closed)
+                        else:
+                            self.fail("Expected batch failure")
+
     @unittest.skipIf(ray is None, "ray is not installed")
     def test_scan_to_ray_map_with_blobs(self):
         started_ray = False

Reply via email to