This is an automated email from the ASF dual-hosted git repository.

lxy-9602 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 370582dc feat(file-index): support BSI and Bloom filter writers (#234)
370582dc is described below

commit 370582dc1e3ec414bd83b739681e02686db86464
Author: Zhang Jiawei <[email protected]>
AuthorDate: Tue Aug 25 17:59:46 2026 +0800

    feat(file-index): support BSI and Bloom filter writers (#234)
---
 .../common/file_index/bitmap/bitmap_file_index.cpp |  24 +----
 .../common/file_index/bitmap/bitmap_file_index.h   |   2 +-
 .../bloomfilter/bloom_filter_file_index.cpp        |  88 +++++++++++++--
 .../bloomfilter/bloom_filter_file_index.h          |  38 +++++--
 .../bloomfilter/bloom_filter_file_index_test.cpp   |  55 +++++++++-
 .../common/file_index/bloomfilter/fast_hash.cpp    |  16 ++-
 .../file_index/bloomfilter/fast_hash_test.cpp      |  31 ++++++
 .../bsi/bit_slice_index_bitmap_file_index.cpp      | 105 ++++++++++++++++++
 .../bsi/bit_slice_index_bitmap_file_index.h        |  34 ++++--
 .../bsi/bit_slice_index_bitmap_file_index_test.cpp | 119 +++++++++++++++------
 .../rangebitmap/range_bitmap_file_index.cpp        |  33 ++----
 .../rangebitmap/range_bitmap_file_index.h          |   6 +-
 .../rangebitmap/range_bitmap_file_index_test.cpp   |  10 +-
 .../rangebitmap/range_bitmap_io_test.cpp           |   8 +-
 src/paimon/common/lookup/lookup_store_factory.cpp  |   3 +-
 src/paimon/common/sst/sst_file_io_test.cpp         |   4 +-
 src/paimon/common/utils/bloom_filter.cpp           |  24 +++--
 src/paimon/common/utils/bloom_filter.h             |   8 +-
 src/paimon/common/utils/bloom_filter64.cpp         |  52 +++++++--
 src/paimon/common/utils/bloom_filter64.h           |  11 +-
 src/paimon/common/utils/bloom_filter64_test.cpp    |  42 +++++++-
 src/paimon/common/utils/bloom_filter_test.cpp      |  35 ++++--
 src/paimon/common/utils/math.h                     |  27 +++++
 src/paimon/common/utils/math_test.cpp              |  18 ++++
 src/paimon/core/io/data_file_index_writer_test.cpp |  31 +++++-
 25 files changed, 658 insertions(+), 166 deletions(-)

diff --git a/src/paimon/common/file_index/bitmap/bitmap_file_index.cpp 
b/src/paimon/common/file_index/bitmap/bitmap_file_index.cpp
index 6df16143..43bb71b0 100644
--- a/src/paimon/common/file_index/bitmap/bitmap_file_index.cpp
+++ b/src/paimon/common/file_index/bitmap/bitmap_file_index.cpp
@@ -103,24 +103,18 @@ Result<std::shared_ptr<FileIndexWriter>> 
BitmapFileIndex::CreateWriter(
             "invalid schema for BitmapFileIndexWriter, supposed to have single 
"
             "field.");
     }
-    auto arrow_field = arrow_schema->field(0);
-    return BitmapFileIndexWriter::Create(arrow_schema, arrow_field->name(), 
options_, pool);
+    return BitmapFileIndexWriter::Create(arrow_schema->field(0), options_, 
pool);
 }
 
 Result<std::shared_ptr<BitmapFileIndexWriter>> BitmapFileIndexWriter::Create(
-    const std::shared_ptr<arrow::Schema>& arrow_schema, const std::string& 
field_name,
-    const std::map<std::string, std::string>& options, const 
std::shared_ptr<MemoryPool>& pool) {
+    const std::shared_ptr<arrow::Field>& field, const std::map<std::string, 
std::string>& options,
+    const std::shared_ptr<MemoryPool>& pool) {
     PAIMON_ASSIGN_OR_RAISE(int8_t version,
                            OptionsUtils::GetValueFromMap<int8_t>(options, 
BitmapFileIndex::VERSION,
                                                                  
BitmapFileIndex::VERSION_2));
-    auto arrow_field = arrow_schema->GetFieldByName(field_name);
-    if (!arrow_field) {
-        return Status::Invalid(
-            fmt::format("field {} not in arrow_schema for 
BitmapFileIndexWriter", field_name));
-    }
-    auto struct_type = arrow::struct_({arrow_field});
+    std::shared_ptr<arrow::DataType> struct_type = arrow::struct_({field});
     return std::shared_ptr<BitmapFileIndexWriter>(
-        new BitmapFileIndexWriter(version, struct_type, arrow_field->type(), 
options, pool));
+        new BitmapFileIndexWriter(version, struct_type, field->type(), 
options, pool));
 }
 
 BitmapFileIndexWriter::BitmapFileIndexWriter(int8_t version,
@@ -137,15 +131,7 @@ BitmapFileIndexWriter::BitmapFileIndexWriter(int8_t 
version,
 Status BitmapFileIndexWriter::AddBatch(::ArrowArray* batch) {
     PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> 
arrow_array,
                                       arrow::ImportArray(batch, struct_type_));
-    if (!arrow_array || arrow_array->type_id() != arrow::Type::STRUCT) {
-        return Status::Invalid("invalid batch for BitmapFileIndexWriter, 
expected a struct array");
-    }
     auto struct_array = checked_pointer_cast<arrow::StructArray>(arrow_array);
-    if (struct_array->num_fields() != 1) {
-        return Status::Invalid(
-            "invalid batch for BitmapFileIndexWriter, expected a struct array 
with exactly one "
-            "field");
-    }
     PAIMON_ASSIGN_OR_RAISE(
         std::vector<Literal> array_values,
         LiteralConverter::ConvertLiteralsFromArray(*(struct_array->field(0)), 
/*own_data=*/true));
diff --git a/src/paimon/common/file_index/bitmap/bitmap_file_index.h 
b/src/paimon/common/file_index/bitmap/bitmap_file_index.h
index 9cf15ddc..6aa0f416 100644
--- a/src/paimon/common/file_index/bitmap/bitmap_file_index.h
+++ b/src/paimon/common/file_index/bitmap/bitmap_file_index.h
@@ -71,7 +71,7 @@ class PAIMON_EXPORT BitmapFileIndex : public FileIndexer {
 class BitmapFileIndexWriter : public FileIndexWriter {
  public:
     static Result<std::shared_ptr<BitmapFileIndexWriter>> Create(
-        const std::shared_ptr<arrow::Schema>& arrow_schema, const std::string& 
field_name,
+        const std::shared_ptr<arrow::Field>& field,
         const std::map<std::string, std::string>& options, const 
std::shared_ptr<MemoryPool>& pool);
 
     Status AddBatch(::ArrowArray* batch) override;
diff --git 
a/src/paimon/common/file_index/bloomfilter/bloom_filter_file_index.cpp 
b/src/paimon/common/file_index/bloomfilter/bloom_filter_file_index.cpp
index ca33c696..86d07d63 100644
--- a/src/paimon/common/file_index/bloomfilter/bloom_filter_file_index.cpp
+++ b/src/paimon/common/file_index/bloomfilter/bloom_filter_file_index.cpp
@@ -19,10 +19,19 @@
 #include "paimon/common/file_index/bloomfilter/bloom_filter_file_index.h"
 
 #include <cstddef>
+#include <cstdint>
+#include <cstring>
 #include <functional>
 #include <utility>
+#include <vector>
 
+#include "arrow/c/bridge.h"
 #include "fmt/format.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/math.h"
+#include "paimon/common/utils/options_utils.h"
 #include "paimon/fs/file_system.h"
 #include "paimon/memory/bytes.h"
 #include "paimon/predicate/literal.h"
@@ -31,7 +40,8 @@
 namespace paimon {
 class MemoryPool;
 
-BloomFilterFileIndex::BloomFilterFileIndex(const std::map<std::string, 
std::string>& options) {}
+BloomFilterFileIndex::BloomFilterFileIndex(const std::map<std::string, 
std::string>& options)
+    : options_(options) {}
 Result<std::shared_ptr<FileIndexReader>> BloomFilterFileIndex::CreateReader(
     ::ArrowSchema* c_arrow_schema, int32_t start, int32_t length,
     const std::shared_ptr<InputStream>& input_stream,
@@ -58,15 +68,77 @@ Result<std::shared_ptr<FileIndexReader>> 
BloomFilterFileIndex::CreateReader(
     return BloomFilterFileIndexReader::Create(arrow_type, bytes);
 }
 
+Result<std::shared_ptr<FileIndexWriter>> BloomFilterFileIndex::CreateWriter(
+    ::ArrowSchema* c_arrow_schema, const std::shared_ptr<MemoryPool>& pool) 
const {
+    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Schema> 
arrow_schema,
+                                      arrow::ImportSchema(c_arrow_schema));
+    if (arrow_schema->num_fields() != 1) {
+        return Status::Invalid(
+            "invalid schema for BloomFilterFileIndexWriter, supposed to have 
single field.");
+    }
+    return BloomFilterFileIndexWriter::Create(arrow_schema->field(0), 
options_, pool);
+}
+
+Result<std::shared_ptr<BloomFilterFileIndexWriter>> 
BloomFilterFileIndexWriter::Create(
+    const std::shared_ptr<arrow::Field>& field, const std::map<std::string, 
std::string>& options,
+    const std::shared_ptr<MemoryPool>& pool) {
+    PAIMON_ASSIGN_OR_RAISE(FastHash::HashFunction hash_function,
+                           FastHash::GetHashFunction(field->type()));
+    PAIMON_ASSIGN_OR_RAISE(
+        int32_t items, OptionsUtils::GetValueFromMap<int32_t>(options, 
BloomFilterFileIndex::kItems,
+                                                              
BloomFilterFileIndex::kDefaultItems));
+    PAIMON_ASSIGN_OR_RAISE(
+        double fpp, OptionsUtils::GetValueFromMap<double>(options, 
BloomFilterFileIndex::kFpp,
+                                                          
BloomFilterFileIndex::kDefaultFpp));
+    std::shared_ptr<arrow::DataType> struct_type = arrow::struct_({field});
+    PAIMON_ASSIGN_OR_RAISE(BloomFilter64 filter, BloomFilter64::Create(items, 
fpp, pool));
+    return std::shared_ptr<BloomFilterFileIndexWriter>(
+        new BloomFilterFileIndexWriter(struct_type, hash_function, 
std::move(filter), pool));
+}
+
+BloomFilterFileIndexWriter::BloomFilterFileIndexWriter(
+    const std::shared_ptr<arrow::DataType>& struct_type,
+    const FastHash::HashFunction& hash_function, BloomFilter64&& filter,
+    const std::shared_ptr<MemoryPool>& pool)
+    : struct_type_(struct_type),
+      hash_function_(hash_function),
+      filter_(std::move(filter)),
+      pool_(pool) {}
+
+Status BloomFilterFileIndexWriter::AddBatch(::ArrowArray* batch) {
+    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
+                                      arrow::ImportArray(batch, struct_type_));
+    std::shared_ptr<arrow::StructArray> struct_array =
+        checked_pointer_cast<arrow::StructArray>(array);
+    PAIMON_ASSIGN_OR_RAISE(
+        std::vector<Literal> values,
+        LiteralConverter::ConvertLiteralsFromArray(*struct_array->field(0), 
/*own_data=*/false));
+    for (const Literal& value : values) {
+        if (!value.IsNull()) {
+            filter_.AddHash(hash_function_(value));
+        }
+    }
+    return Status::OK();
+}
+
+Result<PAIMON_UNIQUE_PTR<Bytes>> BloomFilterFileIndexWriter::SerializedBytes() 
const {
+    constexpr int32_t kHeaderLength = sizeof(int32_t);
+    const int32_t bit_set_length = filter_.GetBitSet().ByteLength();
+    PAIMON_UNIQUE_PTR<Bytes> bytes =
+        Bytes::AllocateBytes(kHeaderLength + bit_set_length, pool_.get());
+    const int32_t num_hash_functions = 
ToBigEndian(filter_.GetNumHashFunctions());
+    std::memcpy(bytes->data(), &num_hash_functions, 
sizeof(num_hash_functions));
+    filter_.GetBitSet().ToByteArray(kHeaderLength, bit_set_length, 
bytes->data());
+    return bytes;
+}
+
 Result<std::shared_ptr<BloomFilterFileIndexReader>> 
BloomFilterFileIndexReader::Create(
     const std::shared_ptr<arrow::DataType>& arrow_type, const 
std::shared_ptr<Bytes>& bytes) {
-    // compatible with java, little endian
-    const char* data = bytes->data();
-    auto num_hash_functions =
-        
static_cast<int32_t>((static_cast<uint32_t>(static_cast<uint8_t>(data[0])) << 
24) |
-                             
(static_cast<uint32_t>(static_cast<uint8_t>(data[1])) << 16) |
-                             
(static_cast<uint32_t>(static_cast<uint8_t>(data[2])) << 8) |
-                             
static_cast<uint32_t>(static_cast<uint8_t>(data[3])));
+    // Compatible with Java's big-endian numHashFunctions header.
+    int32_t big_endian_num_hash_functions;
+    std::memcpy(&big_endian_num_hash_functions, bytes->data(),
+                sizeof(big_endian_num_hash_functions));
+    const int32_t num_hash_functions = 
FromBigEndian(big_endian_num_hash_functions);
     PAIMON_ASSIGN_OR_RAISE(FastHash::HashFunction hash_function,
                            FastHash::GetHashFunction(arrow_type));
     auto bit_set = std::make_unique<BloomFilter64::BitSet>(bytes, 
/*offset=*/sizeof(int32_t));
diff --git a/src/paimon/common/file_index/bloomfilter/bloom_filter_file_index.h 
b/src/paimon/common/file_index/bloomfilter/bloom_filter_file_index.h
index 8152c220..62d082c7 100644
--- a/src/paimon/common/file_index/bloomfilter/bloom_filter_file_index.h
+++ b/src/paimon/common/file_index/bloomfilter/bloom_filter_file_index.h
@@ -25,10 +25,10 @@
 
 #include "arrow/c/bridge.h"
 #include "paimon/common/file_index/bloomfilter/fast_hash.h"
-#include "paimon/common/utils/arrow/status_utils.h"
 #include "paimon/common/utils/bloom_filter64.h"
 #include "paimon/file_index/file_index_reader.h"
 #include "paimon/file_index/file_index_result.h"
+#include "paimon/file_index/file_index_writer.h"
 #include "paimon/file_index/file_indexer.h"
 #include "paimon/result.h"
 namespace paimon {
@@ -55,11 +55,37 @@ class BloomFilterFileIndex : public FileIndexer {
         const std::shared_ptr<MemoryPool>& pool) const override;
 
     Result<std::shared_ptr<FileIndexWriter>> CreateWriter(
-        ::ArrowSchema* arrow_schema, const std::shared_ptr<MemoryPool>& pool) 
const override {
-        PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::DataType> 
arrow_type,
-                                          arrow::ImportType(arrow_schema));
-        return Status::NotImplemented("do not support index writer in bloom 
filter");
-    }
+        ::ArrowSchema* arrow_schema, const std::shared_ptr<MemoryPool>& pool) 
const override;
+
+    static constexpr int32_t kDefaultItems = 1000000;
+    static constexpr double kDefaultFpp = 0.1;
+    static constexpr char kItems[] = "items";
+    static constexpr char kFpp[] = "fpp";
+
+ private:
+    std::map<std::string, std::string> options_;
+};
+
+class BloomFilterFileIndexWriter : public FileIndexWriter {
+ public:
+    static Result<std::shared_ptr<BloomFilterFileIndexWriter>> Create(
+        const std::shared_ptr<arrow::Field>& field,
+        const std::map<std::string, std::string>& options, const 
std::shared_ptr<MemoryPool>& pool);
+
+    Status AddBatch(::ArrowArray* batch) override;
+
+    Result<PAIMON_UNIQUE_PTR<Bytes>> SerializedBytes() const override;
+
+ private:
+    BloomFilterFileIndexWriter(const std::shared_ptr<arrow::DataType>& 
struct_type,
+                               const FastHash::HashFunction& hash_function, 
BloomFilter64&& filter,
+                               const std::shared_ptr<MemoryPool>& pool);
+
+ private:
+    std::shared_ptr<arrow::DataType> struct_type_;
+    FastHash::HashFunction hash_function_;
+    BloomFilter64 filter_;
+    std::shared_ptr<MemoryPool> pool_;
 };
 
 class BloomFilterFileIndexReader : public FileIndexReader {
diff --git 
a/src/paimon/common/file_index/bloomfilter/bloom_filter_file_index_test.cpp 
b/src/paimon/common/file_index/bloomfilter/bloom_filter_file_index_test.cpp
index 209f0b40..23fadce2 100644
--- a/src/paimon/common/file_index/bloomfilter/bloom_filter_file_index_test.cpp
+++ b/src/paimon/common/file_index/bloomfilter/bloom_filter_file_index_test.cpp
@@ -18,10 +18,16 @@
 
 #include "paimon/common/file_index/bloomfilter/bloom_filter_file_index.h"
 
+#include <cstring>
+#include <map>
+#include <string>
 #include <utility>
 #include <vector>
 
+#include "arrow/c/bridge.h"
+#include "arrow/ipc/json_simple.h"
 #include "gtest/gtest.h"
+#include "paimon/common/utils/arrow/status_utils.h"
 #include "paimon/common/utils/field_type_utils.h"
 #include "paimon/data/timestamp.h"
 #include "paimon/defs.h"
@@ -49,21 +55,60 @@ class BloomFilterIndexReaderTest : public ::testing::Test {
         return c_schema;
     }
 
+    Result<PAIMON_UNIQUE_PTR<Bytes>> WriteIndex(
+        const std::shared_ptr<arrow::DataType>& data_type, const std::string& 
json,
+        const std::map<std::string, std::string>& options) const {
+        const std::shared_ptr<arrow::Schema> schema =
+            arrow::schema({arrow::field("f0", data_type)});
+        std::shared_ptr<arrow::Array> array =
+            
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(schema->fields()), 
json)
+                .ValueOrDie();
+        ::ArrowSchema c_schema;
+        PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*schema, 
&c_schema));
+        BloomFilterFileIndex file_index(options);
+        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<FileIndexWriter> writer,
+                               file_index.CreateWriter(&c_schema, pool_));
+        ::ArrowArray c_array;
+        PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, &c_array));
+        PAIMON_RETURN_NOT_OK(writer->AddBatch(&c_array));
+        return writer->SerializedBytes();
+    }
+
  private:
     std::shared_ptr<MemoryPool> pool_;
 };
 
+TEST_F(BloomFilterIndexReaderTest, TestWriterRejectsInvalidOptionsAndType) {
+    auto create_writer = [&](const std::shared_ptr<arrow::DataType>& type,
+                             const std::map<std::string, std::string>& 
options) {
+        BloomFilterFileIndex file_index(options);
+        return file_index.CreateWriter(CreateArrowSchema(type).get(), pool_);
+    };
+    ASSERT_NOK_WITH_MSG(create_writer(arrow::int32(), {{"items", "0"}}),
+                        "items must be greater than 0");
+    ASSERT_NOK_WITH_MSG(create_writer(arrow::int32(), {{"fpp", "1"}}),
+                        "fpp must be greater than 0 and less than 1");
+    ASSERT_NOK_WITH_MSG(create_writer(arrow::boolean(), {}),
+                        "bloom filter index does not support BOOLEAN");
+}
+
 TEST_F(BloomFilterIndexReaderTest, TestStringType) {
-    // data: "a", "b", ""
-    std::vector<uint8_t> index_bytes = {0, 0, 0, 6, 0, 32, 32, 3, 208, 32, 0, 
64, 73, 16, 201};
-    auto input_stream = std::make_shared<ByteArrayInputStream>(
-        reinterpret_cast<char*>(index_bytes.data()), index_bytes.size());
+    // Java writer output for data: "a", "b", "" with items=10 and fpp=0.02.
+    const std::vector<uint8_t> expected = {0, 0, 0, 6, 0, 32, 32, 3, 208, 32, 
0, 64, 73, 16, 201};
+    ASSERT_OK_AND_ASSIGN(
+        PAIMON_UNIQUE_PTR<Bytes> bytes,
+        WriteIndex(arrow::utf8(), R"([["a"], ["b"], [""]])", {{"items", "10"}, 
{"fpp", "0.02"}}));
+    ASSERT_EQ(expected.size(), bytes->size());
+    ASSERT_EQ(0, std::memcmp(expected.data(), bytes->data(), expected.size()));
+
+    std::shared_ptr<ByteArrayInputStream> input_stream =
+        std::make_shared<ByteArrayInputStream>(bytes->data(), bytes->size());
 
     BloomFilterFileIndex file_index({});
     ASSERT_OK_AND_ASSIGN(
         auto reader,
         file_index.CreateReader(CreateArrowSchema(arrow::utf8()).get(),
-                                /*start=*/0, /*length=*/index_bytes.size(), 
input_stream, pool_));
+                                /*start=*/0, /*length=*/bytes->size(), 
input_stream, pool_));
     ASSERT_TRUE(reader);
     ASSERT_TRUE(reader->VisitEqual(Literal(FieldType::STRING, "a", 
1)).value()->IsRemain().value());
     ASSERT_TRUE(reader->VisitEqual(Literal(FieldType::STRING, "b", 
1)).value()->IsRemain().value());
diff --git a/src/paimon/common/file_index/bloomfilter/fast_hash.cpp 
b/src/paimon/common/file_index/bloomfilter/fast_hash.cpp
index d98dc476..b1d8784f 100644
--- a/src/paimon/common/file_index/bloomfilter/fast_hash.cpp
+++ b/src/paimon/common/file_index/bloomfilter/fast_hash.cpp
@@ -19,6 +19,7 @@
 #include "paimon/common/file_index/bloomfilter/fast_hash.h"
 
 #include <cassert>
