blob: f69dc9cbef9b8a96fbf8cfeb442960e696ea8ed5 [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.
"""A catalog-loaded schema retains its authorization after ordinary evolution."""
from contextlib import ExitStack
from unittest.mock import Mock, patch
import pyarrow as pa
import pytest
from pypaimon.execution.native.runtime import (native_runtime_available)
from pypaimon.schema.data_types import AtomicType
from pypaimon.schema.schema_change import SchemaChange
from pypaimon.table.data_evolution_merge_into import WhenMatched
from pypaimon.tests import native_plan_rest_test
from pypaimon.tests.native_authorized_update_test import (
_ROWS, _SCHEMA, _expected, _files, _native_only, _operate, _restricted, _rows, _table,
)
from pypaimon.write.write_dispatch import NativeTableWrite
pytestmark = [pytest.mark.python_write, pytest.mark.python_commit]
rest_catalog = native_plan_rest_test.rest_catalog
_ENGINES = [False, pytest.param(True, marks=[pytest.mark.native_plan, pytest.mark.skipif(
not native_runtime_available(), reason='pypaimon-rust not installed')])]
def _evolved(catalog, native):
loaded = _table(catalog, False)
empty = loaded._copy_with_snapshot(None)
catalog.alter_table('default.t', [SchemaChange.add_column('extra', AtomicType('INT'))])
latest = catalog.get_table('default.t')
assert loaded.table_schema.id != latest.table_schema.id
options = {'write.native.enabled': str(native).lower(), 'commit.native.enabled': str(native).lower()}
return loaded.copy(options), empty.copy(options), latest
def _commit(table, messages, native):
commit = table.new_batch_write_builder().new_commit()
try:
guard = (patch.object(commit.file_store_commit, 'commit', side_effect=AssertionError('Python commit fallback'))
if native else ExitStack())
with guard:
commit.commit(messages)
finally:
commit.close()
@pytest.mark.parametrize('native', _ENGINES)
@pytest.mark.parametrize('operation', ['row-id', 'predicate', 'upsert', 'row-delete', 'shard', 'merge'])
def test_loaded_schema_updates_survive_later_catalog_schema(rest_catalog, native, operation):
catalog, server = rest_catalog
loaded, _, latest = _evolved(catalog, native)
builder = loaded.new_batch_write_builder()
with patch.object(server, '_table_query_auth_handle', wraps=server._table_query_auth_handle) as authorize:
with ExitStack() as stack:
_native_only(stack, native)
messages = _operate(builder.new_update(), operation)
assert messages
assert authorize.call_count >= 1
# The original, non-time-traveled handle also remains a valid committer.
_commit(loaded, messages, native)
assert _rows(latest) == [dict(row, extra=None) for row in _expected(operation)]
assert latest.snapshot_manager().get_latest_snapshot().schema_id == latest.table_schema.id
@pytest.mark.parametrize('native', _ENGINES)
def test_loaded_schema_append_keeps_original_fields_and_current_snapshot_schema(rest_catalog, native):
catalog, _ = rest_catalog
loaded, _, latest = _evolved(catalog, native)
writer = loaded.new_batch_write_builder().new_write()
try:
assert isinstance(writer, NativeTableWrite) == native
writer.write_arrow(pa.Table.from_pylist([dict(id=4, value=40, name='d')], schema=_SCHEMA))
messages = writer.prepare_commit()
assert all(file.schema_id == loaded.table_schema.id for message in messages for file in message.new_files)
_commit(loaded, messages, native)
finally:
writer.close()
assert _rows(latest) == [dict(row, extra=None) for row in _ROWS + [dict(id=4, value=40, name='d')]]
assert latest.snapshot_manager().get_latest_snapshot().schema_id == latest.table_schema.id
@pytest.mark.parametrize('native', _ENGINES)
@pytest.mark.parametrize('operation', ['predicate', 'shard', 'merge'])
def test_loaded_empty_view_stays_empty_after_catalog_schema_evolution(rest_catalog, native, operation):
catalog, server = rest_catalog
_, captured, latest = _evolved(catalog, native)
update = captured.new_batch_write_builder().new_update()
assignment = Mock(side_effect=AssertionError('A captured empty view evaluated an assignment'))
with patch.object(server, '_table_query_auth_handle', wraps=server._table_query_auth_handle) as authorize:
with ExitStack() as stack:
_native_only(stack, native)
if operation == 'merge':
messages = update.merge_into(pa.Table.from_pylist(_ROWS[:1], schema=_SCHEMA), on=['id'],
when_matched=[WhenMatched.update({'value': assignment})])
else:
messages = _operate(update, operation, assignment=assignment)
assert authorize.call_count >= 1
assert messages == []
assignment.assert_not_called()
assert _rows(latest) == [dict(row, extra=None) for row in _ROWS]
@pytest.mark.parametrize('native', _ENGINES)
@pytest.mark.parametrize('policy', ['denied', 'filter', 'mask'])
def test_loaded_schema_still_requires_a_fresh_unrestricted_grant(rest_catalog, native, policy):
catalog, server = rest_catalog
loaded, _, latest = _evolved(catalog, native)
assignment = Mock(side_effect=AssertionError('A restricted update evaluated its assignment'))
before = _files(latest)
with _restricted(server, loaded, policy) as authorize:
with ExitStack() as stack:
_native_only(stack, native)
with pytest.raises(Exception, match='[Pp]ermission|denied|unrestricted|restrict|[Mm]ask|query.auth'):
_operate(loaded.new_batch_write_builder().new_update(), 'predicate', assignment=assignment)
assert authorize.call_count >= 1
assignment.assert_not_called()
assert _files(latest) == before