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

SteNicholas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new ecfff724 perf(blob): optimize array & map blob reading (#395)
ecfff724 is described below

commit ecfff724448cd88badbe06b8125378b17c9d511d
Author: lxy <[email protected]>
AuthorDate: Tue Sep 29 13:49:48 2026 +0800

    perf(blob): optimize array & map blob reading (#395)
---
 .../common/reader/data_evolution_file_reader.cpp   | 124 ++---
 .../common/reader/data_evolution_file_reader.h     |   6 +-
 .../reader/data_evolution_file_reader_test.cpp     | 273 +++++------
 src/paimon/format/blob/blob_file_batch_reader.cpp  | 499 ++++++++++++++++++---
 src/paimon/format/blob/blob_file_batch_reader.h    |  11 +-
 5 files changed, 628 insertions(+), 285 deletions(-)

diff --git a/src/paimon/common/reader/data_evolution_file_reader.cpp 
b/src/paimon/common/reader/data_evolution_file_reader.cpp
index 347fe439..5cace418 100644
--- a/src/paimon/common/reader/data_evolution_file_reader.cpp
+++ b/src/paimon/common/reader/data_evolution_file_reader.cpp
@@ -23,9 +23,9 @@
 #include "arrow/array/util.h"
 #include "arrow/c/abi.h"
 #include "arrow/c/bridge.h"
-#include "fmt/format.h"
 #include "paimon/common/metrics/metrics_impl.h"
 #include "paimon/common/reader/reader_utils.h"
+#include "paimon/common/utils/arrow/arrow_utils.h"
 #include "paimon/common/utils/arrow/mem_utils.h"
 #include "paimon/common/utils/arrow/status_utils.h"
 #include "paimon/common/utils/checked_cast.h"
@@ -59,25 +59,43 @@ Result<std::unique_ptr<DataEvolutionFileReader>> 
DataEvolutionFileReader::Create
 }
 
 Result<BatchReader::ReadBatchWithBitmap> 
DataEvolutionFileReader::NextBatchWithBitmap() {
+    int64_t array_length = read_batch_size_;
+    bool has_active_reader = false;
+    bool has_available_array = false;
+    bool has_eof_reader = false;
+    for (size_t i = 0; i < readers_.size(); i++) {
+        if (!readers_[i]) {
+            continue;
+        }
+        has_active_reader = true;
+        PAIMON_ASSIGN_OR_RAISE(bool has_cached_array, EnsureCachedArray(i));
+        if (!has_cached_array) {
+            has_eof_reader = true;
+            continue;
+        }
+        has_available_array = true;
+        array_length = std::min(array_length, CalculateCachedArrayLength(i));
+    }
+    if (!has_active_reader) {
+        return Status::Invalid("data evolution reader has no active inner 
reader");
+    }
+    if (has_eof_reader) {
+        if (has_available_array) {
+            return Status::Invalid("array for single reader length mismatch 
others");
+        }
+        return BatchReader::MakeEofBatchWithBitmap();
+    }
+
     std::vector<std::shared_ptr<arrow::StructArray>> array_for_each_reader;
     array_for_each_reader.reserve(readers_.size());
-    int64_t array_length = -1;
     for (size_t i = 0; i < readers_.size(); i++) {
         if (!readers_[i]) {
             // no read field from readers_[i]
             array_for_each_reader.push_back(nullptr);
             continue;
         }
-        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> array, 
NextBatchForSingleReader(i));
-        if (array == nullptr) {
-            // read eof
-            return BatchReader::MakeEofBatchWithBitmap();
-        }
-        if (array_length == -1) {
-            array_length = array->length();
-        } else if (array_length != array->length()) {
-            return Status::Invalid("array for single reader length mismatch 
others");
-        }
+        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> array,
+                               TakeCachedArray(i, array_length));
         auto struct_array = checked_pointer_cast<arrow::StructArray>(array);
         array_for_each_reader.push_back(struct_array);
     }
@@ -132,24 +150,19 @@ int64_t 
DataEvolutionFileReader::CalculateCachedArrayLength(size_t reader_idx) c
     return total_length;
 }
 
-Result<std::shared_ptr<arrow::Array>> 
DataEvolutionFileReader::NextBatchForSingleReader(
-    size_t reader_idx) {
-    int64_t total_array_length = CalculateCachedArrayLength(reader_idx);
-    if (total_array_length >= read_batch_size_) {
-        assert(false);
-        return Status::Invalid(fmt::format(
-            "Unexpected: the length of cached array in last turn {} exceed 
read batch size {}",
-            total_array_length, read_batch_size_));
-    }
-    // array left for last turn
-    arrow::ArrayVector concat_array_vec = 
std::move(cached_array_vec_[reader_idx]);
-    cached_array_vec_[reader_idx].clear();
-    while (total_array_length < read_batch_size_) {
+Result<bool> DataEvolutionFileReader::EnsureCachedArray(size_t reader_idx) {
+    if (!cached_array_vec_[reader_idx].empty()) {
+        return true;
+    }
+    if (reader_eof_[reader_idx]) {
+        return false;
+    }
+    while (cached_array_vec_[reader_idx].empty()) {
         PAIMON_ASSIGN_OR_RAISE(ReadBatchWithBitmap src_array_with_bitmap,
                                readers_[reader_idx]->NextBatchWithBitmap());
         if (BatchReader::IsEofBatch(src_array_with_bitmap)) {
-            // read finish
-            break;
+            reader_eof_[reader_idx] = true;
+            return false;
         }
         auto& [read_batch, bitmap] = src_array_with_bitmap;
         auto& [c_array, c_schema] = read_batch;
@@ -158,36 +171,45 @@ Result<std::shared_ptr<arrow::Array>> 
DataEvolutionFileReader::NextBatchForSingl
         PAIMON_ASSIGN_OR_RAISE(arrow::ArrayVector selected_array_vec,
                                
ReaderUtils::GenerateFilteredArrayVector(src_array, bitmap));
         for (const auto& selected_array : selected_array_vec) {
-            if (total_array_length + selected_array->length() > 
read_batch_size_) {
-                // need truncate current array to align read_batch_size_
-                int64_t truncated_length = read_batch_size_ - 
total_array_length;
-                if (truncated_length == 0) {
-                    // total_array_length equals to read_batch_size_, all 
selected_array left will
-                    // be added to cached_array_vec_
-                    cached_array_vec_[reader_idx].push_back(selected_array);
-                } else {
-                    concat_array_vec.push_back(selected_array->Slice(0, 
truncated_length));
-                    cached_array_vec_[reader_idx].push_back(
-                        selected_array->Slice(truncated_length));
-                    total_array_length += truncated_length;
-                }
-            } else {
-                concat_array_vec.push_back(selected_array);
-                total_array_length += selected_array->length();
+            if (selected_array->length() > 0) {
+                cached_array_vec_[reader_idx].push_back(selected_array);
             }
         }
     }
-    if (concat_array_vec.empty()) {
-        return std::shared_ptr<arrow::Array>();
+    return true;
+}
+
+Result<std::shared_ptr<arrow::Array>> DataEvolutionFileReader::TakeCachedArray(
+    size_t reader_idx, int64_t array_length) {
+    if (array_length <= 0) {
+        return Status::Invalid("array length must be larger than zero");
+    }
+    if (CalculateCachedArrayLength(reader_idx) < array_length) {
+        return Status::Invalid("requested array length exceeds cached array 
length");
     }
-    if (concat_array_vec.size() == 1 && concat_array_vec[0]->offset() == 0) {
-        // Avoid data copy when the array is already normalized.
-        return concat_array_vec[0];
+    arrow::ArrayVector selected_array_vec;
+    int64_t remaining_length = array_length;
+    auto& cached_array_vec = cached_array_vec_[reader_idx];
+    while (remaining_length > 0) {
+        const auto& cached_array = cached_array_vec.front();
+        if (cached_array->length() <= remaining_length) {
+            selected_array_vec.push_back(cached_array);
+            remaining_length -= cached_array->length();
+            cached_array_vec.erase(cached_array_vec.begin());
+        } else {
+            selected_array_vec.push_back(cached_array->Slice(0, 
remaining_length));
+            cached_array_vec.front() = cached_array->Slice(remaining_length);
+            remaining_length = 0;
+        }
+    }
+    if (selected_array_vec.size() == 1) {
+        return ArrowUtils::NormalizeArrayOffsets(selected_array_vec[0], 
arrow_pool_.get());
     }
     PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> 
concat_array,
-                                      arrow::Concatenate(concat_array_vec, 
arrow_pool_.get()));
-    assert(concat_array->length() == total_array_length);
-    assert(concat_array->length() <= read_batch_size_);
+                                      arrow::Concatenate(selected_array_vec, 
arrow_pool_.get()));
+    if (concat_array->length() != array_length) {
+        return Status::Invalid("concatenated array length does not match 
requested array length");
+    }
     return concat_array;
 }
 
diff --git a/src/paimon/common/reader/data_evolution_file_reader.h 
b/src/paimon/common/reader/data_evolution_file_reader.h
index 31743d1e..7f6961d8 100644
--- a/src/paimon/common/reader/data_evolution_file_reader.h
+++ b/src/paimon/common/reader/data_evolution_file_reader.h
@@ -85,11 +85,14 @@ class DataEvolutionFileReader : public BatchReader {
           reader_offsets_(reader_offsets),
           field_offsets_(field_offsets),
           cached_array_vec_(readers_.size()),
+          reader_eof_(readers_.size(), false),
           non_exist_array_vec_(read_schema->num_fields(), nullptr) {}
 
     int64_t CalculateCachedArrayLength(size_t reader_idx) const;
 
-    Result<std::shared_ptr<arrow::Array>> NextBatchForSingleReader(size_t 
reader_idx);
+    Result<bool> EnsureCachedArray(size_t reader_idx);
+
+    Result<std::shared_ptr<arrow::Array>> TakeCachedArray(size_t reader_idx, 
int64_t array_length);
 
     Result<std::shared_ptr<arrow::Array>> GetOrCreateNonExistArray(int32_t 
field_idx,
                                                                    int64_t 
array_length);
@@ -102,6 +105,7 @@ class DataEvolutionFileReader : public BatchReader {
     std::vector<int32_t> reader_offsets_;
     std::vector<int32_t> field_offsets_;
     std::vector<arrow::ArrayVector> cached_array_vec_;
+    std::vector<bool> reader_eof_;
     arrow::ArrayVector non_exist_array_vec_;
 };
 }  // namespace paimon
diff --git a/src/paimon/common/reader/data_evolution_file_reader_test.cpp 
b/src/paimon/common/reader/data_evolution_file_reader_test.cpp
index 35cdda40..3a98d42a 100644
--- a/src/paimon/common/reader/data_evolution_file_reader_test.cpp
+++ b/src/paimon/common/reader/data_evolution_file_reader_test.cpp
@@ -93,50 +93,20 @@ class DataEvolutionFileReaderTest : public ::testing::Test,
         }
     }
 
-    void CheckNextBatchForSingleReader(int32_t inner_batch_size, int32_t 
read_batch_size,
-                                       const std::shared_ptr<arrow::Array>& 
src_array,
-                                       const std::optional<RoaringBitmap32>& 
selection_bitmap,
-                                       const std::shared_ptr<arrow::Array>& 
expected_array) const {
-        std::unique_ptr<MockFileBatchReader> file_batch_reader;
-        if (selection_bitmap) {
-            file_batch_reader = std::make_unique<MockFileBatchReader>(
-                src_array, src_array->type(), selection_bitmap.value(), 
inner_batch_size);
-        } else {
-            file_batch_reader = 
std::make_unique<MockFileBatchReader>(src_array, src_array->type(),
-                                                                      
inner_batch_size);
+    Result<std::shared_ptr<arrow::Array>> 
ReadNextOutput(DataEvolutionFileReader* reader) const {
+        PAIMON_ASSIGN_OR_RAISE(BatchReader::ReadBatchWithBitmap 
batch_with_bitmap,
+                               reader->NextBatchWithBitmap());
+        if (BatchReader::IsEofBatch(batch_with_bitmap)) {
+            return std::shared_ptr<arrow::Array>();
         }
-        auto enable_randomize_batch_size = GetParam();
-        
file_batch_reader->EnableRandomizeBatchSize(enable_randomize_batch_size);
-        std::vector<std::unique_ptr<BatchReader>> readers;
-        readers.push_back(std::move(file_batch_reader));
-        DataEvolutionFileReader fake_data_evolution_reader(
-            std::move(readers), /*read_schema=*/arrow::schema({}), 
read_batch_size,
-            /*reader_offsets=*/{}, /*field_offsets=*/{}, GetArrowPool(pool_));
-        arrow::ArrayVector result_array_vec;
-        while (true) {
-            ASSERT_OK_AND_ASSIGN(auto result_array,
-                                 
fake_data_evolution_reader.NextBatchForSingleReader(0));
-            if (result_array == nullptr) {
-                break;
-            }
-            ASSERT_EQ(result_array->offset(), 0);
-            result_array_vec.push_back(result_array);
-        }
-        ASSERT_EQ(result_array_vec.size(),
-                  std::ceil(static_cast<double>(expected_array->length()) / 
read_batch_size));
-        // except for last batch, the length each array is expected to be 
aligned to read_batch_size
-        for (size_t i = 0; i < result_array_vec.size() - 1; i++) {
-            ASSERT_EQ(result_array_vec[i]->length(), read_batch_size);
-        }
-        if (expected_array->length() % read_batch_size == 0) {
-            ASSERT_EQ(result_array_vec.back()->length(), read_batch_size);
-        } else {
-            ASSERT_EQ(result_array_vec.back()->length(),
-                      expected_array->length() % read_batch_size);
+        auto& [batch, bitmap] = batch_with_bitmap;
+        auto& [c_array, c_schema] = batch;
+        if (bitmap.Cardinality() != c_array->length) {
+            return Status::Invalid("data evolution output bitmap should select 
every row");
         }
-        auto result_chunk_array = 
std::make_shared<arrow::ChunkedArray>(result_array_vec);
-        auto expected_chunk_array = 
std::make_shared<arrow::ChunkedArray>(expected_array);
-        ASSERT_TRUE(result_chunk_array->Equals(expected_chunk_array));
+        PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
+                                          arrow::ImportArray(c_array.get(), 
c_schema.get()));
+        return array;
     }
 
  private:
@@ -201,130 +171,103 @@ TEST_F(DataEvolutionFileReaderTest, TestInvalid) {
     }
 }
 
-TEST_P(DataEvolutionFileReaderTest, TestNextBatchForSingleReader) {
-    auto prepare_array = [](int64_t array_length) -> 
std::shared_ptr<arrow::Array> {
-        auto array_builder = std::make_shared<arrow::Int32Builder>();
-        for (int32_t i = 0; i < array_length; ++i) {
-            EXPECT_TRUE(array_builder->Append(i).ok());
-        }
-        std::shared_ptr<arrow::Array> array;
-        EXPECT_TRUE(array_builder->Finish(&array).ok());
-        return array;
-    };
-    auto prepare_array_with_bitmap =
-        [](const RoaringBitmap32& bitmap) -> std::shared_ptr<arrow::Array> {
-        auto array_builder = std::make_shared<arrow::Int32Builder>();
-        for (auto iter = bitmap.Begin(); iter != bitmap.End(); ++iter) {
-            EXPECT_TRUE(array_builder->Append(*iter).ok());
-        }
-        std::shared_ptr<arrow::Array> array;
-        EXPECT_TRUE(array_builder->Finish(&array).ok());
-        return array;
-    };
-    {
-        // src array length = 10, read batch size = 10
-        auto src_array = prepare_array(10);
-        for (int32_t inner_batch_size : arrow::internal::Iota(1, 10)) {
-            CheckNextBatchForSingleReader(inner_batch_size, 
/*read_batch_size=*/10, src_array,
-                                          /*selection_bitmap=*/std::nullopt,
-                                          /*expected_array=*/src_array);
-        }
-    }
-    {
-        // src array length = 10, read batch size = 6
-        auto src_array = prepare_array(10);
-        for (int32_t inner_batch_size : arrow::internal::Iota(1, 6)) {
-            CheckNextBatchForSingleReader(inner_batch_size, 
/*read_batch_size=*/6, src_array,
-                                          /*selection_bitmap=*/std::nullopt,
-                                          /*expected_array=*/src_array);
-        }
-    }
-    {
-        // src array length = 10, read batch size = 15
-        auto src_array = prepare_array(10);
-        for (int32_t inner_batch_size : arrow::internal::Iota(1, 15)) {
-            CheckNextBatchForSingleReader(inner_batch_size, 
/*read_batch_size=*/15, src_array,
-                                          /*selection_bitmap=*/std::nullopt,
-                                          /*expected_array=*/src_array);
-        }
-    }
-    {
-        // test bulk data, src array length = 10000, read batch size = 1024
-        auto src_array = prepare_array(10000);
-        for (int32_t inner_batch_size : {1, 2, 8, 16, 20, 100, 1024}) {
-            CheckNextBatchForSingleReader(inner_batch_size, 
/*read_batch_size=*/1024, src_array,
-                                          /*selection_bitmap=*/std::nullopt,
-                                          /*expected_array=*/src_array);
-        }
-    }
-    {
-        // src array length = 10, selection bitmap = {1, 3, 5}
-        auto src_array = prepare_array(10);
-        RoaringBitmap32 selected_bitmap = RoaringBitmap32::From({1, 3, 5});
-        auto expected_array = prepare_array_with_bitmap(selected_bitmap);
-        for (int32_t inner_batch_size : arrow::internal::Iota(1, 15)) {
-            CheckNextBatchForSingleReader(inner_batch_size, 
/*read_batch_size=*/15, src_array,
-                                          selected_bitmap, expected_array);
-        }
-    }
-    {
-        // src array length = 10, selection all
-        auto src_array = prepare_array(10);
-        RoaringBitmap32 selected_bitmap = RoaringBitmap32::From({0, 1, 2, 3, 
4, 5, 6, 7, 8, 9});
-        for (int32_t inner_batch_size : arrow::internal::Iota(1, 15)) {
-            CheckNextBatchForSingleReader(inner_batch_size, 
/*read_batch_size=*/15, src_array,
-                                          selected_bitmap, 
/*expected_array=*/src_array);
-        }
-    }
-    {
-        // src array length = 10, selection first
-        auto src_array = prepare_array(10);
-        RoaringBitmap32 selected_bitmap = RoaringBitmap32::From({0});
-        auto expected_array = prepare_array_with_bitmap(selected_bitmap);
-        for (int32_t inner_batch_size : arrow::internal::Iota(1, 15)) {
-            CheckNextBatchForSingleReader(inner_batch_size, 
/*read_batch_size=*/15, src_array,
-                                          selected_bitmap, expected_array);
-        }
-    }
-    {
-        // src array length = 10, selection first
-        auto src_array = prepare_array(10);
-        RoaringBitmap32 selected_bitmap = RoaringBitmap32::From({9});
-        auto expected_array = prepare_array_with_bitmap(selected_bitmap);
-        for (int32_t inner_batch_size : arrow::internal::Iota(1, 15)) {
-            CheckNextBatchForSingleReader(inner_batch_size, 
/*read_batch_size=*/15, src_array,
-                                          selected_bitmap, expected_array);
-        }
-    }
-    {
-        // src array length = 10, selection consecutive positions
-        auto src_array = prepare_array(10);
-        RoaringBitmap32 selected_bitmap = RoaringBitmap32::From({2, 3, 4, 5});
-        auto expected_array = prepare_array_with_bitmap(selected_bitmap);
-        for (int32_t inner_batch_size : arrow::internal::Iota(1, 15)) {
-            CheckNextBatchForSingleReader(inner_batch_size, 
/*read_batch_size=*/15, src_array,
-                                          selected_bitmap, expected_array);
-        }
-    }
-    {
-        auto src_array = prepare_array(10);
-        RoaringBitmap32 selected_bitmap = RoaringBitmap32::From({0, 1, 2, 3, 
4, 5, 7, 8, 9});
-        auto expected_array = prepare_array_with_bitmap(selected_bitmap);
-        // inner batch: [0, 1, 2, 3] | [4, 5] [7, 8] | [9]
-        CheckNextBatchForSingleReader(/*inner_batch_size=*/4, 
/*read_batch_size=*/5, src_array,
-                                      selected_bitmap, expected_array);
-    }
-    {
-        // test bulk data, src array length = 10000, read batch size = 1024
-        auto src_array = prepare_array(10000);
-        RoaringBitmap32 selected_bitmap =
-            RoaringBitmap32::From({0, 10, 1000, 2333, 4566, 7838, 8787, 9999});
-        auto expected_array = prepare_array_with_bitmap(selected_bitmap);
-        for (int32_t inner_batch_size : {1, 2, 8, 16, 20, 100, 1024}) {
-            CheckNextBatchForSingleReader(inner_batch_size, 
/*read_batch_size=*/1024, src_array,
-                                          selected_bitmap, expected_array);
-        }
+TEST_F(DataEvolutionFileReaderTest, TestNoActiveReader) {
+    auto read_schema = arrow::schema({arrow::field("missing", 
arrow::int32())});
+    std::vector<std::unique_ptr<BatchReader>> readers;
+    readers.push_back(nullptr);
+    ASSERT_OK_AND_ASSIGN(auto reader,
+                         DataEvolutionFileReader::Create(
+                             std::move(readers), read_schema, 
/*read_batch_size=*/10,
+                             /*reader_offsets=*/{-1}, /*field_offsets=*/{-1}, 
GetArrowPool(pool_)));
+    ASSERT_NOK_WITH_MSG(reader->NextBatchWithBitmap(),
+                        "data evolution reader has no active inner reader");
+}
+
+TEST_F(DataEvolutionFileReaderTest, TestDifferentInnerBatchSizes) {
+    auto f0 = arrow::field("f0", arrow::int32());
+    auto f1 = arrow::field("f1", arrow::int32());
+    auto array0 = 
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({f0}), R"([
+        [0], [1], [2], [3], [4], [5]
+    ])")
+                      .ValueOrDie();
+    auto array1 = 
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({f1}), R"([
+        [10], [11], [12], [13], [14], [15]
+    ])")
+                      .ValueOrDie();
+
+    std::vector<std::unique_ptr<BatchReader>> readers;
+    auto reader0 = std::make_unique<MockFileBatchReader>(array0, 
array0->type(),
+                                                         
/*read_batch_size=*/5);
+    reader0->EnableRandomizeBatchSize(false);
+    readers.push_back(std::move(reader0));
+    auto reader1 = std::make_unique<MockFileBatchReader>(array1, 
array1->type(),
+                                                         
/*read_batch_size=*/2);
+    reader1->EnableRandomizeBatchSize(false);
+    readers.push_back(std::move(reader1));
+    ASSERT_OK_AND_ASSIGN(auto reader, DataEvolutionFileReader::Create(
+                                          std::move(readers), 
arrow::schema({f0, f1}),
+                                          /*read_batch_size=*/10, 
/*reader_offsets=*/{0, 1},
+                                          /*field_offsets=*/{0, 0}, 
GetArrowPool(pool_)));
+
+    arrow::ArrayVector batches;
+    for (int64_t expected_length : {2, 2, 1, 1}) {
+        ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> batch, 
ReadNextOutput(reader.get()));
+        ASSERT_NE(batch, nullptr);
+        ASSERT_EQ(batch->length(), expected_length);
+        ASSERT_EQ(batch->offset(), 0);
+        auto struct_array = 
std::dynamic_pointer_cast<arrow::StructArray>(batch);
+        ASSERT_TRUE(struct_array);
+        ASSERT_EQ(struct_array->field(0)->offset(), 0);
+        ASSERT_EQ(struct_array->field(1)->offset(), 0);
+        batches.push_back(std::move(batch));
     }
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> eof, 
ReadNextOutput(reader.get()));
+    ASSERT_EQ(eof, nullptr);
+    ASSERT_OK_AND_ASSIGN(eof, ReadNextOutput(reader.get()));
+    ASSERT_EQ(eof, nullptr);
+
+    auto expected = 
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({f0, f1}), R"([
+        [0, 10], [1, 11], [2, 12], [3, 13], [4, 14], [5, 15]
+    ])")
+                        .ValueOrDie();
+    ASSERT_TRUE(std::make_shared<arrow::ChunkedArray>(batches)->Equals(
+        std::make_shared<arrow::ChunkedArray>(expected)));
+}
+
+TEST_F(DataEvolutionFileReaderTest, TestBitmapSkipsWholeBatches) {
+    auto f0 = arrow::field("f0", arrow::int32());
+    auto f1 = arrow::field("f1", arrow::int32());
+    auto array0 = 
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({f0}), R"([
+        [0], [1], [2], [3], [4], [5], [6], [7]
+    ])")
+                      .ValueOrDie();
+    auto array1 = 
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({f1}), R"([
+        [100], [101], [102], [103], [104], [105], [106], [107]
+    ])")
+                      .ValueOrDie();
+    RoaringBitmap32 bitmap = RoaringBitmap32::From({4, 7});
+
+    std::vector<std::unique_ptr<BatchReader>> readers;
+    auto reader0 = std::make_unique<MockFileBatchReader>(array0, 
array0->type(), bitmap,
+                                                         
/*read_batch_size=*/2);
+    reader0->EnableRandomizeBatchSize(false);
+    readers.push_back(std::move(reader0));
+    auto reader1 = std::make_unique<MockFileBatchReader>(array1, 
array1->type(), bitmap,
+                                                         
/*read_batch_size=*/3);
+    reader1->EnableRandomizeBatchSize(false);
+    readers.push_back(std::move(reader1));
+    ASSERT_OK_AND_ASSIGN(auto reader, DataEvolutionFileReader::Create(
+                                          std::move(readers), 
arrow::schema({f0, f1}),
+                                          /*read_batch_size=*/10, 
/*reader_offsets=*/{0, 1},
+                                          /*field_offsets=*/{0, 0}, 
GetArrowPool(pool_)));
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> actual,
+                         
ReadResultCollector::CollectResult(std::move(reader)));
+
+    auto expected = 
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({f0, f1}), R"([
+        [4, 104], [7, 107]
+    ])")
+                        .ValueOrDie();
+    
ASSERT_TRUE(actual->Equals(std::make_shared<arrow::ChunkedArray>(expected)));
 }
 
 TEST_P(DataEvolutionFileReaderTest, TestSimple) {
diff --git a/src/paimon/format/blob/blob_file_batch_reader.cpp 
b/src/paimon/format/blob/blob_file_batch_reader.cpp
index 531b32a0..0fba02e1 100644
--- a/src/paimon/format/blob/blob_file_batch_reader.cpp
+++ b/src/paimon/format/blob/blob_file_batch_reader.cpp
@@ -22,8 +22,9 @@
 #include <array>
 #include <cstring>
 #include <numeric>
-#include <set>
 #include <string>
+#include <string_view>
+#include <unordered_set>
 
 #include "arrow/api.h"
 #include "arrow/array/builder_dict.h"
@@ -90,6 +91,141 @@ Result<int32_t> GetMapBlobFixedKeyLength(const 
std::shared_ptr<arrow::DataType>&
     }
 }
 