+#include <cmath>
 #include <cstring>
 #include <string>
 #include <utility>
@@ -34,6 +35,11 @@
 #include "xxhash.h"  // NOLINT(build/include_subdir)
 
 namespace paimon {
+namespace {
+constexpr int32_t kCanonicalFloatNaNBits = 0x7fc00000;
+constexpr int64_t kCanonicalDoubleNaNBits = 0x7ff8000000000000L;
+}  // namespace
+
 Result<FastHash::HashFunction> FastHash::GetHashFunction(
     const std::shared_ptr<arrow::DataType>& arrow_type) {
     PAIMON_ASSIGN_OR_RAISE(FieldType field_type,
@@ -58,14 +64,20 @@ Result<FastHash::HashFunction> FastHash::GetHashFunction(
             });
         case FieldType::FLOAT:
             return HashFunction([](const Literal& literal) -> int64_t {
-                auto raw_value = literal.GetValue<float>();
+                const auto raw_value = literal.GetValue<float>();
+                if (std::isnan(raw_value)) {
+                    return GetLongHash(kCanonicalFloatNaNBits);
+                }
                 int32_t bits = 0;
                 std::memcpy(&bits, &raw_value, sizeof(raw_value));
                 return GetLongHash(bits);
             });
         case FieldType::DOUBLE:
             return HashFunction([](const Literal& literal) -> int64_t {
-                auto raw_value = literal.GetValue<double>();
+                const auto raw_value = literal.GetValue<double>();
+                if (std::isnan(raw_value)) {
+                    return GetLongHash(kCanonicalDoubleNaNBits);
+                }
                 int64_t bits;
                 std::memcpy(&bits, &raw_value, sizeof(raw_value));
                 return GetLongHash(bits);
diff --git a/src/paimon/common/file_index/bloomfilter/fast_hash_test.cpp 
b/src/paimon/common/file_index/bloomfilter/fast_hash_test.cpp
index 680d591b..8a528e44 100644
--- a/src/paimon/common/file_index/bloomfilter/fast_hash_test.cpp
+++ b/src/paimon/common/file_index/bloomfilter/fast_hash_test.cpp
@@ -18,6 +18,9 @@
 
 #include "paimon/common/file_index/bloomfilter/fast_hash.h"
 
+#include <cmath>
+#include <cstdint>
+#include <cstring>
 #include <limits>
 #include <string>
 #include <vector>
@@ -164,4 +167,32 @@ TEST_F(FastHashTest, TestCompatibleWithJava) {
     }
 }
 
+TEST_F(FastHashTest, TestNaNCompatibleWithJava) {
+    auto float_from_bits = [](uint32_t bits) {
+        float value;
+        std::memcpy(&value, &bits, sizeof(value));
+        return value;
+    };
+    const float float_nan = float_from_bits(0x7fc12345);
+    const float negative_float_nan = float_from_bits(0xffc54321);
+    ASSERT_TRUE(std::isnan(float_nan));
+    ASSERT_TRUE(std::isnan(negative_float_nan));
+    ASSERT_OK_AND_ASSIGN(auto float_hash_function, 
FastHash::GetHashFunction(arrow::float32()));
+    CheckResult(float_hash_function, {Literal(float_nan), 
Literal(negative_float_nan)},
+                {0x67c27c6d9936ae63, 0x67c27c6d9936ae63});
+
+    auto double_from_bits = [](uint64_t bits) {
+        double value;
+        std::memcpy(&value, &bits, sizeof(value));
+        return value;
+    };
+    const double double_nan = double_from_bits(0x7ff8123456789abc);
+    const double negative_double_nan = double_from_bits(0xfff8abcdef012345);
+    ASSERT_TRUE(std::isnan(double_nan));
+    ASSERT_TRUE(std::isnan(negative_double_nan));
+    ASSERT_OK_AND_ASSIGN(auto double_hash_function, 
FastHash::GetHashFunction(arrow::float64()));
+    CheckResult(double_hash_function, {Literal(double_nan), 
Literal(negative_double_nan)},
+                {0x13d2d3f2cc0e846e, 0x13d2d3f2cc0e846e});
+}
+
 }  // namespace paimon::test
diff --git 
a/src/paimon/common/file_index/bsi/bit_slice_index_bitmap_file_index.cpp 
b/src/paimon/common/file_index/bsi/bit_slice_index_bitmap_file_index.cpp
index 7ce53e9a..4cb98cdc 100644
--- a/src/paimon/common/file_index/bsi/bit_slice_index_bitmap_file_index.cpp
+++ b/src/paimon/common/file_index/bsi/bit_slice_index_bitmap_file_index.cpp
@@ -18,13 +18,23 @@
 
 #include "paimon/common/file_index/bsi/bit_slice_index_bitmap_file_index.h"
 
+#include <algorithm>
 #include <cassert>
 #include <climits>
 #include <cstddef>
 #include <cstdint>
+#include <limits>
+#include <optional>
+#include <utility>
+#include <vector>
 
+#include "arrow/c/bridge.h"
 #include "fmt/format.h"
 #include "paimon/common/file_index/bsi/bit_slice_index_roaring_bitmap.h"
+#include "paimon/common/io/memory_segment_output_stream.h"
+#include "paimon/common/memory/memory_segment_utils.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/date_time_utils.h"
 #include "paimon/common/utils/field_type_utils.h"
@@ -53,6 +63,101 @@ class MemoryPool;
 BitSliceIndexBitmapFileIndex::BitSliceIndexBitmapFileIndex(
     const std::map<std::string, std::string>& options) {}
 
+Result<std::shared_ptr<FileIndexWriter>> 
BitSliceIndexBitmapFileIndex::CreateWriter(
+    ::ArrowSchema* c_arrow_schema, const std::shared_ptr<MemoryPool>& pool) 
const {
+    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Schema> 
arrow_schema,
+                                      arrow::ImportSchema(c_arrow_schema));
+    if (arrow_schema->num_fields() != 1) {
+        return Status::Invalid(
+            "invalid schema for BitSliceIndexBitmapFileIndexWriter, supposed 
to have single "
+            "field.");
+    }
+    const std::shared_ptr<arrow::Field> field = arrow_schema->field(0);
+    PAIMON_ASSIGN_OR_RAISE(ValueMapperType value_mapper, 
GetValueMapper(field->type()));
+    return std::make_shared<BitSliceIndexBitmapFileIndexWriter>(field, 
value_mapper, pool);
+}
+
+BitSliceIndexBitmapFileIndexWriter::BitSliceIndexBitmapFileIndexWriter(
+    const std::shared_ptr<arrow::Field>& field,
+    const BitSliceIndexBitmapFileIndex::ValueMapperType& value_mapper,
+    const std::shared_ptr<MemoryPool>& pool)
+    : struct_type_(arrow::struct_({field})),
+      field_name_(field->name()),
+      value_mapper_(value_mapper),
+      pool_(pool) {}
+
+Status BitSliceIndexBitmapFileIndexWriter::AddBatch(::ArrowArray* batch) {
+    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
+                                      arrow::ImportArray(batch, struct_type_));
+    auto struct_array = checked_pointer_cast<arrow::StructArray>(array);
+    if (struct_array->length() > 
static_cast<int64_t>(std::numeric_limits<int32_t>::max()) -
+                                     static_cast<int64_t>(values_.size())) {
+        return Status::Invalid("bsi index row count exceeds the supported 
int32 range");
+    }
+    PAIMON_ASSIGN_OR_RAISE(
+        std::vector<Literal> literals,
+        LiteralConverter::ConvertLiteralsFromArray(*struct_array->field(0), 
/*own_data=*/false));
+    values_.reserve(values_.size() + literals.size());
+    for (const Literal& literal : literals) {
+        if (literal.IsNull()) {
+            values_.emplace_back(std::nullopt);
+            continue;
+        }
+        PAIMON_ASSIGN_OR_RAISE(int64_t value, value_mapper_(literal));
+        if (value == std::numeric_limits<int64_t>::min()) {
+            return Status::Invalid(
+                fmt::format("bsi index does not support INT64_MIN for field 
'{}'", field_name_));
+        }
+        values_.emplace_back(value);
+        if (value < 0) {
+            const int64_t absolute_value = SafeAbs(value);
+            negative_min_ = std::min(negative_min_, absolute_value);
+            negative_max_ = std::max(negative_max_, absolute_value);
+        } else {
+            positive_min_ = std::min(positive_min_, value);
+            positive_max_ = std::max(positive_max_, value);
+        }
+    }
+    return Status::OK();
+}
+
+Result<PAIMON_UNIQUE_PTR<Bytes>> 
BitSliceIndexBitmapFileIndexWriter::SerializedBytes() const {
+    PAIMON_ASSIGN_OR_RAISE(
+        std::unique_ptr<BitSliceIndexRoaringBitmap::Appender> positive,
+        BitSliceIndexRoaringBitmap::Appender::Create(positive_min_, 
positive_max_));
+    PAIMON_ASSIGN_OR_RAISE(
+        std::unique_ptr<BitSliceIndexRoaringBitmap::Appender> negative,
+        BitSliceIndexRoaringBitmap::Appender::Create(negative_min_, 
negative_max_));
+    for (size_t i = 0; i < values_.size(); ++i) {
+        if (!values_[i]) {
+            continue;
+        }
+        const int64_t value = values_[i].value();
+        if (value < 0) {
+            PAIMON_RETURN_NOT_OK(negative->Append(static_cast<int32_t>(i), 
SafeAbs(value)));
+        } else {
+            PAIMON_RETURN_NOT_OK(positive->Append(static_cast<int32_t>(i), 
value));
+        }
+    }
+
+    MemorySegmentOutputStream 
output_stream(MemorySegmentOutputStream::DEFAULT_SEGMENT_SIZE, pool_);
+    output_stream.SetOrder(ByteOrder::PAIMON_BIG_ENDIAN);
+    output_stream.WriteValue<int8_t>(BitSliceIndexBitmapFileIndex::VERSION_1);
+    output_stream.WriteValue<int32_t>(static_cast<int32_t>(values_.size()));
+    const bool has_positive = positive->IsNotEmpty();
+    output_stream.WriteValue<bool>(has_positive);
+    if (has_positive) {
+        output_stream.WriteBytes(positive->Serialize(pool_));
+    }
+    const bool has_negative = negative->IsNotEmpty();
+    output_stream.WriteValue<bool>(has_negative);
+    if (has_negative) {
+        output_stream.WriteBytes(negative->Serialize(pool_));
+    }
+    return MemorySegmentUtils::CopyToBytes(output_stream.Segments(), 
/*offset=*/0,
+                                           
/*num_bytes=*/output_stream.CurrentSize(), pool_.get());
+}
+
 Result<std::shared_ptr<FileIndexReader>> 
BitSliceIndexBitmapFileIndex::CreateReader(
     ::ArrowSchema* c_arrow_schema, int32_t start, int32_t length,
     const std::shared_ptr<InputStream>& input_stream,
diff --git 
a/src/paimon/common/file_index/bsi/bit_slice_index_bitmap_file_index.h 
b/src/paimon/common/file_index/bsi/bit_slice_index_bitmap_file_index.h
index e661ea2a..d7dfe092 100644
--- a/src/paimon/common/file_index/bsi/bit_slice_index_bitmap_file_index.h
+++ b/src/paimon/common/file_index/bsi/bit_slice_index_bitmap_file_index.h
@@ -22,15 +22,15 @@
 #include <functional>
 #include <map>
 #include <memory>
+#include <optional>
 #include <string>
 #include <utility>
 #include <vector>
 
-#include "arrow/c/bridge.h"
 #include "arrow/type.h"
-#include "paimon/common/utils/arrow/status_utils.h"
 #include "paimon/file_index/file_index_reader.h"
 #include "paimon/file_index/file_index_result.h"
+#include "paimon/file_index/file_index_writer.h"
 #include "paimon/file_index/file_indexer.h"
 #include "paimon/predicate/literal.h"
 #include "paimon/result.h"
@@ -54,14 +54,12 @@ class BitSliceIndexBitmapFileIndex : public FileIndexer {
         const std::shared_ptr<MemoryPool>& pool) const override;
 
     Result<std::shared_ptr<FileIndexWriter>> CreateWriter(
-        ::ArrowSchema* arrow_schema, const std::shared_ptr<MemoryPool>& pool) 
const override {
-        PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::DataType> 
arrow_type,
-                                          arrow::ImportType(arrow_schema));
-        return Status::NotImplemented("do not support index writer in bsi");
-    }
+        ::ArrowSchema* arrow_schema, const std::shared_ptr<MemoryPool>& pool) 
const override;
 
     using ValueMapperType = std::function<Result<int64_t>(const Literal& 
literal)>;
 
+    static constexpr int8_t VERSION_1 = 1;
+
  private:
     static Result<ValueMapperType> GetValueMapper(
         const std::shared_ptr<arrow::DataType>& arrow_type);
@@ -74,9 +72,29 @@ class BitSliceIndexBitmapFileIndex : public FileIndexer {
         }
         return static_cast<int64_t>(literal.GetValue<T>());
     }
+};
+
+class BitSliceIndexBitmapFileIndexWriter : public FileIndexWriter {
+ public:
+    BitSliceIndexBitmapFileIndexWriter(
+        const std::shared_ptr<arrow::Field>& field,
+        const BitSliceIndexBitmapFileIndex::ValueMapperType& value_mapper,
+        const std::shared_ptr<MemoryPool>& pool);
+
+    Status AddBatch(::ArrowArray* batch) override;
+
+    Result<PAIMON_UNIQUE_PTR<Bytes>> SerializedBytes() const override;
 
  private:
-    static constexpr int8_t VERSION_1 = 1;
+    std::shared_ptr<arrow::DataType> struct_type_;
+    std::string field_name_;
+    BitSliceIndexBitmapFileIndex::ValueMapperType value_mapper_;
+    std::vector<std::optional<int64_t>> values_;
+    int64_t positive_min_ = 0;
+    int64_t positive_max_ = 0;
+    int64_t negative_min_ = 0;
+    int64_t negative_max_ = 0;
+    std::shared_ptr<MemoryPool> pool_;
 };
 
 class BitSliceIndexBitmapFileIndexReader
