blob: cb36f8d50f9e43412f31a50110406dfb6a21c97b [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.
"""Index update policy uses the identities in a prepared file's own schema."""
from dataclasses import replace
from unittest.mock import patch
import pyarrow as pa
import pytest
from pypaimon import Schema
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.tests.global_index_update_action_test import _index_entry
from pypaimon.write.commit_message import CommitMessage
from pypaimon.execution.python.write.global_index_update_checker import scan_global_index_entries
pytestmark = [pytest.mark.python_write, pytest.mark.python_commit]
_ENGINES = [False, pytest.param(True, marks=[pytest.mark.native_plan, pytest.mark.skipif(
not native_runtime_available(), reason='pypaimon-rust not installed')])]
def _commit(table, messages, native=False):
commit = table.new_batch_write_builder().new_commit()
try:
if native:
with patch('pypaimon.execution.python.write.file_store_commit.FileStoreCommit.commit',
side_effect=AssertionError('Python fallback')):
commit.commit(messages)
else:
commit.commit(messages)
finally:
commit.close()
@pytest.mark.parametrize('native', _ENGINES)
@pytest.mark.parametrize('evolution', ['rename', 'drop_readd', 'complete_row'])
@pytest.mark.parametrize('action', ['THROW_ERROR', 'DROP_PARTITION_INDEX', 'IGNORE'])
def test_commit_resolves_updated_index_columns_by_file_schema(native_rest_catalog, native, evolution, action):
catalog = native_rest_catalog
schema = pa.schema([('id', pa.int32()), ('value', pa.int32())])
catalog.create_table('default.identity', Schema.from_pyarrow_schema(schema, options={
'data-evolution.enabled': 'true', 'row-tracking.enabled': 'true',
'write.native.enabled': 'false', 'commit.native.enabled': 'false',
'read.native.enabled': 'false', 'scan.native-plan.enabled': 'false',
'global-index.column-update-action': action,
}), False)
original = catalog.get_table('default.identity')
writer = original.new_batch_write_builder().new_write()
try:
writer.write_arrow(pa.table({'id': [1], 'value': [10]}, schema=schema))
_commit(original, writer.prepare_commit())
finally:
writer.close()
if evolution == 'rename':
catalog.alter_table('default.identity', [SchemaChange.rename_column('value', 'renamed')])
elif evolution == 'drop_readd':
catalog.alter_table('default.identity', [
SchemaChange.drop_column('value'), SchemaChange.add_column('value', AtomicType('INT'))])
latest = catalog.get_table('default.identity')
index_field = latest.fields[1]
_commit(latest, [CommitMessage((), 0, [], index_adds=[_index_entry('identity.index', (), index_field.id)])])
before = latest.snapshot_manager().get_latest_snapshot()
# Prepare externally with IGNORE so the committer, rather than the updater,
# has to enforce its current policy against the latest index manifest.
source = (latest if evolution == 'complete_row' else latest.copy({'scan.snapshot-id': '1'})).copy({
'global-index.column-update-action': 'IGNORE'})
columns = ['id', 'value'] if evolution == 'complete_row' else ['value']
values = {'_ROW_ID': [0], 'value': [11]}
if evolution == 'complete_row':
values['id'] = [1]
messages = source.new_batch_write_builder().new_update().new_update_by_row_id().update_columns(
pa.table(values), columns)
assert messages
if evolution == 'complete_row':
# Java/Native may omit an explicit full column list. The physical
# file is identical; commit policy must handle either representation.
assert messages[0].new_files[0].write_cols in (None, columns)
messages[0].new_files[0].write_cols = None
else:
assert messages[0].new_files[0].schema_id == original.table_schema.id
target = latest.copy({'commit.native.enabled': str(native).lower()})
if action == 'THROW_ERROR' and evolution != 'drop_readd':
with pytest.raises(Exception, match='globally indexed columns'):
_commit(target, messages, native)
assert latest.snapshot_manager().get_latest_snapshot().id == before.id
else:
_commit(target, messages, native)
after = latest.snapshot_manager().get_latest_snapshot()
assert after.id == before.id + 1
indexes = scan_global_index_entries(latest, after)
expected = [] if action == 'DROP_PARTITION_INDEX' and evolution != 'drop_readd' else ['identity.index']
assert [entry.index_file.file_name for entry in indexes] == expected
read = latest.new_read_builder()
rows = read.new_read().to_arrow(read.new_scan().plan().splits()).to_pydict()
assert rows == {'id': [1], index_field.name: [None if evolution == 'drop_readd' else 11]}
@pytest.mark.parametrize('native', _ENGINES)
@pytest.mark.parametrize('action', ['THROW_ERROR', 'DROP_PARTITION_INDEX', 'IGNORE'])
@pytest.mark.parametrize('deleted', ['affected', 'other', 'both'])
def test_explicit_index_deletion_does_not_exempt_other_indexes(native_rest_catalog, native, action, deleted):
catalog = native_rest_catalog
schema = pa.schema([('id', pa.int32()), ('value', pa.int32())])
catalog.create_table('default.index_deletes', Schema.from_pyarrow_schema(schema, options={
'data-evolution.enabled': 'true', 'row-tracking.enabled': 'true',
'write.native.enabled': 'false', 'commit.native.enabled': 'false',
'read.native.enabled': 'false', 'scan.native-plan.enabled': 'false',
'global-index.column-update-action': action,
}), False)
table = catalog.get_table('default.index_deletes')
writer = table.new_batch_write_builder().new_write()
try:
writer.write_arrow(pa.table({'id': [1], 'value': [10]}, schema=schema))
_commit(table, writer.prepare_commit())
finally:
writer.close()
affected = _index_entry('affected.index', (), table.fields[1].id)
other = _index_entry('other.index', (), table.fields[0].id)
_commit(table, [CommitMessage((), 0, [], index_adds=[affected, other])])
before = table.snapshot_manager().get_latest_snapshot()
source = table.copy({'global-index.column-update-action': 'IGNORE'})
messages = source.new_batch_write_builder().new_update().new_update_by_row_id().update_columns(
pa.table({'_ROW_ID': [0], 'value': [11]}), ['value'])
deletions = [affected] if deleted == 'affected' else [other] if deleted == 'other' else [affected, other]
messages.append(CommitMessage((), 0, [], index_deletes=[replace(entry, kind=1) for entry in deletions]))
target = table.copy({'commit.native.enabled': str(native).lower()})
if action == 'THROW_ERROR' and deleted == 'other':
with pytest.raises(Exception, match='globally indexed columns'):
_commit(target, messages, native)
assert table.snapshot_manager().get_latest_snapshot().id == before.id
expected_indexes, expected_value = ['affected.index', 'other.index'], 10
else:
_commit(target, messages, native)
expected_indexes = ['other.index'] if deleted == 'affected' else (
['affected.index'] if deleted == 'other' and action == 'IGNORE' else [])
expected_value = 11
snapshot = table.snapshot_manager().get_latest_snapshot()
actual_indexes = sorted(entry.index_file.file_name for entry in scan_global_index_entries(table, snapshot))
assert actual_indexes == expected_indexes
read = table.new_read_builder()
assert read.new_read().to_arrow(read.new_scan().plan().splits()).to_pydict() == {
'id': [1], 'value': [expected_value]}