blob: 935bf45f31c1bb3757668bf53869feebc40d1115 [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 unittest
from concurrent.futures import Future, ThreadPoolExecutor
from unittest.mock import Mock
from pypaimon.globalindex.batch_vector_search import BatchVectorSearch
from pypaimon.globalindex.global_index_reader import (
FieldRef,
GlobalIndexReader,
_completed_future,
)
from pypaimon.globalindex.global_index_result import GlobalIndexResult
from pypaimon.globalindex.union_global_index_reader import UnionGlobalIndexReader
from pypaimon.utils.range import Range
class _EqualReader(GlobalIndexReader):
def __init__(self, result):
self._result = result
def visit_equal(self, field_ref, literal):
return _completed_future(self._result)
def close(self):
pass
class _VectorReader(GlobalIndexReader):
def __init__(self, result_future):
self._result_future = result_future
def visit_vector_search(self, vector_search):
return self._result_future
def close(self):
pass
class UnionGlobalIndexReaderTest(unittest.TestCase):
def test_scalar_unsupported_is_propagated(self):
supported = GlobalIndexResult.from_range(Range(1, 2))
reader = UnionGlobalIndexReader([_EqualReader(supported), _EqualReader(None)])
result = reader.visit_equal(FieldRef(0, "f0", "INT"), 1).result()
self.assertIsNone(result)
def test_scalar_child_failure_is_not_masked_by_unsupported(self):
field_ref = FieldRef(0, "f0", "INT")
unsupported_reader = Mock(spec=GlobalIndexReader)
unsupported_reader.visit_equal.return_value = _completed_future(None)
failed_future = Future()
failed_future.set_exception(RuntimeError("child failure"))
failed_reader = Mock(spec=GlobalIndexReader)
failed_reader.visit_equal.return_value = failed_future
for readers in [
[unsupported_reader, failed_reader],
[failed_reader, unsupported_reader],
]:
with self.subTest(readers=readers):
reader = UnionGlobalIndexReader(readers)
with self.assertRaisesRegex(RuntimeError, "child failure"):
reader.visit_equal(field_ref, 1).result()
def test_all_scalar_visitors_propagate_unsupported(self):
field_ref = FieldRef(0, "f0", "INT")
cases = [
("visit_not_equal", (field_ref, 1)),
("visit_less_than", (field_ref, 1)),
("visit_less_or_equal", (field_ref, 1)),
("visit_greater_than", (field_ref, 1)),
("visit_greater_or_equal", (field_ref, 1)),
("visit_is_null", (field_ref,)),
("visit_is_not_null", (field_ref,)),
("visit_in", (field_ref, [1, 2])),
("visit_not_in", (field_ref, [1, 2])),
("visit_starts_with", (field_ref, "1")),
("visit_ends_with", (field_ref, "1")),
("visit_contains", (field_ref, "1")),
("visit_like", (field_ref, "1%")),
("visit_between", (field_ref, 1, 2)),
("visit_not_between", (field_ref, 1, 2)),
]
for method_name, args in cases:
with self.subTest(method=method_name):
supported_reader = Mock(spec=GlobalIndexReader)
unsupported_reader = Mock(spec=GlobalIndexReader)
supported_result = GlobalIndexResult.from_range(Range(1, 2))
getattr(supported_reader, method_name).return_value = _completed_future(
supported_result)
getattr(unsupported_reader, method_name).return_value = _completed_future(None)
reader = UnionGlobalIndexReader([supported_reader, unsupported_reader])
result = getattr(reader, method_name)(*args).result()
self.assertIsNone(result)
def test_batch_vector_search_returns_before_children_complete(self):
child_result = Future()
reader = UnionGlobalIndexReader([_VectorReader(child_result)])
search = BatchVectorSearch(vectors=[[1.0]], limit=1, field_name="f0")
executor = ThreadPoolExecutor(max_workers=1)
invocation = executor.submit(reader.visit_batch_vector_search, search)
try:
result_future = invocation.result(timeout=1)
self.assertFalse(result_future.done())
finally:
child_result.set_result(None)
executor.shutdown()
self.assertEqual([None], result_future.result(timeout=1))
def test_batch_vector_search_propagates_failure_asynchronously(self):
child_result = Future()
reader = UnionGlobalIndexReader([_VectorReader(child_result)])
search = BatchVectorSearch(vectors=[[1.0]], limit=1, field_name="f0")
result_future = reader.visit_batch_vector_search(search)
child_result.set_exception(RuntimeError("async failure"))
with self.assertRaisesRegex(RuntimeError, "async failure"):
result_future.result(timeout=1)
def test_close_propagates_failure_after_closing_all_readers(self):
failing_reader = Mock(spec=GlobalIndexReader)
failing_reader.close.side_effect = OSError("close failed")
remaining_reader = Mock(spec=GlobalIndexReader)
reader = UnionGlobalIndexReader([failing_reader, remaining_reader])
with self.assertRaisesRegex(OSError, "close failed"):
reader.close()
remaining_reader.close.assert_called_once_with()
if __name__ == "__main__":
unittest.main()