This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new dca7183 feat(btree): add btree global index integration tests (#90)
dca7183 is described below
commit dca7183723657e66c6ca989ad200dbb927037f09
Author: lszskye <[email protected]>
AuthorDate: Mon Jun 22 02:32:32 2026 -0700
feat(btree): add btree global index integration tests (#90)
---
.../btree/btree_global_index_integration_test.cpp | 2133 ++++++++++++++++++++
1 file changed, 2133 insertions(+)
diff --git
a/src/paimon/common/global_index/btree/btree_global_index_integration_test.cpp
b/src/paimon/common/global_index/btree/btree_global_index_integration_test.cpp
new file mode 100644
index 0000000..b05a0a8
--- /dev/null
+++
b/src/paimon/common/global_index/btree/btree_global_index_integration_test.cpp
@@ -0,0 +1,2133 @@
+/*
+ * 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/c/bridge.h"
+#include "arrow/ipc/json_simple.h"
+#include "gtest/gtest.h"
+#include "paimon/common/compression/block_compression_factory.h"
+#include "paimon/common/factories/io_hook.h"
+#include "paimon/common/global_index/btree/btree_global_index_writer.h"
+#include "paimon/common/global_index/btree/btree_global_indexer.h"
+#include "paimon/common/global_index/btree/lazy_filtered_btree_reader.h"
+#include "paimon/common/options/memory_size.h"
+#include "paimon/common/utils/scope_guard.h"
+#include "paimon/data/decimal.h"
+#include "paimon/data/timestamp.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/global_index/bitmap_global_index_result.h"
+#include "paimon/global_index/io/global_index_file_reader.h"
+#include "paimon/global_index/io/global_index_file_writer.h"
+#include "paimon/io/buffered_input_stream.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/predicate/literal.h"
+#include "paimon/testing/utils/io_exception_helper.h"
+#include "paimon/testing/utils/testharness.h"
+namespace paimon::test {
+
+class FakeGlobalIndexFileWriter : public GlobalIndexFileWriter {
+ public:
+ FakeGlobalIndexFileWriter(const std::shared_ptr<FileSystem>& fs, const
std::string& base_path)
+ : fs_(fs), base_path_(base_path) {}
+
+ Result<std::string> NewFileName(const std::string& prefix) const override {
+ return prefix + "_" + std::to_string(file_counter_++);
+ }
+
+ Result<std::unique_ptr<OutputStream>> NewOutputStream(
+ const std::string& file_name) const override {
+ return fs_->Create(base_path_ + "/" + file_name, true);
+ }
+
+ Result<int64_t> GetFileSize(const std::string& file_name) const override {
+ PAIMON_ASSIGN_OR_RAISE(auto file_status, fs_->GetFileStatus(base_path_
+ "/" + file_name));
+ return static_cast<int64_t>(file_status->GetLen());
+ }
+
+ std::string ToPath(const std::string& file_name) const override {
+ return base_path_ + "/" + file_name;
+ }
+
+ private:
+ std::shared_ptr<FileSystem> fs_;
+ std::string base_path_;
+ mutable int64_t file_counter_ = 0;
+};
+
+class FakeGlobalIndexFileReader : public GlobalIndexFileReader {
+ public:
+ FakeGlobalIndexFileReader(const std::shared_ptr<FileSystem>& fs, const
std::string& base_path)
+ : fs_(fs), base_path_(base_path) {}
+
+ Result<std::unique_ptr<InputStream>> GetInputStream(
+ const std::string& file_path) const override {
+ return fs_->Open(file_path);
+ }
+
+ private:
+ std::shared_ptr<FileSystem> fs_;
+ std::string base_path_;
+};
+
+class BTreeGlobalIndexIntegrationTest : public ::testing::Test,
+ public
::testing::WithParamInterface<std::string> {
+ protected:
+ void SetUp() override {
+ pool_ = GetDefaultPool();
+ test_dir_ = UniqueTestDirectory::Create("local");
+ fs_ = test_dir_->GetFileSystem();
+ base_path_ = test_dir_->Str();
+ compression_factory_ =
BlockCompressionFactory::Create(BlockCompressionType::NONE).value();
+ }
+
+ void TearDown() override {}
+
+ // Helper to create ArrowSchema from arrow type
+ std::unique_ptr<ArrowSchema> CreateArrowSchema(
+ const std::shared_ptr<arrow::Field>& field) const {
+ auto schema = arrow::schema({field});
+ auto c_schema = std::make_unique<ArrowSchema>();
+ EXPECT_TRUE(arrow::ExportSchema(*schema, c_schema.get()).ok());
+ return c_schema;
+ }
+
+ void CheckResult(const std::shared_ptr<GlobalIndexResult>& result,
+ const std::vector<int64_t>& expected) const {
+ auto typed_result =
std::dynamic_pointer_cast<BitmapGlobalIndexResult>(result);
+ ASSERT_TRUE(typed_result);
+ ASSERT_OK_AND_ASSIGN(const RoaringBitmap64* bitmap,
typed_result->GetBitmap());
+ ASSERT_TRUE(bitmap);
+ ASSERT_EQ(*bitmap, RoaringBitmap64::From(expected))
+ << "result=" << bitmap->ToString()
+ << ", expected=" << RoaringBitmap64::From(expected).ToString();
+ }
+
+ std::shared_ptr<MemoryPool> pool_;
+ std::shared_ptr<BlockCompressionFactory> compression_factory_;
+ std::unique_ptr<UniqueTestDirectory> test_dir_;
+ std::shared_ptr<FileSystem> fs_;
+ std::string base_path_;
+};
+
+TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadIntData) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("int_field", arrow::int32());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "4096"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ indexer->CreateWriter("int_field", c_schema.get(),
file_writer, pool_));
+ // Data layout (row_id -> value):
+ // 0->1, 1->1, 2->null, 3->2, 4->2, 5->null, 6->3, 7->4, 8->5, 9->5,
10->5, 11->null
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ [1],
+ [1],
+ [null],
+ [2],
+ [2],
+ [null],
+ [3],
+ [4],
+ [5],
+ [5],
+ [5],
+ [null]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids(array->length());
+ std::iota(row_ids.begin(), row_ids.end(), 0);
+ ASSERT_OK(writer->AddBatch(&c_array, std::move(row_ids)));
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+
+ // Now read back
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ indexer->CreateReader(c_schema.get(), file_reader,
metas, pool_));
+
+ // All non-null row ids: {0,1,3,4,6,7,8,9,10}
+ // Null row ids: {2,5,11}
+
+ // --- VisitIsNull ---
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
+ CheckResult(result, {2, 5, 11});
+ }
+
+ // --- VisitIsNotNull ---
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
+ CheckResult(result, {0, 1, 3, 4, 6, 7, 8, 9, 10});
+ }
+
+ // --- VisitEqual ---
+ {
+ // Equal to 1 -> rows 0,1
+ Literal literal_1(1);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(literal_1));
+ CheckResult(result, {0, 1});
+
+ // Equal to 3 -> row 6
+ Literal literal_3(3);
+ ASSERT_OK_AND_ASSIGN(result, reader->VisitEqual(literal_3));
+ CheckResult(result, {6});
+
+ // Equal to 5 -> rows 8,9,10
+ Literal literal_5(5);
+ ASSERT_OK_AND_ASSIGN(result, reader->VisitEqual(literal_5));
+ CheckResult(result, {8, 9, 10});
+
+ // Equal to 99 (not present) -> empty
+ Literal literal_99(99);
+ ASSERT_OK_AND_ASSIGN(result, reader->VisitEqual(literal_99));
+ CheckResult(result, {});
+ }
+
+ // --- VisitNotEqual ---
+ {
+ // NotEqual to 3 -> all non-null except row 6
+ Literal literal_3(3);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(literal_3));
+ CheckResult(result, {0, 1, 3, 4, 7, 8, 9, 10});
+ }
+
+ // --- VisitLessThan ---
+ {
+ // LessThan 3 -> values 1,2 -> rows 0,1,3,4
+ Literal literal_3(3);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(literal_3));
+ CheckResult(result, {0, 1, 3, 4});
+
+ // LessThan 1 -> empty (no value < 1)
+ Literal literal_1(1);
+ ASSERT_OK_AND_ASSIGN(result, reader->VisitLessThan(literal_1));
+ CheckResult(result, {});
+ }
+
+ // --- VisitLessOrEqual ---
+ {
+ // LessOrEqual 3 -> values 1,2,3 -> rows 0,1,3,4,6
+ Literal literal_3(3);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessOrEqual(literal_3));
+ CheckResult(result, {0, 1, 3, 4, 6});
+ }
+
+ // --- VisitGreaterThan ---
+ {
+ // GreaterThan 3 -> values 4,5 -> rows 7,8,9,10
+ Literal literal_3(3);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitGreaterThan(literal_3));
+ CheckResult(result, {7, 8, 9, 10});
+
+ // GreaterThan 5 -> empty (no value > 5)
+ Literal literal_5(5);
+ ASSERT_OK_AND_ASSIGN(result, reader->VisitGreaterThan(literal_5));
+ CheckResult(result, {});
+
+ // GreaterThan 1 (min_key) -> skip entries with value==1 via
from_inclusive=false
+ Literal literal_1(1);
+ ASSERT_OK_AND_ASSIGN(result, reader->VisitGreaterThan(literal_1));
+ CheckResult(result, {3, 4, 6, 7, 8, 9, 10});
+ }
+
+ // --- VisitGreaterOrEqual ---
+ {
+ // GreaterOrEqual 3 -> values 3,4,5 -> rows 6,7,8,9,10
+ Literal literal_3(3);
+ ASSERT_OK_AND_ASSIGN(auto result,
reader->VisitGreaterOrEqual(literal_3));
+ CheckResult(result, {6, 7, 8, 9, 10});
+ }
+
+ // --- VisitIn ---
+ {
+ // In {1, 4} -> rows 0,1,7
+ std::vector<Literal> in_literals = {Literal(1), Literal(4)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
+ CheckResult(result, {0, 1, 7});
+
+ // In {99} (not present) -> empty
+ std::vector<Literal> in_missing = {Literal(99)};
+ ASSERT_OK_AND_ASSIGN(result, reader->VisitIn(in_missing));
+ CheckResult(result, {});
+ }
+
+ // --- VisitNotIn ---
+ {
+ // NotIn {1, 5} -> all non-null except rows 0,1,8,9,10 -> rows 3,4,6,7
+ std::vector<Literal> not_in_literals = {Literal(1), Literal(5)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotIn(not_in_literals));
+ CheckResult(result, {3, 4, 6, 7});
+ }
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadStringData) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("str_field", arrow::utf8());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ indexer->CreateWriter("str_field", c_schema.get(),
file_writer, pool_));
+ // Data layout (row_id -> value):
+ // 0->"apple", 1->"apricot", 2->null, 3->"banana", 4->"blueberry",
+ // 5->null, 6->"cherry", 7->"cherry", 8->"date"
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ ["apple"],
+ ["apricot"],
+ [null],
+ ["banana"],
+ ["blueberry"],
+ [null],
+ ["cherry"],
+ ["cherry"],
+ ["date"]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids(array->length());
+ std::iota(row_ids.begin(), row_ids.end(), 0);
+ ASSERT_OK(writer->AddBatch(&c_array, std::move(row_ids)));
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ indexer->CreateReader(c_schema.get(), file_reader,
metas, pool_));
+
+ // Non-null rows: {0,1,3,4,6,7,8}, Null rows: {2,5}
+
+ // --- VisitIsNull / VisitIsNotNull ---
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
+ CheckResult(result, {2, 5});
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
+ CheckResult(result, {0, 1, 3, 4, 6, 7, 8});
+ }
+
+ // --- VisitEqual ---
+ {
+ Literal lit_cherry(FieldType::STRING, "cherry", 6);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_cherry));
+ CheckResult(result, {6, 7});
+
+ Literal lit_missing(FieldType::STRING, "fig", 3);
+ ASSERT_OK_AND_ASSIGN(result, reader->VisitEqual(lit_missing));
+ CheckResult(result, {});
+ }
+
+ // --- VisitNotEqual ---
+ {
+ Literal lit_banana(FieldType::STRING, "banana", 6);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(lit_banana));
+ CheckResult(result, {0, 1, 4, 6, 7, 8});
+ }
+
+ // --- VisitLessThan ---
+ {
+ // LessThan "cherry" -> "apple","apricot","banana","blueberry" -> rows
0,1,3,4
+ Literal lit_cherry(FieldType::STRING, "cherry", 6);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(lit_cherry));
+ CheckResult(result, {0, 1, 3, 4});
+ }
+
+ // --- VisitLessOrEqual ---
+ {
+ Literal lit_banana(FieldType::STRING, "banana", 6);
+ ASSERT_OK_AND_ASSIGN(auto result,
reader->VisitLessOrEqual(lit_banana));
+ CheckResult(result, {0, 1, 3});
+ }
+
+ // --- VisitGreaterThan ---
+ {
+ Literal lit_cherry(FieldType::STRING, "cherry", 6);
+ ASSERT_OK_AND_ASSIGN(auto result,
reader->VisitGreaterThan(lit_cherry));
+ CheckResult(result, {8});
+
+ Literal lit_apple(FieldType::STRING, "apple", 5);
+ ASSERT_OK_AND_ASSIGN(result, reader->VisitGreaterThan(lit_apple));
+ CheckResult(result, {1, 3, 4, 6, 7, 8});
+ }
+
+ // --- VisitGreaterOrEqual ---
+ {
+ Literal lit_cherry(FieldType::STRING, "cherry", 6);
+ ASSERT_OK_AND_ASSIGN(auto result,
reader->VisitGreaterOrEqual(lit_cherry));
+ CheckResult(result, {6, 7, 8});
+ }
+
+ // --- VisitIn ---
+ {
+ std::vector<Literal> in_literals = {Literal(FieldType::STRING,
"apple", 5),
+ Literal(FieldType::STRING, "date",
4)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
+ CheckResult(result, {0, 8});
+ }
+
+ // --- VisitNotIn ---
+ {
+ std::vector<Literal> not_in_literals = {Literal(FieldType::STRING,
"apple", 5),
+ Literal(FieldType::STRING,
"cherry", 6)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotIn(not_in_literals));
+ CheckResult(result, {1, 3, 4, 8});
+ }
+
+ // --- VisitStartsWith ---
+ {
+ // StartsWith "ap" -> "apple","apricot" -> rows 0,1
+ Literal lit_ap(FieldType::STRING, "ap", 2);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitStartsWith(lit_ap));
+ CheckResult(result, {0, 1});
+
+ // StartsWith "bl" -> "blueberry" -> row 4
+ Literal lit_bl(FieldType::STRING, "bl", 2);
+ ASSERT_OK_AND_ASSIGN(result, reader->VisitStartsWith(lit_bl));
+ CheckResult(result, {4});
+
+ // StartsWith "z" -> no match
+ Literal lit_z(FieldType::STRING, "z", 1);
+ ASSERT_OK_AND_ASSIGN(result, reader->VisitStartsWith(lit_z));
+ CheckResult(result, {});
+
+ // StartsWith "" (empty prefix) -> all non-null rows
+ Literal lit_empty(FieldType::STRING, "", 0);
+ ASSERT_OK_AND_ASSIGN(result, reader->VisitStartsWith(lit_empty));
+ CheckResult(result, {0, 1, 3, 4, 6, 7, 8});
+
+ // StartsWith "\xFF\xFF" (all 0xFF bytes, overflow branch) -> no match
+ std::string xff_prefix = "\xFF\xFF";
+ Literal lit_xff(FieldType::STRING, xff_prefix.data(),
xff_prefix.size());
+ ASSERT_OK_AND_ASSIGN(result, reader->VisitStartsWith(lit_xff));
+ CheckResult(result, {});
+ }
+
+ // --- VisitEndsWith (falls back to AllNonNullRows) ---
+ {
+ Literal lit_ry(FieldType::STRING, "ry", 2);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEndsWith(lit_ry));
+ CheckResult(result, {0, 1, 3, 4, 6, 7, 8});
+ }
+
+ // --- VisitContains (falls back to AllNonNullRows) ---
+ {
+ Literal lit_an(FieldType::STRING, "an", 2);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitContains(lit_an));
+ CheckResult(result, {0, 1, 3, 4, 6, 7, 8});
+ }
+
+ // --- VisitLike ---
+ {
+ // "ap%" is a prefix pattern -> delegates to VisitStartsWith("ap") ->
rows 0,1
+ Literal lit_ap_pct(FieldType::STRING, "ap%", 3);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLike(lit_ap_pct));
+ CheckResult(result, {0, 1});
+
+ // "%erry" is not a prefix pattern -> falls back to AllNonNullRows
+ Literal lit_suffix(FieldType::STRING, "%erry", 5);
+ ASSERT_OK_AND_ASSIGN(result, reader->VisitLike(lit_suffix));
+ CheckResult(result, {0, 1, 3, 4, 6, 7, 8});
+
+ // "b_nana" contains '_' before '%' -> falls back to AllNonNullRows
+ Literal lit_underscore(FieldType::STRING, "b_nana", 6);
+ ASSERT_OK_AND_ASSIGN(result, reader->VisitLike(lit_underscore));
+ CheckResult(result, {0, 1, 3, 4, 6, 7, 8});
+ }
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadBigIntData) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("bigint_field", arrow::int64());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ indexer->CreateWriter("bigint_field", c_schema.get(),
file_writer, pool_));
+ // Data layout (row_id -> value):
+ // 0->100, 1->null, 2->200, 3->200, 4->300, 5->null, 6->400
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ [100],
+ [null],
+ [200],
+ [200],
+ [300],
+ [null],
+ [400]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids(array->length());
+ std::iota(row_ids.begin(), row_ids.end(), 0);
+ ASSERT_OK(writer->AddBatch(&c_array, std::move(row_ids)));
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ indexer->CreateReader(c_schema.get(), file_reader,
metas, pool_));
+
+ // Non-null rows: {0,2,3,4,6}, Null rows: {1,5}
+
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
+ CheckResult(result, {1, 5});
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
+ CheckResult(result, {0, 2, 3, 4, 6});
+ }
+ {
+ Literal lit_200(static_cast<int64_t>(200));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_200));
+ CheckResult(result, {2, 3});
+ }
+ {
+ Literal lit_200(static_cast<int64_t>(200));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(lit_200));
+ CheckResult(result, {0, 4, 6});
+ }
+ {
+ Literal lit_300(static_cast<int64_t>(300));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(lit_300));
+ CheckResult(result, {0, 2, 3});
+ }
+ {
+ Literal lit_300(static_cast<int64_t>(300));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessOrEqual(lit_300));
+ CheckResult(result, {0, 2, 3, 4});
+ }
+ {
+ Literal lit_200(static_cast<int64_t>(200));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitGreaterThan(lit_200));
+ CheckResult(result, {4, 6});
+ }
+ {
+ Literal lit_200(static_cast<int64_t>(200));
+ ASSERT_OK_AND_ASSIGN(auto result,
reader->VisitGreaterOrEqual(lit_200));
+ CheckResult(result, {2, 3, 4, 6});
+ }
+ {
+ std::vector<Literal> in_literals = {Literal(static_cast<int64_t>(100)),
+
Literal(static_cast<int64_t>(400))};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
+ CheckResult(result, {0, 6});
+ }
+ {
+ std::vector<Literal> not_in_literals =
{Literal(static_cast<int64_t>(200))};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotIn(not_in_literals));
+ CheckResult(result, {0, 4, 6});
+ }
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadFloatData) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("float_field", arrow::float32());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ indexer->CreateWriter("float_field", c_schema.get(),
file_writer, pool_));
+ // Data layout (row_id -> value):
+ // 0->1.0, 1->null, 2->2.5, 3->2.5, 4->3.0, 5->null, 6->4.5
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ [1.0],
+ [null],
+ [2.5],
+ [2.5],
+ [3.0],
+ [null],
+ [4.5]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids(array->length());
+ std::iota(row_ids.begin(), row_ids.end(), 0);
+ ASSERT_OK(writer->AddBatch(&c_array, std::move(row_ids)));
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ indexer->CreateReader(c_schema.get(), file_reader,
metas, pool_));
+
+ // Non-null rows: {0,2,3,4,6}, Null rows: {1,5}
+
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
+ CheckResult(result, {1, 5});
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
+ CheckResult(result, {0, 2, 3, 4, 6});
+ }
+ {
+ Literal lit_2_5(2.5f);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_2_5));
+ CheckResult(result, {2, 3});
+ }
+ {
+ Literal lit_2_5(2.5f);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(lit_2_5));
+ CheckResult(result, {0, 4, 6});
+ }
+ {
+ Literal lit_3_0(3.0f);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(lit_3_0));
+ CheckResult(result, {0, 2, 3});
+ }
+ {
+ Literal lit_3_0(3.0f);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessOrEqual(lit_3_0));
+ CheckResult(result, {0, 2, 3, 4});
+ }
+ {
+ Literal lit_2_5(2.5f);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitGreaterThan(lit_2_5));
+ CheckResult(result, {4, 6});
+ }
+ {
+ Literal lit_2_5(2.5f);
+ ASSERT_OK_AND_ASSIGN(auto result,
reader->VisitGreaterOrEqual(lit_2_5));
+ CheckResult(result, {2, 3, 4, 6});
+ }
+ {
+ std::vector<Literal> in_literals = {Literal(1.0f), Literal(4.5f)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
+ CheckResult(result, {0, 6});
+ }
+ {
+ std::vector<Literal> not_in_literals = {Literal(2.5f)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotIn(not_in_literals));
+ CheckResult(result, {0, 4, 6});
+ }
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadDoubleData) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("double_field", arrow::float64());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ indexer->CreateWriter("double_field", c_schema.get(),
file_writer, pool_));
+ // Data layout (row_id -> value):
+ // 0->1.1, 1->null, 2->2.2, 3->2.2, 4->3.3, 5->null, 6->4.4
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ [1.1],
+ [null],
+ [2.2],
+ [2.2],
+ [3.3],
+ [null],
+ [4.4]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids(array->length());
+ std::iota(row_ids.begin(), row_ids.end(), 0);
+ ASSERT_OK(writer->AddBatch(&c_array, std::move(row_ids)));
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ indexer->CreateReader(c_schema.get(), file_reader,
metas, pool_));
+
+ // Non-null rows: {0,2,3,4,6}, Null rows: {1,5}
+
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
+ CheckResult(result, {1, 5});
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
+ CheckResult(result, {0, 2, 3, 4, 6});
+ }
+ {
+ Literal lit_2_2(2.2);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_2_2));
+ CheckResult(result, {2, 3});
+ }
+ {
+ Literal lit_2_2(2.2);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(lit_2_2));
+ CheckResult(result, {0, 4, 6});
+ }
+ {
+ Literal lit_3_3(3.3);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(lit_3_3));
+ CheckResult(result, {0, 2, 3});
+ }
+ {
+ Literal lit_3_3(3.3);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessOrEqual(lit_3_3));
+ CheckResult(result, {0, 2, 3, 4});
+ }
+ {
+ Literal lit_2_2(2.2);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitGreaterThan(lit_2_2));
+ CheckResult(result, {4, 6});
+ }
+ {
+ Literal lit_2_2(2.2);
+ ASSERT_OK_AND_ASSIGN(auto result,
reader->VisitGreaterOrEqual(lit_2_2));
+ CheckResult(result, {2, 3, 4, 6});
+ }
+ {
+ std::vector<Literal> in_literals = {Literal(1.1), Literal(4.4)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
+ CheckResult(result, {0, 6});
+ }
+ {
+ std::vector<Literal> not_in_literals = {Literal(2.2)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotIn(not_in_literals));
+ CheckResult(result, {0, 4, 6});
+ }
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadAllNonNull) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("int_field", arrow::int32());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ indexer->CreateWriter("int_field", c_schema.get(),
file_writer, pool_));
+ // All values are non-null
+ // Data layout (row_id -> value): 0->10, 1->20, 2->20, 3->30, 4->40, 5->50
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ [10],
+ [20],
+ [20],
+ [30],
+ [40],
+ [50]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids(array->length());
+ std::iota(row_ids.begin(), row_ids.end(), 0);
+ ASSERT_OK(writer->AddBatch(&c_array, std::move(row_ids)));
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ indexer->CreateReader(c_schema.get(), file_reader,
metas, pool_));
+
+ // All rows: {0,1,2,3,4,5}, No null rows
+
+ // --- VisitIsNull -> empty ---
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
+ CheckResult(result, {});
+ }
+
+ // --- VisitIsNotNull -> all rows ---
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
+ CheckResult(result, {0, 1, 2, 3, 4, 5});
+ }
+
+ // --- VisitEqual ---
+ {
+ Literal lit_20(20);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_20));
+ CheckResult(result, {1, 2});
+ }
+
+ // --- VisitNotEqual ---
+ {
+ Literal lit_20(20);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(lit_20));
+ CheckResult(result, {0, 3, 4, 5});
+ }
+
+ // --- VisitLessThan ---
+ {
+ Literal lit_30(30);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(lit_30));
+ CheckResult(result, {0, 1, 2});
+ }
+
+ // --- VisitLessOrEqual ---
+ {
+ Literal lit_30(30);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessOrEqual(lit_30));
+ CheckResult(result, {0, 1, 2, 3});
+ }
+
+ // --- VisitGreaterThan ---
+ {
+ Literal lit_30(30);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitGreaterThan(lit_30));
+ CheckResult(result, {4, 5});
+ }
+
+ // --- VisitGreaterOrEqual ---
+ {
+ Literal lit_30(30);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitGreaterOrEqual(lit_30));
+ CheckResult(result, {3, 4, 5});
+ }
+
+ // --- VisitIn ---
+ {
+ std::vector<Literal> in_literals = {Literal(10), Literal(50)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
+ CheckResult(result, {0, 5});
+ }
+
+ // --- VisitNotIn ---
+ {
+ std::vector<Literal> not_in_literals = {Literal(10), Literal(20)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotIn(not_in_literals));
+ CheckResult(result, {3, 4, 5});
+ }
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadBoolData) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("bool_field", arrow::boolean());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ indexer->CreateWriter("bool_field", c_schema.get(),
file_writer, pool_));
+ // Data layout (row_id -> value), sorted by key (false < true):
+ // 0->false, 1->false, 2->null, 3->false, 4->true, 5->null, 6->true,
7->true
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ [false],
+ [false],
+ [null],
+ [false],
+ [true],
+ [null],
+ [true],
+ [true]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids(array->length());
+ std::iota(row_ids.begin(), row_ids.end(), 0);
+ ASSERT_OK(writer->AddBatch(&c_array, std::move(row_ids)));
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ indexer->CreateReader(c_schema.get(), file_reader,
metas, pool_));
+
+ // Non-null rows: {0,1,3,4,6,7}, Null rows: {2,5}
+
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
+ CheckResult(result, {2, 5});
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
+ CheckResult(result, {0, 1, 3, 4, 6, 7});
+ }
+ {
+ Literal lit_true(true);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_true));
+ CheckResult(result, {4, 6, 7});
+ }
+ {
+ Literal lit_false(false);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_false));
+ CheckResult(result, {0, 1, 3});
+ }
+ {
+ Literal lit_true(true);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(lit_true));
+ CheckResult(result, {0, 1, 3});
+ }
+ {
+ std::vector<Literal> in_literals = {Literal(true)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
+ CheckResult(result, {4, 6, 7});
+ }
+ {
+ std::vector<Literal> not_in_literals = {Literal(true)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotIn(not_in_literals));
+ CheckResult(result, {0, 1, 3});
+ }
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadTinyIntData) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("tinyint_field", arrow::int8());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(
+ auto writer, indexer->CreateWriter("tinyint_field", c_schema.get(),
file_writer, pool_));
+ // Data layout (row_id -> value):
+ // 0->-10, 1->null, 2->0, 3->10, 4->10, 5->null, 6->20
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ [-10],
+ [null],
+ [0],
+ [10],
+ [10],
+ [null],
+ [20]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids(array->length());
+ std::iota(row_ids.begin(), row_ids.end(), 0);
+ ASSERT_OK(writer->AddBatch(&c_array, std::move(row_ids)));
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ indexer->CreateReader(c_schema.get(), file_reader,
metas, pool_));
+
+ // Non-null rows: {0,2,3,4,6}, Null rows: {1,5}
+
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
+ CheckResult(result, {1, 5});
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
+ CheckResult(result, {0, 2, 3, 4, 6});
+ }
+ {
+ Literal lit_10(static_cast<int8_t>(10));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_10));
+ CheckResult(result, {3, 4});
+ }
+ {
+ Literal lit_10(static_cast<int8_t>(10));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(lit_10));
+ CheckResult(result, {0, 2, 6});
+ }
+ {
+ Literal lit_10(static_cast<int8_t>(10));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(lit_10));
+ CheckResult(result, {0, 2});
+ }
+ {
+ Literal lit_10(static_cast<int8_t>(10));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitGreaterOrEqual(lit_10));
+ CheckResult(result, {3, 4, 6});
+ }
+ {
+ std::vector<Literal> in_literals = {Literal(static_cast<int8_t>(-10)),
+ Literal(static_cast<int8_t>(20))};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
+ CheckResult(result, {0, 6});
+ }
+ {
+ std::vector<Literal> not_in_literals =
{Literal(static_cast<int8_t>(10))};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotIn(not_in_literals));
+ CheckResult(result, {0, 2, 6});
+ }
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadSmallIntData) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("smallint_field", arrow::int16());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(
+ auto writer, indexer->CreateWriter("smallint_field", c_schema.get(),
file_writer, pool_));
+ // Data layout (row_id -> value):
+ // 0->-100, 1->null, 2->0, 3->100, 4->100, 5->null, 6->200
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ [-100],
+ [null],
+ [0],
+ [100],
+ [100],
+ [null],
+ [200]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids(array->length());
+ std::iota(row_ids.begin(), row_ids.end(), 0);
+ ASSERT_OK(writer->AddBatch(&c_array, std::move(row_ids)));
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ indexer->CreateReader(c_schema.get(), file_reader,
metas, pool_));
+
+ // Non-null rows: {0,2,3,4,6}, Null rows: {1,5}
+
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
+ CheckResult(result, {1, 5});
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
+ CheckResult(result, {0, 2, 3, 4, 6});
+ }
+ {
+ Literal lit_100(static_cast<int16_t>(100));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_100));
+ CheckResult(result, {3, 4});
+ }
+ {
+ Literal lit_100(static_cast<int16_t>(100));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(lit_100));
+ CheckResult(result, {0, 2, 6});
+ }
+ {
+ Literal lit_100(static_cast<int16_t>(100));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(lit_100));
+ CheckResult(result, {0, 2});
+ }
+ {
+ Literal lit_100(static_cast<int16_t>(100));
+ ASSERT_OK_AND_ASSIGN(auto result,
reader->VisitGreaterOrEqual(lit_100));
+ CheckResult(result, {3, 4, 6});
+ }
+ {
+ std::vector<Literal> in_literals =
{Literal(static_cast<int16_t>(-100)),
+
Literal(static_cast<int16_t>(200))};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
+ CheckResult(result, {0, 6});
+ }
+ {
+ std::vector<Literal> not_in_literals =
{Literal(static_cast<int16_t>(100))};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotIn(not_in_literals));
+ CheckResult(result, {0, 2, 6});
+ }
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadTimestampCompactData) {
+ // Compact timestamp: precision <= 3 (millisecond)
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("ts_field",
arrow::timestamp(arrow::TimeUnit::MILLI));
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ indexer->CreateWriter("ts_field", c_schema.get(),
file_writer, pool_));
+ // Data layout (row_id -> value in millis):
+ // 0->1000, 1->null, 2->2000, 3->2000, 4->3000, 5->null, 6->4000
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ [1000],
+ [null],
+ [2000],
+ [2000],
+ [3000],
+ [null],
+ [4000]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids(array->length());
+ std::iota(row_ids.begin(), row_ids.end(), 0);
+ ASSERT_OK(writer->AddBatch(&c_array, std::move(row_ids)));
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ indexer->CreateReader(c_schema.get(), file_reader,
metas, pool_));
+
+ // Non-null rows: {0,2,3,4,6}, Null rows: {1,5}
+
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
+ CheckResult(result, {1, 5});
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
+ CheckResult(result, {0, 2, 3, 4, 6});
+ }
+ {
+ Literal lit_2000(Timestamp::FromEpochMillis(2000));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_2000));
+ CheckResult(result, {2, 3});
+ }
+ {
+ Literal lit_2000(Timestamp::FromEpochMillis(2000));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(lit_2000));
+ CheckResult(result, {0, 4, 6});
+ }
+ {
+ Literal lit_3000(Timestamp::FromEpochMillis(3000));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(lit_3000));
+ CheckResult(result, {0, 2, 3});
+ }
+ {
+ Literal lit_2000(Timestamp::FromEpochMillis(2000));
+ ASSERT_OK_AND_ASSIGN(auto result,
reader->VisitGreaterOrEqual(lit_2000));
+ CheckResult(result, {2, 3, 4, 6});
+ }
+ {
+ std::vector<Literal> in_literals =
{Literal(Timestamp::FromEpochMillis(1000)),
+
Literal(Timestamp::FromEpochMillis(4000))};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
+ CheckResult(result, {0, 6});
+ }
+ {
+ std::vector<Literal> not_in_literals =
{Literal(Timestamp::FromEpochMillis(2000))};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotIn(not_in_literals));
+ CheckResult(result, {0, 4, 6});
+ }
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadTimestampNonCompactData) {
+ // Non-compact timestamp: precision > 3 (microsecond, precision=6)
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("ts_field",
arrow::timestamp(arrow::TimeUnit::MICRO));
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ indexer->CreateWriter("ts_field", c_schema.get(),
file_writer, pool_));
+ // Data layout (row_id -> value in micros):
+ // 0->1000000 (1s), 1->null, 2->2000123 (2s+123us), 3->2000123,
4->3000456, 5->null,
+ // 6->4000789
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ [1000000],
+ [null],
+ [2000123],
+ [2000123],
+ [3000456],
+ [null],
+ [4000789]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids(array->length());
+ std::iota(row_ids.begin(), row_ids.end(), 0);
+ ASSERT_OK(writer->AddBatch(&c_array, std::move(row_ids)));
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ indexer->CreateReader(c_schema.get(), file_reader,
metas, pool_));
+
+ // Non-null rows: {0,2,3,4,6}, Null rows: {1,5}
+ // micros: 1000000 -> millis=1000, nanos_of_millis=0
+ // 2000123 -> millis=2000, nanos_of_millis=123000
+ // 3000456 -> millis=3000, nanos_of_millis=456000
+ // 4000789 -> millis=4000, nanos_of_millis=789000
+
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
+ CheckResult(result, {1, 5});
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
+ CheckResult(result, {0, 2, 3, 4, 6});
+ }
+ {
+ Literal lit_ts(Timestamp(2000, 123000));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_ts));
+ CheckResult(result, {2, 3});
+ }
+ {
+ Literal lit_ts(Timestamp(2000, 123000));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(lit_ts));
+ CheckResult(result, {0, 4, 6});
+ }
+ {
+ Literal lit_ts(Timestamp(3000, 456000));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(lit_ts));
+ CheckResult(result, {0, 2, 3});
+ }
+ {
+ Literal lit_ts(Timestamp(2000, 123000));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitGreaterOrEqual(lit_ts));
+ CheckResult(result, {2, 3, 4, 6});
+ }
+ {
+ std::vector<Literal> in_literals = {Literal(Timestamp(1000, 0)),
+ Literal(Timestamp(4000, 789000))};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
+ CheckResult(result, {0, 6});
+ }
+ {
+ std::vector<Literal> not_in_literals = {Literal(Timestamp(2000,
123000))};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotIn(not_in_literals));
+ CheckResult(result, {0, 4, 6});
+ }
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadDecimalCompactData) {
+ // Compact decimal: precision <= 18
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("decimal_field", arrow::decimal128(10, 2));
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(
+ auto writer, indexer->CreateWriter("decimal_field", c_schema.get(),
file_writer, pool_));
+ // Data layout (row_id -> value, stored as unscaled int with scale=2):
+ // 0->1.00, 1->null, 2->2.50, 3->2.50, 4->3.00,
+ // 5->null, 6->4.50
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ ["1.00"],
+ [null],
+ ["2.50"],
+ ["2.50"],
+ ["3.00"],
+ [null],
+ ["4.50"]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids(array->length());
+ std::iota(row_ids.begin(), row_ids.end(), 0);
+ ASSERT_OK(writer->AddBatch(&c_array, std::move(row_ids)));
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ indexer->CreateReader(c_schema.get(), file_reader,
metas, pool_));
+
+ // Non-null rows: {0,2,3,4,6}, Null rows: {1,5}
+
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
+ CheckResult(result, {1, 5});
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
+ CheckResult(result, {0, 2, 3, 4, 6});
+ }
+ {
+ Literal lit_250(Decimal::FromUnscaledLong(250, 10, 2));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_250));
+ CheckResult(result, {2, 3});
+ }
+ {
+ Literal lit_250(Decimal::FromUnscaledLong(250, 10, 2));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(lit_250));
+ CheckResult(result, {0, 4, 6});
+ }
+ {
+ Literal lit_300(Decimal::FromUnscaledLong(300, 10, 2));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(lit_300));
+ CheckResult(result, {0, 2, 3});
+ }
+ {
+ Literal lit_250(Decimal::FromUnscaledLong(250, 10, 2));
+ ASSERT_OK_AND_ASSIGN(auto result,
reader->VisitGreaterOrEqual(lit_250));
+ CheckResult(result, {2, 3, 4, 6});
+ }
+ {
+ std::vector<Literal> in_literals =
{Literal(Decimal::FromUnscaledLong(100, 10, 2)),
+
Literal(Decimal::FromUnscaledLong(450, 10, 2))};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
+ CheckResult(result, {0, 6});
+ }
+ {
+ std::vector<Literal> not_in_literals =
{Literal(Decimal::FromUnscaledLong(250, 10, 2))};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotIn(not_in_literals));
+ CheckResult(result, {0, 4, 6});
+ }
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadDecimalNonCompactData) {
+ // Non-compact decimal: precision > 18
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("decimal_field", arrow::decimal128(25, 3));
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(
+ auto writer, indexer->CreateWriter("decimal_field", c_schema.get(),
file_writer, pool_));
+ // Data layout (row_id -> unscaled value with scale=3):
+ // 0->1.000, 1->null, 2->2.500, 3->2.500,
+ // 4->3.000, 5->null, 6->4.500
+ // For non-compact decimal (precision=25), Arrow JSON uses string
representation of unscaled
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ ["1.000"],
+ [null],
+ ["2.500"],
+ ["2.500"],
+ ["3.000"],
+ [null],
+ ["4.500"]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids(array->length());
+ std::iota(row_ids.begin(), row_ids.end(), 0);
+ ASSERT_OK(writer->AddBatch(&c_array, std::move(row_ids)));
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ indexer->CreateReader(c_schema.get(), file_reader,
metas, pool_));
+
+ // Non-null rows: {0,2,3,4,6}, Null rows: {1,5}
+
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
+ CheckResult(result, {1, 5});
+ }
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
+ CheckResult(result, {0, 2, 3, 4, 6});
+ }
+ {
+ Literal lit_2500(Decimal(25, 3, 2500));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_2500));
+ CheckResult(result, {2, 3});
+ }
+ {
+ Literal lit_2500(Decimal(25, 3, 2500));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(lit_2500));
+ CheckResult(result, {0, 4, 6});
+ }
+ {
+ Literal lit_3000(Decimal(25, 3, 3000));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(lit_3000));
+ CheckResult(result, {0, 2, 3});
+ }
+ {
+ Literal lit_2500(Decimal(25, 3, 2500));
+ ASSERT_OK_AND_ASSIGN(auto result,
reader->VisitGreaterOrEqual(lit_2500));
+ CheckResult(result, {2, 3, 4, 6});
+ }
+ {
+ std::vector<Literal> in_literals = {Literal(Decimal(25, 3, 1000)),
+ Literal(Decimal(25, 3, 4500))};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
+ CheckResult(result, {0, 6});
+ }
+ {
+ std::vector<Literal> not_in_literals = {Literal(Decimal(25, 3, 2500))};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotIn(not_in_literals));
+ CheckResult(result, {0, 4, 6});
+ }
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadAllNull) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("int_field", arrow::int32());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ indexer->CreateWriter("int_field", c_schema.get(),
file_writer, pool_));
+ // All values are null
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ [null],
+ [null],
+ [null],
+ [null]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids(array->length());
+ std::iota(row_ids.begin(), row_ids.end(), 0);
+ ASSERT_OK(writer->AddBatch(&c_array, std::move(row_ids)));
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ indexer->CreateReader(c_schema.get(), file_reader,
metas, pool_));
+
+ // All rows are null: {0,1,2,3}, No non-null rows
+
+ // --- VisitIsNull -> all rows ---
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
+ CheckResult(result, {0, 1, 2, 3});
+ }
+
+ // --- VisitIsNotNull -> empty ---
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
+ CheckResult(result, {});
+ }
+
+ // --- VisitEqual -> empty ---
+ {
+ Literal lit_1(1);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_1));
+ CheckResult(result, {});
+ }
+
+ // --- VisitNotEqual -> empty (no non-null rows) ---
+ {
+ Literal lit_1(1);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(lit_1));
+ CheckResult(result, {});
+ }
+
+ // --- VisitLessThan -> empty ---
+ {
+ Literal lit_1(1);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(lit_1));
+ CheckResult(result, {});
+ }
+
+ // --- VisitIn -> empty ---
+ {
+ std::vector<Literal> in_literals = {Literal(1), Literal(2)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
+ CheckResult(result, {});
+ }
+
+ // --- VisitNotIn -> empty ---
+ {
+ std::vector<Literal> not_in_literals = {Literal(1)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotIn(not_in_literals));
+ CheckResult(result, {});
+ }
+
+ // --- VisitGreaterThan -> empty (min_key_/max_key_ are nullopt) ---
+ {
+ Literal lit_1(1);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitGreaterThan(lit_1));
+ CheckResult(result, {});
+ }
+
+ // --- VisitGreaterOrEqual -> empty ---
+ {
+ Literal lit_1(1);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitGreaterOrEqual(lit_1));
+ CheckResult(result, {});
+ }
+
+ // --- VisitLessOrEqual -> empty ---
+ {
+ Literal lit_1(1);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessOrEqual(lit_1));
+ CheckResult(result, {});
+ }
+}
+
+TEST_F(BTreeGlobalIndexIntegrationTest, WriteAndReadLargeDataWithSmallBlocks) {
+ // Use very small block size and cache size to force multiple block
evictions
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("int_field", arrow::int32());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options = {
+ {BtreeDefs::kBtreeIndexBlockSize, "256"},
+ {BtreeDefs::kBtreeIndexCacheSize, "1024"},
+ };
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ indexer->CreateWriter("int_field", c_schema.get(),
file_writer, pool_));
+ // Generate 50000 sorted int values with some nulls and duplicates.
+ // Pattern: every 100th row is null, values increase by 1 every 3 rows
(duplicates).
+ // Data is written in multiple batches of 1000 rows each.
+ constexpr int32_t total_rows = 50000;
+ constexpr int32_t batch_size = 1000;
+
+ std::vector<int64_t> null_row_ids;
+ std::vector<int64_t> non_null_row_ids;
+ int32_t current_value = 0;
+
+ for (int32_t batch_start = 0; batch_start < total_rows; batch_start +=
batch_size) {
+ int32_t batch_end = std::min(batch_start + batch_size, total_rows);
+ int32_t batch_len = batch_end - batch_start;
+
+ arrow::Int32Builder value_builder;
+ ASSERT_TRUE(value_builder.Reserve(batch_len).ok());
+
+ std::vector<int64_t> batch_row_ids;
+ batch_row_ids.reserve(batch_len);
+
+ for (int32_t i = batch_start; i < batch_end; ++i) {
+ batch_row_ids.push_back(i);
+ if (i % 100 == 99) {
+ ASSERT_TRUE(value_builder.AppendNull().ok());
+ null_row_ids.push_back(i);
+ } else {
+ ASSERT_TRUE(value_builder.Append(current_value).ok());
+ non_null_row_ids.push_back(i);
+ if (i % 3 == 2) {
+ ++current_value;
+ }
+ }
+ }
+
+ std::shared_ptr<arrow::Array> value_array;
+ ASSERT_TRUE(value_builder.Finish(&value_array).ok());
+ auto struct_array = arrow::StructArray::Make({value_array},
{field}).ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*struct_array, &c_array).ok());
+ ASSERT_OK(writer->AddBatch(&c_array, std::move(batch_row_ids)));
+ }
+
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+
+ // Helper lambda: given a predicate on value, collect matching row ids
+ auto collect_rows = [total_rows = total_rows](std::function<bool(int32_t)>
predicate) {
+ std::vector<int64_t> result;
+ int32_t val = 0;
+ for (int32_t i = 0; i < total_rows; ++i) {
+ if (i % 100 == 99) {
+ continue;
+ }
+ if (predicate(val)) {
+ result.push_back(i);
+ }
+ if (i % 3 == 2) {
+ ++val;
+ }
+ }
+ return result;
+ };
+
+ // Read back with different read-buffer-size configurations:
+ // "" -> no buffer (original path)
+ // "128" -> very small buffer (smaller than block size 256)
+ // "512" -> moderate buffer (larger than block size)
+ // "64KB" -> large buffer
+ // "1MB" -> very large buffer (likely covers entire file)
+ std::vector<std::string> buffer_sizes = {"", "128", "512", "64KB", "1MB"};
+
+ for (const auto& buf_size : buffer_sizes) {
+ std::map<std::string, std::string> read_options = {
+ {BtreeDefs::kBtreeIndexBlockSize, "256"},
{BtreeDefs::kBtreeIndexCacheSize, "1024"}};
+ if (!buf_size.empty()) {
+ read_options[BtreeDefs::kBtreeIndexReadBufferSize] = buf_size;
+ }
+ ASSERT_OK_AND_ASSIGN(auto read_indexer,
BTreeGlobalIndexer::Create(read_options));
+
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ read_indexer->CreateReader(c_schema.get(),
file_reader, metas, pool_));
+
+ // check input stream and read buffer size
+ {
+ auto lazy_reader =
std::dynamic_pointer_cast<LazyFilteredBTreeReader>(reader);
+ ASSERT_TRUE(lazy_reader);
+ ASSERT_OK_AND_ASSIGN(auto tmp_reader,
lazy_reader->GetOrCreateReader(metas[0]));
+ auto btree_reader =
std::dynamic_pointer_cast<BTreeGlobalIndexReader>(tmp_reader);
+ ASSERT_TRUE(btree_reader);
+ auto input_stream =
btree_reader->sst_file_reader_->block_cache_->in_;
+ auto buffered_stream =
std::dynamic_pointer_cast<BufferedInputStream>(input_stream);
+ if (!buf_size.empty()) {
+ ASSERT_TRUE(buffered_stream);
+ ASSERT_OK_AND_ASSIGN(int32_t expected_buffer_size,
+ MemorySize::ParseBytes(buf_size));
+ ASSERT_EQ(buffered_stream->buffer_size_, expected_buffer_size);
+ } else {
+ ASSERT_FALSE(buffered_stream);
+ }
+ }
+
+ // --- VisitIsNull ---
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
+ CheckResult(result, null_row_ids);
+ }
+
+ // --- VisitIsNotNull ---
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
+ CheckResult(result, non_null_row_ids);
+ }
+
+ // --- VisitEqual for value 0 ---
+ {
+ Literal lit_0(0);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_0));
+ CheckResult(result, collect_rows([](int32_t v) { return v == 0;
}));
+ }
+
+ // --- VisitEqual for a value in the middle ---
+ {
+ Literal lit_100(100);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_100));
+ CheckResult(result, collect_rows([](int32_t v) { return v == 100;
}));
+ }
+
+ // --- VisitEqual for non-existent value ---
+ {
+ Literal lit_neg(static_cast<int32_t>(-1));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(lit_neg));
+ CheckResult(result, {});
+ }
+
+ // --- VisitLessThan for value 5 ---
+ {
+ Literal lit_5(5);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(lit_5));
+ CheckResult(result, collect_rows([](int32_t v) { return v < 5; }));
+ }
+
+ // --- VisitGreaterOrEqual for a high value near max ---
+ {
+ int32_t max_val =
+ static_cast<int32_t>(collect_rows([](int32_t) { return true;
}).size()) / 3;
+ int32_t threshold = max_val - 2;
+ Literal lit_threshold(threshold);
+ ASSERT_OK_AND_ASSIGN(auto result,
reader->VisitGreaterOrEqual(lit_threshold));
+ CheckResult(result, collect_rows([threshold](int32_t v) { return v
>= threshold; }));
+ }
+
+ // --- VisitIn for scattered values ---
+ {
+ std::vector<Literal> in_literals = {Literal(0), Literal(500),
Literal(10000)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(in_literals));
+ CheckResult(result,
+ collect_rows([](int32_t v) { return v == 0 || v == 500
|| v == 10000; }));
+ }
+
+ // --- VisitNotEqual for value 0 ---
+ {
+ Literal lit_0(0);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(lit_0));
+ CheckResult(result, collect_rows([](int32_t v) { return v != 0;
}));
+ }
+ }
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, CreateWriterWithNonStructSchema) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+
+ // Export a plain int32 type (not struct) as ArrowSchema
+ auto plain_type = arrow::int32();
+ ArrowSchema c_schema;
+ ASSERT_TRUE(arrow::ExportType(*plain_type, &c_schema).ok());
+
+ ASSERT_NOK_WITH_MSG(indexer->CreateWriter("int_field", &c_schema,
file_writer, pool_),
+ "arrow schema must be struct type");
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, CreateReaderWithMultiFieldSchema) {
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+
+ // Create a schema with two fields
+ auto schema = arrow::schema(
+ {arrow::field("field1", arrow::int32()), arrow::field("field2",
arrow::int64())});
+ auto c_schema = std::make_unique<ArrowSchema>();
+ ASSERT_TRUE(arrow::ExportSchema(*schema, c_schema.get()).ok());
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+
+ GlobalIndexIOMeta meta("fake_path", 100, nullptr);
+ std::vector<GlobalIndexIOMeta> metas = {meta};
+
+ ASSERT_NOK_WITH_MSG(indexer->CreateReader(c_schema.get(), file_reader,
metas, pool_),
+ "supposed to have single field");
+}
+
+TEST_F(BTreeGlobalIndexIntegrationTest, CreateWriterWithMissingField) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto type = arrow::struct_({arrow::field("existing_field",
arrow::int32())});
+ auto struct_type = std::dynamic_pointer_cast<arrow::StructType>(type);
+ ASSERT_TRUE(struct_type);
+ ASSERT_NOK_WITH_MSG(
+ BTreeGlobalIndexWriter::Create("nonexistent_field", struct_type,
file_writer, 4096,
+ compression_factory_, pool_),
+ "not in arrow_array when Create BTreeGlobalIndexWriter");
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, AddBatchWithNullArray) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("int_field", arrow::int32());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ indexer->CreateWriter("int_field", c_schema.get(),
file_writer, pool_));
+ std::vector<int64_t> row_ids = {0, 1, 2};
+ ASSERT_NOK_WITH_MSG(writer->AddBatch(nullptr, std::move(row_ids)),
+ "CheckRelativeRowIds failed: null c_arrow_array");
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, AddBatchWithMismatchedRowIds) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("int_field", arrow::int32());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ indexer->CreateWriter("int_field", c_schema.get(),
file_writer, pool_));
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ [1],
+ [2],
+ [3]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+
+ // Provide wrong number of row_ids (2 instead of 3)
+ std::vector<int64_t> row_ids = {0, 1};
+ ASSERT_NOK_WITH_MSG(
+ writer->AddBatch(&c_array, std::move(row_ids)),
+ "relative_row_ids length 2 mismatch arrow_array length 3 in
CheckRelativeRowIds");
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, AddBatchWithNonMonotonicKeys) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("int_field", arrow::int32());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ indexer->CreateWriter("int_field", c_schema.get(),
file_writer, pool_));
+ // Write decreasing keys: 3, 2, 1
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ [3],
+ [2],
+ [1]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids = {0, 1, 2};
+ ASSERT_NOK_WITH_MSG(writer->AddBatch(&c_array, std::move(row_ids)),
+ "Users must keep written keys monotonically
incremental");
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, FinishWithEmptyData) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("int_field", arrow::int32());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "128"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ indexer->CreateWriter("int_field", c_schema.get(),
file_writer, pool_));
+ // Finish without adding any data
+ ASSERT_NOK_WITH_MSG(writer->Finish(), "Should never write an empty btree
index file");
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, InvalidReadOptions) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("int_field", arrow::int32());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "4096"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()},
+
{BtreeDefs::kBtreeIndexReadBufferSize, "4GB"}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ ASSERT_NOK_WITH_MSG(indexer->CreateReader(c_schema.get(), file_reader,
/*files=*/{}, pool_),
+ "In BTreeGlobalIndexer::CreateReader: option
btree-index.read-buffer-size "
+ "is 4GB, exceed INT_MAX or less than 0");
+}
+
+TEST_P(BTreeGlobalIndexIntegrationTest, TestIOException) {
+ bool run_complete = false;
+ auto io_hook = paimon::IOHook::GetInstance();
+ for (size_t i = 0; i < 200; i++) {
+ auto test_dir = UniqueTestDirectory::Create("local");
+ ASSERT_TRUE(test_dir);
+ auto local_fs = test_dir->GetFileSystem();
+ auto local_base = test_dir->Str();
+ paimon::ScopeGuard guard([&io_hook]() { io_hook->Clear(); });
+ io_hook->Reset(i, paimon::IOHook::Mode::RETURN_ERROR);
+
+ auto file_writer =
std::make_shared<FakeGlobalIndexFileWriter>(local_fs, local_base);
+ auto field = arrow::field("int_field", arrow::int32());
+ auto c_schema = CreateArrowSchema(field);
+
+ std::map<std::string, std::string> options = {
+ {BtreeDefs::kBtreeIndexBlockSize, "128"},
+ {BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer,
BTreeGlobalIndexer::Create(options));
+
+ // write
+ auto writer_result = indexer->CreateWriter("int_field",
c_schema.get(), file_writer, pool_);
+ CHECK_HOOK_STATUS(writer_result.status(), i);
+ auto writer = std::move(writer_result).value();
+
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ [1], [2], [null], [3], [4], [5]
+ ])")
+ .ValueOrDie();
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids = {0, 1, 2, 3, 4, 5};
+
+ CHECK_HOOK_STATUS(writer->AddBatch(&c_array, std::move(row_ids)), i);
+ auto finish_result = writer->Finish();
+ CHECK_HOOK_STATUS(finish_result.status(), i);
+ auto metas = std::move(finish_result).value();
+
+ // read
+ auto file_reader =
std::make_shared<FakeGlobalIndexFileReader>(local_fs, local_base);
+ c_schema = CreateArrowSchema(field);
+ auto reader_result = indexer->CreateReader(c_schema.get(),
file_reader, metas, pool_);
+ CHECK_HOOK_STATUS(reader_result.status(), i);
+ auto reader = std::move(reader_result).value();
+
+ auto equal_result = reader->VisitEqual(Literal(3));
+ CHECK_HOOK_STATUS(equal_result.status(), i);
+
+ auto typed_result =
+
std::dynamic_pointer_cast<BitmapGlobalIndexResult>(equal_result.value());
+ ASSERT_TRUE(typed_result);
+ auto bitmap_result = typed_result->GetBitmap();
+ CHECK_HOOK_STATUS(bitmap_result.status(), i);
+ ASSERT_TRUE(bitmap_result.value());
+ ASSERT_EQ(*bitmap_result.value(), RoaringBitmap64::From({3}));
+
+ run_complete = true;
+ break;
+ }
+ ASSERT_TRUE(run_complete);
+}
+
+// Multiple files with BTreeFileMetaSelector filtering
+TEST_P(BTreeGlobalIndexIntegrationTest,
WriteAndReadMultiFilesWithMetaSelector) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("int_field", arrow::int32());
+
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "4096"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+
+ // Write 3 separate btree index files with non-overlapping key ranges:
+ //
+ // file0: keys [1, 2, null, 5], row_ids [0, 1, 2, 3]
+ // range [1, 5], has_null=true
+ //
+ // file1: keys [10, 12, 15], row_ids [4, 5, 6]
+ // range [10, 15], has_null=false
+ //
+ // file2: keys [20, null, 25], row_ids [7, 8, 9]
+ // range [20, 25], has_null=true
+ std::vector<GlobalIndexIOMeta> all_metas;
+
+ auto write_file = [&](const std::string& json_data, const
std::vector<int64_t>& row_ids) {
+ auto c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(
+ auto writer, indexer->CreateWriter("int_field", c_schema.get(),
file_writer, pool_));
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), json_data)
+ .ValueOrDie();
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ ASSERT_OK(writer->AddBatch(&c_array, std::vector<int64_t>(row_ids)));
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+ all_metas.push_back(metas[0]);
+ };
+
+ write_file(R"([[1],[2],[null],[5]])", {0, 1, 2, 3});
+ write_file(R"([[10],[12],[15]])", {4, 5, 6});
+ write_file(R"([[20],[null],[25]])", {7, 8, 9});
+
+ ASSERT_EQ(all_metas.size(), 3);
+
+ // Create reader over all 3 files (internally uses LazyFilteredBTreeReader
+
+ // BTreeFileMetaSelector)
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ auto c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ indexer->CreateReader(c_schema.get(), file_reader,
all_metas, pool_));
+
+ // --- VisitEqual: key=12 -> only file1 is selected by meta selector ->
row 5
+ {
+ Literal literal_12(12);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(literal_12));
+ CheckResult(result, {5});
+ }
+
+ // --- VisitEqual: key=1 -> only file0 selected -> row 0
+ {
+ Literal literal_1(1);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(literal_1));
+ CheckResult(result, {0});
+ }
+
+ // --- VisitEqual: key=25 -> only file2 selected -> row 9
+ {
+ Literal literal_25(25);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(literal_25));
+ CheckResult(result, {9});
+ }
+
+ // --- VisitEqual: key=8 -> between file0 and file1 ranges, no file
matches -> empty
+ {
+ Literal literal_8(8);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(literal_8));
+ CheckResult(result, {});
+ }
+
+ // --- VisitLessThan: key < 10 -> only file0 selected -> non-null rows
0,1,3
+ {
+ Literal literal_10(10);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessThan(literal_10));
+ CheckResult(result, {0, 1, 3});
+ }
+
+ // --- VisitGreaterThan: key > 15 -> only file2 selected -> non-null rows
7,9
+ {
+ Literal literal_15(15);
+ ASSERT_OK_AND_ASSIGN(auto result,
reader->VisitGreaterThan(literal_15));
+ CheckResult(result, {7, 9});
+ }
+
+ // --- VisitGreaterOrEqual: key >= 5 -> file0 (key=5) + file1 + file2 ->
rows 3,4,5,6,7,9
+ {
+ Literal literal_5(5);
+ ASSERT_OK_AND_ASSIGN(auto result,
reader->VisitGreaterOrEqual(literal_5));
+ CheckResult(result, {3, 4, 5, 6, 7, 9});
+ }
+
+ // --- VisitLessOrEqual: key <= 2 -> only file0 selected -> rows 0,1
+ {
+ Literal literal_2(2);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitLessOrEqual(literal_2));
+ CheckResult(result, {0, 1});
+ }
+
+ // --- VisitIn: IN(2, 15, 25) -> file0 (key=2) + file1 (key=15) + file2
(key=25) -> rows 1,6,9
+ {
+ std::vector<Literal> literals = {Literal(2), Literal(15), Literal(25)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(literals));
+ CheckResult(result, {1, 6, 9});
+ }
+
+ // --- VisitIn: IN(8, 17) -> no file contains these keys -> empty
+ {
+ std::vector<Literal> literals = {Literal(8), Literal(17)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIn(literals));
+ CheckResult(result, {});
+ }
+
+ // --- VisitNotEqual: key != 10 -> all files selected, all non-null rows
except key=10
+ // non-null: file0 {0,1,3} + file1 {4,5,6} + file2 {7,9}
+ // minus key=10 (row 4) -> {0,1,3,5,6,7,9}
+ {
+ Literal literal_10(10);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotEqual(literal_10));
+ CheckResult(result, {0, 1, 3, 5, 6, 7, 9});
+ }
+
+ // --- VisitIsNull: file0 has null (row 2), file2 has null (row 8) -> {2,
8}
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNull());
+ CheckResult(result, {2, 8});
+ }
+
+ // --- VisitIsNotNull: all non-null rows -> {0,1,3,4,5,6,7,9}
+ {
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitIsNotNull());
+ CheckResult(result, {0, 1, 3, 4, 5, 6, 7, 9});
+ }
+
+ // --- VisitNotIn: NOT IN(1, 12, 20) -> all files selected
+ // non-null rows: {0,1,3,4,5,6,7,9}
+ // minus key=1 (row 0), key=12 (row 5), key=20 (row 7) -> {1,3,4,6,9}
+ {
+ std::vector<Literal> literals = {Literal(1), Literal(12), Literal(20)};
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitNotIn(literals));
+ CheckResult(result, {1, 3, 4, 6, 9});
+ }
+}
+
+// Test both-bounds RangeQuery branch.
+TEST_P(BTreeGlobalIndexIntegrationTest, TestRangeQuery) {
+ auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_,
base_path_);
+ auto field = arrow::field("str_field", arrow::utf8());
+ auto c_schema = CreateArrowSchema(field);
+
+ // Use very small block size to force data into multiple blocks
+ std::map<std::string, std::string> options =
{{BtreeDefs::kBtreeIndexBlockSize, "64"},
+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
+ ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
+ ASSERT_OK_AND_ASSIGN(auto writer,
+ indexer->CreateWriter("str_field", c_schema.get(),
file_writer, pool_));
+ // Data layout
+ // 0->"aaa", 1->"aab", 2->"bba", 3->"bbb", 4->"bbc",
+ // 5->null, 6->"cca", 7->"ccb", 8->"ddd"
+ auto array =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
+ ["aaa"],
+ ["aab"],
+ ["bba"],
+ ["bbb"],
+ ["bbc"],
+ [null],
+ ["cca"],
+ ["ccb"],
+ ["ddd"]
+ ])")
+ .ValueOrDie();
+
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ std::vector<int64_t> row_ids(array->length());
+ std::iota(row_ids.begin(), row_ids.end(), 0);
+ ASSERT_OK(writer->AddBatch(&c_array, std::move(row_ids)));
+ ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
+ ASSERT_EQ(metas.size(), 1);
+
+ auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_,
base_path_);
+ c_schema = CreateArrowSchema(field);
+ ASSERT_OK_AND_ASSIGN(auto reader,
+ indexer->CreateReader(c_schema.get(), file_reader,
metas, pool_));
+
+ // Non-null rows: {0,1,2,3,4,6,7,8}, Null rows: {5}
+
+ {
+ Literal lit_bb(FieldType::STRING, "bb", 2);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitStartsWith(lit_bb));
+ CheckResult(result, {2, 3, 4});
+ }
+
+ {
+ Literal lit_cc(FieldType::STRING, "cc", 2);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitStartsWith(lit_cc));
+ CheckResult(result, {6, 7});
+ }
+
+ {
+ Literal lit_aaa(FieldType::STRING, "aaa", 3);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitGreaterThan(lit_aaa));
+ CheckResult(result, {1, 2, 3, 4, 6, 7, 8});
+ }
+
+ {
+ Literal lit_ddd(FieldType::STRING, "ddd", 3);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitGreaterThan(lit_ddd));
+ CheckResult(result, {});
+ }
+
+ {
+ Literal lit_empty(FieldType::STRING, "", 0);
+ ASSERT_OK_AND_ASSIGN(auto result, reader->VisitStartsWith(lit_empty));
+ CheckResult(result, {0, 1, 2, 3, 4, 6, 7, 8});
+ }
+
+ // Raw RangeQuery call to cover both-bounds branch with
from_inclusive=false and
+ // to_inclusive=false
+ {
+ ASSERT_FALSE(reader->IsThreadSafe());
+ ASSERT_EQ(reader->GetIndexType(), BtreeDefs::kIdentifier);
+ auto lazy_reader =
std::dynamic_pointer_cast<LazyFilteredBTreeReader>(reader);
+ ASSERT_TRUE(lazy_reader);
+ ASSERT_OK_AND_ASSIGN(auto tmp_reader,
lazy_reader->GetOrCreateReader(metas[0]));
+ auto btree_reader =
std::dynamic_pointer_cast<BTreeGlobalIndexReader>(tmp_reader);
+ ASSERT_TRUE(btree_reader);
+ ASSERT_FALSE(btree_reader->IsThreadSafe());
+ ASSERT_EQ(btree_reader->GetIndexType(), BtreeDefs::kIdentifier);
+
+ Literal from_lit(FieldType::STRING, "bba", 3);
+ Literal to_lit(FieldType::STRING, "bbc", 3);
+ ASSERT_OK_AND_ASSIGN(RoaringBitmap64 range_result,
+ btree_reader->RangeQuery(from_lit, to_lit,
/*from_inclusive=*/false,
+ /*to_inclusive=*/false));
+ ASSERT_EQ(range_result, RoaringBitmap64::From({3}));
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(Compression, BTreeGlobalIndexIntegrationTest,
+ ::testing::ValuesIn(std::vector<std::string>({"none",
"zstd", "lz4"})));
+
+} // namespace paimon::test