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 97dbf29d8f [python] Add overwrite API to multimodal table (#8441)
97dbf29d8f is described below

commit 97dbf29d8f56b903e90b3e52e6d85f6fc2994752
Author: Jingsong Lee <[email protected]>
AuthorDate: Fri Jul 3 10:55:26 2026 +0800

    [python] Add overwrite API to multimodal table (#8441)
    
    Add a high-level `MultimodalTable.overwrite` API so multimodal users can
    replace table data without dropping down to the lower-level write
    builder. The API reuses Paimon batch overwrite semantics and documents
    how whole-table, dynamic partition, and static partition overwrite
    behave.
---
 docs/docs/pypaimon/multimodal-api.mdx              |  36 +++++++
 paimon-python/pypaimon/multimodal/table.py         |  17 ++++
 .../pypaimon/tests/multimodal_table_test.py        | 105 +++++++++++++++++++++
 3 files changed, 158 insertions(+)

diff --git a/docs/docs/pypaimon/multimodal-api.mdx 
b/docs/docs/pypaimon/multimodal-api.mdx
index b061aa36c0..83d13ae345 100644
--- a/docs/docs/pypaimon/multimodal-api.mdx
+++ b/docs/docs/pypaimon/multimodal-api.mdx
@@ -190,6 +190,42 @@ docs.add([
 ])
 ```
 
+## Overwrite
+
+`overwrite` accepts the same input formats as `add` and replaces existing data
+using Paimon's batch overwrite semantics. On an unpartitioned table it replaces
+the whole table. On a partitioned table it follows the table's
+`dynamic-partition-overwrite` option; with the default dynamic mode, only
+partitions present in the input data are replaced.
+
+```python
+docs.overwrite([
+    {
+        "id": 3,
+        "content": "Fresh replacement text",
+        "embedding": [0.7, 0.8, 0.9],
+        "category": "docs",
+    }
+])
+```
+
+For static partition overwrite, disable dynamic partition overwrite on the
+table and pass the target partition:
+
+```python
+docs.overwrite(
+    [
+        {
+            "id": 4,
+            "content": "Only this day is replaced.",
+            "category": "docs",
+            "dt": "2024-01-01",
+        }
+    ],
+    partition={"dt": "2024-01-01"},
+)
+```
+
 ## Update
 
 `update` modifies rows matched by a SQL-like predicate.
diff --git a/paimon-python/pypaimon/multimodal/table.py 
b/paimon-python/pypaimon/multimodal/table.py
index 916938137c..12de790060 100644
--- a/paimon-python/pypaimon/multimodal/table.py
+++ b/paimon-python/pypaimon/multimodal/table.py
@@ -109,6 +109,23 @@ class MultimodalTable:
             table_commit.close()
         return self
 
+    def overwrite(self, data, partition: Optional[Mapping[str, object]] = 
None):
+        arrow_table = _to_arrow_table(data, _target_schema(self.raw_table))
+        overwrite_partition = dict(partition) if partition is not None else 
None
+        write_builder = (
+            self.raw_table.new_batch_write_builder()
+            .overwrite(overwrite_partition)
+        )
+        table_write = write_builder.new_write()
+        table_commit = write_builder.new_commit()
+        try:
+            table_write.write_arrow(arrow_table)
+            table_commit.commit(table_write.prepare_commit())
+        finally:
+            table_write.close()
+            table_commit.close()
+        return self
+
     def update(self, where, values):
         query = self.scan().where(where)
         predicate = query._predicate
diff --git a/paimon-python/pypaimon/tests/multimodal_table_test.py 
b/paimon-python/pypaimon/tests/multimodal_table_test.py
index e745932c21..14c11e0040 100644
--- a/paimon-python/pypaimon/tests/multimodal_table_test.py
+++ b/paimon-python/pypaimon/tests/multimodal_table_test.py
@@ -513,6 +513,111 @@ class MultimodalTableTest(unittest.TestCase):
         self.assertEqual(1, result.num_rows)
         self.assertEqual([1], result["id"].to_pylist())
 
+    def test_overwrite_replaces_unpartitioned_table(self):
+        users = self.conn.create_table(
+            "users",
+            data=[
+                {"id": 1, "name": "Alice", "age": 30},
+                {"id": 2, "name": "Bob", "age": 25},
+            ],
+            schema=_schema({
+                "id": pa.int32(),
+                "name": pa.string(),
+                "age": pa.int32(),
+            }),
+            options=_PARQUET_OPTIONS,
+        )
+
+        result = users.overwrite([
+            {"id": 3, "name": "Carol", "age": 40},
+        ])
+
+        self.assertIs(users, result)
+        self.assertEqual(
+            [
+                {"id": 3, "name": "Carol", "age": 40},
+            ],
+            users.scan().to_list(),
+        )
+
+    def test_empty_overwrite_clears_unpartitioned_table(self):
+        users = self.conn.create_table(
+            "users",
+            data=[
+                {"id": 1, "name": "Alice"},
+                {"id": 2, "name": "Bob"},
+            ],
+            schema=_schema({
+                "id": pa.int32(),
+                "name": pa.string(),
+            }),
+            options=_PARQUET_OPTIONS,
+        )
+
+        users.overwrite([])
+
+        self.assertEqual([], users.scan().to_list())
+
+    def test_overwrite_replaces_dynamic_partitions(self):
+        users = self.conn.create_table(
+            "users",
+            data=[
+                {"id": 1, "name": "Alice", "dt": "2024-01-01"},
+                {"id": 2, "name": "Bob", "dt": "2024-01-02"},
+            ],
+            schema=_schema({
+                "id": pa.int32(),
+                "name": pa.string(),
+                "dt": pa.string(),
+            }),
+            options=_PARQUET_OPTIONS,
+            partitioned=["dt"],
+        )
+
+        users.overwrite([
+            {"id": 3, "name": "Carol", "dt": "2024-01-01"},
+        ])
+
+        rows = sorted(users.scan().to_list(), key=lambda r: r["id"])
+        self.assertEqual(
+            [
+                {"id": 2, "name": "Bob", "dt": "2024-01-02"},
+                {"id": 3, "name": "Carol", "dt": "2024-01-01"},
+            ],
+            rows,
+        )
+
+    def test_overwrite_replaces_static_partition(self):
+        users = self.conn.create_table(
+            "users",
+            data=[
+                {"id": 1, "name": "Alice", "dt": "2024-01-01"},
+                {"id": 2, "name": "Bob", "dt": "2024-01-02"},
+            ],
+            schema=_schema({
+                "id": pa.int32(),
+                "name": pa.string(),
+                "dt": pa.string(),
+            }),
+            options=dict(_PARQUET_OPTIONS, **{
+                "dynamic-partition-overwrite": "false",
+            }),
+            partitioned=["dt"],
+        )
+
+        users.overwrite([
+            {"id": 3, "name": "Carol", "dt": "2024-01-01"},
+        ], partition={"dt": "2024-01-01"})
+
+        rows = sorted(users.scan().to_list(), key=lambda r: r["id"])
+        self.assertEqual(
+            [
+                {"id": 2, "name": "Bob", "dt": "2024-01-02"},
+                {"id": 3, "name": "Carol", "dt": "2024-01-01"},
+            ],
+            rows,
+        )
+
     def test_scan_does_not_expose_pre_filter(self):
         users = self.conn.create_table(
             "users",

Reply via email to