blob: bc6f22aa317c0c39fd0d31fc7f9fcc19deac4d6f [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.
"""Rust plans index drops; Python retains commit and dry-run ownership."""
from types import SimpleNamespace
from unittest.mock import Mock, patch
import pytest
from pypaimon.globalindex.drop_global_index import GlobalIndexDropper
from pypaimon.index.index_file_handler import IndexFileHandler
from pypaimon.execution.native.table import (_native_table)
from pypaimon.execution.native.read.builder import (_predicate_to_native)
from pypaimon.execution.native.runtime import (native_runtime_available)
from pypaimon.tests import native_sorted_index_build_test as indexes
from pypaimon.write.table_commit import BatchTableCommit
rest_catalog = indexes.rest_catalog
native_drop = pytest.mark.skipif(
not native_runtime_available(),
reason='pypaimon-rust not installed')
@native_drop
def test_preparation_failure_does_not_retry_python_planning():
table = SimpleNamespace(
field_dict={'id': None},
options=SimpleNamespace(native_write_enabled=lambda: True, query_auth_enabled=False))
native = Mock()
native.with_index_columns.return_value = native
native.with_index_type.return_value = native
native.build.side_effect = OSError('metadata read failed')
factory = SimpleNamespace(supports_index_type=lambda kind: True)
with patch('pypaimon.execution.native.runtime.native_runtime_available', return_value=True), \
patch('pypaimon.execution.native.write.native_commit._rest_catalog_supported', return_value=True), \
patch('pypaimon.execution.native.table._native_table') as native_table, \
patch('pypaimon_rust.datafusion.GlobalIndexDropBuilder', factory, create=True), \
patch.object(IndexFileHandler, 'scan', side_effect=AssertionError('Python fallback')):
native_table.return_value.new_global_index_drop_builder.return_value = native
with pytest.raises(OSError, match='metadata read failed'):
GlobalIndexDropper(table, 'id').build()
native.build.assert_called_once_with()
native.execute.assert_not_called()
@pytest.mark.native_plan
@native_drop
@pytest.mark.parametrize('kind', ['btree', 'bitmap'])
@pytest.mark.parametrize('selection', ['all', 'dict', 'list', 'null', 'predicate'])
def test_prepare_and_dry_run_preserve_files_until_explicit_commit(rest_catalog, kind, selection):
catalog, _ = rest_catalog
table = indexes._create(catalog, partitioned=True)
indexes._append(table, indexes.ROWS)
table.create_global_index('name', index_type=kind)
table.create_global_index('id', index_type=kind)
entries = IndexFileHandler(table).scan(None)
all_files = [entry.index_file for entry in entries]
predicates = table.new_read_builder().new_predicate_builder()
kwargs = {
'all': {},
'dict': {'partitions': {'pt': 1}},
'list': {'partitions': [{'pt': 0}, {'pt': 1}]},
'null': {'partitions': {'pt': None}},
'predicate': {'partition_filter': predicates.equal('pt', 0)},
}[selection]
selected_partitions = {
'all': {0, 1, None}, 'dict': {1}, 'list': {0, 1}, 'null': {None}, 'predicate': {0},
}[selection]
expected = {entry.index_file.file_name for entry in entries
if entry.index_file.global_index_meta.index_field_id == table.field_dict['name'].id
and entry.partition.values[0] in selected_partitions}
before = table.snapshot_manager().get_latest_snapshot().id
with patch.object(IndexFileHandler, 'scan', side_effect=AssertionError('Python drop planning')):
messages = GlobalIndexDropper(table, 'name', index_type=kind, **kwargs).build()
with patch.object(BatchTableCommit, 'commit', side_effect=AssertionError('dry run committed')):
assert table.drop_global_index('name', index_type=kind, dry_run=True, **kwargs) == len(expected)
assert {entry.index_file.file_name for message in messages for entry in message.index_deletes} == expected
assert all(not message.new_files and not message.index_adds for message in messages)
assert table.snapshot_manager().get_latest_snapshot().id == before
assert {file.file_name for file in indexes._files(table)} == {file.file_name for file in all_files}
indexes._commit(table, messages)
assert {file.file_name for file in indexes._files(table)} == {
file.file_name for file in all_files if file.file_name not in expected}
# Old snapshots still refer to the removed metadata's files.
assert all(table.file_io.exists(indexes._path(table, file)) for file in all_files)
assert table.drop_global_index('name', index_type=kind, **kwargs) == 0
assert table.snapshot_manager().get_latest_snapshot().id == before + 1
@pytest.mark.native_plan
@native_drop
def test_native_drop_binding_conjunction_and_execute_dry_run(rest_catalog):
catalog, _ = rest_catalog
table = indexes._create(catalog, partitioned=True)
indexes._append(table, indexes.ROWS)
table.create_global_index('name')
before = table.snapshot_manager().get_latest_snapshot().id
predicates = table.new_read_builder().new_predicate_builder()
native = _native_table(table).new_global_index_drop_builder().with_index_column('name')
native.with_partition_filter(_predicate_to_native(predicates.equal('pt', 0)))
native.with_partition_filter(_predicate_to_native(predicates.equal('pt', 1)))
assert native.build() == []
assert native.execute() == 0
assert table.snapshot_manager().get_latest_snapshot().id == before
with pytest.raises(Exception, match='only partition keys'):
native.with_partition_filter(_predicate_to_native(predicates.equal('id', 1)))
native = _native_table(table).new_global_index_drop_builder().with_index_column('name')
files = indexes._files(table)
assert native.with_dry_run(True).execute() == len(files)
assert table.snapshot_manager().get_latest_snapshot().id == before
assert len(native.build()) > 0
assert native.with_dry_run(False).execute() == len(files)
assert indexes._files(table) == []
assert all(table.file_io.exists(indexes._path(table, file)) for file in files)
@pytest.mark.native_plan
@native_drop
def test_lost_drop_commit_response_preserves_index_files(rest_catalog):
catalog, _ = rest_catalog
table = indexes._create(catalog)
indexes._append(table, indexes.ROWS)
table.create_global_index('name')
files = indexes._files(table)
before = table.snapshot_manager().get_latest_snapshot().id
original = BatchTableCommit.commit
def publish_then_raise(commit, messages, *args, **kwargs):
original(commit, messages, *args, **kwargs)
raise OSError('commit response lost')
with patch.object(BatchTableCommit, 'commit', publish_then_raise):
with pytest.raises(OSError, match='commit response lost'):
table.drop_global_index('name')
assert table.snapshot_manager().get_latest_snapshot().id == before + 1
assert indexes._files(table) == []
assert all(table.file_io.exists(indexes._path(table, file)) for file in files)
@pytest.mark.native_plan
@native_drop
def test_empty_drop_validates_columns_and_publishes_nothing(rest_catalog):
catalog, _ = rest_catalog
table = indexes._create(catalog)
assert GlobalIndexDropper(table, 'name').build() == []
assert table.drop_global_index('name') == 0
assert table.drop_global_index('name', dry_run=True) == 0
assert table.snapshot_manager().get_latest_snapshot() is None
native = _native_table(table).new_global_index_drop_builder()
with pytest.raises(Exception, match='column is required'):
native.build()