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-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new eefe3ece feat(python): deserialize cross-language splits (#889)
eefe3ece is described below

commit eefe3eceb215d185fd47acf353ef8c6866f11482
Author: Jingsong Lee <[email protected]>
AuthorDate: Sun Sep 20 20:06:01 2026 +0800

    feat(python): deserialize cross-language splits (#889)
---
 .../python/python/pypaimon_rust/datafusion.pyi     |  7 +++-
 bindings/python/src/read.rs                        | 13 ++++++
 bindings/python/tests/test_read.py                 | 46 +++++++++++++++++++++-
 3 files changed, 64 insertions(+), 2 deletions(-)

diff --git a/bindings/python/python/pypaimon_rust/datafusion.pyi 
b/bindings/python/python/pypaimon_rust/datafusion.pyi
index d179df9d..a91ad1da 100644
--- a/bindings/python/python/pypaimon_rust/datafusion.pyi
+++ b/bindings/python/python/pypaimon_rust/datafusion.pyi
@@ -39,9 +39,14 @@ class TableSchema:
 
 class Split:
     def __init__(self, state: bytes) -> None: ...
+    @staticmethod
+    def deserialize(state: bytes) -> "Split":
+        """Decode a Java-compatible SplitSerializer v1 frame."""
+        ...
     def row_count(self) -> int: ...
-    # Java SplitSerializer v1 binary: a DataSplit (v8), or an IndexedSplit 
when the split has row ranges.
     def is_streaming(self) -> bool: ...
+    # Java SplitSerializer v1 binary: DataSplit v8/v9, or score-free
+    # IndexedSplit when the native split has row ranges.
     def serialize(self) -> bytes: ...
 
 class Plan:
diff --git a/bindings/python/src/read.rs b/bindings/python/src/read.rs
index f73a8ded..22af9ee6 100644
--- a/bindings/python/src/read.rs
+++ b/bindings/python/src/read.rs
@@ -773,6 +773,19 @@ impl PySplit {
         Ok(PyBytes::new(py, &bytes))
     }
 
+    /// Reconstruct a native split from the stable, cross-language
+    /// `SplitSerializer` v1 wire format.
+    ///
+    /// This is deliberately separate from the constructor used by pickle:
+    /// pickle bytes are an opaque Rust JSON encoding, while this method 
accepts
+    /// Java/Python-compatible DataSplit and score-free IndexedSplit frames.
+    #[staticmethod]
+    fn deserialize(state: &Bound<'_, PyBytes>) -> PyResult<Self> {
+        Ok(Self {
+            inner: 
DataSplit::deserialize_split_v1(state.as_bytes()).map_err(to_py_err)?,
+        })
+    }
+
     /// Reduce to `Split(bytes)` for pickle/copy. The bytes are an opaque,
     /// implementation-detail encoding; only same/compatible-version round-trip
     /// is guaranteed.
diff --git a/bindings/python/tests/test_read.py 
b/bindings/python/tests/test_read.py
index fa836077..c072f32e 100644
--- a/bindings/python/tests/test_read.py
+++ b/bindings/python/tests/test_read.py
@@ -21,7 +21,7 @@ import tempfile
 import pyarrow as pa
 import pytest
 
-from pypaimon_rust.datafusion import PaimonCatalog, SQLContext
+from pypaimon_rust.datafusion import PaimonCatalog, Split, SQLContext
 
 
 def _make_table_with_data(warehouse):
@@ -174,6 +174,50 @@ def test_split_pickle_roundtrip():
         assert restored.row_count() == split.row_count()
 
 
+def test_split_wire_roundtrip_reads_same_rows():
+    with tempfile.TemporaryDirectory() as warehouse:
+        table = _make_table_with_data(warehouse)
+        builder = table.new_read_builder()
+        split = builder.new_scan().plan().splits()[0]
+
+        restored = Split.deserialize(split.serialize())
+
+        assert restored.row_count() == split.row_count()
+        original = pa.Table.from_batches(builder.new_read().read([split]))
+        roundtrip = pa.Table.from_batches(builder.new_read().read([restored]))
+        assert roundtrip.equals(original)
+
+
+def test_indexed_split_wire_roundtrip_preserves_row_ranges():
+    with tempfile.TemporaryDirectory() as warehouse:
+        ctx = SQLContext()
+        ctx.register_catalog("paimon", {"warehouse": warehouse})
+        ctx.sql("CREATE SCHEMA paimon.rdb")
+        ctx.sql("""CREATE TABLE paimon.rdb.de (id INT, name STRING) WITH (
+            'row-tracking.enabled' = 'true',
+            'data-evolution.enabled' = 'true')""")
+        ctx.sql("""INSERT INTO paimon.rdb.de (id, name)
+            VALUES (1, 'a'), (2, 'b'), (3, 'c')""")
+        table = PaimonCatalog({"warehouse": warehouse}).get_table("rdb.de")
+        builder = table.new_read_builder().with_row_ranges([(1, 1)])
+        split = builder.new_scan().plan().splits()[0]
+
+        restored = Split.deserialize(split.serialize())
+        rows = pa.Table.from_batches(builder.new_read().read([restored]))
+
+        assert restored.row_count() == 3
+        assert rows.column("id").to_pylist() == [2]
+
+
[email protected]("payload, message", [
+    (b"", "buffer underrun"),
+    (b"not-a-split", "magic"),
+])
+def test_split_wire_deserialize_rejects_malformed_input(payload, message):
+    with pytest.raises(ValueError, match=message):
+        Split.deserialize(payload)
+
+
 def _make_partitioned_table(warehouse):
     ctx = SQLContext()
     ctx.register_catalog("paimon", {"warehouse": warehouse})

Reply via email to