blob: 93fffb9fe509a3df46b8487755450339b8af3913 [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 contracts and Native planning remain independent of Python execution."""
from pathlib import Path
import subprocess
import sys
import textwrap
def test_public_index_contracts_and_native_plans_do_not_load_python_execution():
script = r"""
import importlib.abc
import pickle
import sys
from contextlib import ExitStack
from tempfile import TemporaryDirectory
from types import SimpleNamespace
from unittest.mock import Mock, patch
class RejectPythonExecution(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname.startswith('pypaimon.execution.python'):
raise AssertionError('Native index planning imported Python execution: ' + fullname)
sys.meta_path.insert(0, RejectPythonExecution())
from pypaimon.catalog.catalog_environment import CatalogEnvironment
from pypaimon.common.file_io import FileIO
from pypaimon.common.identifier import Identifier
from pypaimon.common.predicate_builder import PredicateBuilder
from pypaimon.globalindex.global_index_meta import GlobalIndexMeta
from pypaimon.index.data_evolution_index_source_meta import DataEvolutionIndexSourceMeta
from pypaimon.index.deletion_vector_meta import DeletionVectorMeta
from pypaimon.index.index_file_meta import IndexFileMeta
from pypaimon.index.pk.primary_key_index_definition import PrimaryKeyIndexFamily
from pypaimon.index.pk.primary_key_index_definitions import PrimaryKeyIndexDefinitions
from pypaimon.index.pk.primary_key_index_source_file import PrimaryKeyIndexSourceFile
from pypaimon.index.pk.primary_key_index_source_meta import PrimaryKeyIndexSourceMeta
from pypaimon.schema.data_types import ArrayType, AtomicType, DataField
from pypaimon.schema.table_schema import TableSchema
from pypaimon.table.file_store_table import FileStoreTable
from pypaimon.table.source.search_types import PrimaryKeyFullTextScanPlan, PrimaryKeyVectorScanPlan
# These are transport contracts, so retaining their original class identity
# matters independently of where the two executors keep their internal state.
source = PrimaryKeyIndexSourceFile('data.parquet', 3)
metadata = (
PrimaryKeyIndexSourceMeta(1, [source]),
DataEvolutionIndexSourceMeta(7),
IndexFileMeta('btree', 'index', 17, 3,
{'data.parquet': DeletionVectorMeta('data.parquet', 4, 9, 1)},
global_index_meta=GlobalIndexMeta(0, 2, 0)),
)
transported = pickle.loads(pickle.dumps(metadata))
assert all(type(actual) is type(expected) for actual, expected in zip(transported, metadata))
assert transported[0].source_files == (source,)
assert transported[1].scan_snapshot_id == 7
assert transported[2].dv_ranges == metadata[2].dv_ranges
assert type(transported[2].global_index_meta) is GlobalIndexMeta
schema = TableSchema(fields=[
DataField(0, 'id', AtomicType('INT')),
DataField(1, 'embedding', ArrayType('ARRAY', AtomicType('FLOAT'))),
DataField(2, 'body', AtomicType('STRING')),
], primary_keys=['id'], options={
'bucket': '1', 'scan.native-plan.enabled': 'true', 'read.native.enabled': 'true',
'pk-btree.index.columns': 'id', 'pk-vector.index.columns': 'embedding',
'fields.embedding.pk-vector.index.type': 'ivf-flat',
'pk-full-text.index.columns': 'body',
})
definitions = PrimaryKeyIndexDefinitions.create(schema).definitions
assert [(value.column, value.field_id, value.family) for value in definitions] == [
('id', 0, PrimaryKeyIndexFamily.BTREE),
('embedding', 1, PrimaryKeyIndexFamily.VECTOR),
('body', 2, PrimaryKeyIndexFamily.FULL_TEXT),
]
with TemporaryDirectory() as directory, ExitStack() as stack:
table = FileStoreTable(FileIO.get(directory), Identifier('default', 'indexed'), directory,
schema, CatalogEnvironment(catalog_loader=object()))
predicate = PredicateBuilder(table.fields).greater_than('id', 0)
native_plan = SimpleNamespace(splits=lambda: [], snapshot_id=lambda: 7, stats=lambda: None)
core_scan = Mock()
core_scan.plan.return_value = native_plan
core_builder = Mock()
core_builder.with_filter.return_value = core_builder
core_builder.with_projection.return_value = core_builder
core_builder.new_scan.return_value = core_scan
stack.enter_context(patch('pypaimon.execution.native.runtime.native_runtime_available', return_value=True))
stack.enter_context(patch('pypaimon.execution.native.table._resolved_schema_file_io_options', return_value={}))
stack.enter_context(patch('pypaimon.execution.native.read.builder._native_read_builder',
return_value=core_builder))
scan = table.new_read_builder().with_filter(predicate).new_scan()
plan = scan.plan()
assert plan.snapshot_id == 7 and plan.splits() == []
assert scan._file_scanner is None
core_builder.with_filter.assert_called_once_with(
{'method': 'greaterThan', 'field': 'id', 'literals': [0]})
core_scan.plan.assert_called_once_with()
# Use the same Core Scan/Plan/Read protocol as the Native search adapters.
search_plan = SimpleNamespace(snapshot=lambda: None, splits=lambda: [])
result = SimpleNamespace(snapshot_id=lambda: None, positions=lambda: [], splits=lambda: [])
reader = SimpleNamespace(read_plan=Mock(return_value=result))
search_builder = Mock()
search_builder.with_limit.return_value = search_builder
search_builder.new_vector_search_scan.return_value.scan.return_value = search_plan
search_builder.new_full_text_scan.return_value.scan.return_value = search_plan
search_builder.new_vector_search_read.return_value = reader
search_builder.new_full_text_read.return_value = reader
native_table = Mock()
native_table.new_vector_search_builder.return_value = search_builder
native_table.new_full_text_search_builder.return_value = search_builder
for module in ('native_vector_search', 'native_full_text_search'):
prefix = 'pypaimon.execution.native.read.' + module
stack.enter_context(patch(prefix + '.native_runtime_available', return_value=True))
stack.enter_context(patch(prefix + '._native_table', return_value=native_table))
stack.enter_context(patch('pypaimon.execution.native.read.native_vector_search._catalog_metastore',
return_value='rest'))
stack.enter_context(patch('pypaimon.execution.native.read.native_full_text_search._rest_catalog_supported',
return_value=True))
wire_plan = object()
convert = stack.enter_context(patch(
'pypaimon.execution.native.read.native_search_plan._plan_to_native', return_value=wire_plan))
for family in ('vector', 'text'):
if family == 'vector':
builder = table.new_vector_search_builder().with_vector_column('embedding').with_limit(2)
builder.with_query_vector([1., 0.])
scan, read = builder.new_vector_search_scan(), builder.new_vector_search_read()
expected_plan = PrimaryKeyVectorScanPlan
else:
builder = table.new_full_text_search_builder().with_query('body', 'needle').with_limit(2)
scan, read = builder.new_full_text_scan(), builder.new_full_text_read()
expected_plan = PrimaryKeyFullTextScanPlan
plan = scan.scan()
assert type(plan) is expected_plan and plan.splits() == []
answer = read.read_plan(plan)
assert answer.snapshot_id == 0 and answer.splits == ()
assert scan._python_executor is None and read._python_executor is None
assert convert.call_args[0][0] is plan
reader.read_plan.assert_called_with(wire_plan)
search_builder.with_vector_column.assert_called_with('embedding')
search_builder.with_query.assert_called_with('body', 'needle')
failure = OSError('Core query failed')
reader.read_plan.side_effect = failure
try:
read.read_plan(plan)
except OSError as error:
assert error is failure
else:
raise AssertionError('Native query failure was hidden')
assert read._python_executor is None
assert not any(name.startswith('pypaimon.execution.python') for name in sys.modules)
"""
result = subprocess.run(
[sys.executable, '-c', textwrap.dedent(script)],
cwd=str(Path(__file__).resolve().parents[2]),
stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True,
)
assert result.returncode == 0, result.stdout + result.stderr