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 cecd6cd0 feat(deletion-vectors): support bitmap64 deletion vectors 
(#423)
cecd6cd0 is described below

commit cecd6cd05a5b72b2c71c5b311e0f8212aba0b2c0
Author: lszskye <[email protected]>
AuthorDate: Tue Oct 13 15:30:15 2026 +0800

    feat(deletion-vectors): support bitmap64 deletion vectors (#423)
---
 docs/source/user_guide/read.rst                    |   6 +-
 include/paimon/defs.h                              |   1 -
 src/paimon/CMakeLists.txt                          |   2 +
 .../apply_deletion_vector_batch_reader_test.cpp    |  17 +-
 .../deletionvectors/bitmap64_deletion_vector.cpp   |  86 ++++
 .../deletionvectors/bitmap64_deletion_vector.h     |  56 +++
 .../bitmap64_deletion_vector_test.cpp              | 286 +++++++++++
 .../deletionvectors/bitmap_deletion_vector.cpp     |   8 +-
 .../core/deletionvectors/bitmap_deletion_vector.h  |   7 +
 .../bitmap_deletion_vector_test.cpp                |  27 +-
 .../core/deletionvectors/bucketed_dv_maintainer.h  |  10 +-
 .../bucketed_dv_maintainer_test.cpp                |  41 +-
 .../core/deletionvectors/deletion_vector.cpp       |  41 +-
 src/paimon/core/deletionvectors/deletion_vector.h  |  11 +-
 .../deletion_vector_index_file_writer_test.cpp     |   4 +
 .../core/deletionvectors/deletion_vector_test.cpp  |  64 ++-
 .../deletion_vectors_index_file.cpp                |   1 +
 .../deletion_vectors_index_file_test.cpp           |  14 +
 src/paimon/core/io/file_index_evaluator.cpp        |   3 +-
 src/paimon/core/io/file_index_evaluator_test.cpp   |  26 +
 src/paimon/core/operation/abstract_split_read.cpp  |  18 +-
 .../core/operation/data_evolution_split_read.cpp   |  13 +-
 .../operation/data_evolution_split_read_test.cpp   |  23 +
 .../core/operation/raw_file_split_read_test.cpp    |  42 +-
 .../testing/utils/deletion_vector_test_helper.h    |   8 +-
 test/inte/data_evolution_table_test.cpp            |  52 +-
 test/inte/pk_compaction_inte_test.cpp              | 551 +++++++++++++++++----
 27 files changed, 1202 insertions(+), 216 deletions(-)

diff --git a/docs/source/user_guide/read.rst b/docs/source/user_guide/read.rst
index adebaecb..a9477635 100644
--- a/docs/source/user_guide/read.rst
+++ b/docs/source/user_guide/read.rst
@@ -318,6 +318,10 @@ A data-evolution table (``data-evolution.enabled = true``) 
may enable
 ``deletion-vectors.enabled``. Reading such a table is supported: a deleted row 
disappears
 from the result, including from the columns merged out of the other files that 
cover it.
 
+Both Java-compatible 32-bit and 64-bit deletion vectors can be read, including 
mixed
+formats in one index file. ``deletion-vectors.bitmap64 = true`` selects the 
format of new
+deletion vectors on supported write paths; existing vectors retain their 
format.
+
 How the Deletion Vector Is Located
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -340,8 +344,6 @@ come back.
 Limitations
 ~~~~~~~~~~~
 
-- Only the default 32-bit deletion vectors can be read. 
``deletion-vectors.bitmap64`` is not
-  supported yet, and a read fails when it actually encounters a 64-bit 
deletion vector.
 - Paimon C++ does not write deletion vectors for data-evolution tables, so the 
deletes
   themselves have to be issued by another engine.
 - A commit that drops data files from such a table, an overwrite for instance, 
is refused.
diff --git a/include/paimon/defs.h b/include/paimon/defs.h
index d6abd047..7b473240 100644
--- a/include/paimon/defs.h
+++ b/include/paimon/defs.h
@@ -408,7 +408,6 @@ struct PAIMON_EXPORT Options {
 
     /// "deletion-vectors.bitmap64" - Enable 64 bit bitmap implementation. 
Note that only 64 bit
     /// bitmap implementation is compatible with Iceberg. Default value is 
"false".
-    /// @note: bitmap64 dv is not supported.
     static const char DELETION_VECTOR_BITMAP64[];
 
     /// "changelog-producer" - Whether to double write to a changelog file. 
This changelog file
diff --git a/src/paimon/CMakeLists.txt b/src/paimon/CMakeLists.txt
index ddd0118e..79305ce4 100644
--- a/src/paimon/CMakeLists.txt
+++ b/src/paimon/CMakeLists.txt
@@ -263,6 +263,7 @@ set(PAIMON_CORE_SRCS
     core/deletionvectors/deletion_vector.cpp
     core/deletionvectors/deletion_file_writer.cpp
     core/deletionvectors/bitmap_deletion_vector.cpp
+    core/deletionvectors/bitmap64_deletion_vector.cpp
     core/deletionvectors/deletion_vectors_index_file.cpp
     core/deletionvectors/deletion_vector_index_file_writer.cpp
     core/global_index/global_index_evaluator_impl.cpp
@@ -795,6 +796,7 @@ if(PAIMON_BUILD_TESTS)
                     core/options/table_type_test.cpp
                     
core/deletionvectors/apply_deletion_vector_batch_reader_test.cpp
                     core/deletionvectors/bitmap_deletion_vector_test.cpp
+                    core/deletionvectors/bitmap64_deletion_vector_test.cpp
                     core/deletionvectors/bucketed_dv_maintainer_test.cpp
                     core/deletionvectors/deletion_file_writer_test.cpp
                     core/deletionvectors/deletion_vector_test.cpp
diff --git 
a/src/paimon/core/deletionvectors/apply_deletion_vector_batch_reader_test.cpp 
b/src/paimon/core/deletionvectors/apply_deletion_vector_batch_reader_test.cpp
index 2e7b29b9..8384f485 100644
--- 
a/src/paimon/core/deletionvectors/apply_deletion_vector_batch_reader_test.cpp
+++ 
b/src/paimon/core/deletionvectors/apply_deletion_vector_batch_reader_test.cpp
@@ -28,6 +28,7 @@
 #include "paimon/common/reader/prefetch_file_batch_reader_impl.h"
 #include "paimon/common/utils/arrow/mem_utils.h"
 #include "paimon/common/utils/read_ahead_cache.h"
+#include "paimon/core/deletionvectors/bitmap64_deletion_vector.h"
 #include "paimon/executor.h"
 #include "paimon/testing/mock/mock_file_batch_reader.h"
 #include "paimon/testing/mock/mock_file_system.h"
@@ -43,8 +44,9 @@ class FileSystem;
 }  // namespace paimon
 
 namespace paimon::test {
-class ApplyDeletionVectorBatchReaderTest : public ::testing::Test,
-                                           public 
::testing::WithParamInterface<bool> {
+class ApplyDeletionVectorBatchReaderTest
+    : public ::testing::Test,
+      public ::testing::WithParamInterface<std::tuple<bool, bool>> {
  public:
     void SetUp() override {
         int_type_ = arrow::int32();
@@ -77,9 +79,14 @@ class ApplyDeletionVectorBatchReaderTest : public 
::testing::Test,
 
         int32_t prefetch_batch_count = 3;
         for (int32_t batch_size : {1, 2, 4, 10}) {
-            auto dv = DeletionVector::FromPrimitiveArray(dv_data, pool.get());
+            std::shared_ptr<DeletionVector> dv =
+                DeletionVector::FromPrimitiveArray(dv_data, pool.get());
+            if (std::get<1>(GetParam())) {
+                dv = Bitmap64DeletionVector::FromBitmapDeletionVector(
+                    *static_cast<BitmapDeletionVector*>(dv.get()));
+            }
             std::unique_ptr<FileBatchReader> file_batch_reader;
-            bool enable_prefetch = GetParam();
+            bool enable_prefetch = std::get<0>(GetParam());
             if (enable_prefetch) {
                 MockFormatReaderBuilder reader_builder(data, target_type_, 
batch_size);
                 ASSERT_OK_AND_ASSIGN(
@@ -182,5 +189,5 @@ TEST(ApplyDeletionVectorBatchReaderWarmupTest, 
WarmupForwardsToInnerReader) {
 }
 
 INSTANTIATE_TEST_SUITE_P(EnablePrefetch, ApplyDeletionVectorBatchReaderTest,
-                         ::testing::Values(false, true));
+                         ::testing::Combine(::testing::Bool(), 
::testing::Bool()));
 }  // namespace paimon::test
diff --git a/src/paimon/core/deletionvectors/bitmap64_deletion_vector.cpp 
b/src/paimon/core/deletionvectors/bitmap64_deletion_vector.cpp
new file mode 100644
index 00000000..61514470
--- /dev/null
+++ b/src/paimon/core/deletionvectors/bitmap64_deletion_vector.cpp
@@ -0,0 +1,86 @@
+/*
+ * 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/core/deletionvectors/bitmap64_deletion_vector.h"
+
+#include <cstring>
+#include <limits>
+
+#include "arrow/util/crc32.h"
+#include "paimon/common/io/data_output_stream.h"
+#include "paimon/io/byte_order.h"
+
+namespace paimon {
+
+std::unique_ptr<Bitmap64DeletionVector> 
Bitmap64DeletionVector::FromBitmapDeletionVector(
+    const BitmapDeletionVector& dv) {
+    auto result = std::make_unique<Bitmap64DeletionVector>();
+    result->roaring_bitmap_ = 
OptimizedRoaringBitmap64::FromRoaringBitmap32(*dv.GetBitmap());
+    return result;
+}
+
+Status Bitmap64DeletionVector::Merge(const std::shared_ptr<DeletionVector>& 
deletion_vector) {
+    auto* other = dynamic_cast<Bitmap64DeletionVector*>(deletion_vector.get());
+    if (!other) {
+        return Status::Invalid(
+            "Cannot merge a non-Bitmap64DeletionVector into a 
Bitmap64DeletionVector");
+    }
+    roaring_bitmap_ |= other->roaring_bitmap_;
+    return Status::OK();
+}
+
+Result<PAIMON_UNIQUE_PTR<Bytes>> Bitmap64DeletionVector::SerializeToBytes(
+    const std::shared_ptr<MemoryPool>& pool) {
+    roaring_bitmap_.RunLengthEncode();
+    size_t bitmap_size = roaring_bitmap_.GetSizeInBytes();
+    constexpr int32_t overhead = MAGIC_NUMBER_SIZE_BYTES + LENGTH_SIZE_BYTES + 
CRC_SIZE_BYTES;
+    if (bitmap_size > static_cast<size_t>(std::numeric_limits<int32_t>::max() 
- overhead)) {
+        return Status::Invalid("Cannot serialize deletion vector > 2GB");
+    }
+    auto bitmap = roaring_bitmap_.Serialize(pool.get());
+    auto bytes = Bytes::AllocateBytes(MAGIC_NUMBER_SIZE_BYTES + bitmap_size, 
pool.get());
+    int32_t magic = ToLittleEndian(MAGIC_NUMBER);
+    std::memcpy(bytes->data(), &magic, MAGIC_NUMBER_SIZE_BYTES);
+    std::memcpy(bytes->data() + MAGIC_NUMBER_SIZE_BYTES, bitmap->data(), 
bitmap_size);
+    return bytes;
+}
+
+Result<int32_t> Bitmap64DeletionVector::SerializeTo(const 
std::shared_ptr<MemoryPool>& pool,
+                                                    DataOutputStream* out) {
+    PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Bytes> bytes, 
SerializeToBytes(pool));
+    auto length = static_cast<int32_t>(bytes->size());
+    PAIMON_RETURN_NOT_OK(out->WriteValue<int32_t>(length));
+    PAIMON_RETURN_NOT_OK(out->WriteBytes(bytes));
+    uint32_t crc = arrow::internal::crc32(0, bytes->data(), bytes->size());
+    PAIMON_RETURN_NOT_OK(out->WriteValue<int32_t>(static_cast<int32_t>(crc)));
+    // Unlike DV32, Java DV64 metadata includes the length and CRC fields.
+    return length + LENGTH_SIZE_BYTES + CRC_SIZE_BYTES;
+}
+
+Result<PAIMON_UNIQUE_PTR<DeletionVector>> 
Bitmap64DeletionVector::DeserializeWithoutMagicNumber(
+    const char* buffer, int32_t length, MemoryPool* pool) {
+    // Even an empty portable bitmap has an eight-byte bucket count.
+    if (length < static_cast<int32_t>(sizeof(int64_t))) {
+        return Status::Invalid("Invalid bitmap64 payload length");
+    }
+    auto result = pool->AllocateUnique<Bitmap64DeletionVector>();
+    PAIMON_RETURN_NOT_OK(result->roaring_bitmap_.Deserialize(buffer, length));
+    return result;
+}
+
+}  // namespace paimon
diff --git a/src/paimon/core/deletionvectors/bitmap64_deletion_vector.h 
b/src/paimon/core/deletionvectors/bitmap64_deletion_vector.h
index 7ec827d5..061af459 100644
--- a/src/paimon/core/deletionvectors/bitmap64_deletion_vector.h
+++ b/src/paimon/core/deletionvectors/bitmap64_deletion_vector.h
@@ -18,13 +18,69 @@
  */
 #pragma once
 
+#include "paimon/common/utils/optimized_roaring_bitmap64.h"
+#include "paimon/core/deletionvectors/bitmap_deletion_vector.h"
 #include "paimon/core/deletionvectors/deletion_vector.h"
 
 namespace paimon {
 
+/// A `DeletionVector` based on `OptimizedRoaringBitmap64`, it only supports 
files with
+/// row count not exceeding `OptimizedRoaringBitmap64::kMaxValue`.
+///
+/// Mostly copied from iceberg.
 class Bitmap64DeletionVector : public DeletionVector {
  public:
     static constexpr int32_t MAGIC_NUMBER = 1681511377;
+    static constexpr int32_t MAGIC_NUMBER_SIZE_BYTES = 4;
+    static constexpr int32_t LENGTH_SIZE_BYTES = 4;
+    static constexpr int32_t CRC_SIZE_BYTES = 4;
+
+    static std::unique_ptr<Bitmap64DeletionVector> FromBitmapDeletionVector(
+        const BitmapDeletionVector& dv);
+
+    Status Delete(int64_t position) override {
+        return roaring_bitmap_.Add(position);
+    }
+
+    Result<bool> CheckedDelete(int64_t position) override {
+        PAIMON_ASSIGN_OR_RAISE(bool deleted, IsDeleted(position));
+        if (deleted) {
+            return false;
+        }
+        PAIMON_RETURN_NOT_OK(Delete(position));
+        return true;
+    }
+
+    Result<bool> IsDeleted(int64_t position) const override {
+        return roaring_bitmap_.Contains(position);
+    }
+
+    bool IsEmpty() const override {
+        return roaring_bitmap_.IsEmpty();
+    }
+
+    Result<int64_t> GetCardinality() const override {
+        return roaring_bitmap_.Cardinality();
+    }
+
+    Status ForEachDeletedPosition(const std::function<void(int64_t)>& 
consumer) const override {
+        roaring_bitmap_.ForEach(consumer);
+        return Status::OK();
+    }
+
+    Status Merge(const std::shared_ptr<DeletionVector>& deletion_vector) 
override;
+
+    Result<int32_t> SerializeTo(const std::shared_ptr<MemoryPool>& pool,
+                                DataOutputStream* out) override;
+
+    Result<PAIMON_UNIQUE_PTR<Bytes>> SerializeToBytes(
+        const std::shared_ptr<MemoryPool>& pool) override;
+
+    static Result<PAIMON_UNIQUE_PTR<DeletionVector>> 
DeserializeWithoutMagicNumber(
+        const char* buffer, int32_t length, MemoryPool* pool);
+
+ private:
+    OptimizedRoaringBitmap64 roaring_bitmap_;
 };
 
 }  // namespace paimon
diff --git a/src/paimon/core/deletionvectors/bitmap64_deletion_vector_test.cpp 
b/src/paimon/core/deletionvectors/bitmap64_deletion_vector_test.cpp
new file mode 100644
index 00000000..57c20912
--- /dev/null
+++ b/src/paimon/core/deletionvectors/bitmap64_deletion_vector_test.cpp
@@ -0,0 +1,286 @@
+/*
+ * 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/core/deletionvectors/bitmap64_deletion_vector.h"
+
+#include <set>
+
+#include "gtest/gtest.h"
+#include "paimon/common/io/byte_array_output_stream.h"
+#include "paimon/common/io/data_output_stream.h"
+#include "paimon/common/utils/path_util.h"
+#include "paimon/fs/file_system_factory.h"
+#include "paimon/io/byte_array_input_stream.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+
+TEST(Bitmap64DeletionVectorTest, TestBasicOperations) {
+    Bitmap64DeletionVector dv;
+    ASSERT_TRUE(dv.IsEmpty());
+    std::vector<int64_t> expected;
+    for (const auto& base : {0LL, 1LL << 31, 1LL << 32, 2LL << 32}) {
+        for (int64_t i = 0; i < 2000; i += 2) {
+            ASSERT_OK(dv.Delete(base + i));
+            expected.push_back(base + i);
+        }
+        for (int64_t i = 0; i < 2000; ++i) {
+            ASSERT_EQ(dv.IsDeleted(base + i).value(), i % 2 == 0);
+        }
+    }
+    ASSERT_FALSE(dv.IsEmpty());
+    ASSERT_EQ(dv.GetCardinality().value(), expected.size());
+    std::vector<int64_t> actual;
+    ASSERT_OK(dv.ForEachDeletedPosition([&](int64_t p) { actual.push_back(p); 
}));
+    ASSERT_EQ(actual, expected);
+}
+
+TEST(Bitmap64DeletionVectorTest, TestCheckedDelete) {
+    Bitmap64DeletionVector dv;
+    std::vector<int64_t> positions = {
+        0, 42, (1LL << 31) - 1, 1LL << 31, (1LL << 32) - 1, 1LL << 32, (2LL << 
32) + 42};
+    for (const auto& position : positions) {
+        ASSERT_TRUE(dv.CheckedDelete(position).value());
+        ASSERT_FALSE(dv.CheckedDelete(position).value());
+        ASSERT_TRUE(dv.IsDeleted(position).value());
+    }
+    ASSERT_EQ(dv.GetCardinality().value(), positions.size());
+}
+
+TEST(Bitmap64DeletionVectorTest, TestGetCardinality) {
+    for (const auto& step : {1, 10}) {
+        Bitmap64DeletionVector dv;
+        ASSERT_EQ(dv.GetCardinality().value(), 0);
+        for (const auto& base : {0LL, 1LL << 32}) {
+            for (int64_t i = 0; i < 100; ++i) {
+                ASSERT_OK(dv.Delete(base + i * step));
+            }
+        }
+        ASSERT_EQ(dv.GetCardinality().value(), 200);
+        ASSERT_OK(dv.Delete(0));
+        ASSERT_EQ(dv.GetCardinality().value(), 200);
+        ASSERT_OK(dv.Delete((2LL << 32) + 1));
+        ASSERT_EQ(dv.GetCardinality().value(), 201);
+    }
+}
+
+TEST(Bitmap64DeletionVectorTest, TestFromBitmapDeletionVector) {
+    for (const auto& positions :
+         {std::vector<int32_t>{}, std::vector<int32_t>{0, 1, 3, 
RoaringBitmap32::MAX_VALUE}}) {
+        RoaringBitmap32 bitmap;
+        for (const auto& position : positions) {
+            bitmap.Add(position);
+        }
+        BitmapDeletionVector dv32(bitmap);
+        auto dv64 = Bitmap64DeletionVector::FromBitmapDeletionVector(dv32);
+        ASSERT_EQ(dv64->IsEmpty(), positions.empty());
+        ASSERT_EQ(dv64->GetCardinality().value(), positions.size());
+        std::vector<int64_t> actual;
+        ASSERT_OK(dv64->ForEachDeletedPosition([&](int64_t p) { 
actual.push_back(p); }));
+        ASSERT_EQ(actual, std::vector<int64_t>(positions.begin(), 
positions.end()));
+        ASSERT_OK(dv64->Delete(7));
+        ASSERT_OK(dv64->Delete((1LL << 32) + 7));
+        ASSERT_FALSE(dv32.IsDeleted(7).value());
+        ASSERT_EQ(dv32.GetCardinality().value(), positions.size());
+        ASSERT_OK(dv32.Delete(8));
+        ASSERT_FALSE(dv64->IsDeleted(8).value());
+    }
+}
+
+TEST(Bitmap64DeletionVectorTest, TestPositionOutOfRangeShouldFail) {
+    Bitmap64DeletionVector dv;
+    for (const auto& position : std::vector<int64_t>{-1, 
OptimizedRoaringBitmap64::kMaxValue + 1}) {
+        ASSERT_NOK_WITH_MSG(dv.Delete(position),
+                            "OptimizedRoaringBitmap64 supports positions that 
are >= 0 and <=");
+        ASSERT_NOK_WITH_MSG(dv.CheckedDelete(position),
+                            "OptimizedRoaringBitmap64 supports positions that 
are >= 0 and <=");
+        ASSERT_NOK_WITH_MSG(dv.IsDeleted(position),
+                            "OptimizedRoaringBitmap64 supports positions that 
are >= 0 and <=");
+    }
+    ASSERT_TRUE(dv.IsEmpty());
+    ASSERT_EQ(dv.GetCardinality().value(), 0);
+    ASSERT_FALSE(dv.IsDeleted(OptimizedRoaringBitmap64::kMaxValue).value());
+}
+
+TEST(Bitmap64DeletionVectorTest, TestMerge) {
+    std::vector<int64_t> left = {1, 3, (1LL << 32) + 5};
+    std::vector<int64_t> right = {2, (1LL << 32) + 5, (2LL << 32) + 4};
+    for (const auto& left_empty : {false, true}) {
+        for (const auto& right_empty : {false, true}) {
+            auto dv = std::make_shared<Bitmap64DeletionVector>();
+            auto other = std::make_shared<Bitmap64DeletionVector>();
+            std::set<int64_t> expected;
+            if (!left_empty) {
+                for (const auto& p : left) {
+                    ASSERT_OK(dv->Delete(p));
+                    expected.insert(p);
+                }
+            }
+            if (!right_empty) {
+                for (const auto& p : right) {
+                    ASSERT_OK(other->Delete(p));
+                    expected.insert(p);
+                }
+            }
+            ASSERT_OK(dv->Merge(other));
+            ASSERT_NOK_WITH_MSG(dv->Merge(nullptr), "Cannot merge a 
non-Bitmap64DeletionVector");
+            ASSERT_OK(dv->Merge(other));
+            for (const auto& positions : {std::vector<int32_t>{}, 
std::vector<int32_t>{7}}) {
+                RoaringBitmap32 bitmap;
+                for (const auto& position : positions) {
+                    bitmap.Add(position);
+                }
+                
ASSERT_NOK_WITH_MSG(dv->Merge(std::make_shared<BitmapDeletionVector>(bitmap)),
+                                    "Cannot merge a 
non-Bitmap64DeletionVector");
+            }
+            ASSERT_EQ(dv->IsEmpty(), expected.empty());
+            ASSERT_EQ(dv->GetCardinality().value(), expected.size());
+            std::vector<int64_t> actual;
+            ASSERT_OK(dv->ForEachDeletedPosition([&](int64_t p) { 
actual.push_back(p); }));
+            ASSERT_EQ(actual, std::vector<int64_t>(expected.begin(), 
expected.end()));
+            for (const auto& p : expected) {
+                ASSERT_TRUE(dv->IsDeleted(p).value());
+            }
+        }
+    }
+}
+
+TEST(Bitmap64DeletionVectorTest, JavaCompatibleFramedRecord) {
+    // Generated with Java Paimon Bitmap64DeletionVector.serializeTo(), 
including CRC.
+    std::vector<uint8_t> java_bytes = {
+        0,   0,   0, 110, 209, 211, 57, 100, 3,   0,   0, 0, 0,   0,   0,   0, 
  0,   0,   0, 0,
+        58,  48,  0, 0,   4,   0,   0,  0,   0,   0,   1, 0, 255, 127, 0,   0, 
  0,   128, 0, 0,
+        255, 255, 0, 0,   40,  0,   0,  0,   44,  0,   0, 0, 46,  0,   0,   0, 
  48,  0,   0, 0,
+        0,   0,   1, 0,   255, 255, 0,  0,   255, 255, 1, 0, 0,   0,   58,  
48,  0,   0,   1, 0,
+        0,   0,   0, 0,   0,   0,   16, 0,   0,   0,   0, 0, 2,   0,   0,   0, 
  58,  48,  0, 0,
+        1,   0,   0, 0,   0,   0,   0,  0,   16,  0,   0, 0, 7,   0,   206, 
177, 171, 208};
+    std::vector<int64_t> positions = {
+        0, 1, 2147483647LL, 2147483648LL, 4294967295LL, 4294967296LL, 
8589934599LL};
+    auto pool = GetDefaultPool();
+    for (const auto& with_length : {false, true}) {
+        auto input = std::make_shared<ByteArrayInputStream>(
+            reinterpret_cast<const char*>(java_bytes.data()), 
java_bytes.size());
+        DataInputStream in(input);
+        ASSERT_OK_AND_ASSIGN(
+            auto decoded,
+            DeletionVector::Read(
+                &in, with_length ? 
std::make_optional<int64_t>(java_bytes.size()) : std::nullopt,
+                pool.get()));
+        ASSERT_TRUE(dynamic_cast<Bitmap64DeletionVector*>(decoded.get()));
+        std::vector<int64_t> actual;
+        ASSERT_OK(decoded->ForEachDeletedPosition([&](int64_t p) { 
actual.push_back(p); }));
+        ASSERT_EQ(actual, positions);
+        ASSERT_EQ(input->GetPos().value(), java_bytes.size());
+    }
+    Bitmap64DeletionVector dv;
+    for (const auto& p : positions) {
+        ASSERT_OK(dv.Delete(p));
+    }
+    auto output = std::make_shared<ByteArrayOutputStream>(
+        std::make_unique<MemorySegmentOutputStream>(1024, pool));
+    DataOutputStream out(output);
+    ASSERT_OK_AND_ASSIGN(int32_t metadata_length, dv.SerializeTo(pool, &out));
+    ASSERT_OK_AND_ASSIGN(auto bytes, output->Finish(pool.get()));
+    ASSERT_EQ(metadata_length, java_bytes.size());
+    ASSERT_EQ(std::vector<uint8_t>(bytes->data(), bytes->data() + 
bytes->size()), java_bytes);
+}
+
+TEST(Bitmap64DeletionVectorTest, SerializeAndDeserialize) {
+    Bitmap64DeletionVector dv;
+    std::vector<int64_t> positions = {1, 7, 1024, (1LL << 32) + 7, (2LL << 32) 
+ 1};
+    for (const auto& position : positions) {
+        ASSERT_OK(dv.Delete(position));
+    }
+    auto pool = GetDefaultPool();
+    ASSERT_OK_AND_ASSIGN(auto bytes, dv.SerializeToBytes(pool));
+    ASSERT_OK_AND_ASSIGN(
+        auto decoded,
+        Bitmap64DeletionVector::DeserializeWithoutMagicNumber(
+            bytes->data() + Bitmap64DeletionVector::MAGIC_NUMBER_SIZE_BYTES,
+            bytes->size() - Bitmap64DeletionVector::MAGIC_NUMBER_SIZE_BYTES, 
pool.get()));
+    ASSERT_EQ(decoded->GetCardinality().value(), positions.size());
+    for (const auto& position : positions) {
+        ASSERT_TRUE(decoded->IsDeleted(position).value());
+    }
+    ASSERT_FALSE(decoded->IsDeleted(8).value());
+    ASSERT_FALSE(decoded->IsDeleted((1LL << 32) + 8).value());
+}
+
+TEST(Bitmap64DeletionVectorTest, SerializeToOutputStream) {
+    Bitmap64DeletionVector dv;
+    for (int64_t p = 0; p < 50; p += 2) {
+        ASSERT_OK(dv.Delete((1LL << 32) + p));
+    }
+    auto pool = GetDefaultPool();
+    auto dir = UniqueTestDirectory::Create();
+    auto path = PathUtil::JoinPath(dir->Str(), "dv64");
+    ASSERT_OK_AND_ASSIGN(auto fs, FileSystemFactory::Get("local", path, {}));
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<OutputStream> output, 
fs->Create(path, false));
+    DataOutputStream out(output);
+    ASSERT_OK_AND_ASSIGN(int32_t length, dv.SerializeTo(pool, &out));
+    ASSERT_OK(output->Flush());
+    ASSERT_OK(output->Close());
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> input, fs->Open(path));
+    DataInputStream in(input);
+    ASSERT_OK_AND_ASSIGN(int32_t bitmap_length, in.ReadValue<int32_t>());
+    ASSERT_EQ(length, bitmap_length + 
Bitmap64DeletionVector::LENGTH_SIZE_BYTES +
+                          Bitmap64DeletionVector::CRC_SIZE_BYTES);
+    ASSERT_EQ(in.Length().value(), length);
+    ASSERT_OK(in.Seek(0));
+    ASSERT_OK_AND_ASSIGN(auto decoded, DeletionVector::Read(&in, length, 
pool.get()));
+    ASSERT_EQ(in.GetPos().value(), length);
+    ASSERT_EQ(decoded->GetCardinality().value(), 25);
+    for (int64_t p = 0; p <= 50; ++p) {
+        ASSERT_EQ(decoded->IsDeleted((1LL << 32) + p).value(), p < 50 && p % 2 
== 0);
+    }
+}
+
+TEST(Bitmap64DeletionVectorTest, RejectMalformedRecords) {
+    auto pool = GetDefaultPool();
+    Bitmap64DeletionVector dv;
+    ASSERT_OK(dv.Delete(1LL << 32));
+    auto output = std::make_shared<ByteArrayOutputStream>(
+        std::make_unique<MemorySegmentOutputStream>(1024, pool));
+    DataOutputStream out(output);
+    ASSERT_OK_AND_ASSIGN(int32_t length, dv.SerializeTo(pool, &out));
+    ASSERT_OK_AND_ASSIGN(auto bytes, output->Finish(pool.get()));
+    auto input = std::make_shared<ByteArrayInputStream>(bytes->data(), 
bytes->size());
+    DataInputStream in(input);
+    ASSERT_NOK_WITH_MSG(DeletionVector::Read(&in, length - 8, pool.get()), 
"Size not match");
+    for (const auto& cut : {0, 1, 3, 4, 7, length - 5, length - 1}) {
+        auto truncated = std::make_shared<ByteArrayInputStream>(bytes->data(), 
cut);
+        DataInputStream truncated_in(truncated);
+        ASSERT_NOK_WITH_MSG(DeletionVector::Read(&truncated_in, std::nullopt, 
pool.get()),
+                            "DataInputStream boundary check failed");
+    }
+    // Keep the frame intact and corrupt only its magic number.
+    bytes->data()[Bitmap64DeletionVector::LENGTH_SIZE_BYTES] ^= 1;
+    auto invalid_magic_input = 
std::make_shared<ByteArrayInputStream>(bytes->data(), bytes->size());
+    DataInputStream invalid_magic_in(invalid_magic_input);
+    ASSERT_NOK_WITH_MSG(DeletionVector::Read(&invalid_magic_in, length, 
pool.get()),
+                        "Invalid magic number");
+    // A complete frame containing only magic and CRC has no bitmap payload.
+    char magic_only[] = {0, 0, 0, 4, static_cast<char>(0xD1), 
static_cast<char>(0xD3), 0x39, 0x64,
+                         0, 0, 0, 0};
+    auto magic_only_input = std::make_shared<ByteArrayInputStream>(magic_only, 
sizeof(magic_only));
+    DataInputStream magic_only_in(magic_only_input);
+    ASSERT_NOK_WITH_MSG(DeletionVector::Read(&magic_only_in, 
sizeof(magic_only), pool.get()),
+                        "Invalid bitmap64 payload length");
+}
+
+}  // namespace paimon::test
diff --git a/src/paimon/core/deletionvectors/bitmap_deletion_vector.cpp 
b/src/paimon/core/deletionvectors/bitmap_deletion_vector.cpp
index ed8c3ba2..f789d86b 100644
--- a/src/paimon/core/deletionvectors/bitmap_deletion_vector.cpp
+++ b/src/paimon/core/deletionvectors/bitmap_deletion_vector.cpp
@@ -87,16 +87,12 @@ Result<PAIMON_UNIQUE_PTR<DeletionVector>> 
BitmapDeletionVector::Deserialize(cons
 }
 
 Status BitmapDeletionVector::Merge(const std::shared_ptr<DeletionVector>& 
deletion_vector) {
-    if (!deletion_vector || deletion_vector->IsEmpty()) {
-        return Status::OK();
-    }
     auto* other = dynamic_cast<BitmapDeletionVector*>(deletion_vector.get());
-    if (other != nullptr) {
-        roaring_bitmap_ |= other->roaring_bitmap_;
-    } else {
+    if (!other) {
         return Status::Invalid(
             "Cannot merge a non-BitmapDeletionVector into a 
BitmapDeletionVector");
     }
+    roaring_bitmap_ |= other->roaring_bitmap_;
     return Status::OK();
 }
 
diff --git a/src/paimon/core/deletionvectors/bitmap_deletion_vector.h 
b/src/paimon/core/deletionvectors/bitmap_deletion_vector.h
index d0803b71..7890e97d 100644
--- a/src/paimon/core/deletionvectors/bitmap_deletion_vector.h
+++ b/src/paimon/core/deletionvectors/bitmap_deletion_vector.h
@@ -68,6 +68,13 @@ class BitmapDeletionVector : public DeletionVector {
 
     Status Merge(const std::shared_ptr<DeletionVector>& deletion_vector) 
override;
 
+    Status ForEachDeletedPosition(const std::function<void(int64_t)>& 
consumer) const override {
+        for (auto iter = roaring_bitmap_.Begin(); iter != 
roaring_bitmap_.End(); ++iter) {
+            consumer(*iter);
+        }
+        return Status::OK();
+    }
+
     const RoaringBitmap32* GetBitmap() const {
         return &roaring_bitmap_;
     }
diff --git a/src/paimon/core/deletionvectors/bitmap_deletion_vector_test.cpp 
b/src/paimon/core/deletionvectors/bitmap_deletion_vector_test.cpp
index 1aeba310..2ad00090 100644
--- a/src/paimon/core/deletionvectors/bitmap_deletion_vector_test.cpp
+++ b/src/paimon/core/deletionvectors/bitmap_deletion_vector_test.cpp
@@ -24,6 +24,7 @@
 #include "gtest/gtest.h"
 #include "paimon/common/io/memory_segment_output_stream.h"
 #include "paimon/common/utils/path_util.h"
+#include "paimon/core/deletionvectors/bitmap64_deletion_vector.h"
 #include "paimon/fs/file_system_factory.h"
 #include "paimon/testing/utils/testharness.h"
 
@@ -208,16 +209,38 @@ TEST(BitmapDeletionVectorTest, MergeEmptyDeletionVector) {
     ASSERT_TRUE(dv1->IsDeleted(20).value());
 }
 
-TEST(BitmapDeletionVectorTest, MergeNullDeletionVector) {
+TEST(BitmapDeletionVectorTest, MergeNullDeletionVectorShouldFail) {
     RoaringBitmap32 roaring1;
     roaring1.Add(7);
     auto dv1 = std::make_shared<BitmapDeletionVector>(roaring1);
 
-    ASSERT_OK(dv1->Merge(nullptr));
+    ASSERT_NOK_WITH_MSG(dv1->Merge(nullptr), "Cannot merge a 
non-BitmapDeletionVector");
     ASSERT_EQ(dv1->GetCardinality().value(), 1);
     ASSERT_TRUE(dv1->IsDeleted(7).value());
 }
 
+TEST(BitmapDeletionVectorTest, MergeDifferentTypeShouldFail) {
+    for (const auto& target_empty : {false, true}) {
+        for (const auto& source_empty : {false, true}) {
+            BitmapDeletionVector dv{RoaringBitmap32()};
+            if (!target_empty) {
+                ASSERT_OK(dv.Delete(7));
+            }
+            auto other = std::make_shared<Bitmap64DeletionVector>();
+            if (!source_empty) {
+                ASSERT_OK(other->Delete(8));
+                ASSERT_OK(other->Delete(1LL << 32));
+            }
+            ASSERT_NOK_WITH_MSG(dv.Merge(other), "Cannot merge a 
non-BitmapDeletionVector");
+            ASSERT_EQ(dv.IsEmpty(), target_empty);
+            ASSERT_EQ(dv.GetCardinality().value(), target_empty ? 0 : 1);
+            ASSERT_EQ(dv.IsDeleted(7).value(), !target_empty);
+            ASSERT_FALSE(dv.IsDeleted(8).value());
+            ASSERT_EQ(other->GetCardinality().value(), source_empty ? 0 : 2);
+        }
+    }
+}
+
 TEST(BitmapDeletionVectorTest, MergeIntoEmptyDeletionVector) {
     RoaringBitmap32 empty_roaring;
     auto dv1 = std::make_shared<BitmapDeletionVector>(empty_roaring);
diff --git a/src/paimon/core/deletionvectors/bucketed_dv_maintainer.h 
b/src/paimon/core/deletionvectors/bucketed_dv_maintainer.h
index 4088c342..39cda8bb 100644
--- a/src/paimon/core/deletionvectors/bucketed_dv_maintainer.h
+++ b/src/paimon/core/deletionvectors/bucketed_dv_maintainer.h
@@ -23,6 +23,7 @@
 #include <optional>
 #include <string>
 
+#include "paimon/core/deletionvectors/bitmap64_deletion_vector.h"
 #include "paimon/core/deletionvectors/bitmap_deletion_vector.h"
 #include "paimon/core/deletionvectors/deletion_vector.h"
 #include "paimon/core/deletionvectors/deletion_vectors_index_file.h"
@@ -45,12 +46,11 @@ class BucketedDvMaintainer {
         std::shared_ptr<DeletionVector> dv;
         if (auto it = deletion_vectors_.find(file_name); it == 
deletion_vectors_.end()) {
             if (bitmap64_) {
-                return Status::NotImplemented("not support bitmap 64 deletion 
vectors");
+                dv = std::make_shared<Bitmap64DeletionVector>();
+            } else {
+                dv = std::make_shared<BitmapDeletionVector>(RoaringBitmap32());
             }
-            RoaringBitmap32 roaring_bitmap;
-            auto inserted = 
std::make_shared<BitmapDeletionVector>(roaring_bitmap);
-            deletion_vectors_[file_name] = inserted;
-            dv = std::move(inserted);
+            deletion_vectors_[file_name] = dv;
         } else {
             dv = it->second;
         }
diff --git a/src/paimon/core/deletionvectors/bucketed_dv_maintainer_test.cpp 
b/src/paimon/core/deletionvectors/bucketed_dv_maintainer_test.cpp
index f4ef95ae..9a0e80b5 100644
--- a/src/paimon/core/deletionvectors/bucketed_dv_maintainer_test.cpp
+++ b/src/paimon/core/deletionvectors/bucketed_dv_maintainer_test.cpp
@@ -137,20 +137,33 @@ TEST(BucketedDvMaintainerTest, 
TestNotifyNewDeletionOnExistingVectorOnlyMarksWhe
     ASSERT_TRUE(modified_write.has_value());
 }
 
-TEST(BucketedDvMaintainerTest, 
TestNotifyNewDeletionReturnsNotImplementedForBitmap64) {
-    auto dir = UniqueTestDirectory::Create();
-    ASSERT_TRUE(dir);
-    auto index_file = CreateDvIndexFile(dir->Str(), /*bitmap64=*/true);
-    BucketedDvMaintainer maintainer(index_file, /*deletion_vectors=*/{});
-
-    Status status = maintainer.NotifyNewDeletion("file-new", /*position=*/1);
-    ASSERT_TRUE(status.IsNotImplemented());
-
-    auto lookup = maintainer.DeletionVectorOf("file-new");
-    ASSERT_FALSE(lookup.has_value());
-
-    ASSERT_OK_AND_ASSIGN(auto write_result, 
maintainer.WriteDeletionVectorsIndex());
-    ASSERT_FALSE(write_result.has_value());
+TEST(BucketedDvMaintainerTest, MixedFormatsAfterConfigurationChanges) {
+    for (bool initial_bitmap64 : {false, true}) {
+        auto dir = UniqueTestDirectory::Create();
+        auto index = CreateDvIndexFile(dir->Str(), initial_bitmap64);
+        BucketedDvMaintainer initial(index, {});
+        ASSERT_OK(initial.NotifyNewDeletion("old", 1));
+        ASSERT_OK_AND_ASSIGN(auto first, initial.WriteDeletionVectorsIndex());
+        ASSERT_TRUE(first.has_value());
+        auto changed_index = CreateDvIndexFile(dir->Str(), !initial_bitmap64);
+        ASSERT_OK_AND_ASSIGN(auto restored, 
changed_index->ReadAllDeletionVectors(first.value()));
+        BucketedDvMaintainer changed(changed_index, restored);
+        ASSERT_OK(changed.NotifyNewDeletion("old", 7));
+        ASSERT_OK(changed.NotifyNewDeletion("new", 3));
+        const std::string high_file = initial_bitmap64 ? "old" : "new";
+        ASSERT_OK(changed.NotifyNewDeletion(high_file, (1LL << 32) + 9));
+        ASSERT_OK_AND_ASSIGN(auto second, changed.WriteDeletionVectorsIndex());
+        ASSERT_TRUE(second.has_value());
+        ASSERT_OK_AND_ASSIGN(auto mixed, 
changed_index->ReadAllDeletionVectors(second.value()));
+        ASSERT_EQ(dynamic_cast<Bitmap64DeletionVector*>(mixed.at("old").get()) 
!= nullptr,
+                  initial_bitmap64);
+        ASSERT_EQ(dynamic_cast<Bitmap64DeletionVector*>(mixed.at("new").get()) 
!= nullptr,
+                  !initial_bitmap64);
+        ASSERT_TRUE(mixed.at("old")->IsDeleted(7).value());
+        ASSERT_TRUE(mixed.at(high_file)->IsDeleted((1LL << 32) + 9).value());
+        ASSERT_OK(changed.NotifyNewDeletion(high_file, (1LL << 32) + 9));
+        ASSERT_FALSE(changed.WriteDeletionVectorsIndex().value().has_value());
+    }
 }
 
 }  // namespace paimon::test
diff --git a/src/paimon/core/deletionvectors/deletion_vector.cpp 
b/src/paimon/core/deletionvectors/deletion_vector.cpp
index f981d19b..2241caec 100644
--- a/src/paimon/core/deletionvectors/deletion_vector.cpp
+++ b/src/paimon/core/deletionvectors/deletion_vector.cpp
@@ -20,6 +20,7 @@
 
 #include <cassert>
 #include <cstddef>
+#include <limits>
 #include <string>
 
 #include "fmt/format.h"
@@ -78,11 +79,6 @@ std::unordered_map<std::string, DeletionFile> 
DeletionVector::CreateDeletionFile
     return deletion_file_map;
 }
 
-Result<PAIMON_UNIQUE_PTR<DeletionVector>> 
DeletionVector::DeserializeFromBytes(const Bytes* bytes,
-                                                                               
MemoryPool* pool) {
-    return BitmapDeletionVector::Deserialize(bytes->data(), bytes->size(), 
pool);
-}
-
 Result<PAIMON_UNIQUE_PTR<DeletionVector>> DeletionVector::Read(const 
FileSystem* file_system,
                                                                const 
DeletionFile& deletion_file,
                                                                MemoryPool* 
pool) {
@@ -90,15 +86,7 @@ Result<PAIMON_UNIQUE_PTR<DeletionVector>> 
DeletionVector::Read(const FileSystem*
                            file_system->Open(deletion_file.path));
     DataInputStream file_input_stream(input);
     PAIMON_RETURN_NOT_OK(file_input_stream.Seek(deletion_file.offset));
-    PAIMON_ASSIGN_OR_RAISE(int32_t actual_length, 
file_input_stream.ReadValue<int32_t>());
-    if (actual_length != deletion_file.length) {
-        return Status::Invalid(
-            fmt::format("Size not match, actual size: {}, expect size: {}, 
file path: {}",
-                        actual_length, deletion_file.length, 
deletion_file.path));
-    }
-    auto bytes = Bytes::AllocateBytes(deletion_file.length, pool);
-    PAIMON_RETURN_NOT_OK(file_input_stream.ReadBytes(bytes.get()));
-    return DeserializeFromBytes(bytes.get(), pool);
+    return Read(&file_input_stream, deletion_file.length, pool);
 }
 
 Result<PAIMON_UNIQUE_PTR<DeletionVector>> 
DeletionVector::Read(DataInputStream* input_stream,
@@ -127,10 +115,27 @@ Result<PAIMON_UNIQUE_PTR<DeletionVector>> 
DeletionVector::Read(DataInputStream*
         return 
BitmapDeletionVector::DeserializeWithoutMagicNumber(bytes->data(), 
bytes->size(),
                                                                    pool);
     } else if (EndianSwapValue(magic_number) == 
Bitmap64DeletionVector::MAGIC_NUMBER) {
-        return Status::NotImplemented(
-            "bitmap64 deletion vectors are not supported in this version, "
-            "please use bitmap deletion vectors instead or upgrade to a 
version "
-            "that supports bitmap64.");
+        if (bitmap_length < Bitmap64DeletionVector::MAGIC_NUMBER_SIZE_BYTES ||
+            bitmap_length > std::numeric_limits<int32_t>::max() -
+                                Bitmap64DeletionVector::LENGTH_SIZE_BYTES -
+                                Bitmap64DeletionVector::CRC_SIZE_BYTES) {
+            return Status::Invalid(fmt::format("Invalid bitmap length: {}", 
bitmap_length));
+        }
+        int64_t record_length = static_cast<int64_t>(bitmap_length) +
+                                Bitmap64DeletionVector::LENGTH_SIZE_BYTES +
+                                Bitmap64DeletionVector::CRC_SIZE_BYTES;
+        if (length.has_value() && record_length != length.value()) {
+            return Status::Invalid(fmt::format("Size not match, actual size: 
{}, expected size: {}",
+                                               record_length, length.value()));
+        }
+        auto bytes = Bytes::AllocateBytes(
+            bitmap_length - Bitmap64DeletionVector::MAGIC_NUMBER_SIZE_BYTES, 
pool);
+        PAIMON_RETURN_NOT_OK(input_stream->ReadBytes(bytes.get()));
+        // skip crc (4 bytes)
+        PAIMON_ASSIGN_OR_RAISE([[maybe_unused]] int32_t unused_crc,
+                               input_stream->ReadValue<int32_t>());
+        return 
Bitmap64DeletionVector::DeserializeWithoutMagicNumber(bytes->data(), 
bytes->size(),
+                                                                     pool);
     }
 
     return Status::Invalid(fmt::format(
diff --git a/src/paimon/core/deletionvectors/deletion_vector.h 
b/src/paimon/core/deletionvectors/deletion_vector.h
index f4f876ea..9cb8a6e6 100644
--- a/src/paimon/core/deletionvectors/deletion_vector.h
+++ b/src/paimon/core/deletionvectors/deletion_vector.h
@@ -18,6 +18,7 @@
  */
 #pragma once
 #include <cstdint>
+#include <functional>
 #include <memory>
 #include <unordered_map>
 #include <utility>
@@ -106,6 +107,9 @@ class DeletionVector {
     /// @return the number of distinct integers added to the DeletionVector.
     virtual Result<int64_t> GetCardinality() const = 0;
 
+    /// Iterates over all deleted positions in this deletion vector.
+    virtual Status ForEachDeletedPosition(const std::function<void(int64_t)>& 
consumer) const = 0;
+
     /// Serializes the deletion vector.
     virtual Result<int32_t> SerializeTo(const std::shared_ptr<MemoryPool>& 
pool,
                                         DataOutputStream* out) = 0;
@@ -116,13 +120,6 @@ class DeletionVector {
     virtual Result<PAIMON_UNIQUE_PTR<Bytes>> SerializeToBytes(
         const std::shared_ptr<MemoryPool>& pool) = 0;
 
-    /// Deserializes a deletion vector from a byte array.
-    ///
-    /// @param bytes The byte array containing the serialized deletion vector.
-    /// @return A DeletionVector instance that represents the deserialized 
data.
-    static Result<PAIMON_UNIQUE_PTR<DeletionVector>> 
DeserializeFromBytes(const Bytes* bytes,
-                                                                          
MemoryPool* pool);
-
     static Result<PAIMON_UNIQUE_PTR<DeletionVector>> Read(const FileSystem* 
file_system,
                                                           const DeletionFile& 
deletion_file,
                                                           MemoryPool* pool);
diff --git 
a/src/paimon/core/deletionvectors/deletion_vector_index_file_writer_test.cpp 
b/src/paimon/core/deletionvectors/deletion_vector_index_file_writer_test.cpp
index a435f44e..7f18168e 100644
--- a/src/paimon/core/deletionvectors/deletion_vector_index_file_writer_test.cpp
+++ b/src/paimon/core/deletionvectors/deletion_vector_index_file_writer_test.cpp
@@ -47,6 +47,10 @@ class FailingDeletionVector : public DeletionVector {
         return Status::Invalid("injected is-deleted failure");
     }
 
+    Status ForEachDeletedPosition(const std::function<void(int64_t)>&) const 
override {
+        return Status::OK();
+    }
+
     bool IsEmpty() const override {
         return true;
     }
diff --git a/src/paimon/core/deletionvectors/deletion_vector_test.cpp 
b/src/paimon/core/deletionvectors/deletion_vector_test.cpp
index bff4b46a..7aa0a756 100644
--- a/src/paimon/core/deletionvectors/deletion_vector_test.cpp
+++ b/src/paimon/core/deletionvectors/deletion_vector_test.cpp
@@ -27,12 +27,13 @@
 #include <vector>
 
 #include "gtest/gtest.h"
+#include "paimon/common/io/byte_array_output_stream.h"
+#include "paimon/common/io/data_output_stream.h"
 #include "paimon/core/deletionvectors/bitmap64_deletion_vector.h"
 #include "paimon/core/deletionvectors/bitmap_deletion_vector.h"
 #include "paimon/core/io/data_file_meta.h"
 #include "paimon/core/table/source/deletion_file.h"
 #include "paimon/io/byte_array_input_stream.h"
-#include "paimon/io/byte_order.h"
 #include "paimon/io/data_input_stream.h"
 #include "paimon/testing/utils/testharness.h"
 
@@ -82,9 +83,15 @@ TEST(DeletionVectorTest, TestSimple) {
         }
     }
     auto pool = GetDefaultPool();
-    ASSERT_OK_AND_ASSIGN(auto bytes, deletion_vector->SerializeToBytes(pool));
-    ASSERT_OK_AND_ASSIGN(auto de_deletion_vector,
-                         DeletionVector::DeserializeFromBytes(bytes.get(), 
pool.get()));
+    auto output = std::make_shared<ByteArrayOutputStream>(
+        std::make_unique<MemorySegmentOutputStream>(1024, pool));
+    DataOutputStream out(output);
+    ASSERT_OK_AND_ASSIGN(int32_t length, deletion_vector->SerializeTo(pool, 
&out));
+    ASSERT_OK_AND_ASSIGN(auto bytes, output->Finish(pool.get()));
+    auto input = std::make_shared<ByteArrayInputStream>(bytes->data(), 
bytes->size());
+    DataInputStream in(input);
+    ASSERT_OK_AND_ASSIGN(auto de_deletion_vector, DeletionVector::Read(&in, 
length, pool.get()));
+    ASSERT_EQ(input->GetPos().value(), bytes->size());
 
     ASSERT_FALSE(deletion_vector->IsEmpty());
     ASSERT_FALSE(de_deletion_vector->IsEmpty());
@@ -106,9 +113,16 @@ TEST(DeletionVectorTest, TestCompatibleWithJava) {
     auto serialize_bytes = std::make_shared<Bytes>(data.size(), pool.get());
     memcpy(serialize_bytes->data(), data.data(), data.size());
 
-    // test deserialize
-    ASSERT_OK_AND_ASSIGN(auto deletion_vector,
-                         
DeletionVector::DeserializeFromBytes(serialize_bytes.get(), pool.get()));
+    // Wrap the Java payload in a record with its length and CRC.
+    std::vector<uint8_t> record;
+    AppendInt32BigEndian(&record, data.size());
+    record.insert(record.end(), data.begin(), data.end());
+    AppendInt32BigEndian(&record, 0x5235C7CF);
+    auto input = std::make_shared<ByteArrayInputStream>(
+        reinterpret_cast<const char*>(record.data()), record.size());
+    DataInputStream in(input);
+    ASSERT_OK_AND_ASSIGN(auto deletion_vector, DeletionVector::Read(&in, 
data.size(), pool.get()));
+    ASSERT_EQ(input->GetPos().value(), record.size());
     std::vector<bool> expected = {false, true, true, false, true, false};
     std::vector<bool> result;
     for (size_t i = 0; i < expected.size(); i++) {
@@ -147,20 +161,30 @@ TEST(DeletionVectorTest, 
ReadFromDataInputStreamInvalidBitmapLength) {
                         "Invalid bitmap length");
 }
 
-TEST(DeletionVectorTest, ReadFromDataInputStreamBitmap64NotImplemented) {
-    std::vector<uint8_t> data;
-    AppendInt32BigEndian(&data, /*value=*/8);
-    // Trigger: EndianSwapValue(magic_number) == 
Bitmap64DeletionVector::MAGIC_NUMBER.
-    AppendInt32BigEndian(&data, 
EndianSwapValue(Bitmap64DeletionVector::MAGIC_NUMBER));
-
-    auto input_stream = std::make_shared<ByteArrayInputStream>(
-        reinterpret_cast<const char*>(data.data()), data.size());
-    DataInputStream in(input_stream);
+TEST(DeletionVectorTest, ReadFromDataInputStreamBitmap64) {
     auto pool = GetDefaultPool();
-
-    ASSERT_NOK_WITH_MSG(
-        DeletionVector::Read(&in, std::nullopt, pool.get()),
-        "NotImplemented: bitmap64 deletion vectors are not supported in this 
version");
+    Bitmap64DeletionVector dv;
+    std::vector<int64_t> positions = {1, (1LL << 32) + 7, (2LL << 32) + 3};
+    for (const auto& position : positions) {
+        ASSERT_OK(dv.Delete(position));
+    }
+    auto output = std::make_shared<ByteArrayOutputStream>(
+        std::make_unique<MemorySegmentOutputStream>(1024, pool));
+    DataOutputStream out(output);
+    ASSERT_OK_AND_ASSIGN(int32_t length, dv.SerializeTo(pool, &out));
+    ASSERT_OK_AND_ASSIGN(auto bytes, output->Finish(pool.get()));
+    ASSERT_EQ(length, bytes->size());
+
+    auto input = std::make_shared<ByteArrayInputStream>(bytes->data(), 
bytes->size());
+    DataInputStream in(input);
+    ASSERT_OK_AND_ASSIGN(auto decoded, DeletionVector::Read(&in, length, 
pool.get()));
+    ASSERT_TRUE(dynamic_cast<Bitmap64DeletionVector*>(decoded.get()));
+    ASSERT_EQ(decoded->GetCardinality().value(), positions.size());
+    for (const auto& position : positions) {
+        ASSERT_TRUE(decoded->IsDeleted(position).value());
+    }
+    ASSERT_FALSE(decoded->IsDeleted((1LL << 32) + 8).value());
+    ASSERT_EQ(input->GetPos().value(), bytes->size());
 }
 
 TEST(DeletionVectorTest, ReadFromDataInputStreamInvalidMagicNumber) {
diff --git a/src/paimon/core/deletionvectors/deletion_vectors_index_file.cpp 
b/src/paimon/core/deletionvectors/deletion_vectors_index_file.cpp
index 8743b43a..4467a701 100644
--- a/src/paimon/core/deletionvectors/deletion_vectors_index_file.cpp
+++ b/src/paimon/core/deletionvectors/deletion_vectors_index_file.cpp
@@ -55,6 +55,7 @@ DeletionVectorsIndexFile::ReadAllDeletionVectors(
     auto data_input_stream = std::make_shared<DataInputStream>(input_stream);
     PAIMON_RETURN_NOT_OK(CheckVersion(data_input_stream));
     for (const auto& [_, deletion_vector_meta] : 
deletion_vector_metas.value()) {
+        
PAIMON_RETURN_NOT_OK(data_input_stream->Seek(deletion_vector_meta.GetOffset()));
         PAIMON_ASSIGN_OR_RAISE(
             std::shared_ptr<DeletionVector> dv,
             DeletionVector::Read(data_input_stream.get(),
diff --git 
a/src/paimon/core/deletionvectors/deletion_vectors_index_file_test.cpp 
b/src/paimon/core/deletionvectors/deletion_vectors_index_file_test.cpp
index 817c158b..9181d2bf 100644
--- a/src/paimon/core/deletionvectors/deletion_vectors_index_file_test.cpp
+++ b/src/paimon/core/deletionvectors/deletion_vectors_index_file_test.cpp
@@ -77,6 +77,20 @@ TEST(DeletionVectorsIndexFileTest, Basic) {
     ASSERT_TRUE(is_deleted);
     ASSERT_OK_AND_ASSIGN(is_deleted, read_back.at("dv2")->IsDeleted(99));
     ASSERT_FALSE(is_deleted);
+
+    // Metadata order need not match the physical order of deletion vectors.
+    LinkedHashMap<std::string, DeletionVectorMeta> reversed;
+    for (const auto& name : {"dv2", "dv1"}) {
+        reversed.insert_or_assign(name, 
meta->DvRanges().value().find(name)->second);
+    }
+    auto reversed_meta = std::make_shared<IndexFileMeta>(
+        meta->IndexType(), meta->FileName(), meta->FileSize(), 2, reversed, 
std::nullopt);
+    ASSERT_OK_AND_ASSIGN(auto reversed_read, 
index_file->ReadAllDeletionVectors(reversed_meta));
+    ASSERT_EQ(reversed_read.size(), 2);
+    ASSERT_TRUE(reversed_read.at("dv1")->IsDeleted(0).value());
+    ASSERT_FALSE(reversed_read.at("dv1")->IsDeleted(100).value());
+    ASSERT_TRUE(reversed_read.at("dv2")->IsDeleted(100).value());
+    ASSERT_FALSE(reversed_read.at("dv2")->IsDeleted(0).value());
 }
 
 TEST(DeletionVectorsIndexFileTest, ExternalPathAndIndexFileMeta) {
diff --git a/src/paimon/core/io/file_index_evaluator.cpp 
b/src/paimon/core/io/file_index_evaluator.cpp
index 4405c099..59c20f47 100644
--- a/src/paimon/core/io/file_index_evaluator.cpp
+++ b/src/paimon/core/io/file_index_evaluator.cpp
@@ -46,6 +46,7 @@
 #include "paimon/predicate/predicate_utils.h"
 #include "paimon/predicate/vector_search.h"
 #include "paimon/status.h"
+#include "paimon/utils/roaring_bitmap32.h"
 
 namespace paimon {
 class MemoryPool;
@@ -75,7 +76,7 @@ Result<std::shared_ptr<FileIndexResult>> 
FileIndexEvaluator::Evaluate(
     const std::shared_ptr<DataFilePathFactory>& data_file_path_factory,
     const std::shared_ptr<DataFileMeta>& file_meta, const 
std::shared_ptr<FileSystem>& file_system,
     const std::shared_ptr<MemoryPool>& pool) {
-    if (predicate == nullptr) {
+    if (predicate == nullptr || file_meta->row_count > 
RoaringBitmap32::MAX_VALUE) {
         return FileIndexResult::Remain();
     }
     PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<InputStream> input_stream,
diff --git a/src/paimon/core/io/file_index_evaluator_test.cpp 
b/src/paimon/core/io/file_index_evaluator_test.cpp
index adeb5a39..1a0168ef 100644
--- a/src/paimon/core/io/file_index_evaluator_test.cpp
+++ b/src/paimon/core/io/file_index_evaluator_test.cpp
@@ -604,4 +604,30 @@ INSTANTIATE_TEST_SUITE_P(IndexStorage, 
FileIndexSearchEvaluatorTest, ::testing::
                              return info.param ? "Embedded" : "External";
                          });
 
+TEST_F(FileIndexEvaluatorTest, TestLargeFileSkips32BitIndex) {
+    auto data_file_meta = std::make_shared<DataFileMeta>(
+        "data-414509f5-e40c-4245-b992-bbf486778ac9-0.orc", /*file_size=*/689,
+        /*row_count=*/(1LL << 32), /*min_key=*/BinaryRow::EmptyRow(),
+        /*max_key=*/BinaryRow::EmptyRow(), 
/*key_stats=*/SimpleStats::EmptyStats(),
+        /*value_stats=*/SimpleStats::EmptyStats(), /*min_sequence_number=*/0,
+        /*max_sequence_number=*/7, /*schema_id=*/0,
+        /*level=*/0,
+        /*extra_files=*/
+        std::vector<std::optional<std::string>>(
+            {"data-414509f5-e40c-4245-b992-bbf486778ac9-0.orc.index"}),
+        /*creation_time=*/Timestamp(0ll, 0), /*delete_row_count=*/0,
+        /*embedded_index=*/nullptr, FileSource::Append(),
+        /*value_stats_cols=*/std::nullopt,
+        /*external_path=*/std::nullopt, /*first_row_id=*/std::nullopt, 
/*write_cols=*/std::nullopt,
+        /*column_max_sequence_numbers=*/std::nullopt);
+    auto predicate =
+        PredicateBuilder::IsNull(/*field_index=*/2, /*field_name=*/"f2", 
FieldType::INT);
+    ASSERT_OK_AND_ASSIGN(
+        auto result, FileIndexEvaluator::Evaluate(data_schema_, 
core_options_.ToMap(), predicate,
+                                                  
/*data_file_path_factory=*/nullptr,
+                                                  data_file_meta, 
/*file_system=*/nullptr, pool_));
+    ASSERT_TRUE(result->IsRemain().value());
+    ASSERT_EQ(dynamic_cast<BitmapIndexResult*>(result.get()), nullptr);
+}
+
 }  // namespace paimon::test
diff --git a/src/paimon/core/operation/abstract_split_read.cpp 
b/src/paimon/core/operation/abstract_split_read.cpp
index 690f7bbe..c6669023 100644
--- a/src/paimon/core/operation/abstract_split_read.cpp
+++ b/src/paimon/core/operation/abstract_split_read.cpp
@@ -50,6 +50,8 @@
 #include "paimon/common/types/data_field.h"
 #include "paimon/common/utils/arrow/status_utils.h"
 #include "paimon/common/utils/object_utils.h"
+#include "paimon/core/deletionvectors/apply_deletion_vector_batch_reader.h"
+#include "paimon/core/deletionvectors/bitmap64_deletion_vector.h"
 #include "paimon/core/deletionvectors/bitmap_deletion_vector.h"
 #include "paimon/core/io/complete_row_tracking_fields_reader.h"
 #include "paimon/core/io/data_file_meta.h"
@@ -185,8 +187,13 @@ Result<std::unique_ptr<FileBatchReader>> 
AbstractSplitRead::ApplyIndexAndDvReade
     const std::shared_ptr<DataFilePathFactory>& data_file_path_factory,
     const std::map<std::string, std::string>& file_index_options,
     std::vector<float>* index_scores) const {
+    std::shared_ptr<DeletionVector> deletion_vector;
+    if (dv_factory) {
+        PAIMON_ASSIGN_OR_RAISE(deletion_vector, dv_factory(file->file_name));
+    }
     std::shared_ptr<FileIndexResult> file_index_result;
-    if (options_.FileIndexReadEnabled()) {
+    if (options_.FileIndexReadEnabled() &&
+        dynamic_cast<Bitmap64DeletionVector*>(deletion_vector.get()) == 
nullptr) {
         PAIMON_ASSIGN_OR_RAISE(file_index_result,
                                FileIndexEvaluator::Evaluate(data_schema, 
file_index_options,
                                                             predicate, 
data_file_path_factory, file,
@@ -217,10 +224,6 @@ Result<std::unique_ptr<FileBatchReader>> 
AbstractSplitRead::ApplyIndexAndDvReade
     }
 
     // prepare deletion bitmap for deletion vector
-    std::shared_ptr<DeletionVector> deletion_vector;
-    if (dv_factory) {
-        PAIMON_ASSIGN_OR_RAISE(deletion_vector, dv_factory(file->file_name));
-    }
     const RoaringBitmap32* deletion = nullptr;
     if (auto* bitmap_dv = 
dynamic_cast<BitmapDeletionVector*>(deletion_vector.get())) {
         deletion = bitmap_dv->GetBitmap();
@@ -247,9 +250,8 @@ Result<std::unique_ptr<FileBatchReader>> 
AbstractSplitRead::ApplyIndexAndDvReade
                                                 std::move(actual_selection)));
 
     if (deletion_vector && !deletion && !deletion_vector->IsEmpty()) {
-        // TODO(xinyu.lxy): if deletion vector is bitmap64, use 
ApplyBitmapIndexBatchReader to
-        // filter result
-        return Status::NotImplemented("Only support BitmapDeletionVector");
+        reader =
+            
std::make_unique<ApplyDeletionVectorBatchReader>(std::move(reader), 
deletion_vector);
     }
     return reader;
 }
diff --git a/src/paimon/core/operation/data_evolution_split_read.cpp 
b/src/paimon/core/operation/data_evolution_split_read.cpp
index 7a1a7f18..5c26d341 100644
--- a/src/paimon/core/operation/data_evolution_split_read.cpp
+++ b/src/paimon/core/operation/data_evolution_split_read.cpp
@@ -97,8 +97,17 @@ class PositionShiftedDeletionVector : public DeletionVector {
     /// IsEmpty; the row counting that does consult a vector's cardinality 
builds its factory
     /// from the split's deletion files, never from this view.
     Result<int64_t> GetCardinality() const override {
-        PAIMON_ASSIGN_OR_RAISE(RoaringBitmap32 valid, inner_->IsValid(offset_, 
length_));
-        return length_ - valid.Cardinality();
+        int64_t count = 0;
+        PAIMON_RETURN_NOT_OK(ForEachDeletedPosition([&count](int64_t) { 
++count; }));
+        return count;
+    }
+
+    Status ForEachDeletedPosition(const std::function<void(int64_t)>& 
consumer) const override {
+        return inner_->ForEachDeletedPosition([&](int64_t position) {
+            if (position >= offset_ && position - offset_ < length_) {
+                consumer(position - offset_);
+            }
+        });
     }
 
     Status Delete(int64_t) override {
diff --git a/src/paimon/core/operation/data_evolution_split_read_test.cpp 
b/src/paimon/core/operation/data_evolution_split_read_test.cpp
index 6b4067b0..39fdb100 100644
--- a/src/paimon/core/operation/data_evolution_split_read_test.cpp
+++ b/src/paimon/core/operation/data_evolution_split_read_test.cpp
@@ -26,6 +26,7 @@
 #include "gtest/gtest.h"
 #include "paimon/common/data/binary_row.h"
 #include "paimon/common/table/special_fields.h"
+#include "paimon/core/deletionvectors/bitmap64_deletion_vector.h"
 #include "paimon/core/deletionvectors/bitmap_deletion_vector.h"
 #include "paimon/core/io/data_file_meta.h"
 #include "paimon/core/manifest/file_source.h"
@@ -872,4 +873,26 @@ TEST_F(DataEvolutionSplitReadTest, 
TestExcludeDeletedRowIds) {
     ASSERT_TRUE(remaining.empty());
 }
 
+TEST_F(DataEvolutionSplitReadTest, Bitmap64GroupWindowWithHighOffset) {
+    const int64_t offset = 1LL << 32;
+    auto anchor = CreateNormalFile("anchor.parquet", 100, offset + 30, 10);
+    auto blob = CreateDataFileMeta("blob0.blob", 100 + offset, 20, 20);
+    auto dv = std::make_shared<Bitmap64DeletionVector>();
+    for (int64_t p : {offset - 1, offset, offset + 5, offset + 20}) {
+        ASSERT_OK(dv->Delete(p));
+    }
+    DataEvolutionSplitRead::GroupDeletionVector group_dv{dv, Range(100, 100 + 
offset + 29)};
+    ASSERT_OK_AND_ASSIGN(auto factory,
+                         DataEvolutionSplitRead::CreateGroupDvFactory({anchor, 
blob}, group_dv));
+    ASSERT_OK_AND_ASSIGN(auto view, factory("blob0.blob"));
+    ASSERT_TRUE(view);
+    ASSERT_TRUE(view->IsDeleted(0).value());
+    ASSERT_TRUE(view->IsDeleted(5).value());
+    ASSERT_FALSE(view->IsDeleted(6).value());
+    ASSERT_EQ(view->GetCardinality().value(), 2);
+    std::vector<int64_t> positions;
+    ASSERT_OK(view->ForEachDeletedPosition([&](int64_t p) { 
positions.push_back(p); }));
+    ASSERT_EQ(positions, (std::vector<int64_t>{0, 5}));
+}
+
 }  // namespace paimon::test
diff --git a/src/paimon/core/operation/raw_file_split_read_test.cpp 
b/src/paimon/core/operation/raw_file_split_read_test.cpp
index c6ad7f98..77f92109 100644
--- a/src/paimon/core/operation/raw_file_split_read_test.cpp
+++ b/src/paimon/core/operation/raw_file_split_read_test.cpp
@@ -35,6 +35,7 @@
 #include "paimon/common/utils/arrow/mem_utils.h"
 #include "paimon/common/utils/arrow/status_utils.h"
 #include "paimon/core/core_options.h"
+#include "paimon/core/deletionvectors/bitmap64_deletion_vector.h"
 #include "paimon/core/deletionvectors/bitmap_deletion_vector.h"
 #include "paimon/core/global_index/indexed_split_impl.h"
 #include "paimon/core/io/data_file_meta.h"
@@ -52,6 +53,7 @@
 #include "paimon/fs/local/local_file_system.h"
 #include "paimon/memory/memory_pool.h"
 #include "paimon/predicate/full_text_search.h"
+#include "paimon/predicate/predicate_builder.h"
 #include "paimon/predicate/vector_search.h"
 #include "paimon/read_context.h"
 #include "paimon/status.h"
@@ -190,12 +192,17 @@ class RawFileSplitReadTest : public ::testing::Test {
     }
 
     void CheckReadResult(const std::shared_ptr<arrow::Schema>& read_schema,
-                         const std::shared_ptr<arrow::ChunkedArray>& 
expected_array) const {
+                         const std::shared_ptr<arrow::ChunkedArray>& 
expected_array,
+                         DeletionVector::Factory dv_factory = {}) const {
         std::string path = paimon::test::GetDataDir() +
                            "/orc/multi_partition_append_table.db/"
                            "multi_partition_append_table";
         ReadContextBuilder context_builder(path);
         context_builder.SetReadFieldNames(read_schema->field_names());
+        if (dv_factory) {
+            context_builder.AddOption(Options::FILE_INDEX_READ_ENABLED, 
"true");
+            context_builder.SetPredicate(PredicateBuilder::IsNotNull(0, "f0", 
FieldType::STRING));
+        }
         ASSERT_OK_AND_ASSIGN(std::shared_ptr<ReadContext> read_context, 
context_builder.Finish());
         SchemaManager schema_manager(std::make_shared<LocalFileSystem>(), 
read_context->GetPath());
         ASSERT_OK_AND_ASSIGN(auto table_schema, schema_manager.ReadSchema(0));
@@ -227,8 +234,18 @@ class RawFileSplitReadTest : public ::testing::Test {
         std::vector<std::unique_ptr<BatchReader>> batch_readers;
         batch_readers.reserve(data_splits.size());
         for (const auto& split : data_splits) {
-            ASSERT_OK_AND_ASSIGN(std::unique_ptr<BatchReader> reader,
-                                 split_read->CreateReader(split));
+            auto impl = std::dynamic_pointer_cast<DataSplitImpl>(split);
+            if (dv_factory) {
+                // A DV64 read must bypass file-index evaluation even for a 
small file.
+                for (const auto& file : impl->DataFiles()) {
+                    file->embedded_index = std::make_shared<Bytes>("invalid 
index", pool_.get());
+                }
+            }
+            ASSERT_OK_AND_ASSIGN(
+                std::unique_ptr<BatchReader> reader,
+                dv_factory ? split_read->CreateReader(impl->Partition(), 
impl->Bucket(),
+                                                      impl->DataFiles(), 
dv_factory, std::nullopt)
+                           : split_read->CreateReader(split));
             batch_readers.emplace_back(std::move(reader));
         }
         auto batch_reader =
@@ -874,4 +891,23 @@ TEST_F(RawFileSplitReadTest, TestMatch) {
     ASSERT_NOK(split_read->Match(nullptr, /*force_keep_delete=*/false));
 }
 
+TEST_F(RawFileSplitReadTest, TestBitmap64DeletionVectorFallback) {
+    auto read_schema =
+        arrow::schema({arrow::field("f0", arrow::utf8()), arrow::field("f1", 
arrow::int32()),
+                       arrow::field("f2", arrow::int32()), arrow::field("f3", 
arrow::float64())});
+    auto result_fields = read_schema->fields();
+    result_fields.insert(result_fields.begin(), arrow::field("_VALUE_KIND", 
arrow::int8()));
+    auto expected =
+        arrow::ipc::internal::json::ArrayFromJSON(
+            arrow::struct_(result_fields), R"([[0, "Bob", 10, 0, 12.1], [0, 
"Tony", 10, 0, 14.1]])")
+            .ValueOrDie();
+    DeletionVector::Factory factory =
+        [](const std::string& name) -> Result<std::shared_ptr<DeletionVector>> 
{
+        auto dv = std::make_shared<Bitmap64DeletionVector>();
+        PAIMON_RETURN_NOT_OK(dv->Delete(name.find("01b6a930") != 
std::string::npos ? 1 : 0));
+        return dv;
+    };
+    CheckReadResult(read_schema, 
std::make_shared<arrow::ChunkedArray>(expected), factory);
+}
+
 }  // namespace paimon::test
diff --git a/src/paimon/testing/utils/deletion_vector_test_helper.h 
b/src/paimon/testing/utils/deletion_vector_test_helper.h
index ebca6e4d..35adf7f5 100644
--- a/src/paimon/testing/utils/deletion_vector_test_helper.h
+++ b/src/paimon/testing/utils/deletion_vector_test_helper.h
@@ -31,6 +31,7 @@
 #include "paimon/common/types/data_field.h"
 #include "paimon/common/utils/range_helper.h"
 #include "paimon/core/core_options.h"
+#include "paimon/core/deletionvectors/bitmap64_deletion_vector.h"
 #include "paimon/core/deletionvectors/bitmap_deletion_vector.h"
 #include "paimon/core/deletionvectors/deletion_vector.h"
 #include "paimon/core/deletionvectors/deletion_vectors_index_file.h"
@@ -117,7 +118,12 @@ class DeletionVectorTestHelper {
         for (const auto& [file_name, deleted_positions] : 
deleted_positions_by_file) {
             // deleting through the vector itself applies the same position 
bound a writer is
             // held to, instead of narrowing to the bitmap's index type and 
wrapping silently
-            auto deletion_vector = 
std::make_shared<BitmapDeletionVector>(RoaringBitmap32());
+            std::shared_ptr<DeletionVector> deletion_vector;
+            if (core_options.DeletionVectorsBitmap64()) {
+                deletion_vector = std::make_shared<Bitmap64DeletionVector>();
+            } else {
+                deletion_vector = 
std::make_shared<BitmapDeletionVector>(RoaringBitmap32());
+            }
             for (int64_t position : deleted_positions) {
                 if (position < 0) {
                     return Status::Invalid(fmt::format(
diff --git a/test/inte/data_evolution_table_test.cpp 
b/test/inte/data_evolution_table_test.cpp
index 05da479d..804e7c26 100644
--- a/test/inte/data_evolution_table_test.cpp
+++ b/test/inte/data_evolution_table_test.cpp
@@ -48,11 +48,12 @@
 #include "paimon/testing/utils/test_helper.h"
 #include "paimon/testing/utils/testharness.h"
 namespace paimon::test {
-using DataEvolutionTableParam = std::tuple<std::string, bool>;
+using DataEvolutionTableParam = std::tuple<std::string, bool, bool>;
 
 // This is a sdk end-to-end test for data evolution
 class DataEvolutionTableTest : public ::testing::Test,
                                public 
::testing::WithParamInterface<DataEvolutionTableParam> {
+ protected:
     void SetUp() override {
         dir_ = UniqueTestDirectory::Create("local");
         int64_t seed = DateTimeUtils::GetCurrentUTCTimeUs();
@@ -168,6 +169,8 @@ class DataEvolutionTableTest : public ::testing::Test,
                                                       
{Options::DATA_EVOLUTION_ENABLED, "true"}};
         if (deletion_vectors_enabled) {
             options.emplace(Options::DELETION_VECTORS_ENABLED, "true");
+            options.emplace(Options::DELETION_VECTOR_BITMAP64,
+                            std::get<2>(GetParam()) ? "true" : "false");
         }
         options.insert(extra_options.begin(), extra_options.end());
         CreateTable(/*partition_keys=*/{}, options);
@@ -682,7 +685,7 @@ class DataEvolutionTableTest : public ::testing::Test,
                 .ValueOrDie());
     }
 
- private:
+ protected:
     std::unique_ptr<UniqueTestDirectory> dir_;
     mutable std::shared_ptr<Cache> snapshot_live_manifest_cache_;
     arrow::FieldVector fields_ = {
@@ -692,6 +695,8 @@ class DataEvolutionTableTest : public ::testing::Test,
     };
 };
 
+class DataEvolutionDvTableTest : public DataEvolutionTableTest {};
+
 TEST_P(DataEvolutionTableTest, TestBasic) {
     CreateTable();
     std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
@@ -2935,15 +2940,15 @@ TEST_P(DataEvolutionTableTest, TestWithRowIds) {
     }
 }
 
-TEST_P(DataEvolutionTableTest, TestGlobalIndexScoresWithSingleFileFiltering) {
+TEST_P(DataEvolutionDvTableTest, TestGlobalIndexScoresWithSingleFileFiltering) 
{
     CheckGlobalIndexScoresWithFiltering(/*merge_files=*/false);
 }
 
-TEST_P(DataEvolutionTableTest, TestGlobalIndexScoresWithMergedFileFiltering) {
+TEST_P(DataEvolutionDvTableTest, TestGlobalIndexScoresWithMergedFileFiltering) 
{
     CheckGlobalIndexScoresWithFiltering(/*merge_files=*/true);
 }
 
-TEST_P(DataEvolutionTableTest, TestReadWithDeletionVectors) {
+TEST_P(DataEvolutionDvTableTest, TestReadWithDeletionVectors) {
     CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true);
     std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
     auto schema = arrow::schema(fields_);
@@ -3020,7 +3025,7 @@ TEST_P(DataEvolutionTableTest, 
TestReadWithDeletionVectors) {
                           /*predicate=*/nullptr, /*row_ranges=*/{Range(1, 
2)}));
 }
 
-TEST_P(DataEvolutionTableTest, TestReadWithDeletionVectorsAcrossReadBatches) {
+TEST_P(DataEvolutionDvTableTest, TestReadWithDeletionVectorsAcrossReadBatches) 
{
     // the 12 rows below span several read batches, so the deletion vector 
empties a whole
     // batch of every file of the group: each file reader then skips that 
batch entirely and
     // the column merge has to stay aligned on the surviving row count alone
@@ -3069,7 +3074,7 @@ TEST_P(DataEvolutionTableTest, 
TestReadWithDeletionVectorsAcrossReadBatches) {
                           /*predicate=*/nullptr, /*row_ranges=*/{Range(3, 
8)}));
 }
 
-TEST_P(DataEvolutionTableTest, 
TestReadWithDeletionVectorsOnPartOfRowRangeGroups) {
+TEST_P(DataEvolutionDvTableTest, 
TestReadWithDeletionVectorsOnPartOfRowRangeGroups) {
     // one split per row range group, so a group's deletion file must not 
reach the other
     CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true,
                              {{Options::SOURCE_SPLIT_TARGET_SIZE, "1"}});
@@ -3139,7 +3144,7 @@ TEST_P(DataEvolutionTableTest, 
TestReadWithDeletionVectorsOnPartOfRowRangeGroups
     ASSERT_OK(ScanAndRead(table_path, {"_ROW_ID"}, expected_row_ids));
 }
 
-TEST_P(DataEvolutionTableTest, 
TestReadWithDeletionVectorsOnEveryRowRangeGroup) {
+TEST_P(DataEvolutionDvTableTest, 
TestReadWithDeletionVectorsOnEveryRowRangeGroup) {
     CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true);
     std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
     auto schema = arrow::schema(fields_);
@@ -3225,7 +3230,7 @@ TEST_P(DataEvolutionTableTest, 
TestReadWithDeletionVectorsOnEveryRowRangeGroup)
                           /*predicate=*/nullptr, /*row_ranges=*/{Range(2, 
5)}));
 }
 
-TEST_P(DataEvolutionTableTest, TestReadWithFullyDeletedRowRangeGroup) {
+TEST_P(DataEvolutionDvTableTest, TestReadWithFullyDeletedRowRangeGroup) {
     CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true);
     std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
     auto schema = arrow::schema(fields_);
@@ -3284,7 +3289,7 @@ TEST_P(DataEvolutionTableTest, 
TestReadWithFullyDeletedRowRangeGroup) {
     ASSERT_FALSE(only_deleted.rows);
 }
 
-TEST_P(DataEvolutionTableTest, TestReadAfterUpdatingDeletionVectors) {
+TEST_P(DataEvolutionDvTableTest, TestReadAfterUpdatingDeletionVectors) {
     CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true);
     std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
     auto schema = arrow::schema(fields_);
@@ -3330,7 +3335,7 @@ TEST_P(DataEvolutionTableTest, 
TestReadAfterUpdatingDeletionVectors) {
     ASSERT_OK(ScanAndRead(table_path, schema->field_names(), 
expected_after_update));
 }
 
-TEST_P(DataEvolutionTableTest, TestReadWithDeletionVectorsAfterAddingColumn) {
+TEST_P(DataEvolutionDvTableTest, TestReadWithDeletionVectorsAfterAddingColumn) 
{
     if (FileFormat() == "avro") {
         GTEST_SKIP() << "Avro has no stats, which the added column's scan 
pruning relies on";
     }
@@ -3392,7 +3397,7 @@ TEST_P(DataEvolutionTableTest, 
TestReadWithDeletionVectorsAfterAddingColumn) {
     ASSERT_OK(ScanAndRead(table_path, {"f3"}, expected_f3));
 }
 
-TEST_P(DataEvolutionTableTest, 
TestLimitPushDownWithHeavilyDeletedFirstRowRangeGroup) {
+TEST_P(DataEvolutionDvTableTest, 
TestLimitPushDownWithHeavilyDeletedFirstRowRangeGroup) {
     // one split per row range group, so the limit has to span both to be 
satisfied
     CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true,
                              {{Options::SOURCE_SPLIT_TARGET_SIZE, "1"}});
@@ -3541,23 +3546,36 @@ TEST_P(DataEvolutionTableTest, 
TestLimitPushDownDisabledByRowRangeIndex) {
 std::vector<DataEvolutionTableParam> GetTestValuesForDataEvolutionTableTest() {
     std::vector<DataEvolutionTableParam> values;
     for (bool enable_snapshot_live_manifest_cache : {false, true}) {
-        values.emplace_back("parquet", enable_snapshot_live_manifest_cache);
+        values.emplace_back("parquet", enable_snapshot_live_manifest_cache, 
/*bitmap64=*/false);
 #ifdef PAIMON_ENABLE_MOSAIC
-        values.emplace_back("mosaic", enable_snapshot_live_manifest_cache);
+        values.emplace_back("mosaic", enable_snapshot_live_manifest_cache, 
/*bitmap64=*/false);
 #endif
 #ifdef PAIMON_ENABLE_LANCE
-        values.emplace_back("lance", enable_snapshot_live_manifest_cache);
+        values.emplace_back("lance", enable_snapshot_live_manifest_cache, 
/*bitmap64=*/false);
 #endif
 #ifdef PAIMON_ENABLE_ORC
-        values.emplace_back("orc", enable_snapshot_live_manifest_cache);
+        values.emplace_back("orc", enable_snapshot_live_manifest_cache, 
/*bitmap64=*/false);
 #endif
 #ifdef PAIMON_ENABLE_AVRO
-        values.emplace_back("avro", enable_snapshot_live_manifest_cache);
+        values.emplace_back("avro", enable_snapshot_live_manifest_cache, 
/*bitmap64=*/false);
 #endif
     }
     return values;
 }
 
+std::vector<DataEvolutionTableParam> 
GetTestValuesForDataEvolutionDvTableTest() {
+    std::vector<DataEvolutionTableParam> values;
+    for (const auto& param : GetTestValuesForDataEvolutionTableTest()) {
+        for (const auto& bitmap64 : {false, true}) {
+            values.emplace_back(std::get<0>(param), std::get<1>(param), 
bitmap64);
+        }
+    }
+    return values;
+}
+
+INSTANTIATE_TEST_SUITE_P(FileFormat, DataEvolutionDvTableTest,
+                         
::testing::ValuesIn(GetTestValuesForDataEvolutionDvTableTest()));
+
 INSTANTIATE_TEST_SUITE_P(FileFormat, DataEvolutionTableTest,
                          
::testing::ValuesIn(GetTestValuesForDataEvolutionTableTest()));
 
diff --git a/test/inte/pk_compaction_inte_test.cpp 
b/test/inte/pk_compaction_inte_test.cpp
index 7e713b06..153a7926 100644
--- a/test/inte/pk_compaction_inte_test.cpp
+++ b/test/inte/pk_compaction_inte_test.cpp
@@ -20,6 +20,7 @@
 #include <memory>
 #include <optional>
 #include <string>
+#include <tuple>
 #include <utility>
 #include <vector>
 
@@ -35,6 +36,7 @@
 #include "paimon/common/table/special_fields.h"
 #include "paimon/common/utils/checked_cast.h"
 #include "paimon/common/utils/path_util.h"
+#include "paimon/core/deletionvectors/bitmap64_deletion_vector.h"
 #include "paimon/core/deletionvectors/deletion_vectors_index_file.h"
 #include "paimon/core/io/data_file_meta.h"
 #include "paimon/core/manifest/file_source.h"
@@ -45,12 +47,14 @@
 #include "paimon/defs.h"
 #include "paimon/file_store_commit.h"
 #include "paimon/file_store_write.h"
+#include "paimon/format/file_format_factory.h"
 #include "paimon/fs/file_system.h"
 #include "paimon/fs/local/local_file_system.h"
 #include "paimon/memory/memory_pool.h"
 #include "paimon/predicate/literal.h"
 #include "paimon/predicate/predicate_builder.h"
 #include "paimon/read_context.h"
+#include "paimon/reader/prefetch_file_batch_reader.h"
 #include "paimon/record_batch.h"
 #include "paimon/result.h"
 #include "paimon/scan_context.h"
@@ -67,8 +71,188 @@
 
 namespace paimon::test {
 
-class PkCompactionInteTest : public ::testing::Test,
-                             public ::testing::WithParamInterface<std::string> 
{
+// Stores a few real Parquet rows and exposes sparse file positions to 
exercise the full
+// lookup/DV path. This simulates large positions, not a physically large 
Parquet file.
+class HighPositionFileBatchReader : public PrefetchFileBatchReader {
+ public:
+    explicit 
HighPositionFileBatchReader(std::unique_ptr<PrefetchFileBatchReader> reader)
+        : reader_(std::move(reader)) {}
+
+    Result<ReadBatch> NextBatch() override {
+        return reader_->NextBatch();
+    }
+
+    Result<ReadBatchWithBitmap> NextBatchWithBitmap() override {
+        return reader_->NextBatchWithBitmap();
+    }
+
+    Result<std::unique_ptr<::ArrowSchema>> GetFileSchema() const override {
+        return reader_->GetFileSchema();
+    }
+
+    Status SetReadSchema(::ArrowSchema* schema, const 
std::shared_ptr<Predicate>& predicate,
+                         const std::optional<RoaringBitmap32>& selection) 
override {
+        if (selection) {
+            return Status::Invalid("High position test format does not support 
bitmap32 selection");
+        }
+        return reader_->SetReadSchema(schema, predicate, std::nullopt);
+    }
+
+    Result<uint64_t> GetPreviousBatchFileRowId(uint64_t batch_row_id) const 
override {
+        PAIMON_ASSIGN_OR_RAISE(uint64_t position, 
reader_->GetPreviousBatchFileRowId(batch_row_id));
+        return ToHighPosition(position);
+    }
+
+    Result<uint64_t> GetNumberOfRows() const override {
+        PAIMON_ASSIGN_OR_RAISE(uint64_t count, reader_->GetNumberOfRows());
+        return ToHighPosition(count);
+    }
+
+    Status SeekToRow(uint64_t row_number) override {
+        return reader_->SeekToRow(ToPhysicalPosition(row_number));
+    }
+
+    Result<uint64_t> GetNextRowToRead() const override {
+        PAIMON_ASSIGN_OR_RAISE(uint64_t position, reader_->GetNextRowToRead());
+        return ToHighPosition(position);
+    }
+
+    Result<std::vector<std::pair<uint64_t, uint64_t>>> GenReadRanges(
+        bool* need_prefetch) const override {
+        std::vector<std::pair<uint64_t, uint64_t>> ranges;
+        PAIMON_ASSIGN_OR_RAISE(ranges, reader_->GenReadRanges(need_prefetch));
+        for (auto& range : ranges) {
+            range = {ToHighPosition(range.first), 
ToHighPosition(range.second)};
+        }
+        return ranges;
+    }
+
+    Status SetReadRanges(const std::vector<std::pair<uint64_t, uint64_t>>& 
ranges) override {
+        std::vector<std::pair<uint64_t, uint64_t>> physical_ranges;
+        for (const auto& range : ranges) {
+            physical_ranges.emplace_back(ToPhysicalPosition(range.first),
+                                         ToPhysicalPosition(range.second));
+        }
+        return reader_->SetReadRanges(physical_ranges);
+    }
+
+    Result<std::vector<std::pair<uint64_t, uint64_t>>> PreBufferRange() 
override {
+        return reader_->PreBufferRange();
+    }
+
+    void SetPreBufferRangeCallback(PreBufferRangeCallback callback) override {
+        reader_->SetPreBufferRangeCallback(std::move(callback));
+    }
+
+    bool SupportPreciseBitmapSelection() const override {
+        return false;
+    }
+
+    std::shared_ptr<Metrics> GetReaderMetrics() const override {
+        return reader_->GetReaderMetrics();
+    }
+
+    void Close() override {
+        reader_->Close();
+    }
+
+    void Warmup() override {
+        reader_->Warmup();
+    }
+
+ private:
+    static uint64_t ToHighPosition(uint64_t position) {
+        return (position << 32) + 7;
+    }
+
+    static uint64_t ToPhysicalPosition(uint64_t position) {
+        // Seek to the first stored row at or after a position in the sparse 
coordinate space.
+        return position <= 7 ? 0 : ((position - 8) >> 32) + 1;
+    }
+
+    std::unique_ptr<PrefetchFileBatchReader> reader_;
+};
+
+class HighPositionReaderBuilder : public ReaderBuilder {
+ public:
+    explicit HighPositionReaderBuilder(std::unique_ptr<ReaderBuilder> builder)
+        : builder_(std::move(builder)) {}
+
+    ReaderBuilder* WithMemoryPool(const std::shared_ptr<MemoryPool>& pool) 
override {
+        builder_->WithMemoryPool(pool);
+        return this;
+    }
+
+    ReaderBuilder* WithCache(const std::shared_ptr<Cache>& cache) override {
+        builder_->WithCache(cache);
+        return this;
+    }
+
+    ReaderBuilder* WithReadHints(const std::optional<ReadHints>& hints) 
override {
+        builder_->WithReadHints(hints);
+        return this;
+    }
+
+    Result<std::unique_ptr<FileBatchReader>> Build(
+        const std::shared_ptr<InputStream>& input) const override {
+        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileBatchReader> reader, 
builder_->Build(input));
+        return 
std::unique_ptr<FileBatchReader>(std::make_unique<HighPositionFileBatchReader>(
+            checked_pointer_cast<PrefetchFileBatchReader>(std::move(reader))));
+    }
+
+ private:
+    std::unique_ptr<ReaderBuilder> builder_;
+};
+
+class HighPositionFileFormat : public FileFormat {
+ public:
+    explicit HighPositionFileFormat(std::unique_ptr<FileFormat> parquet)
+        : parquet_(std::move(parquet)) {}
+
+    const std::string& Identifier() const override {
+        return identifier_;
+    }
+
+    Result<std::unique_ptr<ReaderBuilder>> CreateReaderBuilder(int32_t 
batch_size) const override {
+        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ReaderBuilder> builder,
+                               parquet_->CreateReaderBuilder(batch_size));
+        return std::unique_ptr<ReaderBuilder>(
+            std::make_unique<HighPositionReaderBuilder>(std::move(builder)));
+    }
+
+    Result<std::unique_ptr<WriterBuilder>> CreateWriterBuilder(::ArrowSchema* 
schema,
+                                                               int32_t 
batch_size) const override {
+        return parquet_->CreateWriterBuilder(schema, batch_size);
+    }
+
+    Result<std::unique_ptr<FormatStatsExtractor>> CreateStatsExtractor(
+        ::ArrowSchema* schema) const override {
+        return parquet_->CreateStatsExtractor(schema);
+    }
+
+ private:
+    const std::string identifier_ = "high_position_parquet";
+    std::unique_ptr<FileFormat> parquet_;
+};
+
+class HighPositionFileFormatFactory : public FileFormatFactory {
+ public:
+    const char* Identifier() const override {
+        return "high_position_parquet";
+    }
+
+    Result<std::unique_ptr<FileFormat>> Create(
+        const std::map<std::string, std::string>& options) const override {
+        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileFormat> parquet,
+                               FileFormatFactory::Get("parquet", options));
+        return std::unique_ptr<FileFormat>(
+            std::make_unique<HighPositionFileFormat>(std::move(parquet)));
+    }
+};
+
+REGISTER_PAIMON_FACTORY(HighPositionFileFormatFactory);
+
+class PkCompactionInteTest : public ::testing::Test {
  public:
     void SetUp() override {
         pool_ = GetDefaultPool();
@@ -211,8 +395,10 @@ class PkCompactionInteTest : public ::testing::Test,
 
     Result<std::vector<std::shared_ptr<CommitMessage>>> CompactAndCommit(
         const std::string& table_path, const std::map<std::string, 
std::string>& partition,
-        int32_t bucket, bool full_compaction, int64_t commit_identifier) {
+        int32_t bucket, bool full_compaction, int64_t commit_identifier,
+        const std::map<std::string, std::string>& options = {}) {
         WriteContextBuilder write_builder(table_path, "commit_user_1");
+        write_builder.SetOptions(options);
         write_builder.WithStreamingMode(true).WithTempDirectory(
             PathUtil::JoinPath(dir_->Str(), "tmp"));
         PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<WriteContext> write_context, 
write_builder.Finish());
@@ -378,12 +564,22 @@ class PkCompactionInteTest : public ::testing::Test,
         return split.value();
     }
 
- private:
+ protected:
     std::shared_ptr<MemoryPool> pool_;
     std::unique_ptr<UniqueTestDirectory> dir_;
     arrow::FieldVector fields_;
 };
 
+class PkCompactionFormatInteTest : public PkCompactionInteTest,
+                                   public 
::testing::WithParamInterface<std::string> {};
+
+class PkCompactionDvInteTest : public PkCompactionInteTest,
+                               public ::testing::WithParamInterface<bool> {};
+
+class PkCompactionFormatDvInteTest
+    : public PkCompactionInteTest,
+      public ::testing::WithParamInterface<std::tuple<std::string, bool>> {};
+
 TEST_F(PkCompactionInteTest, TestMetadataOnlyLevelUpgradeKeepsValueStats) {
     arrow::FieldVector fields = {arrow::field("id", arrow::int32()),
                                  arrow::field("value", arrow::utf8())};
@@ -442,7 +638,7 @@ TEST_F(PkCompactionInteTest, 
TestMetadataOnlyLevelUpgradeKeepsValueStats) {
 }
 
 // Verify shared-shredding MAP can be read correctly after PK full compaction.
-TEST_P(PkCompactionInteTest, 
TestKeyValueTableFullCompactionWithMapSharedShredding) {
+TEST_P(PkCompactionFormatInteTest, 
TestKeyValueTableFullCompactionWithMapSharedShredding) {
     auto file_format = GetParam();
     if (file_format == "avro" || file_format == "mosaic" || file_format == 
"lance") {
         return;
@@ -514,8 +710,8 @@ TEST_P(PkCompactionInteTest, 
TestKeyValueTableFullCompactionWithMapSharedShreddi
     ASSERT_TRUE(success);
 }
 
-TEST_P(PkCompactionInteTest, 
TestKeyValueTableDvCompactionWithMapSharedShredding) {
-    auto file_format = GetParam();
+TEST_P(PkCompactionFormatDvInteTest, 
TestKeyValueTableDvCompactionWithMapSharedShredding) {
+    auto file_format = std::get<0>(GetParam());
     if (file_format == "avro" || file_format == "mosaic" || file_format == 
"lance") {
         return;
     }
@@ -535,6 +731,7 @@ TEST_P(PkCompactionInteTest, 
TestKeyValueTableDvCompactionWithMapSharedShredding
         {Options::FILE_SYSTEM, "local"},
         {Options::FILE_COMPRESSION, "none"},
         {Options::DELETION_VECTORS_ENABLED, "true"},
+        {Options::DELETION_VECTOR_BITMAP64, std::get<1>(GetParam()) ? "true" : 
"false"},
         {"fields.tags.map.storage-layout", "shared-shredding"},
         {"fields.tags.map.shared-shredding.max-columns", "2"},
         {"parquet.page.size", "1"},
@@ -640,7 +837,7 @@ TEST_P(PkCompactionInteTest, 
TestKeyValueTableDvCompactionWithMapSharedShredding
 //   7. ScanAndVerify after DV compact (data read with DV applied)
 //   8. Full compact to merge everything
 //   9. ScanAndVerify after full compact (all data merged)
-TEST_F(PkCompactionInteTest, DeduplicateWithDeletionVectors) {
+TEST_P(PkCompactionDvInteTest, DeduplicateWithDeletionVectors) {
     // f4 is a large padding field to make the initial file substantially 
bigger than
     // subsequent small level-0 files, preventing PickForSizeRatio from 
merging them all.
     arrow::FieldVector fields = {
@@ -649,11 +846,13 @@ TEST_F(PkCompactionInteTest, 
DeduplicateWithDeletionVectors) {
         arrow::field("f4", arrow::utf8())};
     std::vector<std::string> primary_keys = {"f0", "f1", "f2"};
     std::vector<std::string> partition_keys = {"f1"};
-    std::map<std::string, std::string> options = {{Options::FILE_FORMAT, 
"parquet"},
-                                                  {Options::BUCKET, "2"},
-                                                  {Options::BUCKET_KEY, "f2"},
-                                                  {Options::FILE_SYSTEM, 
"local"},
-                                                  
{Options::DELETION_VECTORS_ENABLED, "true"}};
+    std::map<std::string, std::string> options = {
+        {Options::FILE_FORMAT, "parquet"},
+        {Options::BUCKET, "2"},
+        {Options::BUCKET_KEY, "f2"},
+        {Options::FILE_SYSTEM, "local"},
+        {Options::DELETION_VECTORS_ENABLED, "true"},
+        {Options::DELETION_VECTOR_BITMAP64, GetParam() ? "true" : "false"}};
     CreateTable(fields, partition_keys, primary_keys, options);
     std::string table_path = TablePath();
     auto data_type = arrow::struct_(fields);
@@ -754,6 +953,111 @@ TEST_F(PkCompactionInteTest, 
DeduplicateWithDeletionVectors) {
     }
 }
 
+TEST_F(PkCompactionInteTest, DeduplicateWithBitmap64HighPositions) {
+    arrow::FieldVector fields = {
+        arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::int32()),
+        arrow::field("f2", arrow::int32()), arrow::field("f3", 
arrow::float64()),
+        arrow::field("f4", arrow::utf8())};
+    CreateTable(fields, /*partition_keys=*/{"f1"}, /*primary_keys=*/{"f0", 
"f1", "f2"},
+                {{Options::FILE_FORMAT, "high_position_parquet"},
+                 {Options::BUCKET, "2"},
+                 {Options::BUCKET_KEY, "f2"},
+                 {Options::FILE_SYSTEM, "local"},
+                 {Options::DELETION_VECTORS_ENABLED, "true"},
+                 {Options::DELETION_VECTOR_BITMAP64, "true"},
+                 {Options::READ_BATCH_SIZE, "2"}});
+    std::string table_path = TablePath();
+    auto data_type = arrow::struct_(fields);
+    int64_t commit_id = 0;
+    std::string padding(2048, 'X');
+    auto initial = arrow::ipc::internal::json::ArrayFromJSON(data_type, R"([
+        ["Alice", 10, 0, 1.0, ")" + padding + R"("],
+        ["Bob",   10, 0, 2.0, ")" + padding + R"("],
+        ["Carol", 10, 0, 3.0, ")" + padding + R"("],
+        ["Dave",  10, 0, 4.0, ")" + padding + R"("],
+        ["Eve",   10, 0, 5.0, ")" + padding + R"("]
+    ])")
+                       .ValueOrDie();
+    ASSERT_OK(WriteAndCommit(table_path, {{"f1", "10"}}, 0, initial, 
commit_id++));
+    ASSERT_OK_AND_ASSIGN(auto upgrade_msgs,
+                         CompactAndCommit(table_path, {{"f1", "10"}}, 0,
+                                          /*full_compaction=*/true, 
commit_id++));
+    ASSERT_FALSE(HasDeletionVectorIndexFiles(upgrade_msgs));
+
+    // Two small L0 files trigger lookup against the larger, upgraded file. 
Bob and Carol
+    // occupy distinct positions above UINT32_MAX, both sharing Alice's low 32 
bits (7).
+    for (const auto& update :
+         {R"([["Bob", 10, 0, 102.0, "u1"]])", R"([["Carol", 10, 0, 203.0, 
"u2"]])"}) {
+        auto array = arrow::ipc::internal::json::ArrayFromJSON(data_type, 
update).ValueOrDie();
+        ASSERT_OK(WriteAndCommit(table_path, {{"f1", "10"}}, 0, array, 
commit_id++));
+    }
+    ASSERT_OK_AND_ASSIGN(auto compact_msgs,
+                         CompactAndCommit(table_path, {{"f1", "10"}}, 0,
+                                          /*full_compaction=*/false, 
commit_id++));
+    ASSERT_TRUE(HasDeletionVectorIndexFiles(compact_msgs));
+
+    // Reload committed metadata and DV bytes; no test-created DV or lookup 
result is injected.
+    ScanContextBuilder scan_builder(table_path);
+    ASSERT_OK_AND_ASSIGN(auto scan_context, scan_builder.Finish());
+    ASSERT_OK_AND_ASSIGN(auto scan, 
TableScan::Create(std::move(scan_context)));
+    ASSERT_OK_AND_ASSIGN(auto plan, scan->CreatePlan());
+    int32_t dv_count = 0;
+    for (const auto& split : plan->Splits()) {
+        auto data_split = std::dynamic_pointer_cast<DataSplitImpl>(split);
+        ASSERT_TRUE(data_split);
+        auto deletion_files = 
DeletionVector::CreateDeletionFileMap(data_split->DataFiles(),
+                                                                    
data_split->DeletionFiles());
+        auto factory = DeletionVector::CreateFactory(dir_->GetFileSystem(), 
deletion_files, pool_);
+        for (const auto& file : data_split->DataFiles()) {
+            if (deletion_files.count(file->file_name) == 0) {
+                continue;
+            }
+            ASSERT_EQ(file->row_count, 5);
+            ASSERT_OK_AND_ASSIGN(auto dv, factory(file->file_name));
+            ASSERT_TRUE(std::dynamic_pointer_cast<Bitmap64DeletionVector>(dv));
+            ASSERT_OK_AND_ASSIGN(int64_t cardinality, dv->GetCardinality());
+            ASSERT_EQ(cardinality, 2);
+            for (const auto& position : {(1LL << 32) + 7, (2LL << 32) + 7}) {
+                ASSERT_OK_AND_ASSIGN(bool deleted, dv->IsDeleted(position));
+                ASSERT_TRUE(deleted);
+            }
+            for (const auto& position : {7LL, (3LL << 32) + 7, (4LL << 32) + 
7}) {
+                ASSERT_OK_AND_ASSIGN(bool deleted, dv->IsDeleted(position));
+                ASSERT_FALSE(deleted);
+            }
+            ++dv_count;
+        }
+    }
+    ASSERT_EQ(dv_count, 1);
+
+    std::map<std::pair<std::string, int32_t>, std::string> expected_data;
+    expected_data[{"f1=10/", 0}] = R"([
+        [0, "Alice", 10, 0, 1.0, ")" +
+                                   padding + R"("],
+        [0, "Dave",  10, 0, 4.0, ")" +
+                                   padding + R"("],
+        [0, "Eve",   10, 0, 5.0, ")" +
+                                   padding + R"("],
+        [0, "Bob",   10, 0, 102.0, "u1"],
+        [0, "Carol", 10, 0, 203.0, "u2"]
+    ])";
+    ScanAndVerify(table_path, fields, expected_data);
+
+    ASSERT_OK(CompactAndCommit(table_path, {{"f1", "10"}}, 0,
+                               /*full_compaction=*/true, commit_id++));
+    expected_data[{"f1=10/", 0}] = R"([
+        [0, "Alice", 10, 0, 1.0, ")" +
+                                   padding + R"("],
+        [0, "Bob",   10, 0, 102.0, "u1"],
+        [0, "Carol", 10, 0, 203.0, "u2"],
+        [0, "Dave",  10, 0, 4.0, ")" +
+                                   padding + R"("],
+        [0, "Eve",   10, 0, 5.0, ")" +
+                                   padding + R"("]
+    ])";
+    ScanAndVerify(table_path, fields, expected_data);
+}
+
 // Test: PK table compact writes output files to external path.
 // Verifies that after configuring external paths with round-robin strategy,
 // compact output files (compactAfter) have their external_path set, and the
@@ -863,7 +1167,7 @@ TEST_F(PkCompactionInteTest, CompactWithExternalPath) {
 //   7. ScanAndVerify after DV compact
 //   8. Full compact to merge everything
 //   9. ScanAndVerify after full compact
-TEST_F(PkCompactionInteTest, AggMinWithAllNonNestedTypes) {
+TEST_P(PkCompactionDvInteTest, AggMinWithAllNonNestedTypes) {
     // f15 is a large padding field to inflate the initial file size for DV 
strategy.
     arrow::FieldVector fields = {arrow::field("f0", arrow::utf8()),   // PK
                                  arrow::field("f1", arrow::int32()),  // PK
@@ -884,13 +1188,15 @@ TEST_F(PkCompactionInteTest, 
AggMinWithAllNonNestedTypes) {
     std::vector<std::string> primary_keys = {"f0", "f1"};
     std::vector<std::string> partition_keys = {};
 
-    std::map<std::string, std::string> options = {{Options::FILE_FORMAT, 
"parquet"},
-                                                  {Options::BUCKET, "1"},
-                                                  {Options::FILE_SYSTEM, 
"local"},
-                                                  {Options::MERGE_ENGINE, 
"aggregation"},
-                                                  
{Options::FIELDS_DEFAULT_AGG_FUNC, "min"},
-                                                  
{Options::DELETION_VECTORS_ENABLED, "true"},
-                                                  
{"fields.f7.aggregate-function", "bool_and"}};
+    std::map<std::string, std::string> options = {
+        {Options::FILE_FORMAT, "parquet"},
+        {Options::BUCKET, "1"},
+        {Options::FILE_SYSTEM, "local"},
+        {Options::MERGE_ENGINE, "aggregation"},
+        {Options::FIELDS_DEFAULT_AGG_FUNC, "min"},
+        {Options::DELETION_VECTORS_ENABLED, "true"},
+        {Options::DELETION_VECTOR_BITMAP64, GetParam() ? "true" : "false"},
+        {"fields.f7.aggregate-function", "bool_and"}};
     CreateTable(fields, partition_keys, primary_keys, options);
     std::string table_path = TablePath();
     auto data_type = arrow::struct_(fields);
@@ -1004,7 +1310,7 @@ TEST_F(PkCompactionInteTest, AggMinWithAllNonNestedTypes) 
{
 //   7. ScanAndVerify after DV compact
 //   8. Full compact
 //   9. ScanAndVerify after full compact
-TEST_F(PkCompactionInteTest, AggMinWithPkInMiddle) {
+TEST_P(PkCompactionDvInteTest, AggMinWithPkInMiddle) {
     // f4 is a large padding field to inflate the initial file size for DV 
strategy.
     // PK fields f1(utf8) and f2(int32) are deliberately placed in the middle 
of the schema,
     // and the PK declaration order (f2, f1) differs from the schema order 
(f1, f2).
@@ -1018,9 +1324,13 @@ TEST_F(PkCompactionInteTest, AggMinWithPkInMiddle) {
     std::vector<std::string> partition_keys = {};
 
     std::map<std::string, std::string> options = {
-        {Options::FILE_FORMAT, "parquet"},         {Options::BUCKET, "1"},
-        {Options::FILE_SYSTEM, "local"},           {Options::MERGE_ENGINE, 
"aggregation"},
-        {Options::FIELDS_DEFAULT_AGG_FUNC, "min"}, 
{Options::DELETION_VECTORS_ENABLED, "true"}};
+        {Options::FILE_FORMAT, "parquet"},
+        {Options::BUCKET, "1"},
+        {Options::FILE_SYSTEM, "local"},
+        {Options::MERGE_ENGINE, "aggregation"},
+        {Options::FIELDS_DEFAULT_AGG_FUNC, "min"},
+        {Options::DELETION_VECTORS_ENABLED, "true"},
+        {Options::DELETION_VECTOR_BITMAP64, GetParam() ? "true" : "false"}};
     CreateTable(fields, partition_keys, primary_keys, options);
     std::string table_path = TablePath();
     auto data_type = arrow::struct_(fields);
@@ -1128,7 +1438,7 @@ TEST_F(PkCompactionInteTest, AggMinWithPkInMiddle) {
 //   7. ScanAndVerify after DV compact
 //   8. Full compact
 //   9. ScanAndVerify after full compact
-TEST_F(PkCompactionInteTest, DeduplicateWithSequenceFieldAndPkInMiddle) {
+TEST_P(PkCompactionDvInteTest, DeduplicateWithSequenceFieldAndPkInMiddle) {
     arrow::FieldVector fields = {
         arrow::field("f0", arrow::int32()),    // value
         arrow::field("f1", arrow::utf8()),     // PK
@@ -1141,9 +1451,13 @@ TEST_F(PkCompactionInteTest, 
DeduplicateWithSequenceFieldAndPkInMiddle) {
     std::vector<std::string> partition_keys = {};
 
     std::map<std::string, std::string> options = {
-        {Options::FILE_FORMAT, "parquet"},  {Options::BUCKET, "1"},
-        {Options::FILE_SYSTEM, "local"},    {Options::MERGE_ENGINE, 
"deduplicate"},
-        {Options::SEQUENCE_FIELD, "s1,s0"}, 
{Options::DELETION_VECTORS_ENABLED, "true"}};
+        {Options::FILE_FORMAT, "parquet"},
+        {Options::BUCKET, "1"},
+        {Options::FILE_SYSTEM, "local"},
+        {Options::MERGE_ENGINE, "deduplicate"},
+        {Options::SEQUENCE_FIELD, "s1,s0"},
+        {Options::DELETION_VECTORS_ENABLED, "true"},
+        {Options::DELETION_VECTOR_BITMAP64, GetParam() ? "true" : "false"}};
     CreateTable(fields, partition_keys, primary_keys, options);
     std::string table_path = TablePath();
     auto data_type = arrow::struct_(fields);
@@ -1252,7 +1566,7 @@ TEST_F(PkCompactionInteTest, 
DeduplicateWithSequenceFieldAndPkInMiddle) {
 //   7. ScanAndVerify after DV compact
 //   8. Full compact
 //   9. ScanAndVerify after full compact
-TEST_F(PkCompactionInteTest, DeduplicateNestedTypesWithSequenceField) {
+TEST_P(PkCompactionDvInteTest, DeduplicateNestedTypesWithSequenceField) {
     auto map_type =
         std::make_shared<arrow::MapType>(arrow::field("key", arrow::utf8(), 
/*nullable=*/false),
                                          arrow::field("value", 
arrow::int32()));
@@ -1269,9 +1583,13 @@ TEST_F(PkCompactionInteTest, 
DeduplicateNestedTypesWithSequenceField) {
     std::vector<std::string> partition_keys = {};
 
     std::map<std::string, std::string> options = {
-        {Options::FILE_FORMAT, "parquet"}, {Options::BUCKET, "1"},
-        {Options::FILE_SYSTEM, "local"},   {Options::MERGE_ENGINE, 
"deduplicate"},
-        {Options::SEQUENCE_FIELD, "seq"},  {Options::DELETION_VECTORS_ENABLED, 
"true"}};
+        {Options::FILE_FORMAT, "parquet"},
+        {Options::BUCKET, "1"},
+        {Options::FILE_SYSTEM, "local"},
+        {Options::MERGE_ENGINE, "deduplicate"},
+        {Options::SEQUENCE_FIELD, "seq"},
+        {Options::DELETION_VECTORS_ENABLED, "true"},
+        {Options::DELETION_VECTOR_BITMAP64, GetParam() ? "true" : "false"}};
     CreateTable(fields, partition_keys, primary_keys, options);
     std::string table_path = TablePath();
     auto data_type = arrow::struct_(fields);
@@ -1503,7 +1821,7 @@ TEST_F(PkCompactionInteTest, CompactWithSchemaEvolution) {
 //   6. ScanAndVerify after DV compact
 //   7. Full compact → everything merges to level-5 orc
 //   8. ScanAndVerify after full compact
-TEST_F(PkCompactionInteTest, FileFormatPerLevelWithDV) {
+TEST_P(PkCompactionDvInteTest, FileFormatPerLevelWithDV) {
     arrow::FieldVector fields = {
         arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::int32()),
         arrow::field("f2", arrow::int32()), arrow::field("f3", 
arrow::float64()),
@@ -1521,7 +1839,8 @@ TEST_F(PkCompactionInteTest, FileFormatPerLevelWithDV) {
         {Options::BUCKET, "2"},
         {Options::BUCKET_KEY, "f2"},
         {Options::FILE_SYSTEM, "local"},
-        {Options::DELETION_VECTORS_ENABLED, "true"}};
+        {Options::DELETION_VECTORS_ENABLED, "true"},
+        {Options::DELETION_VECTOR_BITMAP64, GetParam() ? "true" : "false"}};
     CreateTable(fields, partition_keys, primary_keys, options);
     std::string table_path = TablePath();
     auto data_type = arrow::struct_(fields);
@@ -1778,17 +2097,19 @@ TEST_F(PkCompactionInteTest, 
ContinuousWriteWithBackgroundCompact) {
 //   4. Non-full compact → lookup against max-level file produces DV.
 //   5. ScanAndVerify: "Carol" must be gone, "Bob" must have updated value.
 //   6. Full compact → ScanAndVerify again, data must be identical.
-TEST_F(PkCompactionInteTest, DeduplicateWithRowKindAndDV) {
+TEST_P(PkCompactionDvInteTest, DeduplicateWithRowKindAndDV) {
     // f3 is a large padding field to make the initial file substantially 
bigger.
     arrow::FieldVector fields = {
         arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::int32()),
         arrow::field("f2", arrow::float64()), arrow::field("f3", 
arrow::utf8())};
     std::vector<std::string> primary_keys = {"f0"};
     std::vector<std::string> partition_keys = {};
-    std::map<std::string, std::string> options = {{Options::FILE_FORMAT, 
"parquet"},
-                                                  {Options::BUCKET, "1"},
-                                                  {Options::FILE_SYSTEM, 
"local"},
-                                                  
{Options::DELETION_VECTORS_ENABLED, "true"}};
+    std::map<std::string, std::string> options = {
+        {Options::FILE_FORMAT, "parquet"},
+        {Options::BUCKET, "1"},
+        {Options::FILE_SYSTEM, "local"},
+        {Options::DELETION_VECTORS_ENABLED, "true"},
+        {Options::DELETION_VECTOR_BITMAP64, GetParam() ? "true" : "false"}};
     CreateTable(fields, partition_keys, primary_keys, options);
     std::string table_path = TablePath();
     auto data_type = arrow::struct_(fields);
@@ -2162,7 +2483,7 @@ TEST_F(PkCompactionInteTest, TestFirstRowNoDV) {
     }
 }
 
-TEST_F(PkCompactionInteTest, TestPartialUpdateWithDV) {
+TEST_P(PkCompactionDvInteTest, TestPartialUpdateWithDV) {
     // f2 and f3 are nullable value fields; PartialUpdate keeps the latest 
non-null value
     // per field. f4 is a large padding field to inflate the initial file size 
so that
     // PickForSizeRatio does not merge L0 files directly into Lmax.
@@ -2172,12 +2493,14 @@ TEST_F(PkCompactionInteTest, TestPartialUpdateWithDV) {
         arrow::field("f3", arrow::float64(), /*nullable=*/true), 
arrow::field("f4", arrow::utf8())};
     std::vector<std::string> primary_keys = {"f0", "f1"};
     std::vector<std::string> partition_keys = {"f1"};
-    std::map<std::string, std::string> options = {{Options::FILE_FORMAT, 
"parquet"},
-                                                  {Options::BUCKET, "1"},
-                                                  {Options::BUCKET_KEY, "f0"},
-                                                  {Options::FILE_SYSTEM, 
"local"},
-                                                  {Options::MERGE_ENGINE, 
"partial-update"},
-                                                  
{Options::DELETION_VECTORS_ENABLED, "true"}};
+    std::map<std::string, std::string> options = {
+        {Options::FILE_FORMAT, "parquet"},
+        {Options::BUCKET, "1"},
+        {Options::BUCKET_KEY, "f0"},
+        {Options::FILE_SYSTEM, "local"},
+        {Options::MERGE_ENGINE, "partial-update"},
+        {Options::DELETION_VECTORS_ENABLED, "true"},
+        {Options::DELETION_VECTOR_BITMAP64, GetParam() ? "true" : "false"}};
     CreateTable(fields, partition_keys, primary_keys, options);
     std::string table_path = TablePath();
     auto data_type = arrow::struct_(fields);
@@ -2291,18 +2614,20 @@ TEST_F(PkCompactionInteTest, TestPartialUpdateWithDV) {
     }
 }
 
-TEST_F(PkCompactionInteTest, TestDeduplicateWithDvInAllLevels) {
+TEST_P(PkCompactionDvInteTest, TestDeduplicateWithDvInAllLevels) {
     arrow::FieldVector fields = {
         arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::int32()),
         arrow::field("f2", arrow::int32()), arrow::field("f3", 
arrow::float64()),
         arrow::field("f4", arrow::utf8())};
     std::vector<std::string> primary_keys = {"f0", "f1", "f2"};
     std::vector<std::string> partition_keys = {"f1"};
-    std::map<std::string, std::string> options = {{Options::FILE_FORMAT, 
"parquet"},
-                                                  {Options::BUCKET, "1"},
-                                                  {Options::BUCKET_KEY, "f2"},
-                                                  {Options::FILE_SYSTEM, 
"local"},
-                                                  
{Options::DELETION_VECTORS_ENABLED, "true"}};
+    std::map<std::string, std::string> options = {
+        {Options::FILE_FORMAT, "parquet"},
+        {Options::BUCKET, "1"},
+        {Options::BUCKET_KEY, "f2"},
+        {Options::FILE_SYSTEM, "local"},
+        {Options::DELETION_VECTORS_ENABLED, "true"},
+        {Options::DELETION_VECTOR_BITMAP64, GetParam() ? "true" : "false"}};
     CreateTable(fields, partition_keys, primary_keys, options);
     std::string table_path = TablePath();
     auto data_type = arrow::struct_(fields);
@@ -2726,19 +3051,21 @@ TEST_F(PkCompactionInteTest, 
TestAggregateWithNoDvAndOrphanDelete) {
     }
 }
 
-TEST_F(PkCompactionInteTest, TestDuplicateWithDvAndOrphanDelete) {
+TEST_P(PkCompactionDvInteTest, TestDuplicateWithDvAndOrphanDelete) {
     arrow::FieldVector fields = {
         arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::int32()),
         arrow::field("f2", arrow::int32()), arrow::field("f3", 
arrow::float64()),
         arrow::field("f4", arrow::utf8())};
     std::vector<std::string> primary_keys = {"f0", "f1"};
     std::vector<std::string> partition_keys = {"f1"};
-    std::map<std::string, std::string> options = {{Options::FILE_FORMAT, 
"parquet"},
-                                                  {Options::BUCKET, "1"},
-                                                  {Options::BUCKET_KEY, "f0"},
-                                                  {Options::FILE_SYSTEM, 
"local"},
-                                                  {Options::MERGE_ENGINE, 
"deduplicate"},
-                                                  
{Options::DELETION_VECTORS_ENABLED, "true"}};
+    std::map<std::string, std::string> options = {
+        {Options::FILE_FORMAT, "parquet"},
+        {Options::BUCKET, "1"},
+        {Options::BUCKET_KEY, "f0"},
+        {Options::FILE_SYSTEM, "local"},
+        {Options::MERGE_ENGINE, "deduplicate"},
+        {Options::DELETION_VECTORS_ENABLED, "true"},
+        {Options::DELETION_VECTOR_BITMAP64, GetParam() ? "true" : "false"}};
     CreateTable(fields, partition_keys, primary_keys, options);
     std::string table_path = TablePath();
     auto data_type = arrow::struct_(fields);
@@ -2890,7 +3217,7 @@ TEST_F(PkCompactionInteTest, 
TestKeyValueTableCompactionWithIOException) {
     ASSERT_TRUE(compaction_run_complete);
 }
 
-TEST_P(PkCompactionInteTest, TestKeyValueTableStreamWriteFullCompaction) {
+TEST_P(PkCompactionFormatInteTest, TestKeyValueTableStreamWriteFullCompaction) 
{
     arrow::FieldVector fields = {
         arrow::field("f0", arrow::utf8()), arrow::field("f1", arrow::int32()),
         arrow::field("f2", arrow::int32()), arrow::field("f3", 
arrow::float64())};
@@ -2983,7 +3310,7 @@ TEST_P(PkCompactionInteTest, 
TestKeyValueTableStreamWriteFullCompaction) {
 //   7. ScanAndVerify after DV compact
 //   8. Full compact
 //   9. ScanAndVerify after full compact
-TEST_F(PkCompactionInteTest, RemoteLookupFileWithExternalPath) {
+TEST_P(PkCompactionDvInteTest, RemoteLookupFileWithExternalPath) {
     // f4 is a large padding field to inflate the initial file size for DV 
strategy.
     arrow::FieldVector fields = {
         arrow::field("f0", arrow::int32()),    // value field (min agg)
@@ -3006,6 +3333,7 @@ TEST_F(PkCompactionInteTest, 
RemoteLookupFileWithExternalPath) {
         {Options::MERGE_ENGINE, "aggregation"},
         {Options::FIELDS_DEFAULT_AGG_FUNC, "min"},
         {Options::DELETION_VECTORS_ENABLED, "true"},
+        {Options::DELETION_VECTOR_BITMAP64, GetParam() ? "true" : "false"},
         {Options::LOOKUP_REMOTE_FILE_ENABLED, "true"},
         {Options::LOOKUP_REMOTE_LEVEL_THRESHOLD, "1"},
         {Options::DATA_FILE_EXTERNAL_PATHS, "FILE://" + external_path},
@@ -3121,7 +3449,7 @@ TEST_F(PkCompactionInteTest, 
RemoteLookupFileWithExternalPath) {
 //   8. ScanAndVerify after DV compact
 //   9. Full compact
 //   10. ScanAndVerify after full compact
-TEST_F(PkCompactionInteTest, RemoteLookupFileWithSchemaEvolution) {
+TEST_P(PkCompactionDvInteTest, RemoteLookupFileWithSchemaEvolution) {
     // f4 is a large padding field to inflate the initial file size for DV 
strategy.
     arrow::FieldVector fields = {
         arrow::field("f0", arrow::int32()),    // value field (min agg)
@@ -3132,14 +3460,16 @@ TEST_F(PkCompactionInteTest, 
RemoteLookupFileWithSchemaEvolution) {
     std::vector<std::string> primary_keys = {"f2", "f1"};
     std::vector<std::string> partition_keys = {};
 
-    std::map<std::string, std::string> options = {{Options::FILE_FORMAT, 
"parquet"},
-                                                  {Options::BUCKET, "1"},
-                                                  {Options::FILE_SYSTEM, 
"local"},
-                                                  {Options::MERGE_ENGINE, 
"aggregation"},
-                                                  
{Options::FIELDS_DEFAULT_AGG_FUNC, "min"},
-                                                  
{Options::DELETION_VECTORS_ENABLED, "true"},
-                                                  
{Options::LOOKUP_REMOTE_FILE_ENABLED, "true"},
-                                                  
{Options::LOOKUP_REMOTE_LEVEL_THRESHOLD, "1"}};
+    std::map<std::string, std::string> options = {
+        {Options::FILE_FORMAT, "parquet"},
+        {Options::BUCKET, "1"},
+        {Options::FILE_SYSTEM, "local"},
+        {Options::MERGE_ENGINE, "aggregation"},
+        {Options::FIELDS_DEFAULT_AGG_FUNC, "min"},
+        {Options::DELETION_VECTORS_ENABLED, "true"},
+        {Options::DELETION_VECTOR_BITMAP64, GetParam() ? "true" : "false"},
+        {Options::LOOKUP_REMOTE_FILE_ENABLED, "true"},
+        {Options::LOOKUP_REMOTE_LEVEL_THRESHOLD, "1"}};
     CreateTable(fields, partition_keys, primary_keys, options);
     std::string table_path = TablePath();
     auto data_type = arrow::struct_(fields);
@@ -3275,8 +3605,8 @@ TEST_F(PkCompactionInteTest, 
RemoteLookupFileWithSchemaEvolution) {
 //   4. ScanAndVerify after DV compact
 //   5. Full compact
 //   6. ScanAndVerify after full compact
-TEST_P(PkCompactionInteTest, TestLookupCompatibility) {
-    auto file_format = GetParam();
+TEST_P(PkCompactionFormatDvInteTest, TestLookupCompatibility) {
+    auto file_format = std::get<0>(GetParam());
     if (file_format == "avro" || file_format == "mosaic" || file_format == 
"lance") {
         return;
     }
@@ -3292,11 +3622,13 @@ TEST_P(PkCompactionInteTest, TestLookupCompatibility) {
         arrow::field("f3", arrow::float64()),  // value field (min agg)
         arrow::field("f4", arrow::utf8())};    // padding value field (min agg)
 
+    std::map<std::string, std::string> options = {
+        {Options::DELETION_VECTOR_BITMAP64, std::get<1>(GetParam()) ? "true" : 
"false"}};
     int64_t commit_id = 6;
     // Step 2: Non-full compact → two level-0 files merge, lookup against 
max-level produces DV.
     ASSERT_OK_AND_ASSIGN(
         auto dv_compact_msgs,
-        CompactAndCommit(table_path, {}, 0, /*full_compaction=*/false, 
commit_id++));
+        CompactAndCommit(table_path, {}, 0, /*full_compaction=*/false, 
commit_id++, options));
 
     // Step 2: Assert DV index files are present.
     ASSERT_TRUE(HasDeletionVectorIndexFiles(dv_compact_msgs))
@@ -3322,7 +3654,7 @@ TEST_P(PkCompactionInteTest, TestLookupCompatibility) {
     // Step 4: Full compact to merge everything.
     ASSERT_OK_AND_ASSIGN(
         auto full_compact_msgs,
-        CompactAndCommit(table_path, {}, 0, /*full_compaction=*/true, 
commit_id++));
+        CompactAndCommit(table_path, {}, 0, /*full_compaction=*/true, 
commit_id++, options));
     ASSERT_TRUE(HasExtraLookupFiles(full_compact_msgs));
 
     // Step 5: ScanAndVerify after full compact.
@@ -3341,7 +3673,7 @@ TEST_P(PkCompactionInteTest, TestLookupCompatibility) {
     }
 }
 
-TEST_F(PkCompactionInteTest, PkDvAndAggWithIOException) {
+TEST_P(PkCompactionDvInteTest, PkDvAndAggWithIOException) {
     // f4 is a large padding field to inflate the initial file size for DV 
strategy.
     arrow::FieldVector fields = {
         arrow::field("f0", arrow::int32()),    // value field (min agg)
@@ -3352,14 +3684,16 @@ TEST_F(PkCompactionInteTest, PkDvAndAggWithIOException) 
{
     std::vector<std::string> primary_keys = {"f2", "f1"};
     std::vector<std::string> partition_keys = {};
 
-    std::map<std::string, std::string> options = {{Options::FILE_FORMAT, 
"parquet"},
-                                                  {Options::BUCKET, "1"},
-                                                  {Options::FILE_SYSTEM, 
"local"},
-                                                  {Options::MERGE_ENGINE, 
"aggregation"},
-                                                  
{Options::FIELDS_DEFAULT_AGG_FUNC, "min"},
-                                                  
{Options::DELETION_VECTORS_ENABLED, "true"},
-                                                  
{Options::LOOKUP_REMOTE_FILE_ENABLED, "true"},
-                                                  
{Options::LOOKUP_REMOTE_LEVEL_THRESHOLD, "1"}};
+    std::map<std::string, std::string> options = {
+        {Options::FILE_FORMAT, "parquet"},
+        {Options::BUCKET, "1"},
+        {Options::FILE_SYSTEM, "local"},
+        {Options::MERGE_ENGINE, "aggregation"},
+        {Options::FIELDS_DEFAULT_AGG_FUNC, "min"},
+        {Options::DELETION_VECTORS_ENABLED, "true"},
+        {Options::DELETION_VECTOR_BITMAP64, GetParam() ? "true" : "false"},
+        {Options::LOOKUP_REMOTE_FILE_ENABLED, "true"},
+        {Options::LOOKUP_REMOTE_LEVEL_THRESHOLD, "1"}};
 
     auto data_type = arrow::struct_(fields);
     int64_t commit_id = 0;
@@ -3450,7 +3784,7 @@ TEST_F(PkCompactionInteTest, PkDvAndAggWithIOException) {
 
 // End-to-end coverage for the collect / merge_map / nested_update 
aggregators: values are merged
 // both on read (across level-0 files) and during compaction, then verified 
from the written files.
-TEST_F(PkCompactionInteTest, AggCollectMergeMapAndNestedUpdate) {
+TEST_P(PkCompactionDvInteTest, AggCollectMergeMapAndNestedUpdate) {
     auto map_type =
         std::make_shared<arrow::MapType>(arrow::field("key", arrow::int32(), 
/*nullable=*/false),
                                          arrow::field("value", 
arrow::int32()));
@@ -3459,16 +3793,18 @@ TEST_F(PkCompactionInteTest, 
AggCollectMergeMapAndNestedUpdate) {
         arrow::field("f1", arrow::list(arrow::int32())), arrow::field("f2", 
map_type),
         arrow::field("f3", arrow::list(arrow::struct_({arrow::field("id", 
arrow::int32()),
                                                        arrow::field("name", 
arrow::utf8())})))};
-    std::map<std::string, std::string> options = {{Options::FILE_FORMAT, 
"parquet"},
-                                                  {Options::BUCKET, "1"},
-                                                  {Options::FILE_SYSTEM, 
"local"},
-                                                  {Options::MERGE_ENGINE, 
"aggregation"},
-                                                  
{Options::DELETION_VECTORS_ENABLED, "true"},
-                                                  
{"fields.f1.aggregate-function", "collect"},
-                                                  {"fields.f1.distinct", 
"true"},
-                                                  
{"fields.f2.aggregate-function", "merge_map"},
-                                                  
{"fields.f3.aggregate-function", "nested_update"},
-                                                  {"fields.f3.nested-key", 
"id"}};
+    std::map<std::string, std::string> options = {
+        {Options::FILE_FORMAT, "parquet"},
+        {Options::BUCKET, "1"},
+        {Options::FILE_SYSTEM, "local"},
+        {Options::MERGE_ENGINE, "aggregation"},
+        {Options::DELETION_VECTORS_ENABLED, "true"},
+        {Options::DELETION_VECTOR_BITMAP64, GetParam() ? "true" : "false"},
+        {"fields.f1.aggregate-function", "collect"},
+        {"fields.f1.distinct", "true"},
+        {"fields.f2.aggregate-function", "merge_map"},
+        {"fields.f3.aggregate-function", "nested_update"},
+        {"fields.f3.nested-key", "id"}};
     CreateTable(fields, /*partition_keys=*/{}, /*primary_keys=*/{"f0"}, 
options);
     std::string table_path = TablePath();
     auto data_type = arrow::struct_(fields);
@@ -3535,17 +3871,19 @@ TEST_F(PkCompactionInteTest, 
AggCollectMergeMapAndNestedUpdate) {
 // End-to-end coverage for the sketch aggregators. The last batch writes null 
sketches, which is the
 // path where the aggregator must hand back an owning copy of the accumulator 
rather than a view
 // into the row buffer that is about to be overwritten.
-TEST_F(PkCompactionInteTest, AggHllAndThetaSketches) {
+TEST_P(PkCompactionDvInteTest, AggHllAndThetaSketches) {
     arrow::FieldVector fields = {arrow::field("f0", arrow::utf8()),  // PK
                                  arrow::field("f1", arrow::binary()),
                                  arrow::field("f2", arrow::binary())};
-    std::map<std::string, std::string> options = {{Options::FILE_FORMAT, 
"parquet"},
-                                                  {Options::BUCKET, "1"},
-                                                  {Options::FILE_SYSTEM, 
"local"},
-                                                  {Options::MERGE_ENGINE, 
"aggregation"},
-                                                  
{Options::DELETION_VECTORS_ENABLED, "true"},
-                                                  
{"fields.f1.aggregate-function", "hll_sketch"},
-                                                  
{"fields.f2.aggregate-function", "theta_sketch"}};
+    std::map<std::string, std::string> options = {
+        {Options::FILE_FORMAT, "parquet"},
+        {Options::BUCKET, "1"},
+        {Options::FILE_SYSTEM, "local"},
+        {Options::MERGE_ENGINE, "aggregation"},
+        {Options::DELETION_VECTORS_ENABLED, "true"},
+        {Options::DELETION_VECTOR_BITMAP64, GetParam() ? "true" : "false"},
+        {"fields.f1.aggregate-function", "hll_sketch"},
+        {"fields.f2.aggregate-function", "theta_sketch"}};
     CreateTable(fields, /*partition_keys=*/{}, /*primary_keys=*/{"f0"}, 
options);
     std::string table_path = TablePath();
     int64_t commit_id = 0;
@@ -3654,7 +3992,12 @@ std::vector<std::string> 
GetTestValuesForCompactionInteTest() {
     return values;
 }
 
-INSTANTIATE_TEST_SUITE_P(FileFormat, PkCompactionInteTest,
+INSTANTIATE_TEST_SUITE_P(FileFormat, PkCompactionFormatInteTest,
                          
::testing::ValuesIn(GetTestValuesForCompactionInteTest()));
+INSTANTIATE_TEST_SUITE_P(Bitmap64, PkCompactionDvInteTest, ::testing::Bool());
+INSTANTIATE_TEST_SUITE_P(
+    FileFormatAndBitmap64, PkCompactionFormatDvInteTest,
+    
::testing::Combine(::testing::ValuesIn(GetTestValuesForCompactionInteTest()),
+                       ::testing::Bool()));
 
 }  // namespace paimon::test

Reply via email to