blob: 93175a52b262bd4db60b3151d47a7dce34e6450e [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.
import json
from types import ModuleType
from unittest.mock import Mock, patch
import pytest
from pypaimon import CatalogFactory, Schema
from pypaimon.execution.native.read.builder import _native_read_builder
from pypaimon.execution.native.read.plan import native_plan, native_stream_scan
from pypaimon.execution.native.table import _resolved_schema_json, _options_to_native
from pypaimon.execution.native.write.native_commit import create_native_write_table
from pypaimon.schema.data_types import AtomicType, DataField, VectorType
def _table(tmp_path, primary_key, extra=None):
options = {'row-tracking.enabled': 'false' if primary_key else 'true',
'bucket': '2' if primary_key else '-1',
'deletion-vectors.enabled': 'true' if primary_key else 'false',
'data-evolution.enabled': 'false' if primary_key else 'true'}
options.update(extra or {})
catalog = CatalogFactory.create({'warehouse': str(tmp_path / 'warehouse')})
catalog.create_database('default', True)
fields = [DataField(0, 'id', AtomicType('INT', False)),
DataField(1, 'amount', AtomicType('INT')),
DataField(2, 'other', AtomicType('INT')),
DataField(3, 'embedding', VectorType(True, AtomicType('FLOAT'), 2)),
DataField(4, 'body', AtomicType('STRING'))]
catalog.create_table('default.search', Schema(
fields=fields, primary_keys=['id'] if primary_key else [], options=options), False)
return catalog.get_table('default.search')
@pytest.mark.parametrize('primary_key', [False, True])
@pytest.mark.parametrize('declaration', ['legacy', 'index-column', 'index-columns'])
def test_native_schemas_preserve_index_configuration_verbatim(tmp_path, primary_key, declaration):
values = {'ivf-pq.dimension': '2', 'opaque.native.option': 'preserved',
'btree-index.block-size': '4 kb',
'fields.amount.index-options': '{"block-size":"8 kb"}',
'fields.other.index-options': '{"block-size":"16 kb"}',
'fields.embedding.index-options': '{"nlist":64,"metric":"COSINE"}',
'fields.embedding.pk-vector.index.options': '{"nlist":32}',
'fields.body.index-options': '{"stop-words":["paimon","lake"],"lower-case":false}'}
if declaration == 'legacy':
values.update({'pk-vector.index.columns': 'embedding',
'fields.embedding.pk-vector.index.type': 'IVF-PQ',
'pk-btree.index.columns': 'amount,other', 'pk-full-text.index.columns': 'body'})
else:
values.update({'global-index.ivf-pq.' + declaration: 'embedding',
'global-index.btree.' + declaration: 'amount,other',
'global-index.full-text.' + declaration: 'body'})
table = _table(tmp_path, primary_key, values)
original = dict(table.table_schema.options)
read = json.loads(_resolved_schema_json(table))['options']
module = ModuleType('pypaimon_rust.datafusion')
module.Table = Mock()
with patch.dict('sys.modules', {'pypaimon_rust.datafusion': module}):
assert create_native_write_table(table) is module.Table.from_resolved_schema.return_value
write = json.loads(module.Table.from_resolved_schema.call_args[0][1])['options']
for actual in (read, write):
for key, value in values.items():
assert actual[key] == value
assert table.table_schema.options == original
@pytest.mark.parametrize('primary_key', [False, True])
@pytest.mark.parametrize('key', ['index_type', 'index-type', 'vector.index-type', 'fields.embedding.index-type'])
def test_native_schema_keeps_uninterpreted_options(tmp_path, primary_key, key):
values = {'global-index.ivf-pq.index-columns': 'embedding', key: 'diskann',
'fields.embedding.index-options': '{"index-type":"other","nlist":64}'}
table = _table(tmp_path, primary_key, values)
actual = json.loads(_resolved_schema_json(table))['options']
for name, value in values.items():
assert actual[name] == value
assert 'ivf-pq.nlist' not in actual
assert 'fields.embedding.metric' not in actual
@pytest.mark.parametrize('field_options', ['{"nlist":64}', '[1,2]'])
def test_native_defers_field_option_parsing_to_rust(tmp_path, field_options):
table = _table(tmp_path, False, {'fields.embedding.index-options': field_options})
actual = json.loads(_resolved_schema_json(table))['options']
assert actual['fields.embedding.index-options'] == field_options
@pytest.mark.parametrize('streaming', [False, True])
def test_distinct_scalar_field_options_reach_native_scan(tmp_path, streaming):
table = _table(tmp_path, False, {
'global-index.btree.index-columns': 'amount,other',
'fields.amount.index-options': '{"fallback-scan-max-size":"1 kb"}',
'fields.other.index-options': '{"fallback-scan-max-size":"2 kb"}',
})
predicate = table.new_read_builder().new_predicate_builder().equal('amount', 1)
from pypaimon.execution.native.read import plan
with patch.object(plan.runtime, 'native_runtime_available', return_value=True), \
patch.object(plan.native_builder, '_native_read_builder') as bridge, \
patch.object(plan.native_builder, '_configure_native_read_builder',
side_effect=lambda builder, *args, **kwargs: builder), \
patch.object(plan, '_from_native_plan') as convert:
if streaming:
assert native_stream_scan(table, predicate=predicate) is bridge.return_value.new_stream_scan.return_value
else:
assert native_plan(table, predicate=predicate) is convert.return_value
bridge.assert_called_once_with(table)
options = json.loads(_resolved_schema_json(table))['options']
assert options['fields.amount.index-options'] != options['fields.other.index-options']
@pytest.mark.parametrize('global_index', [True, False])
def test_index_field_options_do_not_disable_native_physical_reads(tmp_path, global_index):
table = _table(tmp_path, False, {
'global-index.enabled': str(global_index).lower(),
'fields.embedding.index-options': '{"nlist":64}',
})
with patch('pypaimon.execution.native.table._native_table') as bridge:
assert _native_read_builder(table) is bridge.return_value.new_read_builder.return_value
bridge.assert_called_once_with(table)
def test_native_options_only_encode_values_and_leave_input_untouched():
original = {'boolean': True, 'number': 4, 'list': ['paimon', 'lake'],
'object': {'value': False}, 'text': ' COSINE ', 'unset': None}
actual = _options_to_native(original)
assert actual == {'boolean': 'true', 'number': '4', 'list': '["paimon", "lake"]',
'object': '{"value": false}', 'text': ' COSINE '}
assert original['unset'] is None
assert original['list'] == ['paimon', 'lake']