Skip to content

Commit 5cf9d46

Browse files
authored
Merge branch 'main' into PAIMON-91
2 parents da0bad0 + a05088a commit 5cf9d46

14 files changed

Lines changed: 442 additions & 84 deletions

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,17 @@ Paimon C++ is a high-performance C++ implementation of [Apache Paimon](https://p
3232
- **File systems**: file system abstraction with built-in local and Jindo file system support.
3333
- **File formats**: file format abstraction with built-in ORC, Parquet, and Avro support.
3434
- **Runtime utilities**: memory pool and thread pool abstractions with default implementations.
35-
- **AI-Oriented Features**: supports RowTracking and DataEvolution mode and provides Global Index capabilities including bitmap index, B-tree index, DiskANN-based vector search with Lumina, and Lucene-based full-text search.
35+
- **AI-Oriented Features**: supports RowTracking and DataEvolution mode and provides Global Index
36+
capabilities including B-tree index, DiskANN-based vector search with Lumina, and Lucene-based
37+
full-text search.
3638
- **Compatibility**: compatibility with Apache Paimon Java format and communication protocols,
3739
including commit messages, data splits, and manifests.
3840

41+
> **Bitmap global index compatibility:** Java Paimon now uses a dedicated bitmap global index
42+
> format instead of the previously shared wrapped bitmap file index format.
43+
> Paimon C++ therefore currently treats the `bitmap` global index type as unsupported. The legacy
44+
> implementation remains in the codebase pending migration to the Java-compatible format.
45+
3946
Note: Linux x86_64 and macOS arm64 builds are currently verified.
4047

4148
## Write And Commit Example

include/paimon/global_index/global_index_reader.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class PAIMON_EXPORT GlobalIndexReader : public FunctionVisitor<std::shared_ptr<G
4747
/// @return true if the reader is thread-safe; false otherwise.
4848
virtual bool IsThreadSafe() const = 0;
4949

50-
/// @return An identifier representing the index type. (e.g., "bitmap", "lumina").
50+
/// @return An identifier representing the index type. (e.g., "btree", "lumina").
5151
virtual std::string GetIndexType() const = 0;
5252
};
5353

