blob: 8c3f500113ad6d5412d39dc03b7c42bd82fd4b32 [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.
"""Public update constructors select one backend before acquiring resources."""
import subprocess
import sys
from unittest.mock import Mock, patch
import pytest
from pypaimon.write.table_update import ShardTableUpdator
@pytest.mark.parametrize('fails', [False, True])
def test_direct_shard_constructor_never_loads_or_replays_python(tmp_path, fails):
source = r'''
import importlib.abc
import sys
from types import SimpleNamespace
from unittest.mock import Mock, patch
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 shard loaded Python execution: ' + fullname)
sys.meta_path.insert(0, BlockPythonExecution())
from pypaimon.write.table_update import ShardTableUpdator
failure = OSError('A staged mutation may already have succeeded')
core = Mock()
core.prepare_commit.return_value = ['prepared']
core.update_by_arrow_batch.side_effect = failure if sys.argv[1] == 'True' else None
with patch('pypaimon.write.backend_dispatch.prepare_native_write_operation', return_value=core) as prepare:
table = SimpleNamespace(options=object(), identifier=object(),
catalog_environment=SimpleNamespace(table_query_auth=lambda *args: None))
shard = ShardTableUpdator(table, ['value'], ['value'], 'writer', 0, 2)
prepare.assert_called_once_with('shard_update', table, 'writer', ['value'], ['value'], 0, 2)
assert shard.arrow_reader() is core.arrow_reader.return_value
data = object()
try:
shard.update_by_arrow_batch(data)
except OSError as caught:
assert sys.argv[1] == 'True' and caught is failure
else:
assert sys.argv[1] == 'False'
assert shard.prepare_commit() == ['prepared']
shard.close()
core.update_by_arrow_batch.assert_called_once_with(data)
core.close.assert_called_once_with()
'''
result = subprocess.run(
[sys.executable, '-c', source, str(fails)], stdout=subprocess.PIPE,
stderr=subprocess.PIPE, universal_newlines=True, timeout=30)
assert result.returncode == 0, result.stderr
@pytest.mark.parametrize('shard,count,columns', [
(-1, 1, ['value']), (1, 1, ['value']), (0, 0, ['value']), (0, 1, [])])
def test_direct_shard_constructor_validates_before_backend_preparation(shard, count, columns):
with patch('pypaimon.write.backend_dispatch.prepare_native_write_operation') as prepare:
with pytest.raises(ValueError):
ShardTableUpdator(Mock(), None, columns, 'writer', shard, count)
prepare.assert_not_called()
@pytest.mark.parametrize('operation', ['direct', 'grouped', 'predicate', 'upsert-arrow', 'upsert-rows',
'delete-predicate', 'delete-id', 'shard'])
def test_python_update_keeps_selected_backend_for_nested_work(tmp_path, operation):
import pyarrow as pa
from pypaimon import CatalogFactory, Schema
from pypaimon.table.row.generic_row import GenericRow
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', 'scan.native-plan.enabled': 'false', 'read.native.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()
table = table.copy({'write.native.enabled': 'true'})
update = table.new_batch_write_builder().new_update().with_update_type(['value'])
data = pa.table({'id': [1], 'value': [11]}, schema=schema)
row_ids = pa.table({'_ROW_ID': [0], 'value': pa.array([11], pa.int32())})
# The top-level preparation is unsupported. Nested target-file writing
# must use the selected Python backend without reopening Native resources.
prepare = Mock(return_value=None)
with patch('pypaimon.write.backend_dispatch.prepare_native_write_operation', prepare), \
patch('pypaimon.write.update_dispatch.prepare_native_write_operation', prepare):
if operation == 'direct':
messages = update.update_by_arrow_with_row_id(row_ids)
elif operation == 'grouped':
messages = update.update_by_arrow_batches_with_row_id(iter([row_ids]))
elif operation == 'predicate':
messages = update.update_by_predicate(None, {'value': 11})
elif operation == 'upsert-arrow':
messages = update.upsert_by_arrow_with_key(data, ['id'])
elif operation == 'upsert-rows':
messages = update.upsert_by_key([GenericRow([1, 11], table.fields)], ['id'])
elif operation == 'delete-predicate':
messages = update.delete_by_predicate(update.new_predicate_builder().equal('id', 1))
elif operation == 'delete-id':
messages = update.delete_by_row_id([0])
else:
shard = update.new_shard_updator(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, pa.int32())], names=['value']))
messages = shard.prepare_commit()
finally:
shard.close()
assert messages
prepare.assert_called_once()
commit = table.new_batch_write_builder().new_commit()
try:
commit.commit(messages)
finally:
commit.close()
read = table.new_read_builder()
rows = read.new_read().to_arrow(read.new_scan().plan().splits()).sort_by('id').to_pylist()
if operation.startswith('delete'):
assert rows == [{'id': 2, 'value': 20}]
else:
assert rows == [{'id': 1, 'value': 11},
{'id': 2, 'value': 11 if operation in ('predicate', 'shard') else 20}]