This is an automated email from the ASF dual-hosted git repository.
SteNicholas 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 d688d399 feat(global-index): add bitmap index format (#352)
d688d399 is described below
commit d688d3992a2df2e592843beaf533d587b2d11ff9
Author: lxy <[email protected]>
AuthorDate: Thu Sep 24 18:17:46 2026 +0800
feat(global-index): add bitmap index format (#352)
---
src/paimon/CMakeLists.txt | 2 +
src/paimon/common/global_index/CMakeLists.txt | 3 +
.../bitmap/bitmap_global_index_format.cpp | 583 +++++++++++++++++++++
.../bitmap/bitmap_global_index_format.h | 303 +++++++++++
.../bitmap/bitmap_global_index_format_test.cpp | 143 +++++
.../bitmap/bitmap_global_index_writer.cpp | 182 +++++++
.../bitmap/bitmap_global_index_writer.h | 97 ++++
.../global_index/bitmap/bitmap_index_reader.cpp | 579 ++++++++++++++++++++
.../global_index/bitmap/bitmap_index_reader.h | 160 ++++++
.../bitmap/bitmap_index_reader_test.cpp | 172 ++++++
src/paimon/common/io/data_output_stream.cpp | 6 +-
src/paimon/common/io/data_output_stream.h | 4 +-
src/paimon/common/utils/var_length_int_utils.h | 37 ++
.../common/utils/var_length_int_utils_test.cpp | 50 ++
14 files changed, 2319 insertions(+), 2 deletions(-)
diff --git a/src/paimon/CMakeLists.txt b/src/paimon/CMakeLists.txt
index 6fc99343..eb17538d 100644
--- a/src/paimon/CMakeLists.txt
+++ b/src/paimon/CMakeLists.txt
@@ -617,6 +617,8 @@ if(PAIMON_BUILD_TESTS)
common/global_index/bitmap_global_index_result_test.cpp
common/global_index/bitmap_scored_global_index_result_test.cpp
common/global_index/bitmap/bitmap_global_index_test.cpp
+
common/global_index/bitmap/bitmap_global_index_format_test.cpp
+ common/global_index/bitmap/bitmap_index_reader_test.cpp
common/global_index/sorted_index_file_meta_test.cpp
common/global_index/btree/btree_file_footer_test.cpp
common/global_index/key_serializer_test.cpp
diff --git a/src/paimon/common/global_index/CMakeLists.txt
b/src/paimon/common/global_index/CMakeLists.txt
index 18806d58..4a0e92fa 100644
--- a/src/paimon/common/global_index/CMakeLists.txt
+++ b/src/paimon/common/global_index/CMakeLists.txt
@@ -16,6 +16,9 @@
set(PAIMON_GLOBAL_INDEX_SRC
bitmap/bitmap_global_index.cpp
+ bitmap/bitmap_global_index_format.cpp
+ bitmap/bitmap_global_index_writer.cpp
+ bitmap/bitmap_index_reader.cpp
bitmap/bitmap_global_index_factory.cpp
btree/btree_file_footer.cpp
btree/btree_global_index_factory.cpp
diff --git
a/src/paimon/common/global_index/bitmap/bitmap_global_index_format.cpp
b/src/paimon/common/global_index/bitmap/bitmap_global_index_format.cpp
new file mode 100644
index 00000000..279ac27a
--- /dev/null
+++ b/src/paimon/common/global_index/bitmap/bitmap_global_index_format.cpp
@@ -0,0 +1,583 @@
+/*
+ * 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 "paimon/common/global_index/bitmap/bitmap_global_index_format.h"
+
+#include <algorithm>
+#include <limits>
+
+#include "fmt/format.h"
+#include "paimon/common/global_index/key_serializer.h"
+#include "paimon/common/io/data_output_stream.h"
+#include "paimon/common/memory/memory_slice_input.h"
+#include "paimon/common/memory/memory_slice_output.h"
+#include "paimon/common/sst/block_trailer.h"
+#include "paimon/common/sst/sst_file_utils.h"
+#include "paimon/common/utils/crc32c.h"
+#include "paimon/common/utils/math.h"
+#include "paimon/common/utils/var_length_int_utils.h"
+#include "paimon/io/byte_order.h"
+#include "paimon/predicate/literal.h"
+
+namespace paimon {
+namespace {
+
+Status WriteAll(const char* data, int64_t length, OutputStream* output_stream)
{
+ PAIMON_ASSIGN_OR_RAISE(int64_t written, output_stream->Write(data,
length));
+ if (written != length) {
+ return Status::IOError(
+ fmt::format("Failed to write bitmap global index block: expected
{} bytes, wrote {}.",
+ length, written));
+ }
+ return Status::OK();
+}
+
+Result<std::shared_ptr<Bytes>> ReadKey(MemorySliceInput* input, MemoryPool*
pool) {
+ PAIMON_ASSIGN_OR_RAISE(int32_t key_length,
VarLengthIntUtils::ReadVarLenInt(input));
+ if (key_length > input->Available()) {
+ return Status::Invalid(
+ fmt::format("Bitmap dictionary key length {} exceeds remaining
block size {}.",
+ key_length, input->Available()));
+ }
+ return input->ReadSliceView(key_length).CopyBytes(pool);
+}
+
+Status ValidateBlockInfo(const BitmapGlobalIndexFormat::BlockInfo& block) {
+ if (block.Offset() < 0) {
+ return Status::Invalid("Invalid negative bitmap block offset.");
+ }
+ if (block.Length() < 0) {
+ return Status::Invalid("Invalid negative bitmap block length.");
+ }
+ return Status::OK();
+}
+
+} // namespace
+
+Result<BitmapGlobalIndexFormat::SerializedKey>
BitmapGlobalIndexFormat::SerializedKey::FromLiteral(
+ const std::shared_ptr<KeySerializer>& serializer, const Literal& literal) {
+ if (serializer == nullptr) {
+ return Status::Invalid("Cannot serialize a bitmap dictionary key
without KeySerializer.");
+ }
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Bytes> bytes,
serializer->Serialize(literal));
+ return SerializedKey(std::move(bytes));
+}
+
+int32_t BitmapGlobalIndexFormat::SerializedKey::CompareTo(const SerializedKey&
other) const {
+ if (bytes_ == nullptr || other.bytes_ == nullptr) {
+ if (bytes_ == other.bytes_) {
+ return 0;
+ }
+ return bytes_ == nullptr ? -1 : 1;
+ }
+ size_t compare_length = std::min(bytes_->size(), other.bytes_->size());
+ for (size_t i = 0; i < compare_length; ++i) {
+ int32_t left = static_cast<uint8_t>(bytes_->data()[i]);
+ int32_t right = static_cast<uint8_t>(other.bytes_->data()[i]);
+ if (left != right) {
+ return left < right ? -1 : 1;
+ }
+ }
+ if (bytes_->size() == other.bytes_->size()) {
+ return 0;
+ }
+ return bytes_->size() < other.bytes_->size() ? -1 : 1;
+}
+
+Result<std::shared_ptr<Bytes>> BitmapGlobalIndexFormat::SeekableReader::Read(
+ const BlockInfo& block) {
+ PAIMON_RETURN_NOT_OK(ValidateBlockInfo(block));
+ return Read(block.Offset(), block.Length());
+}
+
+Result<int32_t> BitmapGlobalIndexFormat::DictionaryEntry::EstimatedSize()
const {
+ if (key_.GetBytes() == nullptr) {
+ return Status::Invalid("Bitmap dictionary key is null.");
+ }
+ PAIMON_RETURN_NOT_OK(
+ ValidateValueInRange<int32_t>(key_.GetBytes()->size(), "bitmap
dictionary key length"));
+ PAIMON_ASSIGN_OR_RAISE(int32_t key_length_size,
+
EstimatedVarLenIntSize(static_cast<int32_t>(key_.GetBytes()->size())));
+ PAIMON_ASSIGN_OR_RAISE(int32_t offset_size,
EstimatedVarLenLongSize(bitmap_block_.Offset()));
+ PAIMON_ASSIGN_OR_RAISE(int32_t length_size,
EstimatedVarLenIntSize(bitmap_block_.Length()));
+ int64_t size =
+ static_cast<int64_t>(key_length_size) + key_.GetBytes()->size() +
offset_size + length_size;
+ PAIMON_RETURN_NOT_OK(ValidateValueInRange<int32_t>(size, "bitmap
dictionary entry size"));
+ return static_cast<int32_t>(size);
+}
+
+Result<std::unique_ptr<BitmapGlobalIndexFormat::StreamingWriter>>
+BitmapGlobalIndexFormat::StreamingWriter::Create(
+ const std::shared_ptr<OutputStream>& output_stream, int32_t
dictionary_block_size,
+ const std::shared_ptr<BlockCompressionFactory>& compression_factory,
+ const std::shared_ptr<MemoryPool>& pool) {
+ if (output_stream == nullptr) {
+ return Status::Invalid("Cannot create bitmap StreamingWriter without
an output stream.");
+ }
+ if (dictionary_block_size <= 0) {
+ return Status::Invalid("Bitmap dictionary block size must be greater
than 0.");
+ }
+ if (pool == nullptr) {
+ return Status::Invalid("Cannot create bitmap StreamingWriter without a
memory pool.");
+ }
+ return std::unique_ptr<StreamingWriter>(
+ new StreamingWriter(output_stream, dictionary_block_size,
compression_factory, pool));
+}
+
+Status BitmapGlobalIndexFormat::StreamingWriter::Write(SerializedKey key,
+ const RoaringBitmap64&
bitmap) {
+ if (finished_) {
+ return Status::Invalid("Cannot write to a finished bitmap
StreamingWriter.");
+ }
+ if (key.GetBytes() == nullptr) {
+ return Status::Invalid("Cannot write a null serialized bitmap
dictionary key.");
+ }
+ PAIMON_ASSIGN_OR_RAISE(BlockInfo bitmap_block,
+ WriteBitmapBlock(bitmap, output_stream_.get(),
pool_.get()));
+ DictionaryEntry entry(std::move(key), std::move(bitmap_block));
+ PAIMON_ASSIGN_OR_RAISE(int32_t estimated_size,
EstimatedDictionaryBlockSizeAfter(entry));
+ if (!current_dictionary_entries_.empty() && estimated_size >
dictionary_block_size_) {
+ PAIMON_RETURN_NOT_OK(FlushDictionaryBlock());
+ }
+ PAIMON_ASSIGN_OR_RAISE(int32_t entry_size, entry.EstimatedSize());
+ if (value_count_ == std::numeric_limits<int32_t>::max()) {
+ return Status::Invalid("Bitmap global index value count exceeds
INT32_MAX.");
+ }
+ current_dictionary_entries_size_ += entry_size;
+ current_dictionary_entries_.push_back(std::move(entry));
+ ++value_count_;
+ return Status::OK();
+}
+
+Status BitmapGlobalIndexFormat::StreamingWriter::Finish(const RoaringBitmap64&
null_rows,
+ const RoaringBitmap64&
non_null_rows) {
+ if (finished_) {
+ return Status::Invalid("Bitmap StreamingWriter has already been
finished.");
+ }
+ PAIMON_RETURN_NOT_OK(FlushDictionaryBlock());
+ PAIMON_ASSIGN_OR_RAISE(BlockInfo null_rows_block,
+ WriteBitmapBlock(null_rows, output_stream_.get(),
pool_.get()));
+ PAIMON_ASSIGN_OR_RAISE(BlockInfo non_null_rows_block,
+ WriteBitmapBlock(non_null_rows,
output_stream_.get(), pool_.get()));
+ PAIMON_ASSIGN_OR_RAISE(BlockInfo index_block,
+ WriteIndexBlock(dictionary_block_metas_,
compression_factory_.get(),
+ output_stream_.get(), pool_.get()));
+ PAIMON_RETURN_NOT_OK(WriteFooter(null_rows_block, non_null_rows_block,
index_block,
+ value_count_, output_stream_.get()));
+ finished_ = true;
+ return Status::OK();
+}
+
+Status BitmapGlobalIndexFormat::StreamingWriter::FlushDictionaryBlock() {
+ if (current_dictionary_entries_.empty()) {
+ return Status::OK();
+ }
+ PAIMON_ASSIGN_OR_RAISE(
+ DictionaryBlockMeta block,
+ WriteDictionaryBlock(current_dictionary_entries_,
compression_factory_.get(),
+ output_stream_.get(), pool_.get()));
+ dictionary_block_metas_.push_back(std::move(block));
+ current_dictionary_entries_.clear();
+ current_dictionary_entries_size_ = 0;
+ return Status::OK();
+}
+
+Result<int32_t>
BitmapGlobalIndexFormat::StreamingWriter::EstimatedDictionaryBlockSizeAfter(
+ const DictionaryEntry& entry) const {
+ size_t entry_count = current_dictionary_entries_.size() + 1;
+ PAIMON_RETURN_NOT_OK(
+ ValidateValueInRange<int32_t>(entry_count, "bitmap dictionary block
entry count"));
+ PAIMON_ASSIGN_OR_RAISE(int32_t count_size,
+
EstimatedVarLenIntSize(static_cast<int32_t>(entry_count)));
+ PAIMON_ASSIGN_OR_RAISE(int32_t entry_size, entry.EstimatedSize());
+ int64_t total =
+ static_cast<int64_t>(count_size) + current_dictionary_entries_size_ +
entry_size;
+ PAIMON_RETURN_NOT_OK(ValidateValueInRange<int32_t>(total, "bitmap
dictionary block size"));
+ return static_cast<int32_t>(total);
+}
+
+Status BitmapGlobalIndexFormat::WriteFooter(const BlockInfo& null_rows_block,
+ const BlockInfo&
non_null_rows_block,
+ const BlockInfo& index_block,
int32_t value_count,
+ OutputStream* output_stream) {
+ PAIMON_RETURN_NOT_OK(ValidateBlockInfo(null_rows_block));
+ PAIMON_RETURN_NOT_OK(ValidateBlockInfo(non_null_rows_block));
+ PAIMON_RETURN_NOT_OK(ValidateBlockInfo(index_block));
+ if (value_count < 0) {
+ return Status::Invalid("Invalid negative bitmap value count.");
+ }
+ DataOutputStream output(output_stream);
+ output.SetOrder(ByteOrder::PAIMON_BIG_ENDIAN);
+ PAIMON_RETURN_NOT_OK(output.WriteValue<int64_t>(null_rows_block.Offset()));
+ PAIMON_RETURN_NOT_OK(output.WriteValue<int32_t>(null_rows_block.Length()));
+
PAIMON_RETURN_NOT_OK(output.WriteValue<int64_t>(non_null_rows_block.Offset()));
+
PAIMON_RETURN_NOT_OK(output.WriteValue<int32_t>(non_null_rows_block.Length()));
+ PAIMON_RETURN_NOT_OK(output.WriteValue<int64_t>(index_block.Offset()));
+ PAIMON_RETURN_NOT_OK(output.WriteValue<int32_t>(index_block.Length()));
+ PAIMON_RETURN_NOT_OK(output.WriteValue<int32_t>(value_count));
+ PAIMON_RETURN_NOT_OK(output.WriteValue<int32_t>(kVersion));
+ PAIMON_RETURN_NOT_OK(output.WriteValue<int32_t>(kMagic));
+ return output_stream->Flush();
+}
+
+Result<BitmapGlobalIndexFormat::BlockInfo>
BitmapGlobalIndexFormat::WriteBitmapBlock(
+ const RoaringBitmap64& bitmap, OutputStream* output_stream, MemoryPool*
pool) {
+ std::shared_ptr<Bytes> bytes = bitmap.Serialize(pool);
+ PAIMON_RETURN_NOT_OK(
+ ValidateValueInRange<int32_t>(bytes->size(), "serialized bitmap block
size"));
+ PAIMON_ASSIGN_OR_RAISE(int64_t offset, output_stream->GetPos());
+ PAIMON_RETURN_NOT_OK(WriteAll(bytes->data(), bytes->size(),
output_stream));
+ return BlockInfo(offset, static_cast<int32_t>(bytes->size()));
+}
+
+Result<BitmapGlobalIndexFormat::DictionaryBlockMeta>
BitmapGlobalIndexFormat::WriteDictionaryBlock(
+ const std::vector<DictionaryEntry>& entries, BlockCompressionFactory*
compression_factory,
+ OutputStream* output_stream, MemoryPool* pool) {
+ if (entries.empty()) {
+ return Status::Invalid("Cannot write an empty bitmap dictionary
block.");
+ }
+ PAIMON_RETURN_NOT_OK(
+ ValidateValueInRange<int32_t>(entries.size(), "bitmap dictionary block
entry count"));
+ PAIMON_ASSIGN_OR_RAISE(int32_t entry_count_size,
+
EstimatedVarLenIntSize(static_cast<int32_t>(entries.size())));
+ int64_t estimated_size = entry_count_size;
+ for (const DictionaryEntry& entry : entries) {
+ PAIMON_ASSIGN_OR_RAISE(int32_t entry_size, entry.EstimatedSize());
+ estimated_size += entry_size;
+ }
+ PAIMON_RETURN_NOT_OK(
+ ValidateValueInRange<int32_t>(estimated_size, "bitmap dictionary block
size"));
+ MemorySliceOutput output(static_cast<int32_t>(estimated_size), pool);
+
PAIMON_RETURN_NOT_OK(output.WriteVarLenInt(static_cast<int32_t>(entries.size())));
+ for (const DictionaryEntry& entry : entries) {
+ const std::shared_ptr<Bytes>& key_bytes = entry.Key().GetBytes();
+
PAIMON_RETURN_NOT_OK(output.WriteVarLenInt(static_cast<int32_t>(key_bytes->size())));
+ output.WriteBytes(key_bytes);
+
PAIMON_RETURN_NOT_OK(output.WriteVarLenLong(entry.BitmapBlock().Offset()));
+
PAIMON_RETURN_NOT_OK(output.WriteVarLenInt(entry.BitmapBlock().Length()));
+ }
+ std::shared_ptr<Bytes> bytes = output.ToSlice().CopyBytes(pool);
+ PAIMON_ASSIGN_OR_RAISE(BlockInfo block,
+ WriteCompressibleBlock(bytes, compression_factory,
output_stream, pool));
+ return DictionaryBlockMeta(entries.front().Key(), block.Offset(),
block.Length());
+}
+
+Result<BitmapGlobalIndexFormat::BlockInfo>
BitmapGlobalIndexFormat::WriteIndexBlock(
+ const std::vector<DictionaryBlockMeta>& blocks, BlockCompressionFactory*
compression_factory,
+ OutputStream* output_stream, MemoryPool* pool) {
+ PAIMON_ASSIGN_OR_RAISE(int32_t estimated_size,
EstimatedIndexBlockSize(blocks));
+ MemorySliceOutput output(estimated_size, pool);
+
PAIMON_RETURN_NOT_OK(output.WriteVarLenInt(static_cast<int32_t>(blocks.size())));
+ for (const DictionaryBlockMeta& block : blocks) {
+ const std::shared_ptr<Bytes>& key_bytes = block.FirstKey().GetBytes();
+
PAIMON_RETURN_NOT_OK(output.WriteVarLenInt(static_cast<int32_t>(key_bytes->size())));
+ output.WriteBytes(key_bytes);
+ PAIMON_RETURN_NOT_OK(output.WriteVarLenLong(block.Offset()));
+ PAIMON_RETURN_NOT_OK(output.WriteVarLenInt(block.Length()));
+ }
+ std::shared_ptr<Bytes> bytes = output.ToSlice().CopyBytes(pool);
+ return WriteCompressibleBlock(bytes, compression_factory, output_stream,
pool);
+}
+
+Result<BitmapGlobalIndexFormat::BlockInfo>
BitmapGlobalIndexFormat::WriteCompressibleBlock(
+ const std::shared_ptr<Bytes>& uncompressed, BlockCompressionFactory*
compression_factory,
+ OutputStream* output_stream, MemoryPool* pool) {
+ PAIMON_ASSIGN_OR_RAISE(BlockEncoding encoding,
+ EncodeBlock(uncompressed, compression_factory,
pool));
+ PAIMON_ASSIGN_OR_RAISE(int64_t offset, output_stream->GetPos());
+ PAIMON_RETURN_NOT_OK(WriteAll(encoding.bytes->data(), encoding.length,
output_stream));
+
+ uint32_t crc = CRC32C::calculate(encoding.bytes->data(), encoding.length);
+ char compression_value =
+ static_cast<char>(static_cast<int32_t>(encoding.compression_type) &
0xFF);
+ crc = CRC32C::calculate(&compression_value, sizeof(compression_value),
crc);
+ BlockTrailer trailer(static_cast<int8_t>(encoding.compression_type),
static_cast<int32_t>(crc));
+ MemorySlice trailer_slice = trailer.WriteBlockTrailer(pool);
+ PAIMON_RETURN_NOT_OK(WriteAll(trailer_slice.Data(),
trailer_slice.Length(), output_stream));
+ return BlockInfo(offset, encoding.length);
+}
+
+Result<BitmapGlobalIndexFormat::BlockEncoding>
BitmapGlobalIndexFormat::EncodeBlock(
+ const std::shared_ptr<Bytes>& uncompressed, BlockCompressionFactory*
compression_factory,
+ MemoryPool* pool) {
+ if (uncompressed == nullptr) {
+ return Status::Invalid("Uncompressed bitmap index block is null.");
+ }
+ PAIMON_RETURN_NOT_OK(ValidateValueInRange<int32_t>(uncompressed->size(),
+ "uncompressed bitmap
index block size"));
+ auto uncompressed_length = static_cast<int32_t>(uncompressed->size());
+ BlockEncoding result{uncompressed, uncompressed_length,
BlockCompressionType::NONE};
+ if (compression_factory == nullptr ||
+ compression_factory->GetCompressionType() ==
BlockCompressionType::NONE) {
+ return result;
+ }
+
+ std::shared_ptr<BlockCompressor> compressor =
compression_factory->GetCompressor();
+ if (compressor == nullptr) {
+ return Status::Invalid("Bitmap block compression factory returned a
null compressor.");
+ }
+ int32_t maximum_compressed_size =
compressor->GetMaxCompressedSize(uncompressed_length);
+ if (maximum_compressed_size < 0 ||
+ maximum_compressed_size >
+ std::numeric_limits<int32_t>::max() -
VarLengthIntUtils::kMaxVarIntSize) {
+ return Status::Invalid("Invalid maximum compressed bitmap block
size.");
+ }
+ std::shared_ptr<Bytes> compressed =
+ Bytes::AllocateBytes(maximum_compressed_size +
VarLengthIntUtils::kMaxVarIntSize, pool);
+ PAIMON_ASSIGN_OR_RAISE(int32_t prefix_length,
+ VarLengthIntUtils::EncodeInt(uncompressed_length,
compressed->data()));
+ PAIMON_ASSIGN_OR_RAISE(
+ int32_t compressed_length,
+ compressor->Compress(
+ uncompressed->data(), uncompressed_length, compressed->data() +
prefix_length,
+ maximum_compressed_size + VarLengthIntUtils::kMaxVarIntSize -
prefix_length));
+ if (compressed_length < 0 ||
+ compressed_length > std::numeric_limits<int32_t>::max() -
prefix_length) {
+ return Status::Invalid("Invalid compressed bitmap block size.");
+ }
+ int32_t encoded_length = prefix_length + compressed_length;
+ if (encoded_length < uncompressed_length - (uncompressed_length / 8)) {
+ result.bytes = std::move(compressed);
+ result.length = encoded_length;
+ result.compression_type = compression_factory->GetCompressionType();
+ }
+ return result;
+}
+
+Result<BitmapGlobalIndexFormat::Footer> BitmapGlobalIndexFormat::ReadFooter(
+ int64_t file_size, SeekableReader* reader) {
+ if (reader == nullptr) {
+ return Status::Invalid("Cannot read bitmap footer without a reader.");
+ }
+ if (file_size < kFooterLength) {
+ return Status::Invalid("Invalid bitmap global index file size.");
+ }
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Bytes> bytes,
+ reader->Read(file_size - kFooterLength,
kFooterLength));
+ if (bytes == nullptr || bytes->size() != kFooterLength) {
+ return Status::Invalid("Truncated bitmap global index footer.");
+ }
+ MemorySliceInput input(MemorySlice::Wrap(bytes));
+ input.SetOrder(ByteOrder::PAIMON_BIG_ENDIAN);
+ int64_t null_rows_offset = input.ReadLong();
+ int32_t null_rows_length = input.ReadInt();
+ BlockInfo null_rows_block(null_rows_offset, null_rows_length);
+ int64_t non_null_rows_offset = input.ReadLong();
+ int32_t non_null_rows_length = input.ReadInt();
+ BlockInfo non_null_rows_block(non_null_rows_offset, non_null_rows_length);
+ int64_t index_offset = input.ReadLong();
+ int32_t index_length = input.ReadInt();
+ BlockInfo index_block(index_offset, index_length);
+ int32_t value_count = input.ReadInt();
+ int32_t version = input.ReadInt();
+ int32_t magic = input.ReadInt();
+ if (magic != kMagic) {
+ return Status::Invalid("File is not a bitmap global index file (bad
footer magic).");
+ }
+ if (version != kVersion) {
+ return Status::Invalid(
+ fmt::format("Unsupported bitmap global index file version: {}.",
version));
+ }
+ if (value_count < 0) {
+ return Status::Invalid("Invalid bitmap value count.");
+ }
+ for (const BlockInfo* block : {&null_rows_block, &non_null_rows_block,
&index_block}) {
+ PAIMON_RETURN_NOT_OK(ValidateBlockInfo(*block));
+ if (block->Offset() > file_size - kFooterLength ||
+ block->Length() > file_size - kFooterLength - block->Offset()) {
+ return Status::Invalid("Bitmap footer references a block outside
the file payload.");
+ }
+ }
+ return Footer(std::move(null_rows_block), std::move(non_null_rows_block),
+ std::move(index_block));
+}
+
+Result<std::vector<BitmapGlobalIndexFormat::DictionaryBlockMeta>>
+BitmapGlobalIndexFormat::ReadIndexBlock(const BlockInfo& index_block,
SeekableReader* reader,
+ MemoryPool* pool) {
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Bytes> bytes,
+ ReadCompressibleBlock(index_block, reader, pool));
+ MemorySliceInput input(MemorySlice::Wrap(bytes));
+ PAIMON_ASSIGN_OR_RAISE(int32_t block_count,
VarLengthIntUtils::ReadVarLenInt(&input));
+ if (block_count > input.Available()) {
+ return Status::Invalid("Bitmap dictionary block count exceeds the
encoded block size.");
+ }
+ std::vector<DictionaryBlockMeta> blocks;
+ blocks.reserve(block_count);
+ for (int32_t i = 0; i < block_count; ++i) {
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Bytes> key_bytes,
ReadKey(&input, pool));
+ PAIMON_ASSIGN_OR_RAISE(int64_t offset,
VarLengthIntUtils::ReadVarLenLong(&input));
+ PAIMON_ASSIGN_OR_RAISE(int32_t length,
VarLengthIntUtils::ReadVarLenInt(&input));
+ BlockInfo block(offset, length);
+ PAIMON_RETURN_NOT_OK(ValidateBlockInfo(block));
+ blocks.emplace_back(SerializedKey(std::move(key_bytes)), offset,
length);
+ }
+ if (input.Available() != 0) {
+ return Status::Invalid("Bitmap dictionary block index has trailing
bytes.");
+ }
+ return blocks;
+}
+
+Result<BitmapGlobalIndexFormat::DictionaryBlock>
BitmapGlobalIndexFormat::ReadDictionaryBlock(
+ const DictionaryBlockMeta& block, SeekableReader* reader, MemoryPool*
pool) {
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Bytes> bytes,
+ ReadCompressibleBlock(block, reader, pool));
+ MemorySliceInput input(MemorySlice::Wrap(bytes));
+ PAIMON_ASSIGN_OR_RAISE(int32_t entry_count,
VarLengthIntUtils::ReadVarLenInt(&input));
+ if (entry_count > input.Available()) {
+ return Status::Invalid("Bitmap dictionary entry count exceeds the
encoded block size.");
+ }
+ std::vector<DictionaryEntry> entries;
+ entries.reserve(entry_count);
+ for (int32_t i = 0; i < entry_count; ++i) {
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Bytes> key_bytes,
ReadKey(&input, pool));
+ PAIMON_ASSIGN_OR_RAISE(int64_t bitmap_offset,
VarLengthIntUtils::ReadVarLenLong(&input));
+ PAIMON_ASSIGN_OR_RAISE(int32_t bitmap_length,
VarLengthIntUtils::ReadVarLenInt(&input));
+ BlockInfo bitmap_block(bitmap_offset, bitmap_length);
+ PAIMON_RETURN_NOT_OK(ValidateBlockInfo(bitmap_block));
+ entries.emplace_back(SerializedKey(std::move(key_bytes)),
std::move(bitmap_block));
+ }
+ if (input.Available() != 0) {
+ return Status::Invalid("Bitmap dictionary block has trailing bytes.");
+ }
+ return DictionaryBlock(std::move(entries));
+}
+
+Result<RoaringBitmap64> BitmapGlobalIndexFormat::ReadBitmap(const BlockInfo&
block,
+ SeekableReader*
reader) {
+ if (reader == nullptr) {
+ return Status::Invalid("Cannot read bitmap block without a reader.");
+ }
+ PAIMON_RETURN_NOT_OK(ValidateBlockInfo(block));
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Bytes> bytes, reader->Read(block));
+ RoaringBitmap64 bitmap;
+ PAIMON_RETURN_NOT_OK(bitmap.Deserialize(bytes->data(), bytes->size()));
+ return bitmap;
+}
+
+Result<std::shared_ptr<Bytes>> BitmapGlobalIndexFormat::ReadCompressibleBlock(
+ const BlockInfo& block, SeekableReader* reader, MemoryPool* pool) {
+ if (reader == nullptr || pool == nullptr) {
+ return Status::Invalid("Cannot read compressed bitmap block without
reader and pool.");
+ }
+ PAIMON_RETURN_NOT_OK(ValidateBlockInfo(block));
+ if (block.Length() > std::numeric_limits<int32_t>::max() -
BlockTrailer::ENCODED_LENGTH) {
+ return Status::Invalid("Bitmap block is too large.");
+ }
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<Bytes> block_and_trailer,
+ reader->Read(block.Offset(), block.Length() +
BlockTrailer::ENCODED_LENGTH));
+ if (block_and_trailer == nullptr ||
+ block_and_trailer->size() !=
+ static_cast<size_t>(block.Length() +
BlockTrailer::ENCODED_LENGTH)) {
+ return Status::Invalid("Truncated compressed bitmap index block.");
+ }
+
+ MemorySlice all = MemorySlice::Wrap(block_and_trailer);
+ MemorySlice block_slice = all.Slice(0, block.Length());
+ MemorySlice trailer_slice = all.Slice(block.Length(),
BlockTrailer::ENCODED_LENGTH);
+ MemorySliceInput trailer_input(trailer_slice);
+ std::unique_ptr<BlockTrailer> trailer =
BlockTrailer::ReadBlockTrailer(&trailer_input);
+ PAIMON_ASSIGN_OR_RAISE(BlockCompressionType compression_type,
+ SstFileUtils::From(trailer->CompressionType()));
+ uint32_t crc = CRC32C::calculate(block_slice.Data(), block_slice.Length());
+ auto compression_value =
static_cast<char>(static_cast<int32_t>(compression_type) & 0xFF);
+ crc = CRC32C::calculate(&compression_value, sizeof(compression_value),
crc);
+ if (trailer->Crc32c() != static_cast<int32_t>(crc)) {
+ return Status::Invalid(
+ fmt::format("Expected CRC32C({:#x}) but found CRC32C({:#x}) for
bitmap index block.",
+ static_cast<uint32_t>(trailer->Crc32c()), crc));
+ }
+ if (compression_type == BlockCompressionType::NONE) {
+ return block_slice.CopyBytes(pool);
+ }
+
+ MemorySliceInput compressed_input(block_slice);
+ PAIMON_ASSIGN_OR_RAISE(int32_t uncompressed_length,
+
VarLengthIntUtils::ReadVarLenInt(&compressed_input));
+ std::shared_ptr<Bytes> uncompressed =
Bytes::AllocateBytes(uncompressed_length, pool);
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<BlockCompressionFactory>
compression_factory,
+ BlockCompressionFactory::Create(compression_type));
+ std::shared_ptr<BlockDecompressor> decompressor =
compression_factory->GetDecompressor();
+ if (decompressor == nullptr) {
+ return Status::Invalid("Bitmap block compression factory returned a
null decompressor.");
+ }
+ PAIMON_ASSIGN_OR_RAISE(
+ int32_t actual_length,
+ decompressor->Decompress(block_slice.Data() +
compressed_input.Position(),
+ compressed_input.Available(),
uncompressed->data(),
+ uncompressed_length));
+ if (actual_length != uncompressed_length) {
+ return Status::Invalid(
+ fmt::format("Invalid bitmap block: expected uncompressed size {},
actual size {}.",
+ uncompressed_length, actual_length));
+ }
+ return uncompressed;
+}
+
+Result<int32_t> BitmapGlobalIndexFormat::EstimatedVarLenIntSize(int32_t value)
{
+ if (value < 0) {
+ return Status::Invalid(fmt::format("Invalid negative var length int:
{}.", value));
+ }
+ int32_t size = 1;
+ while ((value & ~0x7F) != 0) {
+ value >>= 7;
+ ++size;
+ }
+ return size;
+}
+
+Result<int32_t> BitmapGlobalIndexFormat::EstimatedVarLenLongSize(int64_t
value) {
+ if (value < 0) {
+ return Status::Invalid(fmt::format("Invalid negative var length long:
{}.", value));
+ }
+ int32_t size = 1;
+ while ((value & ~0x7FLL) != 0) {
+ value >>= 7;
+ ++size;
+ }
+ return size;
+}
+
+Result<int32_t> BitmapGlobalIndexFormat::EstimatedIndexBlockSize(
+ const std::vector<DictionaryBlockMeta>& blocks) {
+ PAIMON_RETURN_NOT_OK(
+ ValidateValueInRange<int32_t>(blocks.size(), "bitmap dictionary block
count"));
+ PAIMON_ASSIGN_OR_RAISE(int32_t count_size,
+
EstimatedVarLenIntSize(static_cast<int32_t>(blocks.size())));
+ int64_t size = count_size;
+ for (const DictionaryBlockMeta& block : blocks) {
+ const std::shared_ptr<Bytes>& key = block.FirstKey().GetBytes();
+ if (key == nullptr) {
+ return Status::Invalid("Bitmap dictionary index key is null.");
+ }
+ PAIMON_RETURN_NOT_OK(
+ ValidateValueInRange<int32_t>(key->size(), "bitmap dictionary
index key length"));
+ PAIMON_ASSIGN_OR_RAISE(int32_t key_length_size,
+
EstimatedVarLenIntSize(static_cast<int32_t>(key->size())));
+ PAIMON_ASSIGN_OR_RAISE(int32_t offset_size,
EstimatedVarLenLongSize(block.Offset()));
+ PAIMON_ASSIGN_OR_RAISE(int32_t length_size,
EstimatedVarLenIntSize(block.Length()));
+ size += key_length_size + key->size() + offset_size + length_size;
+ PAIMON_RETURN_NOT_OK(
+ ValidateValueInRange<int32_t>(size, "bitmap dictionary index block
size"));
+ }
+ return static_cast<int32_t>(size);
+}
+
+} // namespace paimon
diff --git a/src/paimon/common/global_index/bitmap/bitmap_global_index_format.h
b/src/paimon/common/global_index/bitmap/bitmap_global_index_format.h
new file mode 100644
index 00000000..967ce267
--- /dev/null
+++ b/src/paimon/common/global_index/bitmap/bitmap_global_index_format.h
@@ -0,0 +1,303 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "paimon/common/compression/block_compression_factory.h"
+#include "paimon/common/memory/memory_slice.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/memory/bytes.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+#include "paimon/utils/roaring_bitmap64.h"
+
+namespace paimon {
+
+class KeySerializer;
+class Literal;
+
+/// Shared file format helpers for bitmap global index.
+///
+/// Bitmap blocks for non-null keys are written as keys arrive. Dictionary
blocks are flushed
+/// periodically, so these two kinds of blocks may be interleaved. The blocks
at the end of the
+/// file have a fixed order:
+///
+/// +--------------------------------------------------+
+/// | Bitmap Block for key 1 (RoaringBitmap64) |
+/// +--------------------------------------------------+
+/// | ... Bitmap / Dictionary Blocks may interleave ...|
+/// +--------------------------------------------------+
+/// | Dictionary Block N |
+/// +--------------------------------------------------+
+/// | Null Rows Bitmap Block |
+/// +--------------------------------------------------+
+/// | Non-null Rows Bitmap Block |
+/// +--------------------------------------------------+
+/// | Dictionary Block Index |
+/// +--------------------------------------------------+
+/// | Footer (48 bytes) |
+/// +--------------------------------------------------+
+///
+/// Each dictionary entry maps one serialized key to its bitmap block:
+///
+/// Dictionary Block:
+/// [entry count]
+/// [key length | key bytes | bitmap offset | bitmap length] ...
+///
+/// The dictionary block index maps the first key of each dictionary block to
that block:
+///
+/// Dictionary Block Index:
+/// [block count]
+/// [first key length | first key bytes | dictionary offset | dictionary
length] ...
+///
+/// The footer stores the offset and length of the null rows bitmap, non-null
rows bitmap and
+/// dictionary block index, followed by value count, version and magic.
Dictionary blocks and the
+/// dictionary block index use the compressible-block encoding and are
followed by a block trailer;
+/// bitmap blocks contain the serialized RoaringBitmap64 bytes directly.
+class BitmapGlobalIndexFormat {
+ public:
+ BitmapGlobalIndexFormat() = delete;
+ ~BitmapGlobalIndexFormat() = delete;
+
+ class BlockInfo;
+
+ /// Serialized bitmap dictionary key.
+ class SerializedKey {
+ public:
+ explicit SerializedKey(std::shared_ptr<Bytes> bytes) :
bytes_(std::move(bytes)) {}
+
+ static Result<SerializedKey> FromLiteral(const
std::shared_ptr<KeySerializer>& serializer,
+ const Literal& literal);
+
+ const std::shared_ptr<Bytes>& GetBytes() const {
+ return bytes_;
+ }
+
+ int32_t CompareTo(const SerializedKey& other) const;
+
+ bool operator==(const SerializedKey& other) const {
+ return CompareTo(other) == 0;
+ }
+
+ private:
+ std::shared_ptr<Bytes> bytes_;
+ };
+
+ /// Minimal random-access reader used by bitmap index format decoders.
+ class SeekableReader {
+ public:
+ virtual ~SeekableReader() = default;
+
+ virtual Result<std::shared_ptr<Bytes>> Read(int64_t offset, int32_t
length) = 0;
+
+ Result<std::shared_ptr<Bytes>> Read(const BlockInfo& block);
+ };
+
+ /// Encoded block location within a bitmap index file.
+ class BlockInfo {
+ public:
+ BlockInfo(int64_t offset, int32_t length) : offset_(offset),
length_(length) {}
+
+ int64_t Offset() const {
+ return offset_;
+ }
+
+ int32_t Length() const {
+ return length_;
+ }
+
+ private:
+ int64_t offset_;
+ int32_t length_;
+ };
+
+ /// Dictionary block metadata read from the block index.
+ class DictionaryBlockMeta : public BlockInfo {
+ public:
+ DictionaryBlockMeta(SerializedKey first_key, int64_t offset, int32_t
length)
+ : BlockInfo(offset, length), first_key_(std::move(first_key)) {}
+
+ const SerializedKey& FirstKey() const {
+ return first_key_;
+ }
+
+ private:
+ SerializedKey first_key_;
+ };
+
+ /// One encoded dictionary key and its bitmap block.
+ class DictionaryEntry {
+ public:
+ DictionaryEntry(SerializedKey key, BlockInfo bitmap_block)
+ : key_(std::move(key)), bitmap_block_(std::move(bitmap_block)) {}
+
+ const SerializedKey& Key() const {
+ return key_;
+ }
+
+ const BlockInfo& BitmapBlock() const {
+ return bitmap_block_;
+ }
+
+ Result<int32_t> EstimatedSize() const;
+
+ private:
+ SerializedKey key_;
+ BlockInfo bitmap_block_;
+ };
+
+ /// Decoded dictionary block.
+ class DictionaryBlock {
+ public:
+ explicit DictionaryBlock(std::vector<DictionaryEntry> entries)
+ : entries_(std::move(entries)) {}
+
+ const std::vector<DictionaryEntry>& Entries() const {
+ return entries_;
+ }
+
+ private:
+ std::vector<DictionaryEntry> entries_;
+ };
+
+ /// Bitmap index footer block references.
+ class Footer {
+ public:
+ Footer(BlockInfo null_rows_block, BlockInfo non_null_rows_block,
BlockInfo index_block)
+ : null_rows_block_(std::move(null_rows_block)),
+ non_null_rows_block_(std::move(non_null_rows_block)),
+ index_block_(std::move(index_block)) {}
+
+ const BlockInfo& NullRowsBlock() const {
+ return null_rows_block_;
+ }
+
+ const BlockInfo& NonNullRowsBlock() const {
+ return non_null_rows_block_;
+ }
+
+ const BlockInfo& IndexBlock() const {
+ return index_block_;
+ }
+
+ private:
+ BlockInfo null_rows_block_;
+ BlockInfo non_null_rows_block_;
+ BlockInfo index_block_;
+ };
+
+ /// Streaming writer for encoded bitmap dictionary entries.
+ class StreamingWriter {
+ public:
+ static Result<std::unique_ptr<StreamingWriter>> Create(
+ const std::shared_ptr<OutputStream>& output_stream, int32_t
dictionary_block_size,
+ const std::shared_ptr<BlockCompressionFactory>&
compression_factory,
+ const std::shared_ptr<MemoryPool>& pool);
+
+ Status Write(SerializedKey key, const RoaringBitmap64& bitmap);
+
+ Status Finish(const RoaringBitmap64& null_rows, const RoaringBitmap64&
non_null_rows);
+
+ private:
+ StreamingWriter(const std::shared_ptr<OutputStream>& output_stream,
+ int32_t dictionary_block_size,
+ const std::shared_ptr<BlockCompressionFactory>&
compression_factory,
+ const std::shared_ptr<MemoryPool>& pool)
+ : output_stream_(output_stream),
+ dictionary_block_size_(dictionary_block_size),
+ compression_factory_(compression_factory),
+ pool_(pool) {}
+
+ Status FlushDictionaryBlock();
+ Result<int32_t> EstimatedDictionaryBlockSizeAfter(const
DictionaryEntry& entry) const;
+
+ std::shared_ptr<OutputStream> output_stream_;
+ int32_t dictionary_block_size_;
+ std::shared_ptr<BlockCompressionFactory> compression_factory_;
+ std::shared_ptr<MemoryPool> pool_;
+ std::vector<DictionaryBlockMeta> dictionary_block_metas_;
+ std::vector<DictionaryEntry> current_dictionary_entries_;
+ int32_t current_dictionary_entries_size_ = 0;
+ int32_t value_count_ = 0;
+ bool finished_ = false;
+ };
+
+ static Result<Footer> ReadFooter(int64_t file_size, SeekableReader*
reader);
+
+ static Result<std::vector<DictionaryBlockMeta>> ReadIndexBlock(const
BlockInfo& index_block,
+
SeekableReader* reader,
+ MemoryPool*
pool);
+
+ static Result<DictionaryBlock> ReadDictionaryBlock(const
DictionaryBlockMeta& block,
+ SeekableReader* reader,
MemoryPool* pool);
+
+ static Result<RoaringBitmap64> ReadBitmap(const BlockInfo& block,
SeekableReader* reader);
+
+ static constexpr int32_t kMagic = 0x42474958;
+ static constexpr int32_t kVersion = 1;
+ static constexpr int32_t kFooterLength = 48;
+
+ private:
+ friend class StreamingWriter;
+
+ struct BlockEncoding {
+ std::shared_ptr<Bytes> bytes;
+ int32_t length;
+ BlockCompressionType compression_type;
+ };
+
+ static Status WriteFooter(const BlockInfo& null_rows_block,
+ const BlockInfo& non_null_rows_block, const
BlockInfo& index_block,
+ int32_t value_count, OutputStream*
output_stream);
+
+ static Result<BlockInfo> WriteBitmapBlock(const RoaringBitmap64& bitmap,
+ OutputStream* output_stream,
MemoryPool* pool);
+
+ static Result<DictionaryBlockMeta> WriteDictionaryBlock(
+ const std::vector<DictionaryEntry>& entries, BlockCompressionFactory*
compression_factory,
+ OutputStream* output_stream, MemoryPool* pool);
+
+ static Result<BlockInfo> WriteIndexBlock(const
std::vector<DictionaryBlockMeta>& blocks,
+ BlockCompressionFactory*
compression_factory,
+ OutputStream* output_stream,
MemoryPool* pool);
+
+ static Result<BlockInfo> WriteCompressibleBlock(const
std::shared_ptr<Bytes>& uncompressed,
+ BlockCompressionFactory*
compression_factory,
+ OutputStream*
output_stream, MemoryPool* pool);
+
+ static Result<BlockEncoding> EncodeBlock(const std::shared_ptr<Bytes>&
uncompressed,
+ BlockCompressionFactory*
compression_factory,
+ MemoryPool* pool);
+
+ static Result<std::shared_ptr<Bytes>> ReadCompressibleBlock(const
BlockInfo& block,
+
SeekableReader* reader,
+ MemoryPool*
pool);
+
+ static Result<int32_t> EstimatedVarLenIntSize(int32_t value);
+ static Result<int32_t> EstimatedVarLenLongSize(int64_t value);
+ static Result<int32_t> EstimatedIndexBlockSize(const
std::vector<DictionaryBlockMeta>& blocks);
+};
+
+} // namespace paimon
diff --git
a/src/paimon/common/global_index/bitmap/bitmap_global_index_format_test.cpp
b/src/paimon/common/global_index/bitmap/bitmap_global_index_format_test.cpp
new file mode 100644
index 00000000..2ad25cf0
--- /dev/null
+++ b/src/paimon/common/global_index/bitmap/bitmap_global_index_format_test.cpp
@@ -0,0 +1,143 @@
+/*
+ * 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 "paimon/common/global_index/bitmap/bitmap_global_index_format.h"
+
+#include <cstring>
+#include <memory>
+#include <string>
+
+#include "gtest/gtest.h"
+#include "paimon/common/io/byte_array_output_stream.h"
+#include "paimon/common/io/memory_segment_output_stream.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+namespace {
+
+class BytesSeekableReader : public BitmapGlobalIndexFormat::SeekableReader {
+ public:
+ BytesSeekableReader(std::shared_ptr<Bytes> bytes,
std::shared_ptr<MemoryPool> pool)
+ : bytes_(std::move(bytes)), pool_(std::move(pool)) {}
+
+ Result<std::shared_ptr<Bytes>> Read(int64_t offset, int32_t length)
override {
+ if (offset < 0 || length < 0 || offset >
static_cast<int64_t>(bytes_->size()) ||
+ length > static_cast<int64_t>(bytes_->size()) - offset) {
+ return Status::Invalid("Read exceeds in-memory bitmap index
data.");
+ }
+ std::shared_ptr<Bytes> result = Bytes::AllocateBytes(length,
pool_.get());
+ std::memcpy(result->data(), bytes_->data() + offset, length);
+ return result;
+ }
+
+ private:
+ std::shared_ptr<Bytes> bytes_;
+ std::shared_ptr<MemoryPool> pool_;
+};
+
+BitmapGlobalIndexFormat::SerializedKey SerializedString(const std::string&
value,
+ MemoryPool* pool) {
+ return BitmapGlobalIndexFormat::SerializedKey(Bytes::AllocateBytes(value,
pool));
+}
+
+std::shared_ptr<ByteArrayOutputStream> CreateOutput(const
std::shared_ptr<MemoryPool>& pool) {
+ std::unique_ptr<MemorySegmentOutputStream> segmented =
+ std::make_unique<MemorySegmentOutputStream>(/*segment_size=*/16, pool);
+ return std::make_shared<ByteArrayOutputStream>(std::move(segmented));
+}
+
+} // namespace
+
+TEST(BitmapGlobalIndexFormatTest, RoundTripMultipleDictionaryBlocks) {
+ std::shared_ptr<MemoryPool> pool = GetDefaultPool();
+ std::shared_ptr<ByteArrayOutputStream> output = CreateOutput(pool);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<BlockCompressionFactory>
compression_factory,
+
BlockCompressionFactory::Create(BlockCompressionType::NONE));
+
ASSERT_OK_AND_ASSIGN(std::unique_ptr<BitmapGlobalIndexFormat::StreamingWriter>
writer,
+ BitmapGlobalIndexFormat::StreamingWriter::Create(
+ output, /*dictionary_block_size=*/1,
compression_factory, pool));
+
+ ASSERT_OK(writer->Write(SerializedString("apple", pool.get()),
RoaringBitmap64::From({0, 2})));
+ ASSERT_OK(writer->Write(SerializedString("banana", pool.get()),
RoaringBitmap64::From({3})));
+ ASSERT_OK(writer->Finish(RoaringBitmap64::From({1}),
RoaringBitmap64::From({0, 2, 3})));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Bytes> bytes,
output->Finish(pool.get()));
+
+ BytesSeekableReader reader(bytes, pool);
+ ASSERT_OK_AND_ASSIGN(BitmapGlobalIndexFormat::Footer footer,
+ BitmapGlobalIndexFormat::ReadFooter(bytes->size(),
&reader));
+ ASSERT_OK_AND_ASSIGN(
+ std::vector<BitmapGlobalIndexFormat::DictionaryBlockMeta> block_metas,
+ BitmapGlobalIndexFormat::ReadIndexBlock(footer.IndexBlock(), &reader,
pool.get()));
+ ASSERT_EQ(2, block_metas.size());
+
+ ASSERT_OK_AND_ASSIGN(
+ BitmapGlobalIndexFormat::DictionaryBlock first_block,
+ BitmapGlobalIndexFormat::ReadDictionaryBlock(block_metas.front(),
&reader, pool.get()));
+ ASSERT_EQ(1, first_block.Entries().size());
+ ASSERT_EQ("apple",
std::string(first_block.Entries()[0].Key().GetBytes()->data(),
+
first_block.Entries()[0].Key().GetBytes()->size()));
+ ASSERT_OK_AND_ASSIGN(
+ RoaringBitmap64 first_bitmap,
+
BitmapGlobalIndexFormat::ReadBitmap(first_block.Entries()[0].BitmapBlock(),
&reader));
+ ASSERT_EQ(RoaringBitmap64::From({0, 2}), first_bitmap);
+
+ ASSERT_OK_AND_ASSIGN(
+ BitmapGlobalIndexFormat::DictionaryBlock second_block,
+ BitmapGlobalIndexFormat::ReadDictionaryBlock(block_metas.back(),
&reader, pool.get()));
+ ASSERT_EQ(1, second_block.Entries().size());
+ ASSERT_EQ("banana",
std::string(second_block.Entries()[0].Key().GetBytes()->data(),
+
second_block.Entries()[0].Key().GetBytes()->size()));
+ ASSERT_OK_AND_ASSIGN(
+ RoaringBitmap64 second_bitmap,
+
BitmapGlobalIndexFormat::ReadBitmap(second_block.Entries()[0].BitmapBlock(),
&reader));
+ ASSERT_EQ(RoaringBitmap64::From({3}), second_bitmap);
+
+ ASSERT_OK_AND_ASSIGN(RoaringBitmap64 null_rows,
+
BitmapGlobalIndexFormat::ReadBitmap(footer.NullRowsBlock(), &reader));
+ ASSERT_EQ(RoaringBitmap64::From({1}), null_rows);
+ ASSERT_OK_AND_ASSIGN(RoaringBitmap64 non_null_rows,
+
BitmapGlobalIndexFormat::ReadBitmap(footer.NonNullRowsBlock(), &reader));
+ ASSERT_EQ(RoaringBitmap64::From({0, 2, 3}), non_null_rows);
+}
+
+TEST(BitmapGlobalIndexFormatTest, RoundTripEmptyNullBitmap) {
+ std::shared_ptr<MemoryPool> pool = GetDefaultPool();
+ std::shared_ptr<ByteArrayOutputStream> output = CreateOutput(pool);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<BlockCompressionFactory>
compression_factory,
+
BlockCompressionFactory::Create(BlockCompressionType::NONE));
+
ASSERT_OK_AND_ASSIGN(std::unique_ptr<BitmapGlobalIndexFormat::StreamingWriter>
writer,
+ BitmapGlobalIndexFormat::StreamingWriter::Create(
+ output, /*dictionary_block_size=*/4096,
compression_factory, pool));
+
+ ASSERT_OK(writer->Write(SerializedString("apple", pool.get()),
RoaringBitmap64::From({0})));
+ ASSERT_OK(writer->Finish(RoaringBitmap64(), RoaringBitmap64::From({0})));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Bytes> bytes,
output->Finish(pool.get()));
+
+ BytesSeekableReader reader(bytes, pool);
+ ASSERT_OK_AND_ASSIGN(BitmapGlobalIndexFormat::Footer footer,
+ BitmapGlobalIndexFormat::ReadFooter(bytes->size(),
&reader));
+ ASSERT_OK_AND_ASSIGN(RoaringBitmap64 null_rows,
+
BitmapGlobalIndexFormat::ReadBitmap(footer.NullRowsBlock(), &reader));
+ ASSERT_TRUE(null_rows.IsEmpty());
+ ASSERT_OK_AND_ASSIGN(RoaringBitmap64 non_null_rows,
+
BitmapGlobalIndexFormat::ReadBitmap(footer.NonNullRowsBlock(), &reader));
+ ASSERT_EQ(RoaringBitmap64::From({0}), non_null_rows);
+}
+
+} // namespace paimon::test
diff --git
a/src/paimon/common/global_index/bitmap/bitmap_global_index_writer.cpp
b/src/paimon/common/global_index/bitmap/bitmap_global_index_writer.cpp
new file mode 100644
index 00000000..f7f5865d
--- /dev/null
+++ b/src/paimon/common/global_index/bitmap/bitmap_global_index_writer.cpp
@@ -0,0 +1,182 @@
+/*
+ * 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 "paimon/common/global_index/bitmap/bitmap_global_index_writer.h"
+
+#include <limits>
+#include <utility>
+
+#include "arrow/c/bridge.h"
+#include "fmt/format.h"
+#include "paimon/common/global_index/global_index_utils.h"
+#include "paimon/common/global_index/key_serializer.h"
+#include "paimon/common/global_index/sorted_index_file_meta.h"
+#include "paimon/common/predicate/literal_converter.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/checked_cast.h"
+#include "paimon/common/utils/preconditions.h"
+
+namespace paimon {
+namespace {
+
+constexpr char kBitmapIdentifier[] = "bitmap";
+
+} // namespace
+
+Result<std::shared_ptr<BitmapGlobalIndexWriter>>
BitmapGlobalIndexWriter::Create(
+ const std::string& field_name, const std::shared_ptr<arrow::StructType>&
arrow_type,
+ const std::shared_ptr<GlobalIndexFileWriter>& file_writer, int32_t
dictionary_block_size,
+ const std::shared_ptr<BlockCompressionFactory>& compression_factory,
+ const std::shared_ptr<MemoryPool>& pool) {
+ if (arrow_type == nullptr || file_writer == nullptr || pool == nullptr) {
+ return Status::Invalid(
+ "Cannot create BitmapGlobalIndexWriter without schema, file
writer, and memory pool.");
+ }
+ if (dictionary_block_size <= 0) {
+ return Status::Invalid("Bitmap dictionary block size must be greater
than 0.");
+ }
+ std::shared_ptr<arrow::Field> key_field =
arrow_type->GetFieldByName(field_name);
+ PAIMON_RETURN_NOT_OK(Preconditions::CheckNotNull(
+ key_field, fmt::format("field {} not in arrow_array when Create
BitmapGlobalIndexWriter",
+ field_name)));
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<KeySerializer> key_serializer,
+ KeySerializer::Create(key_field->type(), pool));
+ return std::shared_ptr<BitmapGlobalIndexWriter>(
+ new BitmapGlobalIndexWriter(field_name, arrow_type,
std::move(key_serializer), file_writer,
+ dictionary_block_size,
compression_factory, pool));
+}
+
+Status BitmapGlobalIndexWriter::AddBatch(::ArrowArray* arrow_array,
+ std::vector<int64_t>&&
relative_row_ids) {
+ if (finished_) {
+ return Status::Invalid("Cannot add a batch to a finished
BitmapGlobalIndexWriter.");
+ }
+ PAIMON_RETURN_NOT_OK(GlobalIndexUtils::CheckRelativeRowIds(
+ arrow_array, relative_row_ids, /*expected_next_row_id=*/std::nullopt));
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
+ arrow::ImportArray(arrow_array,
arrow_type_));
+ if (array == nullptr || array->type_id() != arrow::Type::STRUCT) {
+ return Status::Invalid(
+ "arrow array must be struct array when AddBatch to
BitmapGlobalIndexWriter");
+ }
+ std::shared_ptr<arrow::StructArray> struct_array =
+ checked_pointer_cast<arrow::StructArray>(array);
+ std::shared_ptr<arrow::Array> value_array =
struct_array->GetFieldByName(field_name_);
+ PAIMON_RETURN_NOT_OK(Preconditions::CheckNotNull(
+ value_array,
+ fmt::format("field {} not in arrow_array when AddBatch to
BitmapGlobalIndexWriter",
+ field_name_)));
+ PAIMON_ASSIGN_OR_RAISE(std::vector<Literal> literals,
+
LiteralConverter::ConvertLiteralsFromArray(*value_array,
+
/*own_data=*/true));
+ for (size_t i = 0; i < literals.size(); ++i) {
+ if (row_count_ == std::numeric_limits<int64_t>::max()) {
+ return Status::Invalid("Bitmap global index row count exceeds
INT64_MAX.");
+ }
+ ++row_count_;
+ int64_t row_id = relative_row_ids[i];
+ const Literal& literal = literals[i];
+ if (literal.IsNull()) {
+ null_rows_.Add(row_id);
+ continue;
+ }
+
+ non_null_rows_.Add(row_id);
+ if (last_key_.has_value()) {
+ PAIMON_ASSIGN_OR_RAISE(int32_t comparison,
literal.CompareTo(last_key_.value()));
+ if (comparison < 0) {
+ return Status::Invalid(
+ "Bitmap index keys must be written in monotonically
increasing order.");
+ }
+ if (comparison > 0) {
+ PAIMON_RETURN_NOT_OK(FlushCurrentBitmap());
+ }
+ }
+ if (!first_key_.has_value()) {
+ first_key_ = literal;
+ }
+ last_key_ = literal;
+ current_bitmap_.Add(row_id);
+ }
+ return Status::OK();
+}
+
+Result<std::vector<GlobalIndexIOMeta>> BitmapGlobalIndexWriter::Finish() {
+ if (finished_) {
+ return Status::Invalid("BitmapGlobalIndexWriter has already been
finished.");
+ }
+ finished_ = true;
+ if (row_count_ == 0) {
+ return std::vector<GlobalIndexIOMeta>();
+ }
+
+ PAIMON_RETURN_NOT_OK(FlushCurrentBitmap());
+ PAIMON_ASSIGN_OR_RAISE(BitmapGlobalIndexFormat::StreamingWriter *
streaming_writer,
+ GetOrCreateStreamingWriter());
+ PAIMON_RETURN_NOT_OK(streaming_writer->Finish(null_rows_, non_null_rows_));
+ PAIMON_RETURN_NOT_OK(output_stream_->Close());
+
+ std::shared_ptr<Bytes> first_key_bytes;
+ std::shared_ptr<Bytes> last_key_bytes;
+ if (first_key_.has_value()) {
+ PAIMON_ASSIGN_OR_RAISE(first_key_bytes,
key_serializer_->Serialize(first_key_.value()));
+ }
+ if (last_key_.has_value()) {
+ PAIMON_ASSIGN_OR_RAISE(last_key_bytes,
key_serializer_->Serialize(last_key_.value()));
+ }
+ SortedIndexFileMeta index_meta(first_key_bytes, last_key_bytes,
!null_rows_.IsEmpty());
+ std::shared_ptr<Bytes> metadata = index_meta.Serialize(pool_.get());
+ PAIMON_ASSIGN_OR_RAISE(int64_t file_size,
file_writer_->GetFileSize(file_name_));
+ return std::vector<GlobalIndexIOMeta>{
+ GlobalIndexIOMeta(file_writer_->ToPath(file_name_), file_size,
std::move(metadata))};
+}
+
+Status BitmapGlobalIndexWriter::FlushCurrentBitmap() {
+ if (current_bitmap_.IsEmpty()) {
+ return Status::OK();
+ }
+ if (!last_key_.has_value()) {
+ return Status::Invalid("BitmapGlobalIndexWriter has a bitmap without a
dictionary key.");
+ }
+ PAIMON_ASSIGN_OR_RAISE(BitmapGlobalIndexFormat::StreamingWriter *
streaming_writer,
+ GetOrCreateStreamingWriter());
+ PAIMON_ASSIGN_OR_RAISE(
+ BitmapGlobalIndexFormat::SerializedKey key,
+ BitmapGlobalIndexFormat::SerializedKey::FromLiteral(key_serializer_,
last_key_.value()));
+ PAIMON_RETURN_NOT_OK(streaming_writer->Write(std::move(key),
current_bitmap_));
+ current_bitmap_ = RoaringBitmap64();
+ return Status::OK();
+}
+
+Result<BitmapGlobalIndexFormat::StreamingWriter*>
+BitmapGlobalIndexWriter::GetOrCreateStreamingWriter() {
+ if (streaming_writer_ != nullptr) {
+ return streaming_writer_.get();
+ }
+ PAIMON_ASSIGN_OR_RAISE(file_name_,
file_writer_->NewFileName(kBitmapIdentifier));
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<OutputStream> output_stream,
+ file_writer_->NewOutputStream(file_name_));
+ output_stream_ = std::move(output_stream);
+ PAIMON_ASSIGN_OR_RAISE(streaming_writer_,
BitmapGlobalIndexFormat::StreamingWriter::Create(
+ output_stream_,
dictionary_block_size_,
+ compression_factory_,
pool_));
+ return streaming_writer_.get();
+}
+
+} // namespace paimon
diff --git a/src/paimon/common/global_index/bitmap/bitmap_global_index_writer.h
b/src/paimon/common/global_index/bitmap/bitmap_global_index_writer.h
new file mode 100644
index 00000000..3f7e8a2b
--- /dev/null
+++ b/src/paimon/common/global_index/bitmap/bitmap_global_index_writer.h
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "arrow/api.h"
+#include "paimon/common/compression/block_compression_factory.h"
+#include "paimon/common/global_index/bitmap/bitmap_global_index_format.h"
+#include "paimon/global_index/global_index_writer.h"
+#include "paimon/global_index/io/global_index_file_writer.h"
+#include "paimon/predicate/literal.h"
+#include "paimon/utils/roaring_bitmap64.h"
+
+namespace paimon {
+
+class KeySerializer;
+
+/// Streaming global index writer for the Java-compatible bitmap format.
+///
+/// Non-null keys must be written in monotonically increasing order so
completed bitmaps can be
+/// streamed to the output file instead of retained until Finish().
+class BitmapGlobalIndexWriter : public GlobalIndexWriter {
+ public:
+ static Result<std::shared_ptr<BitmapGlobalIndexWriter>> Create(
+ const std::string& field_name, const
std::shared_ptr<arrow::StructType>& arrow_type,
+ const std::shared_ptr<GlobalIndexFileWriter>& file_writer, int32_t
dictionary_block_size,
+ const std::shared_ptr<BlockCompressionFactory>& compression_factory,
+ const std::shared_ptr<MemoryPool>& pool);
+
+ ~BitmapGlobalIndexWriter() override = default;
+
+ Status AddBatch(::ArrowArray* arrow_array, std::vector<int64_t>&&
relative_row_ids) override;
+
+ Result<std::vector<GlobalIndexIOMeta>> Finish() override;
+
+ private:
+ BitmapGlobalIndexWriter(std::string field_name,
std::shared_ptr<arrow::DataType> arrow_type,
+ std::shared_ptr<KeySerializer> key_serializer,
+ std::shared_ptr<GlobalIndexFileWriter> file_writer,
+ int32_t dictionary_block_size,
+ std::shared_ptr<BlockCompressionFactory>
compression_factory,
+ std::shared_ptr<MemoryPool> pool)
+ : field_name_(std::move(field_name)),
+ arrow_type_(std::move(arrow_type)),
+ key_serializer_(std::move(key_serializer)),
+ file_writer_(std::move(file_writer)),
+ dictionary_block_size_(dictionary_block_size),
+ compression_factory_(std::move(compression_factory)),
+ pool_(std::move(pool)) {}
+
+ Status FlushCurrentBitmap();
+
+ Result<BitmapGlobalIndexFormat::StreamingWriter*>
GetOrCreateStreamingWriter();
+
+ std::string field_name_;
+ std::shared_ptr<arrow::DataType> arrow_type_;
+ std::shared_ptr<KeySerializer> key_serializer_;
+ std::shared_ptr<GlobalIndexFileWriter> file_writer_;
+ int32_t dictionary_block_size_;
+ std::shared_ptr<BlockCompressionFactory> compression_factory_;
+ std::shared_ptr<MemoryPool> pool_;
+
+ std::string file_name_;
+ std::shared_ptr<OutputStream> output_stream_;
+ std::unique_ptr<BitmapGlobalIndexFormat::StreamingWriter>
streaming_writer_;
+ int64_t row_count_ = 0;
+ std::optional<Literal> first_key_;
+ std::optional<Literal> last_key_;
+ RoaringBitmap64 current_bitmap_;
+ RoaringBitmap64 null_rows_;
+ RoaringBitmap64 non_null_rows_;
+ bool finished_ = false;
+};
+
+} // namespace paimon
diff --git a/src/paimon/common/global_index/bitmap/bitmap_index_reader.cpp
b/src/paimon/common/global_index/bitmap/bitmap_index_reader.cpp
new file mode 100644
index 00000000..8a38a67a
--- /dev/null
+++ b/src/paimon/common/global_index/bitmap/bitmap_index_reader.cpp
@@ -0,0 +1,579 @@
+/*
+ * 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 "paimon/common/global_index/bitmap/bitmap_index_reader.h"
+
+#include <algorithm>
+#include <cstring>
+#include <set>
+#include <string_view>
+#include <utility>
+
+#include "fmt/format.h"
+#include "paimon/common/global_index/key_serializer.h"
+#include "paimon/common/predicate/like.h"
+#include "paimon/global_index/bitmap_global_index_result.h"
+#include "paimon/predicate/literal.h"
+
+namespace paimon {
+
+Result<std::shared_ptr<BitmapIndexReader>> BitmapIndexReader::Create(
+ const std::shared_ptr<KeySerializer>& key_serializer,
+ const std::shared_ptr<GlobalIndexFileReader>& file_reader, const
GlobalIndexIOMeta& meta,
+ const std::shared_ptr<MemoryPool>& pool) {
+ if (key_serializer == nullptr || file_reader == nullptr || pool ==
nullptr) {
+ return Status::Invalid(
+ "Cannot create BitmapIndexReader without serializer, file reader,
and memory pool.");
+ }
+ if (meta.file_size < 0) {
+ return Status::Invalid("Cannot create BitmapIndexReader with a
negative file size.");
+ }
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<InputStream> input,
+ file_reader->GetInputStream(meta.file_path));
+ std::shared_ptr<BitmapIndexReader> reader(
+ new BitmapIndexReader(key_serializer, std::move(input),
meta.file_size, pool));
+ Result<BitmapGlobalIndexFormat::Footer> footer =
+ BitmapGlobalIndexFormat::ReadFooter(meta.file_size, reader.get());
+ if (!footer.ok()) {
+ [[maybe_unused]] Status close_status = reader->Close();
+ return footer.status();
+ }
+ reader->footer_ = std::move(footer).value();
+ return reader;
+}
+
+BitmapIndexReader::~BitmapIndexReader() {
+ [[maybe_unused]] Status close_status = Close();
+}
+
+std::shared_ptr<GlobalIndexResult> BitmapIndexReader::CreateResult(
+ std::function<Result<RoaringBitmap64>()> supplier) {
+ return std::make_shared<BitmapGlobalIndexResult>(std::move(supplier));
+}
+
+Result<std::shared_ptr<GlobalIndexResult>> BitmapIndexReader::VisitIsNotNull()
{
+ return CreateResult([reader = shared_from_this()]() { return
reader->IsNotNull(); });
+}
+
+Result<std::shared_ptr<GlobalIndexResult>> BitmapIndexReader::VisitIsNull() {
+ return CreateResult([reader = shared_from_this()]() { return
reader->IsNull(); });
+}
+
+Result<std::shared_ptr<GlobalIndexResult>> BitmapIndexReader::VisitEqual(const
Literal& literal) {
+ return CreateResult(
+ [reader = shared_from_this(), literal]() { return
reader->Equal(literal); });
+}
+
+Result<std::shared_ptr<GlobalIndexResult>> BitmapIndexReader::VisitNotEqual(
+ const Literal& literal) {
+ return CreateResult(
+ [reader = shared_from_this(), literal]() { return
reader->NotEqual(literal); });
+}
+
+Result<std::shared_ptr<GlobalIndexResult>> BitmapIndexReader::VisitLessThan(
+ const Literal& literal) {
+ return CreateResult(
+ [reader = shared_from_this(), literal]() { return
reader->LessThan(literal); });
+}
+
+Result<std::shared_ptr<GlobalIndexResult>> BitmapIndexReader::VisitLessOrEqual(
+ const Literal& literal) {
+ return CreateResult(
+ [reader = shared_from_this(), literal]() { return
reader->LessOrEqual(literal); });
+}
+
+Result<std::shared_ptr<GlobalIndexResult>> BitmapIndexReader::VisitGreaterThan(
+ const Literal& literal) {
+ return CreateResult(
+ [reader = shared_from_this(), literal]() { return
reader->GreaterThan(literal); });
+}
+
+Result<std::shared_ptr<GlobalIndexResult>>
BitmapIndexReader::VisitGreaterOrEqual(
+ const Literal& literal) {
+ return CreateResult(
+ [reader = shared_from_this(), literal]() { return
reader->GreaterOrEqual(literal); });
+}
+
+Result<std::shared_ptr<GlobalIndexResult>> BitmapIndexReader::VisitIn(
+ const std::vector<Literal>& literals) {
+ return CreateResult([reader = shared_from_this(), literals]() { return
reader->In(literals); });
+}
+
+Result<std::shared_ptr<GlobalIndexResult>> BitmapIndexReader::VisitNotIn(
+ const std::vector<Literal>& literals) {
+ return CreateResult(
+ [reader = shared_from_this(), literals]() { return
reader->NotIn(literals); });
+}
+
+Result<std::shared_ptr<GlobalIndexResult>> BitmapIndexReader::VisitStartsWith(
+ const Literal& prefix) {
+ return CreateResult(
+ [reader = shared_from_this(), prefix]() { return
reader->StartsWith(prefix); });
+}
+
+Result<std::shared_ptr<GlobalIndexResult>>
BitmapIndexReader::VisitEndsWith(const Literal& suffix) {
+ return CreateResult(
+ [reader = shared_from_this(), suffix]() { return
reader->EndsWith(suffix); });
+}
+
+Result<std::shared_ptr<GlobalIndexResult>> BitmapIndexReader::VisitContains(
+ const Literal& literal) {
+ return CreateResult(
+ [reader = shared_from_this(), literal]() { return
reader->Contains(literal); });
+}
+
+Result<std::shared_ptr<GlobalIndexResult>> BitmapIndexReader::VisitLike(const
Literal& literal) {
+ return CreateResult([reader = shared_from_this(), literal]() { return
reader->Like(literal); });
+}
+
+Result<std::shared_ptr<ScoredGlobalIndexResult>>
BitmapIndexReader::VisitVectorSearch(
+ const std::shared_ptr<VectorSearch>& vector_search) {
+ return Status::Invalid("Vector search is not supported in
BitmapIndexReader.");
+}
+
+Result<std::shared_ptr<GlobalIndexResult>>
BitmapIndexReader::VisitFullTextSearch(
+ const std::shared_ptr<FullTextSearch>& full_text_search) {
+ return Status::Invalid("Full text search is not supported in
BitmapIndexReader.");
+}
+
+Result<std::shared_ptr<Bytes>> BitmapIndexReader::Read(int64_t offset, int32_t
length) {
+ if (closed_ || input_ == nullptr) {
+ return Status::Invalid("Cannot read from a closed BitmapIndexReader.");
+ }
+ if (offset < 0 || length < 0 || offset > file_size_ || length > file_size_
- offset) {
+ return Status::Invalid(fmt::format("Bitmap index read range [{}, {})
exceeds file size {}.",
+ offset, offset + length,
file_size_));
+ }
+ std::shared_ptr<Bytes> bytes = Bytes::AllocateBytes(length, pool_.get());
+ if (length == 0) {
+ return bytes;
+ }
+ PAIMON_ASSIGN_OR_RAISE(int64_t actual_length, input_->Read(bytes->data(),
length, offset));
+ if (actual_length != length) {
+ return Status::IOError(
+ fmt::format("Truncated bitmap index read: expected {} bytes at {},
but read {}.",
+ length, offset, actual_length));
+ }
+ return bytes;
+}
+
+Status BitmapIndexReader::Close() {
+ if (closed_) {
+ return Status::OK();
+ }
+ closed_ = true;
+ if (input_ == nullptr) {
+ return Status::OK();
+ }
+ return input_->Close();
+}
+
+Result<RoaringBitmap64> BitmapIndexReader::IsNull() {
+ PAIMON_ASSIGN_OR_RAISE(const RoaringBitmap64* bitmap, GetNullRows());
+ return *bitmap;
+}
+
+Result<RoaringBitmap64> BitmapIndexReader::IsNotNull() {
+ PAIMON_ASSIGN_OR_RAISE(const RoaringBitmap64* bitmap, GetNonNullRows());
+ return *bitmap;
+}
+
+Result<RoaringBitmap64> BitmapIndexReader::Equal(const Literal& literal) {
+ if (literal.IsNull()) {
+ return RoaringBitmap64();
+ }
+ PAIMON_ASSIGN_OR_RAISE(std::optional<BitmapGlobalIndexFormat::BlockInfo>
bitmap_block,
+ FindBitmapBlock(literal));
+ if (!bitmap_block.has_value()) {
+ return RoaringBitmap64();
+ }
+ return BitmapGlobalIndexFormat::ReadBitmap(bitmap_block.value(), this);
+}
+
+Result<RoaringBitmap64> BitmapIndexReader::In(const std::vector<Literal>&
literals) {
+ RoaringBitmap64 result;
+ std::set<std::string> serialized_keys;
+ for (const Literal& literal : literals) {
+ if (literal.IsNull()) {
+ continue;
+ }
+ PAIMON_ASSIGN_OR_RAISE(
+ BitmapGlobalIndexFormat::SerializedKey serialized_key,
+
BitmapGlobalIndexFormat::SerializedKey::FromLiteral(key_serializer_, literal));
+ const std::shared_ptr<Bytes>& key_bytes = serialized_key.GetBytes();
+ if (!serialized_keys.emplace(key_bytes->data(),
key_bytes->size()).second) {
+ continue;
+ }
+ PAIMON_ASSIGN_OR_RAISE(RoaringBitmap64 bitmap, Equal(literal));
+ result |= bitmap;
+ }
+ return result;
+}
+
+Result<RoaringBitmap64> BitmapIndexReader::StartsWith(const Literal& literal) {
+ if (literal.IsNull()) {
+ return RoaringBitmap64();
+ }
+ if (literal.GetType() != FieldType::STRING) {
+ return Status::Invalid("StartsWith requires a string literal in
BitmapIndexReader.");
+ }
+ PAIMON_ASSIGN_OR_RAISE(
+ BitmapGlobalIndexFormat::SerializedKey prefix,
+ BitmapGlobalIndexFormat::SerializedKey::FromLiteral(key_serializer_,
literal));
+ if (prefix.GetBytes()->size() == 0) {
+ return IsNotNull();
+ }
+ std::optional<BitmapGlobalIndexFormat::SerializedKey> upper_bound =
+ PrefixUpperBound(prefix, pool_.get());
+ PAIMON_ASSIGN_OR_RAISE(const
std::vector<BitmapGlobalIndexFormat::DictionaryBlockMeta>* blocks,
+ GetDictionaryBlocks());
+ int32_t index = std::max(FindSerializedDictionaryBlockIndex(*blocks,
prefix), 0);
+ RoaringBitmap64 result;
+ while (index < static_cast<int32_t>(blocks->size())) {
+ const BitmapGlobalIndexFormat::DictionaryBlockMeta& block_meta =
(*blocks)[index];
+ if (upper_bound.has_value() &&
block_meta.FirstKey().CompareTo(upper_bound.value()) >= 0) {
+ return result;
+ }
+ PAIMON_ASSIGN_OR_RAISE(const BitmapGlobalIndexFormat::DictionaryBlock*
block,
+ GetDictionaryBlock(block_meta));
+ for (const BitmapGlobalIndexFormat::DictionaryEntry& entry :
block->Entries()) {
+ if (entry.Key().CompareTo(prefix) < 0) {
+ continue;
+ }
+ if (!StartsWith(entry.Key(), prefix)) {
+ return result;
+ }
+ PAIMON_ASSIGN_OR_RAISE(RoaringBitmap64 bitmap,
+
BitmapGlobalIndexFormat::ReadBitmap(entry.BitmapBlock(), this));
+ result |= bitmap;
+ }
+ ++index;
+ }
+ return result;
+}
+
+Result<RoaringBitmap64> BitmapIndexReader::EndsWith(const Literal& literal) {
+ if (literal.IsNull()) {
+ return RoaringBitmap64();
+ }
+ if (literal.GetType() != FieldType::STRING) {
+ return Status::Invalid("EndsWith requires a string literal in
BitmapIndexReader.");
+ }
+ PAIMON_ASSIGN_OR_RAISE(
+ BitmapGlobalIndexFormat::SerializedKey suffix,
+ BitmapGlobalIndexFormat::SerializedKey::FromLiteral(key_serializer_,
literal));
+ if (suffix.GetBytes()->size() == 0) {
+ return IsNotNull();
+ }
+ return ScanSerializedDictionary(
+ [&suffix](const BitmapGlobalIndexFormat::SerializedKey& key) ->
Result<bool> {
+ return EndsWith(key, suffix);
+ });
+}
+
+Result<RoaringBitmap64> BitmapIndexReader::Contains(const Literal& literal) {
+ if (literal.IsNull()) {
+ return RoaringBitmap64();
+ }
+ if (literal.GetType() != FieldType::STRING) {
+ return Status::Invalid("Contains requires a string literal in
BitmapIndexReader.");
+ }
+ PAIMON_ASSIGN_OR_RAISE(
+ BitmapGlobalIndexFormat::SerializedKey infix,
+ BitmapGlobalIndexFormat::SerializedKey::FromLiteral(key_serializer_,
literal));
+ if (infix.GetBytes()->size() == 0) {
+ return IsNotNull();
+ }
+ return ScanSerializedDictionary(
+ [&infix](const BitmapGlobalIndexFormat::SerializedKey& key) ->
Result<bool> {
+ return Contains(key, infix);
+ });
+}
+
+// TODO(xinyu.lxy): Optimize range predicates by locating the dictionary
lower/upper bound and
+// scanning only the matching side instead of traversing the entire dictionary.
+Result<RoaringBitmap64> BitmapIndexReader::LessThan(const Literal& literal) {
+ if (literal.IsNull()) {
+ return RoaringBitmap64();
+ }
+ return ScanDictionary([&literal](const Literal& key) -> Result<bool> {
+ PAIMON_ASSIGN_OR_RAISE(int32_t comparison, key.CompareTo(literal));
+ return comparison < 0;
+ });
+}
+
+Result<RoaringBitmap64> BitmapIndexReader::LessOrEqual(const Literal& literal)
{
+ if (literal.IsNull()) {
+ return RoaringBitmap64();
+ }
+ return ScanDictionary([&literal](const Literal& key) -> Result<bool> {
+ PAIMON_ASSIGN_OR_RAISE(int32_t comparison, key.CompareTo(literal));
+ return comparison <= 0;
+ });
+}
+
+Result<RoaringBitmap64> BitmapIndexReader::GreaterThan(const Literal& literal)
{
+ if (literal.IsNull()) {
+ return RoaringBitmap64();
+ }
+ return ScanDictionary([&literal](const Literal& key) -> Result<bool> {
+ PAIMON_ASSIGN_OR_RAISE(int32_t comparison, key.CompareTo(literal));
+ return comparison > 0;
+ });
+}
+
+Result<RoaringBitmap64> BitmapIndexReader::GreaterOrEqual(const Literal&
literal) {
+ if (literal.IsNull()) {
+ return RoaringBitmap64();
+ }
+ return ScanDictionary([&literal](const Literal& key) -> Result<bool> {
+ PAIMON_ASSIGN_OR_RAISE(int32_t comparison, key.CompareTo(literal));
+ return comparison >= 0;
+ });
+}
+
+Result<RoaringBitmap64> BitmapIndexReader::NotEqual(const Literal& literal) {
+ if (literal.IsNull()) {
+ return RoaringBitmap64();
+ }
+ PAIMON_ASSIGN_OR_RAISE(RoaringBitmap64 result, IsNotNull());
+ PAIMON_ASSIGN_OR_RAISE(RoaringBitmap64 equal, Equal(literal));
+ result -= equal;
+ return result;
+}
+
+Result<RoaringBitmap64> BitmapIndexReader::NotIn(const std::vector<Literal>&
literals) {
+ for (const Literal& literal : literals) {
+ if (literal.IsNull()) {
+ return RoaringBitmap64();
+ }
+ }
+ PAIMON_ASSIGN_OR_RAISE(RoaringBitmap64 result, IsNotNull());
+ PAIMON_ASSIGN_OR_RAISE(RoaringBitmap64 in, In(literals));
+ result -= in;
+ return result;
+}
+
+Result<RoaringBitmap64> BitmapIndexReader::Like(const Literal& literal) {
+ if (literal.IsNull()) {
+ return RoaringBitmap64();
+ }
+ if (literal.GetType() != FieldType::STRING) {
+ return Status::Invalid("LIKE requires a string literal in
BitmapIndexReader.");
+ }
+ auto pattern = literal.GetValue<std::string>();
+ return ScanDictionary([pattern = std::move(pattern)](const Literal& key)
-> Result<bool> {
+ if (key.GetType() != FieldType::STRING) {
+ return false;
+ }
+ return
paimon::Like::Instance().TestString(key.GetValue<std::string>(), pattern);
+ });
+}
+
+Result<RoaringBitmap64> BitmapIndexReader::ScanDictionary(const
LiteralPredicate& predicate) {
+ PAIMON_ASSIGN_OR_RAISE(
+ const std::vector<BitmapGlobalIndexFormat::DictionaryBlockMeta>*
block_metas,
+ GetDictionaryBlocks());
+ RoaringBitmap64 result;
+ for (const BitmapGlobalIndexFormat::DictionaryBlockMeta& block_meta :
*block_metas) {
+ PAIMON_ASSIGN_OR_RAISE(const BitmapGlobalIndexFormat::DictionaryBlock*
block,
+ GetDictionaryBlock(block_meta));
+ for (const BitmapGlobalIndexFormat::DictionaryEntry& entry :
block->Entries()) {
+ PAIMON_ASSIGN_OR_RAISE(Literal key, key_serializer_->Deserialize(
+
MemorySlice::Wrap(entry.Key().GetBytes())));
+ PAIMON_ASSIGN_OR_RAISE(bool matches, predicate(key));
+ if (matches) {
+ PAIMON_ASSIGN_OR_RAISE(RoaringBitmap64 bitmap,
BitmapGlobalIndexFormat::ReadBitmap(
+
entry.BitmapBlock(), this));
+ result |= bitmap;
+ }
+ }
+ }
+ return result;
+}
+
+Result<RoaringBitmap64> BitmapIndexReader::ScanSerializedDictionary(
+ const DictionaryPredicate& predicate) {
+ PAIMON_ASSIGN_OR_RAISE(
+ const std::vector<BitmapGlobalIndexFormat::DictionaryBlockMeta>*
block_metas,
+ GetDictionaryBlocks());
+ RoaringBitmap64 result;
+ for (const BitmapGlobalIndexFormat::DictionaryBlockMeta& block_meta :
*block_metas) {
+ PAIMON_ASSIGN_OR_RAISE(const BitmapGlobalIndexFormat::DictionaryBlock*
block,
+ GetDictionaryBlock(block_meta));
+ for (const BitmapGlobalIndexFormat::DictionaryEntry& entry :
block->Entries()) {
+ PAIMON_ASSIGN_OR_RAISE(bool matches, predicate(entry.Key()));
+ if (matches) {
+ PAIMON_ASSIGN_OR_RAISE(RoaringBitmap64 bitmap,
BitmapGlobalIndexFormat::ReadBitmap(
+
entry.BitmapBlock(), this));
+ result |= bitmap;
+ }
+ }
+ }
+ return result;
+}
+
+Result<std::optional<BitmapGlobalIndexFormat::BlockInfo>>
BitmapIndexReader::FindBitmapBlock(
+ const Literal& literal) {
+ PAIMON_ASSIGN_OR_RAISE(const
std::vector<BitmapGlobalIndexFormat::DictionaryBlockMeta>* blocks,
+ GetDictionaryBlocks());
+ if (blocks->empty()) {
+ return std::optional<BitmapGlobalIndexFormat::BlockInfo>();
+ }
+ PAIMON_ASSIGN_OR_RAISE(int32_t index,
FindLogicalDictionaryBlockIndex(*blocks, literal));
+ if (index < 0) {
+ return std::optional<BitmapGlobalIndexFormat::BlockInfo>();
+ }
+ PAIMON_ASSIGN_OR_RAISE(const BitmapGlobalIndexFormat::DictionaryBlock*
block,
+ GetDictionaryBlock((*blocks)[index]));
+ for (const BitmapGlobalIndexFormat::DictionaryEntry& entry :
block->Entries()) {
+ PAIMON_ASSIGN_OR_RAISE(
+ Literal key,
key_serializer_->Deserialize(MemorySlice::Wrap(entry.Key().GetBytes())));
+ PAIMON_ASSIGN_OR_RAISE(int32_t comparison, key.CompareTo(literal));
+ if (comparison == 0) {
+ return
std::optional<BitmapGlobalIndexFormat::BlockInfo>(entry.BitmapBlock());
+ }
+ if (comparison > 0) {
+ break;
+ }
+ }
+ return std::optional<BitmapGlobalIndexFormat::BlockInfo>();
+}
+
+Result<const std::vector<BitmapGlobalIndexFormat::DictionaryBlockMeta>*>
+BitmapIndexReader::GetDictionaryBlocks() {
+ if (!dictionary_blocks_.has_value()) {
+ PAIMON_ASSIGN_OR_RAISE(
+ std::vector<BitmapGlobalIndexFormat::DictionaryBlockMeta> blocks,
+ BitmapGlobalIndexFormat::ReadIndexBlock(footer_.IndexBlock(),
this, pool_.get()));
+ dictionary_blocks_ = std::move(blocks);
+ }
+ return &dictionary_blocks_.value();
+}
+
+Result<const BitmapGlobalIndexFormat::DictionaryBlock*>
BitmapIndexReader::GetDictionaryBlock(
+ const BitmapGlobalIndexFormat::DictionaryBlockMeta& block_meta) {
+ auto iterator = dictionary_block_cache_.find(block_meta.Offset());
+ if (iterator != dictionary_block_cache_.end()) {
+ return iterator->second.get();
+ }
+ PAIMON_ASSIGN_OR_RAISE(
+ BitmapGlobalIndexFormat::DictionaryBlock block,
+ BitmapGlobalIndexFormat::ReadDictionaryBlock(block_meta, this,
pool_.get()));
+ auto inserted = dictionary_block_cache_.emplace(
+ block_meta.Offset(),
+
std::make_shared<BitmapGlobalIndexFormat::DictionaryBlock>(std::move(block)));
+ return inserted.first->second.get();
+}
+
+Result<const RoaringBitmap64*> BitmapIndexReader::GetNullRows() {
+ if (!null_rows_.has_value()) {
+ PAIMON_ASSIGN_OR_RAISE(RoaringBitmap64 bitmap,
+
BitmapGlobalIndexFormat::ReadBitmap(footer_.NullRowsBlock(), this));
+ null_rows_ = std::move(bitmap);
+ }
+ return &null_rows_.value();
+}
+
+Result<const RoaringBitmap64*> BitmapIndexReader::GetNonNullRows() {
+ if (!non_null_rows_.has_value()) {
+ PAIMON_ASSIGN_OR_RAISE(RoaringBitmap64 bitmap,
BitmapGlobalIndexFormat::ReadBitmap(
+
footer_.NonNullRowsBlock(), this));
+ non_null_rows_ = std::move(bitmap);
+ }
+ return &non_null_rows_.value();
+}
+
+Result<int32_t> BitmapIndexReader::FindLogicalDictionaryBlockIndex(
+ const std::vector<BitmapGlobalIndexFormat::DictionaryBlockMeta>& blocks,
+ const Literal& literal) {
+ int32_t low = 0;
+ auto high = static_cast<int32_t>(blocks.size()) - 1;
+ while (low <= high) {
+ int32_t middle = low + ((high - low) / 2);
+ PAIMON_ASSIGN_OR_RAISE(
+ Literal first_key,
+
key_serializer_->Deserialize(MemorySlice::Wrap(blocks[middle].FirstKey().GetBytes())));
+ PAIMON_ASSIGN_OR_RAISE(int32_t comparison,
first_key.CompareTo(literal));
+ if (comparison <= 0) {
+ low = middle + 1;
+ } else {
+ high = middle - 1;
+ }
+ }
+ return high;
+}
+
+int32_t BitmapIndexReader::FindSerializedDictionaryBlockIndex(
+ const std::vector<BitmapGlobalIndexFormat::DictionaryBlockMeta>& blocks,
+ const BitmapGlobalIndexFormat::SerializedKey& key) {
+ int32_t low = 0;
+ auto high = static_cast<int32_t>(blocks.size()) - 1;
+ while (low <= high) {
+ int32_t middle = low + ((high - low) / 2);
+ int32_t comparison = blocks[middle].FirstKey().CompareTo(key);
+ if (comparison <= 0) {
+ low = middle + 1;
+ } else {
+ high = middle - 1;
+ }
+ }
+ return high;
+}
+
+bool BitmapIndexReader::StartsWith(const
BitmapGlobalIndexFormat::SerializedKey& key,
+ const
BitmapGlobalIndexFormat::SerializedKey& prefix) {
+ const std::shared_ptr<Bytes>& key_bytes = key.GetBytes();
+ const std::shared_ptr<Bytes>& prefix_bytes = prefix.GetBytes();
+ return key_bytes->size() >= prefix_bytes->size() &&
+ std::memcmp(key_bytes->data(), prefix_bytes->data(),
prefix_bytes->size()) == 0;
+}
+
+bool BitmapIndexReader::EndsWith(const BitmapGlobalIndexFormat::SerializedKey&
key,
+ const BitmapGlobalIndexFormat::SerializedKey&
suffix) {
+ const std::shared_ptr<Bytes>& key_bytes = key.GetBytes();
+ const std::shared_ptr<Bytes>& suffix_bytes = suffix.GetBytes();
+ return key_bytes->size() >= suffix_bytes->size() &&
+ std::memcmp(key_bytes->data() + key_bytes->size() -
suffix_bytes->size(),
+ suffix_bytes->data(), suffix_bytes->size()) == 0;
+}
+
+bool BitmapIndexReader::Contains(const BitmapGlobalIndexFormat::SerializedKey&
key,
+ const BitmapGlobalIndexFormat::SerializedKey&
infix) {
+ std::string_view key_bytes(key.GetBytes()->data(), key.GetBytes()->size());
+ std::string_view infix_bytes(infix.GetBytes()->data(),
infix.GetBytes()->size());
+ return key_bytes.find(infix_bytes) != std::string_view::npos;
+}
+
+std::optional<BitmapGlobalIndexFormat::SerializedKey>
BitmapIndexReader::PrefixUpperBound(
+ const BitmapGlobalIndexFormat::SerializedKey& prefix, MemoryPool* pool) {
+ const std::shared_ptr<Bytes>& prefix_bytes = prefix.GetBytes();
+ for (auto i = static_cast<int64_t>(prefix_bytes->size()) - 1; i >= 0; --i)
{
+ auto value = static_cast<uint8_t>(prefix_bytes->data()[i]);
+ if (value != 0xFF) {
+ std::shared_ptr<Bytes> upper_bound = Bytes::AllocateBytes(i + 1,
pool);
+ std::memcpy(upper_bound->data(), prefix_bytes->data(), i + 1);
+ upper_bound->data()[i] = static_cast<char>(value + 1);
+ return
BitmapGlobalIndexFormat::SerializedKey(std::move(upper_bound));
+ }
+ }
+ return std::nullopt;
+}
+
+} // namespace paimon
diff --git a/src/paimon/common/global_index/bitmap/bitmap_index_reader.h
b/src/paimon/common/global_index/bitmap/bitmap_index_reader.h
new file mode 100644
index 00000000..41890050
--- /dev/null
+++ b/src/paimon/common/global_index/bitmap/bitmap_index_reader.h
@@ -0,0 +1,160 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <optional>
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "paimon/common/global_index/bitmap/bitmap_global_index_format.h"
+#include "paimon/global_index/global_index_io_meta.h"
+#include "paimon/global_index/global_index_reader.h"
+#include "paimon/global_index/io/global_index_file_reader.h"
+#include "paimon/utils/roaring_bitmap64.h"
+
+namespace paimon {
+
+class KeySerializer;
+
+/// Reader for one Java-compatible bitmap global index file.
+class BitmapIndexReader : public GlobalIndexReader,
+ public BitmapGlobalIndexFormat::SeekableReader,
+ public
std::enable_shared_from_this<BitmapIndexReader> {
+ public:
+ static Result<std::shared_ptr<BitmapIndexReader>> Create(
+ const std::shared_ptr<KeySerializer>& key_serializer,
+ const std::shared_ptr<GlobalIndexFileReader>& file_reader, const
GlobalIndexIOMeta& meta,
+ const std::shared_ptr<MemoryPool>& pool);
+
+ ~BitmapIndexReader() override;
+
+ Result<std::shared_ptr<GlobalIndexResult>> VisitIsNotNull() override;
+ Result<std::shared_ptr<GlobalIndexResult>> VisitIsNull() override;
+ Result<std::shared_ptr<GlobalIndexResult>> VisitEqual(const Literal&
literal) override;
+ Result<std::shared_ptr<GlobalIndexResult>> VisitNotEqual(const Literal&
literal) override;
+ Result<std::shared_ptr<GlobalIndexResult>> VisitLessThan(const Literal&
literal) override;
+ Result<std::shared_ptr<GlobalIndexResult>> VisitLessOrEqual(const Literal&
literal) override;
+ Result<std::shared_ptr<GlobalIndexResult>> VisitGreaterThan(const Literal&
literal) override;
+ Result<std::shared_ptr<GlobalIndexResult>> VisitGreaterOrEqual(const
Literal& literal) override;
+ Result<std::shared_ptr<GlobalIndexResult>> VisitIn(
+ const std::vector<Literal>& literals) override;
+ Result<std::shared_ptr<GlobalIndexResult>> VisitNotIn(
+ const std::vector<Literal>& literals) override;
+ Result<std::shared_ptr<GlobalIndexResult>> VisitStartsWith(const Literal&
prefix) override;
+ Result<std::shared_ptr<GlobalIndexResult>> VisitEndsWith(const Literal&
suffix) override;
+ Result<std::shared_ptr<GlobalIndexResult>> VisitContains(const Literal&
literal) override;
+ Result<std::shared_ptr<GlobalIndexResult>> VisitLike(const Literal&
literal) override;
+
+ Result<std::shared_ptr<ScoredGlobalIndexResult>> VisitVectorSearch(
+ const std::shared_ptr<VectorSearch>& vector_search) override;
+ Result<std::shared_ptr<GlobalIndexResult>> VisitFullTextSearch(
+ const std::shared_ptr<FullTextSearch>& full_text_search) override;
+
+ bool IsThreadSafe() const override {
+ return false;
+ }
+
+ std::string GetIndexType() const override {
+ return "bitmap";
+ }
+
+ using BitmapGlobalIndexFormat::SeekableReader::Read;
+ Result<std::shared_ptr<Bytes>> Read(int64_t offset, int32_t length)
override;
+
+ Status Close();
+
+ private:
+ using DictionaryPredicate =
+ std::function<Result<bool>(const
BitmapGlobalIndexFormat::SerializedKey&)>;
+ using LiteralPredicate = std::function<Result<bool>(const Literal&)>;
+
+ BitmapIndexReader(std::shared_ptr<KeySerializer> key_serializer,
+ std::shared_ptr<InputStream> input, int64_t file_size,
+ std::shared_ptr<MemoryPool> pool)
+ : key_serializer_(std::move(key_serializer)),
+ input_(std::move(input)),
+ file_size_(file_size),
+ pool_(std::move(pool)) {}
+
+ std::shared_ptr<GlobalIndexResult> CreateResult(
+ std::function<Result<RoaringBitmap64>()> supplier);
+
+ Result<RoaringBitmap64> IsNull();
+ Result<RoaringBitmap64> IsNotNull();
+ Result<RoaringBitmap64> Equal(const Literal& literal);
+ Result<RoaringBitmap64> In(const std::vector<Literal>& literals);
+ Result<RoaringBitmap64> StartsWith(const Literal& literal);
+ Result<RoaringBitmap64> EndsWith(const Literal& literal);
+ Result<RoaringBitmap64> Contains(const Literal& literal);
+ Result<RoaringBitmap64> LessThan(const Literal& literal);
+ Result<RoaringBitmap64> LessOrEqual(const Literal& literal);
+ Result<RoaringBitmap64> GreaterThan(const Literal& literal);
+ Result<RoaringBitmap64> GreaterOrEqual(const Literal& literal);
+ Result<RoaringBitmap64> NotEqual(const Literal& literal);
+ Result<RoaringBitmap64> NotIn(const std::vector<Literal>& literals);
+ Result<RoaringBitmap64> Like(const Literal& literal);
+
+ Result<RoaringBitmap64> ScanDictionary(const LiteralPredicate& predicate);
+ Result<RoaringBitmap64> ScanSerializedDictionary(const
DictionaryPredicate& predicate);
+ Result<std::optional<BitmapGlobalIndexFormat::BlockInfo>> FindBitmapBlock(
+ const Literal& literal);
+
+ Result<const std::vector<BitmapGlobalIndexFormat::DictionaryBlockMeta>*>
GetDictionaryBlocks();
+ Result<const BitmapGlobalIndexFormat::DictionaryBlock*> GetDictionaryBlock(
+ const BitmapGlobalIndexFormat::DictionaryBlockMeta& block_meta);
+ Result<const RoaringBitmap64*> GetNullRows();
+ Result<const RoaringBitmap64*> GetNonNullRows();
+
+ Result<int32_t> FindLogicalDictionaryBlockIndex(
+ const std::vector<BitmapGlobalIndexFormat::DictionaryBlockMeta>&
blocks,
+ const Literal& literal);
+ static int32_t FindSerializedDictionaryBlockIndex(
+ const std::vector<BitmapGlobalIndexFormat::DictionaryBlockMeta>&
blocks,
+ const BitmapGlobalIndexFormat::SerializedKey& key);
+
+ static bool StartsWith(const BitmapGlobalIndexFormat::SerializedKey& key,
+ const BitmapGlobalIndexFormat::SerializedKey&
prefix);
+ static bool EndsWith(const BitmapGlobalIndexFormat::SerializedKey& key,
+ const BitmapGlobalIndexFormat::SerializedKey& suffix);
+ static bool Contains(const BitmapGlobalIndexFormat::SerializedKey& key,
+ const BitmapGlobalIndexFormat::SerializedKey& infix);
+ static std::optional<BitmapGlobalIndexFormat::SerializedKey>
PrefixUpperBound(
+ const BitmapGlobalIndexFormat::SerializedKey& prefix, MemoryPool*
pool);
+
+ std::shared_ptr<KeySerializer> key_serializer_;
+ std::shared_ptr<InputStream> input_;
+ int64_t file_size_;
+ BitmapGlobalIndexFormat::Footer footer_{{0, 0}, {0, 0}, {0, 0}};
+ std::shared_ptr<MemoryPool> pool_;
+
+ std::optional<RoaringBitmap64> null_rows_;
+ std::optional<RoaringBitmap64> non_null_rows_;
+ std::optional<std::vector<BitmapGlobalIndexFormat::DictionaryBlockMeta>>
dictionary_blocks_;
+ std::unordered_map<int64_t,
std::shared_ptr<BitmapGlobalIndexFormat::DictionaryBlock>>
+ dictionary_block_cache_;
+ bool closed_ = false;
+};
+
+} // namespace paimon
diff --git a/src/paimon/common/global_index/bitmap/bitmap_index_reader_test.cpp
b/src/paimon/common/global_index/bitmap/bitmap_index_reader_test.cpp
new file mode 100644
index 00000000..cb6f58bd
--- /dev/null
+++ b/src/paimon/common/global_index/bitmap/bitmap_index_reader_test.cpp
@@ -0,0 +1,172 @@
+/*
+ * 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 "paimon/common/global_index/bitmap/bitmap_index_reader.h"
+
+#include <memory>
+#include <numeric>
+#include <string>
+#include <vector>
+
+#include "arrow/c/bridge.h"
+#include "arrow/ipc/api.h"
+#include "gtest/gtest.h"
+#include "paimon/common/global_index/bitmap/bitmap_global_index_writer.h"
+#include "paimon/common/global_index/key_serializer.h"
+#include "paimon/core/global_index/global_index_file_manager.h"
+#include "paimon/global_index/bitmap_global_index_result.h"
+#include "paimon/testing/mock/mock_index_path_factory.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+
+class BitmapIndexReaderTest : public ::testing::Test {
+ protected:
+ // Use a small threshold to exercise dictionary block rollover and
cross-block reads.
+ static constexpr int32_t kBitmapDictionaryBlockSize = 24;
+
+ static Literal StringLiteral(const std::string& value) {
+ return Literal(FieldType::STRING, value.data(), value.size());
+ }
+
+ static void CheckResult(const Result<std::shared_ptr<GlobalIndexResult>>&
result,
+ const std::vector<int64_t>& expected) {
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<GlobalIndexResult> index_result,
result);
+ std::shared_ptr<BitmapGlobalIndexResult> bitmap_result =
+ std::dynamic_pointer_cast<BitmapGlobalIndexResult>(index_result);
+ ASSERT_TRUE(bitmap_result);
+ ASSERT_OK_AND_ASSIGN(const RoaringBitmap64* bitmap,
bitmap_result->GetBitmap());
+ ASSERT_EQ(RoaringBitmap64::From(expected), *bitmap);
+ }
+
+ void SetUp() override {
+ pool_ = GetDefaultPool();
+ test_dir_ = UniqueTestDirectory::Create("local");
+ ASSERT_TRUE(test_dir_);
+ file_manager_ = std::make_shared<GlobalIndexFileManager>(
+ test_dir_->GetFileSystem(),
std::make_shared<MockIndexPathFactory>(test_dir_->Str()),
+ /*checkpoint_path_factory=*/nullptr);
+ ASSERT_OK_AND_ASSIGN(compression_factory_,
+
BlockCompressionFactory::Create(BlockCompressionType::NONE));
+ }
+
+ Result<std::shared_ptr<BitmapGlobalIndexWriter>> CreateWriter(
+ const std::shared_ptr<arrow::Field>& field,
+ int32_t dictionary_block_size = kBitmapDictionaryBlockSize) const {
+ std::shared_ptr<arrow::StructType> struct_type =
+
std::static_pointer_cast<arrow::StructType>(arrow::struct_({field}));
+ return BitmapGlobalIndexWriter::Create(field->name(), struct_type,
file_manager_,
+ dictionary_block_size,
compression_factory_, pool_);
+ }
+
+ Result<std::shared_ptr<BitmapIndexReader>> CreateReader(
+ const std::shared_ptr<arrow::DataType>& type, const GlobalIndexIOMeta&
meta) const {
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<KeySerializer> serializer,
+ KeySerializer::Create(type, pool_));
+ return BitmapIndexReader::Create(serializer, file_manager_, meta,
pool_);
+ }
+
+ std::shared_ptr<MemoryPool> pool_;
+ std::unique_ptr<UniqueTestDirectory> test_dir_;
+ std::shared_ptr<GlobalIndexFileManager> file_manager_;
+ std::shared_ptr<BlockCompressionFactory> compression_factory_;
+};
+
+TEST_F(BitmapIndexReaderTest, WriteAndReadStringPredicates) {
+ std::shared_ptr<arrow::Field> field = arrow::field("f0", arrow::utf8());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<BitmapGlobalIndexWriter> writer,
CreateWriter(field));
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(
+ arrow::struct_({field}),
+ R"([["apple"], ["apple"], [null], ["banana"], ["band"], ["cab"],
[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(std::vector<GlobalIndexIOMeta> metas,
writer->Finish());
+ ASSERT_EQ(1, metas.size());
+ ASSERT_TRUE(metas[0].metadata);
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<BitmapIndexReader> reader,
+ CreateReader(field->type(), metas[0]));
+ CheckResult(reader->VisitIsNull(), {2, 6});
+ CheckResult(reader->VisitIsNotNull(), {0, 1, 3, 4, 5});
+ CheckResult(reader->VisitEqual(StringLiteral("apple")), {0, 1});
+ CheckResult(reader->VisitEqual(StringLiteral("missing")), {});
+ CheckResult(
+ reader->VisitIn({StringLiteral("apple"), StringLiteral("apple"),
StringLiteral("cab")}),
+ {0, 1, 5});
+ CheckResult(reader->VisitNotEqual(StringLiteral("banana")), {0, 1, 4, 5});
+ CheckResult(reader->VisitNotIn({StringLiteral("apple"),
StringLiteral("cab")}), {3, 4});
+
+ CheckResult(reader->VisitLessThan(StringLiteral("band")), {0, 1, 3});
+ CheckResult(reader->VisitLessOrEqual(StringLiteral("band")), {0, 1, 3, 4});
+ CheckResult(reader->VisitGreaterThan(StringLiteral("banana")), {4, 5});
+ CheckResult(reader->VisitGreaterOrEqual(StringLiteral("banana")), {3, 4,
5});
+
+ CheckResult(reader->VisitStartsWith(StringLiteral("ban")), {3, 4});
+ CheckResult(reader->VisitEndsWith(StringLiteral("le")), {0, 1});
+ CheckResult(reader->VisitContains(StringLiteral("an")), {3, 4});
+ CheckResult(reader->VisitLike(StringLiteral("ban_n_")), {3});
+ ASSERT_OK(reader->Close());
+}
+
+TEST_F(BitmapIndexReaderTest, WriteAndReadLogicalIntegerOrder) {
+ std::shared_ptr<arrow::Field> field = arrow::field("f0", arrow::int32());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<BitmapGlobalIndexWriter> writer,
+ CreateWriter(field, /*dictionary_block_size=*/8));
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}),
+ R"([[-257], [-1], [0], [1],
[256], [1000]])")
+ .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(std::vector<GlobalIndexIOMeta> metas,
writer->Finish());
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<BitmapIndexReader> reader,
+ CreateReader(field->type(), metas[0]));
+ CheckResult(reader->VisitEqual(Literal(static_cast<int32_t>(256))), {4});
+ CheckResult(reader->VisitLessThan(Literal(static_cast<int32_t>(1))), {0,
1, 2});
+ CheckResult(reader->VisitGreaterOrEqual(Literal(static_cast<int32_t>(0))),
{2, 3, 4, 5});
+}
+
+TEST_F(BitmapIndexReaderTest, RejectsDescendingKeys) {
+ std::shared_ptr<arrow::Field> field = arrow::field("f0", arrow::int32());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<BitmapGlobalIndexWriter> writer,
CreateWriter(field));
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({field}),
R"([[2], [1]])")
+ .ValueOrDie();
+ ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ ASSERT_NOK_WITH_MSG(writer->AddBatch(&c_array, {0, 1}), "monotonically
increasing");
+}
+
+TEST_F(BitmapIndexReaderTest, EmptyWriterCreatesNoFile) {
+ std::shared_ptr<arrow::Field> field = arrow::field("f0", arrow::utf8());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<BitmapGlobalIndexWriter> writer,
CreateWriter(field));
+ ASSERT_OK_AND_ASSIGN(std::vector<GlobalIndexIOMeta> metas,
writer->Finish());
+ ASSERT_TRUE(metas.empty());
+}
+
+} // namespace paimon::test
diff --git a/src/paimon/common/io/data_output_stream.cpp
b/src/paimon/common/io/data_output_stream.cpp
index 53825a05..12e46c11 100644
--- a/src/paimon/common/io/data_output_stream.cpp
+++ b/src/paimon/common/io/data_output_stream.cpp
@@ -25,7 +25,11 @@
namespace paimon {
DataOutputStream::DataOutputStream(const std::shared_ptr<OutputStream>&
output_stream)
- : output_stream_(output_stream) {
+ : owned_output_stream_(output_stream),
output_stream_(owned_output_stream_.get()) {
+ assert(output_stream_);
+}
+
+DataOutputStream::DataOutputStream(OutputStream* output_stream) :
output_stream_(output_stream) {
assert(output_stream_);
}
diff --git a/src/paimon/common/io/data_output_stream.h
b/src/paimon/common/io/data_output_stream.h
index dfb6456b..a1ce5f01 100644
--- a/src/paimon/common/io/data_output_stream.h
+++ b/src/paimon/common/io/data_output_stream.h
@@ -40,6 +40,7 @@ class OutputStream;
class PAIMON_EXPORT DataOutputStream {
public:
explicit DataOutputStream(const std::shared_ptr<OutputStream>&
output_stream);
+ explicit DataOutputStream(OutputStream* output_stream);
template <typename T>
Status WriteValue(const T& value) {
@@ -71,7 +72,8 @@ class PAIMON_EXPORT DataOutputStream {
bool NeedSwap() const;
private:
- std::shared_ptr<OutputStream> output_stream_;
+ std::shared_ptr<OutputStream> owned_output_stream_;
+ OutputStream* output_stream_;
ByteOrder byte_order_ = ByteOrder::PAIMON_BIG_ENDIAN;
};
diff --git a/src/paimon/common/utils/var_length_int_utils.h
b/src/paimon/common/utils/var_length_int_utils.h
index ec76460c..a4c67fb2 100644
--- a/src/paimon/common/utils/var_length_int_utils.h
+++ b/src/paimon/common/utils/var_length_int_utils.h
@@ -22,6 +22,7 @@
#include <cstring>
#include "fmt/format.h"
+#include "paimon/common/utils/math.h"
#include "paimon/macros.h"
#include "paimon/result.h"
namespace paimon {
@@ -134,6 +135,42 @@ class VarLengthIntUtils {
}
return Status::Invalid("Malformed varint64: too many continuation
bytes");
}
+
+ /// Reads a non-negative varint32 from a bounded input.
+ template <typename Input>
+ static Result<int32_t> ReadVarLenInt(Input* input) {
+ uint32_t result = 0;
+ for (int32_t shift = 0; shift < 32; shift += 7) {
+ if (input->Available() == 0) {
+ return Status::Invalid("Truncated varint32 input.");
+ }
+ auto value = static_cast<uint8_t>(input->ReadByte());
+ result |= static_cast<uint32_t>(value & 0x7F) << shift;
+ if ((value & 0x80) == 0) {
+ PAIMON_RETURN_NOT_OK(ValidateValueInRange<int32_t>(result,
"varint32"));
+ return static_cast<int32_t>(result);
+ }
+ }
+ return Status::Invalid("Malformed varint32 input.");
+ }
+
+ /// Reads a non-negative varint64 from a bounded input.
+ template <typename Input>
+ static Result<int64_t> ReadVarLenLong(Input* input) {
+ uint64_t result = 0;
+ for (int32_t shift = 0; shift <= 56; shift += 7) {
+ if (input->Available() == 0) {
+ return Status::Invalid("Truncated varint64 input.");
+ }
+ auto value = static_cast<uint8_t>(input->ReadByte());
+ result |= static_cast<uint64_t>(value & 0x7F) << shift;
+ if ((value & 0x80) == 0) {
+ PAIMON_RETURN_NOT_OK(ValidateValueInRange<int64_t>(result,
"varint64"));
+ return static_cast<int64_t>(result);
+ }
+ }
+ return Status::Invalid("Malformed varint64 input.");
+ }
};
} // namespace paimon
diff --git a/src/paimon/common/utils/var_length_int_utils_test.cpp
b/src/paimon/common/utils/var_length_int_utils_test.cpp
index c2404e2f..5c414b26 100644
--- a/src/paimon/common/utils/var_length_int_utils_test.cpp
+++ b/src/paimon/common/utils/var_length_int_utils_test.cpp
@@ -23,6 +23,7 @@
#include <vector>
#include "gtest/gtest.h"
+#include "paimon/common/memory/memory_slice_input.h"
#include "paimon/testing/utils/testharness.h"
namespace paimon::test {
@@ -141,4 +142,53 @@ TEST(VarLengthIntUtilsTest, TestEncodeLongBytesNumber) {
ASSERT_EQ(encoded_length, i + 1) << values[i];
}
}
+
+TEST(VarLengthIntUtilsTest, TestReadVarLenInt) {
+ const std::vector<int32_t> values = {0, 127, 128, 16384,
std::numeric_limits<int32_t>::max()};
+ for (int32_t value : values) {
+ char buffer[VarLengthIntUtils::kMaxVarIntSize];
+ ASSERT_OK_AND_ASSIGN(int32_t encoded_length,
VarLengthIntUtils::EncodeInt(value, buffer));
+ MemorySliceInput
input{MemorySlice::Wrap(MemorySegment::WrapView(buffer, encoded_length))};
+ ASSERT_OK_AND_ASSIGN(int32_t actual,
VarLengthIntUtils::ReadVarLenInt(&input));
+ ASSERT_EQ(value, actual);
+ ASSERT_EQ(0, input.Available());
+ }
+
+ const char truncated[] = {static_cast<char>(0x80)};
+ MemorySliceInput truncated_input{MemorySlice::Wrap(
+ MemorySegment::WrapView(truncated,
static_cast<int32_t>(sizeof(truncated))))};
+ ASSERT_NOK(VarLengthIntUtils::ReadVarLenInt(&truncated_input));
+
+ const char malformed[] = {static_cast<char>(0x80), static_cast<char>(0x80),
+ static_cast<char>(0x80), static_cast<char>(0x80),
+ static_cast<char>(0x80)};
+ MemorySliceInput malformed_input{MemorySlice::Wrap(
+ MemorySegment::WrapView(malformed,
static_cast<int32_t>(sizeof(malformed))))};
+ ASSERT_NOK(VarLengthIntUtils::ReadVarLenInt(&malformed_input));
+}
+
+TEST(VarLengthIntUtilsTest, TestReadVarLenLong) {
+ const std::vector<int64_t> values = {0, 127, 128, 16384,
std::numeric_limits<int64_t>::max()};
+ for (int64_t value : values) {
+ char buffer[VarLengthIntUtils::kMaxVarLongSize];
+ ASSERT_OK_AND_ASSIGN(int32_t encoded_length,
VarLengthIntUtils::EncodeLong(value, buffer));
+ MemorySliceInput
input{MemorySlice::Wrap(MemorySegment::WrapView(buffer, encoded_length))};
+ ASSERT_OK_AND_ASSIGN(int64_t actual,
VarLengthIntUtils::ReadVarLenLong(&input));
+ ASSERT_EQ(value, actual);
+ ASSERT_EQ(0, input.Available());
+ }
+
+ const char truncated[] = {static_cast<char>(0x80)};
+ MemorySliceInput truncated_input{MemorySlice::Wrap(
+ MemorySegment::WrapView(truncated,
static_cast<int32_t>(sizeof(truncated))))};
+ ASSERT_NOK(VarLengthIntUtils::ReadVarLenLong(&truncated_input));
+
+ const char malformed[] = {
+ static_cast<char>(0x80), static_cast<char>(0x80),
static_cast<char>(0x80),
+ static_cast<char>(0x80), static_cast<char>(0x80),
static_cast<char>(0x80),
+ static_cast<char>(0x80), static_cast<char>(0x80),
static_cast<char>(0x80)};
+ MemorySliceInput malformed_input{MemorySlice::Wrap(
+ MemorySegment::WrapView(malformed,
static_cast<int32_t>(sizeof(malformed))))};
+ ASSERT_NOK(VarLengthIntUtils::ReadVarLenLong(&malformed_input));
+}
} // namespace paimon::test