include/paimon/global_index/global_index_write_task.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class PAIMON_EXPORT GlobalIndexWriteTask {
3636
///
3737
/// @param table_path Path to the table root directory where index files are stored.
3838
/// @param field_name Name of the indexed column (must be present in the table schema).
39-
/// @param index_type Type of global index to build (e.g., "bitmap", "lumina").
39+
/// @param index_type Type of global index to build (e.g., "btree", "lumina").
4040
/// @param index_split The indexed split containing the actual data (e.g., Parquet file) and
4141
// row id range [from, to] for data to build index.
4242
/// The range must be fully contained within the data covered

include/paimon/global_index/global_indexer_factory.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class PAIMON_EXPORT GlobalIndexerFactory : public Factory {
3232
public:
3333
~GlobalIndexerFactory() override = default;
3434

35-
/// Suffix used to distinguish global index identifiers (e.g., "bitmap-global").
35+
/// Suffix used to distinguish global index identifiers (e.g., "btree-global").
3636
static const char GLOBAL_INDEX_IDENTIFIER_SUFFIX[];
3737

3838
/// Creates a `GlobalIndexer` instance by looking up a registered factory using an identifier.
@@ -41,11 +41,11 @@ class PAIMON_EXPORT GlobalIndexerFactory : public Factory {
4141
/// (e.g., "-global") to form the full key used for factory lookup. This ensures namespace
4242
/// separation between file and global index types.
4343
///
44-
/// @param identifier The base name of the index type (e.g., "bitmap").
44+
/// @param identifier The base name of the index type (e.g., "btree").
4545
/// @param options Configuration parameters for the indexer.
4646
/// @return A `Result` containing a unique pointer to the created `GlobalIndexer`,
4747
/// or an error if creation fails.
48-
/// @return nullptr if no matching factory.
48+
/// @return nullptr if no matching factory exists or the index type is temporarily unsupported.
4949
static Result<std::unique_ptr<GlobalIndexer>> Get(
5050
const std::string& identifier, const std::map<std::string, std::string>& options);
5151

src/paimon/common/global_index/btree/btree_file_meta_selector.cpp

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,7 @@ Result<std::vector<GlobalIndexIOMeta>> BTreeFileMetaSelector::VisitEqual(const L
4646
if (meta.OnlyNulls()) {
4747
return false;
4848
}
49-
MemorySlice min_key_slice = WrapKeySlice(meta.FirstKey());
50-
MemorySlice max_key_slice = WrapKeySlice(meta.LastKey());
51-
PAIMON_ASSIGN_OR_RAISE(int32_t cmp_min, comparator_(literal_slice, min_key_slice));
52-
PAIMON_ASSIGN_OR_RAISE(int32_t cmp_max, comparator_(literal_slice, max_key_slice));
53-
return cmp_min >= 0 && cmp_max <= 0;
49+
return Overlaps(meta, literal_slice, literal_slice);
5450
});
5551
}
5652

@@ -67,8 +63,7 @@ Result<std::vector<GlobalIndexIOMeta>> BTreeFileMetaSelector::VisitLessThan(
6763
if (meta.OnlyNulls()) {
6864
return false;
6965
}
70-
MemorySlice min_key_slice = WrapKeySlice(meta.FirstKey());
71-
PAIMON_ASSIGN_OR_RAISE(int32_t cmp, comparator_(min_key_slice, literal_slice));
66+
PAIMON_ASSIGN_OR_RAISE(int32_t cmp, CompareFirstKey(meta, literal_slice));
7267
return cmp < 0;
7368
});
7469
}
@@ -81,8 +76,7 @@ Result<std::vector<GlobalIndexIOMeta>> BTreeFileMetaSelector::VisitLessOrEqual(
8176
if (meta.OnlyNulls()) {
8277
return false;
8378
}
84-
MemorySlice min_key_slice = WrapKeySlice(meta.FirstKey());
85-
PAIMON_ASSIGN_OR_RAISE(int32_t cmp, comparator_(min_key_slice, literal_slice));
79+
PAIMON_ASSIGN_OR_RAISE(int32_t cmp, CompareFirstKey(meta, literal_slice));
8680
return cmp <= 0;
8781
});
8882
}
@@ -95,8 +89,7 @@ Result<std::vector<GlobalIndexIOMeta>> BTreeFileMetaSelector::VisitGreaterThan(
9589
if (meta.OnlyNulls()) {
9690
return false;
9791
}
98-
MemorySlice max_key_slice = WrapKeySlice(meta.LastKey());
99-
PAIMON_ASSIGN_OR_RAISE(int32_t cmp, comparator_(max_key_slice, literal_slice));
92+
PAIMON_ASSIGN_OR_RAISE(int32_t cmp, CompareLastKey(meta, literal_slice));
10093
return cmp > 0;
10194
});
10295
}
@@ -109,8 +102,7 @@ Result<std::vector<GlobalIndexIOMeta>> BTreeFileMetaSelector::VisitGreaterOrEqua
109102
if (meta.OnlyNulls()) {
110103
return false;
111104
}
112-
MemorySlice max_key_slice = WrapKeySlice(meta.LastKey());
113-
PAIMON_ASSIGN_OR_RAISE(int32_t cmp, comparator_(max_key_slice, literal_slice));
105+
PAIMON_ASSIGN_OR_RAISE(int32_t cmp, CompareLastKey(meta, literal_slice));
114106
return cmp >= 0;
115107
});
116108
}
@@ -127,12 +119,9 @@ Result<std::vector<GlobalIndexIOMeta>> BTreeFileMetaSelector::VisitIn(
127119
if (meta.OnlyNulls()) {
128120
return false;
129121
}
130-
MemorySlice min_key_slice = WrapKeySlice(meta.FirstKey());
131-
MemorySlice max_key_slice = WrapKeySlice(meta.LastKey());
132122
for (const auto& literal_slice : literal_slices) {
133-
PAIMON_ASSIGN_OR_RAISE(int32_t cmp_min, comparator_(literal_slice, min_key_slice));
134-
PAIMON_ASSIGN_OR_RAISE(int32_t cmp_max, comparator_(literal_slice, max_key_slice));
135-
if (cmp_min >= 0 && cmp_max <= 0) {
123+
PAIMON_ASSIGN_OR_RAISE(bool overlaps, Overlaps(meta, literal_slice, literal_slice));
124+
if (overlaps) {
136125
return true;
137126
}
138127
}
@@ -176,6 +165,39 @@ Result<std::vector<GlobalIndexIOMeta>> BTreeFileMetaSelector::Filter(
176165
return result;
177166
}
178167

168+
Result<bool> BTreeFileMetaSelector::Overlaps(const BTreeIndexMeta& meta, const MemorySlice& from,
169+
const MemorySlice& to) const {
170+
if (meta.FirstKey()) {
171+
PAIMON_ASSIGN_OR_RAISE(int32_t cmp, comparator_(to, WrapKeySlice(meta.FirstKey())));
172+
if (cmp < 0) {
173+
return false;
174+
}
175+
}
176+
if (meta.LastKey()) {
177+
PAIMON_ASSIGN_OR_RAISE(int32_t cmp, comparator_(from, WrapKeySlice(meta.LastKey())));
178+
if (cmp > 0) {
179+
return false;
180+
}
181+
}
182+
return true;
183+
}
184+
185+
Result<int32_t> BTreeFileMetaSelector::CompareFirstKey(const BTreeIndexMeta& meta,
186+
const MemorySlice& literal) const {
187+
if (!meta.FirstKey()) {
188+
return -1;
189+
}
190+
return comparator_(WrapKeySlice(meta.FirstKey()), literal);
191+
}
192+
193+
Result<int32_t> BTreeFileMetaSelector::CompareLastKey(const BTreeIndexMeta& meta,
194+
const MemorySlice& literal) const {
195+
if (!meta.LastKey()) {
196+
return 1;
197+
}
198+
return comparator_(WrapKeySlice(meta.LastKey()), literal);
199+
}
200+
179201
MemorySlice BTreeFileMetaSelector::WrapKeySlice(const std::shared_ptr<Bytes>& key) {
180202
return MemorySlice::Wrap(MemorySegment::WrapView(key->data(), key->size()));
181203
}

src/paimon/common/global_index/btree/btree_file_meta_selector.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ class BTreeFileMetaSelector : public FunctionVisitor<std::vector<GlobalIndexIOMe
5656

5757
Result<std::vector<GlobalIndexIOMeta>> Filter(const MetaPredicate& predicate) const;
5858

59+
Result<bool> Overlaps(const BTreeIndexMeta& meta, const MemorySlice& from,
60+
const MemorySlice& to) const;
61+
62+
Result<int32_t> CompareFirstKey(const BTreeIndexMeta& meta, const MemorySlice& literal) const;
63+
64+
Result<int32_t> CompareLastKey(const BTreeIndexMeta& meta, const MemorySlice& literal) const;
65+
5966
Result<MemorySlice> SerializeLiteral(const Literal& literal) const;
6067

6168
/// Create a non-owning MemorySlice view over the raw bytes of a key,

src/paimon/common/global_index/btree/btree_file_meta_selector_test.cpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,4 +225,43 @@ TEST_F(BTreeFileMetaSelectorTest, TestOnlyNullsFileExcludedFromRangeQueries) {
225225
ASSERT_EQ(names.count("file6"), 1u);
226226
}
227227

228+
TEST_F(BTreeFileMetaSelectorTest, TestEmptyStringKeyDoesNotCrash) {
229+
std::shared_ptr<MemoryPool> pool = GetDefaultPool();
230+
std::shared_ptr<arrow::DataType> key_type = arrow::utf8();
231+
auto serialize = [&](const char* value, size_t size) {
232+
Literal literal(FieldType::STRING, value, size);
233+
EXPECT_OK_AND_ASSIGN(std::shared_ptr<Bytes> result,
234+
KeySerializer::SerializeKey(literal, key_type, pool.get()));
235+
return result;
236+
};
237+
238+
auto empty_meta =
239+
std::make_shared<BTreeIndexMeta>(serialize("", 0), serialize("www.example.com", 15), false);
240+
auto normal_meta =
241+
std::make_shared<BTreeIndexMeta>(serialize("aaa.com", 7), serialize("zzz.com", 7), false);
242+
auto null_meta = std::make_shared<BTreeIndexMeta>(nullptr, nullptr, true);
243+
std::vector<GlobalIndexIOMeta> files = {
244+
GlobalIndexIOMeta("file_empty", 1, empty_meta->Serialize(pool.get())),
245+
GlobalIndexIOMeta("file_normal", 1, normal_meta->Serialize(pool.get())),
246+
GlobalIndexIOMeta("file_nulls", 1, null_meta->Serialize(pool.get())),
247+
};
248+
249+
BTreeFileMetaSelector selector(files, key_type, pool);
250+
251+
ASSERT_OK_AND_ASSIGN(std::vector<GlobalIndexIOMeta> result,
252+
selector.VisitEqual(Literal(FieldType::STRING, "www.example.com", 15)));
253+
CheckResult(result, {"file_empty", "file_normal"});
254+
255+
ASSERT_OK_AND_ASSIGN(result, selector.VisitLessThan(Literal(FieldType::STRING, "bbb.com", 7)));
256+
CheckResult(result, {"file_empty", "file_normal"});
257+
258+
ASSERT_OK_AND_ASSIGN(
259+
result, selector.VisitGreaterThan(Literal(FieldType::STRING, "www.example.com", 15)));
260+
CheckResult(result, {"file_normal"});
261+
262+
ASSERT_OK_AND_ASSIGN(result, selector.VisitIn({Literal(FieldType::STRING, "", 0),
263+
Literal(FieldType::STRING, "zzz.com", 7)}));
264+
CheckResult(result, {"file_empty", "file_normal"});
265+
}
266+
228267
} // namespace paimon::test

src/paimon/common/global_index/btree/btree_global_index_integration_test.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "paimon/common/factories/io_hook.h"
2121
#include "paimon/common/global_index/btree/btree_global_index_writer.h"
2222
#include "paimon/common/global_index/btree/btree_global_indexer.h"
23+
#include "paimon/common/global_index/btree/btree_index_meta.h"
2324
#include "paimon/common/global_index/btree/lazy_filtered_btree_reader.h"
2425
#include "paimon/common/options/memory_size.h"
2526
#include "paimon/common/utils/scope_guard.h"
@@ -457,6 +458,50 @@ TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadStringData) {
457458
}
458459
}
459460

