blob: 7711be866713708faff87c820661053a7fe9a939 [file]
# 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.
"""Real SDK coverage of the joined-batch MERGE boundary used by Ray workers."""
import pyarrow as pa
import pytest
from pypaimon.write.backend_dispatch import create_merge_batch_transform
from pypaimon.write.merge_types import LiteralValue, SourceColumnRef, _NormalizedClause
pytestmark = pytest.mark.native_plan
def test_native_matched_actions_preserve_clause_order_and_callable_view():
batch = pa.table({
't._ROW_ID': pa.chunked_array([[0, 1], [2, 3]], type=pa.int64()),
't.id': [10, 11, 12, 13],
't.value': pa.array([90, 91, 92, 93], type=pa.int32()),
't.other': ['a', 'b', 'c', 'd'],
's.value': pa.array([None, 5, 15, 25], type=pa.int32()),
})
calls = []
def fallback(rows):
calls.append(rows.to_pydict())
assert rows.column_names == ['s.key', 's.value', 't._ROW_ID', 't.id', 't.other', 't.value']
return pa.chunked_array([[str(value)] for value in rows.column('s.key').to_pylist()])
clauses = [
_NormalizedClause({}, condition='s.value > 20', delete=True),
_NormalizedClause({'value': SourceColumnRef('value')}, condition='s.value > 10'),
_NormalizedClause({'other': fallback}),
]
# Output order can differ from the first appearance of fields in clauses.
schema = pa.schema([pa.field('_ROW_ID', pa.int64(), nullable=False),
('other', pa.string()), ('value', pa.int32())])
transform = create_merge_batch_transform(
'update', clauses, {'key': 'id'}, [('key', 'id')], ['other', 'value'],
'_ROW_ID', schema, native_enabled=True)
actual = transform(batch)
assert actual.schema == schema
assert actual.to_pylist() == [
{'_ROW_ID': 2, 'other': 'c', 'value': 15},
{'_ROW_ID': 0, 'other': '10', 'value': 90},
{'_ROW_ID': 1, 'other': '11', 'value': 91},
]
assert len(calls) == 1
assert calls[0]['t._ROW_ID'] == [0, 1]
delete_schema = pa.schema([pa.field('_ROW_ID', pa.int64(), nullable=False)])
deletes = create_merge_batch_transform(
'delete', clauses, {'key': 'id'}, '_ROW_ID', delete_schema, native_enabled=True)
assert deletes(batch).to_pylist() == [{'_ROW_ID': 3}]
assert len(calls) == 1 # UPDATE assignments are irrelevant to the delete route.
def test_native_insert_actions_fill_typed_nulls_and_keep_chunk_order():
schema = pa.schema([('value', pa.int32()), ('optional', pa.string())])
clauses = [
_NormalizedClause({'value': LiteralValue(20)}, condition='s.value >= (SELECT 20)'),
_NormalizedClause({'value': LiteralValue(10)}, condition='s.value >= 10'),
_NormalizedClause({'value': SourceColumnRef('value')}),
]
batch = pa.table({'s.value': pa.chunked_array([
pa.array([None, 5], type=pa.int32()), pa.array([15, 25], type=pa.int32()),
])})
transform = create_merge_batch_transform(
'insert', clauses, schema.names, schema, native_enabled=True)
actual = transform(batch)
assert actual.schema == schema
assert actual.to_pylist() == [
{'value': 20, 'optional': None}, {'value': 10, 'optional': None},
{'value': None, 'optional': None}, {'value': 5, 'optional': None},
]
def test_native_worker_transform_is_serializable_after_compilation():
import cloudpickle
schema = pa.schema([pa.field('_ROW_ID', pa.int64(), nullable=False), ('value', pa.int32())])
transform = create_merge_batch_transform(
'update', [_NormalizedClause({'value': SourceColumnRef('key')})],
{'key': 'id'}, [('key', 'id')], ['value'], '_ROW_ID', schema, native_enabled=True)
batch = pa.table({'t._ROW_ID': [7], 't.id': pa.array([42], type=pa.int32())})
assert transform(batch).to_pylist() == [{'_ROW_ID': 7, 'value': 42}]
restored = cloudpickle.loads(cloudpickle.dumps(transform))
assert restored(batch).to_pylist() == [{'_ROW_ID': 7, 'value': 42}]
def test_native_self_update_callable_keeps_explicit_unqualified_projection():
calls = []
def assignment(rows):
calls.append(rows.column_names)
assert rows.schema == pa.schema([('value', pa.int32()), ('id', pa.int64())])
return rows.column('value')
schema = pa.schema([pa.field('_ROW_ID', pa.int64(), nullable=False), ('value', pa.int32())])
transform = create_merge_batch_transform(
'update', [_NormalizedClause({'value': assignment}, condition='s.id > 1')],
{'id': 'id'}, [('id', 'id')], ['value'], '_ROW_ID', schema,
callable_input_columns=['value', 'id'], native_enabled=True)
batch = pa.table({'t._ROW_ID': [0, 1], 't.id': [1, 2],
't.value': pa.array([3, 4], type=pa.int32())})
assert transform(batch).to_pylist() == [{'_ROW_ID': 1, 'value': 4}]
assert calls == [['value', 'id']]