blob: 3091dc485cac5bb39779dbb82247bcc607409edf [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.
*/
#include "arrow/type.h"
#include "gtest/gtest.h"
#include "paimon/common/factories/io_hook.h"
#include "paimon/common/global_index/bitmap/bitmap_global_index_factory.h"
#include "paimon/common/global_index/union_global_index_reader.h"
#include "paimon/common/table/special_fields.h"
#include "paimon/common/utils/scope_guard.h"
#include "paimon/core/global_index/global_index_scan_impl.h"
#include "paimon/core/global_index/indexed_split_impl.h"
#include "paimon/core/table/source/data_split_impl.h"
#include "paimon/defs.h"
#include "paimon/executor.h"
#include "paimon/fs/file_system.h"
#include "paimon/global_index/bitmap_global_index_result.h"
#include "paimon/global_index/bitmap_scored_global_index_result.h"
#include "paimon/global_index/global_index_reader.h"
#include "paimon/global_index/global_index_result.h"
#include "paimon/global_index/global_index_scan.h"
#include "paimon/global_index/global_index_write_task.h"
#include "paimon/predicate/literal.h"
#include "paimon/predicate/predicate_builder.h"
#include "paimon/result.h"
#include "paimon/status.h"
#include "paimon/testing/utils/binary_row_generator.h"
#include "paimon/testing/utils/io_exception_helper.h"
#include "paimon/testing/utils/test_helper.h"
#include "paimon/testing/utils/testharness.h"
namespace paimon::test {
// string: FileFormat, bool: UseSpecificFileSystem
using ParamType = std::tuple<std::string, bool>;
/// This is a sdk end-to-end test for global index.
class GlobalIndexTest : public ::testing::Test, public ::testing::WithParamInterface<ParamType> {
void SetUp() override {
file_format_ = std::get<0>(GetParam());
dir_ = UniqueTestDirectory::Create("local");
if (std::get<1>(GetParam())) {
fs_ = dir_->GetFileSystem();
}
int64_t seed = DateTimeUtils::GetCurrentUTCTimeUs();
std::srand(seed);
}
void TearDown() override {
dir_.reset();
}
void CreateTable(const std::vector<std::string>& partition_keys,
const std::shared_ptr<arrow::Schema>& schema,
const std::map<std::string, std::string>& options) const {
::ArrowSchema c_schema;
ASSERT_TRUE(arrow::ExportSchema(*schema, &c_schema).ok());
ASSERT_OK_AND_ASSIGN(auto catalog, Catalog::Create(dir_->Str(), {}, fs_));
ASSERT_OK(catalog->CreateDatabase("foo", {}, /*ignore_if_exists=*/false));
ASSERT_OK(catalog->CreateTable(Identifier("foo", "bar"), &c_schema, partition_keys,
/*primary_keys=*/{}, options,
/*ignore_if_exists=*/false));
}
void CreateTable(const std::vector<std::string>& partition_keys) const {
std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"},
{Options::FILE_FORMAT, file_format_},
{Options::FILE_SYSTEM, "local"},
{Options::ROW_TRACKING_ENABLED, "true"},
{Options::DATA_EVOLUTION_ENABLED, "true"}};
return CreateTable(partition_keys, arrow::schema(fields_), options);
}
void CreateTable() const {
return CreateTable(/*partition_keys=*/{});
}
Result<std::vector<std::shared_ptr<CommitMessage>>> WriteArray(
const std::string& table_path, const std::map<std::string, std::string>& partition,
const std::vector<std::string>& write_cols,
const std::shared_ptr<arrow::Array>& write_array) const {
// write
WriteContextBuilder write_builder(table_path, "commit_user_1");
write_builder.WithWriteSchema(write_cols);
write_builder.WithFileSystem(fs_);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<WriteContext> write_context, write_builder.Finish());
PAIMON_ASSIGN_OR_RAISE(auto file_store_write,
FileStoreWrite::Create(std::move(write_context)));
ArrowArray c_array;
EXPECT_TRUE(arrow::ExportArray(*write_array, &c_array).ok());
auto record_batch = std::make_unique<RecordBatch>(
partition, /*bucket=*/0,
/*row_kinds=*/std::vector<RecordBatch::RowKind>(), &c_array);
PAIMON_RETURN_NOT_OK(file_store_write->Write(std::move(record_batch)));
PAIMON_ASSIGN_OR_RAISE(auto commit_msgs,
file_store_write->PrepareCommit(
/*wait_compaction=*/false, /*commit_identifier=*/0));
PAIMON_RETURN_NOT_OK(file_store_write->Close());
return commit_msgs;
}
Result<std::vector<std::shared_ptr<CommitMessage>>> WriteArray(
const std::string& table_path, const std::vector<std::string>& write_cols,
const std::shared_ptr<arrow::Array>& write_array) const {
return WriteArray(table_path, /*partition=*/{}, write_cols, write_array);
}
Status Commit(const std::string& table_path,
const std::vector<std::shared_ptr<CommitMessage>>& commit_msgs) const {
// commit
CommitContextBuilder commit_builder(table_path, "commit_user_1");
commit_builder.WithFileSystem(fs_);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<CommitContext> commit_context,
commit_builder.Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreCommit> file_store_commit,
FileStoreCommit::Create(std::move(commit_context)));
return file_store_commit->Commit(commit_msgs);
}
Result<std::shared_ptr<DataSplitImpl>> ScanData(
const std::string& table_path,
const std::vector<std::map<std::string, std::string>>& partition_filters) const {
ScanContextBuilder scan_context_builder(table_path);
scan_context_builder.SetPartitionFilter(partition_filters);
scan_context_builder.WithFileSystem(fs_);
PAIMON_ASSIGN_OR_RAISE(auto scan_context, scan_context_builder.Finish());
PAIMON_ASSIGN_OR_RAISE(auto table_scan, TableScan::Create(std::move(scan_context)));
PAIMON_ASSIGN_OR_RAISE(auto result_plan, table_scan->CreatePlan());
EXPECT_EQ(result_plan->Splits().size(), 1);
return std::dynamic_pointer_cast<DataSplitImpl>(result_plan->Splits()[0]);
}
Status WriteIndex(const std::string& table_path,
const std::vector<std::map<std::string, std::string>>& partition_filters,
const std::string& index_field_name, const std::string& index_type,
const std::map<std::string, std::string>& options, const Range& range) {
PAIMON_ASSIGN_OR_RAISE(auto split, ScanData(table_path, partition_filters));
PAIMON_ASSIGN_OR_RAISE(auto index_commit_msg, GlobalIndexWriteTask::WriteIndex(
table_path, index_field_name, index_type,
std::make_shared<IndexedSplitImpl>(
split, std::vector<Range>({range})),
options, pool_, fs_));
return Commit(table_path, {index_commit_msg});
}
Result<std::shared_ptr<Plan>> ScanGlobalIndexAndData(
const std::string& table_path, const std::shared_ptr<Predicate>& predicate,
const std::map<std::string, std::string>& options = {},
const std::shared_ptr<GlobalIndexResult>& index_result = nullptr) const {
ScanContextBuilder scan_context_builder(table_path);
scan_context_builder.SetPredicate(predicate)
.SetOptions(options)
.SetGlobalIndexResult(index_result)
.WithFileSystem(fs_);
PAIMON_ASSIGN_OR_RAISE(auto scan_context, scan_context_builder.Finish());
PAIMON_ASSIGN_OR_RAISE(auto table_scan, TableScan::Create(std::move(scan_context)));
PAIMON_ASSIGN_OR_RAISE(auto result_plan, table_scan->CreatePlan());
return result_plan;
}
Result<std::shared_ptr<Plan>> ScanDataWithIndexResult(
const std::string& table_path, const std::vector<Range>& row_ranges,
const std::map<int64_t, float>& id_to_score) const {
std::shared_ptr<GlobalIndexResult> index_result;
if (id_to_score.empty()) {
index_result = BitmapGlobalIndexResult::FromRanges(row_ranges);
} else {
RoaringBitmap64 bitmap;
for (const auto& range : row_ranges) {
bitmap.AddRange(range.from, range.to + 1);
}
std::vector<float> scores;
for (auto iter = bitmap.Begin(); iter != bitmap.End(); ++iter) {
scores.push_back(id_to_score.at(*iter));
}
index_result = std::make_shared<BitmapScoredGlobalIndexResult>(std::move(bitmap),
std::move(scores));
}
return ScanGlobalIndexAndData(table_path, /*predicate=*/nullptr,
/*options=*/{}, index_result);
}
Status ReadData(const std::string& table_path, const std::vector<std::string>& read_schema,
const std::shared_ptr<arrow::Array>& expected_array,
const std::shared_ptr<Predicate>& predicate,
const std::shared_ptr<Plan>& result_plan) const {
auto splits = result_plan->Splits();
ReadContextBuilder read_context_builder(table_path);
read_context_builder.SetReadSchema(read_schema).SetPredicate(predicate).WithFileSystem(fs_);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ReadContext> read_context,
read_context_builder.Finish());
PAIMON_ASSIGN_OR_RAISE(auto table_read, TableRead::Create(std::move(read_context)));
PAIMON_ASSIGN_OR_RAISE(auto batch_reader, table_read->CreateReader(splits));
PAIMON_ASSIGN_OR_RAISE(auto read_result,
ReadResultCollector::CollectResult(batch_reader.get()));
if (!expected_array) {
if (read_result) {
return Status::Invalid("expected array is empty, but read result is not empty");
}
return Status::OK();
}
auto expected_chunk_array = std::make_shared<arrow::ChunkedArray>(expected_array);
if (!expected_chunk_array->ApproxEquals(*read_result,
arrow::EqualOptions::Defaults().atol(1E-2))) {
std::cout << "result=" << read_result->ToString() << std::endl
<< "expected=" << expected_chunk_array->ToString() << std::endl;
return Status::Invalid("expected array and result array not equal");
}
return Status::OK();
}
private:
std::string file_format_;
std::unique_ptr<UniqueTestDirectory> dir_;
arrow::FieldVector fields_ = {
arrow::field("f0", arrow::utf8()),
arrow::field("f1", arrow::int32()),
arrow::field("f2", arrow::int32()),
arrow::field("f3", arrow::float64()),
};
std::shared_ptr<MemoryPool> pool_ = GetDefaultPool();
std::shared_ptr<FileSystem> fs_ = nullptr;
};
#ifdef PAIMON_ENABLE_LUMINA
TEST_P(GlobalIndexTest, TestWriteLuminaIndex) {
arrow::FieldVector fields = {arrow::field("f0", arrow::utf8()),
arrow::field("f1", arrow::list(arrow::float32()))};
auto schema = arrow::schema(fields);
std::map<std::string, std::string> lumina_options = {{"lumina.index.dimension", "4"},
{"lumina.index.type", "bruteforce"},
{"lumina.distance.metric", "l2"},
{"lumina.encoding.type", "rawf32"},
{"lumina.search.parallel_number", "10"}};
std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"},
{Options::FILE_FORMAT, file_format_},
{Options::FILE_SYSTEM, "local"},
{Options::ROW_TRACKING_ENABLED, "true"},
{Options::DATA_EVOLUTION_ENABLED, "true"}};
CreateTable(/*partition_keys=*/{}, schema, options);
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
std::vector<std::string> write_cols = schema->field_names();
auto src_array = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
["a", [0.0, 0.0, 0.0, 0.0]],
["b", [0.0, 1.0, 0.0, 1.0]],
["c", [1.0, 0.0, 1.0, 0.0]],
["d", [1.0, 1.0, 1.0, 1.0]]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
ASSERT_OK_AND_ASSIGN(auto split, ScanData(table_path, /*partition_filters=*/{}));
ASSERT_OK_AND_ASSIGN(auto index_commit_msg, GlobalIndexWriteTask::WriteIndex(
table_path, "f1", "lumina",
std::make_shared<IndexedSplitImpl>(
split, std::vector<Range>({Range(0, 3)})),
/*options=*/lumina_options, pool_));
auto index_commit_msg_impl = std::dynamic_pointer_cast<CommitMessageImpl>(index_commit_msg);
ASSERT_TRUE(index_commit_msg_impl);
// check commit message
std::string index_meta_json =
R"({"distance.metric":"l2","encoding.type":"rawf32","index.dimension":"4","index.type":"bruteforce","search.parallel_number":"10"})";
GlobalIndexMeta expected_global_index_meta(
/*row_range_start=*/0, /*row_range_end=*/3, /*index_field_id=*/1,
/*extra_field_ids=*/std::nullopt, std::make_shared<Bytes>(index_meta_json, pool_.get()));
auto expected_index_file_meta =
std::make_shared<IndexFileMeta>("lumina", /*file_name=*/"fake_index_file", /*file_size=*/10,
/*row_count=*/4, /*dv_ranges=*/std::nullopt,
/*external_path=*/std::nullopt, expected_global_index_meta);
DataIncrement expected_data_increment({expected_index_file_meta});
auto expected_commit_message = std::make_shared<CommitMessageImpl>(
/*partition=*/BinaryRow::EmptyRow(), /*bucket=*/0, /*total_buckets=*/std::nullopt,
expected_data_increment, CompactIncrement({}, {}, {}));
ASSERT_TRUE(expected_commit_message->TEST_Equal(*index_commit_msg_impl));
}
TEST_P(GlobalIndexTest, TestWriteLuminaIndexWithMismatchedDimension) {
arrow::FieldVector fields = {arrow::field("f0", arrow::utf8()),
arrow::field("f1", arrow::list(arrow::float32()))};
auto schema = arrow::schema(fields);
std::map<std::string, std::string> lumina_options = {{"lumina.index.dimension", "3"},
{"lumina.index.type", "bruteforce"},
{"lumina.distance.metric", "l2"},
{"lumina.encoding.type", "rawf32"},
{"lumina.search.parallel_number", "10"}};
std::map<std::string, std::string> options = {
{Options::MANIFEST_FORMAT, "orc"}, {Options::FILE_FORMAT, file_format_},
{Options::FILE_SYSTEM, "local"}, {Options::ROW_TRACKING_ENABLED, "true"},
{Options::DATA_EVOLUTION_ENABLED, "true"}, {Options::READ_BATCH_SIZE, "1"}};
CreateTable(/*partition_keys=*/{}, schema, options);
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
std::vector<std::string> write_cols = schema->field_names();
auto src_array = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
["a", [0.0, 0.0, 0.0]],
["b", [0.0, 0.0, 0.0, 0.0]]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
ASSERT_NOK_WITH_MSG(
WriteIndex(table_path, /*partition_filters=*/{}, "f1", "lumina",
/*options=*/lumina_options, Range(0, 1)),
"invalid input array in LuminaIndexWriter, length of field array [1] multiplied "
"dimension [3] must match length of field value array [4]");
}
TEST_P(GlobalIndexTest, TestWriteIndex) {
CreateTable();
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
auto schema = arrow::schema(fields_);
std::vector<std::string> write_cols = schema->field_names();
auto src_array = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Alice", 10, 1, 11.1],
["Bob", 10, 1, 12.1],
["Emily", 10, 0, 13.1],
["Tony", 10, 0, 14.1],
["Lucy", 20, 1, 15.1],
["Bob", 10, 1, 16.1],
["Tony", 20, 0, 17.1],
["Alice", 20, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
ASSERT_OK_AND_ASSIGN(auto split, ScanData(table_path, /*partition_filters=*/{}));
ASSERT_OK_AND_ASSIGN(auto index_commit_msg, GlobalIndexWriteTask::WriteIndex(
table_path, "f0", "bitmap",
std::make_shared<IndexedSplitImpl>(
split, std::vector<Range>({Range(0, 7)})),
/*options=*/{}, pool_));
auto index_commit_msg_impl = std::dynamic_pointer_cast<CommitMessageImpl>(index_commit_msg);
ASSERT_TRUE(index_commit_msg_impl);
// check commit message
GlobalIndexMeta expected_global_index_meta(
/*row_range_start=*/0, /*row_range_end=*/7, /*index_field_id=*/0,
/*extra_field_ids=*/std::nullopt, /*index_meta=*/nullptr);
auto expected_index_file_meta =
std::make_shared<IndexFileMeta>("bitmap", /*file_name=*/"fake_index_file", /*file_size=*/10,
/*row_count=*/8, /*dv_ranges=*/std::nullopt,
/*external_path=*/std::nullopt, expected_global_index_meta);
DataIncrement expected_data_increment({expected_index_file_meta});
auto expected_commit_message = std::make_shared<CommitMessageImpl>(
/*partition=*/BinaryRow::EmptyRow(), /*bucket=*/0, /*total_buckets=*/std::nullopt,
expected_data_increment, CompactIncrement({}, {}, {}));
ASSERT_TRUE(expected_commit_message->TEST_Equal(*index_commit_msg_impl));
{
// test invalid write task with none-registered index type
ASSERT_NOK_WITH_MSG(
GlobalIndexWriteTask::WriteIndex(
table_path, "f0", "invalid",
std::make_shared<IndexedSplitImpl>(split, std::vector<Range>({Range(0, 7)})),
/*options=*/{}, pool_),
"Unknown index type invalid, may not registered");
}
{
// test invalid multiple ranges
ASSERT_NOK_WITH_MSG(GlobalIndexWriteTask::WriteIndex(
table_path, "f0", "bitmap",
std::make_shared<IndexedSplitImpl>(
split, std::vector<Range>({Range(0, 6), Range(7, 7)})),
/*options=*/{}, pool_),
"GlobalIndexWriteTask only supports a single contiguous range.");
}
}
TEST_P(GlobalIndexTest, TestWriteIndexWithPartition) {
CreateTable(/*partition_keys=*/{"f1"});
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
auto schema = arrow::schema(fields_);
std::vector<std::string> write_cols = schema->field_names();
auto src_array1 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Alice", 10, 1, 11.1],
["Bob", 10, 1, 12.1],
["Emily", 10, 0, 13.1],
["Tony", 10, 0, 14.1],
["Bob", 10, 1, 16.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs1,
WriteArray(table_path, {{"f1", "10"}}, write_cols, src_array1));
ASSERT_OK(Commit(table_path, commit_msgs1));
auto src_array2 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Lucy", 20, 1, 15.1],
["Tony", 20, 0, 17.1],
["Alice", 20, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs2,
WriteArray(table_path, {{"f1", "20"}}, write_cols, src_array2));
ASSERT_OK(Commit(table_path, commit_msgs2));
auto build_index_and_check =
[&](const std::vector<std::map<std::string, std::string>>& partition,
const Range& expected_range, const BinaryRow& expected_partition_row) {
ASSERT_OK_AND_ASSIGN(auto split, ScanData(table_path, partition));
ASSERT_OK_AND_ASSIGN(
auto index_commit_msg,
GlobalIndexWriteTask::WriteIndex(
table_path, "f0", "bitmap",
std::make_shared<IndexedSplitImpl>(split, std::vector<Range>({expected_range})),
/*options=*/{}, pool_));
auto index_commit_msg_impl =
std::dynamic_pointer_cast<CommitMessageImpl>(index_commit_msg);
ASSERT_TRUE(index_commit_msg_impl);
// check commit message
GlobalIndexMeta expected_global_index_meta(
/*row_range_start=*/expected_range.from, /*row_range_end=*/expected_range.to,
/*index_field_id=*/0,
/*extra_field_ids=*/std::nullopt, /*index_meta=*/nullptr);
auto expected_index_file_meta = std::make_shared<IndexFileMeta>(
"bitmap", /*file_name=*/"fake_index_file", /*file_size=*/10,
/*row_count=*/expected_range.Count(), /*dv_ranges=*/std::nullopt,
/*external_path=*/std::nullopt, expected_global_index_meta);
DataIncrement expected_data_increment({expected_index_file_meta});
auto expected_commit_message = std::make_shared<CommitMessageImpl>(
/*partition=*/expected_partition_row,
/*bucket=*/0,
/*total_buckets=*/std::nullopt, expected_data_increment,
CompactIncrement({}, {}, {}));
ASSERT_TRUE(expected_commit_message->TEST_Equal(*index_commit_msg_impl));
};
// build index for f1=20 partition
build_index_and_check({{{"f1", "20"}}}, Range(5, 7),
BinaryRowGenerator::GenerateRow({20}, pool_.get()));
// build index for f1=10 partition
build_index_and_check({{{"f1", "10"}}}, Range(0, 4),
BinaryRowGenerator::GenerateRow({10}, pool_.get()));
}
#endif
TEST_P(GlobalIndexTest, TestScanIndex) {
if (file_format_ == "avro") {
return;
}
std::string table_path = paimon::test::GetDataDir() + "/" + file_format_ +
"/append_with_global_index.db/append_with_global_index";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt,
/*partitions=*/std::nullopt, /*options=*/{}, fs_,
/*executor=*/nullptr, pool_));
// test index reader
// test f0 field
ASSERT_OK_AND_ASSIGN(auto index_readers, global_index_scan->CreateReaders("f0", std::nullopt));
ASSERT_EQ(index_readers.size(), 1u);
ASSERT_OK_AND_ASSIGN(auto index_result,
index_readers[0]->VisitEqual(Literal(FieldType::STRING, "Alice", 5)));
ASSERT_EQ(index_result->ToString(), "{0,7}");
// test f0, f1, f2 fields
auto global_index_scan_impl = std::dynamic_pointer_cast<GlobalIndexScanImpl>(global_index_scan);
{
// test with non predicate
ASSERT_OK_AND_ASSIGN(auto index_result,
global_index_scan_impl->Scan(/*predicate=*/nullptr));
ASSERT_FALSE(index_result);
}
{
// test equal predicate for f0
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_EQ(index_result->ToString(), "{0,7}");
}
{
// test not equal predicate for f0
auto predicate =
PredicateBuilder::NotEqual(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_EQ(index_result->ToString(), "{1,2,3,4,5,6}");
}
{
// test equal predicate for f1
auto predicate = PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(20));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_EQ(index_result->ToString(), "{4,6,7}");
}
{
// test equal predicate for f2
auto predicate = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2",
FieldType::INT, Literal(1));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_EQ(index_result->ToString(), "{0,1,4,5}");
}
{
// test is null predicate
auto predicate =
PredicateBuilder::IsNull(/*field_index=*/2, /*field_name=*/"f2", FieldType::INT);
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_EQ(index_result->ToString(), "{7}");
}
{
// test is not null predicate
auto predicate =
PredicateBuilder::IsNotNull(/*field_index=*/2, /*field_name=*/"f2", FieldType::INT);
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_EQ(index_result->ToString(), "{0,1,2,3,4,5,6}");
}
{
// test in predicate
auto predicate = PredicateBuilder::In(
/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
{Literal(FieldType::STRING, "Alice", 5), Literal(FieldType::STRING, "Bob", 3),
Literal(FieldType::STRING, "Lucy", 4)});
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_EQ(index_result->ToString(), "{0,1,4,5,7}");
}
{
// test not in predicate
auto predicate = PredicateBuilder::NotIn(
/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
{Literal(FieldType::STRING, "Alice", 5), Literal(FieldType::STRING, "Bob", 3),
Literal(FieldType::STRING, "Lucy", 4)});
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_EQ(index_result->ToString(), "{2,3,6}");
}
{
// test and predicate
auto f0_predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
auto f1_predicate = PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(20));
ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({f0_predicate, f1_predicate}));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_EQ(index_result->ToString(), "{7}");
}
{
// test or predicate
auto f0_predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
auto f1_predicate = PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(20));
ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::Or({f0_predicate, f1_predicate}));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_EQ(index_result->ToString(), "{0,4,6,7}");
}
{
// test non-result
auto predicate = PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(30));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_EQ(index_result->ToString(), "{}");
}
{
// test early stopping
auto f1_predicate = PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(10));
auto f2_predicate = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2",
FieldType::INT, Literal(6));
auto f0_predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
ASSERT_OK_AND_ASSIGN(auto predicate,
PredicateBuilder::And({f1_predicate, f2_predicate, f0_predicate}));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_EQ(index_result->ToString(), "{}");
}
{
// test greater than predicate which bitmap index is not support, will return all range
auto predicate = PredicateBuilder::GreaterThan(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(10));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_FALSE(index_result);
}
{
// test greater or equal predicate which bitmap index is not support, will return all range
auto predicate = PredicateBuilder::GreaterOrEqual(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(10));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_FALSE(index_result);
}
{
// test less than predicate which bitmap index is not support, will return all range
auto predicate = PredicateBuilder::LessThan(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(10));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_FALSE(index_result);
}
{
// test less or equal predicate which bitmap index is not support, will return all range
auto predicate = PredicateBuilder::LessOrEqual(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(10));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_FALSE(index_result);
}
{
// test a predicate for field with no index
auto f3_predicate = PredicateBuilder::Equal(/*field_index=*/3, /*field_name=*/"f3",
FieldType::DOUBLE, Literal(1.2));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(f3_predicate));
ASSERT_FALSE(index_result);
}
}
TEST_P(GlobalIndexTest, TestScanIndexWithSpecificSnapshot) {
if (file_format_ == "avro") {
return;
}
std::string table_path = paimon::test::GetDataDir() + "/" + file_format_ +
"/append_with_global_index.db/append_with_global_index";
// snapshot 2 has f0 index
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/2l,
/*partitions=*/std::nullopt, /*options=*/{}, fs_,
/*executor=*/nullptr, pool_));
// test index reader
// test f0 field
ASSERT_OK_AND_ASSIGN(auto index_readers, global_index_scan->CreateReaders("f0", std::nullopt));
ASSERT_EQ(index_readers.size(), 1u);
ASSERT_OK_AND_ASSIGN(auto index_result,
index_readers[0]->VisitEqual(Literal(FieldType::STRING, "Alice", 5)));
ASSERT_EQ(index_result->ToString(), "{0,7}");
// test f1 field
ASSERT_OK_AND_ASSIGN(auto index_readers2, global_index_scan->CreateReaders("f1", std::nullopt));
ASSERT_EQ(index_readers2.size(), 0u);
auto global_index_scan_impl = std::dynamic_pointer_cast<GlobalIndexScanImpl>(global_index_scan);
{
// test and predicate
auto f0_predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
auto f1_predicate = PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(20));
ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({f0_predicate, f1_predicate}));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_EQ(index_result->ToString(), "{0,7}");
}
{
// test or predicate
auto f0_predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
auto f1_predicate = PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(20));
ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::Or({f0_predicate, f1_predicate}));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_FALSE(index_result);
}
}
TEST_P(GlobalIndexTest, TestScanIndexWithSpecificSnapshotWithNoIndex) {
if (file_format_ == "avro") {
return;
}
std::string table_path = paimon::test::GetDataDir() + "/" + file_format_ +
"/append_with_global_index.db/append_with_global_index";
// snapshot 1 has no index
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/1l,
/*partitions=*/std::nullopt, /*options=*/{}, fs_,
/*executor=*/nullptr, pool_));
// test index reader
ASSERT_OK_AND_ASSIGN(auto index_readers, global_index_scan->CreateReaders("f0", std::nullopt));
ASSERT_EQ(index_readers.size(), 0u);
auto global_index_scan_impl = std::dynamic_pointer_cast<GlobalIndexScanImpl>(global_index_scan);
auto predicate =
PredicateBuilder::NotEqual(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_FALSE(index_result);
}
TEST_P(GlobalIndexTest, TestScanIndexWithRange) {
if (file_format_ == "avro") {
return;
}
std::string table_path = paimon::test::GetDataDir() + "/" + file_format_ +
"/append_with_global_index.db/append_with_global_index";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt,
/*partitions=*/std::nullopt, /*options=*/{}, fs_,
/*executor=*/nullptr, pool_));
auto global_index_scan_impl = std::dynamic_pointer_cast<GlobalIndexScanImpl>(global_index_scan);
{
// test index reader
ASSERT_OK_AND_ASSIGN(auto index_readers,
global_index_scan->CreateReaders("f0", std::nullopt));
ASSERT_EQ(index_readers.size(), 1u);
ASSERT_OK_AND_ASSIGN(auto index_result,
index_readers[0]->VisitEqual(Literal(FieldType::STRING, "Alice", 5)));
ASSERT_EQ(index_result->ToString(), "{0,7}");
auto predicate =
PredicateBuilder::NotEqual(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
ASSERT_OK_AND_ASSIGN(auto evaluator_result, global_index_scan_impl->Scan(predicate));
ASSERT_EQ(evaluator_result->ToString(), "{1,2,3,4,5,6}");
}
{
// invalid range
ASSERT_OK_AND_ASSIGN(RowRangeIndex row_range_index, RowRangeIndex::Create({Range(10, 13)}));
ASSERT_OK_AND_ASSIGN(auto index_readers,
global_index_scan->CreateReaders("f0", row_range_index));
ASSERT_EQ(index_readers.size(), 0u);
}
}
TEST_P(GlobalIndexTest, TestScanIndexWithPartition) {
if (file_format_ == "avro") {
return;
}
// only f1=10 has index
std::string table_path =
paimon::test::GetDataDir() + "/" + file_format_ +
"/append_with_global_index_with_partition.db/append_with_global_index_with_partition";
auto check_result =
[&](const std::optional<std::vector<std::map<std::string, std::string>>>& partitions) {
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt, partitions,
/*options=*/{}, fs_, /*executor=*/nullptr, pool_));
// test index reader
ASSERT_OK_AND_ASSIGN(RowRangeIndex row_range_index,
RowRangeIndex::Create({Range(0, 4)}));
ASSERT_OK_AND_ASSIGN(auto index_readers,
global_index_scan->CreateReaders("f0", row_range_index));
ASSERT_EQ(index_readers.size(), 1u);
ASSERT_OK_AND_ASSIGN(auto index_result, index_readers[0]->VisitEqual(
Literal(FieldType::STRING, "Bob", 3)));
ASSERT_EQ(index_result->ToString(), "{1,4}");
auto global_index_scan_impl =
std::dynamic_pointer_cast<GlobalIndexScanImpl>(global_index_scan);
{
// null result as f2 does not have index
auto predicate = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2",
FieldType::INT, Literal(1));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_FALSE(index_result);
}
{
// test not equal predicate for Bob
ASSERT_OK_AND_ASSIGN(auto index_result, index_readers[0]->VisitNotEqual(
Literal(FieldType::STRING, "Bob", 3)));
ASSERT_EQ(index_result->ToString(), "{0,2,3}");
}
{
// test equal predicate for Alice
ASSERT_OK_AND_ASSIGN(auto index_result, index_readers[0]->VisitEqual(Literal(
FieldType::STRING, "Alice", 5)));
ASSERT_EQ(index_result->ToString(), "{0}");
}
};
std::vector<std::map<std::string, std::string>> partitions = {{{"f1", "10"}}};
check_result(partitions);
check_result(std::nullopt);
}
TEST_P(GlobalIndexTest, TestScanUnregisteredIndex) {
if (file_format_ == "avro") {
return;
}
auto factory_creator = FactoryCreator::GetInstance();
factory_creator->TEST_Unregister("bitmap-global");
ScopeGuard guard([&factory_creator]() {
factory_creator->Register("bitmap-global", (new BitmapGlobalIndexFactory));
});
std::string table_path = paimon::test::GetDataDir() + "/" + file_format_ +
"/append_with_global_index.db/append_with_global_index";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt,
/*partitions=*/std::nullopt, /*options=*/{}, fs_,
/*executor=*/nullptr, pool_));
ASSERT_OK_AND_ASSIGN(auto index_readers, global_index_scan->CreateReaders("f0", std::nullopt));
ASSERT_EQ(index_readers.size(), 0u);
auto global_index_scan_impl = std::dynamic_pointer_cast<GlobalIndexScanImpl>(global_index_scan);
auto predicate =
PredicateBuilder::NotEqual(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Bob", 3));
ASSERT_OK_AND_ASSIGN(auto index_result, global_index_scan_impl->Scan(predicate));
ASSERT_FALSE(index_result);
}
TEST_P(GlobalIndexTest, TestWriteCommitScanReadIndex) {
CreateTable();
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
auto schema = arrow::schema(fields_);
std::vector<std::string> write_cols = schema->field_names();
auto src_array = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Alice", 10, 1, 11.1],
["Bob", 10, 1, 12.1],
["Emily", 10, 0, 13.1],
["Tony", 10, 0, 14.1],
["Lucy", 20, 1, 15.1],
["Bob", 10, 1, 16.1],
["Tony", 20, 0, 17.1],
["Alice", 20, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f0", "bitmap",
/*options=*/{}, Range(0, 7)));
ASSERT_OK_AND_ASSIGN(auto global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt,
/*partitions=*/std::nullopt, /*options=*/{}, fs_,
/*executor=*/nullptr, pool_));
ASSERT_OK_AND_ASSIGN(auto index_readers, global_index_scan->CreateReaders("f0", std::nullopt));
ASSERT_EQ(index_readers.size(), 1u);
ASSERT_OK_AND_ASSIGN(auto index_result,
index_readers[0]->VisitEqual(Literal(FieldType::STRING, "Alice", 5)));
ASSERT_EQ(index_result->ToString(), "{0,7}");
}
#ifdef PAIMON_ENABLE_LUMINA
TEST_P(GlobalIndexTest, TestWriteCommitScanReadIndexWithPartition) {
arrow::FieldVector fields = {
arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::list(arrow::float32())),
arrow::field("f2", arrow::int32()), arrow::field("f3", arrow::float64())};
std::map<std::string, std::string> lumina_options = {{"lumina.index.dimension", "4"},
{"lumina.index.type", "bruteforce"},
{"lumina.distance.metric", "l2"},
{"lumina.encoding.type", "rawf32"},
{"lumina.search.parallel_number", "10"}};
auto schema = arrow::schema(fields);
std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"},
{Options::FILE_FORMAT, file_format_},
{Options::FILE_SYSTEM, "local"},
{Options::ROW_TRACKING_ENABLED, "true"},
{Options::DATA_EVOLUTION_ENABLED, "true"}};
CreateTable(/*partition_keys=*/{"f2"}, schema, options);
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
std::vector<std::string> write_cols = schema->field_names();
auto write_data_and_index = [&](const std::shared_ptr<arrow::Array>& src_array,
const std::map<std::string, std::string>& partition,
const Range& expected_range) {
ASSERT_OK_AND_ASSIGN(auto commit_msgs,
WriteArray(table_path, partition, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
// write bitmap index
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{partition}, "f0", "bitmap",
/*options=*/{}, expected_range));
// write and commit lumina index
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{partition}, "f1", "lumina",
/*options=*/lumina_options, expected_range));
};
// write partition f2 = 10
auto src_array1 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
["Alice", [0.0, 0.0, 0.0, 0.0], 10, 11.1],
["Bob", [0.0, 1.0, 0.0, 1.0], 10, 12.1],
["Emily", [1.0, 0.0, 1.0, 0.0], 10, 13.1],
["Tony", [1.0, 1.0, 1.0, 1.0], 10, 14.1]
])")
.ValueOrDie();
write_data_and_index(src_array1, {{"f2", "10"}}, Range(0, 3));
// write partition f2 = 20
auto src_array2 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
["Lucy", [10.0, 10.0, 10.0, 10.0], 20, 15.1],
["Bob", [10.0, 11.0, 10.0, 11.0], 20, 16.1],
["Tony", [11.0, 10.0, 11.0, 10.0], 20, 17.1],
["Alice", [11.0, 11.0, 11.0, 11.0], 20, 18.1],
["Paul", [10.0, 10.0, 10.0, 10.0], 20, 19.1]
])")
.ValueOrDie();
write_data_and_index(src_array2, {{"f2", "20"}}, Range(4, 8));
auto scan_and_check_result = [&](const std::map<std::string, std::string>& partition,
const std::optional<RowRangeIndex>& row_range_index,
VectorSearch::PreFilter filter, int32_t limit,
const std::string& bitmap_result,
const std::string& lumina_result,
const std::shared_ptr<arrow::Array>& expected_array,
const std::map<int64_t, float>& id_to_score) {
std::vector<std::map<std::string, std::string>> partitions = {partition};
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt, partitions,
lumina_options, fs_, /*executor=*/nullptr, pool_));
// check bitmap index
ASSERT_OK_AND_ASSIGN(auto readers, global_index_scan->CreateReaders("f0", row_range_index));
ASSERT_EQ(readers.size(), 1u);
ASSERT_OK_AND_ASSIGN(auto result1,
readers[0]->VisitEqual(Literal(FieldType::STRING, "Alice", 5)));
ASSERT_OK_AND_ASSIGN(auto result2,
readers[0]->VisitEqual(Literal(FieldType::STRING, "Paul", 4)));
ASSERT_OK_AND_ASSIGN(auto index_result, result1->Or(result2));
ASSERT_EQ(index_result->ToString(), bitmap_result);
// check lumina index
ASSERT_OK_AND_ASSIGN(auto lumina_readers,
global_index_scan->CreateReaders("f1", row_range_index));
ASSERT_EQ(lumina_readers.size(), 1u);
auto lumina_reader = lumina_readers[0];
std::vector<float> query = {1.0f, 1.0f, 1.0f, 1.1f};
auto vector_search = std::make_shared<VectorSearch>(
"f1", limit, query, filter,
/*predicate=*/nullptr, /*distance_type=*/std::nullopt, /*options=*/lumina_options);
ASSERT_OK_AND_ASSIGN(auto scored_result, lumina_reader->VisitVectorSearch(vector_search));
ASSERT_EQ(scored_result->ToString(), lumina_result);
// check read array
std::vector<std::string> read_field_names = schema->field_names();
read_field_names.push_back("_INDEX_SCORE");
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, /*predicate=*/nullptr,
/*options=*/{}, scored_result));
ASSERT_OK(ReadData(table_path, read_field_names, expected_array,
/*predicate=*/nullptr, plan));
};
auto result_fields = fields;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
result_fields.push_back(SpecialFields::IndexScore().ArrowField());
std::map<int64_t, float> id_to_score1 = {{0, 4.21f}, {1, 2.01f}, {2, 2.21f}, {3, 0.01f}};
std::map<int64_t, float> id_to_score2 = {
{0, 322.21f}, {1, 360.01f}, {2, 360.21f}, {3, 398.01}, {4, 322.21f}};
{
// test scan and read for f2=10
auto filter = [](int64_t id) -> bool { return id == 0; };
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", [0.0, 0.0, 0.0, 0.0], 10, 11.1, 4.21]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(RowRangeIndex row_range_index, RowRangeIndex::Create({Range(0, 3)}));
scan_and_check_result({{"f2", "10"}}, row_range_index, filter, /*limit=*/2, "{0}",
"row ids: {0}, scores: {4.21}", expected_array, id_to_score1);
}
{
// test scan and read for f2=20
auto filter = [](int64_t id) -> bool { return id == 7 || id == 8; };
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Paul", [10.0, 10.0, 10.0, 10.0], 20, 19.1, 322.21]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(RowRangeIndex row_range_index, RowRangeIndex::Create({Range(4, 8)}));
scan_and_check_result({{"f2", "20"}}, row_range_index, filter, /*limit=*/1, "{7,8}",
"row ids: {8}, scores: {322.21}", expected_array, id_to_score2);
}
{
// test invalid partition input
ASSERT_NOK_WITH_MSG(
GlobalIndexScan::Create(
table_path, /*snapshot_id=*/std::nullopt,
/*partitions=*/std::vector<std::map<std::string, std::string>>(), lumina_options,
fs_, /*executor=*/nullptr, pool_),
"invalid input partition, supposed to be null or at least one partition");
}
}
TEST_P(GlobalIndexTest, TestWriteCommitScanReadIndexWithScore) {
arrow::FieldVector fields = {
arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::list(arrow::float32())),
arrow::field("f2", arrow::int32()), arrow::field("f3", arrow::float64())};
std::map<std::string, std::string> lumina_options = {{"lumina.index.dimension", "4"},
{"lumina.index.type", "bruteforce"},
{"lumina.distance.metric", "l2"},
{"lumina.encoding.type", "rawf32"},
{"lumina.search.parallel_number", "10"}};
auto schema = arrow::schema(fields);
std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"},
{Options::FILE_FORMAT, file_format_},
{Options::FILE_SYSTEM, "local"},
{Options::ROW_TRACKING_ENABLED, "true"},
{Options::DATA_EVOLUTION_ENABLED, "true"}};
CreateTable(/*partition_keys=*/{}, schema, options);
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
std::vector<std::string> write_cols = schema->field_names();
auto src_array = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
["Alice", [0.0, 0.0, 0.0, 0.0], 10, 11.1],
["Bob", [0.0, 1.0, 0.0, 1.0], 10, 12.1],
["Emily", [1.0, 0.0, 1.0, 0.0], 10, 13.1],
["Tony", [1.0, 1.0, 1.0, 1.0], 10, 14.1],
["NullGuy1", null, 10, 20.0],
["Lucy", [10.0, 10.0, 10.0, 10.0], 20, 15.1],
["Bob", [10.0, 11.0, 10.0, 11.0], 20, 16.1],
["NullGuy2", null, 20, 21.0],
["Tony", [11.0, 10.0, 11.0, 10.0], 20, 17.1],
["Alice", [11.0, 11.0, 11.0, 11.0], 20, 18.1],
["Paul", [10.0, 10.0, 10.0, 10.0], 20, 19.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
// write and commit lumina index
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f1", "lumina",
/*options=*/lumina_options, Range(0, 10)));
auto scan_and_check_result = [&](const std::vector<Range>& read_row_ranges,
const std::shared_ptr<arrow::Array>& expected_array,
const std::map<int64_t, float>& id_to_score) {
// check read array
std::vector<std::string> read_field_names = schema->field_names();
read_field_names.push_back("_INDEX_SCORE");
ASSERT_OK_AND_ASSIGN(auto plan,
ScanDataWithIndexResult(table_path, read_row_ranges, id_to_score));
ASSERT_OK(ReadData(table_path, read_field_names, expected_array,
/*predicate=*/nullptr, plan));
};
auto result_fields = fields;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
result_fields.push_back(SpecialFields::IndexScore().ArrowField());
std::map<int64_t, float> id_to_score = {{0, 4.21f}, {1, 2.01f}, {2, 2.21f},
{3, 0.01f}, {5, 322.21f}, {6, 360.01f},
{8, 360.21f}, {9, 398.01f}, {10, 322.21f}};
{
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", [0.0, 0.0, 0.0, 0.0], 10, 11.1, 4.21],
[0, "Bob", [0.0, 1.0, 0.0, 1.0], 10, 12.1, 2.01],
[0, "Emily", [1.0, 0.0, 1.0, 0.0], 10, 13.1, 2.21],
[0, "Tony", [1.0, 1.0, 1.0, 1.0], 10, 14.1, 0.01],
[0, "Lucy", [10.0, 10.0, 10.0, 10.0], 20, 15.1, 322.21],
[0, "Bob", [10.0, 11.0, 10.0, 11.0], 20, 16.1, 360.01],
[0, "Tony", [11.0, 10.0, 11.0, 10.0], 20, 17.1, 360.21],
[0, "Alice", [11.0, 11.0, 11.0, 11.0], 20, 18.1, 398.01],
[0, "Paul", [10.0, 10.0, 10.0, 10.0], 20, 19.1, 322.21]
])")
.ValueOrDie();
scan_and_check_result({Range(0, 3), Range(5, 6), Range(8, 10)}, expected_array,
id_to_score);
}
{
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Emily", [1.0, 0.0, 1.0, 0.0], 10, 13.1, 2.21],
[0, "Tony", [1.0, 1.0, 1.0, 1.0], 10, 14.1, 0.01],
[0, "Alice", [11.0, 11.0, 11.0, 11.0], 20, 18.1, 398.01],
[0, "Paul", [10.0, 10.0, 10.0, 10.0], 20, 19.1, 322.21]
])")
.ValueOrDie();
scan_and_check_result({Range(2, 3), Range(9, 10)}, expected_array, id_to_score);
}
{
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Bob", [10.0, 11.0, 10.0, 11.0], 20, 16.1, 360.01]
])")
.ValueOrDie();
scan_and_check_result({Range(6, 6)}, expected_array, id_to_score);
}
{
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Emily", [1.0, 0.0, 1.0, 0.0], 10, 13.1, null],
[0, "Tony", [1.0, 1.0, 1.0, 1.0], 10, 14.1, null],
[0, "Alice", [11.0, 11.0, 11.0, 11.0], 20, 18.1, null],
[0, "Paul", [10.0, 10.0, 10.0, 10.0], 20, 19.1, null]
])")
.ValueOrDie();
scan_and_check_result({Range(2, 3), Range(9, 10)}, expected_array, /*id_to_score=*/{});
}
{
// Verify null rows (id 4 and 7) are never recalled by vector search
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt,
/*partitions=*/std::nullopt, lumina_options, fs_,
/*executor=*/nullptr, pool_));
ASSERT_OK_AND_ASSIGN(auto lumina_readers,
global_index_scan->CreateReaders("f1", std::nullopt));
ASSERT_EQ(lumina_readers.size(), 1u);
std::vector<float> query = {1.0f, 1.0f, 1.0f, 1.1f};
auto vector_search = std::make_shared<VectorSearch>(
"f1", /*limit=*/20, query, /*filter=*/nullptr,
/*predicate=*/nullptr, /*distance_type=*/std::nullopt, /*options=*/lumina_options);
ASSERT_OK_AND_ASSIGN(auto scored_result,
lumina_readers[0]->VisitVectorSearch(vector_search));
auto typed_result = std::dynamic_pointer_cast<BitmapScoredGlobalIndexResult>(scored_result);
ASSERT_TRUE(typed_result);
// Should recall 9 vectors (11 total rows - 2 null rows)
ASSERT_EQ(typed_result->bitmap_.Cardinality(), 9u);
// Null row ids 4 and 7 must not be in the result
ASSERT_FALSE(typed_result->bitmap_.Contains(4));
ASSERT_FALSE(typed_result->bitmap_.Contains(7));
}
}
#endif
TEST_P(GlobalIndexTest, TestDataEvolutionBatchScan) {
CreateTable();
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
auto schema = arrow::schema(fields_);
// write and commit data
std::vector<std::string> write_cols = schema->field_names();
auto src_array = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Alice", 10, 1, 11.1],
["Bob", 10, 1, 12.1],
["Emily", 10, 0, 13.1],
["Tony", 10, 0, 14.1],
["Lucy", 20, 1, 15.1],
["Bob", 10, 1, 16.1],
["Tony", 20, 0, 17.1],
["Alice", 20, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
auto result_fields = fields_;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
auto expected_all_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1],
[0, "Bob", 10, 1, 12.1],
[0, "Emily", 10, 0, 13.1],
[0, "Tony", 10, 0, 14.1],
[0, "Lucy", 20, 1, 15.1],
[0, "Bob", 10, 1, 16.1],
[0, "Tony", 20, 0, 17.1],
[0, "Alice", 20, null, 18.1]
])")
.ValueOrDie();
{
// read when no index is built
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
ASSERT_OK(ReadData(table_path, write_cols, expected_all_array, predicate, plan));
}
// write and commit global index
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f0", "bitmap", /*options=*/{},
Range(0, 7)));
// scan and read with global index
{
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1],
[0, "Alice", 20, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice2", 6));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
ASSERT_OK(ReadData(table_path, write_cols, /*expected_array=*/nullptr, predicate, plan));
}
{
auto predicate1 =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
auto predicate2 =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Bob", 3));
ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::Or({predicate1, predicate2}));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1],
[0, "Bob", 10, 1, 12.1],
[0, "Bob", 10, 1, 16.1],
[0, "Alice", 20, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, /*predicate=*/nullptr));
ASSERT_OK(
ReadData(table_path, write_cols, expected_all_array, /*predicate=*/nullptr, plan));
}
{
// f1 does not have global index
auto predicate = PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(19));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
ASSERT_OK(ReadData(table_path, write_cols, expected_all_array, predicate, plan));
}
{
// disable global index
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
ASSERT_OK_AND_ASSIGN(
auto plan,
ScanGlobalIndexAndData(table_path, predicate, {{"global-index.enabled", "false"}}));
ASSERT_OK(ReadData(table_path, write_cols, expected_all_array, predicate, plan));
}
}
TEST_P(GlobalIndexTest, TestDataEvolutionBatchScanWithOnlyOnePartitionHasIndex) {
CreateTable(/*partition_keys=*/{"f1"});
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
auto schema = arrow::schema(fields_);
// write and commit data
std::vector<std::string> write_cols = schema->field_names();
auto src_array1 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Alice", 10, 1, 11.1],
["Bob", 10, 1, 12.1],
["Emily", 10, 0, 13.1],
["Tony", 10, 0, 14.1],
["Bob", 10, 1, 16.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs1,
WriteArray(table_path, {{"f1", "10"}}, write_cols, src_array1));
ASSERT_OK(Commit(table_path, commit_msgs1));
auto src_array2 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Lucy", 20, 1, 15.1],
["Tony", 20, 0, 17.1],
["Alice", 20, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs2,
WriteArray(table_path, {{"f1", "20"}}, write_cols, src_array2));
ASSERT_OK(Commit(table_path, commit_msgs2));
// write and commit global index for f1 = 10
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{{{"f1", "10"}}}, "f0", "bitmap",
/*options=*/{}, Range(0, 4)));
auto result_fields = fields_;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
{
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
}
TEST_P(GlobalIndexTest, TestDataEvolutionBatchScanWithTwoIndexInDiffTwoPartition) {
CreateTable(/*partition_keys=*/{"f1"});
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
auto schema = arrow::schema(fields_);
// write and commit data
std::vector<std::string> write_cols = schema->field_names();
auto src_array1 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Alice", 10, 1, 11.1],
["Bob", 10, 1, 12.1],
["Emily", 10, 0, 13.1],
["Tony", 10, 0, 14.1],
["Bob", 10, 1, 16.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs1,
WriteArray(table_path, {{"f1", "10"}}, write_cols, src_array1));
ASSERT_OK(Commit(table_path, commit_msgs1));
auto src_array2 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Lucy", 20, 1, 15.1],
["Tony", 20, 0, 17.1],
["Alice", 20, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs2,
WriteArray(table_path, {{"f1", "20"}}, write_cols, src_array2));
ASSERT_OK(Commit(table_path, commit_msgs2));
// write and commit global index for f1 = 10
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{{{"f1", "10"}}}, "f0", "bitmap",
/*options=*/{}, Range(0, 4)));
// write and commit global index for f1 = 20
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{{{"f1", "20"}}}, "f2", "bitmap",
/*options=*/{}, Range(5, 7)));
auto result_fields = fields_;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
{
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
auto predicate = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2",
FieldType::INT, Literal(1));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Lucy", 20, 1, 15.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
// only f1 = 10 partition has f0 index, query predicate1 results in ["Alice", 10, 1, 11.1]
// only f2 = 20 partition has f2 index, query predicate2 results in ["Lucy", 20, 1, 15.1]
auto predicate1 =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
auto predicate2 = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2",
FieldType::INT, Literal(1));
ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({predicate1, predicate2}));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
ASSERT_OK(ReadData(table_path, write_cols, /*expected_array=*/nullptr, predicate, plan));
}
{
// predicate2 is partition filter
auto predicate1 =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
auto predicate2 = PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(10));
ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({predicate1, predicate2}));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
}
TEST_P(GlobalIndexTest, TestDataEvolutionBatchScanWithTwoPartitionAllWithIndex) {
CreateTable(/*partition_keys=*/{"f1"});
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
auto schema = arrow::schema(fields_);
// write and commit data
std::vector<std::string> write_cols = schema->field_names();
auto src_array1 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Alice", 10, 1, 11.1],
["Bob", 10, 1, 12.1],
["Emily", 10, 0, 13.1],
["Tony", 10, 0, 14.1],
["Bob", 10, 1, 16.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs1,
WriteArray(table_path, {{"f1", "10"}}, write_cols, src_array1));
ASSERT_OK(Commit(table_path, commit_msgs1));
auto src_array2 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Lucy", 20, 1, 15.1],
["Tony", 20, 0, 17.1],
["Alice", 20, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs2,
WriteArray(table_path, {{"f1", "20"}}, write_cols, src_array2));
ASSERT_OK(Commit(table_path, commit_msgs2));
// write and commit global index for f1 = 10
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{{{"f1", "10"}}}, "f0", "bitmap",
/*options=*/{}, Range(0, 4)));
// write and commit global index for f1 = 20
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{{{"f1", "20"}}}, "f0", "bitmap",
/*options=*/{}, Range(5, 7)));
auto result_fields = fields_;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
{
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1],
[0, "Alice", 20, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
// predicate2 is partition filter
auto predicate1 =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
auto predicate2 = PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(10));
ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({predicate1, predicate2}));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
// predicate2 is partition filter
auto predicate1 =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
auto predicate2 = PredicateBuilder::LessThan(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(20));
ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({predicate1, predicate2}));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
// predicate2 is partition filter
auto predicate1 =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
auto predicate2 = PredicateBuilder::LessThan(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(30));
ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({predicate1, predicate2}));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1],
[0, "Alice", 20, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
}
TEST_P(GlobalIndexTest, TestDataEvolutionBatchScanWithPartitionWithTwoFields) {
CreateTable(/*partition_keys=*/{"f1", "f2"});
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
auto schema = arrow::schema(fields_);
// write and commit data
std::vector<std::string> write_cols = schema->field_names();
auto src_array1 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Alice", 10, 1, 11.1],
["Bob", 10, 1, 12.1],
["Bob", 10, 1, 16.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, {{"f1", "10"}, {"f2", "1"}},
write_cols, src_array1));
ASSERT_OK(Commit(table_path, commit_msgs1));
auto src_array2 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Lucy", 20, 1, 15.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs2, WriteArray(table_path, {{"f1", "20"}, {"f2", "1"}},
write_cols, src_array2));
ASSERT_OK(Commit(table_path, commit_msgs2));
auto src_array3 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Emily", 10, 0, 13.1],
["Tony", 10, 0, 14.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs3, WriteArray(table_path, {{"f1", "10"}, {"f2", "0"}},
write_cols, src_array3));
ASSERT_OK(Commit(table_path, commit_msgs3));
// build index
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{{{"f1", "10"}, {"f2", "1"}}}, "f0",
"bitmap",
/*options=*/{}, Range(0, 2)));
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{{{"f1", "20"}, {"f2", "1"}}}, "f0",
"bitmap",
/*options=*/{}, Range(3, 3)));
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{{{"f1", "10"}, {"f2", "0"}}}, "f0",
"bitmap",
/*options=*/{}, Range(4, 5)));
auto result_fields = fields_;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
{
auto predicate1 = PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(10));
auto predicate2 =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Bob", 3));
ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({predicate1, predicate2}));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Bob", 10, 1, 12.1],
[0, "Bob", 10, 1, 16.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
auto predicate1 = PredicateBuilder::GreaterThan(
/*field_index=*/1, /*field_name=*/"f1", FieldType::INT, Literal(10));
auto predicate2 = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2",
FieldType::INT, Literal(1));
ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::Or({predicate1, predicate2}));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1],
[0, "Bob", 10, 1, 12.1],
[0, "Bob", 10, 1, 16.1],
[0, "Lucy", 20, 1, 15.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
auto predicate1 = PredicateBuilder::GreaterThan(
/*field_index=*/1, /*field_name=*/"f1", FieldType::INT, Literal(10));
auto predicate2 = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f2",
FieldType::INT, Literal(1));
ASSERT_OK_AND_ASSIGN(auto or_predicate, PredicateBuilder::Or({predicate1, predicate2}));
auto predicate3 =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Emily", 5));
ASSERT_OK_AND_ASSIGN(auto and_predicate, PredicateBuilder::And({predicate3, or_predicate}));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, and_predicate));
ASSERT_OK(
ReadData(table_path, write_cols, /*expected_array=*/nullptr, and_predicate, plan));
}
}
#ifdef PAIMON_ENABLE_LUMINA
TEST_P(GlobalIndexTest, TestScanIndexWithTwoIndexes) {
arrow::FieldVector fields = {
arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::list(arrow::float32())),
arrow::field("f2", arrow::int32()), arrow::field("f3", arrow::float64())};
std::map<std::string, std::string> lumina_options = {{"lumina.index.dimension", "4"},
{"lumina.index.type", "bruteforce"},
{"lumina.distance.metric", "l2"},
{"lumina.encoding.type", "rawf32"},
{"lumina.search.parallel_number", "10"}};
auto schema = arrow::schema(fields);
std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"},
{Options::FILE_FORMAT, file_format_},
{Options::FILE_SYSTEM, "local"},
{Options::ROW_TRACKING_ENABLED, "true"},
{Options::DATA_EVOLUTION_ENABLED, "true"}};
CreateTable(/*partition_keys=*/{}, schema, options);
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
std::vector<std::string> write_cols = schema->field_names();
auto src_array = std::dynamic_pointer_cast<arrow::StructArray>(
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
["Alice", [0.0, 0.0, 0.0, 0.0], 10, 11.1],
["Bob", [0.0, 1.0, 0.0, 1.0], 10, 12.1],
["Emily", [1.0, 0.0, 1.0, 0.0], 10, 13.1],
["Tony", [1.0, 1.0, 1.0, 1.0], 10, 14.1],
["Lucy", [10.0, 10.0, 10.0, 10.0], 20, 15.1],
["Bob", [10.0, 11.0, 10.0, 11.0], 20, 16.1],
["Tony", [11.0, 10.0, 11.0, 10.0], 20, 17.1],
["Alice", [11.0, 11.0, 11.0, 11.0], 20, 18.1],
["Paul", [10.0, 10.0, 10.0, 10.0], 20, 19.1]
])")
.ValueOrDie());
ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
// write and commit bitmap global index
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f0", "bitmap",
/*options=*/{}, Range(0, 8)));
// write and commit lumina global index
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f1", "lumina",
/*options=*/lumina_options, Range(0, 8)));
ASSERT_OK_AND_ASSIGN(
auto global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt,
/*partitions=*/std::nullopt,
/*options=*/lumina_options, fs_, /*executor=*/nullptr, pool_));
// query f0
ASSERT_OK_AND_ASSIGN(auto index_readers, global_index_scan->CreateReaders("f0", std::nullopt));
ASSERT_EQ(index_readers.size(), 1);
ASSERT_OK_AND_ASSIGN(auto index_result,
index_readers[0]->VisitEqual(Literal(FieldType::STRING, "Alice", 5)));
ASSERT_EQ(index_result->ToString(), "{0,7}");
// query f1
ASSERT_OK_AND_ASSIGN(index_readers, global_index_scan->CreateReaders("f1", std::nullopt));
ASSERT_EQ(index_readers.size(), 1);
std::vector<float> query = {11.0f, 11.0f, 11.0f, 11.0f};
ASSERT_OK_AND_ASSIGN(
auto scored_result,
index_readers[0]->VisitVectorSearch(std::make_shared<VectorSearch>(
"f1", 1, query, /*filter=*/nullptr,
/*predicate=*/nullptr, /*distance_type=*/std::nullopt, /*options=*/lumina_options)));
ASSERT_EQ(scored_result->ToString(), "row ids: {7}, scores: {0.00}");
// query f2
ASSERT_OK_AND_ASSIGN(index_readers, global_index_scan->CreateReaders("f2", std::nullopt));
ASSERT_EQ(index_readers.size(), 0);
}
#endif
TEST_P(GlobalIndexTest, TestDataEvolutionBatchScanWithExternalPath) {
arrow::FieldVector fields = {
arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::list(arrow::float32())),
arrow::field("f2", arrow::int32()), arrow::field("f3", arrow::float64())};
auto schema = arrow::schema(fields);
std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"},
{Options::FILE_FORMAT, file_format_},
{Options::FILE_SYSTEM, "local"},
{Options::ROW_TRACKING_ENABLED, "true"},
{Options::DATA_EVOLUTION_ENABLED, "true"}};
CreateTable(/*partition_keys=*/{}, schema, options);
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
std::vector<std::string> write_cols = schema->field_names();
auto src_array = std::dynamic_pointer_cast<arrow::StructArray>(
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
["Alice", [0.0, 0.0, 0.0, 0.0], 10, 11.1],
["Bob", [0.0, 1.0, 0.0, 1.0], 10, 12.1],
["Emily", [1.0, 0.0, 1.0, 0.0], 10, 13.1],
["Tony", [1.0, 1.0, 1.0, 1.0], 10, 14.1],
["Lucy", [10.0, 10.0, 10.0, 10.0], 20, 15.1],
["Bob", [10.0, 11.0, 10.0, 11.0], 20, 16.1],
["Tony", [11.0, 10.0, 11.0, 10.0], 20, 17.1],
["Alice", [11.0, 11.0, 11.0, 11.0], 20, 18.1],
["Paul", [10.0, 10.0, 10.0, 10.0], 20, 19.1]
])")
.ValueOrDie());
ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
// write and commit bitmap global index
auto external_dir1 = UniqueTestDirectory::Create("local");
std::map<std::string, std::string> index_options = {
{"global-index.external-path", "FILE://" + external_dir1->Str()}};
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f0", "bitmap", index_options,
Range(0, 8)));
auto result_fields = fields;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
// test scan and read
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate, index_options));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", [0.0, 0.0, 0.0, 0.0], 10, 11.1],
[0, "Alice", [11.0, 11.0, 11.0, 11.0], 20, 18.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
TEST_P(GlobalIndexTest, TestIOException) {
arrow::FieldVector fields = {
arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::list(arrow::float32())),
arrow::field("f2", arrow::int32()), arrow::field("f3", arrow::float64())};
auto schema = arrow::schema(fields);
std::vector<std::string> write_cols = schema->field_names();
auto src_array = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
["Alice", [0.0, 0.0, 0.0, 0.0], 10, 11.1],
["Bob", [0.0, 1.0, 0.0, 1.0], 10, 12.1],
["Alice", [1.0, 0.0, 1.0, 0.0], 10, 13.1],
["Tony", [1.0, 1.0, 1.0, 1.0], 10, 14.1]
])")
.ValueOrDie();
std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"},
{Options::FILE_FORMAT, file_format_},
{Options::FILE_SYSTEM, "local"},
{Options::ROW_TRACKING_ENABLED, "true"},
{Options::DATA_EVOLUTION_ENABLED, "true"}};
std::string table_path;
bool write_run_complete = false;
auto io_hook = IOHook::GetInstance();
for (size_t i = 0; i < 2000; i += paimon::test::RandomNumber(20, 30)) {
ScopeGuard guard([&io_hook]() { io_hook->Clear(); });
dir_ = UniqueTestDirectory::Create("local");
// create table and write data
CreateTable(/*partition_keys=*/{}, schema, options);
table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
io_hook->Reset(i, IOHook::Mode::RETURN_ERROR);
// write bitmap index
auto bitmap_index_write_status =
WriteIndex(table_path, /*partition_filters=*/{}, "f0", "bitmap",
/*options=*/{}, Range(0, 3));
CHECK_HOOK_STATUS(bitmap_index_write_status, i);
write_run_complete = true;
break;
}
ASSERT_TRUE(write_run_complete);
// read for bitmap
bool read_run_complete = false;
for (size_t i = 0; i < 2000; i += paimon::test::RandomNumber(20, 30)) {
ScopeGuard guard([&io_hook]() { io_hook->Clear(); });
io_hook->Reset(i, IOHook::Mode::RETURN_ERROR);
auto result_fields = fields;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", [0.0, 0.0, 0.0, 0.0], 10, 11.1],
[0, "Alice", [1.0, 0.0, 1.0, 0.0], 10, 13.1]
])")
.ValueOrDie();
auto plan_result = ScanGlobalIndexAndData(table_path, predicate);
CHECK_HOOK_STATUS_WITHOUT_MESSAGE_CHECK(plan_result.status());
auto plan = std::move(plan_result).value();
auto read_status = ReadData(table_path, write_cols, expected_array, predicate, plan);
CHECK_HOOK_STATUS(read_status, i);
read_run_complete = true;
break;
}
ASSERT_TRUE(read_run_complete);
}
TEST_P(GlobalIndexTest, TestDataEvolutionBatchScanWithRangeBitmap) {
CreateTable();
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
auto schema = arrow::schema(fields_);
std::vector<std::string> write_cols = schema->field_names();
auto src_array = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Alice", 10, 1, 11.1],
["Bob", 10, 1, 12.1],
["Emily", 15, 0, 13.1],
["Tony", 20, 0, 14.1],
["Lucy", 20, 1, 15.1],
["Bob", 25, 1, 16.1],
["Tony", 30, 0, 17.1],
["Alice", 30, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
auto result_fields = fields_;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
auto expected_all_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1],
[0, "Bob", 10, 1, 12.1],
[0, "Emily", 15, 0, 13.1],
[0, "Tony", 20, 0, 14.1],
[0, "Lucy", 20, 1, 15.1],
[0, "Bob", 25, 1, 16.1],
[0, "Tony", 30, 0, 17.1],
[0, "Alice", 30, null, 18.1]
])")
.ValueOrDie();
{
// read when no index is built
auto predicate = PredicateBuilder::LessThan(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(20));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
ASSERT_OK(ReadData(table_path, write_cols, expected_all_array, predicate, plan));
}
// write and commit range-bitmap global index on f1 (int32)
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f1", "range-bitmap", /*options=*/{},
Range(0, 7)));
// scan and read with range-bitmap global index
{
// f1 < 20: rows with f1=10,10,15 -> Alice(10), Bob(10), Emily(15)
auto predicate = PredicateBuilder::LessThan(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(20));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1],
[0, "Bob", 10, 1, 12.1],
[0, "Emily", 15, 0, 13.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
// f1 >= 25: rows with f1=25,30,30 -> Bob(25), Tony(30), Alice(30)
auto predicate = PredicateBuilder::GreaterOrEqual(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(25));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Bob", 25, 1, 16.1],
[0, "Tony", 30, 0, 17.1],
[0, "Alice", 30, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
// f1 > 30: no rows match
auto predicate = PredicateBuilder::GreaterThan(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(30));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
ASSERT_OK(ReadData(table_path, write_cols, /*expected_array=*/nullptr, predicate, plan));
}
{
// f1 <= 10: rows with f1=10,10 -> Alice(10), Bob(10)
auto predicate = PredicateBuilder::LessOrEqual(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(10));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1],
[0, "Bob", 10, 1, 12.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
// no predicate: return all rows
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, /*predicate=*/nullptr));
ASSERT_OK(
ReadData(table_path, write_cols, expected_all_array, /*predicate=*/nullptr, plan));
}
{
// f0 does not have range-bitmap index, should return all rows
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
ASSERT_OK(ReadData(table_path, write_cols, expected_all_array, predicate, plan));
}
}
TEST_P(GlobalIndexTest, TestDataEvolutionBatchScanWithRangeBitmapAndBitmap) {
CreateTable();
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
auto schema = arrow::schema(fields_);
std::vector<std::string> write_cols = schema->field_names();
auto src_array = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Alice", 10, 1, 11.1],
["Bob", 10, 1, 12.1],
["Emily", 15, 0, 13.1],
["Tony", 20, 0, 14.1],
["Lucy", 20, 1, 15.1],
["Bob", 25, 1, 16.1],
["Tony", 30, 0, 17.1],
["Alice", 30, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
auto result_fields = fields_;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
auto expected_all_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1],
[0, "Bob", 10, 1, 12.1],
[0, "Emily", 15, 0, 13.1],
[0, "Tony", 20, 0, 14.1],
[0, "Lucy", 20, 1, 15.1],
[0, "Bob", 25, 1, 16.1],
[0, "Tony", 30, 0, 17.1],
[0, "Alice", 30, null, 18.1]
])")
.ValueOrDie();
// write and commit bitmap global index on f0 (string)
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f0", "bitmap", /*options=*/{},
Range(0, 7)));
// write and commit range-bitmap global index on f1 (int32)
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f1", "range-bitmap", /*options=*/{},
Range(0, 7)));
// scan and read with both indexes
{
// bitmap: f0 == "Alice" AND range-bitmap: f1 < 20
// Alice has f1=10 and f1=30, only f1=10 < 20 -> Alice(10)
auto predicate1 =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
auto predicate2 = PredicateBuilder::LessThan(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(20));
ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({predicate1, predicate2}));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
// bitmap: f0 == "Bob" AND range-bitmap: f1 >= 20
// Bob has f1=10 and f1=25, only f1=25 >= 20 -> Bob(25)
auto predicate1 =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Bob", 3));
auto predicate2 = PredicateBuilder::GreaterOrEqual(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(20));
ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({predicate1, predicate2}));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Bob", 25, 1, 16.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
// bitmap: (f0 == "Alice" OR f0 == "Tony") AND range-bitmap: f1 > 20
// Alice: f1=10,30 -> f1=30 > 20; Tony: f1=20,30 -> f1=30 > 20
auto predicate_alice =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
auto predicate_tony =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Tony", 4));
ASSERT_OK_AND_ASSIGN(auto or_predicate,
PredicateBuilder::Or({predicate_alice, predicate_tony}));
auto range_predicate = PredicateBuilder::GreaterThan(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(20));
ASSERT_OK_AND_ASSIGN(auto predicate,
PredicateBuilder::And({or_predicate, range_predicate}));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Tony", 30, 0, 17.1],
[0, "Alice", 30, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
// only bitmap predicate: f0 == "Emily"
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Emily", 5));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Emily", 15, 0, 13.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
// only range-bitmap predicate: f1 <= 15
auto predicate = PredicateBuilder::LessOrEqual(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(15));
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, predicate));
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1],
[0, "Bob", 10, 1, 12.1],
[0, "Emily", 15, 0, 13.1]
])")
.ValueOrDie();
ASSERT_OK(ReadData(table_path, write_cols, expected_array, predicate, plan));
}
{
// no predicate: return all rows
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, /*predicate=*/nullptr));
ASSERT_OK(
ReadData(table_path, write_cols, expected_all_array, /*predicate=*/nullptr, plan));
}
}
#ifdef PAIMON_ENABLE_LUCENE
TEST_P(GlobalIndexTest, TestLuceneWriteCommitScanReadIndexWithScore) {
arrow::FieldVector fields = {arrow::field("f0", arrow::utf8()),
arrow::field("f1", arrow::int32())};
auto tmp_dir = paimon::test::UniqueTestDirectory::Create();
ASSERT_TRUE(tmp_dir);
std::map<std::string, std::string> lucene_options = {
{"lucene-fts.write.omit-term-freq-and-position", "false"},
{"lucene-fts.write.tmp.directory", tmp_dir->Str()}};
auto schema = arrow::schema(fields);
std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"},
{Options::FILE_FORMAT, file_format_},
{Options::FILE_SYSTEM, "local"},
{Options::ROW_TRACKING_ENABLED, "true"},
{Options::DATA_EVOLUTION_ENABLED, "true"}};
CreateTable(/*partition_keys=*/{}, schema, options);
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
std::vector<std::string> write_cols = schema->field_names();
auto src_array = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
["This is an test document.", 0],
["This is an new document document document.", 1],
["Document document document document test.", 2],
["unordered user-defined doc id", 3]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
// write and commit lucene index
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f0", "lucene-fts",
/*options=*/lucene_options, Range(0, 3)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt,
/*partitions=*/std::nullopt, /*options=*/{}, fs_,
/*executor=*/nullptr, pool_));
// test f0 field
ASSERT_OK_AND_ASSIGN(auto index_readers,
global_index_scan->CreateReaders("f0", /*row_range_index=*/std::nullopt));
ASSERT_EQ(index_readers.size(), 1u);
auto index_reader = index_readers[0];
{
ASSERT_OK_AND_ASSIGN(auto index_result,
index_reader->VisitFullTextSearch(std::make_shared<FullTextSearch>(
"f0",
/*limit=*/10, "document", FullTextSearch::SearchType::MATCH_ALL,
/*pre_filter=*/std::nullopt)));
ASSERT_TRUE(index_result->ToString().find("row ids: {0,1,2}") != std::string::npos);
}
{
std::optional<RoaringBitmap64> pre_filter = RoaringBitmap64::From({1, 2, 3});
ASSERT_OK_AND_ASSIGN(
auto index_result,
index_reader->VisitFullTextSearch(std::make_shared<FullTextSearch>(
"f0",
/*limit=*/10, "document", FullTextSearch::SearchType::MATCH_ALL, pre_filter)));
ASSERT_TRUE(index_result->ToString().find("row ids: {1,2}") != std::string::npos);
}
{
ASSERT_OK_AND_ASSIGN(auto index_result,
index_reader->VisitFullTextSearch(std::make_shared<FullTextSearch>(
"f0",
/*limit=*/10, "*or*er*", FullTextSearch::SearchType::WILDCARD,
/*pre_filter=*/std::nullopt)));
ASSERT_TRUE(index_result->ToString().find("row ids: {3}") != std::string::npos);
}
}
TEST_P(GlobalIndexTest, TestWriteCommitScanReadLuceneIndexWithPartition) {
arrow::FieldVector fields = {arrow::field("f0", arrow::utf8()),
arrow::field("f1", arrow::int32())};
auto tmp_dir = paimon::test::UniqueTestDirectory::Create();
ASSERT_TRUE(tmp_dir);
std::map<std::string, std::string> lucene_options = {
{"lucene-fts.write.omit-term-freq-and-position", "false"},
{"lucene-fts.write.tmp.directory", tmp_dir->Str()}};
auto schema = arrow::schema(fields);
std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"},
{Options::FILE_FORMAT, file_format_},
{Options::FILE_SYSTEM, "local"},
{Options::ROW_TRACKING_ENABLED, "true"},
{Options::DATA_EVOLUTION_ENABLED, "true"}};
CreateTable(/*partition_keys=*/{"f1"}, schema, options);
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
std::vector<std::string> write_cols = schema->field_names();
auto write_data_and_index = [&](const std::shared_ptr<arrow::Array>& src_array,
const std::map<std::string, std::string>& partition,
const Range& expected_range) {
ASSERT_OK_AND_ASSIGN(auto commit_msgs,
WriteArray(table_path, partition, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
// write lucene index
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{partition}, "f0", "lucene-fts",
/*options=*/lucene_options, expected_range));
};
// write partition f1 = 10
auto src_array1 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
["This is an test document.", 10],
["This is an new document document document.", 10]
])")
.ValueOrDie();
write_data_and_index(src_array1, {{"f1", "10"}}, Range(0, 1));
// write partition f1 = 20
auto src_array2 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
["Document document document document test.", 20],
["unordered user-defined doc id", 20]
])")
.ValueOrDie();
write_data_and_index(src_array2, {{"f1", "20"}}, Range(2, 3));
auto scan_and_check_result = [&](const std::map<std::string, std::string>& partition,
const std::optional<RowRangeIndex>& row_range_index,
const std::optional<RoaringBitmap64>& pre_filter,
const std::string& index_expected) {
std::vector<std::map<std::string, std::string>> partitions = {partition};
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt, partitions,
lucene_options, fs_, /*executor=*/nullptr, pool_));
// check lucene index
ASSERT_OK_AND_ASSIGN(auto readers, global_index_scan->CreateReaders("f0", row_range_index));
ASSERT_EQ(readers.size(), 1u);
ASSERT_OK_AND_ASSIGN(
auto index_result,
readers[0]->VisitFullTextSearch(std::make_shared<FullTextSearch>(
"f0",
/*limit=*/10, "document", FullTextSearch::SearchType::MATCH_ALL, pre_filter)));
ASSERT_TRUE(index_result->ToString().find(index_expected) != std::string::npos);
};
{
// test scan and read for f1=10
auto filter = RoaringBitmap64::From(std::vector<int64_t>({0l}));
ASSERT_OK_AND_ASSIGN(RowRangeIndex row_range_index, RowRangeIndex::Create({Range(0, 1)}));
scan_and_check_result({{"f1", "10"}}, row_range_index, filter, "row ids: {0}");
}
{
// test scan and read for f1=20
auto filter = RoaringBitmap64::From(std::vector<int64_t>({2l}));
ASSERT_OK_AND_ASSIGN(RowRangeIndex row_range_index, RowRangeIndex::Create({Range(2, 3)}));
scan_and_check_result({{"f1", "20"}}, row_range_index, filter, "row ids: {2}");
}
}
#endif
TEST_P(GlobalIndexTest, TestBTreeWriteCommitScanReadIndex) {
// BTreeGlobalIndexWriter requires keys to be written in monotonically increasing order.
// Therefore the source data must be pre-sorted by the indexed column (f0, string).
CreateTable();
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
auto schema = arrow::schema(fields_);
std::vector<std::string> write_cols = schema->field_names();
// Data sorted by f0 (string, ascending): Alice < Bob < Bob < Emily < Lucy < Tony < Tony
// The last row has f0=null which is treated separately by the null bitmap.
auto src_array = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Alice", 10, 1, 11.1],
["Bob", 10, 1, 12.1],
["Bob", 20, 0, 16.1],
["Emily", 10, 0, 13.1],
["Lucy", 20, 1, 15.1],
["Tony", 10, 0, 14.1],
["Tony", 20, 0, 17.1],
[null, 20, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
// Write btree-global index on f0
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f0", "btree",
/*options=*/{}, Range(0, 7)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt,
/*partitions=*/std::nullopt, /*options=*/{}, fs_,
/*executor=*/nullptr, pool_));
ASSERT_OK_AND_ASSIGN(auto index_readers,
global_index_scan->CreateReaders("f0", /*row_range_index=*/std::nullopt));
ASSERT_EQ(index_readers.size(), 1u);
auto index_reader = index_readers[0];
{
// VisitEqual: "Alice" -> row 0
ASSERT_OK_AND_ASSIGN(auto result,
index_reader->VisitEqual(Literal(FieldType::STRING, "Alice", 5)));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{0}");
}
{
// VisitEqual: "Bob" -> rows 1,2
ASSERT_OK_AND_ASSIGN(auto result,
index_reader->VisitEqual(Literal(FieldType::STRING, "Bob", 3)));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{1,2}");
}
{
// VisitEqual: non-existent key -> empty
ASSERT_OK_AND_ASSIGN(auto result,
index_reader->VisitEqual(Literal(FieldType::STRING, "Zara", 4)));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{}");
}
{
// VisitNotEqual: "Bob" -> all non-null except Bob rows -> {0,3,4,5,6}
ASSERT_OK_AND_ASSIGN(auto result,
index_reader->VisitNotEqual(Literal(FieldType::STRING, "Bob", 3)));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{0,3,4,5,6}");
}
{
// VisitIsNull -> row 7 (null key)
ASSERT_OK_AND_ASSIGN(auto result, index_reader->VisitIsNull());
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{7}");
}
{
// VisitIsNotNull -> rows 0-6
ASSERT_OK_AND_ASSIGN(auto result, index_reader->VisitIsNotNull());
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{0,1,2,3,4,5,6}");
}
{
// VisitIn: {"Alice", "Lucy"} -> rows {0, 4}
ASSERT_OK_AND_ASSIGN(auto result,
index_reader->VisitIn({Literal(FieldType::STRING, "Alice", 5),
Literal(FieldType::STRING, "Lucy", 4)}));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{0,4}");
}
{
// VisitNotIn: {"Alice", "Lucy"} -> all non-null except {0,4} -> {1,2,3,5,6}
ASSERT_OK_AND_ASSIGN(auto result,
index_reader->VisitNotIn({Literal(FieldType::STRING, "Alice", 5),
Literal(FieldType::STRING, "Lucy", 4)}));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{1,2,3,5,6}");
}
{
// VisitLessThan: "Emily" -> keys < "Emily" -> Alice(0), Bob(1,2) -> {0,1,2}
ASSERT_OK_AND_ASSIGN(auto result,
index_reader->VisitLessThan(Literal(FieldType::STRING, "Emily", 5)));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{0,1,2}");
}
{
// VisitLessOrEqual: "Emily" -> keys <= "Emily" -> Alice(0), Bob(1,2), Emily(3) ->
// {0,1,2,3}
ASSERT_OK_AND_ASSIGN(
auto result, index_reader->VisitLessOrEqual(Literal(FieldType::STRING, "Emily", 5)));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{0,1,2,3}");
}
{
// VisitGreaterThan: "Emily" -> keys > "Emily" -> Lucy(4), Tony(5,6) -> {4,5,6}
ASSERT_OK_AND_ASSIGN(
auto result, index_reader->VisitGreaterThan(Literal(FieldType::STRING, "Emily", 5)));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{4,5,6}");
}
{
// VisitGreaterOrEqual: "Emily" -> keys >= "Emily" -> Emily(3), Lucy(4), Tony(5,6) ->
// {3,4,5,6}
ASSERT_OK_AND_ASSIGN(
auto result, index_reader->VisitGreaterOrEqual(Literal(FieldType::STRING, "Emily", 5)));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{3,4,5,6}");
}
auto scan_impl = std::dynamic_pointer_cast<GlobalIndexScanImpl>(global_index_scan);
ASSERT_TRUE(scan_impl);
{
// Equal predicate via evaluator
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Tony", 4));
ASSERT_OK_AND_ASSIGN(auto result, scan_impl->Scan(predicate));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{5,6}");
}
{
// AND predicate: f0 == "Bob" AND f1 == 20
// f0 == "Bob" -> {1,2}, but f1 index does not exist -> AND yields {1,2}
// (fields without index return nullptr, AND with nullptr keeps the other side)
auto f0_predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Bob", 3));
auto f1_predicate = PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(20));
ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({f0_predicate, f1_predicate}));
ASSERT_OK_AND_ASSIGN(auto result, scan_impl->Scan(predicate));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{1,2}");
}
{
// row_range_index filtering: range [0,2] should only load that range
ASSERT_OK_AND_ASSIGN(RowRangeIndex row_range_index, RowRangeIndex::Create({Range(0, 2)}));
ASSERT_OK_AND_ASSIGN(auto range_readers,
global_index_scan->CreateReaders("f0", row_range_index));
ASSERT_EQ(range_readers.size(), 1u);
ASSERT_OK_AND_ASSIGN(auto result,
range_readers[0]->VisitEqual(Literal(FieldType::STRING, "Alice", 5)));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{0}");
}
{
// Invalid row_range_index: no intersection -> empty readers
ASSERT_OK_AND_ASSIGN(RowRangeIndex row_range_index,
RowRangeIndex::Create({Range(100, 200)}));
ASSERT_OK_AND_ASSIGN(auto range_readers,
global_index_scan->CreateReaders("f0", row_range_index));
ASSERT_EQ(range_readers.size(), 0u);
}
// Test full pipeline: scan with predicate -> read data
{
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Bob", 3));
auto scan_impl = std::dynamic_pointer_cast<GlobalIndexScanImpl>(global_index_scan);
ASSERT_OK_AND_ASSIGN(auto index_result, scan_impl->Scan(predicate));
ASSERT_TRUE(index_result);
ASSERT_EQ(index_result->ToString(), "{1,2}");
auto result_fields = fields_;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Bob", 10, 1, 12.1],
[0, "Bob", 20, 0, 16.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, /*predicate=*/nullptr,
/*options=*/{}, index_result));
ASSERT_OK(ReadData(table_path, write_cols, expected_array, /*predicate=*/nullptr, plan));
}
}
TEST_P(GlobalIndexTest, TestBTreeWriteCommitScanReadIndexWithPartition) {
// BTree index with partitioned table. Each partition's data is sorted by f0 independently.
auto schema = arrow::schema(fields_);
std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"},
{Options::FILE_FORMAT, file_format_},
{Options::FILE_SYSTEM, "local"},
{Options::ROW_TRACKING_ENABLED, "true"},
{Options::DATA_EVOLUTION_ENABLED, "true"}};
CreateTable(/*partition_keys=*/{"f1"}, schema, options);
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
std::vector<std::string> write_cols = schema->field_names();
// Write partition f1=10. Data sorted by f0: Alice < Bob < Bob < Emily < Tony
auto src_array1 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Alice", 10, 1, 11.1],
["Bob", 10, 1, 12.1],
["Bob", 10, 0, 13.1],
["Emily", 10, 0, 14.1],
["Tony", 10, 1, 15.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs1,
WriteArray(table_path, {{"f1", "10"}}, write_cols, src_array1));
ASSERT_OK(Commit(table_path, commit_msgs1));
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{{{"f1", "10"}}}, "f0", "btree",
/*options=*/{}, Range(0, 4)));
// Write partition f1=20. Data sorted by f0: Alice < Lucy < Tony
auto src_array2 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Alice", 20, null, 16.1],
["Lucy", 20, 1, 17.1],
["Tony", 20, 0, 18.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs2,
WriteArray(table_path, {{"f1", "20"}}, write_cols, src_array2));
ASSERT_OK(Commit(table_path, commit_msgs2));
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{{{"f1", "20"}}}, "f0", "btree",
/*options=*/{}, Range(5, 7)));
// Scan all partitions
{
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt,
/*partitions=*/std::nullopt, /*options=*/{},
fs_, /*executor=*/nullptr, pool_));
ASSERT_OK_AND_ASSIGN(auto index_readers,
global_index_scan->CreateReaders("f0", std::nullopt));
// One reader per partition range -> 2 ranges -> UnionGlobalIndexReader wraps them
ASSERT_EQ(index_readers.size(), 1u);
// "Alice" exists in both partitions: local ids {0} in range [0,4] -> global 0,
// and local ids {0} in range [5,7] -> global 5
ASSERT_OK_AND_ASSIGN(auto result,
index_readers[0]->VisitEqual(Literal(FieldType::STRING, "Alice", 5)));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{0,5}");
// "Bob" only in partition f1=10: local ids {1,2} -> global {1,2}
ASSERT_OK_AND_ASSIGN(auto result2,
index_readers[0]->VisitEqual(Literal(FieldType::STRING, "Bob", 3)));
ASSERT_TRUE(result2);
ASSERT_EQ(result2->ToString(), "{1,2}");
// "Lucy" only in partition f1=20: local ids {1} -> global {6}
ASSERT_OK_AND_ASSIGN(auto result3,
index_readers[0]->VisitEqual(Literal(FieldType::STRING, "Lucy", 4)));
ASSERT_TRUE(result3);
ASSERT_EQ(result3->ToString(), "{6}");
}
// Scan with partition filter: only f1=10
{
std::vector<std::map<std::string, std::string>> partitions = {{{"f1", "10"}}};
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt, partitions,
/*options=*/{}, fs_, /*executor=*/nullptr, pool_));
ASSERT_OK_AND_ASSIGN(auto index_readers,
global_index_scan->CreateReaders("f0", std::nullopt));
ASSERT_EQ(index_readers.size(), 1u);
// "Alice" in f1=10 only -> global {0}
ASSERT_OK_AND_ASSIGN(auto result,
index_readers[0]->VisitEqual(Literal(FieldType::STRING, "Alice", 5)));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{0}");
// "Lucy" not in f1=10 -> empty
ASSERT_OK_AND_ASSIGN(auto result2,
index_readers[0]->VisitEqual(Literal(FieldType::STRING, "Lucy", 4)));
ASSERT_TRUE(result2);
ASSERT_EQ(result2->ToString(), "{}");
}
// Scan with row_range_index filtering: only range [5,7] (partition f1=20)
{
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt,
/*partitions=*/std::nullopt, /*options=*/{},
fs_, /*executor=*/nullptr, pool_));
ASSERT_OK_AND_ASSIGN(RowRangeIndex row_range_index, RowRangeIndex::Create({Range(5, 7)}));
ASSERT_OK_AND_ASSIGN(auto index_readers,
global_index_scan->CreateReaders("f0", row_range_index));
ASSERT_EQ(index_readers.size(), 1u);
// "Tony" in range [5,7]: local id {2} in range [5,7] -> global {7}
ASSERT_OK_AND_ASSIGN(auto result,
index_readers[0]->VisitEqual(Literal(FieldType::STRING, "Tony", 4)));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{7}");
}
// Full pipeline with evaluator: Scan(predicate) -> read data
{
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt,
/*partitions=*/std::nullopt, /*options=*/{},
fs_, /*executor=*/nullptr, pool_));
auto scanner_impl = std::dynamic_pointer_cast<GlobalIndexScanImpl>(global_index_scan);
ASSERT_TRUE(scanner_impl);
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Tony", 4));
ASSERT_OK_AND_ASSIGN(auto index_result, scanner_impl->Scan(predicate));
ASSERT_TRUE(index_result);
ASSERT_EQ(index_result->ToString(), "{4,7}");
auto result_fields = fields_;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Tony", 10, 1, 15.1],
[0, "Tony", 20, 0, 18.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, /*predicate=*/nullptr,
/*options=*/{}, index_result));
ASSERT_OK(ReadData(table_path, write_cols, expected_array, /*predicate=*/nullptr, plan));
}
}
TEST_P(GlobalIndexTest, TestBTreeWithPartitionAndCustomExecutor) {
// Test that UnionGlobalIndexReader uses a custom 8-thread executor to read
// btree indexes from two partitions in parallel.
auto schema = arrow::schema(fields_);
std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"},
{Options::FILE_FORMAT, file_format_},
{Options::FILE_SYSTEM, "local"},
{Options::ROW_TRACKING_ENABLED, "true"},
{Options::DATA_EVOLUTION_ENABLED, "true"}};
CreateTable(/*partition_keys=*/{"f1"}, schema, options);
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
std::vector<std::string> write_cols = schema->field_names();
// Write partition f1=10 (5 rows, sorted by f0)
auto src_array1 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Alice", 10, 1, 11.1],
["Bob", 10, 1, 12.1],
["Bob", 10, 0, 13.1],
["Emily", 10, 0, 14.1],
["Tony", 10, 1, 15.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs1,
WriteArray(table_path, {{"f1", "10"}}, write_cols, src_array1));
ASSERT_OK(Commit(table_path, commit_msgs1));
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{{{"f1", "10"}}}, "f0", "btree",
/*options=*/{}, Range(0, 4)));
// Write partition f1=20 (3 rows, sorted by f0)
auto src_array2 = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Alice", 20, null, 16.1],
["Lucy", 20, 1, 17.1],
["Tony", 20, 0, 18.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs2,
WriteArray(table_path, {{"f1", "20"}}, write_cols, src_array2));
ASSERT_OK(Commit(table_path, commit_msgs2));
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{{{"f1", "20"}}}, "f0", "btree",
/*options=*/{}, Range(5, 7)));
// Create a GlobalIndexScan with an explicit 8-thread executor
std::shared_ptr<Executor> executor = CreateDefaultExecutor(/*thread_count=*/8);
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt,
/*partitions=*/std::nullopt, /*options=*/{}, fs_, executor, pool_));
// CreateReaders should return 1 UnionGlobalIndexReader (2 sub-readers for 2 ranges)
ASSERT_OK_AND_ASSIGN(auto index_readers, global_index_scan->CreateReaders("f0", std::nullopt));
ASSERT_EQ(index_readers.size(), 1u);
auto union_reader = std::dynamic_pointer_cast<UnionGlobalIndexReader>(index_readers[0]);
ASSERT_TRUE(union_reader);
ASSERT_EQ(union_reader->executor_, executor);
// "Alice" in both partitions: global ids {0, 5}
ASSERT_OK_AND_ASSIGN(auto result,
index_readers[0]->VisitEqual(Literal(FieldType::STRING, "Alice", 5)));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{0,5}");
// "Bob" only in f1=10: global ids {1, 2}
ASSERT_OK_AND_ASSIGN(auto result2,
index_readers[0]->VisitEqual(Literal(FieldType::STRING, "Bob", 3)));
ASSERT_TRUE(result2);
ASSERT_EQ(result2->ToString(), "{1,2}");
// "Lucy" only in f1=20: global id {6}
ASSERT_OK_AND_ASSIGN(auto result3,
index_readers[0]->VisitEqual(Literal(FieldType::STRING, "Lucy", 4)));
ASSERT_TRUE(result3);
ASSERT_EQ(result3->ToString(), "{6}");
// Full pipeline: evaluator with the 8-thread executor
auto scanner_impl = std::dynamic_pointer_cast<GlobalIndexScanImpl>(global_index_scan);
ASSERT_TRUE(scanner_impl);
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Tony", 4));
ASSERT_OK_AND_ASSIGN(auto index_result, scanner_impl->Scan(predicate));
ASSERT_TRUE(index_result);
ASSERT_EQ(index_result->ToString(), "{4,7}");
auto result_fields = fields_;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Tony", 10, 1, 15.1],
[0, "Tony", 20, 0, 18.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, /*predicate=*/nullptr,
/*options=*/{}, index_result));
ASSERT_OK(ReadData(table_path, write_cols, expected_array, /*predicate=*/nullptr, plan));
}
TEST_P(GlobalIndexTest, TestBTreeAndBitmapCoexist) {
// Test btree-global and bitmap index coexisting on the same field (f0).
// The evaluator should AND their results, producing the intersection.
CreateTable();
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
auto schema = arrow::schema(fields_);
std::vector<std::string> write_cols = schema->field_names();
// Data sorted by f0 for btree: Alice < Bob < Bob < Emily < Lucy < Tony < Tony
auto src_array = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
["Alice", 10, 1, 11.1],
["Bob", 10, 1, 12.1],
["Bob", 25, 1, 16.1],
["Emily", 15, 0, 13.1],
["Lucy", 20, 1, 15.1],
["Tony", 20, 0, 14.1],
["Tony", 30, 0, 17.1],
[null, 30, null, 18.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
// Build both indexes on f0
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f0", "btree",
/*options=*/{}, Range(0, 7)));
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f0", "bitmap",
/*options=*/{}, Range(0, 7)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt,
/*partitions=*/std::nullopt, /*options=*/{}, fs_,
/*executor=*/nullptr, pool_));
// Two index types on f0 -> 2 readers
ASSERT_OK_AND_ASSIGN(auto index_readers, global_index_scan->CreateReaders("f0", std::nullopt));
ASSERT_EQ(index_readers.size(), 2u);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexReader> btree_reader,
global_index_scan->CreateReader("f0", "btree", std::nullopt));
ASSERT_TRUE(btree_reader);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexResult> btree_result,
btree_reader->VisitEqual(Literal(FieldType::STRING, "Bob", 3)));
ASSERT_TRUE(btree_result);
ASSERT_EQ(btree_result->ToString(), "{1,2}");
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexResult> btree_less_than_result,
btree_reader->VisitLessThan(Literal(FieldType::STRING, "Emily", 5)));
ASSERT_TRUE(btree_less_than_result);
ASSERT_EQ(btree_less_than_result->ToString(), "{0,1,2}");
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexReader> bitmap_reader,
global_index_scan->CreateReader("f0", "bitmap", std::nullopt));
ASSERT_TRUE(bitmap_reader);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexResult> bitmap_result,
bitmap_reader->VisitEqual(Literal(FieldType::STRING, "Bob", 3)));
ASSERT_TRUE(bitmap_result);
ASSERT_EQ(bitmap_result->ToString(), "{1,2}");
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexResult> bitmap_less_than_result,
bitmap_reader->VisitLessThan(Literal(FieldType::STRING, "Emily", 5)));
ASSERT_FALSE(bitmap_less_than_result);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexReader> missing_reader,
global_index_scan->CreateReader("f0", "lucene", std::nullopt));
ASSERT_FALSE(missing_reader);
// Each reader individually should return the same result for Equal("Bob")
for (const auto& index_reader : index_readers) {
ASSERT_OK_AND_ASSIGN(auto result,
index_reader->VisitEqual(Literal(FieldType::STRING, "Bob", 3)));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{1,2}");
}
// Via evaluator: the two indexes' results get AND, still {1,2}
auto scanner_impl = std::dynamic_pointer_cast<GlobalIndexScanImpl>(global_index_scan);
ASSERT_TRUE(scanner_impl);
ASSERT_OK_AND_ASSIGN(auto evaluator, scanner_impl->GetOrCreateIndexEvaluator());
{
// Equal predicate
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Bob", 3));
ASSERT_OK_AND_ASSIGN(auto result, evaluator->Evaluate(predicate));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{1,2}");
}
{
// NotEqual predicate: both indexes agree on non-null, non-"Bob" rows -> {0,3,4,5,6}
auto predicate =
PredicateBuilder::NotEqual(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Bob", 3));
ASSERT_OK_AND_ASSIGN(auto result, evaluator->Evaluate(predicate));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{0,3,4,5,6}");
}
{
// IsNull: both agree on row 7
auto predicate =
PredicateBuilder::IsNull(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING);
ASSERT_OK_AND_ASSIGN(auto result, evaluator->Evaluate(predicate));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{7}");
}
// Full pipeline: f0 == "Alice" -> read data
{
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Alice", 5));
ASSERT_OK_AND_ASSIGN(auto index_result, scanner_impl->Scan(predicate));
ASSERT_TRUE(index_result);
ASSERT_EQ(index_result->ToString(), "{0}");
auto result_fields = fields_;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Alice", 10, 1, 11.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, /*predicate=*/nullptr,
/*options=*/{}, index_result));
ASSERT_OK(ReadData(table_path, write_cols, expected_array, /*predicate=*/nullptr, plan));
}
// Full pipeline with AND across btree(f0) and bitmap(f0):
// btree supports LessOrEqual, bitmap returns nullptr for LessOrEqual
// So AND(LessOrEqual, Equal) -> only the field(s) that both can evaluate get AND
{
// f0 == "Bob" AND f1 == 10 (f1 has no index -> nullptr -> keeps btree+bitmap result)
auto f0_pred =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Bob", 3));
auto f1_pred = PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
FieldType::INT, Literal(10));
ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({f0_pred, f1_pred}));
ASSERT_OK_AND_ASSIGN(auto index_result, scanner_impl->Scan(predicate));
ASSERT_TRUE(index_result);
ASSERT_EQ(index_result->ToString(), "{1,2}");
auto result_fields = fields_;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Bob", 10, 1, 12.1],
[0, "Bob", 25, 1, 16.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, /*predicate=*/nullptr,
/*options=*/{}, index_result));
ASSERT_OK(ReadData(table_path, write_cols, expected_array, /*predicate=*/nullptr, plan));
}
}
TEST_P(GlobalIndexTest, TestBTreeScanWithPartitionWithMultiMeta) {
if (file_format_ == "avro") {
return;
}
std::string table_path =
paimon::test::GetDataDir() + "/" + file_format_ +
"/append_with_btree_with_partition.db/append_with_btree_with_partition";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt,
/*partitions=*/std::nullopt, /*options=*/{}, fs_,
/*executor=*/nullptr, pool_));
auto count_rows = [](const std::shared_ptr<GlobalIndexResult>& result) -> int64_t {
EXPECT_TRUE(result);
EXPECT_OK_AND_ASSIGN(std::vector<Range> ranges, result->ToRanges());
int64_t total = 0;
for (const auto& range : ranges) {
total += range.Count();
}
return total;
};
auto get_reader = [&](const std::string& column) -> std::shared_ptr<GlobalIndexReader> {
EXPECT_OK_AND_ASSIGN(auto readers, global_index_scan->CreateReaders(column, std::nullopt));
EXPECT_EQ(readers.size(), 1u);
return readers[0];
};
// ---- col_boolean ----
{
auto reader = get_reader("col_boolean");
ASSERT_TRUE(reader);
ASSERT_OK_AND_ASSIGN(auto eq_true, reader->VisitEqual(Literal(true)));
ASSERT_EQ(count_rows(eq_true), 20);
ASSERT_OK_AND_ASSIGN(auto eq_false, reader->VisitEqual(Literal(false)));
ASSERT_EQ(count_rows(eq_false), 20);
}
// ---- col_int ----
{
auto reader = get_reader("col_int");
ASSERT_TRUE(reader);
ASSERT_OK_AND_ASSIGN(auto eq_15, reader->VisitEqual(Literal(15)));
ASSERT_EQ(count_rows(eq_15), 2);
ASSERT_OK_AND_ASSIGN(auto eq_missing, reader->VisitEqual(Literal(100)));
ASSERT_EQ(count_rows(eq_missing), 0);
// GreaterThan(30): i*3 > 30 -> i in [11, 19], 9 indices per partition -> 18 rows.
ASSERT_OK_AND_ASSIGN(auto gt_30, reader->VisitGreaterThan(Literal(30)));
ASSERT_EQ(count_rows(gt_30), 18);
// GreaterThan(57): nothing greater than the max value.
ASSERT_OK_AND_ASSIGN(auto gt_max, reader->VisitGreaterThan(Literal(57)));
ASSERT_EQ(count_rows(gt_max), 0);
}
// ---- col_date (values are 18000 + i for i in [0,19]) ----
{
auto reader = get_reader("col_date");
ASSERT_TRUE(reader);
// 18005 is present at i=5 in both partitions.
ASSERT_OK_AND_ASSIGN(auto eq_present, reader->VisitEqual(Literal(FieldType::DATE, 18005)));
ASSERT_EQ(count_rows(eq_present), 2);
ASSERT_OK_AND_ASSIGN(auto eq_missing, reader->VisitEqual(Literal(FieldType::DATE, 17999)));
ASSERT_EQ(count_rows(eq_missing), 0);
// GreaterThan(18010): i in [11, 19] -> 9 per partition -> 18 rows.
ASSERT_OK_AND_ASSIGN(auto gt_mid,
reader->VisitGreaterThan(Literal(FieldType::DATE, 18010)));
ASSERT_EQ(count_rows(gt_mid), 18);
}
// ---- col_double (values are i * 2.2 for i in [0,19]) ----
{
auto reader = get_reader("col_double");
ASSERT_TRUE(reader);
// i=5 -> 11.0
ASSERT_OK_AND_ASSIGN(auto eq_present, reader->VisitEqual(Literal(11.0)));
ASSERT_EQ(count_rows(eq_present), 2);
ASSERT_OK_AND_ASSIGN(auto eq_missing, reader->VisitEqual(Literal(123.456)));
ASSERT_EQ(count_rows(eq_missing), 0);
// GreaterThan(10 * 2.2 = 22.0): i in [11, 19] -> 9 per partition -> 18 rows.
ASSERT_OK_AND_ASSIGN(auto gt_mid, reader->VisitGreaterThan(Literal(10 * 2.2)));
ASSERT_EQ(count_rows(gt_mid), 18);
}
// ---- col_timestamp (Timestamp from epoch millis = 1700000000000 + i*1000) ----
{
auto reader = get_reader("col_timestamp");
ASSERT_TRUE(reader);
// i=5 -> 1700000005000 ms.
ASSERT_OK_AND_ASSIGN(
auto eq_present,
reader->VisitEqual(Literal(Timestamp::FromEpochMillis(1700000000000L + 5 * 1000L))));
ASSERT_EQ(count_rows(eq_present), 2);
ASSERT_OK_AND_ASSIGN(auto eq_missing,
reader->VisitEqual(Literal(Timestamp::FromEpochMillis(1L))));
ASSERT_EQ(count_rows(eq_missing), 0);
// GreaterThan(i=10 boundary): i in [11, 19] -> 18 rows globally.
ASSERT_OK_AND_ASSIGN(auto gt_mid,
reader->VisitGreaterThan(
Literal(Timestamp::FromEpochMillis(1700000000000L + 10 * 1000L))));
ASSERT_EQ(count_rows(gt_mid), 18);
}
// ---- col_timestamp_ltz (same physical values as col_timestamp) ----
{
auto reader = get_reader("col_timestamp_ltz");
ASSERT_TRUE(reader);
ASSERT_OK_AND_ASSIGN(
auto eq_present,
reader->VisitEqual(Literal(Timestamp::FromEpochMillis(1700000000000L + 7 * 1000L))));
ASSERT_EQ(count_rows(eq_present), 2);
ASSERT_OK_AND_ASSIGN(auto gt_mid,
reader->VisitGreaterThan(
Literal(Timestamp::FromEpochMillis(1700000000000L + 10 * 1000L))));
ASSERT_EQ(count_rows(gt_mid), 18);
}
// ---- col_decimal (unscaled = i * 123456, precision=18, scale=6) ----
{
auto reader = get_reader("col_decimal");
ASSERT_TRUE(reader);
// i=5 -> unscaled 617280
ASSERT_OK_AND_ASSIGN(
auto eq_present,
reader->VisitEqual(Literal(Decimal::FromUnscaledLong(5 * 123456L, 18, 6))));
ASSERT_EQ(count_rows(eq_present), 2);
ASSERT_OK_AND_ASSIGN(auto eq_missing,
reader->VisitEqual(Literal(Decimal::FromUnscaledLong(1L, 18, 6))));
ASSERT_EQ(count_rows(eq_missing), 0);
// GreaterThan(i=10): i in [11, 19] -> 18 rows globally.
ASSERT_OK_AND_ASSIGN(
auto gt_mid,
reader->VisitGreaterThan(Literal(Decimal::FromUnscaledLong(10 * 123456L, 18, 6))));
ASSERT_EQ(count_rows(gt_mid), 18);
}
// ---- col_string (values are "str_00000" .. "str_00019") ----
{
auto reader = get_reader("col_string");
ASSERT_TRUE(reader);
std::string present_value = "str_00005";
ASSERT_OK_AND_ASSIGN(auto eq_present,
reader->VisitEqual(Literal(FieldType::STRING, present_value.data(),
present_value.size())));
ASSERT_EQ(count_rows(eq_present), 2);
std::string missing_value = "str_99999";
ASSERT_OK_AND_ASSIGN(auto eq_missing,
reader->VisitEqual(Literal(FieldType::STRING, missing_value.data(),
missing_value.size())));
ASSERT_EQ(count_rows(eq_missing), 0);
// GreaterThan("str_00010"): lexicographically greater values are i in [11, 19].
std::string mid_value = "str_00010";
ASSERT_OK_AND_ASSIGN(auto gt_mid,
reader->VisitGreaterThan(
Literal(FieldType::STRING, mid_value.data(), mid_value.size())));
ASSERT_EQ(count_rows(gt_mid), 18);
}
}
#ifdef PAIMON_ENABLE_LUMINA
TEST_P(GlobalIndexTest, TestBTreeWithLumina) {
// Test btree on f0 (string) and lumina on f1 (vector) coexisting on different fields.
arrow::FieldVector fields = {
arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::list(arrow::float32())),
arrow::field("f2", arrow::int32()), arrow::field("f3", arrow::float64())};
std::map<std::string, std::string> lumina_options = {{"lumina.index.dimension", "4"},
{"lumina.index.type", "bruteforce"},
{"lumina.distance.metric", "l2"},
{"lumina.encoding.type", "rawf32"},
{"lumina.search.parallel_number", "10"}};
auto schema = arrow::schema(fields);
std::map<std::string, std::string> options = {{Options::MANIFEST_FORMAT, "orc"},
{Options::FILE_FORMAT, file_format_},
{Options::FILE_SYSTEM, "local"},
{Options::ROW_TRACKING_ENABLED, "true"},
{Options::DATA_EVOLUTION_ENABLED, "true"}};
CreateTable(/*partition_keys=*/{}, schema, options);
std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
std::vector<std::string> write_cols = schema->field_names();
// Data sorted by f0 for btree: Alice < Alice < Bob < Bob < Emily < Lucy < Paul < Tony
auto src_array = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
["Alice", [0.0, 0.0, 0.0, 0.0], 10, 11.1],
["Alice", [11.0, 11.0, 11.0, 11.0], 20, 18.1],
["Bob", [0.0, 1.0, 0.0, 1.0], 10, 12.1],
["Bob", [10.0, 11.0, 10.0, 11.0], 20, 16.1],
["Emily", [1.0, 0.0, 1.0, 0.0], 10, 13.1],
["Lucy", [10.0, 10.0, 10.0, 10.0], 20, 15.1],
["Paul", [10.0, 10.0, 10.0, 10.0], 20, 19.1],
["Tony", [1.0, 1.0, 1.0, 1.0], 10, 14.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto commit_msgs, WriteArray(table_path, write_cols, src_array));
ASSERT_OK(Commit(table_path, commit_msgs));
// Build btree index on f0
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f0", "btree",
/*options=*/{}, Range(0, 7)));
// Build lumina index on f1
ASSERT_OK(WriteIndex(table_path, /*partition_filters=*/{}, "f1", "lumina",
/*options=*/lumina_options, Range(0, 7)));
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<GlobalIndexScan> global_index_scan,
GlobalIndexScan::Create(table_path, /*snapshot_id=*/std::nullopt,
/*partitions=*/std::nullopt,
/*options=*/lumina_options, fs_, /*executor=*/nullptr, pool_));
// Query f0 via btree
{
ASSERT_OK_AND_ASSIGN(auto index_readers,
global_index_scan->CreateReaders("f0", std::nullopt));
ASSERT_EQ(index_readers.size(), 1u);
ASSERT_OK_AND_ASSIGN(auto result,
index_readers[0]->VisitEqual(Literal(FieldType::STRING, "Alice", 5)));
ASSERT_TRUE(result);
ASSERT_EQ(result->ToString(), "{0,1}");
ASSERT_OK_AND_ASSIGN(
auto result2, index_readers[0]->VisitLessThan(Literal(FieldType::STRING, "Emily", 5)));
ASSERT_TRUE(result2);
ASSERT_EQ(result2->ToString(), "{0,1,2,3}");
}
// Query f1 via lumina (vector search)
{
ASSERT_OK_AND_ASSIGN(auto index_readers,
global_index_scan->CreateReaders("f1", std::nullopt));
ASSERT_EQ(index_readers.size(), 1u);
std::vector<float> query = {11.0f, 11.0f, 11.0f, 11.0f};
auto vector_search = std::make_shared<VectorSearch>(
"f1", /*limit=*/1, query, /*filter=*/nullptr,
/*predicate=*/nullptr, /*distance_type=*/std::nullopt, /*options=*/lumina_options);
ASSERT_OK_AND_ASSIGN(auto scored_result,
index_readers[0]->VisitVectorSearch(vector_search));
ASSERT_TRUE(scored_result);
ASSERT_EQ(scored_result->ToString(), "row ids: {1}, scores: {0.00}");
}
// Evaluator: btree on f0 = "Bob"
{
auto scanner_impl = std::dynamic_pointer_cast<GlobalIndexScanImpl>(global_index_scan);
ASSERT_TRUE(scanner_impl);
auto predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
Literal(FieldType::STRING, "Bob", 3));
ASSERT_OK_AND_ASSIGN(auto index_result, scanner_impl->Scan(predicate));
ASSERT_TRUE(index_result);
ASSERT_EQ(index_result->ToString(), "{2,3}");
// Read data for Bob
auto result_fields = fields;
result_fields.insert(result_fields.begin(), SpecialFields::ValueKind().ArrowField());
auto expected_array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), R"([
[0, "Bob", [0.0, 1.0, 0.0, 1.0], 10, 12.1],
[0, "Bob", [10.0, 11.0, 10.0, 11.0], 20, 16.1]
])")
.ValueOrDie();
ASSERT_OK_AND_ASSIGN(auto plan, ScanGlobalIndexAndData(table_path, /*predicate=*/nullptr,
/*options=*/{}, index_result));
ASSERT_OK(ReadData(table_path, write_cols, expected_array, /*predicate=*/nullptr, plan));
}
// Combined: btree f0 filter + lumina vector search with pre-filter
// Use btree result as pre_filter for lumina search
{
ASSERT_OK_AND_ASSIGN(auto btree_readers,
global_index_scan->CreateReaders("f0", std::nullopt));
ASSERT_EQ(btree_readers.size(), 1u);
// Get rows where f0 == "Alice" -> {0, 1}
ASSERT_OK_AND_ASSIGN(auto btree_result,
btree_readers[0]->VisitEqual(Literal(FieldType::STRING, "Alice", 5)));
ASSERT_TRUE(btree_result);
ASSERT_EQ(btree_result->ToString(), "{0,1}");
// Now vector search on f1 with pre_filter limiting to Alice's rows {0, 1}
ASSERT_OK_AND_ASSIGN(auto lumina_readers,
global_index_scan->CreateReaders("f1", std::nullopt));
ASSERT_EQ(lumina_readers.size(), 1u);
std::vector<float> query = {11.0f, 11.0f, 11.0f, 11.0f};
auto filter = [](int64_t id) -> bool { return id == 0 || id == 1; };
auto vector_search = std::make_shared<VectorSearch>(
"f1", /*limit=*/1, query, filter,
/*predicate=*/nullptr, /*distance_type=*/std::nullopt, /*options=*/lumina_options);
ASSERT_OK_AND_ASSIGN(auto scored_result,
lumina_readers[0]->VisitVectorSearch(vector_search));
ASSERT_EQ(scored_result->ToString(), "row ids: {1}, scores: {0.00}");
}
}
#endif
std::vector<ParamType> GetTestValuesForGlobalIndexTest() {
std::vector<ParamType> values;
values.emplace_back("parquet", false);
values.emplace_back("parquet", true);
#ifdef PAIMON_ENABLE_ORC
values.emplace_back("orc", false);
values.emplace_back("orc", true);
#endif
#ifdef PAIMON_ENABLE_AVRO
values.emplace_back("avro", false);
values.emplace_back("avro", true);
#endif
return values;
}
INSTANTIATE_TEST_SUITE_P(FileFormat, GlobalIndexTest,
::testing::ValuesIn(GetTestValuesForGlobalIndexTest()));
} // namespace paimon::test