blob: b687dafca4350b876e106ec832b83a69899aec47 [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.
"""Shared index contracts and Native builds do not load Python index executors."""
from pathlib import Path
import subprocess
import sys
import textwrap
import pytest
def test_native_index_read_build_and_drop_do_not_import_python_index_execution():
script = r"""
import importlib.abc
import sys
from types import ModuleType, SimpleNamespace
from unittest.mock import Mock, patch
class RejectPythonIndexes(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname == 'ray' or fullname.startswith('pypaimon.ray'):
raise AssertionError('Ray integration imported for a local index: ' + fullname)
if fullname.startswith('pypaimon.execution.python'):
raise AssertionError('Python index execution imported: ' + fullname)
if fullname in ('paimon_vindex', 'paimon_ftindex', 'lumina'):
raise AssertionError('Python index SDK imported: ' + fullname)
sys.meta_path.insert(0, RejectPythonIndexes())
from pypaimon.common.options.core_options import CoreOptions
from pypaimon.common.options.options import Options
from pypaimon.globalindex import GlobalIndexBuilder, GlobalIndexDropper
from pypaimon.globalindex.global_index_reader import GlobalIndexReader
from pypaimon.globalindex.global_index_result import GlobalIndexResult
from pypaimon.globalindex.global_index_meta import GlobalIndexMeta
from pypaimon.globalindex.vindex_options import VINDEX_IDENTIFIERS
from pypaimon.schema.data_types import ArrayType, AtomicType, DataField
from pypaimon.execution.native.write import native_index_build, native_index_drop
runtime = ModuleType('pypaimon_rust')
runtime.__path__ = []
ffi = ModuleType('pypaimon_rust.datafusion')
ffi.GlobalIndexBuildBuilder = SimpleNamespace(supports_index_type=lambda kind: True)
ffi.GlobalIndexDropBuilder = SimpleNamespace(supports_index_type=lambda kind: True)
sys.modules['pypaimon_rust'] = runtime
sys.modules['pypaimon_rust.datafusion'] = ffi
native = Mock()
for name in ('with_index_column', 'with_index_columns', 'with_index_type', 'with_options'):
getattr(native, name).return_value = native
native.build.return_value = []
native_table = Mock()
native_table.new_global_index_build_builder.return_value = native
native_table.new_global_index_drop_builder.return_value = native
options = CoreOptions(Options({
'bucket': '-1', 'row-tracking.enabled': 'true', 'write.native.enabled': 'true',
}))
table = SimpleNamespace(
options=options, identifier='db.table', is_primary_key_table=False,
field_dict={'embedding': DataField(0, 'embedding', ArrayType('ARRAY', AtomicType('FLOAT')))},
)
def copy_table(values):
copied = SimpleNamespace(**vars(table))
copied.options = CoreOptions(Options(dict(table.options.options.to_map(), **values)))
return copied
table.copy_without_time_travel = copy_table
# An empty Native result is a successful build/drop, never a Python fallback.
with patch.object(native_index_build, 'native_runtime_available', return_value=True), \
patch.object(native_index_build, '_rest_catalog_supported', return_value=True), \
patch.object(native_index_build, '_native_table', return_value=native_table), \
patch.object(native_index_build, 'from_native_commit_messages', side_effect=lambda table, messages: messages), \
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', return_value=native_table), \
patch('pypaimon.execution.native.write.native_commit.from_native_commit_messages',
side_effect=lambda table, messages: messages):
for kind in ('btree', 'bitmap', 'full-text') + VINDEX_IDENTIFIERS:
assert GlobalIndexBuilder(table, 'embedding', index_type=kind).build() == []
assert GlobalIndexDropper(table, 'embedding', index_type=kind).build() == []
assert native.build.call_count == 2 * (3 + len(VINDEX_IDENTIFIERS))
# The low-level vindex API also selects Rust before opening a Python SDK reader.
import io
from pypaimon.globalindex.global_index_meta import GlobalIndexIOMeta
from pypaimon.globalindex.vector_search import VectorSearch
from pypaimon.globalindex.batch_vector_search import BatchVectorSearch
from pypaimon.globalindex.vindex_reader import VindexVectorGlobalIndexReader
from pypaimon.execution.native.read import vindex_reader
reader_options = {
'vindex.read.parallelism': 1,
'global-index.ivf-pq.index-column': 'embedding',
'pk-vector.index.columns': 'embedding',
'fields.embedding.pk-vector.index.type': 'ivf-pq',
'fields.embedding.index-options': '{"nlist":64}',
'opaque.enabled': True,
}
wire_options = dict(reader_options, **{'vindex.read.parallelism': '1', 'opaque.enabled': 'true'})
query_options = {'ivf.nprobe': 3, 'fields.embedding.index-options': '{"refine_factor":4}',
'index_type': 'ignored', 'opaque.query.option': 'preserved'}
class RustReader:
failure_phase = None
def __init__(self, input_, file_path, file_size, options):
assert input_.pread_many([(0, 1)]) == [b'x']
assert (file_path, file_size, options) == ('/indexes/file', 1, wire_options)
if self.failure_phase == 'open':
raise failure
def metric(self):
if self.failure_phase == 'metric':
raise failure
return 'inner_product'
def search(self, vectors, limit, filter_bytes=None, options=None):
assert limit == 2 and filter_bytes is None
assert options == dict(query_options, **{'ivf.nprobe': '3'})
if self.failure_phase == 'search':
raise failure
return [{7: .5} for _ in vectors]
def close(self):
pass
with patch.object(vindex_reader, '_native_reader_type', return_value=RustReader):
for phase in (None, 'open', 'metric', 'search'):
RustReader.failure_phase = phase
failure = OSError('Native ' + str(phase) + ' failed')
stream = io.BytesIO(b'x')
reader = VindexVectorGlobalIndexReader(
SimpleNamespace(new_input_stream=lambda path: stream),
'/indexes', [GlobalIndexIOMeta('file', 1)], reader_options)
try:
if phase is None:
assert reader.vector_metric() == 'inner_product'
result = reader.visit_vector_search(VectorSearch([1., 2.], 2, 'embedding',
options=query_options)).result()
assert list(result.results()) == [7] and result.score_getter()(7) == .5
results = reader.visit_batch_vector_search(BatchVectorSearch(
[[1., 2.], [2., 1.]], 2, 'embedding', options=query_options)).result()
assert [list(result.results()) for result in results] == [[7], [7]]
else:
try:
if phase == 'search':
reader.visit_vector_search(VectorSearch([1., 2.], 2, 'embedding',
options=query_options))
else:
reader.vector_metric()
except OSError as error:
assert error is failure
else:
raise AssertionError('Native error was hidden')
finally:
reader.close()
reader.close()
assert stream.closed
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
def test_local_python_index_build_does_not_import_ray_integration():
script = r"""
import importlib.abc
import sys
from types import SimpleNamespace
from unittest.mock import Mock
class RejectRay(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname == 'ray' or fullname.startswith('pypaimon.ray'):
raise AssertionError('Ray integration imported for a local index: ' + fullname)
sys.meta_path.insert(0, RejectRay())
from pypaimon.common.options.core_options import CoreOptions
from pypaimon.common.options.options import Options
from pypaimon.globalindex.create_global_index import GlobalIndexBuilder
from pypaimon.globalindex.vindex_options import VINDEX_IDENTIFIERS
from pypaimon.schema.data_types import ArrayType, AtomicType, DataField
read_builder = Mock()
read_builder.new_scan.return_value.plan_all.return_value.splits.return_value = []
table = SimpleNamespace(
options=CoreOptions(Options({
'bucket': '-1', 'row-tracking.enabled': 'true', 'write.native.enabled': 'false',
})),
identifier='db.table', is_primary_key_table=False,
field_dict={'embedding': DataField(0, 'embedding', ArrayType('ARRAY', AtomicType('FLOAT')))},
new_read_builder=lambda: read_builder,
)
for kind in ('btree', 'bitmap', 'full-text') + VINDEX_IDENTIFIERS:
builder = GlobalIndexBuilder(table, 'embedding', index_type=kind)
assert builder.build() == []
try:
builder.build(execution='local', concurrency=1)
except ValueError as error:
assert "execution='ray'" in str(error)
else:
raise AssertionError('Local index build accepted Ray-only options')
assert not any(name == 'ray' or name.startswith('pypaimon.ray') 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
@pytest.mark.skipif(sys.version_info < (3, 7), reason='Python 3.6 retains eager package exports')
def test_public_index_exports_are_backend_independent_contracts():
script = r"""
import importlib.abc
import pickle
import sys
class RejectExecution(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname == 'pypaimon.execution' or fullname.startswith('pypaimon.execution.'):
raise AssertionError('Public index export imported execution: ' + fullname)
sys.meta_path.insert(0, RejectExecution())
import pypaimon.globalindex as indexes
for name in indexes.__all__:
exported = getattr(indexes, name)
# Existing public import paths still resolve in transported references.
reference = ('cpypaimon.globalindex\n%s\n.' % name).encode('ascii')
assert pickle.loads(reference) is exported
for name in ('GlobalIndexEvaluator', 'DataEvolutionGlobalIndexScanner',
'KeySerializer', 'MemorySliceInput', 'OffsetGlobalIndexReader',
'SortedFileGlobalIndexReader', 'SortedFileMetaSelector'):
assert name not in indexes.__all__ and not hasattr(indexes, name), name
assert not any(name == 'pypaimon.execution' or name.startswith('pypaimon.execution.')
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