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 40b011ddba [Python] Fix execution boundary regressions and restore 
Native CI (#10782)
40b011ddba is described below

commit 40b011ddbab8254e1f82687aa3a09c2c24fb1545
Author: Jingsong Lee <[email protected]>
AuthorDate: Sat Oct 24 08:53:16 2026 +0800

    [Python] Fix execution boundary regressions and restore Native CI (#10782)
---
 .github/workflows/ci-python.yml                    |  1 +
 .../execution/native/write/native_commit.py        |  5 ++-
 .../vindex/vindex_vector_global_index_reader.py    |  6 +---
 paimon-python/pypaimon/read/read_dispatch.py       | 10 ++++--
 .../pypaimon/table/source/search_dispatch.py       | 42 +++++++++++++---------
 .../tests/multimodal_lance_descriptor_test.py      |  3 +-
 .../pypaimon/tests/native_blob_update_test.py      | 37 +++++++++----------
 .../tests/native_map_shredding_write_test.py       |  8 ++---
 .../pypaimon/tests/native_nested_update_test.py    | 23 ++++++------
 .../tests/native_parquet_vector_update_test.py     | 25 ++++++-------
 .../pypaimon/tests/native_ray_index_build_test.py  |  9 +++--
 .../pypaimon/tests/native_row_sidecar_test.py      |  2 +-
 .../tests/native_update_container_casts_test.py    | 25 ++++++-------
 .../native_update_inactive_vector_option_test.py   | 29 +++++++--------
 .../pypaimon/tests/native_upsert_layout_test.py    | 21 +++++------
 .../pypaimon/tests/native_variant_update_test.py   | 21 +++++------
 .../tests/native_vector_index_build_test.py        |  3 +-
 paimon-python/pypaimon/tests/native_write_test.py  |  2 +-
 .../tests/raw_file_scan_auth_scope_test.py         | 14 ++++++--
 .../pypaimon/tests/ray_bucket_join_test.py         |  2 +-
 .../pypaimon/tests/read_execution_boundary_test.py |  6 +++-
 .../pypaimon/tests/vindex_vector_index_test.py     | 23 +++++++++++-
 .../tests/write/execution_lifecycle_test.py        |  4 +++
 .../tests/write/row_id_conflict_protocol_test.py   |  5 +--
 24 files changed, 195 insertions(+), 131 deletions(-)

diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml
index 1dd9742be0..580a6a2a78 100644
--- a/.github/workflows/ci-python.yml
+++ b/.github/workflows/ci-python.yml
@@ -239,6 +239,7 @@ jobs:
       - name: Run Python tests with native planning, reading, writing, 
updating and committing
         shell: bash
         env:
+          DAFT_RUNNER: 'native'
           PYTEST_ADDOPTS: '--durations=20'
           PYPAIMON_TEST_NATIVE_PLAN: '1'
           PYPAIMON_TEST_NATIVE_READ: '1'
diff --git a/paimon-python/pypaimon/execution/native/write/native_commit.py 
b/paimon-python/pypaimon/execution/native/write/native_commit.py
index adc819e1aa..9cf739cf59 100644
--- a/paimon-python/pypaimon/execution/native/write/native_commit.py
+++ b/paimon-python/pypaimon/execution/native/write/native_commit.py
@@ -204,7 +204,10 @@ def from_native_commit_messages(table, messages):
 
 def native_commit_error(error):
     """Keep retry classification structured; never infer it from error text."""
-    from pypaimon_rust import RowIdExistenceConflict as NativeRowIdConflict
+    try:
+        from pypaimon_rust import RowIdExistenceConflict as NativeRowIdConflict
+    except ImportError:
+        return error
     from pypaimon.write.row_id_conflict import RowIdExistenceConflict
 
     if not isinstance(error, NativeRowIdConflict):
diff --git 
a/paimon-python/pypaimon/execution/python/globalindex/vindex/vindex_vector_global_index_reader.py
 
b/paimon-python/pypaimon/execution/python/globalindex/vindex/vindex_vector_global_index_reader.py
index ee97f68579..7a36decc16 100644
--- 
a/paimon-python/pypaimon/execution/python/globalindex/vindex/vindex_vector_global_index_reader.py
+++ 
b/paimon-python/pypaimon/execution/python/globalindex/vindex/vindex_vector_global_index_reader.py
@@ -59,12 +59,8 @@ class 
PythonVindexVectorGlobalIndexReader(VindexReaderResources, GlobalIndexRead
         params = _search_params(self._search_params_type, options, effective_k)
         filter_bytes = _filter_bytes(include_row_ids)
 
-        if n == 1:
-            ids, distances = self._reader.search(
-                queries[0], params, filter_bytes=filter_bytes)
-            return [_result_from_scores(ids, distances, self._metadata.metric)]
-
         if hasattr(self._reader, "search_batch"):
+            # The batch ABI pads short results; the scalar ABI requires 
exactly K.
             batch_result = self._reader.search_batch(
                 np.ascontiguousarray(queries),
                 params,
diff --git a/paimon-python/pypaimon/read/read_dispatch.py 
b/paimon-python/pypaimon/read/read_dispatch.py
index a78356f2b6..f91f46c57b 100644
--- a/paimon-python/pypaimon/read/read_dispatch.py
+++ b/paimon-python/pypaimon/read/read_dispatch.py
@@ -223,6 +223,9 @@ def plan_files(scanner):
         native_supported = _native_query_auth_supported(scan.table)
     else:
         native_supported = False
+    native_auth = native_supported and scan.table.options.query_auth_enabled
+    if not native_auth and scan._TableScan__auth_query(all_columns=True) is 
not None:
+        raise TableNoPermissionException(scan.table.identifier)
     if native_supported:
         from pypaimon.execution.native.read.plan import NativeTableScan
         result = NativeTableScan(scan).plan_files(scanner.skip_level0, 
scanner.only_read_real_buckets)
@@ -231,7 +234,7 @@ def plan_files(scanner):
             scanner._scanned_snapshot = None
             scanner._scanned_snapshot_id = snapshot_id
             return entries
-    if scan._TableScan__auth_query(all_columns=True) is not None:
+    if native_auth and scan._TableScan__auth_query(all_columns=True) is not 
None:
         raise TableNoPermissionException(scan.table.identifier)
     from pypaimon.execution.python.read.table_scan import PythonTableScan
     # Let the selected executor refresh cached rules for this unrestricted 
scope.
@@ -253,11 +256,14 @@ def plan_all(self) -> Plan:
         native_supported = _native_query_auth_supported(self.table) and 
self._native_plan_supported()
     else:
         native_supported = False
+    native_auth = native_supported and self.table.options.query_auth_enabled
+    if not native_auth and self._TableScan__auth_query(all_columns=True) is 
not None:
+        raise TableNoPermissionException(self.table.identifier)
     if native_supported:
         plan = self._try_native_plan(all_files=True)
         if plan is not None:
             return plan
-    if self._TableScan__auth_query(all_columns=True) is not None:
+    if native_auth and self._TableScan__auth_query(all_columns=True) is not 
None:
         raise TableNoPermissionException(self.table.identifier)
     from pypaimon.execution.python.read.table_scan import PythonTableScan
     return PythonTableScan(self).plan_all()
diff --git a/paimon-python/pypaimon/table/source/search_dispatch.py 
b/paimon-python/pypaimon/table/source/search_dispatch.py
index 963d96ab18..c1e2cad4fa 100644
--- a/paimon-python/pypaimon/table/source/search_dispatch.py
+++ b/paimon-python/pypaimon/table/source/search_dispatch.py
@@ -21,6 +21,28 @@ The selected backend consumes ordinary snapshot-and-split 
plans, including
 transported plans. An execution error never triggers another search.
 """
 
+from functools import partial
+
+
+def _python_vector_scan(execution):
+    from pypaimon.execution.python.read.search.factories import new_vector_scan
+    return new_vector_scan(execution)
+
+
+def _python_vector_read(builder, batch):
+    from pypaimon.execution.python.read.search.factories import 
new_batch_vector_read, new_vector_read
+    return new_batch_vector_read(builder) if batch else 
new_vector_read(builder)
+
+
+def _python_full_text_scan(execution):
+    from pypaimon.execution.python.read.search.factories import 
new_full_text_scan
+    return new_full_text_scan(execution)
+
+
+def _python_full_text_read(execution):
+    from pypaimon.execution.python.read.search.factories import 
new_full_text_read
+    return new_full_text_read(execution)
+
 
 class _SearchScan:
     def __init__(self, native, python_factory):
@@ -78,10 +100,7 @@ def _native_requested(builder):
 
 
 def create_vector_scan(builder, execution, batch=False):
-    def python_factory():
-        from pypaimon.execution.python.read.search.factories import 
new_vector_scan
-        return new_vector_scan(execution)
-
+    python_factory = partial(_python_vector_scan, execution)
     if not _native_requested(builder):
         return python_factory()
     from pypaimon.execution.native.read.native_search_plan import 
native_vector_scan
@@ -90,10 +109,7 @@ def create_vector_scan(builder, execution, batch=False):
 
 
 def create_vector_read(builder, batch=False):
-    def python_factory():
-        from pypaimon.execution.python.read.search.factories import 
new_batch_vector_read, new_vector_read
-        return new_batch_vector_read(builder) if batch else 
new_vector_read(builder)
-
+    python_factory = partial(_python_vector_read, builder, batch)
     if not _native_requested(builder):
         return python_factory()
     from pypaimon.execution.native.read.native_search_plan import 
native_vector_read
@@ -105,10 +121,7 @@ def create_vector_read(builder, batch=False):
 
 
 def create_full_text_scan(builder, execution):
-    def python_factory():
-        from pypaimon.execution.python.read.search.factories import 
new_full_text_scan
-        return new_full_text_scan(execution)
-
+    python_factory = partial(_python_full_text_scan, execution)
     if not _native_requested(builder):
         return python_factory()
     from pypaimon.execution.native.read.native_search_plan import 
native_full_text_scan
@@ -117,10 +130,7 @@ def create_full_text_scan(builder, execution):
 
 
 def create_full_text_read(builder, execution):
-    def python_factory():
-        from pypaimon.execution.python.read.search.factories import 
new_full_text_read
-        return new_full_text_read(execution)
-
+    python_factory = partial(_python_full_text_read, execution)
     if not _native_requested(builder):
         return python_factory()
     from pypaimon.execution.native.read.native_search_plan import 
native_full_text_read
diff --git a/paimon-python/pypaimon/tests/multimodal_lance_descriptor_test.py 
b/paimon-python/pypaimon/tests/multimodal_lance_descriptor_test.py
index 46c508406e..5ea0948419 100644
--- a/paimon-python/pypaimon/tests/multimodal_lance_descriptor_test.py
+++ b/paimon-python/pypaimon/tests/multimodal_lance_descriptor_test.py
@@ -119,9 +119,10 @@ def 
test_export_lance_rejects_video_frame_in_ordinary_descriptor_field(conn, tmp
     video = tmp_path / "video.bin"
     video.write_bytes(b"encoded-video")
     descriptor = VideoFrameDescriptor(str(video), 0, video.stat().st_size, 2, 
-1, 0)
+    # Seed the invalid legacy value through Python; Native rejects it before 
export.
     table = lance_tests._table(conn, "frame_reference", {"id": pa.int32(), 
"payload": pa.large_binary()}, [
         {"id": 1, "payload": descriptor},
-    ], options={"blob-descriptor-field": "payload"})
+    ], options={"blob-descriptor-field": "payload", "write.native.enabled": 
"false"})
     path = tmp_path / "frame_reference.lance"
     if mode == "overwrite":
         lance.write_dataset(pa.table({"id": [99]}), str(path))
diff --git a/paimon-python/pypaimon/tests/native_blob_update_test.py 
b/paimon-python/pypaimon/tests/native_blob_update_test.py
index 19e2968f2b..b434fe5015 100644
--- a/paimon-python/pypaimon/tests/native_blob_update_test.py
+++ b/paimon-python/pypaimon/tests/native_blob_update_test.py
@@ -18,7 +18,9 @@
 """Native updates share the Java Blob placeholder and row-id layout."""
 
 from pypaimon.execution.python.write.table_update_by_row_id import 
PythonTableUpdateByRowId
-from contextlib import ExitStack
+from pypaimon.execution.python.write.table_update import PythonTableUpdate
+from pypaimon.execution.python.write.table_upsert_by_key import 
PythonTableUpsert
+from contextlib import ExitStack, contextmanager
 from unittest.mock import patch
 from urllib.parse import urlparse
 
@@ -31,11 +33,8 @@ from pypaimon.schema.data_types import ArrayType, 
AtomicType, DataField, MapType
 from pypaimon.table.row.blob import Blob, BlobDescriptor, BlobViewStruct
 from pypaimon.table.row.generic_row import GenericRow
 from pypaimon.table.data_evolution_merge_into import WhenMatched, 
WhenNotMatched, source_col
-from pypaimon.write.table_delete import TableDeleteByRowId
 from pypaimon.execution.native.write.native_update import 
NativeTableUpdateByRowId
 from pypaimon.write.write_dispatch import NativeTableWrite
-from pypaimon.write.table_update import TableUpdate
-from pypaimon.write.table_upsert_by_key import TableUpsertByKey
 
 pytestmark = pytest.mark.native_plan
 
@@ -85,15 +84,17 @@ def _read(table, descriptors=False, projection=None, 
resolve_views=True):
         return read.to_arrow(splits).sort_by('id').to_pylist()
 
 
+@contextmanager
 def _native_only():
-    stack = ExitStack()
-    for cls, method in [(PythonTableUpdateByRowId, 
'_load_existing_files_info'),
-                        (TableUpdate, '_build_predicate_update_table'),
-                        (TableUpsertByKey, '_upsert_partition'),
-                        (TableUpsertByKey, '_upsert_row_partition'),
-                        (TableDeleteByRowId, 'delete')]:
-        stack.enter_context(patch.object(cls, method, 
side_effect=AssertionError('Python update fallback')))
-    return stack
+    with ExitStack() as stack:
+        for cls, method in [(PythonTableUpdateByRowId, 
'_load_existing_files_info'),
+                            (PythonTableUpdate, 
'_build_predicate_update_table'),
+                            (PythonTableUpsert, '_upsert_partition'),
+                            (PythonTableUpsert, '_upsert_row_partition'),
+                            (PythonTableUpdate, 'delete'),
+                            (PythonTableUpdate, 'delete_by_row_id')]:
+            stack.enter_context(patch.object(cls, method, 
side_effect=AssertionError('Python update fallback')))
+        yield
 
 
 def _commit(builder, messages, stream):
@@ -238,13 +239,13 @@ def test_native_inline_blob_updates_preserve_references(
                         else update.update_by_arrow_with_row_id(data))
         elif operation == 'incremental':
             updater = update.new_update_by_row_id(7) if stream else 
update.new_update_by_row_id()
-            assert isinstance(updater, NativeTableUpdateByRowId) == native
+            assert isinstance(updater._execution, NativeTableUpdateByRowId) == 
native
             updater.update_columns(data.slice(0, 1), ['payload'])
             updater.update_columns(data.slice(1), ['payload'])
             messages = updater.commit_messages
         elif operation in ('row', 'rows'):
             updater = update.new_update_by_row_id(7) if stream else 
update.new_update_by_row_id()
-            assert isinstance(updater, NativeTableUpdateByRowId) == native
+            assert isinstance(updater._execution, NativeTableUpdateByRowId) == 
native
             value = (Blob.from_view(BlobViewStruct.deserialize(replacement)) 
if option == 'blob-view-field'
                      else Blob.from_descriptor_bytes(replacement, 
file_io=table.file_io))
             rows = [GenericRow([1, 'a', 999, value], table.fields),
@@ -347,13 +348,13 @@ def 
test_native_raw_blob_updates_across_existing_update_apis(tmp_path, kind, str
                 messages = update.update_by_arrow_batches_with_row_id(batches)
         elif operation == 'incremental':
             updater = update.new_update_by_row_id(7) if stream else 
update.new_update_by_row_id()
-            assert isinstance(updater, NativeTableUpdateByRowId)
+            assert isinstance(updater._execution, NativeTableUpdateByRowId)
             updater.update_columns(data.slice(0, 1), ['payload'])
             updater.update_columns(data.slice(1), ['payload'])
             messages = updater.commit_messages
         elif operation in ('row', 'rows'):
             updater = update.new_update_by_row_id(7) if stream else 
update.new_update_by_row_id()
-            assert isinstance(updater, NativeTableUpdateByRowId)
+            assert isinstance(updater._execution, NativeTableUpdateByRowId)
             rows = [GenericRow([1, 'a', 999, replacement], table.fields),
                     GenericRow([3, 'b', 999, None], table.fields)]
             if operation == 'row':
@@ -499,8 +500,8 @@ def test_raw_blob_abort_preserves_prepared_files(tmp_path, 
published):
         paths = set(tmp_path.rglob('*.blob'))
         if published:
             _commit(builder, messages, False)
-        updater.writer._abort()
-        updater.writer._abort()
+        updater._execution.writer._abort()
+        updater._execution.writer._abort()
     assert set(tmp_path.rglob('*.blob')) == paths
     if not published:
         assert _read(table)[0]['payload'] == b'large' * 10
diff --git a/paimon-python/pypaimon/tests/native_map_shredding_write_test.py 
b/paimon-python/pypaimon/tests/native_map_shredding_write_test.py
index df647e6361..52fe2f5ea2 100644
--- a/paimon-python/pypaimon/tests/native_map_shredding_write_test.py
+++ b/paimon-python/pypaimon/tests/native_map_shredding_write_test.py
@@ -278,8 +278,8 @@ def test_map_row_id_update_keeps_physical_layout(tmp_path, 
native_update):
 def test_native_map_predicate_update_and_upsert(tmp_path, stream, operation):
     from contextlib import ExitStack
     from pypaimon.common.predicate_builder import PredicateBuilder
-    from pypaimon.write.table_update import TableUpdate
-    from pypaimon.write.table_upsert_by_key import TableUpsertByKey
+    from pypaimon.execution.python.write.table_update import PythonTableUpdate
+    from pypaimon.execution.python.write.table_upsert_by_key import 
PythonTableUpsert
 
     table, schema = _table(tmp_path, 'evolution')
     _write(table, schema, [
@@ -291,8 +291,8 @@ def test_native_map_predicate_update_and_upsert(tmp_path, 
stream, operation):
     replacement = [('new', 7), ('a', None)]
     with ExitStack() as stack:
         for cls, method in [(PythonTableUpdateByRowId, 
'_load_existing_files_info'),
-                            (TableUpdate, '_build_predicate_update_table'),
-                            (TableUpsertByKey, '_upsert_partition')]:
+                            (PythonTableUpdate, 
'_build_predicate_update_table'),
+                            (PythonTableUpsert, '_upsert_partition')]:
             stack.enter_context(patch.object(cls, method, 
side_effect=AssertionError('Python update fallback')))
         if operation == 'upsert':
             data = pa.Table.from_pylist([
diff --git a/paimon-python/pypaimon/tests/native_nested_update_test.py 
b/paimon-python/pypaimon/tests/native_nested_update_test.py
index ed5df8a02d..1eea346dc0 100644
--- a/paimon-python/pypaimon/tests/native_nested_update_test.py
+++ b/paimon-python/pypaimon/tests/native_nested_update_test.py
@@ -18,7 +18,7 @@
 """Nested Native update dispatch and parity with the Python implementation."""
 
 from pypaimon.execution.python.write.table_update_by_row_id import 
PythonTableUpdateByRowId
-from contextlib import ExitStack
+from contextlib import ExitStack, contextmanager
 from unittest.mock import patch
 
 import pyarrow as pa
@@ -27,8 +27,8 @@ import pytest
 from pypaimon import CatalogFactory, Schema
 from pypaimon.common.predicate_builder import PredicateBuilder
 from pypaimon.table.row.generic_row import GenericRow
-from pypaimon.write.table_update import TableUpdate
-from pypaimon.write.table_upsert_by_key import TableUpsertByKey
+from pypaimon.execution.python.write.table_update import PythonTableUpdate
+from pypaimon.execution.python.write.table_upsert_by_key import 
PythonTableUpsert
 
 
 pytestmark = pytest.mark.native_plan
@@ -66,15 +66,16 @@ def _table(tmp_path, data_type, native):
     return table, schema
 
 
+@contextmanager
 def _native_only(native):
-    stack = ExitStack()
-    if native:
-        for cls, method in [(PythonTableUpdateByRowId, 
'_load_existing_files_info'),
-                            (TableUpdate, '_build_predicate_update_table'),
-                            (TableUpsertByKey, '_upsert_partition'),
-                            (TableUpsertByKey, '_upsert_row_partition')]:
-            stack.enter_context(patch.object(cls, method, 
side_effect=AssertionError('Python fallback')))
-    return stack
+    with ExitStack() as stack:
+        if native:
+            for cls, method in [(PythonTableUpdateByRowId, 
'_load_existing_files_info'),
+                                (PythonTableUpdate, 
'_build_predicate_update_table'),
+                                (PythonTableUpsert, '_upsert_partition'),
+                                (PythonTableUpsert, '_upsert_row_partition')]:
+                stack.enter_context(patch.object(cls, method, 
side_effect=AssertionError('Python fallback')))
+        yield
 
 
 def _commit(builder, messages, stream):
diff --git a/paimon-python/pypaimon/tests/native_parquet_vector_update_test.py 
b/paimon-python/pypaimon/tests/native_parquet_vector_update_test.py
index 06f3c8dc7d..43f1bf4227 100644
--- a/paimon-python/pypaimon/tests/native_parquet_vector_update_test.py
+++ b/paimon-python/pypaimon/tests/native_parquet_vector_update_test.py
@@ -18,7 +18,7 @@
 """Parquet vector sidecars must not force row-level updates back to Python."""
 
 from pypaimon.execution.python.write.table_update_by_row_id import 
PythonTableUpdateByRowId
-from contextlib import ExitStack
+from contextlib import ExitStack, contextmanager
 from unittest.mock import patch
 
 import pyarrow as pa
@@ -29,8 +29,8 @@ from pypaimon import CatalogFactory, Schema
 from pypaimon.execution.native.read.table_read import (native_read as 
read_native_splits)
 from pypaimon.schema.data_types import PyarrowFieldParser
 from pypaimon.table.data_evolution_merge_into import WhenMatched, 
WhenNotMatched, source_col
-from pypaimon.write.table_update import TableUpdate
-from pypaimon.write.table_upsert_by_key import TableUpsertByKey
+from pypaimon.execution.python.write.table_update import PythonTableUpdate
+from pypaimon.execution.python.write.table_upsert_by_key import 
PythonTableUpsert
 
 
 pytestmark = pytest.mark.native_plan
@@ -73,16 +73,17 @@ def _rows(table):
         yield sorted(rows, key=lambda row: row['id'])
 
 
+@contextmanager
 def _native_only(native):
-    stack = ExitStack()
-    if native:
-        for owner, method in [(PythonTableUpdateByRowId, 
'_load_existing_files_info'),
-                              (TableUpsertByKey, '_upsert_partition'),
-                              (TableUpdate, '_build_predicate_update_table')]:
-            stack.enter_context(patch.object(owner, method, 
side_effect=AssertionError('Python update fallback')))
-        
stack.enter_context(patch('pypaimon.execution.python.write.merge_into._build_tables',
-                                  side_effect=AssertionError('Python merge 
fallback')))
-    return stack
+    with ExitStack() as stack:
+        if native:
+            for owner, method in [(PythonTableUpdateByRowId, 
'_load_existing_files_info'),
+                                  (PythonTableUpsert, '_upsert_partition'),
+                                  (PythonTableUpdate, 
'_build_predicate_update_table')]:
+                stack.enter_context(patch.object(owner, method, 
side_effect=AssertionError('Python update fallback')))
+            
stack.enter_context(patch('pypaimon.execution.python.write.merge_into._build_tables',
+                                      side_effect=AssertionError('Python merge 
fallback')))
+        yield
 
 
 def _commit(builder, messages, stream):
diff --git a/paimon-python/pypaimon/tests/native_ray_index_build_test.py 
b/paimon-python/pypaimon/tests/native_ray_index_build_test.py
index 5f6fd1b626..d7de610781 100644
--- a/paimon-python/pypaimon/tests/native_ray_index_build_test.py
+++ b/paimon-python/pypaimon/tests/native_ray_index_build_test.py
@@ -96,7 +96,7 @@ def _request(tmp_path):
 
 
 @pytest.fixture
-def native_protocol():
+def native_protocol(monkeypatch):
     module = 'pypaimon.execution.native.write.native_index_build'
     core = Mock()
     shards = [SimpleNamespace(ordinal=ordinal) for ordinal in range(3)]
@@ -106,8 +106,11 @@ def native_protocol():
     sdk = ModuleType('pypaimon_rust.datafusion')
     sdk.GlobalIndexBuildBuilder = SimpleNamespace(supports_index_type=lambda 
kind: True)
     scheduler = _Ray()
-    with patch.dict(sys.modules, {'pypaimon_rust.datafusion': sdk, 'ray': 
scheduler}), \
-            patch(module + '.native_runtime_available', return_value=True), \
+    # Retain lazy imports between cases instead of restoring all of 
sys.modules,
+    # which leaves stale package attributes for Python 3.10's mock resolver.
+    monkeypatch.setitem(sys.modules, 'pypaimon_rust.datafusion', sdk)
+    monkeypatch.setitem(sys.modules, 'ray', scheduler)
+    with patch(module + '.native_runtime_available', return_value=True), \
             patch(module + '._rest_catalog_supported', return_value=True), \
             patch(module + '._native_table', return_value=native_table), \
             patch(module + '.from_native_commit_messages', side_effect=lambda 
table, messages: messages), \
diff --git a/paimon-python/pypaimon/tests/native_row_sidecar_test.py 
b/paimon-python/pypaimon/tests/native_row_sidecar_test.py
index 7e91ccea0d..5f514b9cf8 100644
--- a/paimon-python/pypaimon/tests/native_row_sidecar_test.py
+++ b/paimon-python/pypaimon/tests/native_row_sidecar_test.py
@@ -203,7 +203,7 @@ def 
test_sidecars_keep_updates_native_and_apply_deletion_vectors(tmp_path, strea
     data = pa.table({'_ROW_ID': [5, 7], 'value': ['changed-5', 'changed-7']})
     if incremental:
         updater = update.new_update_by_row_id(1) if stream else 
update.new_update_by_row_id()
-        assert isinstance(updater, NativeTableUpdateByRowId)
+        assert isinstance(updater._execution, NativeTableUpdateByRowId)
         messages = updater.update_columns(data, ['value'])
     else:
         with patch.object(TableUpdateByRowId, 'update_columns',
diff --git a/paimon-python/pypaimon/tests/native_update_container_casts_test.py 
b/paimon-python/pypaimon/tests/native_update_container_casts_test.py
index 3b2be5e984..fc043a9cf2 100644
--- a/paimon-python/pypaimon/tests/native_update_container_casts_test.py
+++ b/paimon-python/pypaimon/tests/native_update_container_casts_test.py
@@ -18,7 +18,7 @@
 """Equivalent Arrow container inputs must work on the existing update APIs."""
 
 from pypaimon.execution.python.write.table_update_by_row_id import 
PythonTableUpdateByRowId
-from contextlib import ExitStack
+from contextlib import ExitStack, contextmanager
 from decimal import Decimal
 from unittest.mock import patch
 
@@ -28,8 +28,8 @@ import pytest
 from pypaimon import CatalogFactory, Schema
 from pypaimon.common.predicate_builder import PredicateBuilder
 from pypaimon.table.data_evolution_merge_into import WhenMatched, source_col
-from pypaimon.write.table_update import TableUpdate
-from pypaimon.write.table_upsert_by_key import TableUpsertByKey
+from pypaimon.execution.python.write.table_update import PythonTableUpdate
+from pypaimon.execution.python.write.table_upsert_by_key import 
PythonTableUpsert
 
 
 pytestmark = pytest.mark.native_plan
@@ -92,16 +92,17 @@ def _table(tmp_path, value_type, native):
     return table
 
 
+@contextmanager
 def _native_only(native):
-    stack = ExitStack()
-    if native:
-        for cls, method in [(PythonTableUpdateByRowId, 
'_load_existing_files_info'),
-                            (TableUpdate, '_build_predicate_update_table'),
-                            (TableUpsertByKey, '_upsert_partition')]:
-            stack.enter_context(patch.object(cls, method, 
side_effect=AssertionError('Python update fallback')))
-        
stack.enter_context(patch('pypaimon.execution.python.write.merge_into._build_tables',
-                                  side_effect=AssertionError('Python MERGE 
fallback')))
-    return stack
+    with ExitStack() as stack:
+        if native:
+            for cls, method in [(PythonTableUpdateByRowId, 
'_load_existing_files_info'),
+                                (PythonTableUpdate, 
'_build_predicate_update_table'),
+                                (PythonTableUpsert, '_upsert_partition')]:
+                stack.enter_context(patch.object(cls, method, 
side_effect=AssertionError('Python update fallback')))
+            
stack.enter_context(patch('pypaimon.execution.python.write.merge_into._build_tables',
+                                      side_effect=AssertionError('Python MERGE 
fallback')))
+        yield
 
 
 def _apply(table, source, native, stream, operation):
diff --git 
a/paimon-python/pypaimon/tests/native_update_inactive_vector_option_test.py 
b/paimon-python/pypaimon/tests/native_update_inactive_vector_option_test.py
index 083845654f..e12920c829 100644
--- a/paimon-python/pypaimon/tests/native_update_inactive_vector_option_test.py
+++ b/paimon-python/pypaimon/tests/native_update_inactive_vector_option_test.py
@@ -19,7 +19,7 @@
 
 from pypaimon.execution.python.write.table_update_by_row_id import 
PythonTableUpdateByRowId
 from pypaimon.execution.python.write.table_delete import 
PythonTableDeleteByRowId
-from contextlib import ExitStack
+from contextlib import ExitStack, contextmanager
 from unittest.mock import patch
 
 import pyarrow as pa
@@ -32,8 +32,8 @@ from pypaimon.table.data_evolution_merge_into import 
WhenMatched, WhenNotMatched
 from pypaimon.table.row.generic_row import GenericRow
 from pypaimon.tests import native_plan_rest_test
 from pypaimon.execution.native.write.native_update import 
_native_update_formats_supported
-from pypaimon.write.table_update import TableUpdate
-from pypaimon.write.table_upsert_by_key import TableUpsertByKey
+from pypaimon.execution.python.write.table_update import PythonTableUpdate
+from pypaimon.execution.python.write.table_upsert_by_key import 
PythonTableUpsert
 
 
 rest_catalog = native_plan_rest_test.rest_catalog
@@ -80,18 +80,19 @@ def _read(table):
     return 
builder.new_read().to_arrow(builder.new_scan().plan().splits()).sort_by('id').to_pylist()
 
 
+@contextmanager
 def _native_only(native):
-    stack = ExitStack()
-    if native:
-        for owner, method in [(PythonTableUpdateByRowId, 
'_load_existing_files_info'),
-                              (TableUpsertByKey, '_upsert_partition'),
-                              (TableUpsertByKey, '_upsert_row_partition'),
-                              (TableUpdate, '_build_predicate_update_table'),
-                              (PythonTableDeleteByRowId, 
'_scan_anchor_ranges')]:
-            stack.enter_context(patch.object(owner, method, 
side_effect=AssertionError('Python mutation fallback')))
-        
stack.enter_context(patch('pypaimon.execution.python.write.merge_into._build_tables',
-                                  side_effect=AssertionError('Python MERGE 
fallback')))
-    return stack
+    with ExitStack() as stack:
+        if native:
+            for owner, method in [(PythonTableUpdateByRowId, 
'_load_existing_files_info'),
+                                  (PythonTableUpsert, '_upsert_partition'),
+                                  (PythonTableUpsert, '_upsert_row_partition'),
+                                  (PythonTableUpdate, 
'_build_predicate_update_table'),
+                                  (PythonTableDeleteByRowId, 
'_scan_anchor_ranges')]:
+                stack.enter_context(patch.object(owner, method, 
side_effect=AssertionError('Python mutation fallback')))
+            
stack.enter_context(patch('pypaimon.execution.python.write.merge_into._build_tables',
+                                      side_effect=AssertionError('Python MERGE 
fallback')))
+        yield
 
 
 @pytest.mark.parametrize('native', _ENGINES, ids=['python', 'native'])
diff --git a/paimon-python/pypaimon/tests/native_upsert_layout_test.py 
b/paimon-python/pypaimon/tests/native_upsert_layout_test.py
index 223fda6079..189080d8a9 100644
--- a/paimon-python/pypaimon/tests/native_upsert_layout_test.py
+++ b/paimon-python/pypaimon/tests/native_upsert_layout_test.py
@@ -18,7 +18,7 @@
 """Equivalent Arrow layouts must not send upsert matching back to Python."""
 
 from pypaimon.execution.python.write.table_update_by_row_id import 
PythonTableUpdateByRowId
-from contextlib import ExitStack
+from contextlib import ExitStack, contextmanager
 from unittest.mock import patch
 
 import pyarrow as pa
@@ -26,8 +26,8 @@ import pytest
 
 from pypaimon import CatalogFactory, Schema
 from pypaimon.schema.data_types import PyarrowFieldParser
-from pypaimon.write.table_update import TableUpdate
-from pypaimon.write.table_upsert_by_key import TableUpsertByKey
+from pypaimon.execution.python.write.table_update import PythonTableUpdate
+from pypaimon.execution.python.write.table_upsert_by_key import 
PythonTableUpsert
 
 
 pytestmark = pytest.mark.native_plan
@@ -76,14 +76,15 @@ def _seed(table, schema, keys, values):
         writer.close()
 
 
+@contextmanager
 def _native_only(native):
-    stack = ExitStack()
-    if native:
-        for owner, method in [(TableUpsertByKey, '_upsert_partition'),
-                              (PythonTableUpdateByRowId, 
'_load_existing_files_info'),
-                              (TableUpdate, '_build_predicate_update_table')]:
-            stack.enter_context(patch.object(owner, method, 
side_effect=AssertionError('Python update fallback')))
-    return stack
+    with ExitStack() as stack:
+        if native:
+            for owner, method in [(PythonTableUpsert, '_upsert_partition'),
+                                  (PythonTableUpdateByRowId, 
'_load_existing_files_info'),
+                                  (PythonTableUpdate, 
'_build_predicate_update_table')]:
+                stack.enter_context(patch.object(owner, method, 
side_effect=AssertionError('Python update fallback')))
+        yield
 
 
 def _check(table, values, key, new_key='new'):
diff --git a/paimon-python/pypaimon/tests/native_variant_update_test.py 
b/paimon-python/pypaimon/tests/native_variant_update_test.py
index 0335cf9956..6f0f5497c5 100644
--- a/paimon-python/pypaimon/tests/native_variant_update_test.py
+++ b/paimon-python/pypaimon/tests/native_variant_update_test.py
@@ -19,7 +19,7 @@
 
 from pypaimon.execution.python.write.table_update_by_row_id import 
PythonTableUpdateByRowId
 import json
-from contextlib import ExitStack
+from contextlib import ExitStack, contextmanager
 from unittest.mock import patch
 
 import pyarrow as pa
@@ -32,8 +32,8 @@ from pypaimon.data.generic_variant import GenericVariant
 from pypaimon.schema.data_types import PyarrowFieldParser
 from pypaimon.execution.native.write.native_update import 
NativeTableUpdateByRowId
 from pypaimon.write.write_dispatch import NativeTableWrite
-from pypaimon.write.table_update import TableUpdate
-from pypaimon.write.table_upsert_by_key import TableUpsertByKey
+from pypaimon.execution.python.write.table_update import PythonTableUpdate
+from pypaimon.execution.python.write.table_upsert_by_key import 
PythonTableUpsert
 
 pytestmark = pytest.mark.native_plan
 
@@ -104,13 +104,14 @@ def _read(table, native, projection=None):
     return rows
 
 
+@contextmanager
 def _native_only():
-    stack = ExitStack()
-    for cls, method in [(PythonTableUpdateByRowId, 
'_load_existing_files_info'),
-                        (TableUpdate, '_build_predicate_update_table'),
-                        (TableUpsertByKey, '_upsert_partition')]:
-        stack.enter_context(patch.object(cls, method, 
side_effect=AssertionError('Python update fallback')))
-    return stack
+    with ExitStack() as stack:
+        for cls, method in [(PythonTableUpdateByRowId, 
'_load_existing_files_info'),
+                            (PythonTableUpdate, 
'_build_predicate_update_table'),
+                            (PythonTableUpsert, '_upsert_partition')]:
+            stack.enter_context(patch.object(cls, method, 
side_effect=AssertionError('Python update fallback')))
+        yield
 
 
 def test_python_configured_variant_schema_uses_java_semantics(tmp_path):
@@ -145,7 +146,7 @@ def test_native_variant_partial_updates(tmp_path, stream, 
key, enabled, operatio
                         else update.update_by_arrow_with_row_id(data))
         elif operation == 'incremental':
             updater = update.new_update_by_row_id(7) if stream else 
update.new_update_by_row_id()
-            assert isinstance(updater, NativeTableUpdateByRowId)
+            assert isinstance(updater._execution, NativeTableUpdateByRowId)
             updater.update_columns(data.slice(0, 1), ['payload'])
             updater.update_columns(data.slice(1), ['payload'])
             messages = updater.commit_messages
diff --git a/paimon-python/pypaimon/tests/native_vector_index_build_test.py 
b/paimon-python/pypaimon/tests/native_vector_index_build_test.py
index 04fd3e5d98..5392d27ad0 100644
--- a/paimon-python/pypaimon/tests/native_vector_index_build_test.py
+++ b/paimon-python/pypaimon/tests/native_vector_index_build_test.py
@@ -137,8 +137,7 @@ def 
test_native_search_filters_live_null_vectors_with_deletion_vectors(rest_cata
     result = 
(_native_table(table).new_vector_search_builder().with_vector_column('embedding')
               .with_query_vector([1., 
1.]).with_limit(64).with_options(options).execute())
     assert sorted(result.row_ids()) == [1, 4]
-    # Python table-level planning also uses the core low-level index reader,
-    # avoiding the third-party scalar FFI's fixed-result-length restriction.
+    # Python table-level reads use the batch ABI for short filtered results.
     _assert_ids(table, [1, 4], kind)
 
 
diff --git a/paimon-python/pypaimon/tests/native_write_test.py 
b/paimon-python/pypaimon/tests/native_write_test.py
index 48d3b7765c..ecc2f1420f 100644
--- a/paimon-python/pypaimon/tests/native_write_test.py
+++ b/paimon-python/pypaimon/tests/native_write_test.py
@@ -528,7 +528,7 @@ def 
test_uri_reader_factory_none_keeps_native_and_rejects_late_change(tmp_path):
     native.write_arrow_batch(_batch([2], ['a']))
     with pytest.raises(RuntimeError, match='before any write'):
         native.with_blob_uri_reader_factory(None)
-    with pytest.raises(ValueError, match='before any data is written'):
+    with pytest.raises(RuntimeError, match='before any write'):
         native.with_write_type(['id'])
     native.abort()
 
diff --git a/paimon-python/pypaimon/tests/raw_file_scan_auth_scope_test.py 
b/paimon-python/pypaimon/tests/raw_file_scan_auth_scope_test.py
index d1cbe980f8..83566f6e67 100644
--- a/paimon-python/pypaimon/tests/raw_file_scan_auth_scope_test.py
+++ b/paimon-python/pypaimon/tests/raw_file_scan_auth_scope_test.py
@@ -92,14 +92,22 @@ def test_raw_scope_clears_cached_query_rules(table, 
restriction, native_fallback
     assert native.call_count == int(native_fallback)
 
 
-def test_restricted_raw_scope_does_not_clear_cached_rules(table):
[email protected]('operation', ['raw-files', 'all-files'])
+def test_restricted_raw_scope_does_not_clear_cached_rules(table, operation):
     scan = table.new_read_builder().new_scan()
     scan._query_auth_fn = lambda select: 
TableQueryAuthResult([_partition_filter()], None)
     scan.plan()
     source = scan._python_file_scanner
     prior_partition_filter = source.auth_partition_predicate
     assert prior_partition_filter is not None
-    with patch.object(source, 'plan_files', 
side_effect=AssertionError('Unauthorized file access')):
+    plan = scan.file_scanner.plan_files if operation == 'raw-files' else 
scan.plan_all
+    with patch.object(source, 'plan_files', 
side_effect=AssertionError('Unauthorized file access')), \
+            patch.object(NativeTableScan, 'plan_files',
+                         side_effect=AssertionError('Unauthorized Native file 
access')) as native, \
+            patch.object(scan, '_try_native_plan',
+                         side_effect=AssertionError('Unauthorized Native 
planning')) as native_plan:
         with pytest.raises(TableNoPermissionException):
-            scan.file_scanner.plan_files()
+            plan()
+    native.assert_not_called()
+    native_plan.assert_not_called()
     assert source.auth_partition_predicate is prior_partition_filter
diff --git a/paimon-python/pypaimon/tests/ray_bucket_join_test.py 
b/paimon-python/pypaimon/tests/ray_bucket_join_test.py
index 292712361d..115be8aae5 100644
--- a/paimon-python/pypaimon/tests/ray_bucket_join_test.py
+++ b/paimon-python/pypaimon/tests/ray_bucket_join_test.py
@@ -309,7 +309,7 @@ class RayBucketJoinTest(unittest.TestCase):
         # Files left under an old bucket count (rescale not yet rewritten) 
would carry a
         # different total_buckets; the same bucket id must not be treated as 
co-located.
         import types
-        from pypaimon.execution.python.read.scanner.file_scanner import 
(FileScanner)
+        from pypaimon.read.file_scanner import FileScanner
         sch = pa.schema([("url", pa.string())])
         self._bucketed_table("default.rs_a", sch, "url",
                              pa.Table.from_pydict({"url": ["u0", "u1"]}, 
schema=sch))
diff --git a/paimon-python/pypaimon/tests/read_execution_boundary_test.py 
b/paimon-python/pypaimon/tests/read_execution_boundary_test.py
index 4f1236e7bc..878a2b05f8 100644
--- a/paimon-python/pypaimon/tests/read_execution_boundary_test.py
+++ b/paimon-python/pypaimon/tests/read_execution_boundary_test.py
@@ -17,6 +17,7 @@
 
 """Read entry points load only the selected execution backend."""
 
+import importlib
 import subprocess
 import sys
 import textwrap
@@ -259,7 +260,8 @@ def 
test_native_row_conversion_keeps_its_failure_when_closing_batches_also_fails
     read.output_projection = None
     read._output_arrow_schema = lambda splits: pa.schema([('id', pa.int32())])
     read._try_native_batches = Mock(return_value=source)
-    with patch('pypaimon.execution.native.read.row_iterator.native_rows', 
side_effect=primary), \
+    row_iterator = 
importlib.import_module('pypaimon.execution.native.read.row_iterator')
+    with patch.object(row_iterator, 'native_rows', side_effect=primary), \
             patch.dict(sys.modules, 
{'pypaimon.execution.python.read.table_read': None}):
         rows = read._try_native_rows([])
         with pytest.raises(ValueError) as caught:
@@ -272,6 +274,7 @@ def _python_read_source(primary=None, cleanup=None):
     from types import SimpleNamespace
     import pyarrow as pa
     from pypaimon.read.reader.iface.record_batch_reader import 
RecordBatchReader
+    from pypaimon.read.read_support import _try_to_pad_batch_by_schema
     from pypaimon.schema.data_types import AtomicType, DataField
     from pypaimon.table.row.offset_row import OffsetRow
 
@@ -310,6 +313,7 @@ def _python_read_source(primary=None, cleanup=None):
         _blob_as_descriptor=False,
         _blob_view_output_indices=(),
         _project_batch_to_output=lambda batch: batch,
+        _try_to_pad_batch_by_schema=_try_to_pad_batch_by_schema,
         output_projection=None,
         _adapter_read_type=[DataField(0, 'id', AtomicType('INT'))],
         include_row_kind=False,
diff --git a/paimon-python/pypaimon/tests/vindex_vector_index_test.py 
b/paimon-python/pypaimon/tests/vindex_vector_index_test.py
index c2d67bec7d..46a83ae283 100644
--- a/paimon-python/pypaimon/tests/vindex_vector_index_test.py
+++ b/paimon-python/pypaimon/tests/vindex_vector_index_test.py
@@ -19,17 +19,21 @@ import io
 import sys
 import types
 import unittest
-from unittest.mock import patch
+from unittest.mock import Mock, patch
+
+import numpy as np
 
 from pypaimon.globalindex.batch_vector_search import BatchVectorSearch
 from pypaimon.globalindex.global_index_meta import GlobalIndexIOMeta
 from pypaimon.globalindex.vector_search_result import 
DictBasedScoredIndexResult
+from pypaimon.globalindex.vector_search import VectorSearch
 from pypaimon.globalindex.vindex_reader import (
     VindexVectorGlobalIndexReader,
 )
 from 
pypaimon.execution.python.globalindex.vindex.vindex_vector_global_index_reader 
import (
     _build_scores,
     _search_params,
+    PythonVindexVectorGlobalIndexReader,
 )
 from pypaimon.utils.roaring_bitmap import RoaringBitmap64
 
@@ -113,6 +117,23 @@ class _BytesFileIO:
 
 class VindexVectorIndexTest(unittest.TestCase):
 
+    def test_single_query_preserves_short_filtered_results(self):
+        reader = PythonVindexVectorGlobalIndexReader(
+            _BytesFileIO(), '/tmp', [GlobalIndexIOMeta('index', 1)])
+        reader._metadata = _FakeMetadata()
+        reader._search_params_type = _FakeSearchParams
+        reader._reader = Mock()
+        reader._reader.search.side_effect = RuntimeError('scalar ABI requires 
exactly K results')
+        reader._reader.search_batch.return_value = (
+            np.array([[10, 11, -1]]), np.array([[0., 1., np.nan]]))
+        with patch.object(reader, '_ensure_loaded'):
+            result = reader.visit_vector_search(VectorSearch([1., 0.], 3, 
'embedding')).result()
+        self.assertEqual([10, 11], result.results().to_list())
+        self.assertEqual(1., result.score_getter()(10))
+        self.assertEqual(.5, result.score_getter()(11))
+        reader._reader.search.assert_not_called()
+        reader.close()
+
     def test_query_widths_match_java_integer_text_and_bounds(self):
         for key in ('ivf.nprobe', 'diskann.l_search'):
             for value, expected in (('+12', 12), ('12', 12), ('٢', 2), 
('2147483647', 2147483647), (12, 12)):
diff --git a/paimon-python/pypaimon/tests/write/execution_lifecycle_test.py 
b/paimon-python/pypaimon/tests/write/execution_lifecycle_test.py
index 1c3938cfb5..3272eab174 100644
--- a/paimon-python/pypaimon/tests/write/execution_lifecycle_test.py
+++ b/paimon-python/pypaimon/tests/write/execution_lifecycle_test.py
@@ -267,7 +267,10 @@ def 
test_native_merge_does_not_construct_python_writer_or_file_index(tmp_path):
     table = _data_evolution_table(tmp_path)
     native_table, builder = _core_builder()
     expected = [_message()]
+    encoded_clause = object()
     with 
patch('pypaimon.execution.native.write.native_merge_into._native_row_id_table', 
return_value=native_table), \
+            
patch('pypaimon.execution.native.write.native_merge_into.encode_merge_clause',
+                  return_value=encoded_clause), \
             
patch('pypaimon.execution.native.write.native_merge_into.from_native_commit_messages',
                   return_value=expected), \
             
patch('pypaimon.execution.python.write.table_write.PythonTableWrite.__init__',
@@ -279,6 +282,7 @@ def 
test_native_merge_does_not_construct_python_writer_or_file_index(tmp_path):
             when_matched=[WhenMatched.update({'value': source_col('value')})])
         assert result is expected
     builder.new_update.return_value.merge_into.assert_called_once()
+    assert 
builder.new_update.return_value.merge_into.call_args[1]['when_matched'] == 
[encoded_clause]
 
 
 def 
test_failed_python_construction_closes_snapshot_and_preserves_original_error():
diff --git 
a/paimon-python/pypaimon/tests/write/row_id_conflict_protocol_test.py 
b/paimon-python/pypaimon/tests/write/row_id_conflict_protocol_test.py
index 091cc57d20..4c22822587 100644
--- a/paimon-python/pypaimon/tests/write/row_id_conflict_protocol_test.py
+++ b/paimon-python/pypaimon/tests/write/row_id_conflict_protocol_test.py
@@ -91,7 +91,8 @@ def 
test_native_commit_translates_only_typed_row_id_conflicts(batch):
 
 
 @pytest.mark.parametrize('opaque_message', ['unknown commit result', 'Row ID 
existence conflict: not a typed error'])
-def test_native_unknown_outcome_is_not_a_rewrite_signal(opaque_message):
[email protected]('native_available', [False, True])
+def test_native_unknown_outcome_is_not_a_rewrite_signal(opaque_message, 
native_available):
     from pypaimon.execution.native.write.commit import NativeTableCommit
     from pypaimon.ray.row_id_conflict_rewriter import _find_row_id_conflict
 
@@ -100,7 +101,7 @@ def 
test_native_unknown_outcome_is_not_a_rewrite_signal(opaque_message):
     failure.__cause__ = _native_conflict(module)
     core = Mock()
     core.commit.side_effect = failure
-    with patch.dict(sys.modules, {'pypaimon_rust': module}):
+    with patch.dict(sys.modules, {'pypaimon_rust': module if native_available 
else None}):
         with pytest.raises(ValueError) as raised:
             NativeTableCommit(core, is_batch=True).commit([], 7)
     assert raised.value is failure

Reply via email to