blob: a462c34063d295e753321c1ba99ef5d619c3a8e0 [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.
"""Python readers keep their internal dependency reads on the same backend."""
import builtins
from contextlib import contextmanager
from unittest.mock import patch
import pyarrow as pa
import pytest
from pypaimon import CatalogFactory, Schema
from pypaimon.execution.python.read.search import global_index_live_row_filter
from pypaimon.execution.python.read.search.full_text_read import DataEvolutionFullTextRead
from pypaimon.execution.python.read.search.global_index_row_filter import matching_rows
from pypaimon.execution.python.read.search.primary_key_full_text_read import PrimaryKeyFullTextRead
from pypaimon.execution.python.read.search.primary_key_vector_read import PrimaryKeyVectorRead
from pypaimon.execution.python.read.search.primary_key_vector_scan import _residual_row_ranges
from pypaimon.execution.python.read.search.vector_search_read import (
BatchVectorSearchReadImpl, DataEvolutionVectorRead)
from pypaimon.table.row.blob import BlobViewStruct
from pypaimon.table.source.primary_key_scored_result import PrimaryKeySearchPosition, _partition_bytes
from pypaimon.table.source.search_types import PrimaryKeyVectorScanPlan, PrimaryKeyVectorSearchSplit
from pypaimon.utils.range import Range
from pypaimon.utils.roaring_bitmap import RoaringBitmap64
pytestmark = [pytest.mark.python_plan, pytest.mark.python_read,
pytest.mark.python_write, pytest.mark.python_commit]
@contextmanager
def _forbid_native():
original_import = builtins.__import__
def import_python_only(name, *args, **kwargs):
if name.startswith(('pypaimon.execution.native', 'pypaimon_rust')):
raise AssertionError('Selected Python reader entered Native: ' + name)
return original_import(name, *args, **kwargs)
with patch('builtins.__import__', side_effect=import_python_only):
yield
def _write(table, data):
builder = table.new_batch_write_builder()
writer, commit = builder.new_write(), builder.new_commit()
try:
writer.write_arrow(data)
commit.commit(writer.prepare_commit())
finally:
writer.close()
commit.close()
def test_python_blob_view_lookup_keeps_native_upstream_in_python(tmp_path):
catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
catalog.create_database('default', True)
schema = pa.schema([('id', pa.int32()), ('picture', pa.large_binary())])
options = {'row-tracking.enabled': 'true', 'data-evolution.enabled': 'true',
'write.native.enabled': 'false', 'commit.native.enabled': 'false',
'scan.native-plan.enabled': 'true', 'read.native.enabled': 'true'}
catalog.create_table('default.source', Schema.from_pyarrow_schema(schema, options=options), False)
source = catalog.get_table('default.source')
_write(source.copy({'scan.native-plan.enabled': 'false', 'read.native.enabled': 'false'}),
pa.table({'id': [1], 'picture': [b'upstream payload']}, schema=schema))
catalog.create_table('default.target', Schema.from_pyarrow_schema(schema, options={
**options, 'blob-view-field': 'picture', 'scan.native-plan.enabled': 'false',
'read.native.enabled': 'false'}), False)
target = catalog.get_table('default.target')
_write(target, pa.table({'id': [2], 'picture': [
BlobViewStruct.of(source, 'picture', 0).serialize()]}, schema=schema))
with _forbid_native():
builder = target.new_read_builder()
result = builder.new_read().to_arrow(builder.new_scan().plan().splits())
assert result.to_pylist() == [{'id': 2, 'picture': b'upstream payload'}]
assert source.options.native_read_enabled()
assert source.options.native_plan_enabled()
assert not target.options.native_read_enabled()
assert not target.options.native_plan_enabled()
@pytest.fixture
def search_table(tmp_path, request):
primary_key = getattr(request, 'param', False)
catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
catalog.create_database('default', True)
schema = pa.schema([('id', pa.int32()), ('text', pa.string()),
('embedding', pa.list_(pa.float32()))])
catalog.create_table('default.search', Schema.from_pyarrow_schema(
schema, primary_keys=['id'] if primary_key else [], options={
'bucket': '1' if primary_key else '-1',
'row-tracking.enabled': 'false' if primary_key else 'true',
'data-evolution.enabled': 'false' if primary_key else 'true',
'deletion-vectors.enabled': 'true',
'write.native.enabled': 'false', 'commit.native.enabled': 'false',
'read.native.enabled': 'false', 'scan.native-plan.enabled': 'false'}), False)
table = catalog.get_table('default.search')
_write(table, pa.table({'id': [1, 2], 'text': ['first', 'second'],
'embedding': [[0., 0.], [1., 0.]]}, schema=schema))
return table.copy_without_time_travel({
'read.native.enabled': 'true', 'scan.native-plan.enabled': 'true'})
@pytest.mark.parametrize('path', ['vector', 'batch-vector', 'full-text', 'filter', 'live-rows'])
@pytest.mark.parametrize('captured', [False, True])
def test_python_search_dependency_reads_keep_real_rows_in_python(search_table, path, captured):
table = search_table
if captured:
table = table._copy_with_snapshot(table.snapshot_manager().get_latest_snapshot())
predicate = table.new_read_builder().new_predicate_builder().equal('id', 2)
with _forbid_native():
if path == 'vector':
reader = DataEvolutionVectorRead(table, 2, table.field_dict['embedding'], [0., 0.])
result = reader._read_raw_arrow([Range(0, 1)], include_filter=False)
assert result.column('_ROW_ID').to_pylist() == [0, 1]
assert result.column('embedding').to_pylist() == [[0., 0.], [1., 0.]]
elif path == 'batch-vector':
reader = BatchVectorSearchReadImpl(table, 2, table.field_dict['embedding'], [[0., 0.]])
result = reader._read_raw_batch_search([Range(0, 1)], None)[0]
assert result.results().to_list() == [0, 1]
assert result.score_getter()(0) > result.score_getter()(1)
elif path == 'full-text':
reader = DataEvolutionFullTextRead(table, 2, table.field_dict['text'], 'first')
result = reader._read_raw_rows([Range(1, 1)])
assert result.column('text').to_pylist() == ['second']
elif path == 'filter':
candidates = RoaringBitmap64()
candidates.add_range(0, 1)
assert matching_rows(table, predicate, candidates).to_list() == [1]
else:
assert global_index_live_row_filter.live_rows(table).to_list() == [0, 1]
assert table.options.native_read_enabled()
assert table.options.native_plan_enabled()
@pytest.mark.parametrize('search_table', [True], indirect=True)
@pytest.mark.parametrize('path', ['raw-vector', 'rerank-vector', 'residual', 'raw-full-text'])
@pytest.mark.parametrize('captured', [False, True])
def test_python_primary_key_search_dependency_reads_keep_python(search_table, path, captured):
table = search_table
if captured:
table = table._copy_with_snapshot(table.snapshot_manager().get_latest_snapshot())
scan = table.copy_without_time_travel({
'read.native.enabled': 'false', 'scan.native-plan.enabled': 'false'}).new_read_builder().new_scan()
splits = scan.plan_all().splits()
plan = PrimaryKeyVectorScanPlan(1, [PrimaryKeyVectorSearchSplit(
split, (), tuple(file.file_name for file in split.files), {}) for split in splits])
column = table.field_dict['embedding']
reader = PrimaryKeyVectorRead(table, 2, column, [0., 0.], options={'metric': 'l2'})
with _forbid_native():
if path == 'raw-vector':
candidates = list(reader._raw_candidates(plan))
assert [candidate.row_position for candidate in candidates] == [0, 1]
assert candidates[0].score > candidates[1].score
elif path == 'rerank-vector':
split = splits[0]
candidates = [PrimaryKeySearchPosition(
_partition_bytes(split.partition), split.bucket, split.files[0].file_name, pos, 0.)
for pos in (0, 1)]
result = reader._rerank_indexed(plan, candidates, 'ivf-flat')
assert [candidate.row_position for candidate in result] == [0, 1]
assert result[0].score > result[1].score
elif path == 'residual':
predicate = table.new_read_builder().new_predicate_builder().equal('id', 2)
assert _residual_row_ranges(table, predicate, splits[0], {}) == {
splits[0].files[0].file_name: (Range(1, 1),)}
else:
text_reader = PrimaryKeyFullTextRead.__new__(PrimaryKeyFullTextRead)
text_reader._table, text_reader._text_columns = table, [table.field_dict['text']]
with patch.object(text_reader, '_build_raw_index', return_value=None) as build_index:
assert text_reader._raw_candidates(plan) == []
build_index.assert_called_once_with([0, 1], ['first', 'second'], 0)
assert table.options.native_read_enabled()
assert table.options.native_plan_enabled()
@pytest.mark.parametrize('empty', [False, True])
def test_pinned_view_copies_do_not_load_backend(search_table, empty):
snapshot = None if empty else search_table.snapshot_manager().get_latest_snapshot()
pinned = search_table._copy_with_snapshot(snapshot)
with _forbid_native():
copied = pinned.copy_without_time_travel({'read.batch-size': '1'})
copied = copied.copy({'read.batch-size': '2'})
assert copied._read_snapshot is snapshot
assert copied.table_schema.fields == pinned.table_schema.fields
assert copied.current_branch() == pinned.current_branch()
assert copied.options.native_read_enabled()
assert copied.options.native_plan_enabled()