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 836b1d484e [python] Preserve schema fields in Python multimodal inputs 
(#10058)
836b1d484e is described below

commit 836b1d484e612a504cee69b5d361cade9b32d8d5
Author: chaoyang <[email protected]>
AuthorDate: Tue Sep 22 10:44:33 2026 +0800

    [python] Preserve schema fields in Python multimodal inputs (#10058)
---
 paimon-python/pypaimon/multimodal/table.py         | 12 +++++
 .../pypaimon/tests/multimodal_table_test.py        | 57 ++++++++++++++++++++++
 2 files changed, 69 insertions(+)

diff --git a/paimon-python/pypaimon/multimodal/table.py 
b/paimon-python/pypaimon/multimodal/table.py
index 4931da1dd1..81f302c499 100644
--- a/paimon-python/pypaimon/multimodal/table.py
+++ b/paimon-python/pypaimon/multimodal/table.py
@@ -582,6 +582,18 @@ def _blob_columns(table):
 def _to_arrow_table(data, target_schema=None):
     if target_schema is not None:
         data = _serialize_blob_values(data, target_schema)
+        if isinstance(data, list):
+            # Untyped from_pylist only discovers columns present in the first 
row.
+            data = {
+                field.name: [row.get(field.name) for row in data]
+                for field in target_schema
+            }
+        if isinstance(data, dict):
+            data = dict(data)
+            for field in target_schema:
+                if field.name in data and pa.types.is_map(field.type):
+                    # Inferred STRUCT/LIST arrays cannot be cast back to MAP.
+                    data[field.name] = pa.array(data[field.name], 
type=field.type)
     if isinstance(data, pa.Table):
         table = data
     elif isinstance(data, pa.RecordBatch):
diff --git a/paimon-python/pypaimon/tests/multimodal_table_test.py 
b/paimon-python/pypaimon/tests/multimodal_table_test.py
index 71160f8a8d..acca5be9d9 100644
--- a/paimon-python/pypaimon/tests/multimodal_table_test.py
+++ b/paimon-python/pypaimon/tests/multimodal_table_test.py
@@ -972,6 +972,63 @@ class MultimodalTableTest(unittest.TestCase):
         self.assertEqual(["id", "name"], result.column_names)
         self.assertEqual([1, 2], result["id"].to_pylist())
 
+    def test_add_preserves_fields_missing_from_first_row(self):
+        table = self.conn.create_table(
+            "sparse_rows", schema=_schema({
+                "id": pa.int32(), "caption": pa.string(), "quality": 
pa.float64(),
+                "image": pa.large_binary(), "missing": pa.string(),
+            }), options=_PARQUET_OPTIONS)
+        table.add([
+            {"id": "1"},
+            {"id": "2", "caption": "keep-me", "quality": 0.9,
+             "image": b"payload", "extra": "ignored"},
+        ])
+        scalar, blobs = table.scan().read_blobs("image")
+        rows = sorted(zip(scalar.to_pylist(), blobs["image"]), key=lambda row: 
row[0]["id"])
+        self.assertEqual([
+            ({"id": 1, "caption": None, "quality": None, "missing": None}, 
None),
+            ({"id": 2, "caption": "keep-me", "quality": 0.9, "missing": None}, 
b"payload"),
+        ], rows)
+
+    def test_add_python_map_values(self):
+        schema = _schema({
+            "id": pa.int32(), "assets": pa.map_(pa.string(), 
pa.large_binary()),
+            "scores": pa.map_(pa.string(), pa.int64()),
+        })
+        rows = [
+            {"id": 1, "assets": {"image": pmm.Blob.from_data(b"body"), 
"missing": None},
+             "scores": {"quality": 9}},
+            {"id": 2, "assets": [("empty", b""), ("image", b"second")], 
"scores": []},
+            {"id": 3, "assets": [], "scores": None},
+            {"id": 4, "assets": None, "scores": None},
+        ]
+        for columnar in (False, True):
+            with self.subTest(columnar=columnar):
+                table = self.conn.create_table(
+                    "map_input_%s" % columnar, schema=schema, 
options=_PARQUET_OPTIONS)
+                data = {name: [row[name] for row in rows] for name in 
schema.names} if columnar else rows
+                table.add(data)
+                scalar, blobs = table.scan().read_blobs("assets")
+                self.assertEqual({
+                    1: [("image", b"body"), ("missing", None)],
+                    2: [("empty", b""), ("image", b"second")], 3: [], 4: None,
+                }, dict(zip(scalar["id"].to_pylist(), blobs["assets"])))
+                self.assertEqual({1: [("quality", 9)], 2: [], 3: None, 4: 
None},
+                                 dict(zip(scalar["id"].to_pylist(), 
scalar["scores"].to_pylist())))
+
+    def test_python_input_preserves_safe_casts_and_empty_rows(self):
+        from pypaimon.multimodal.table import _to_arrow_table
+
+        schema = _schema({"id": pa.int32(), "caption": pa.string()})
+        for data in ([{"id": 1.5}], {"id": [1.5]}):
+            with self.assertRaises(pa.ArrowInvalid):
+                _to_arrow_table(data, schema)
+        self.assertEqual([], _to_arrow_table([], schema).to_pylist())
+        self.assertEqual([{"id": None, "caption": None}] * 2,
+                         _to_arrow_table([{}, {}], schema).to_pylist())
+        self.assertEqual([{"id": 2, "caption": None}],
+                         _to_arrow_table({"id": ["2"]}, schema).to_pylist())
+
     def test_add_scan_where_select_limit(self):
         users = self.conn.create_table(
             "users",

Reply via email to