This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-mosaic.git


The following commit(s) were added to refs/heads/main by this push:
     new c082354  fix(python): handle partial stream writes (#94)
c082354 is described below

commit c082354aaa1e7cc4a61154b45aa2066e8b2148cb
Author: jianguotian <[email protected]>
AuthorDate: Wed Sep 30 07:16:25 2026 +0800

    fix(python): handle partial stream writes (#94)
---
 python/mosaic/mosaic.py     | 12 +++++++-
 python/tests/test_mosaic.py | 68 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 79 insertions(+), 1 deletion(-)

diff --git a/python/mosaic/mosaic.py b/python/mosaic/mosaic.py
index 4796890..16d39ae 100644
--- a/python/mosaic/mosaic.py
+++ b/python/mosaic/mosaic.py
@@ -16,6 +16,7 @@
 # under the License.
 
 import ctypes
+import operator
 
 import pyarrow as pa
 
@@ -221,7 +222,16 @@ class MosaicWriter:
     def _on_write(self, ctx, data, length):
         try:
             buf = (ctypes.c_char * length).from_address(ctypes.cast(data, 
ctypes.c_void_p).value)
-            self._stream.write(buf)
+            view = memoryview(buf)
+            offset = 0
+            while offset < length:
+                written = self._stream.write(view[offset:])
+                if written is None:
+                    return -1
+                written = operator.index(written)
+                if written <= 0 or written > length - offset:
+                    return -1
+                offset += written
             self._pos += length
             return 0
         except Exception:
diff --git a/python/tests/test_mosaic.py b/python/tests/test_mosaic.py
index f88720c..0057acc 100644
--- a/python/tests/test_mosaic.py
+++ b/python/tests/test_mosaic.py
@@ -15,9 +15,11 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import ctypes
 import io
 import struct
 import traceback
+from types import SimpleNamespace
 
 import pyarrow as pa
 import pytest
@@ -46,6 +48,13 @@ def _reader_from_bytes(data):
     )
 
 
+def _invoke_write_callback(stream, data, position=0):
+    state = SimpleNamespace(_stream=stream, _pos=position)
+    buf = (ctypes.c_uint8 * len(data)).from_buffer_copy(data)
+    result = MosaicWriter._on_write(state, None, buf, len(data))
+    return result, state._pos
+
+
 class TestRoundtrip:
     def test_basic_roundtrip(self):
         pa_schema = pa.schema(
@@ -752,6 +761,65 @@ class TestConvenience:
 
 
 class TestWriter:
+    def test_write_callback_handles_full_write(self):
+        class FullWriteOutput(io.BytesIO):
+            def __init__(self):
+                super().__init__()
+                self.request_sizes = []
+
+            def write(self, data):
+                self.request_sizes.append(len(data))
+                return super().write(data)
+
+        stream = FullWriteOutput()
+        result, position = _invoke_write_callback(stream, b"abcdef", 
position=7)
+
+        assert result == 0
+        assert position == 13
+        assert stream.getvalue() == b"abcdef"
+        assert stream.request_sizes == [6]
+
+    def test_write_callback_retries_short_writes(self):
+        class ShortWriteOutput(io.BytesIO):
+            def __init__(self):
+                super().__init__()
+                self.request_sizes = []
+
+            def write(self, data):
+                self.request_sizes.append(len(data))
+                return super().write(data[:2])
+
+        stream = ShortWriteOutput()
+        result, position = _invoke_write_callback(stream, b"abcdef", 
position=7)
+
+        assert result == 0
+        assert position == 13
+        assert stream.getvalue() == b"abcdef"
+        assert stream.request_sizes == [6, 4, 2]
+
+    @pytest.mark.parametrize("stall_result", [0, None], ids=["zero", "none"])
+    def test_write_callback_rejects_stall_without_advancing_position(
+        self, stall_result
+    ):
+        class StallingOutput(io.BytesIO):
+            def __init__(self):
+                super().__init__()
+                self.request_sizes = []
+
+            def write(self, data):
+                self.request_sizes.append(len(data))
+                if len(self.request_sizes) == 1:
+                    return super().write(data[:2])
+                return stall_result
+
+        stream = StallingOutput()
+        result, position = _invoke_write_callback(stream, b"abcdef", 
position=7)
+
+        assert result == -1
+        assert position == 7
+        assert stream.getvalue() == b"ab"
+        assert stream.request_sizes == [6, 4]
+
     def test_estimated_file_size(self):
         pa_schema = pa.schema(
             [pa.field("x", pa.int32()), pa.field("y", pa.utf8())]

Reply via email to