diff --git 
a/src/paimon/common/file_index/bsi/bit_slice_index_bitmap_file_index_test.cpp 
b/src/paimon/common/file_index/bsi/bit_slice_index_bitmap_file_index_test.cpp
index 128c1a69..760434e1 100644
--- 
a/src/paimon/common/file_index/bsi/bit_slice_index_bitmap_file_index_test.cpp
+++ 
b/src/paimon/common/file_index/bsi/bit_slice_index_bitmap_file_index_test.cpp
@@ -19,9 +19,13 @@
 #include "paimon/common/file_index/bsi/bit_slice_index_bitmap_file_index.h"
 
 #include <cstdint>
+#include <string>
 #include <utility>
 
+#include "arrow/c/bridge.h"
+#include "arrow/ipc/json_simple.h"
 #include "gtest/gtest.h"
+#include "paimon/common/utils/arrow/status_utils.h"
 #include "paimon/common/utils/field_type_utils.h"
 #include "paimon/data/timestamp.h"
 #include "paimon/defs.h"
@@ -60,6 +64,24 @@ class BitSliceIndexBitmapIndexReaderTest : public 
::testing::Test {
             << ", expected=" << RoaringBitmap32::From(expected).ToString();
     }
 
+    Result<PAIMON_UNIQUE_PTR<Bytes>> WriteIndex(const 
std::shared_ptr<arrow::DataType>& data_type,
+                                                const std::string& json) const 
{
+        const std::shared_ptr<arrow::Schema> schema =
+            arrow::schema({arrow::field("f0", data_type)});
+        std::shared_ptr<arrow::Array> array =
+            
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(schema->fields()), 
json)
+                .ValueOrDie();
+        ::ArrowSchema c_schema;
+        PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*schema, 
&c_schema));
+        BitSliceIndexBitmapFileIndex file_index({});
+        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<FileIndexWriter> writer,
+                               file_index.CreateWriter(&c_schema, pool_));
+        ::ArrowArray c_array;
+        PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, &c_array));
+        PAIMON_RETURN_NOT_OK(writer->AddBatch(&c_array));
+        return writer->SerializedBytes();
+    }
+
  private:
     std::shared_ptr<MemoryPool> pool_;
 };
