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 d59063ed82 [Python] Reuse typed native MERGE clauses and factories 
(#10764)
d59063ed82 is described below

commit d59063ed82ce92bd685dd111d00b97fe9fb4305b
Author: Jingsong Lee <[email protected]>
AuthorDate: Fri Oct 23 09:55:30 2026 +0800

    [Python] Reuse typed native MERGE clauses and factories (#10764)
---
 .../execution/native/write/merge_transform.py      |  48 ++++---
 .../execution/native/write/native_merge_into.py    |   8 +-
 .../tests/merge_execution_boundary_test.py         |   7 +-
 .../tests/merge_execution_protocol_test.py         | 141 ++++++++++++++++++---
 .../pypaimon/tests/merge_sdk_test_support.py       |  76 +++++++++++
 .../tests/native_merge_source_metadata_test.py     |   7 +-
 6 files changed, 238 insertions(+), 49 deletions(-)

diff --git a/paimon-python/pypaimon/execution/native/write/merge_transform.py 
b/paimon-python/pypaimon/execution/native/write/merge_transform.py
index f3a5d64178..8a6ff9e08c 100644
--- a/paimon-python/pypaimon/execution/native/write/merge_transform.py
+++ b/paimon-python/pypaimon/execution/native/write/merge_transform.py
@@ -38,13 +38,14 @@ def _callable_assignment(function, on_pairs, 
callable_input_columns=None, hidden
 
 
 def encode_merge_clause(clause, on_pairs=(), on_map=None, input_schema=None,
-                        callable_input_columns=None, hidden_columns=()):
-    """Encode the existing clause contract without evaluating user 
callbacks."""
+                        callable_input_columns=None, hidden_columns=(), 
not_matched=False):
+    """Adapt a normalized PyPaimon clause to the typed SDK without running 
callbacks."""
+    from pypaimon_rust.datafusion import MergeAssignment, WhenMatched, 
WhenNotMatched
     from pypaimon.write.merge_types import SourceColumnRef, TargetColumnRef, 
LiteralValue
     from pypaimon.write.merge_condition import remap_source_on_keys, 
rewrite_condition
 
     available = None if input_schema is None else set(input_schema.names)
-    values = []
+    values = {}
     for name, value in clause.spec.items():
         if isinstance(value, SourceColumnRef):
             kind, column = 'source', value.column
@@ -52,20 +53,21 @@ def encode_merge_clause(clause, on_pairs=(), on_map=None, 
input_schema=None,
                 target = next((target for source, target in on_pairs
                                if source == column and 't.' + target in 
available), None)
                 if target is None:
-                    values.append((name, 'literal', None))
+                    values[name] = MergeAssignment.literal(None)
                     continue
                 kind, column = 'target', target
-            values.append((name, kind, column))
+            values[name] = (MergeAssignment.source_column(column) if kind == 
'source' else
+                            MergeAssignment.target_column(column))
         elif isinstance(value, TargetColumnRef):
             if available is not None and 't.' + value.column not in available:
-                values.append((name, 'literal', None))
+                values[name] = MergeAssignment.literal(None)
             else:
-                values.append((name, 'target', value.column))
+                values[name] = MergeAssignment.target_column(value.column)
         elif isinstance(value, LiteralValue):
-            values.append((name, 'literal', value.value))
+            values[name] = MergeAssignment.literal(value.value)
         elif callable(value) and not isinstance(value, type):
-            values.append((name, 'callable', _callable_assignment(
-                value, on_pairs, callable_input_columns, hidden_columns)))
+            values[name] = MergeAssignment.function(_callable_assignment(
+                value, on_pairs, callable_input_columns, hidden_columns))
         else:
             return None
     condition = None
@@ -73,8 +75,12 @@ def encode_merge_clause(clause, on_pairs=(), on_map=None, 
input_schema=None,
         rewritten = rewrite_condition(clause.condition)
         if on_map:
             rewritten = remap_source_on_keys(rewritten, on_map)
-        condition = dict(sql=rewritten)
-    return dict(assignments=values, condition=condition, delete=clause.delete)
+        condition = rewritten
+    if not_matched:
+        return WhenNotMatched(values, condition)
+    if clause.delete:
+        return WhenMatched.delete(condition)
+    return WhenMatched.update(values, condition)
 
 
 class _NativeMergeTransform:
@@ -82,7 +88,9 @@ class _NativeMergeTransform:
                  callable_input_columns=None, row_id_name='_ROW_ID'):
         # Require this release's protocol before consuming input. An installed
         # incompatible SDK must not silently select another executor.
-        from pypaimon_rust.datafusion import MergeBatchExecution  # noqa: F401
+        from pypaimon_rust.datafusion import (  # noqa: F401
+            MergeAssignment, MergeBatchExecution, WhenMatched, WhenNotMatched,
+        )
 
         self.clauses = clauses
         self.output_schema = output_schema
@@ -114,15 +122,19 @@ class _NativeMergeTransform:
 
             clauses = [encode_merge_clause(
                 clause, self.on_pairs, self.on_map, batch.schema,
-                self.callable_input_columns, hidden_columns,
+                self.callable_input_columns, hidden_columns, 
not_matched=self.mode == 'insert',
             ) for clause in self.clauses]
             if any(clause is None for clause in clauses):
                 raise TypeError('Unsupported normalized MERGE assignment')
-            self._execution = MergeBatchExecution(
-                batch.schema, output_schema, clauses, self.mode,
-            )
+            if self.mode == 'update':
+                self._execution = 
MergeBatchExecution.for_updates(batch.schema, output_schema, clauses)
+            elif self.mode == 'delete':
+                self._execution = 
MergeBatchExecution.for_deletes(batch.schema, clauses)
+            else:
+                self._execution = 
MergeBatchExecution.for_inserts(batch.schema, output_schema, clauses)
         result = self._execution.execute(batch)
-        if hidden_columns:
+        if hidden_columns or (self.mode == 'delete' and not 
result.schema.equals(
+                self.output_schema, check_metadata=True)):
             import pyarrow as pa
             result = pa.Table.from_arrays(result.columns, 
schema=self.output_schema)
         return result
diff --git a/paimon-python/pypaimon/execution/native/write/native_merge_into.py 
b/paimon-python/pypaimon/execution/native/write/native_merge_into.py
index c60540f8b4..8aa072b54f 100644
--- a/paimon-python/pypaimon/execution/native/write/native_merge_into.py
+++ b/paimon-python/pypaimon/execution/native/write/native_merge_into.py
@@ -89,11 +89,9 @@ def create_native_merge_into(table, source, on, matched, 
not_matched, commit_use
         if source_type != target_type or not 
_supported_upsert_key_type(target_type):
             return None
 
-    def encode(clause):
-        return encode_merge_clause(clause, list(zip(source_keys, target_keys)))
-
-    encoded_matched = [encode(clause) for clause in matched]
-    encoded_not_matched = [encode(clause) for clause in not_matched]
+    on_pairs = list(zip(source_keys, target_keys))
+    encoded_matched = [encode_merge_clause(clause, on_pairs) for clause in 
matched]
+    encoded_not_matched = [encode_merge_clause(clause, on_pairs, 
not_matched=True) for clause in not_matched]
     if any(clause is None for clause in encoded_matched + encoded_not_matched):
         return None
     builder = 
(native_table.new_batch_write_builder().with_commit_user(commit_user)
diff --git a/paimon-python/pypaimon/tests/merge_execution_boundary_test.py 
b/paimon-python/pypaimon/tests/merge_execution_boundary_test.py
index 92a524e3ab..c89ce69cc3 100644
--- a/paimon-python/pypaimon/tests/merge_execution_boundary_test.py
+++ b/paimon-python/pypaimon/tests/merge_execution_boundary_test.py
@@ -77,6 +77,8 @@ if engine == 'python':
     assert 
read.new_read().to_arrow(read.new_scan().plan().splits()).sort_by('id').to_pylist()
 == [
         {'id': 1, 'value': 9}, {'id': 2, 'value': 7}]
 else:
+    from pypaimon.tests.merge_sdk_test_support import merge_sdk_module
+    sys.modules['pypaimon_rust.datafusion'] = merge_sdk_module()
     core = Mock()
     builder = core.new_batch_write_builder.return_value
     builder.with_commit_user.return_value = builder
@@ -91,10 +93,9 @@ else:
                   side_effect=lambda table, messages: messages):
         assert merge_into(table, data, on=['id'], when_matched=matched, 
commit_user='test') == []
     writer.merge_into.assert_called_once()
-    callback = 
writer.merge_into.call_args[1]['when_matched'][0]['assignments'][0][2]
+    callback = 
writer.merge_into.call_args[1]['when_matched'][0].assignments['value'].value
     assert callback(pa.table({'t.id': [1], 't.value': [3], 's.value': 
[9]})).to_pylist() == [9]
-    assert writer.merge_into.call_args[1]['when_matched'][0]['condition'] == {
-        'sql': '\"s.value\" > \"t.value\"'}
+    assert writer.merge_into.call_args[1]['when_matched'][0].condition == 
'\"s.value\" > \"t.value\"'
 assert not any(name.startswith('pypaimon.ray') or name == blocked
                or name.startswith(blocked + '.') for name in sys.modules)
 """
diff --git a/paimon-python/pypaimon/tests/merge_execution_protocol_test.py 
b/paimon-python/pypaimon/tests/merge_execution_protocol_test.py
index 083476ec45..61a6940802 100644
--- a/paimon-python/pypaimon/tests/merge_execution_protocol_test.py
+++ b/paimon-python/pypaimon/tests/merge_execution_protocol_test.py
@@ -26,6 +26,7 @@ from unittest.mock import Mock, patch
 import pyarrow as pa
 import pytest
 
+from pypaimon.tests.merge_sdk_test_support import MergeAssignment, 
WhenMatched, WhenNotMatched, merge_sdk_module
 from pypaimon.write.backend_dispatch import create_merge_batch_transform
 from pypaimon.write.merge_types import (
     LiteralValue, SourceColumnRef, TargetColumnRef, _NormalizedClause,
@@ -38,15 +39,32 @@ def native_protocol(monkeypatch):
     execute = Mock()
 
     class MergeBatchExecution:
-        def __init__(self, input_schema, output_schema, clauses, mode):
-            calls.append((input_schema, output_schema, clauses, mode))
+        @classmethod
+        def for_updates(cls, input_schema, update_schema, when_matched):
+            assert all(isinstance(clause, WhenMatched) for clause in 
when_matched)
+            return cls((input_schema, update_schema, when_matched, 'update'))
+
+        @classmethod
+        def for_deletes(cls, input_schema, when_matched):
+            assert all(isinstance(clause, WhenMatched) for clause in 
when_matched)
+            schema = pa.schema([pa.field('_ROW_ID', pa.int64(), 
nullable=False)])
+            return cls((input_schema, schema, when_matched, 'delete'))
+
+        @classmethod
+        def for_inserts(cls, input_schema, target_schema, when_not_matched):
+            assert all(isinstance(clause, WhenNotMatched) for clause in 
when_not_matched)
+            return cls((input_schema, target_schema, when_not_matched, 
'insert'))
+
+        def __init__(self, compiled):
+            self.compiled = compiled
+            calls.append(compiled)
 
         def execute(self, batch):
-            return execute(batch, calls[-1])
+            return execute(batch, self.compiled)
 
     runtime = ModuleType('pypaimon_rust')
     runtime.__path__ = []
-    datafusion = ModuleType('pypaimon_rust.datafusion')
+    datafusion = merge_sdk_module()
     datafusion.MergeBatchExecution = MergeBatchExecution
     monkeypatch.setitem(sys.modules, 'pypaimon_rust', runtime)
     monkeypatch.setitem(sys.modules, 'pypaimon_rust.datafusion', datafusion)
@@ -101,9 +119,10 @@ def 
test_native_update_restores_callable_aliases_and_preserves_result_chunks(nat
         assert input_schema == batch.schema
         assert output_schema == _update_schema()
         assert mode == 'update'
-        assert clauses[0]['condition'] == {'sql': '"t.id" > 0'}
-        name, kind, callback = clauses[0]['assignments'][0]
-        assert (name, kind) == ('value', 'callable')
+        assert clauses[0].condition == '"t.id" > 0'
+        assignment = clauses[0].assignments['value']
+        assert assignment.kind == 'function'
+        callback = assignment.value
         assert callback(rows).equals(result['value'])
         return result
 
@@ -130,7 +149,7 @@ def 
test_native_self_update_keeps_explicit_callable_read_order(native_protocol):
     batch = _joined_batch()
 
     def execute(rows, compiled):
-        callback = compiled[2][0]['assignments'][0][2]
+        callback = compiled[2][0].assignments['value'].value
         return pa.Table.from_arrays([rows['t._ROW_ID'], callback(rows)], 
schema=_update_schema())
 
     native_protocol.execute.side_effect = execute
@@ -162,11 +181,11 @@ def 
test_native_custom_row_id_preserves_aliases_and_output_schema(native_protoco
 
     def execute(rows, compiled):
         assert rows['t._ROW_ID'].equals(batch['t.custom_id'])
-        assert compiled[2][0]['condition'] == {'sql': '"t.custom_id" > 0'}
+        assert compiled[2][0].condition == '"t.custom_id" > 0'
         assert compiled[1].names[0] == '_ROW_ID'
         columns = [rows['t._ROW_ID']]
         if operation == 'update':
-            columns.append(compiled[2][0]['assignments'][0][2](rows))
+            columns.append(compiled[2][0].assignments['value'].value(rows))
         return pa.Table.from_arrays(columns, schema=compiled[1])
 
     native_protocol.execute.side_effect = execute
@@ -189,11 +208,87 @@ def 
test_native_transports_coalesced_and_missing_column_assignments(native_proto
     transform = _native_update(clauses)
     native_protocol.execute.return_value = _update_schema().empty_table()
     transform(_joined_batch())
-    assert native_protocol.calls[0][2][0]['assignments'] == [
-        ('key', 'target', 'id'), ('source', 'source', 'value'),
-        ('target', 'target', 'value'), ('missing_source', 'literal', None),
-        ('missing_target', 'literal', None), ('constant', 'literal', 9),
-    ]
+    assert native_protocol.calls[0][2][0].assignments == {
+        'key': MergeAssignment.target_column('id'),
+        'source': MergeAssignment.source_column('value'),
+        'target': MergeAssignment.target_column('value'),
+        'missing_source': MergeAssignment.literal(None),
+        'missing_target': MergeAssignment.literal(None),
+        'constant': MergeAssignment.literal(9),
+    }
+
+
[email protected]('stream', [False, True], ids=['batch', 'stream'])
+def 
test_native_full_merge_uses_typed_branches_without_evaluating_callbacks(tmp_path,
 native_protocol, stream):
+    from pypaimon import CatalogFactory, Schema
+    from pypaimon.write.merge_types import WhenMatched as PublicWhenMatched, 
WhenNotMatched as PublicWhenNotMatched
+    from pypaimon.write.merge_types import lit, source_col, target_col
+
+    catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
+    catalog.create_database('default', True)
+    schema = pa.schema([('id', pa.int32()), ('value', pa.int32()), ('other', 
pa.int32())])
+    catalog.create_table('default.t', Schema.from_pyarrow_schema(schema, 
options={
+        'data-evolution.enabled': 'true', 'row-tracking.enabled': 'true', 
'write.native.enabled': 'true',
+        'deletion-vectors.enabled': 'true',
+    }), False)
+    table = catalog.get_table('default.t')
+    native_table = Mock()
+    builder = (native_table.new_stream_write_builder.return_value if stream 
else
+               native_table.new_batch_write_builder.return_value)
+    builder.with_commit_user.return_value = builder
+    writer = builder.new_update.return_value
+    writer.merge_into.return_value = []
+    callback = Mock(return_value=pa.array([11], pa.int32()))
+    source = pa.table({'source_id': pa.array([1], pa.int32()), 'value': 
pa.array([11], pa.int32())})
+    public_builder = table.new_stream_write_builder() if stream else 
table.new_batch_write_builder()
+    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.from_native_commit_messages',
+                  side_effect=lambda table, messages: messages):
+        assert public_builder.new_update().merge_into(
+            source, on={'id': 'source_id'}, when_matched=[
+                PublicWhenMatched.delete('s.value < 0'),
+                PublicWhenMatched.update({'value': callback}, 's.source_id > 
0'),
+                PublicWhenMatched.update({'value': source_col('value'), 
'other': target_col('value')}),
+            ], when_not_matched=[PublicWhenNotMatched(
+                {'id': source_col('source_id'), 'value': lit(5)}, 's.value > 
0')],
+            **({'commit_identifier': 31} if stream else {})) == []
+    callback.assert_not_called()
+    args, kwargs = writer.merge_into.call_args
+    assert args[0] is source
+    assert kwargs['on'] == [('id', 'source_id')]
+    if stream:
+        assert kwargs['commit_identifier'] == 31
+    else:
+        assert 'commit_identifier' not in kwargs
+    matched = kwargs['when_matched']
+    assert all(isinstance(clause, WhenMatched) for clause in matched)
+    assert [clause.action for clause in matched] == ['delete', 'update', 
'update']
+    assert [clause.condition for clause in matched] == ['"s.value" < 0', 
'"s.source_id" > 0', None]
+    assert matched[2].assignments == {
+        'value': MergeAssignment.source_column('value'), 'other': 
MergeAssignment.target_column('value'),
+    }
+    inserted = kwargs['when_not_matched']
+    assert all(isinstance(clause, WhenNotMatched) for clause in inserted)
+    assert inserted[0].condition == '"s.value" > 0'
+    assert inserted[0].assignments == {
+        'id': MergeAssignment.source_column('source_id'), 'value': 
MergeAssignment.literal(5),
+    }
+    joined = pa.table({'t.id': [1], 't.value': [3], 't.other': [4], 's.value': 
[11]})
+    assert matched[1].assignments['value'].value(joined).to_pylist() == [11]
+    assert callback.call_args[0][0].column_names == ['s.source_id', 's.value', 
't.id', 't.other', 't.value']
+
+
+def 
test_native_delete_restores_requested_field_and_schema_metadata(native_protocol):
+    schema = pa.schema([pa.field('_ROW_ID', pa.int64(), nullable=False, 
metadata={b'field': b'value'})],
+                       metadata={b'schema': b'value'})
+    transform = create_merge_batch_transform(
+        'delete', [_NormalizedClause({}, delete=True)], {}, '_ROW_ID', schema, 
native_enabled=True,
+    )
+    native_protocol.execute.side_effect = lambda batch, compiled: 
pa.Table.from_arrays(
+        [batch['t._ROW_ID']], schema=compiled[1])
+    result = transform(_joined_batch())
+    assert result.schema.equals(schema, check_metadata=True)
+    assert result['_ROW_ID'].to_pylist() == [7, 8]
 
 
 def 
test_native_insert_callback_retains_source_aliases_and_dictionary_types(native_protocol):
@@ -216,7 +311,7 @@ def 
test_native_insert_callback_retains_source_aliases_and_dictionary_types(nati
 
     def execute(rows, compiled):
         assert compiled[3] == 'insert'
-        return 
pa.Table.from_arrays([compiled[2][0]['assignments'][0][2](rows)], schema=schema)
+        return 
pa.Table.from_arrays([compiled[2][0].assignments['value'].value(rows)], 
schema=schema)
 
     native_protocol.execute.side_effect = execute
     assert transform(batch)['value'].to_pylist() == [10, 20]
@@ -263,11 +358,11 @@ def 
test_native_failure_preserves_exception_identity_without_python_replay(nativ
         raise failure
 
     if phase == 'compile':
-        native_protocol.module.MergeBatchExecution = Mock(side_effect=failure)
+        native_protocol.module.MergeBatchExecution.for_updates = 
Mock(side_effect=failure)
     elif phase == 'execute':
         native_protocol.execute.side_effect = failure
     else:
-        native_protocol.execute.side_effect = lambda rows, compiled: 
compiled[2][0]['assignments'][0][2](rows)
+        native_protocol.execute.side_effect = lambda rows, compiled: 
compiled[2][0].assignments['value'].value(rows)
     with 
patch('pypaimon.execution.python.write.merge_transform._build_matched_transform')
 as python:
         transform = _native_update([_NormalizedClause({'value': callback})])
         with pytest.raises(ValueError) as caught:
@@ -368,7 +463,7 @@ def 
test_native_ray_preparation_does_not_require_python_datafusion(
     batch = (pa.table({'s.id': pa.array([1], pa.int32()), 's.value': 
pa.array([10], pa.int32())})
              if operation == 'insert' else _joined_batch())
     assert transform(batch).schema == output_schema
-    assert native_protocol.calls[0][2][0]['condition'] == {'sql': '"s.value" > 
0'}
+    assert native_protocol.calls[0][2][0].condition == '"s.value" > 0'
 
 
 @pytest.mark.parametrize('operation', ['update', 'delete', 'insert'])
@@ -425,11 +520,15 @@ class RejectOtherBackend(importlib.abc.MetaPathFinder):
 sys.meta_path.insert(0, RejectOtherBackend())
 if native_enabled:
     class MergeBatchExecution:
-        def __init__(self, input_schema, output_schema, clauses, mode):
+        @classmethod
+        def for_updates(cls, input_schema, update_schema, when_matched):
+            return cls(update_schema)
+        def __init__(self, output_schema):
             self.output_schema = output_schema
         def execute(self, batch):
             return self.output_schema.empty_table()
-    module = ModuleType('pypaimon_rust.datafusion')
+    from pypaimon.tests.merge_sdk_test_support import merge_sdk_module
+    module = merge_sdk_module()
     module.MergeBatchExecution = MergeBatchExecution
     sys.modules['pypaimon_rust.datafusion'] = module
 from pypaimon.ray.data_evolution_merge_join import _build_matched_transform
diff --git a/paimon-python/pypaimon/tests/merge_sdk_test_support.py 
b/paimon-python/pypaimon/tests/merge_sdk_test_support.py
new file mode 100644
index 0000000000..db2cef51e2
--- /dev/null
+++ b/paimon-python/pypaimon/tests/merge_sdk_test_support.py
@@ -0,0 +1,76 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Typed companion SDK doubles for transport tests that do not load Rust."""
+
+from dataclasses import dataclass
+from types import ModuleType
+
+
+@dataclass(frozen=True)
+class MergeAssignment:
+    kind: str
+    value: object
+
+    @classmethod
+    def source_column(cls, name):
+        return cls('source', name)
+
+    @classmethod
+    def target_column(cls, name):
+        return cls('target', name)
+
+    @classmethod
+    def literal(cls, value):
+        return cls('literal', value)
+
+    @classmethod
+    def function(cls, function):
+        return cls('function', function)
+
+
+@dataclass(frozen=True)
+class WhenMatched:
+    assignments: dict
+    condition: object
+    action: str
+
+    @classmethod
+    def update(cls, assignments, condition=None):
+        assert all(isinstance(value, MergeAssignment) for value in 
assignments.values())
+        return cls(dict(assignments), condition, 'update')
+
+    @classmethod
+    def delete(cls, condition=None):
+        return cls({}, condition, 'delete')
+
+
+@dataclass(frozen=True)
+class WhenNotMatched:
+    assignments: dict
+    condition: object = None
+
+    def __post_init__(self):
+        assert all(isinstance(value, MergeAssignment) for value in 
self.assignments.values())
+
+
+def merge_sdk_module():
+    module = ModuleType('pypaimon_rust.datafusion')
+    module.MergeAssignment = MergeAssignment
+    module.WhenMatched = WhenMatched
+    module.WhenNotMatched = WhenNotMatched
+    return module
diff --git a/paimon-python/pypaimon/tests/native_merge_source_metadata_test.py 
b/paimon-python/pypaimon/tests/native_merge_source_metadata_test.py
index 9a5251026d..03c7b29b71 100644
--- a/paimon-python/pypaimon/tests/native_merge_source_metadata_test.py
+++ b/paimon-python/pypaimon/tests/native_merge_source_metadata_test.py
@@ -19,6 +19,7 @@
 
 from contextlib import ExitStack
 import importlib.util
+import sys
 from unittest.mock import Mock, patch
 
 import pyarrow as pa
@@ -27,6 +28,7 @@ import pytest
 
 from pypaimon import Schema
 from pypaimon.execution.native.runtime import (native_runtime_available)
+from pypaimon.tests.merge_sdk_test_support import merge_sdk_module
 from pypaimon.table.data_evolution_merge_into import WhenMatched, 
WhenNotMatched, source_col
 
 
@@ -128,7 +130,8 @@ def 
test_table_source_metadata_conditions_keep_selected_view_and_callable_rows(
 
 
 @pytest.mark.parametrize('table_source', [False, True], ids=['arrow-source', 
'table-source'])
-def 
test_native_condition_transport_does_not_load_python_datafusion(native_rest_catalog,
 table_source):
+def 
test_native_condition_transport_does_not_load_python_datafusion(native_rest_catalog,
 monkeypatch, table_source):
+    monkeypatch.setitem(sys.modules, 'pypaimon_rust.datafusion', 
merge_sdk_module())
     target = _new_table(native_rest_catalog, 
'default.no_python_engine').copy({'write.native.enabled': 'true'})
     source = (_new_table(native_rest_catalog, 'default.metadata_source') if 
table_source else
               pa.table({'key': [0], 'value': [91]}, schema=_SCHEMA))
@@ -150,7 +153,7 @@ def 
test_native_condition_transport_does_not_load_python_datafusion(native_rest_
     core.merge_into.assert_called_once()
     args, kwargs = core.merge_into.call_args
     assert args[0] is (native_source if table_source else source)
-    assert kwargs['when_matched'][0]['condition'] == {'sql': '"s.{}" = 
0'.format(source_key)}
+    assert kwargs['when_matched'][0].condition == '"s.{}" = 
0'.format(source_key)
 
 
 def 
test_python_merge_conditions_still_require_the_python_execution_engine(native_rest_catalog):

Reply via email to