blob: c9f9185dd00acb3e2167423ccac49c657d71b99f [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.
"""Execution ownership and public writer hook contracts."""
import builtins
from unittest.mock import Mock, patch
import pyarrow as pa
import pytest
from pypaimon import CatalogFactory, Schema
from pypaimon.execution.native.write.native_write import NativeTableWrite as NativeWrite
from pypaimon.write.table_write import BatchTableWrite
def _table(tmp_path, options=None):
catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
catalog.create_database('default', True)
catalog.create_table('default.data', Schema.from_pyarrow_schema(
pa.schema([('id', pa.int32()), ('value', pa.string())]),
options=options or {}), False)
return catalog.get_table('default.data')
@pytest.mark.parametrize('input_method', ['write_arrow', 'write_pandas'])
@pytest.mark.parametrize('override', ['subclass', 'instance'])
def test_public_writer_keeps_dynamic_batch_hooks(tmp_path, input_method, override):
table = _table(tmp_path)
builder = table.new_batch_write_builder()
batches = []
class ObservedWriter(BatchTableWrite):
def write_arrow_batch(self, data, bucket=None):
batches.append(data)
return super().write_arrow_batch(data, bucket)
if override == 'subclass':
writer = ObservedWriter(table, builder.commit_user)
else:
writer = builder.new_write()
original = writer.write_arrow_batch
def observed(data, bucket=None):
batches.append(data)
return original(data, bucket)
writer.write_arrow_batch = observed
data = pa.table({'id': pa.array([1, 2], pa.int32()), 'value': ['one', 'two']})
getattr(writer, input_method)(data if input_method == 'write_arrow' else data.to_pandas())
assert len(batches) == 1
assert batches[0].to_pydict() == data.to_pydict()
commit = builder.new_commit()
try:
commit.commit(writer.prepare_commit())
finally:
writer.close()
commit.close()
read = table.new_read_builder()
assert read.new_read().to_arrow(read.new_scan().plan().splits()).to_pylist() == data.to_pylist()
def test_native_adapter_owns_only_one_backend(tmp_path):
core = Mock()
writer = NativeWrite(_table(tmp_path), 'job', None, False, core)
for name in ('_python_writer', '_new_python_writer', '_switch_to_python',
'file_store_write', 'row_key_extractor'):
assert not hasattr(writer, name)
writer.write_arrow_batch(pa.record_batch(
[pa.array([1], pa.int32()), ['one']], names=['id', 'value']))
assert writer._written
core.write_arrow.assert_called_once()
writer.close()
core.close.assert_called_once_with()
@pytest.mark.python_write
def test_invalid_blob_configuration_keeps_the_configured_consumer(tmp_path):
table = _table(tmp_path)
writer = table.new_batch_write_builder().new_write()
consumer = Mock()
factory = Mock()
try:
assert writer.with_blob_consumer(consumer) is writer
assert writer.with_blob_uri_reader_factory(factory) is writer
with pytest.raises(TypeError, match='blob_consumer must be callable'):
writer.with_blob_consumer(42)
with pytest.raises(TypeError, match='factory must provide callable create'):
writer.with_blob_uri_reader_factory(42)
assert writer._execution.file_store_write.blob_consumer is consumer
assert writer._execution.file_store_write.blob_uri_reader_factory is factory
assert writer.with_blob_consumer(None) is writer
assert writer.with_blob_uri_reader_factory(None) is writer
assert writer._execution.file_store_write.blob_consumer is None
assert writer._execution.file_store_write.blob_uri_reader_factory is None
finally:
writer.close()
@pytest.mark.parametrize('row_bridge', [False, True])
def test_native_uri_reader_factory_validation_precedes_core_configuration(tmp_path, row_bridge):
core = Mock()
writer = NativeWrite(_table(tmp_path), 'job', None, False, core)
factory = Mock()
if row_bridge:
writer._blob_rows = Mock()
try:
assert writer.with_blob_uri_reader_factory(factory) is writer
core.with_blob_uri_reader_factory.reset_mock()
with pytest.raises(TypeError, match='factory must provide callable create'):
writer.with_blob_uri_reader_factory(42)
assert writer._blob_uri_reader_factory is factory
if row_bridge:
assert writer._blob_rows.fallback is factory
core.with_blob_uri_reader_factory.assert_not_called()
finally:
writer.close()
@pytest.mark.parametrize('attribute', [
'file_store_write', 'row_key_extractor', 'table_pyarrow_schema',
'restore_snapshot_id', 'batch_committed',
])
def test_native_facade_does_not_expose_execution_state(tmp_path, attribute):
from pypaimon.write.write_dispatch import NativeTableWrite
core = Mock()
writer = NativeTableWrite(_table(tmp_path), 'job', None, False, core)
try:
with patch('pypaimon.execution.python.write.table_write.PythonTableWrite.__init__',
side_effect=AssertionError('attribute inspection selected Python')):
assert not hasattr(writer, attribute)
with pytest.raises(AttributeError):
getattr(writer, attribute)
core.close.assert_not_called()
assert writer._python_writer is None
finally:
writer.close()
core.close.assert_called_once_with()
def test_python_delete_matching_respects_independent_read_backend(tmp_path):
table = _table(tmp_path, {'row-tracking.enabled': 'true', 'data-evolution.enabled': 'true',
'deletion-vectors.enabled': 'true'})
builder = table.new_batch_write_builder()
writer, commit = builder.new_write(), builder.new_commit()
try:
writer.write_arrow(pa.table({'id': pa.array([1, 2], pa.int32()), 'value': ['one', 'two']}))
commit.commit(writer.prepare_commit())
finally:
writer.close()
commit.close()
table = table.copy({'write.native.enabled': 'true', 'read.native.enabled': 'false',
'scan.native-plan.enabled': 'false'})
updater = table.new_batch_write_builder().new_update()
predicate = table.new_read_builder().new_predicate_builder().equal('id', 2)
original_import = builtins.__import__
def no_native(name, *args, **kwargs):
if name.startswith(('pypaimon.execution.native', 'pypaimon_rust')):
raise AssertionError('Python delete matching selected Native execution: ' + name)
return original_import(name, *args, **kwargs)
with patch('builtins.__import__', side_effect=no_native):
from pypaimon.execution.python.write.table_update import PythonTableUpdate
assert PythonTableUpdate(updater)._matched_delete_row_ids(predicate) == [1]