@@ -75,48 +97,75 @@ TEST_F(BitSliceIndexBitmapIndexReaderTest, TestMix) {
         0, 0, 0, 2,  58, 48, 0,  0, 1, 0, 0,  0, 0, 0, 2, 0,  16, 0,  0,  0,  
3, 0, 4, 0,  5,
         0, 0, 0, 0,  2,  58, 48, 0, 0, 1, 0,  0, 0, 0, 0, 0,  0,  16, 0,  0,  
0, 5, 0, 58, 48,
         0, 0, 1, 0,  0,  0,  0,  0, 1, 0, 16, 0, 0, 0, 3, 0,  4,  0};
+    const auto check_mix_reader = [this](const 
std::shared_ptr<FileIndexReader>& reader) {
+        // test equal
+        CheckResult(reader->VisitEqual(Literal(2)).value(), {1, 7});
+        CheckResult(reader->VisitEqual(Literal(-2)).value(), {3, 4});
+        CheckResult(reader->VisitEqual(Literal(100)).value(), {});
+
+        // test not equal
+        CheckResult(reader->VisitNotEqual(Literal(2)).value(), {0, 3, 4, 5, 8, 
9});
+        CheckResult(reader->VisitNotEqual(Literal(-2)).value(), {0, 1, 5, 7, 
8, 9});
+        CheckResult(reader->VisitNotEqual(Literal(100)).value(), {0, 1, 3, 4, 
5, 7, 8, 9});
+
+        // test in
+        CheckResult(reader->VisitIn({Literal(-1), Literal(1), Literal(2), 
Literal(3)}).value(),
+                    {0, 1, 5, 7});
+
+        // test not in
+        CheckResult(reader->VisitNotIn({Literal(-1), Literal(1), Literal(2), 
Literal(3)}).value(),
+                    {3, 4, 8, 9});
+
+        // test null
+        CheckResult(reader->VisitIsNull().value(), {2, 6, 10});
+
+        // test not null
+        CheckResult(reader->VisitIsNotNull().value(), {0, 1, 3, 4, 5, 7, 8, 
9});
+
+        // test less than
+        CheckResult(reader->VisitLessThan(Literal(2)).value(), {0, 3, 4, 5, 
8});
+        CheckResult(reader->VisitLessOrEqual(Literal(2)).value(), {0, 1, 3, 4, 
5, 7, 8});
+        CheckResult(reader->VisitLessThan(Literal(-1)).value(), {3, 4});
+        CheckResult(reader->VisitLessOrEqual(Literal(-1)).value(), {3, 4, 5});
+
+        // test greater than
+        CheckResult(reader->VisitGreaterThan(Literal(-2)).value(), {0, 1, 5, 
7, 8, 9});
+        CheckResult(reader->VisitGreaterOrEqual(Literal(-2)).value(), {0, 1, 
3, 4, 5, 7, 8, 9});
+        CheckResult(reader->VisitGreaterThan(Literal(2)).value(), {9});
+        CheckResult(reader->VisitGreaterOrEqual(Literal(2)).value(), {1, 7, 
9});
+    };
+
+    ASSERT_OK_AND_ASSIGN(
+        PAIMON_UNIQUE_PTR<Bytes> written_bytes,
+        WriteIndex(arrow::int32(),
+                   R"([[1], [2], [null], [-2], [-2], [-1], [null], [2], [0], 
[5], [null]])"));
+    auto written_stream =
+        std::make_shared<ByteArrayInputStream>(written_bytes->data(), 
written_bytes->size());
+    BitSliceIndexBitmapFileIndex file_index({});
+    ASSERT_OK_AND_ASSIGN(auto written_reader,
+                         
file_index.CreateReader(CreateArrowSchema(arrow::int32()).get(),
+                                                 /*start=*/0, 
/*length=*/written_bytes->size(),
+                                                 written_stream, pool_));
+    check_mix_reader(written_reader);
+
+    // Reading the Java-produced fixture below. C++ and Java may choose 
different valid portable
+    // roaring containers for the same bitmap, so their complete byte streams 
need not be identical.
     auto input_stream =
         std::make_shared<ByteArrayInputStream>(index_bytes.data(), 
index_bytes.size());
-    BitSliceIndexBitmapFileIndex file_index({});
     ASSERT_OK_AND_ASSIGN(
-        auto reader,
+        auto java_bytes_reader,
         file_index.CreateReader(CreateArrowSchema(arrow::int32()).get(),
                                 /*start=*/0, /*length=*/index_bytes.size(), 
input_stream, pool_));
-    // test equal
-    CheckResult(reader->VisitEqual(Literal(2)).value(), {1, 7});
-    CheckResult(reader->VisitEqual(Literal(-2)).value(), {3, 4});
-    CheckResult(reader->VisitEqual(Literal(100)).value(), {});
-
-    // test not equal
-    CheckResult(reader->VisitNotEqual(Literal(2)).value(), {0, 3, 4, 5, 8, 9});
-    CheckResult(reader->VisitNotEqual(Literal(-2)).value(), {0, 1, 5, 7, 8, 
9});
-    CheckResult(reader->VisitNotEqual(Literal(100)).value(), {0, 1, 3, 4, 5, 
7, 8, 9});
-
-    // test in
-    CheckResult(reader->VisitIn({Literal(-1), Literal(1), Literal(2), 
Literal(3)}).value(),
-                {0, 1, 5, 7});
-
-    // test not in
-    CheckResult(reader->VisitNotIn({Literal(-1), Literal(1), Literal(2), 
Literal(3)}).value(),
-                {3, 4, 8, 9});
-
-    // test null
-    CheckResult(reader->VisitIsNull().value(), {2, 6, 10});
-
-    // test not null
-    CheckResult(reader->VisitIsNotNull().value(), {0, 1, 3, 4, 5, 7, 8, 9});
+    check_mix_reader(java_bytes_reader);
+}
 
