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 77c568940e [python] Make multimodal table creation race-safe (#9590)
77c568940e is described below

commit 77c568940e5f1a938bdd005fd9d343812b4fba2c
Author: XiaoHongbo <[email protected]>
AuthorDate: Thu Sep 3 21:05:18 2026 +0800

    [python] Make multimodal table creation race-safe (#9590)
---
 paimon-python/pypaimon/multimodal/connection.py    |  28 ++++-
 .../pypaimon/tests/multimodal_table_test.py        | 124 +++++++++++++++++++++
 2 files changed, 148 insertions(+), 4 deletions(-)

diff --git a/paimon-python/pypaimon/multimodal/connection.py 
b/paimon-python/pypaimon/multimodal/connection.py
index 8f51cdaf97..f69a723920 100644
--- a/paimon-python/pypaimon/multimodal/connection.py
+++ b/paimon-python/pypaimon/multimodal/connection.py
@@ -71,18 +71,35 @@ class MultimodalConnection:
         """Create a multimodal table and optionally add initial data."""
         identifier = self._identifier(name)
         already_exists = _table_exists(self.catalog, identifier)
-        paimon_schema = _to_paimon_schema(schema, data, options, partitioned)
+        if already_exists and ignore_if_exists:
+            try:
+                return self.get_table(name)
+            except (DatabaseNotExistException, TableNotExistException):
+                pass
+        try:
+            paimon_schema = _to_paimon_schema(
+                schema, data, options, partitioned)
+            _validate_multimodal_schema(paimon_schema, identifier)
+        except ValueError:
+            if ignore_if_exists:
+                try:
+                    return self.get_table(name)
+                except (DatabaseNotExistException, TableNotExistException):
+                    pass
+            raise
 
         self._create_database_for(identifier)
+        created = False
         try:
             self.catalog.create_table(
-                identifier, paimon_schema, ignore_if_exists)
+                identifier, paimon_schema, False)
+            created = True
         except TableAlreadyExistException:
             if not ignore_if_exists:
                 raise
 
         table = self.get_table(name)
-        if data is not None and not already_exists:
+        if data is not None and created:
             table.add(data)
         return table
 
@@ -193,7 +210,10 @@ def _table_exists(catalog, identifier: str) -> bool:
 
 
 def _validate_multimodal_table(table, identifier: str):
-    table_schema = table.table_schema
+    _validate_multimodal_schema(table.table_schema, identifier)
+
+
+def _validate_multimodal_schema(table_schema, identifier: str):
     options = table_schema.options
     if str(options.get("data-evolution.enabled", "false")).lower() != "true":
         raise ValueError(
diff --git a/paimon-python/pypaimon/tests/multimodal_table_test.py 
b/paimon-python/pypaimon/tests/multimodal_table_test.py
index e99fe19437..50975a3892 100644
--- a/paimon-python/pypaimon/tests/multimodal_table_test.py
+++ b/paimon-python/pypaimon/tests/multimodal_table_test.py
@@ -760,6 +760,130 @@ class MultimodalTableTest(unittest.TestCase):
         with self.assertRaisesRegex(ValueError, "primary keys"):
             self.conn.get_table("pk")
 
+    def test_create_table_ignores_invalid_options_when_table_exists(self):
+        schema = _schema({"id": pa.int32()})
+        expected = self.conn.create_table("existing", schema=schema)
+
+        actual = self.conn.create_table(
+            "existing",
+            schema=_schema({
+                "id": pa.int32(),
+                "embedding": _vector(3),
+            }),
+            options={"data-evolution.enabled": "false"},
+            ignore_if_exists=True,
+        )
+
+        self.assertEqual(expected.identifier, actual.identifier)
+        with patch(
+                "pypaimon.multimodal.connection._table_exists",
+                return_value=False):
+            raced = self.conn.create_table(
+                "existing",
+                schema=_schema({
+                    "id": pa.int32(),
+                    "embedding": _vector(3),
+                }),
+                options={"data-evolution.enabled": "false"},
+                ignore_if_exists=True,
+            )
+        self.assertEqual(expected.identifier, raced.identifier)
+
+    def test_create_table_handles_concurrent_delete_when_ignoring(self):
+        schema = _schema({"id": pa.int32()})
+        self.conn.create_table("deleted", schema=schema)
+        original_get = self.conn.get_table
+        deleted = [False]
+
+        def delete_once(name):
+            if not deleted[0]:
+                deleted[0] = True
+                self.conn.catalog.drop_table("default.deleted", False)
+            return original_get(name)
+
+        with patch.object(
+                self.conn, "get_table", side_effect=delete_once):
+            table = self.conn.create_table(
+                "deleted",
+                data=pa.table({"id": [1, 2, 3]}),
+                schema=schema,
+                ignore_if_exists=True,
+            )
+        self.assertEqual("default.deleted", table.identifier)
+        self.assertEqual(
+            [1, 2, 3], table.scan().to_arrow()["id"].to_pylist())
+
+        self.conn.create_table("fallback_deleted", schema=schema)
+        deleted[0] = False
+
+        def delete_fallback_once(name):
+            if not deleted[0]:
+                deleted[0] = True
+                self.conn.catalog.drop_table(
+                    "default.fallback_deleted", False)
+            return original_get(name)
+
+        with patch(
+                "pypaimon.multimodal.connection._table_exists",
+                return_value=False):
+            with patch.object(
+                    self.conn,
+                    "get_table",
+                    side_effect=delete_fallback_once):
+                with self.assertRaisesRegex(
+                        ValueError, "data-evolution.enabled"):
+                    self.conn.create_table(
+                        "fallback_deleted",
+                        schema=schema,
+                        options={"data-evolution.enabled": "false"},
+                        ignore_if_exists=True,
+                    )
+
+    def test_create_table_does_not_add_data_to_concurrent_winner(self):
+        schema = _schema({"id": pa.int32()})
+        winner = self.conn.create_table("winner", schema=schema)
+
+        with patch(
+                "pypaimon.multimodal.connection._table_exists",
+                return_value=False):
+            actual = self.conn.create_table(
+                "winner",
+                data=pa.table({"id": [1, 2, 3]}),
+                schema=schema,
+                ignore_if_exists=True,
+            )
+
+        self.assertEqual(winner.identifier, actual.identifier)
+        self.assertEqual([], actual.scan().to_arrow().to_pylist())
+
+    def test_create_table_preserves_unknown_create_error(self):
+        schema = _schema({"id": pa.int32()})
+        create = self.conn.catalog.create_table
+        create_error = TimeoutError("create response lost")
+
+        def create_then_lose_response(*args, **kwargs):
+            create(*args, **kwargs)
+            raise create_error
+
+        with patch.object(
+                self.conn.catalog,
+                "create_table",
+                side_effect=create_then_lose_response):
+            with self.assertRaises(TimeoutError) as context:
+                self.conn.create_table(
+                    "failed_create",
+                    data=pa.table({"id": [1, 2, 3]}),
+                    schema=schema,
+                    ignore_if_exists=True,
+                )
+
+        self.assertIs(create_error, context.exception)
+        self.assertEqual(
+            [],
+            self.conn.get_table("failed_create")
+            .scan().to_arrow().to_pylist(),
+        )
+
     def test_create_table_can_add_initial_data_and_get_by_short_name(self):
         self.conn.create_table(
             "users",

Reply via email to