blob: 6f4b928dbb2e0aa5a8f476ef369acc67e743e238 [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.
"""Public factories retain captured inputs and keep standalone helpers independent."""
import subprocess
import sys
from unittest.mock import Mock, patch
import pyarrow as pa
import pytest
from pypaimon import CatalogFactory, Schema
from pypaimon.table.source.batch_vector_search_builder import BatchVectorSearchBuilderImpl
from pypaimon.table.source.full_text_search_builder import FullTextSearchBuilderImpl
from pypaimon.table.source.vector_search_builder import VectorSearchBuilderImpl
def _search_table(path, native):
catalog = CatalogFactory.create({'warehouse': str(path)})
catalog.create_database('default', True)
catalog.create_table('default.search', Schema.from_pyarrow_schema(
pa.schema([('embedding', pa.list_(pa.float32())), ('body', pa.string())]),
options={'row-tracking.enabled': 'true', 'data-evolution.enabled': 'true',
'read.native.enabled': str(native).lower()}), False)
return catalog.get_table('default.search')
@pytest.mark.parametrize('kind', ['vector', 'batch-vector', 'full-text'])
@pytest.mark.parametrize('phase', ['scan', 'read'])
@pytest.mark.parametrize('native', [False, True])
def test_search_factories_keep_captured_configuration(tmp_path, kind, phase, native):
from pypaimon.table.source.search_dispatch import python_search_reader
table = _search_table(tmp_path, native)
query = [1.0, 2.0]
if kind == 'vector':
builder = (VectorSearchBuilderImpl(table).with_vector_column('embedding')
.with_query_vector(query).with_limit(2).with_option('ivf.nprobe', '1'))
elif kind == 'batch-vector':
builder = (BatchVectorSearchBuilderImpl(table).with_vector_column('embedding')
.with_query_vectors([query]).with_limit(2).with_option('ivf.nprobe', '1'))
else:
builder = FullTextSearchBuilderImpl(table).with_query('body', 'term').with_limit(2)
family = 'full_text' if kind == 'full-text' else 'vector'
python_name = ('new_batch_vector_read' if kind == 'batch-vector' and phase == 'read'
else 'new_' + family + '_' + phase)
native_name = 'native_' + family + '_' + phase
python_execution, native_execution = Mock(), Mock()
with patch('pypaimon.execution.python.read.search.factories.' + python_name,
return_value=python_execution) as python_factory, \
patch('pypaimon.execution.native.read.native_search_plan.' + native_name,
return_value=native_execution) as native_factory:
if phase == 'scan':
selected = (builder.new_full_text_scan() if kind == 'full-text'
else builder.new_vector_search_scan())
else:
selected = (builder.new_full_text_read() if kind == 'full-text'
else builder.new_batch_vector_search_read() if kind == 'batch-vector'
else builder.new_vector_search_read())
if native:
python_factory.assert_not_called()
native_factory.assert_called_once()
else:
native_factory.assert_not_called()
builder.with_limit(9)
if kind == 'full-text':
builder.with_query('body', 'later')
else:
query[0] = 99.0
builder.with_option('ivf.nprobe', '9')
if phase == 'scan':
native_execution.available.return_value = False
assert selected.scan() is python_execution.scan.return_value
else:
assert python_search_reader(selected) is python_execution
python_factory.assert_called_once()
captured = python_factory.call_args[0][0]
assert captured is not builder and captured._limit == 2
if kind == 'full-text':
assert captured._query == 'term'
else:
assert captured._options == {'ivf.nprobe': '1'}
if phase == 'read':
assert (captured._query_vector if kind == 'vector' else captured._query_vectors[0]) == [1.0, 2.0]
def test_native_search_factories_do_not_import_python_execution(tmp_path):
source = r"""
import importlib.abc
import sys
from unittest.mock import Mock, patch
import pyarrow as pa
class RejectPythonExecution(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname.startswith('pypaimon.execution.python'):
raise AssertionError('Native factory imported Python execution: ' + fullname)
sys.meta_path.insert(0, RejectPythonExecution())
from pypaimon import CatalogFactory, Schema
from pypaimon.table.source.vector_search_builder import VectorSearchBuilderImpl
from pypaimon.table.source.batch_vector_search_builder import BatchVectorSearchBuilderImpl
from pypaimon.table.source.full_text_search_builder import FullTextSearchBuilderImpl
from pypaimon.execution.native.read import native_search_plan
catalog = CatalogFactory.create({'warehouse': sys.argv[1]})
catalog.create_database('default', True)
catalog.create_table('default.search', Schema.from_pyarrow_schema(
pa.schema([('embedding', pa.list_(pa.float32())), ('body', pa.string())]),
options={'row-tracking.enabled': 'true', 'data-evolution.enabled': 'true',
'read.native.enabled': 'true'}), False)
table = catalog.get_table('default.search')
vector = (VectorSearchBuilderImpl(table).with_vector_column('embedding')
.with_query_vector([1., 2.]).with_limit(2))
batch = (BatchVectorSearchBuilderImpl(table).with_vector_column('embedding')
.with_query_vectors([[1., 2.]]).with_limit(2))
text = FullTextSearchBuilderImpl(table).with_query('body', 'term').with_limit(2)
native = Mock()
native.available.return_value = True
expected = object()
for name in ['scan', 'read_plan', 'read_batch_plan']:
getattr(native, name).return_value = expected
constructors = {name: Mock(return_value=native) for name in [
'native_vector_scan', 'native_vector_read', 'native_full_text_scan', 'native_full_text_read']}
with patch.multiple(native_search_plan, **constructors):
operations = [
lambda: vector.new_vector_search_scan().scan(),
lambda: vector.new_vector_search_read().read_plan(object()),
lambda: batch.new_vector_search_scan().scan(),
lambda: batch.new_batch_vector_search_read().read_batch_plan(object()),
lambda: text.new_full_text_scan().scan(),
lambda: text.new_full_text_read().read_plan(object()),
]
for operation in operations:
assert operation() is expected
failure = OSError('selected native executor failed')
for name in ['scan', 'read_plan', 'read_batch_plan']:
getattr(native, name).side_effect = failure
for operation in operations:
try:
operation()
except OSError as error:
assert error is failure
else:
raise AssertionError('Native failure was hidden')
assert not any(name.startswith('pypaimon.execution.python') for name in sys.modules)
"""
result = subprocess.run(
[sys.executable, '-c', source, str(tmp_path)], stdout=subprocess.PIPE,
stderr=subprocess.PIPE, universal_newlines=True, timeout=45)
assert result.returncode == 0, result.stdout + result.stderr
@pytest.mark.parametrize('bucket,primary_keys,expected', [
('2', ['id', 'p'], 'FixedBucketRowKeyExtractor'),
('-1', [], 'UnawareBucketRowKeyExtractor'),
('-2', ['id', 'p'], 'PostponeBucketRowKeyExtractor'),
('-1', ['id', 'p'], 'DynamicBucketRowKeyExtractor'),
])
def test_python_row_key_factory_preserves_per_row_contract(tmp_path, bucket, primary_keys, expected):
from pypaimon.execution.python.write.row_key_extractor import create_row_key_extractor
catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
catalog.create_database('default', True)
arrow_schema = pa.schema([('id', pa.int32()), ('p', pa.string())])
catalog.create_table('default.keys', Schema.from_pyarrow_schema(
arrow_schema, primary_keys=primary_keys, partition_keys=['p'], options={
'bucket': bucket, 'write.native.enabled': 'true', 'read.native.enabled': 'false',
'scan.native-plan.enabled': 'false', 'commit.native.enabled': 'false'}), False)
table = catalog.get_table('default.keys')
rows = pa.record_batch([pa.array([1, 2], pa.int32()), pa.array(['a', 'a'])], schema=arrow_schema)
with patch('pypaimon.execution.native.runtime.native_runtime_available',
side_effect=AssertionError('Per-row helper rediscovered the Native writer')):
extractor = create_row_key_extractor(table, ignore_existing=True)
partitions, buckets = extractor.extract_partition_bucket_batch(rows)
assert type(extractor).__name__ == expected
assert partitions == [('a',), ('a',)]
if bucket == '2':
assert all(0 <= value < 2 for value in buckets)
else:
assert buckets == [-2 if bucket == '-2' else 0] * 2
def test_shared_merge_condition_helpers_do_not_load_execution():
if sys.version_info < (3, 10):
pytest.skip('Python MERGE conditions require Python 3.10')
pytest.importorskip('datafusion')
source = r'''
import importlib.abc
import sys
import pyarrow as pa
class RejectExecution(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path=None, target=None):
if fullname.startswith(('pypaimon.execution', 'pypaimon_rust')):
raise AssertionError('Shared condition helper selected execution: ' + fullname)
sys.meta_path.insert(0, RejectExecution())
from pypaimon.ray.merge_condition import apply_condition, filter_batch, rewrite_condition
rows = pa.table({'s.id': pa.chunked_array([[1, 2], [3]]), 't.id': [2, 1, 2]})
assert filter_batch(rows, 's.id > t.id').column('s.id').to_pylist() == [2, 3]
schema = pa.schema([('s.id', pa.int64())])
empty = apply_condition(rows, rewrite_condition('s.id < 0'), schema)
assert empty.num_rows == 0 and empty.schema == schema
assert not any(name.startswith(('pypaimon.execution', 'pypaimon_rust')) for name in sys.modules)
'''
result = subprocess.run([sys.executable, '-c', source], stdout=subprocess.PIPE,
stderr=subprocess.PIPE, universal_newlines=True, timeout=30)
assert result.returncode == 0, result.stdout + result.stderr