-    // test less than
-    CheckResult(reader->VisitLessThan(Literal(2)).value(), {0, 3, 4, 5, 8});
-    CheckResult(reader->VisitLessOrEqual(Literal(2)).value(), {0, 1, 3, 4, 5, 
7, 8});
-    CheckResult(reader->VisitLessThan(Literal(-1)).value(), {3, 4});
-    CheckResult(reader->VisitLessOrEqual(Literal(-1)).value(), {3, 4, 5});
+TEST_F(BitSliceIndexBitmapIndexReaderTest, 
TestWriterRejectsInt64MinAndUnsupportedType) {
+    ASSERT_NOK_WITH_MSG(WriteIndex(arrow::int64(), 
R"([[-9223372036854775808]])"),
+                        "bsi index does not support INT64_MIN for field 'f0'");
 
-    // test greater than
-    CheckResult(reader->VisitGreaterThan(Literal(-2)).value(), {0, 1, 5, 7, 8, 
9});
-    CheckResult(reader->VisitGreaterOrEqual(Literal(-2)).value(), {0, 1, 3, 4, 
5, 7, 8, 9});
-    CheckResult(reader->VisitGreaterThan(Literal(2)).value(), {9});
-    CheckResult(reader->VisitGreaterOrEqual(Literal(2)).value(), {1, 7, 9});
+    BitSliceIndexBitmapFileIndex file_index({});
+    
ASSERT_NOK_WITH_MSG(file_index.CreateWriter(CreateArrowSchema(arrow::boolean()).get(),
 pool_),
+                        "BitSliceIndexBitmapFileIndex only support");
 }
 
 TEST_F(BitSliceIndexBitmapIndexReaderTest, TestPositiveOnly) {
diff --git 
a/src/paimon/common/file_index/rangebitmap/range_bitmap_file_index.cpp 
b/src/paimon/common/file_index/rangebitmap/range_bitmap_file_index.cpp
index 5d0543be..88e081bd 100644
--- a/src/paimon/common/file_index/rangebitmap/range_bitmap_file_index.cpp
+++ b/src/paimon/common/file_index/rangebitmap/range_bitmap_file_index.cpp
@@ -60,18 +60,12 @@ Result<std::shared_ptr<FileIndexWriter>> 
RangeBitmapFileIndex::CreateWriter(
         return Status::Invalid(
             "invalid schema for RangeBitmapFileIndexWriter, supposed to have 
single field.");
     }
-    const auto arrow_field = arrow_schema_ptr->field(0);
-    return RangeBitmapFileIndexWriter::Create(arrow_schema_ptr, 
arrow_field->name(), options_,
-                                              pool);
+    return RangeBitmapFileIndexWriter::Create(arrow_schema_ptr->field(0), 
options_, pool);
 }
 
 Result<std::shared_ptr<RangeBitmapFileIndexWriter>> 
RangeBitmapFileIndexWriter::Create(
-    const std::shared_ptr<arrow::Schema>& arrow_schema, const std::string& 
field_name,
-    const std::map<std::string, std::string>& options, const 
std::shared_ptr<MemoryPool>& pool) {
-    const auto field = arrow_schema->GetFieldByName(field_name);
-    if (!field) {
-        return Status::Invalid(fmt::format("Field not found in schema: {}", 
field_name));
-    }
+    const std::shared_ptr<arrow::Field>& field, const std::map<std::string, 
std::string>& options,
+    const std::shared_ptr<MemoryPool>& pool) {
     PAIMON_ASSIGN_OR_RAISE(FieldType field_type,
                            
FieldTypeUtils::ConvertToFieldType(field->type()->id()));
     PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<KeyFactory> shared_key_factory,
@@ -82,27 +76,18 @@ Result<std::shared_ptr<RangeBitmapFileIndexWriter>> 
RangeBitmapFileIndexWriter::
         chunk_size_it != options.end()) {
         PAIMON_ASSIGN_OR_RAISE(parsed_chunk_size, 
MemorySize::ParseBytes(chunk_size_it->second));
     }
-    auto struct_type = arrow::struct_({field});
+    std::shared_ptr<arrow::DataType> struct_type = arrow::struct_({field});
     PAIMON_ASSIGN_OR_RAISE(
         std::unique_ptr<RangeBitmap::Appender> appender_ptr,
         RangeBitmap::Appender::Create(shared_key_factory, parsed_chunk_size, 
pool));
-    return std::make_shared<RangeBitmapFileIndexWriter>(
-        struct_type, field->type(), options, pool, shared_key_factory, 
std::move(appender_ptr));
+    return std::make_shared<RangeBitmapFileIndexWriter>(struct_type, pool, 
shared_key_factory,
+                                                        
std::move(appender_ptr));
 }
 
 Status RangeBitmapFileIndexWriter::AddBatch(::ArrowArray* batch) {
     PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
                                       arrow::ImportArray(batch, struct_type_));
-    if (!array || array->type_id() != arrow::Type::STRUCT) {
-        return Status::Invalid(
-            "invalid batch for RangeBitmapFileIndexWriter, expected a struct 
array");
-    }
     auto struct_array = checked_pointer_cast<arrow::StructArray>(array);
-    if (struct_array->num_fields() != 1) {
-        return Status::Invalid(
-            "invalid batch for RangeBitmapFileIndexWriter, expected a struct 
array with exactly "
-            "one field");
-    }
     PAIMON_ASSIGN_OR_RAISE(std::vector<Literal> array_values,
                            
LiteralConverter::ConvertLiteralsFromArray(*(struct_array->field(0)),
                                                                       
/*own_data=*/true));
@@ -117,13 +102,9 @@ Result<PAIMON_UNIQUE_PTR<Bytes>> 
RangeBitmapFileIndexWriter::SerializedBytes() c
 }
 
 RangeBitmapFileIndexWriter::RangeBitmapFileIndexWriter(
-    const std::shared_ptr<arrow::DataType>& struct_type,
-    const std::shared_ptr<arrow::DataType>& arrow_type,
-    const std::map<std::string, std::string>& options, const 
std::shared_ptr<MemoryPool>& pool,
+    const std::shared_ptr<arrow::DataType>& struct_type, const 
std::shared_ptr<MemoryPool>& pool,
     const std::shared_ptr<KeyFactory>& key_factory, 
std::unique_ptr<RangeBitmap::Appender> appender)
     : struct_type_(struct_type),
-      arrow_type_(arrow_type),
-      options_(options),
       pool_(pool),
       key_factory_(key_factory),
       appender_(std::move(appender)) {}
diff --git a/src/paimon/common/file_index/rangebitmap/range_bitmap_file_index.h 
b/src/paimon/common/file_index/rangebitmap/range_bitmap_file_index.h
index fc99cac3..64289a53 100644
--- a/src/paimon/common/file_index/rangebitmap/range_bitmap_file_index.h
+++ b/src/paimon/common/file_index/rangebitmap/range_bitmap_file_index.h
@@ -61,15 +61,13 @@ class PAIMON_EXPORT RangeBitmapFileIndex final : public 
FileIndexer {
 class RangeBitmapFileIndexWriter final : public FileIndexWriter {
  public:
     static Result<std::shared_ptr<RangeBitmapFileIndexWriter>> Create(
-        const std::shared_ptr<arrow::Schema>& arrow_schema, const std::string& 
field_name,
+        const std::shared_ptr<arrow::Field>& field,
         const std::map<std::string, std::string>& options, const 
std::shared_ptr<MemoryPool>& pool);
 
     Status AddBatch(::ArrowArray* batch) override;
     Result<PAIMON_UNIQUE_PTR<Bytes>> SerializedBytes() const override;
 
     RangeBitmapFileIndexWriter(const std::shared_ptr<arrow::DataType>& 
struct_type,
-                               const std::shared_ptr<arrow::DataType>& 
arrow_type,
-                               const std::map<std::string, std::string>& 
options,
                                const std::shared_ptr<MemoryPool>& pool,
                                const std::shared_ptr<KeyFactory>& key_factory,
                                std::unique_ptr<RangeBitmap::Appender> 
appender);
@@ -78,8 +76,6 @@ class RangeBitmapFileIndexWriter final : public 
FileIndexWriter {
     /// @note struct_type_ contains only one field with arrow_type_, used for 
import from C
     /// interface.
     std::shared_ptr<arrow::DataType> struct_type_;
-    std::shared_ptr<arrow::DataType> arrow_type_;
-    std::map<std::string, std::string> options_;
     std::shared_ptr<MemoryPool> pool_;
     std::shared_ptr<KeyFactory> key_factory_;
     std::unique_ptr<RangeBitmap::Appender> appender_;
diff --git 
a/src/paimon/common/file_index/rangebitmap/range_bitmap_file_index_test.cpp 
b/src/paimon/common/file_index/rangebitmap/range_bitmap_file_index_test.cpp
index e1479784..6157ee02 100644
--- a/src/paimon/common/file_index/rangebitmap/range_bitmap_file_index_test.cpp
+++ b/src/paimon/common/file_index/rangebitmap/range_bitmap_file_index_test.cpp
@@ -123,17 +123,15 @@ Result<std::shared_ptr<RangeBitmapFileIndexReader>> 
RangeBitmapFileIndexTest::Cr
     std::shared_ptr<arrow::Array> arrow_array;
     PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Finish(&arrow_array));
     // Wrap in StructArray (single field) as required by 
RangeBitmapFileIndexWriter
-    arrow::FieldVector fields = {arrow::field("test_field", arrow_type)};
+    auto field = arrow::field("test_field", arrow_type);
+    arrow::FieldVector fields = {field};
     PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::StructArray> 
struct_array,
                                       arrow::StructArray::Make({arrow_array}, 
fields));
     auto c_array = std::make_unique<::ArrowArray>();
     PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*struct_array, 
c_array.get()));
-    // Create schema for the field
-    const auto schema = arrow::schema({arrow::field("test_field", 
arrow_type)});
     // Create writer
