blob: 27110439d22daf2b698f69f1b1b0e15200f057ea [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.
"""Bound update execution retains public methods and preparation boundaries."""
import subprocess
import sys
import pyarrow as pa
import pytest
from pypaimon import CatalogFactory, Schema
from pypaimon.snapshot.snapshot import BATCH_COMMIT_IDENTIFIER
from pypaimon.table.row.generic_row import GenericRow
from pypaimon.write.table_delete import TableDeleteByRowId
from pypaimon.write.table_update import BatchTableUpdate
from pypaimon.write.table_update_by_row_id import TableUpdateByRowId
from pypaimon.write.table_upsert_by_key import TableUpsertByKey
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 / 'warehouse')})
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={
'bucket': '-1', 'row-tracking.enabled': 'true', 'data-evolution.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
def _commit(table, messages):
commit = table.new_batch_write_builder().new_commit()
try:
commit.commit(messages)
finally:
commit.close()
def _values(table):
read = table.new_read_builder()
return read.new_read().to_arrow(read.new_scan().plan().splits()).sort_by('id').to_pydict()
def test_bound_update_uses_live_configuration_and_callable_assignments(table):
built = []
def assignment(matched):
built.append(matched.to_pydict())
return pa.compute.add(matched['value'], 1)
update = BatchTableUpdate(table, 'public-update')
update.with_update_type(['value'])
_commit(table, update.update_by_arrow_with_row_id(pa.table({'_ROW_ID': [0], 'value': [11]})))
update.with_update_type(['id'])
_commit(table, update.update_by_arrow_with_row_id(pa.table({'_ROW_ID': [1], 'id': [3]})))
predicate = update.new_predicate_builder().equal('id', 3)
_commit(table, update.update_by_predicate(
predicate, {'value': assignment}, read_columns=['value']))
assert built == [{'value': [20], '_ROW_ID': [1]}]
assert _values(table) == {'id': [1, 3], 'value': [11, 21]}
def test_bound_row_id_execution_owns_file_routing_and_committable_state(table):
update = TableUpdateByRowId(table, 'row-id-update', BATCH_COMMIT_IDENTIFIER)
messages = update.update_columns(pa.table({'_ROW_ID': [0], 'value': [33]}), ['value'])
assert messages is update.commit_messages
assert not hasattr(update, 'first_row_ids')
assert not hasattr(update, '_write_by_first_row_id')
_commit(table, messages)
assert _values(table) == {'id': [1, 2], 'value': [33, 20]}
def test_bound_delete_owns_anchor_routing_and_committable_result(table):
delete = TableDeleteByRowId(table)
messages = delete.delete([0, 0])
assert not hasattr(delete, '_build_commit_message')
assert not hasattr(delete, '_write_deletion_vector_index')
_commit(table, messages)
assert _values(table) == {'id': [2], 'value': [20]}
@pytest.mark.parametrize('rows', [False, True])
def test_prepared_python_upsert_keeps_public_input_methods(table, rows):
visited = []
class PublicUpsert(TableUpsertByKey):
def upsert(self, data, keys, update_cols=None):
visited.append(('arrow', data.num_rows))
return super().upsert(data, keys, update_cols)
def upsert_rows(self, row_items, keys, update_cols=None):
visited.append(('rows', len(row_items)))
return super().upsert_rows(row_items, keys, update_cols)
update = PublicUpsert(table, 'upsert', BATCH_COMMIT_IDENTIFIER)
data = [{'id': 1, 'value': 12}, {'id': 3, 'value': 30}]
if rows:
messages = update.upsert_rows(
[GenericRow([row['id'], row['value']], table.fields) for row in data], ['id'], ['value'])
else:
arrow_data = pa.Table.from_pylist(data, schema=pa.schema([('id', pa.int32()), ('value', pa.int32())]))
messages = update.upsert(arrow_data, ['id'], ['value'])
assert visited == [('rows' if rows else 'arrow', 2)]
_commit(table, messages)
assert _values(table) == {'id': [1, 2, 3], 'value': [12, 20, 30]}
@pytest.mark.parametrize('operation', ['direct', 'grouped', 'predicate', 'delete-predicate', 'delete-id',
'upsert-arrow', 'upsert-rows'])
@pytest.mark.parametrize('fails', [False, True])
def test_prepared_native_updates_do_not_initialize_or_replay_python(tmp_path, operation, fails):
source = r'''
import importlib.abc
import sys
from types import SimpleNamespace
from unittest.mock import Mock, patch
import pyarrow as pa
class BlockPythonExecution(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname == 'pypaimon.execution.python' or fullname.startswith('pypaimon.execution.python.'):
raise AssertionError('Native update loaded Python execution: ' + fullname)
sys.meta_path.insert(0, BlockPythonExecution())
from pypaimon import CatalogFactory, Schema
from pypaimon.table.row.generic_row import GenericRow
operation, fails, warehouse = sys.argv[1:]
catalog = CatalogFactory.create({'warehouse': warehouse})
catalog.create_database('default', True)
catalog.create_table('default.data', Schema.from_pyarrow_schema(
pa.schema([('id', pa.int32()), ('value', pa.int32())]), options={
'row-tracking.enabled': 'true', 'data-evolution.enabled': 'true', 'deletion-vectors.enabled': 'true',
'write.native.enabled': 'true', 'commit.native.enabled': 'false',
'scan.native-plan.enabled': 'false', 'read.native.enabled': 'false'}), False)
table = catalog.get_table('default.data')
update = table.new_batch_write_builder().new_update().with_update_type(['value'])
data = pa.table({'id': [1], 'value': [10]})
row_ids = pa.table({'_ROW_ID': [0], 'value': [11]})
expected = [object()]
failure = OSError('A staged mutation may already have succeeded')
execute = Mock(return_value=expected, side_effect=failure if fails == 'True' else None)
method, args = {
'direct': ('update_by_arrow_with_row_id', (row_ids,)),
'grouped': ('update_by_arrow_batches_with_row_id', (iter([row_ids]),)),
'predicate': ('update_by_predicate', (None, {'value': 12})),
'delete-predicate': ('delete_by_predicate', (None,)),
'delete-id': ('delete_by_row_id', ([0],)),
'upsert-arrow': ('upsert_by_arrow_with_key', (data, ['id'])),
'upsert-rows': ('upsert_by_key', ([GenericRow([1, 10], table.fields)], ['id'])),
}[operation]
backend_method = {'predicate': 'update', 'delete-predicate': 'delete',
'upsert-arrow': 'upsert', 'upsert-rows': 'upsert'}.get(operation, method)
backend = SimpleNamespace(**{backend_method: execute})
with patch('pypaimon.write.update_dispatch.prepare_native_write_operation', return_value=backend):
try:
result = getattr(update, method)(*args)
except OSError as caught:
assert fails == 'True' and caught is failure
else:
assert fails == 'False' and result is expected
execute.assert_called_once()
'''
result = subprocess.run(
[sys.executable, '-c', source, operation, str(fails), str(tmp_path / 'warehouse')],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, timeout=30)
assert result.returncode == 0, result.stderr