461+
TEST_P(BTreeGlobalIndexIntegrationTest, WriteEmptyStringKeyMetadata) {
462+
auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_, base_path_);
463+
auto field = arrow::field("str_field", arrow::utf8());
464+
auto c_schema = CreateArrowSchema(field);
465+
466+
std::map<std::string, std::string> options = {{BtreeDefs::kBtreeIndexBlockSize, "128"},
467+
{BtreeDefs::kBtreeIndexCompression, GetParam()}};
468+
ASSERT_OK_AND_ASSIGN(auto indexer, BTreeGlobalIndexer::Create(options));
469+
ASSERT_OK_AND_ASSIGN(auto writer,
470+
indexer->CreateWriter("str_field", c_schema.get(), file_writer, pool_));
471+
auto array = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}), R"([
472+
[null],
473+
[""],
474+
["abc"]
475+
])")
476+
.ValueOrDie();
477+
478+
ArrowArray c_array;
479+
ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
480+
std::vector<int64_t> row_ids(array->length());
481+
std::iota(row_ids.begin(), row_ids.end(), 0);
482+
ASSERT_OK(writer->AddBatch(&c_array, std::move(row_ids)));
483+
ASSERT_OK_AND_ASSIGN(auto metas, writer->Finish());
484+
ASSERT_EQ(metas.size(), 1);
485+
486+
std::shared_ptr<BTreeIndexMeta> meta =
487+
BTreeIndexMeta::Deserialize(metas[0].metadata, pool_.get());
488+
ASSERT_TRUE(meta->FirstKey());
489+
ASSERT_EQ(meta->FirstKey()->size(), 0);
490+
ASSERT_TRUE(meta->LastKey());
491+
ASSERT_EQ(std::string(meta->LastKey()->data(), meta->LastKey()->size()), "abc");
492+
ASSERT_TRUE(meta->HasNulls());
493+
ASSERT_FALSE(meta->OnlyNulls());
494+
495+
auto file_reader = std::make_shared<FakeGlobalIndexFileReader>(fs_, base_path_);
496+
c_schema = CreateArrowSchema(field);
497+
ASSERT_OK_AND_ASSIGN(auto reader,
498+
indexer->CreateReader(c_schema.get(), file_reader, metas, pool_));
499+
500+
Literal empty(FieldType::STRING, "", 0);
501+
ASSERT_OK_AND_ASSIGN(auto result, reader->VisitEqual(empty));
502+
CheckResult(result, {1});
503+
}
504+
460505
TEST_P(BTreeGlobalIndexIntegrationTest, WriteAndReadBigIntData) {
461506
auto file_writer = std::make_shared<FakeGlobalIndexFileWriter>(fs_, base_path_);
462507
auto field = arrow::field("bigint_field", arrow::int64());

src/paimon/common/global_index/btree/btree_index_meta.cpp

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,48 +19,71 @@
1919
#include "paimon/common/memory/memory_slice_output.h"
2020

2121
namespace paimon {
22+
namespace {
23+
24+
std::shared_ptr<Bytes> ReadKey(MemorySliceInput* input, int32_t key_length, MemoryPool* pool) {
25+
if (key_length == 0) {
26+
return std::make_shared<Bytes>(0, pool);
27+
}
28+
return input->ReadSliceView(key_length).CopyBytes(pool);
29+
}
30+
31+
} // namespace
2232

2333
std::shared_ptr<BTreeIndexMeta> BTreeIndexMeta::Deserialize(const std::shared_ptr<Bytes>& meta,
2434
paimon::MemoryPool* pool) {
25-
auto slice = MemorySlice::Wrap(meta);
26-
auto input = slice.ToInput();
27-
auto first_key_len = input.ReadInt();
28-
std::shared_ptr<Bytes> first_key;
29-
if (first_key_len) {
30-
first_key = input.ReadSliceView(first_key_len).CopyBytes(pool);
31-
}
32-
auto last_key_len = input.ReadInt();
33-
std::shared_ptr<Bytes> last_key;
34-
if (last_key_len) {
35-
last_key = input.ReadSliceView(last_key_len).CopyBytes(pool);
35+
MemorySlice slice = MemorySlice::Wrap(meta);
36+
MemorySliceInput input = slice.ToInput();
37+
int32_t first_key_len = input.ReadInt();
38+
std::shared_ptr<Bytes> first_key = ReadKey(&input, first_key_len, pool);
39+
int32_t last_key_len = input.ReadInt();
40+
std::shared_ptr<Bytes> last_key = ReadKey(&input, last_key_len, pool);
41+
bool has_nulls = input.ReadByte() == static_cast<int8_t>(1);
42+
43+
if (input.Available() >= 2) {
44+
int8_t format_version = input.ReadByte();
45+
if (format_version == kFormatVersionWithNullFlags) {
46+
int8_t null_key_flags = input.ReadByte();
47+
if ((null_key_flags & kFirstKeyIsNull) != 0) {
48+
first_key.reset();
49+
}
50+
if ((null_key_flags & kLastKeyIsNull) != 0) {
51+
last_key.reset();
52+
}
53+
}
54+
} else if (first_key_len == 0 && last_key_len == 0 && has_nulls) {
55+
// Legacy metadata used zero length for null keys. Both empty boundaries plus a null bitmap
56+
// identify an all-null file; a single empty boundary remains a valid serialized key.
57+
first_key.reset();
58+
last_key.reset();
3659
}
37-
auto has_nulls = input.ReadByte() == static_cast<int8_t>(1);
3860
return std::make_shared<BTreeIndexMeta>(first_key, last_key, has_nulls);
3961
}
4062

4163
std::shared_ptr<Bytes> BTreeIndexMeta::Serialize(paimon::MemoryPool* pool) const {
42-
// Calculate total size: first_key_len(4) + first_key + last_key_len(4) + last_key +
43-
// has_nulls(1)
4464
int32_t first_key_size = first_key_ ? first_key_->size() : 0;
4565
int32_t last_key_size = last_key_ ? last_key_->size() : 0;
4666
int32_t total_size = Size();
47-
4867
MemorySliceOutput output(total_size, pool);
68+
int8_t null_key_flags = 0;
4969

50-
// Write first_key_len and first_key
5170
output.WriteValue(first_key_size);
5271
if (first_key_) {
5372
output.WriteBytes(first_key_);
73+
} else {
74+
null_key_flags |= kFirstKeyIsNull;
5475
}
5576

56-
// Write last_key_len and last_key
5777
output.WriteValue(last_key_size);
5878
if (last_key_) {
5979
output.WriteBytes(last_key_);
80+
} else {
81+
null_key_flags |= kLastKeyIsNull;
6082
}
6183

62-
// Write has_nulls
6384
output.WriteValue(static_cast<int8_t>(has_nulls_ ? 1 : 0));
85+
output.WriteValue(kFormatVersionWithNullFlags);
86+
output.WriteValue(null_key_flags);
6487

6588
return output.ToSlice().GetOrCreateHeapMemory(pool);
6689
}

0 commit comments

Comments
 (0)