-    PAIMON_ASSIGN_OR_RAISE(
-        std::shared_ptr<RangeBitmapFileIndexWriter> writer,
-        RangeBitmapFileIndexWriter::Create(schema, "test_field", options, 
pool_));
+    PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<RangeBitmapFileIndexWriter> writer,
+                           RangeBitmapFileIndexWriter::Create(field, options, 
pool_));
     // Add the batch
     PAIMON_RETURN_NOT_OK(writer->AddBatch(c_array.get()));
     // Get serialized payload
diff --git a/src/paimon/common/file_index/rangebitmap/range_bitmap_io_test.cpp 
b/src/paimon/common/file_index/rangebitmap/range_bitmap_io_test.cpp
index e2bb867e..fd8e0c88 100644
--- a/src/paimon/common/file_index/rangebitmap/range_bitmap_io_test.cpp
+++ b/src/paimon/common/file_index/rangebitmap/range_bitmap_io_test.cpp
@@ -65,18 +65,16 @@ class RangeBitmapIoTest : public ::testing::Test {
         PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Finish(&arrow_array));
 
         // Wrap in StructArray
-        arrow::FieldVector fields = {arrow::field("test_field", arrow_type)};
+        const std::shared_ptr<arrow::Field> field = arrow::field("test_field", 
arrow_type);
+        arrow::FieldVector fields = {field};
         PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::StructArray> 
struct_array,
                                           
arrow::StructArray::Make({arrow_array}, fields));
         auto c_array = std::make_unique<::ArrowArray>();
         PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*struct_array, 
c_array.get()));
 
-        // Create schema
-        const auto schema = arrow::schema({arrow::field("test_field", 
arrow_type)});
-
         // Create writer and write data
         PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<RangeBitmapFileIndexWriter> 
writer,
-                               RangeBitmapFileIndexWriter::Create(schema, 
"test_field", {}, pool_));
+                               RangeBitmapFileIndexWriter::Create(field, {}, 
pool_));
 
         PAIMON_RETURN_NOT_OK(writer->AddBatch(c_array.get()));
         return writer->SerializedBytes();
diff --git a/src/paimon/common/lookup/lookup_store_factory.cpp 
b/src/paimon/common/lookup/lookup_store_factory.cpp
index c5742e12..d6cb8d40 100644
--- a/src/paimon/common/lookup/lookup_store_factory.cpp
+++ b/src/paimon/common/lookup/lookup_store_factory.cpp
@@ -36,7 +36,8 @@ Result<std::shared_ptr<BloomFilter>> 
LookupStoreFactory::BfGenerator(int64_t row
     if (row_count <= 0 || !options.LookupCacheBloomFilterEnabled()) {
         return std::shared_ptr<BloomFilter>();
     }
-    auto bloom_filter = BloomFilter::Create(row_count, 
options.GetLookupCacheBloomFilterFpp());
+    PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<BloomFilter> bloom_filter,
+                           BloomFilter::Create(row_count, 
options.GetLookupCacheBloomFilterFpp()));
     MemorySegment memory_segment =
         MemorySegment::AllocateHeapMemory(bloom_filter->ByteLength(), pool);
     PAIMON_RETURN_NOT_OK(bloom_filter->SetMemorySegment(memory_segment));
diff --git a/src/paimon/common/sst/sst_file_io_test.cpp 
b/src/paimon/common/sst/sst_file_io_test.cpp
index eac9a8a8..54d4b1b1 100644
--- a/src/paimon/common/sst/sst_file_io_test.cpp
+++ b/src/paimon/common/sst/sst_file_io_test.cpp
@@ -93,7 +93,7 @@ TEST_P(SstFileIOTest, TestSimple) {
                          fs_->Create(index_path, /*overwrite=*/false));
 
     // write data
-    auto bf = BloomFilter::Create(30, 0.01);
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<BloomFilter> bf, 
BloomFilter::Create(30, 0.01));
     auto seg_for_bf = MemorySegment::AllocateHeapMemory(bf->ByteLength(), 
pool_.get());
     ASSERT_OK(bf->SetMemorySegment(seg_for_bf));
     auto writer = std::make_shared<SstFileWriter>(out, bf, 50, factory, pool_);