+Status AddBuilderCapacity(int64_t increment, int64_t* capacity, const char* 
capacity_name) {
+    if (increment < 0 || increment > std::numeric_limits<int64_t>::max() - 
*capacity) {
+        return Status::CapacityError(fmt::format("{} exceeds int64 capacity", 
capacity_name));
+    }
+    *capacity += increment;
+    return Status::OK();
+}
+
+Status AddBuilderCapacityProduct(int64_t count, int64_t element_size, int64_t* 
capacity,
+                                 const char* capacity_name) {
+    if (count < 0 || element_size < 0 ||
+        (count > 0 && element_size > (std::numeric_limits<int64_t>::max() - 
*capacity) / count)) {
+        return Status::CapacityError(fmt::format("{} exceeds int64 capacity", 
capacity_name));
+    }
+    *capacity += count * element_size;
+    return Status::OK();
+}
+
+Result<int64_t> GetSerializedDescriptorSize(const std::string& file_path,
+                                            const std::shared_ptr<MemoryPool>& 
pool) {
+    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<Blob> blob,
+                           Blob::FromPath(file_path, /*offset=*/0, 
/*length=*/0));
+    PAIMON_UNIQUE_PTR<Bytes> descriptor = blob->ToDescriptor(pool);
+    if (!InRange<int64_t>(descriptor->size())) {
+        return Status::CapacityError("serialized BLOB descriptor exceeds int64 
capacity");
+    }
+    return static_cast<int64_t>(descriptor->size());
+}
+
+template <typename T>
+Result<std::shared_ptr<arrow::Buffer>> AllocateTypedBuffer(
+    int64_t element_count, const std::shared_ptr<arrow::MemoryPool>& pool) {
+    if (element_count < 0 ||
+        element_count > std::numeric_limits<int64_t>::max() / 
static_cast<int64_t>(sizeof(T))) {
+        return Status::CapacityError("Arrow buffer exceeds int64 capacity");
+    }
+    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+        std::shared_ptr<arrow::Buffer> buffer,
+        arrow::AllocateBuffer(element_count * static_cast<int64_t>(sizeof(T)), 
pool.get()));
+    return buffer;
+}
+
+Result<std::shared_ptr<arrow::Buffer>> AllocateValidityBitmap(
+    int64_t value_count, int64_t null_count, const 
std::shared_ptr<arrow::MemoryPool>& pool) {
+    if (null_count == 0) {
+        return std::shared_ptr<arrow::Buffer>();
+    }
+    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Buffer> 
null_bitmap,
+                                      arrow::AllocateBitmap(value_count, 
pool.get()));
+    memset(null_bitmap->mutable_data(), 0xFF, null_bitmap->size());
+    return null_bitmap;
+}
+
+class PreallocatedLargeBinaryBuilder {
+ public:
+    static Result<std::unique_ptr<PreallocatedLargeBinaryBuilder>> Create(
+        int64_t value_count, int64_t data_length, int64_t null_count,
+        const std::shared_ptr<arrow::MemoryPool>& pool) {
+        if (value_count < 0 || value_count == 
std::numeric_limits<int64_t>::max() ||
+            data_length < 0 || null_count < 0 || null_count > value_count) {
+            return Status::Invalid("invalid preallocated large binary array 
capacity");
+        }
+        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Buffer> offsets,
+                               AllocateTypedBuffer<int64_t>(value_count + 1, 
pool));
+        PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Buffer> data,
+                                          arrow::AllocateBuffer(data_length, 
pool.get()));
+        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Buffer> null_bitmap,
+                               AllocateValidityBitmap(value_count, null_count, 
pool));
+        auto builder =
+            std::unique_ptr<PreallocatedLargeBinaryBuilder>(new 
PreallocatedLargeBinaryBuilder(
+                value_count, data_length, null_count, std::move(offsets), 
std::move(data),
+                std::move(null_bitmap)));
+        return builder;
+    }
+
+    Result<uint8_t*> Append(int64_t length) {
+        if (length < 0 || value_index_ >= value_count_ || length > 
data_length_ - data_offset_) {
+            return Status::Invalid("large binary value exceeds preallocated 
capacity");
+        }
+        uint8_t* target = length == 0 ? nullptr : data_->mutable_data() + 
data_offset_;
+        data_offset_ += length;
+        offsets_[++value_index_] = data_offset_;
+        return target;
+    }
+
+    Status AppendNull() {
+        if (value_index_ >= value_count_ || null_bitmap_ == nullptr) {
+            return Status::Invalid("large binary null exceeds preallocated 
capacity");
+        }
+        arrow::bit_util::ClearBit(null_bitmap_->mutable_data(), value_index_);
+        offsets_[++value_index_] = data_offset_;
+        ++appended_null_count_;
+        return Status::OK();
+    }
+
+    int64_t length() const {
+        return value_index_;
+    }
+
+    Result<std::shared_ptr<arrow::LargeBinaryArray>> Finish() {
+        if (value_index_ != value_count_ || data_offset_ != data_length_ ||
+            appended_null_count_ != null_count_) {
+            return Status::Invalid("large binary output does not match 
preallocated capacity");
+        }
+        return std::make_shared<arrow::LargeBinaryArray>(value_count_, 
offsets_buffer_, data_,
+                                                         null_bitmap_, 
null_count_);
+    }
+
+ private:
+    PreallocatedLargeBinaryBuilder(int64_t value_count, int64_t data_length, 
int64_t null_count,
+                                   std::shared_ptr<arrow::Buffer> offsets,
+                                   std::shared_ptr<arrow::Buffer> data,
+                                   std::shared_ptr<arrow::Buffer> null_bitmap)
+        : value_count_(value_count),
+          data_length_(data_length),
+          null_count_(null_count),
+          offsets_buffer_(std::move(offsets)),
+          data_(std::move(data)),
+          null_bitmap_(std::move(null_bitmap)),
+          
offsets_(reinterpret_cast<int64_t*>(offsets_buffer_->mutable_data())) {
+        offsets_[0] = 0;
+    }
+
+    const int64_t value_count_;
+    const int64_t data_length_;
+    const int64_t null_count_;
+    std::shared_ptr<arrow::Buffer> offsets_buffer_;
+    std::shared_ptr<arrow::Buffer> data_;
+    std::shared_ptr<arrow::Buffer> null_bitmap_;
+    int64_t* offsets_;
+    int64_t value_index_ = 0;
+    int64_t data_offset_ = 0;
+    int64_t appended_null_count_ = 0;
+};
+
 Status AppendMapBlobKey(const std::shared_ptr<arrow::DataType>& key_type, 
const uint8_t* data,
                         int32_t length, arrow::ArrayBuilder* builder) {
     switch (key_type->id()) {
@@ -251,6 +387,14 @@ BlobFileBatchReader::BlobFileBatchReader(
     std::iota(target_blob_row_indexes_.begin(), 
target_blob_row_indexes_.end(), 0);
 }
 
+Result<int64_t> BlobFileBatchReader::GetOrCreateSerializedDescriptorSize() 
const {
+    if (serialized_descriptor_size_ < 0) {
+        PAIMON_ASSIGN_OR_RAISE(serialized_descriptor_size_,
+                               GetSerializedDescriptorSize(file_path_, pool_));
+    }
+    return serialized_descriptor_size_;
+}
+
 Status BlobFileBatchReader::SetReadSchema(::ArrowSchema* read_schema,
                                           const std::shared_ptr<Predicate>& 
predicate,
                                           const 
std::optional<RoaringBitmap32>& selection_bitmap) {
@@ -469,31 +613,19 @@ Result<BlobFileBatchReader::ArrayBlobPayload> 
BlobFileBatchReader::ReadArrayBlob
     return ArrayBlobPayload{std::move(element_lengths), data_offset};
 }
 
-Status BlobFileBatchReader::AppendArrayBlobValues(const ArrayBlobPayload& 
payload,
-                                                  arrow::LargeBinaryBuilder* 
blob_builder) const {
+Status BlobFileBatchReader::AppendArrayBlobDescriptors(
+    const ArrayBlobPayload& payload, arrow::LargeBinaryBuilder* blob_builder) 
const {
     int64_t element_offset = payload.data_offset;
     for (int64_t element_length : payload.element_lengths) {
         if (element_length == BlobDefs::kNullBinLength) {
             PAIMON_RETURN_NOT_OK_FROM_ARROW(blob_builder->AppendNull());
             continue;
         }
-        if (blob_as_descriptor_) {
-            PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<Blob> blob,
-                                   Blob::FromPath(file_path_, element_offset, 
element_length));
-            PAIMON_UNIQUE_PTR<Bytes> descriptor = blob->ToDescriptor(pool_);
-            PAIMON_RETURN_NOT_OK_FROM_ARROW(
-                blob_builder->Append(descriptor->data(), descriptor->size()));
-        } else {
-            PAIMON_UNIQUE_PTR<Bytes> element_bytes =
-                Bytes::AllocateBytes(static_cast<size_t>(element_length), 
pool_.get());
-            if (element_length > 0) {
-                PAIMON_RETURN_NOT_OK(
-                    ReadBlobContentAt(element_offset, element_length,
-                                      
reinterpret_cast<uint8_t*>(element_bytes->data())));
-            }
-            PAIMON_RETURN_NOT_OK_FROM_ARROW(
-                blob_builder->Append(element_bytes->data(), element_length));
-        }
+        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<Blob> blob,
+                               Blob::FromPath(file_path_, element_offset, 
element_length));
+        PAIMON_UNIQUE_PTR<Bytes> descriptor = blob->ToDescriptor(pool_);
+        PAIMON_RETURN_NOT_OK_FROM_ARROW(
+            blob_builder->Append(descriptor->data(), descriptor->size()));
         element_offset += element_length;
     }
     return Status::OK();
@@ -509,12 +641,118 @@ Result<std::shared_ptr<arrow::Array>> 
BlobFileBatchReader::BuildArrayBlobArray(
         return Status::Invalid("ARRAY<BLOB> element type must be large 
binary");
     }
 
+    std::vector<ArrayBlobPayload> payloads;
+    payloads.reserve(rows_to_read);
+    int64_t value_count = 0;
+    int64_t value_data_length = 0;
+    int64_t descriptor_count = 0;
+    int64_t list_null_count = 0;
+    int64_t value_null_count = 0;
+    for (int32_t k = 0; k < rows_to_read; ++k) {
+        const size_t row_index = current_pos_ + k;
+        if (IsTargetPlaceholder(row_index)) {
+            PAIMON_RETURN_NOT_OK(AddBuilderCapacity(1, &value_count, 
"ARRAY<BLOB> element count"));
+            
PAIMON_RETURN_NOT_OK(AddBuilderCapacity(BlobDefs::kPlaceholderSentinelLength,
+                                                    &value_data_length,
+                                                    "ARRAY<BLOB> element data 
size"));
+            continue;
+        }
+        if (IsTargetNull(row_index)) {
+            ++list_null_count;
+            continue;
+        }
+
+        PAIMON_ASSIGN_OR_RAISE(ArrayBlobPayload payload, 
ReadArrayBlobPayload(row_index));
+        PAIMON_RETURN_NOT_OK(
+            
AddBuilderCapacity(static_cast<int64_t>(payload.element_lengths.size()), 
&value_count,
+                               "ARRAY<BLOB> element count"));
+        for (int64_t element_length : payload.element_lengths) {
+            if (element_length == BlobDefs::kNullBinLength) {
+                ++value_null_count;
+                continue;
+            }
+            if (blob_as_descriptor_) {
+                PAIMON_RETURN_NOT_OK(
+                    AddBuilderCapacity(1, &descriptor_count, "ARRAY<BLOB> 
descriptor count"));
+            } else {
+                PAIMON_RETURN_NOT_OK(AddBuilderCapacity(element_length, 
&value_data_length,
+                                                        "ARRAY<BLOB> element 
data size"));
+            }
+        }
+        payloads.emplace_back(std::move(payload));
+    }
+    if (descriptor_count > 0) {
+        PAIMON_ASSIGN_OR_RAISE(int64_t descriptor_size, 
GetOrCreateSerializedDescriptorSize());
+        PAIMON_RETURN_NOT_OK(AddBuilderCapacityProduct(descriptor_count, 
descriptor_size,
+                                                       &value_data_length,
+                                                       "ARRAY<BLOB> descriptor 
data size"));
+    }
+
+    if (value_count > std::numeric_limits<int32_t>::max()) {
+        return Status::CapacityError("ARRAY<BLOB> element count exceeds list 
offset capacity");
+    }
+    if (!blob_as_descriptor_) {
+        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Buffer> list_offsets,
+                               AllocateTypedBuffer<int32_t>(rows_to_read + 1, 
arrow_pool_));
+        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Buffer> list_null_bitmap,
+                               AllocateValidityBitmap(rows_to_read, 
list_null_count, arrow_pool_));
+        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<PreallocatedLargeBinaryBuilder> 
value_builder,
+                               PreallocatedLargeBinaryBuilder::Create(
+                                   value_count, value_data_length, 
value_null_count, arrow_pool_));
+
+        auto* raw_list_offsets = 
reinterpret_cast<int32_t*>(list_offsets->mutable_data());
+        raw_list_offsets[0] = 0;
+        size_t payload_index = 0;
+        for (int32_t k = 0; k < rows_to_read; ++k) {
+            const size_t row_index = current_pos_ + k;
+            if (IsTargetNull(row_index)) {
+                arrow::bit_util::ClearBit(list_null_bitmap->mutable_data(), k);
+            } else if (IsTargetPlaceholder(row_index)) {
+                PAIMON_ASSIGN_OR_RAISE(auto* target,
+                                       
value_builder->Append(BlobDefs::kPlaceholderSentinelLength));
+                memcpy(target, BlobDefs::kPlaceholderSentinel,
+                       BlobDefs::kPlaceholderSentinelLength);
+            } else {
+                const ArrayBlobPayload& payload = payloads[payload_index++];
+                uint8_t* row_target = nullptr;
+                int64_t row_data_length = 0;
+                for (int64_t element_length : payload.element_lengths) {
+                    if (element_length == BlobDefs::kNullBinLength) {
+                        PAIMON_RETURN_NOT_OK(value_builder->AppendNull());
+                        continue;
+                    }
+                    PAIMON_ASSIGN_OR_RAISE(auto* target, 
value_builder->Append(element_length));
+                    if (row_target == nullptr && element_length > 0) {
+                        row_target = target;
+                    }
+                    row_data_length += element_length;
+                }
+                if (row_data_length > 0) {
+                    PAIMON_RETURN_NOT_OK(
+                        ReadBlobContentAt(payload.data_offset, 
row_data_length, row_target));
+                }
+            }
+            raw_list_offsets[k + 1] = 
static_cast<int32_t>(value_builder->length());
+        }
+
+        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::LargeBinaryArray> values,
+                               value_builder->Finish());
+        auto list_array = std::make_shared<arrow::ListArray>(
+            list_type, rows_to_read, list_offsets, values, list_null_bitmap, 
list_null_count);
+        PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::StructArray> 
struct_array,
+                                          
arrow::StructArray::Make({list_array}, {list_field}));
+        return struct_array;
+    }
+
     std::unique_ptr<arrow::ArrayBuilder> array_builder;
     PAIMON_RETURN_NOT_OK_FROM_ARROW(
         arrow::MakeBuilder(arrow_pool_.get(), list_type, &array_builder));
     auto* list_builder = 
checked_cast<arrow::ListBuilder*>(array_builder.get());
     auto* blob_builder = 
checked_cast<arrow::LargeBinaryBuilder*>(list_builder->value_builder());
     PAIMON_RETURN_NOT_OK_FROM_ARROW(list_builder->Reserve(rows_to_read));
+    PAIMON_RETURN_NOT_OK_FROM_ARROW(blob_builder->Reserve(value_count));
+    
PAIMON_RETURN_NOT_OK_FROM_ARROW(blob_builder->ReserveData(value_data_length));
+    size_t payload_index = 0;
     for (int32_t k = 0; k < rows_to_read; ++k) {
         const size_t row_index = current_pos_ + k;
         const bool is_null = IsTargetNull(row_index);
@@ -523,8 +761,8 @@ Result<std::shared_ptr<arrow::Array>> 
BlobFileBatchReader::BuildArrayBlobArray(
             PAIMON_RETURN_NOT_OK_FROM_ARROW(blob_builder->Append(
                 BlobDefs::kPlaceholderSentinel, 
BlobDefs::kPlaceholderSentinelLength));
         } else if (!is_null) {
-            PAIMON_ASSIGN_OR_RAISE(ArrayBlobPayload payload, 
ReadArrayBlobPayload(row_index));
-            PAIMON_RETURN_NOT_OK(AppendArrayBlobValues(payload, blob_builder));
+            
PAIMON_RETURN_NOT_OK(AppendArrayBlobDescriptors(payloads[payload_index], 
blob_builder));
+            ++payload_index;
         }
     }
 
@@ -658,56 +896,48 @@ Result<BlobFileBatchReader::MapBlobPayload> 
BlobFileBatchReader::ReadMapBlobPayl
 Status BlobFileBatchReader::AppendMapBlobKeys(const MapBlobPayload& payload,
                                               const 
std::shared_ptr<arrow::DataType>& key_type,
                                               arrow::ArrayBuilder* 
key_builder) const {
-    int64_t key_offset = payload.data_offset;
-    std::set<std::string> serialized_keys;
+    if (!InRange<size_t>(payload.key_data_length)) {
+        return Status::CapacityError("MAP<..., BLOB> key data exceeds size_t 
capacity");
+    }
+    PAIMON_UNIQUE_PTR<Bytes> key_bytes =
+        Bytes::AllocateBytes(static_cast<size_t>(payload.key_data_length), 
pool_.get());
+    if (payload.key_data_length > 0) {
+        PAIMON_RETURN_NOT_OK(ReadBlobContentAt(payload.data_offset, 
payload.key_data_length,
+                                               
reinterpret_cast<uint8_t*>(key_bytes->data())));
+    }
+    int64_t key_offset = 0;
+    std::unordered_set<std::string_view> serialized_keys;
+    serialized_keys.reserve(payload.key_lengths.size());
     for (int64_t key_length_64 : payload.key_lengths) {
         const auto key_length = static_cast<int32_t>(key_length_64);
-        PAIMON_UNIQUE_PTR<Bytes> key_bytes =
-            Bytes::AllocateBytes(static_cast<size_t>(key_length), pool_.get());
-        if (key_length > 0) {
-            PAIMON_RETURN_NOT_OK(ReadBlobContentAt(key_offset, key_length,
-                                                   
reinterpret_cast<uint8_t*>(key_bytes->data())));
-        }
-        std::string serialized_key;
-        if (key_length > 0) {
-            serialized_key.assign(key_bytes->data(), key_length);
-        }
-        if (!serialized_keys.emplace(std::move(serialized_key)).second) {
+        const char* key_data = key_length == 0 ? nullptr : key_bytes->data() + 
key_offset;
+        const std::string_view serialized_key =
+            key_length == 0 ? std::string_view() : std::string_view(key_data, 
key_length);
+        if (!serialized_keys.emplace(serialized_key).second) {
             return Status::Invalid("invalid MAP<..., BLOB> payload: duplicate 
key");
         }
         const uint8_t empty_key = 0;
-        const uint8_t* key_data =
-            key_length == 0 ? &empty_key : reinterpret_cast<const 
uint8_t*>(key_bytes->data());
-        PAIMON_RETURN_NOT_OK(AppendMapBlobKey(key_type, key_data, key_length, 
key_builder));
+        const uint8_t* append_data =
+            key_length == 0 ? &empty_key : reinterpret_cast<const 
uint8_t*>(key_data);
+        PAIMON_RETURN_NOT_OK(AppendMapBlobKey(key_type, append_data, 
key_length, key_builder));
         key_offset += key_length;
     }
     return Status::OK();
 }
 
-Status BlobFileBatchReader::AppendMapBlobValues(const MapBlobPayload& payload,
-                                                arrow::LargeBinaryBuilder* 
blob_builder) const {
+Status BlobFileBatchReader::AppendMapBlobDescriptors(
+    const MapBlobPayload& payload, arrow::LargeBinaryBuilder* blob_builder) 
const {
     int64_t value_offset = payload.data_offset + payload.key_data_length;
     for (int64_t value_length : payload.value_lengths) {
         if (value_length == BlobDefs::kNullBinLength) {
             PAIMON_RETURN_NOT_OK_FROM_ARROW(blob_builder->AppendNull());
             continue;
         }
-        if (blob_as_descriptor_) {
-            PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<Blob> blob,
-                                   Blob::FromPath(file_path_, value_offset, 
value_length));
-            PAIMON_UNIQUE_PTR<Bytes> descriptor = blob->ToDescriptor(pool_);
-            PAIMON_RETURN_NOT_OK_FROM_ARROW(
-                blob_builder->Append(descriptor->data(), descriptor->size()));
-        } else {
-            PAIMON_UNIQUE_PTR<Bytes> value_bytes =
-                Bytes::AllocateBytes(static_cast<size_t>(value_length), 
pool_.get());
-            if (value_length > 0) {
-                PAIMON_RETURN_NOT_OK(ReadBlobContentAt(
-                    value_offset, value_length, 
reinterpret_cast<uint8_t*>(value_bytes->data())));
-            }
-            PAIMON_RETURN_NOT_OK_FROM_ARROW(
-                blob_builder->Append(value_bytes->data(), value_length));
-        }
+        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<Blob> blob,
+                               Blob::FromPath(file_path_, value_offset, 
value_length));
+        PAIMON_UNIQUE_PTR<Bytes> descriptor = blob->ToDescriptor(pool_);
+        PAIMON_RETURN_NOT_OK_FROM_ARROW(
+            blob_builder->Append(descriptor->data(), descriptor->size()));
         value_offset += value_length;
     }
     return Status::OK();
@@ -724,19 +954,140 @@ Result<std::shared_ptr<arrow::Array>> 
BlobFileBatchReader::BuildMapBlobArray(
     }
     PAIMON_ASSIGN_OR_RAISE(int32_t fixed_key_length, 
GetMapBlobFixedKeyLength(key_type));
 
-    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::unique_ptr<arrow::ArrayBuilder> 
key_builder_unique,
+    std::vector<MapBlobPayload> payloads;
+    payloads.reserve(rows_to_read);
+    int64_t entry_count = 0;
+    int64_t key_data_length = 0;
+    int64_t value_data_length = 0;
+    int64_t descriptor_count = 0;
+    int64_t map_null_count = 0;
+    int64_t value_null_count = 0;
+    for (int32_t k = 0; k < rows_to_read; ++k) {
+        const size_t row_index = current_pos_ + k;
+        if (IsTargetPlaceholder(row_index)) {
+            PAIMON_RETURN_NOT_OK(AddBuilderCapacity(2, &entry_count, "MAP<..., 
BLOB> entry count"));
+            value_null_count += 2;
+            continue;
+        }
+        if (IsTargetNull(row_index)) {
+            ++map_null_count;
+            continue;
+        }
+
+        PAIMON_ASSIGN_OR_RAISE(MapBlobPayload payload,
+                               ReadMapBlobPayload(row_index, 
fixed_key_length));
+        
PAIMON_RETURN_NOT_OK(AddBuilderCapacity(static_cast<int64_t>(payload.value_lengths.size()),
+                                                &entry_count, "MAP<..., BLOB> 
entry count"));
+        PAIMON_RETURN_NOT_OK(AddBuilderCapacity(payload.key_data_length, 
&key_data_length,
+                                                "MAP<..., BLOB> key data 
size"));
+        for (int64_t value_length : payload.value_lengths) {
+            if (value_length == BlobDefs::kNullBinLength) {
+                ++value_null_count;
+                continue;
+            }
+            if (blob_as_descriptor_) {
+                PAIMON_RETURN_NOT_OK(
+                    AddBuilderCapacity(1, &descriptor_count, "MAP<..., BLOB> 
descriptor count"));
+            } else {
+                PAIMON_RETURN_NOT_OK(AddBuilderCapacity(value_length, 
&value_data_length,
+                                                        "MAP<..., BLOB> value 
data size"));
+            }
+        }
+        payloads.emplace_back(std::move(payload));
+    }
+    if (descriptor_count > 0) {
+        PAIMON_ASSIGN_OR_RAISE(int64_t descriptor_size, 
GetOrCreateSerializedDescriptorSize());
+        PAIMON_RETURN_NOT_OK(AddBuilderCapacityProduct(descriptor_count, 
descriptor_size,
+                                                       &value_data_length,
+                                                       "MAP<..., BLOB> 
descriptor data size"));
+    }
+
+    if (entry_count > std::numeric_limits<int32_t>::max()) {
+        return Status::CapacityError("MAP<..., BLOB> entry count exceeds map 
offset capacity");
+    }
+
+    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::ArrayBuilder> 
key_builder,
                                       arrow::MakeBuilder(key_type, 
arrow_pool_.get()));
-    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::unique_ptr<arrow::ArrayBuilder> 
item_builder_unique,
+    PAIMON_RETURN_NOT_OK_FROM_ARROW(key_builder->Reserve(entry_count));
+    if (key_type->id() == arrow::Type::STRING) {
+        PAIMON_RETURN_NOT_OK_FROM_ARROW(
+            
checked_cast<arrow::StringBuilder*>(key_builder.get())->ReserveData(key_data_length));
+    } else if (key_type->id() == arrow::Type::BINARY) {
+        PAIMON_RETURN_NOT_OK_FROM_ARROW(
+            
checked_cast<arrow::BinaryBuilder*>(key_builder.get())->ReserveData(key_data_length));
+    }
+
+    if (!blob_as_descriptor_) {
+        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Buffer> map_offsets,
+                               AllocateTypedBuffer<int32_t>(rows_to_read + 1, 
arrow_pool_));
+        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Buffer> map_null_bitmap,
+                               AllocateValidityBitmap(rows_to_read, 
map_null_count, arrow_pool_));
+        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<PreallocatedLargeBinaryBuilder> 
value_builder,
+                               PreallocatedLargeBinaryBuilder::Create(
+                                   entry_count, value_data_length, 
value_null_count, arrow_pool_));
+
+        auto* raw_map_offsets = 
reinterpret_cast<int32_t*>(map_offsets->mutable_data());
+        raw_map_offsets[0] = 0;
+        size_t payload_index = 0;
+        for (int32_t k = 0; k < rows_to_read; ++k) {
+            const size_t row_index = current_pos_ + k;
+            if (IsTargetNull(row_index)) {
+                arrow::bit_util::ClearBit(map_null_bitmap->mutable_data(), k);
+            } else if (IsTargetPlaceholder(row_index)) {
+                
PAIMON_RETURN_NOT_OK_FROM_ARROW(key_builder->AppendEmptyValues(2));
+                PAIMON_RETURN_NOT_OK(value_builder->AppendNull());
+                PAIMON_RETURN_NOT_OK(value_builder->AppendNull());
+            } else {
+                const MapBlobPayload& payload = payloads[payload_index++];
+                PAIMON_RETURN_NOT_OK(AppendMapBlobKeys(payload, key_type, 
key_builder.get()));
+                uint8_t* row_target = nullptr;
+                int64_t row_data_length = 0;
+                for (int64_t value_length : payload.value_lengths) {
+                    if (value_length == BlobDefs::kNullBinLength) {
+                        PAIMON_RETURN_NOT_OK(value_builder->AppendNull());
+                        continue;
+                    }
+                    PAIMON_ASSIGN_OR_RAISE(auto* target, 
value_builder->Append(value_length));
+                    if (row_target == nullptr && value_length > 0) {
+                        row_target = target;
+                    }
+                    row_data_length += value_length;
+                }
+                if (row_data_length > 0) {
+                    PAIMON_RETURN_NOT_OK(
+                        ReadBlobContentAt(payload.data_offset + 
payload.key_data_length,
+                                          row_data_length, row_target));
+                }
+            }
+            raw_map_offsets[k + 1] = 
static_cast<int32_t>(value_builder->length());
+        }
+
+        std::shared_ptr<arrow::Array> keys;
+        PAIMON_RETURN_NOT_OK_FROM_ARROW(key_builder->Finish(&keys));
+        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::LargeBinaryArray> values,
+                               value_builder->Finish());
+        auto map_array = std::make_shared<arrow::MapArray>(
+            map_type, rows_to_read, map_offsets, keys, values, 
map_null_bitmap, map_null_count);
+        PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::StructArray> 
struct_array,
+                                          
arrow::StructArray::Make({map_array}, {map_field}));
+        return struct_array;
+    }
+
+    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::ArrayBuilder> 
item_builder,
                                       
arrow::MakeBuilder(map_type->item_type(), arrow_pool_.get()));
-    std::shared_ptr<arrow::ArrayBuilder> 
key_builder(std::move(key_builder_unique));
-    std::shared_ptr<arrow::ArrayBuilder> 
item_builder(std::move(item_builder_unique));
     if (!item_builder || !item_builder->type() ||
         item_builder->type()->id() != arrow::Type::LARGE_BINARY) {
         return Status::Invalid("cast MAP<..., BLOB> item builder to large 
binary builder failed");
     }
     auto* blob_builder = 
checked_cast<arrow::LargeBinaryBuilder*>(item_builder.get());
     arrow::MapBuilder map_builder(arrow_pool_.get(), key_builder, 
item_builder, map_type);
+    auto* entry_builder = 
checked_cast<arrow::StructBuilder*>(map_builder.value_builder());
+    PAIMON_RETURN_NOT_OK_FROM_ARROW(map_builder.Reserve(rows_to_read));
+    PAIMON_RETURN_NOT_OK_FROM_ARROW(entry_builder->Reserve(entry_count));
+    PAIMON_RETURN_NOT_OK_FROM_ARROW(blob_builder->Reserve(entry_count));
+    
PAIMON_RETURN_NOT_OK_FROM_ARROW(blob_builder->ReserveData(value_data_length));
 
+    size_t payload_index = 0;
     for (int32_t k = 0; k < rows_to_read; ++k) {
         const size_t row_index = current_pos_ + k;
         if (IsTargetNull(row_index)) {
@@ -752,11 +1103,11 @@ Result<std::shared_ptr<arrow::Array>> 
BlobFileBatchReader::BuildMapBlobArray(
             continue;
         }
 
-        PAIMON_ASSIGN_OR_RAISE(MapBlobPayload payload,
-                               ReadMapBlobPayload(row_index, 
fixed_key_length));
         PAIMON_RETURN_NOT_OK_FROM_ARROW(map_builder.Append());
-        PAIMON_RETURN_NOT_OK(AppendMapBlobKeys(payload, key_type, 
key_builder.get()));
-        PAIMON_RETURN_NOT_OK(AppendMapBlobValues(payload, blob_builder));
+        PAIMON_RETURN_NOT_OK(
+            AppendMapBlobKeys(payloads[payload_index], key_type, 
key_builder.get()));
+        PAIMON_RETURN_NOT_OK(AppendMapBlobDescriptors(payloads[payload_index], 
blob_builder));
+        ++payload_index;
     }
 
     std::shared_ptr<arrow::MapArray> built_map_array;
@@ -796,6 +1147,26 @@ Result<std::shared_ptr<arrow::Array>> 
BlobFileBatchReader::BuildTargetArray(
         return Status::Invalid("cast to large binary builder failed");
     }
     auto* field_builder = 
checked_cast<arrow::LargeBinaryBuilder*>(field_builder_base);
+    int64_t descriptor_count = 0;
+    int64_t value_data_length = 0;
+    for (int32_t k = 0; k < rows_to_read; ++k) {
+        const size_t i = current_pos_ + k;
+        if (IsTargetPlaceholder(i)) {
+            
PAIMON_RETURN_NOT_OK(AddBuilderCapacity(BlobDefs::kPlaceholderSentinelLength,
+                                                    &value_data_length,
+                                                    "BLOB descriptor data 
size"));
+        } else if (!IsTargetNull(i)) {
+            PAIMON_RETURN_NOT_OK(AddBuilderCapacity(1, &descriptor_count, 
"BLOB descriptor count"));
+        }
+    }
+    if (descriptor_count > 0) {
+        PAIMON_ASSIGN_OR_RAISE(int64_t descriptor_size, 
GetOrCreateSerializedDescriptorSize());
+        PAIMON_RETURN_NOT_OK(AddBuilderCapacityProduct(
+            descriptor_count, descriptor_size, &value_data_length, "BLOB 
descriptor data size"));
+    }
+    PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Reserve(rows_to_read));
+    PAIMON_RETURN_NOT_OK_FROM_ARROW(field_builder->Reserve(rows_to_read));
+    
PAIMON_RETURN_NOT_OK_FROM_ARROW(field_builder->ReserveData(value_data_length));
     for (int32_t k = 0; k < rows_to_read; ++k) {
         const size_t i = current_pos_ + k;
         PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Append());
diff --git a/src/paimon/format/blob/blob_file_batch_reader.h 
b/src/paimon/format/blob/blob_file_batch_reader.h
index e49a2180..372c742d 100644
--- a/src/paimon/format/blob/blob_file_batch_reader.h
+++ b/src/paimon/format/blob/blob_file_batch_reader.h
@@ -178,6 +178,8 @@ class BlobFileBatchReader : public FileBatchReader {
                         const std::shared_ptr<MemoryPool>& pool,
                         const std::shared_ptr<arrow::MemoryPool>& arrow_pool);
 
+    Result<int64_t> GetOrCreateSerializedDescriptorSize() const;
+
     Status ReadBlobContentAt(const int64_t offset, const int64_t length, 
uint8_t* content) const;
 
     Result<std::shared_ptr<arrow::Buffer>> NextBlobOffsets(int32_t 
rows_to_read) const;
@@ -186,15 +188,15 @@ class BlobFileBatchReader : public FileBatchReader {
     Result<std::shared_ptr<arrow::Buffer>> BuildNullBitmap(int32_t 
rows_to_read) const;
     Result<std::shared_ptr<arrow::Array>> BuildContentArray(int32_t 
rows_to_read) const;
     Result<ArrayBlobPayload> ReadArrayBlobPayload(size_t row_index) const;
-    Status AppendArrayBlobValues(const ArrayBlobPayload& payload,
-                                 arrow::LargeBinaryBuilder* blob_builder) 
const;
+    Status AppendArrayBlobDescriptors(const ArrayBlobPayload& payload,
+                                      arrow::LargeBinaryBuilder* blob_builder) 
const;
     Result<std::shared_ptr<arrow::Array>> BuildArrayBlobArray(int32_t 
rows_to_read) const;
     Result<MapBlobPayload> ReadMapBlobPayload(size_t row_index, int32_t 
fixed_key_length) const;
     Status AppendMapBlobKeys(const MapBlobPayload& payload,
                              const std::shared_ptr<arrow::DataType>& key_type,
                              arrow::ArrayBuilder* key_builder) const;
-    Status AppendMapBlobValues(const MapBlobPayload& payload,
-                               arrow::LargeBinaryBuilder* blob_builder) const;
+    Status AppendMapBlobDescriptors(const MapBlobPayload& payload,
+                                    arrow::LargeBinaryBuilder* blob_builder) 
const;
     Result<std::shared_ptr<arrow::Array>> BuildMapBlobArray(int32_t 
rows_to_read) const;
     Result<std::shared_ptr<arrow::Array>> BuildTargetArray(int32_t 
rows_to_read) const;
 
@@ -242,6 +244,7 @@ class BlobFileBatchReader : public FileBatchReader {
     const bool emit_placeholder_sentinel_;
     std::shared_ptr<MemoryPool> pool_;
     std::shared_ptr<arrow::MemoryPool> arrow_pool_;
+    mutable int64_t serialized_descriptor_size_ = -1;
 
     std::shared_ptr<arrow::DataType> target_type_;
     std::shared_ptr<Metrics> metrics_;

Reply via email to