blob: bdf7c873fe63f197d67bd96aaa01201d6a1052fa [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.
"""Selected Python factories retain state without reopening Native execution."""
from unittest.mock import patch
import pyarrow as pa
import pytest
from pypaimon import CatalogFactory, Schema
from pypaimon.snapshot.snapshot import BATCH_COMMIT_IDENTIFIER
from pypaimon.write.table_delete import TableDeleteByRowId
from pypaimon.write.table_update import ShardTableUpdator
from pypaimon.write.table_update_by_row_id import TableUpdateByRowId
pytestmark = [pytest.mark.python_plan, pytest.mark.python_read,
pytest.mark.python_write, pytest.mark.python_commit]
@pytest.fixture
def table(tmp_path):
catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
catalog.create_database('default', True)
schema = pa.schema([('id', pa.int32()), ('value', pa.int32())])
catalog.create_table('default.data', Schema.from_pyarrow_schema(schema, options={
'data-evolution.enabled': 'true', 'row-tracking.enabled': 'true',
'deletion-vectors.enabled': 'true', 'write.native.enabled': 'false',
'commit.native.enabled': 'false', 'read.native.enabled': 'false', 'scan.native-plan.enabled': 'false',
}), False)
table = catalog.get_table('default.data')
builder = table.new_batch_write_builder()
writer, commit = builder.new_write(), builder.new_commit()
try:
writer.write_arrow(pa.table({'id': [1, 2], 'value': [10, 20]}, schema=schema))
commit.commit(writer.prepare_commit())
finally:
writer.close()
commit.close()
return table.copy({'write.native.enabled': 'true'})
@pytest.mark.parametrize('operation', ['row-id', 'delete', 'shard'])
def test_explicit_python_factory_keeps_captured_state_and_committable_results(table, operation):
with patch('pypaimon.write.backend_dispatch.prepare_native_write_operation',
side_effect=AssertionError('Selected Python operation reopened Native')):
if operation == 'row-id':
updater = TableUpdateByRowId._from_python(table, 'writer', BATCH_COMMIT_IDENTIFIER)
captured = updater._snapshot_files_info()
messages = updater.update_columns(pa.table({'_ROW_ID': [0], 'value': [11]}), ['value'])
# Different columns of the same file group share the captured
# snapshot and append to the same executor's accumulated messages.
assert updater.update_columns(pa.table({'_ROW_ID': [1], 'id': [3]}), ['id']) is messages
assert updater._snapshot_files_info().snapshot_id == captured.snapshot_id
assert all(message.check_from_snapshot == captured.snapshot_id for message in messages)
expected = [{'id': 1, 'value': 11}, {'id': 3, 'value': 20}]
elif operation == 'delete':
coordinator = TableDeleteByRowId._from_python(table)
captured = coordinator._snapshot_anchor_ranges()
worker = TableDeleteByRowId._from_python(table, captured)
messages = worker.delete([0, 0])
assert all(message.check_from_snapshot == captured.snapshot_id for message in messages)
expected = [{'id': 2, 'value': 20}]
else:
shard = ShardTableUpdator._from_python(table, ['value'], ['value'], 'writer', 0, 1)
try:
reader = shard.arrow_reader()
for batch in reader:
shard.update_by_arrow_batch(pa.record_batch([
pa.array([11] * batch.num_rows, type=pa.int32())], names=['value']))
messages = shard.prepare_commit()
assert shard.prepare_commit() == messages
finally:
shard.close()
expected = [{'id': 1, 'value': 11}, {'id': 2, 'value': 11}]
assert messages
commit = table.new_batch_write_builder().new_commit()
try:
commit.commit(messages)
finally:
commit.close()
read = table.new_read_builder()
assert read.new_read().to_arrow(read.new_scan().plan().splits()).sort_by('id').to_pylist() == expected
@pytest.mark.parametrize('selection', ['explicit-python', 'preparation-fallback'])
def test_selected_python_writer_keeps_dynamic_metadata_in_python(tmp_path, selection):
from pypaimon.execution.python.write.table_write import PythonTableWrite
from pypaimon.write import backend_dispatch
from pypaimon.write.table_write import BatchTableWrite
catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
catalog.create_database('default', True)
schema = pa.schema([('id', pa.int32()), ('value', pa.string())])
catalog.create_table('default.dynamic', Schema.from_pyarrow_schema(
schema, primary_keys=['id'], options={
'bucket': '-1', 'dynamic-bucket.target-row-num': '1',
'write.native.enabled': 'false', 'commit.native.enabled': 'false',
'scan.native-plan.enabled': 'false', 'read.native.enabled': 'false',
}), False)
table = catalog.get_table('default.dynamic')
builder = table.new_batch_write_builder()
writer, commit = builder.new_write(), builder.new_commit()
try:
writer.write_arrow(pa.table({'id': [1], 'value': ['old']}, schema=schema))
commit.commit(writer.prepare_commit())
finally:
writer.close()
commit.close()
snapshot = table.snapshot_manager().get_latest_snapshot()
assert snapshot is not None
table = table.copy({
'write.native.enabled': 'true', 'scan.native-plan.enabled': 'true', 'read.native.enabled': 'true'})
with patch.object(backend_dispatch, 'try_prepare_native',
wraps=backend_dispatch.try_prepare_native) as native_preparation, \
patch('pypaimon.execution.native.write.native_write.create_native_write',
side_effect=RuntimeError('ordinary Native preparation failure')) as native_writer, \
patch('pypaimon.execution.native.read.plan.native_plan',
side_effect=AssertionError('Selected Python writer entered Native')) as native_plan:
if selection == 'explicit-python':
writer = backend_dispatch.create_python_write(table, 'selected-python')
else:
writer = BatchTableWrite(table, 'selected-python')
assert isinstance(writer._execution, PythonTableWrite)
assert table.options.native_write_enabled()
try:
# Restoring the existing key must use the historical HASH index;
# assigning a new key also validates all live data buckets.
writer.write_arrow(pa.table({'id': [1, 2], 'value': ['new', 'other']}, schema=schema))
messages = writer.prepare_commit()
finally:
writer.close()
expected_preparations = int(selection == 'preparation-fallback')
assert native_preparation.call_count == expected_preparations
assert native_writer.call_count == expected_preparations
native_plan.assert_not_called()
commit = table.new_batch_write_builder().new_commit()
try:
commit.commit(messages)
finally:
commit.close()
read = table.copy({'scan.native-plan.enabled': 'false', 'read.native.enabled': 'false'}).new_read_builder()
assert read.new_read().to_arrow(read.new_scan().plan().splits()).sort_by('id').to_pylist() == [
{'id': 1, 'value': 'new'}, {'id': 2, 'value': 'other'}]