@@ -234,7 +234,7 @@ TEST_F(SstFileIOTest, TestIOException) {
         CHECK_HOOK_STATUS(out_result.status(), i);
         std::shared_ptr<OutputStream> out = std::move(out_result).value();
 
-        auto bf = BloomFilter::Create(30, 0.01);
+        ASSERT_OK_AND_ASSIGN(std::shared_ptr<BloomFilter> bf, 
BloomFilter::Create(30, 0.01));
         MemorySegment seg_for_bf = 
MemorySegment::AllocateHeapMemory(bf->ByteLength(), pool_.get());
         ASSERT_OK(bf->SetMemorySegment(seg_for_bf));
         auto writer = std::make_shared<SstFileWriter>(out, bf, 50, factory, 
pool_);
diff --git a/src/paimon/common/utils/bloom_filter.cpp 
b/src/paimon/common/utils/bloom_filter.cpp
index d66420f4..ba9fa4b4 100644
--- a/src/paimon/common/utils/bloom_filter.cpp
+++ b/src/paimon/common/utils/bloom_filter.cpp
@@ -25,29 +25,35 @@
 
 namespace paimon {
 
-int32_t BloomFilter::OptimalNumOfBits(int64_t expect_entries, double fpp) {
-    if (expect_entries <= 0 || fpp <= 0.0 || fpp >= 1.0) {
+int32_t BloomFilter::OptimalNumOfBits(int64_t expected_entries, double fpp) {
+    if (expected_entries <= 0 || fpp <= 0.0 || fpp >= 1.0) {
         return 0;
     }
-    double result = -static_cast<double>(expect_entries) * log(fpp) / (log(2) 
* log(2));
+    double result = -static_cast<double>(expected_entries) * log(fpp) / 
(log(2) * log(2));
     if (result > INT32_MAX) return INT32_MAX;
     if (result < 0) return 0;
     return static_cast<int32_t>(result);
 }
 
-int32_t BloomFilter::OptimalNumOfHashFunctions(int64_t expect_entries, int64_t 
bit_size) {
-    if (expect_entries <= 0) {
+int32_t BloomFilter::OptimalNumOfHashFunctions(int64_t expected_entries, 
int64_t bit_size) {
+    if (expected_entries <= 0) {
         return 1;
     }
-    double ratio = static_cast<double>(bit_size) / 
static_cast<double>(expect_entries);
+    double ratio = static_cast<double>(bit_size) / 
static_cast<double>(expected_entries);
     double result = ratio * std::log(2.0);
     return std::max(1, static_cast<int32_t>(std::round(result)));
 }
 
-std::shared_ptr<BloomFilter> BloomFilter::Create(int64_t expect_entries, 
double fpp) {
+Result<std::shared_ptr<BloomFilter>> BloomFilter::Create(int64_t 
expected_entries, double fpp) {
+    if (expected_entries <= 0) {
+        return Status::Invalid("expected entries must be greater than 0 for 
bloom filter");
+    }
+    if (!std::isfinite(fpp) || fpp <= 0.0 || fpp >= 1.0) {
+        return Status::Invalid("fpp must be greater than 0 and less than 1 for 
bloom filter");
+    }
     auto bytes =
-        
static_cast<int32_t>(ceil(BloomFilter::OptimalNumOfBits(expect_entries, fpp) / 
8.0));
-    return std::make_shared<BloomFilter>(expect_entries, bytes);
+        
static_cast<int32_t>(ceil(BloomFilter::OptimalNumOfBits(expected_entries, fpp) 
/ 8.0));
+    return std::make_shared<BloomFilter>(expected_entries, bytes);
 }
 
 BloomFilter::BloomFilter(int64_t expected_entries, int32_t byte_length)
diff --git a/src/paimon/common/utils/bloom_filter.h 
b/src/paimon/common/utils/bloom_filter.h
index 8f1b3271..61e27af5 100644
--- a/src/paimon/common/utils/bloom_filter.h
+++ b/src/paimon/common/utils/bloom_filter.h
@@ -22,7 +22,7 @@
 #include <memory>
 
 #include "paimon/common/utils/bit_set.h"
-#include "paimon/memory/bytes.h"
+#include "paimon/result.h"
 #include "paimon/visibility.h"
 
 namespace paimon {
@@ -30,9 +30,9 @@ namespace paimon {
 /// Bloom filter based on MemorySegment.
 class PAIMON_EXPORT BloomFilter {
  public:
-    static int32_t OptimalNumOfBits(int64_t expect_entries, double fpp);
-    static int32_t OptimalNumOfHashFunctions(int64_t expect_entries, int64_t 
bit_size);
-    static std::shared_ptr<BloomFilter> Create(int64_t expect_entries, double 
fpp);
+    static int32_t OptimalNumOfBits(int64_t expected_entries, double fpp);
+    static int32_t OptimalNumOfHashFunctions(int64_t expected_entries, int64_t 
bit_size);
+    static Result<std::shared_ptr<BloomFilter>> Create(int64_t 
expected_entries, double fpp);
 
  public:
     BloomFilter(int64_t expected_entries, int32_t byte_length);
diff --git a/src/paimon/common/utils/bloom_filter64.cpp 
b/src/paimon/common/utils/bloom_filter64.cpp
index f2685a13..7a975161 100644
--- a/src/paimon/common/utils/bloom_filter64.cpp
+++ b/src/paimon/common/utils/bloom_filter64.cpp
@@ -21,6 +21,8 @@
 #include <algorithm>
 #include <cassert>
 #include <cmath>
+#include <cstring>
+#include <limits>
 #include <utility>
 
 #include "paimon/memory/bytes.h"
@@ -47,17 +49,42 @@ bool BloomFilter64::BitSet::Get(int32_t index) const {
 }
 
 int32_t BloomFilter64::BitSet::BitSize() const {
-    return (bytes_->size() - offset_) * BloomFilter64::BYTE_SIZE;
+    return ByteLength() * BloomFilter64::BYTE_SIZE;
 }
 
-BloomFilter64::BloomFilter64(int64_t items, double fpp, const 
std::shared_ptr<MemoryPool>& pool)
-    : pool_(pool) {
-    auto nb = static_cast<int32_t>(-items * std::log(fpp) / (std::log(2) * 
std::log(2)));
-    num_bits_ = nb + (BloomFilter64::BYTE_SIZE - (nb % 
BloomFilter64::BYTE_SIZE));
-    num_hash_functions_ = std::max(
-        1, static_cast<int32_t>(std::round(static_cast<double>(num_bits_) / 
items * std::log(2))));
-    auto bytes = std::make_shared<Bytes>(num_bits_ / BloomFilter64::BYTE_SIZE, 
pool_.get());
-    bit_set_ = std::make_unique<BitSet>(bytes, /*offset=*/0);
+int32_t BloomFilter64::BitSet::ByteLength() const {
+    return static_cast<int32_t>(bytes_->size() - offset_);
+}
+
+void BloomFilter64::BitSet::ToByteArray(int32_t offset, int32_t length, char* 
bytes) const {
+    assert(bytes);
+    assert(offset >= 0);
+    assert(length >= 0);
+    assert(static_cast<size_t>(offset_ + length) <= bytes_->size());
+    std::memcpy(bytes + offset, bytes_->data() + offset_, length);
+}
+
+Result<BloomFilter64> BloomFilter64::Create(int64_t items, double fpp,
+                                            const std::shared_ptr<MemoryPool>& 
pool) {
+    if (items <= 0) {
+        return Status::Invalid("items must be greater than 0 for bloom 
filter");
+    }
+    if (!std::isfinite(fpp) || fpp <= 0.0 || fpp >= 1.0) {
+        return Status::Invalid("fpp must be greater than 0 and less than 1 for 
bloom filter");
+    }
+    const double log_two = std::log(2);
+    const double estimated_bits = -static_cast<double>(items) * std::log(fpp) 
/ (log_two * log_two);
+    if (estimated_bits > std::numeric_limits<int32_t>::max() - BYTE_SIZE) {
+        return Status::Invalid("bloom filter size exceeds the supported 
range");
+    }
+    const auto num_bits_without_padding = static_cast<int32_t>(estimated_bits);
+    const int32_t num_bits =
+        num_bits_without_padding + (BYTE_SIZE - (num_bits_without_padding % 
BYTE_SIZE));
+    const int32_t num_hash_functions = std::max(
+        1, static_cast<int32_t>(std::round(static_cast<double>(num_bits) / 
items * log_two)));
+    auto bytes = std::make_shared<Bytes>(num_bits / BYTE_SIZE, pool.get());
+    auto bit_set = std::make_unique<BitSet>(bytes, /*offset=*/0);
+    return BloomFilter64(num_hash_functions, std::move(bit_set), pool);
 }
 
 BloomFilter64::BloomFilter64(int32_t num_hash_functions, 
std::unique_ptr<BitSet>&& bit_set)
@@ -65,6 +92,13 @@ BloomFilter64::BloomFilter64(int32_t num_hash_functions, 
std::unique_ptr<BitSet>
       num_hash_functions_(num_hash_functions),
       bit_set_(std::move(bit_set)) {}
 
+BloomFilter64::BloomFilter64(int32_t num_hash_functions, 
std::unique_ptr<BitSet>&& bit_set,
+                             const std::shared_ptr<MemoryPool>& pool)
+    : num_bits_(bit_set->BitSize()),
+      num_hash_functions_(num_hash_functions),
+      pool_(pool),
+      bit_set_(std::move(bit_set)) {}
+
 void BloomFilter64::AddHash(int64_t hash64) {
     auto hash1 = static_cast<int32_t>(hash64);
     auto hash2 = static_cast<int32_t>(static_cast<uint64_t>(hash64) >> 32);
diff --git a/src/paimon/common/utils/bloom_filter64.h 
b/src/paimon/common/utils/bloom_filter64.h
index 3ba4e86f..2440b303 100644
--- a/src/paimon/common/utils/bloom_filter64.h
+++ b/src/paimon/common/utils/bloom_filter64.h
@@ -22,6 +22,7 @@
 #include <memory>
 
 #include "paimon/memory/bytes.h"
+#include "paimon/result.h"
 #include "paimon/visibility.h"
 
 namespace paimon {
@@ -31,7 +32,9 @@ class MemoryPool;
 /// Bloom filter 64 handle 64 bits hash.
 class PAIMON_EXPORT BloomFilter64 {
  public:
-    BloomFilter64(int64_t items, double fpp, const 
std::shared_ptr<MemoryPool>& pool);
+    static Result<BloomFilter64> Create(int64_t items, double fpp,
+                                        const std::shared_ptr<MemoryPool>& 
pool);
+
     class BitSet;
 
     BloomFilter64(int32_t num_hash_functions, std::unique_ptr<BitSet>&& 
bit_set);
@@ -54,6 +57,8 @@ class PAIMON_EXPORT BloomFilter64 {
         void Set(int32_t index);
         bool Get(int32_t index) const;
         int32_t BitSize() const;
+        int32_t ByteLength() const;
+        void ToByteArray(int32_t offset, int32_t length, char* bytes) const;
 
      private:
         static constexpr int8_t MASK = 0x07;
@@ -64,9 +69,11 @@ class PAIMON_EXPORT BloomFilter64 {
     };
 
  private:
+    BloomFilter64(int32_t num_hash_functions, std::unique_ptr<BitSet>&& 
bit_set,
+                  const std::shared_ptr<MemoryPool>& pool);
+
     static constexpr int32_t BYTE_SIZE = 8;
 
- private:
     int32_t num_bits_ = -1;
     int32_t num_hash_functions_ = -1;
     std::shared_ptr<MemoryPool> pool_;
diff --git a/src/paimon/common/utils/bloom_filter64_test.cpp 
b/src/paimon/common/utils/bloom_filter64_test.cpp
index aa548fb7..6f60fd01 100644
--- a/src/paimon/common/utils/bloom_filter64_test.cpp
+++ b/src/paimon/common/utils/bloom_filter64_test.cpp
@@ -28,13 +28,14 @@
 #include "gtest/gtest.h"
 #include "paimon/memory/bytes.h"
 #include "paimon/memory/memory_pool.h"
+#include "paimon/testing/utils/testharness.h"
 
 namespace paimon::test {
 
 TEST(BloomFilter64Test, TestSimple) {
     int32_t items = 10000;
     auto pool = GetDefaultPool();
-    BloomFilter64 bloom_filter(items, 0.02, pool);
+    ASSERT_OK_AND_ASSIGN(BloomFilter64 bloom_filter, 
BloomFilter64::Create(items, 0.02, pool));
     std::mt19937_64 engine(std::random_device{}());  // 
NOLINT(whitespace/braces)
     std::uniform_int_distribution<int64_t> 
distribution(std::numeric_limits<int64_t>::min(),
                                                         
std::numeric_limits<int64_t>::max());
@@ -61,6 +62,39 @@ TEST(BloomFilter64Test, TestSimple) {
     ASSERT_TRUE(static_cast<double>(false_positives) / num < 0.03);
 }
 
+TEST(BloomFilter64Test, TestInvalidItemsAndFpp) {
+    std::shared_ptr<MemoryPool> pool = GetDefaultPool();
+
+    ASSERT_NOK_WITH_MSG(BloomFilter64::Create(/*items=*/0, /*fpp=*/0.1, pool),
+                        "items must be greater than 0");
+    ASSERT_NOK_WITH_MSG(BloomFilter64::Create(/*items=*/-1, /*fpp=*/0.1, pool),
+                        "items must be greater than 0");
+    ASSERT_NOK_WITH_MSG(BloomFilter64::Create(/*items=*/100, /*fpp=*/0.0, 
pool),
+                        "fpp must be greater than 0 and less than 1");
+    ASSERT_NOK_WITH_MSG(BloomFilter64::Create(/*items=*/100, /*fpp=*/-0.1, 
pool),
+                        "fpp must be greater than 0 and less than 1");
+    ASSERT_NOK_WITH_MSG(BloomFilter64::Create(/*items=*/100, /*fpp=*/1.0, 
pool),
+                        "fpp must be greater than 0 and less than 1");
+    ASSERT_NOK_WITH_MSG(BloomFilter64::Create(/*items=*/100, /*fpp=*/1.1, 
pool),
+                        "fpp must be greater than 0 and less than 1");
+    ASSERT_NOK_WITH_MSG(
+        BloomFilter64::Create(/*items=*/std::numeric_limits<int64_t>::max(), 
/*fpp=*/0.1, pool),
+        "bloom filter size exceeds the supported range");
+}
+
+TEST(BloomFilter64Test, TestKeepMemoryPoolAlive) {
+    std::weak_ptr<MemoryPool> weak_pool;
+    {
+        std::shared_ptr<MemoryPool> pool(GetMemoryPool());
+        weak_pool = pool;
+        ASSERT_OK_AND_ASSIGN(BloomFilter64 bloom_filter,
+                             BloomFilter64::Create(/*items=*/100, /*fpp=*/0.1, 
pool));
+        pool.reset();
+        ASSERT_FALSE(weak_pool.expired());
+    }
+    ASSERT_TRUE(weak_pool.expired());
+}
+
 TEST(BloomFilter64Test, TestCompatibleWithJava) {
     // data: -10, -5, 0, 13, 100, 200, 500
     std::vector<uint8_t> se_bytes = {241, 255, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0};
@@ -74,10 +108,10 @@ TEST(BloomFilter64Test, TestCompatibleWithJava) {
         ASSERT_TRUE(bloom_filter.TestHash(value));
     }
 
-    BloomFilter64 bloom_filter2(10, 0.01, pool);
+    ASSERT_OK_AND_ASSIGN(BloomFilter64 bloom_filter2, 
BloomFilter64::Create(10, 0.01, pool));
     ASSERT_EQ(7, bloom_filter2.GetNumHashFunctions());
-    ASSERT_EQ(se_bytes.size() * BloomFilter64::BYTE_SIZE, 
bloom_filter2.num_bits_);
-    ASSERT_EQ(se_bytes.size(), bloom_filter2.GetBitSet().bytes_->size());
+    ASSERT_EQ(se_bytes.size() * 8, bloom_filter2.GetBitSet().BitSize());
+    ASSERT_EQ(se_bytes.size(), bloom_filter2.GetBitSet().ByteLength());
 }
 
 }  // namespace paimon::test
diff --git a/src/paimon/common/utils/bloom_filter_test.cpp 
b/src/paimon/common/utils/bloom_filter_test.cpp
index fdd6abb3..0832fdf5 100644
--- a/src/paimon/common/utils/bloom_filter_test.cpp
+++ b/src/paimon/common/utils/bloom_filter_test.cpp
@@ -35,7 +35,8 @@ namespace paimon::test {
 TEST(BloomFilterTest, TestOneSegmentBuilder) {
     int32_t items = 100;
     auto pool = GetDefaultPool();
-    auto bloom_filter = BloomFilter::Create(items, 0.01);
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<BloomFilter> bloom_filter,
+                         BloomFilter::Create(items, 0.01));
     auto seg = MemorySegment::AllocateHeapMemory(1024, pool.get());
     ASSERT_OK(bloom_filter->SetMemorySegment(seg));
 
@@ -54,12 +55,32 @@ TEST(BloomFilterTest, TestOneSegmentBuilder) {
 }
 
 TEST(BloomFilterTest, TestEstimatedHashFunctions) {
-    ASSERT_EQ(7, BloomFilter::Create(1000, 0.01)->GetNumHashFunctions());
-    ASSERT_EQ(7, BloomFilter::Create(10000, 0.01)->GetNumHashFunctions());
-    ASSERT_EQ(7, BloomFilter::Create(100000, 0.01)->GetNumHashFunctions());
-    ASSERT_EQ(4, BloomFilter::Create(100000, 0.05)->GetNumHashFunctions());
-    ASSERT_EQ(7, BloomFilter::Create(1000000, 0.01)->GetNumHashFunctions());
-    ASSERT_EQ(4, BloomFilter::Create(1000000, 0.05)->GetNumHashFunctions());
+    auto get_num_hash_functions = [](int64_t expected_entries, double fpp) {
+        EXPECT_OK_AND_ASSIGN(std::shared_ptr<BloomFilter> bloom_filter,
+                             BloomFilter::Create(expected_entries, fpp));
+        return bloom_filter->GetNumHashFunctions();
+    };
+    ASSERT_EQ(7, get_num_hash_functions(1000, 0.01));
+    ASSERT_EQ(7, get_num_hash_functions(10000, 0.01));
+    ASSERT_EQ(7, get_num_hash_functions(100000, 0.01));
+    ASSERT_EQ(4, get_num_hash_functions(100000, 0.05));
+    ASSERT_EQ(7, get_num_hash_functions(1000000, 0.01));
+    ASSERT_EQ(4, get_num_hash_functions(1000000, 0.05));
+}
+
+TEST(BloomFilterTest, TestInvalidExpectedEntriesAndFpp) {
+    ASSERT_NOK_WITH_MSG(BloomFilter::Create(/*expected_entries=*/0, 
/*fpp=*/0.1),
+                        "expected entries must be greater than 0");
+    ASSERT_NOK_WITH_MSG(BloomFilter::Create(/*expected_entries=*/-1, 
/*fpp=*/0.1),
+                        "expected entries must be greater than 0");
+    ASSERT_NOK_WITH_MSG(BloomFilter::Create(/*expected_entries=*/100, 
/*fpp=*/0.0),
+                        "fpp must be greater than 0 and less than 1");
+    ASSERT_NOK_WITH_MSG(BloomFilter::Create(/*expected_entries=*/100, 
/*fpp=*/-0.1),
+                        "fpp must be greater than 0 and less than 1");
+    ASSERT_NOK_WITH_MSG(BloomFilter::Create(/*expected_entries=*/100, 
/*fpp=*/1.0),
+                        "fpp must be greater than 0 and less than 1");
+    ASSERT_NOK_WITH_MSG(BloomFilter::Create(/*expected_entries=*/100, 
/*fpp=*/1.1),
+                        "fpp must be greater than 0 and less than 1");
 }
 
 TEST(BloomFilterTest, TestBloomNumBits) {
diff --git a/src/paimon/common/utils/math.h b/src/paimon/common/utils/math.h
index 54ad6cf7..6aba6523 100644
--- a/src/paimon/common/utils/math.h
+++ b/src/paimon/common/utils/math.h
@@ -36,6 +36,7 @@
 
 #include "fmt/format.h"
 #include "paimon/common/utils/options_utils.h"
+#include "paimon/io/byte_order.h"
 #include "paimon/status.h"
 
 namespace paimon {
@@ -136,4 +137,30 @@ inline T EndianSwapValue(T v) {
     }
 }
 
+template <typename T>
+inline T ToBigEndian(T value) {
+    if constexpr (SystemByteOrder() == ByteOrder::PAIMON_LITTLE_ENDIAN) {
+        return EndianSwapValue(value);
+    }
+    return value;
+}
+
+template <typename T>
+inline T ToLittleEndian(T value) {
+    if constexpr (SystemByteOrder() == ByteOrder::PAIMON_BIG_ENDIAN) {
+        return EndianSwapValue(value);
+    }
+    return value;
+}
+
+template <typename T>
+inline T FromBigEndian(T value) {
+    return ToBigEndian(value);
+}
+
+template <typename T>
+inline T FromLittleEndian(T value) {
+    return ToLittleEndian(value);
+}
+
 }  // namespace paimon
diff --git a/src/paimon/common/utils/math_test.cpp 
b/src/paimon/common/utils/math_test.cpp
index 36df1cba..49d31d47 100644
--- a/src/paimon/common/utils/math_test.cpp
+++ b/src/paimon/common/utils/math_test.cpp
@@ -19,6 +19,8 @@
 
 #include "paimon/common/utils/math.h"
 
+#include <array>
+#include <cstring>
 #include <limits>
 
 #include "gtest/gtest.h"
@@ -44,6 +46,22 @@ TEST(MathTest, EndianSwapValue) {
     ASSERT_EQ(swapped64, 0xF0DEBC9A78563412);
 }
 
+TEST(MathTest, ToEndian) {
+    constexpr uint32_t kValue = 0x12345678;
+
+    const uint32_t big_endian = ToBigEndian(kValue);
+    std::array<uint8_t, sizeof(big_endian)> big_endian_bytes{};
+    std::memcpy(big_endian_bytes.data(), &big_endian, sizeof(big_endian));
+    ASSERT_EQ((std::array<uint8_t, 4>{0x12, 0x34, 0x56, 0x78}), 
big_endian_bytes);
+    ASSERT_EQ(kValue, FromBigEndian(big_endian));
+
+    const uint32_t little_endian = ToLittleEndian(kValue);
+    std::array<uint8_t, sizeof(little_endian)> little_endian_bytes{};
+    std::memcpy(little_endian_bytes.data(), &little_endian, 
sizeof(little_endian));
+    ASSERT_EQ((std::array<uint8_t, 4>{0x78, 0x56, 0x34, 0x12}), 
little_endian_bytes);
+    ASSERT_EQ(kValue, FromLittleEndian(little_endian));
+}
+
 TEST(MathTest, InRange) {
     // signed -> unsigned: negative values out of range, boundary values in 
range
     ASSERT_TRUE(InRange<uint32_t>(0));
diff --git a/src/paimon/core/io/data_file_index_writer_test.cpp 
b/src/paimon/core/io/data_file_index_writer_test.cpp
index e9ab7940..1e2c9593 100644
--- a/src/paimon/core/io/data_file_index_writer_test.cpp
+++ b/src/paimon/core/io/data_file_index_writer_test.cpp
@@ -197,11 +197,38 @@ TEST_F(DataFileIndexWriterTest, 
TestExternalIndexAndAbortCleanup) {
     ASSERT_FALSE(exists);
 }
 
+TEST_F(DataFileIndexWriterTest, TestBsiAndBloomFilterEmbeddedRoundTrip) {
+    ASSERT_OK_AND_ASSIGN(auto writer,
+                         CreateWriter({{"file-index.bsi.columns", "f0"},
+                                       {"file-index.bloom-filter.columns", 
"f1"},
+                                       {"file-index.bloom-filter.f1.items", 
"100"},
+                                       {"file-index.bloom-filter.f1.fpp", 
"0.01"},
+                                       
{Options::FILE_INDEX_IN_MANIFEST_THRESHOLD, "1MB"}}));
+    ASSERT_OK(writer->AddBatch(CreateBatch(R"([{"f0": 1, "f1": 10},
+                                                {"f0": -2, "f1": 20}])")));
+    ASSERT_OK(writer->AddBatch(CreateBatch(R"([{"f0": null, "f1": 30},
+                                                {"f0": 5, "f1": 40}])")));
+
+    ASSERT_OK_AND_ASSIGN(FileIndexWriteResult result, 
writer->Finish("unused.orc"));
+    ASSERT_TRUE(result.embedded_index);
+    ASSERT_OK_AND_ASSIGN(auto reader, CreateReader(result.embedded_index));
+
+    ASSERT_OK_AND_ASSIGN(auto bsi_readers, ReadColumn(reader.get(), "f0"));
+    ASSERT_EQ(1, bsi_readers.size());
+    ASSERT_OK_AND_ASSIGN(auto greater_result, 
bsi_readers[0]->VisitGreaterThan(Literal(1)));
+    ASSERT_EQ("{3}", greater_result->ToString());
+    ASSERT_OK_AND_ASSIGN(auto null_result, bsi_readers[0]->VisitIsNull());
+    ASSERT_EQ("{2}", null_result->ToString());
+
+    ASSERT_OK_AND_ASSIGN(auto bloom_readers, ReadColumn(reader.get(), "f1"));
+    ASSERT_EQ(1, bloom_readers.size());
+    ASSERT_OK_AND_ASSIGN(auto present_result, 
bloom_readers[0]->VisitEqual(Literal(30)));
+    ASSERT_TRUE(present_result->IsRemain().value());
+}
+
 TEST_F(DataFileIndexWriterTest, TestUnavailableWriterFailsCreation) {
     ASSERT_NOK_WITH_MSG(CreateWriter({{"file-index.unknown.columns", "f0"}}),
                         "File index type 'unknown' is not registered");
-    ASSERT_NOK_WITH_MSG(CreateWriter({{"file-index.bloom-filter.columns", 
"f0"}}),
-                        "do not support index writer in bloom filter");
 }
 
 TEST_F(DataFileIndexWriterTest, TestRejectSystemFieldIndex) {

Reply via email to