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 0e46d3c4 perf(parquet): size Arrow builders by selected rows in 
page-filtered reads (#309)
0e46d3c4 is described below

commit 0e46d3c4a3eb79a1a7a78452e564903b5bff6ae8
Author: Yonghao Fang <[email protected]>
AuthorDate: Wed Sep 9 22:22:08 2026 +0800

    perf(parquet): size Arrow builders by selected rows in page-filtered reads 
(#309)
---
 cmake_modules/arrow.diff                           | 128 +++++++++++++++------
 .../parquet/page_filtered_row_group_reader.cpp     |  56 +++++++--
 .../parquet/page_filtered_row_group_reader.h       |   5 +
 3 files changed, 146 insertions(+), 43 deletions(-)

diff --git a/cmake_modules/arrow.diff b/cmake_modules/arrow.diff
index bb71e9c3..f42eea64 100644
--- a/cmake_modules/arrow.diff
+++ b/cmake_modules/arrow.diff
@@ -12,17 +12,18 @@ index e7523add27..e079a1ad41 100644
        )
      endif()
 diff --git a/cpp/cmake_modules/ThirdpartyToolchain.cmake 
b/cpp/cmake_modules/ThirdpartyToolchain.cmake
-index 8cb3ec83f5..0765df8fa8 100644
+index 8cb3ec83f5..9aad83ac95 100644
 --- a/cpp/cmake_modules/ThirdpartyToolchain.cmake
 +++ b/cpp/cmake_modules/ThirdpartyToolchain.cmake
-@@ -814,5 +814,6 @@ if(DEFINED ENV{ARROW_THRIFT_URL})
+@@ -814,6 +814,7 @@ if(DEFINED ENV{ARROW_THRIFT_URL})
    set(THRIFT_SOURCE_URL "$ENV{ARROW_THRIFT_URL}")
  else()
    set_urls(THRIFT_SOURCE_URL
 +           
"https://archive.apache.org/dist/thrift/${ARROW_THRIFT_BUILD_VERSION}/thrift-${ARROW_THRIFT_BUILD_VERSION}.tar.gz";
             
"https://www.apache.org/dyn/closer.cgi?action=download&filename=/thrift/${ARROW_THRIFT_BUILD_VERSION}/thrift-${ARROW_THRIFT_BUILD_VERSION}.tar.gz";
             
"https://downloads.apache.org/thrift/${ARROW_THRIFT_BUILD_VERSION}/thrift-${ARROW_THRIFT_BUILD_VERSION}.tar.gz";
-@@ -983,6 +983,11 @@ if(CMAKE_TOOLCHAIN_FILE)
+            
"https://apache.claz.org/thrift/${ARROW_THRIFT_BUILD_VERSION}/thrift-${ARROW_THRIFT_BUILD_VERSION}.tar.gz";
+@@ -983,6 +984,11 @@ if(CMAKE_TOOLCHAIN_FILE)
    list(APPEND EP_COMMON_CMAKE_ARGS 
-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE})
  endif()
 
@@ -34,7 +35,7 @@ index 8cb3ec83f5..0765df8fa8 100644
  # and crosscompiling emulator (for try_run() )
  if(CMAKE_CROSSCOMPILING_EMULATOR)
    string(REPLACE ";" ${EP_LIST_SEPARATOR} EP_CMAKE_CROSSCOMPILING_EMULATOR
-@@ -1716,6 +1721,7 @@ macro(build_thrift)
+@@ -1716,6 +1722,7 @@ macro(build_thrift)
        -DWITH_JAVASCRIPT=OFF
        -DWITH_LIBEVENT=OFF
        -DWITH_NODEJS=OFF
@@ -56,7 +57,7 @@ index b36c38c6d4..f974a33073 100644
    /// \brief Return zero-copy string_view to upcoming bytes.
    ///
 diff --git a/cpp/src/arrow/util/bit_run_reader.h 
b/cpp/src/arrow/util/bit_run_reader.h
-index a436a503a0..27d483978c 100644
+index a436a50b86..27d483978c 100644
 --- a/cpp/src/arrow/util/bit_run_reader.h
 +++ b/cpp/src/arrow/util/bit_run_reader.h
 @@ -168,6 +168,26 @@ class ARROW_EXPORT BitRunReader {
@@ -87,7 +88,7 @@ index a436a503a0..27d483978c 100644
    int64_t position;
    int64_t length;
 diff --git a/cpp/src/parquet/arrow/reader.cc b/cpp/src/parquet/arrow/reader.cc
-index 285e2a5973..52f42cf5b3 100644
+index 285e2a5973..40bddef195 100644
 --- a/cpp/src/parquet/arrow/reader.cc
 +++ b/cpp/src/parquet/arrow/reader.cc
 @@ -19,12 +19,14 @@
@@ -120,7 +121,7 @@ index 285e2a5973..52f42cf5b3 100644
  #include "arrow/util/tracing_internal.h"
  #include "parquet/arrow/reader_internal.h"
  #include "parquet/column_reader.h"
-@@ -254,6 +254,11 @@ class FileReaderImpl : public FileReader {
+@@ -254,6 +258,11 @@ class FileReaderImpl : public FileReader {
      return GetColumn(i, AllRowGroupsFactory(), out);
    }
 
@@ -132,7 +133,7 @@ index 285e2a5973..52f42cf5b3 100644
    Status GetSchema(std::shared_ptr<::arrow::Schema>* out) override {
      return FromParquetSchema(reader_->metadata()->schema(), 
reader_properties_,
                               reader_->metadata()->key_value_metadata(), out);
-@@ -493,10 +498,40 @@ class LeafReader : public ColumnReaderImpl {
+@@ -493,10 +502,43 @@ class LeafReader : public ColumnReaderImpl {
 
    ::arrow::Status BuildArray(int64_t length_upper_bound,
                               std::shared_ptr<::arrow::ChunkedArray>* out) 
final {
@@ -150,12 +151,15 @@ index 285e2a5973..52f42cf5b3 100644
 +    return {input_->column_index()};
 +  }
 +
-+  ::arrow::Status ResetLeaf(int col_idx, int64_t reserve) final {
++  ::arrow::Status ResetLeaf(int col_idx, int64_t reserve_records,
++                            int64_t reserve_values,
++                            int64_t reserve_value_bytes) final {
 +    if (col_idx != input_->column_index()) return Status::OK();
 +    BEGIN_PARQUET_CATCH_EXCEPTIONS
 +    out_ = nullptr;
 +    record_reader_->Reset();
-+    record_reader_->Reserve(reserve);
++    record_reader_->Reserve(reserve_records);
++    record_reader_->ReserveValueBytes(reserve_values, reserve_value_bytes);
 +    return Status::OK();
 +    END_PARQUET_CATCH_EXCEPTIONS
 +  }
@@ -173,7 +177,7 @@ index 285e2a5973..52f42cf5b3 100644
    const std::shared_ptr<Field> field() override { return field_; }
 
   private:
-@@ -532,6 +567,22 @@ class ExtensionReader : public ColumnReaderImpl {
+@@ -532,6 +574,25 @@ class ExtensionReader : public ColumnReaderImpl {
      return storage_reader_->LoadBatch(number_of_records);
    }
 
@@ -181,8 +185,11 @@ index 285e2a5973..52f42cf5b3 100644
 +    return storage_reader_->LeafColumnIndices();
 +  }
 +
-+  ::arrow::Status ResetLeaf(int col_idx, int64_t reserve) final {
-+    return storage_reader_->ResetLeaf(col_idx, reserve);
++  ::arrow::Status ResetLeaf(int col_idx, int64_t reserve_records,
++                            int64_t reserve_values,
++                            int64_t reserve_value_bytes) final {
++    return storage_reader_->ResetLeaf(col_idx, reserve_records, 
reserve_values,
++                                      reserve_value_bytes);
 +  }
 +
 +  int64_t SkipRecords(int col_idx, int64_t num_records) final {
@@ -196,7 +203,7 @@ index 285e2a5973..52f42cf5b3 100644
    Status BuildArray(int64_t length_upper_bound,
                      std::shared_ptr<ChunkedArray>* out) override {
      std::shared_ptr<ChunkedArray> storage;
-@@ -576,6 +627,22 @@ class ListReader : public ColumnReaderImpl {
+@@ -576,6 +637,25 @@ class ListReader : public ColumnReaderImpl {
      return item_reader_->LoadBatch(number_of_records);
    }
 
@@ -204,8 +211,11 @@ index 285e2a5973..52f42cf5b3 100644
 +    return item_reader_->LeafColumnIndices();
 +  }
 +
-+  ::arrow::Status ResetLeaf(int col_idx, int64_t reserve) final {
-+    return item_reader_->ResetLeaf(col_idx, reserve);
++  ::arrow::Status ResetLeaf(int col_idx, int64_t reserve_records,
++                            int64_t reserve_values,
++                            int64_t reserve_value_bytes) final {
++    return item_reader_->ResetLeaf(col_idx, reserve_records, reserve_values,
++                                   reserve_value_bytes);
 +  }
 +
 +  int64_t SkipRecords(int col_idx, int64_t num_records) final {
@@ -219,7 +229,7 @@ index 285e2a5973..52f42cf5b3 100644
    virtual ::arrow::Result<std::shared_ptr<ChunkedArray>> AssembleArray(
        std::shared_ptr<ArrayData> data) {
      if (field_->type()->id() == ::arrow::Type::MAP) {
-@@ -642,8 +713,10 @@ class ListReader : public ColumnReaderImpl {
+@@ -642,8 +722,10 @@ class ListReader : public ColumnReaderImpl {
 
    const std::shared_ptr<Field> field() override { return field_; }
 
@@ -231,7 +241,7 @@ index 285e2a5973..52f42cf5b3 100644
    std::shared_ptr<Field> field_;
    ::parquet::internal::LevelInfo level_info_;
    std::unique_ptr<ColumnReaderImpl> item_reader_;
-@@ -662,12 +735,62 @@ class PARQUET_NO_EXPORT FixedSizeListReader : public 
ListReader<int32_t> {
+@@ -662,12 +744,62 @@ class PARQUET_NO_EXPORT FixedSizeListReader : public 
ListReader<int32_t> {
      DCHECK_EQ(field()->type()->id(), ::arrow::Type::FIXED_SIZE_LIST);
      const auto& type = 
checked_cast<::arrow::FixedSizeListType&>(*field()->type());
      const int32_t* offsets = reinterpret_cast<const 
int32_t*>(data->buffers[1]->data());
@@ -299,7 +309,7 @@ index 285e2a5973..52f42cf5b3 100644
      }
      data->buffers.resize(1);
      std::shared_ptr<Array> result = ::arrow::MakeArray(data);
-@@ -709,6 +832,39 @@ class PARQUET_NO_EXPORT StructReader : public 
ColumnReaderImpl {
+@@ -709,6 +841,42 @@ class PARQUET_NO_EXPORT StructReader : public 
ColumnReaderImpl {
      }
      return Status::OK();
    }
@@ -313,9 +323,12 @@ index 285e2a5973..52f42cf5b3 100644
 +    return indices;
 +  }
 +
-+  ::arrow::Status ResetLeaf(int col_idx, int64_t reserve) override {
++  ::arrow::Status ResetLeaf(int col_idx, int64_t reserve_records,
++                            int64_t reserve_values,
++                            int64_t reserve_value_bytes) override {
 +    for (const std::unique_ptr<ColumnReaderImpl>& reader : children_) {
-+      RETURN_NOT_OK(reader->ResetLeaf(col_idx, reserve));
++      RETURN_NOT_OK(reader->ResetLeaf(col_idx, reserve_records, 
reserve_values,
++                                      reserve_value_bytes));
 +    }
 +    return Status::OK();
 +  }
@@ -339,7 +352,7 @@ index 285e2a5973..52f42cf5b3 100644
    Status BuildArray(int64_t length_upper_bound,
                      std::shared_ptr<ChunkedArray>* out) override;
    Status GetDefLevels(const int16_t** data, int64_t* length) override;
-@@ -1013,25 +1169,32 @@ Status FileReaderImpl::GetRecordBatchReader(const 
std::vector<int>& row_groups,
+@@ -1013,25 +1181,32 @@ Status FileReaderImpl::GetRecordBatchReader(const 
std::vector<int>& row_groups,
      return Status::OK();
    }
 
@@ -378,7 +391,7 @@ index 285e2a5973..52f42cf5b3 100644
 
          RETURN_NOT_OK(::arrow::internal::OptionalParallelFor(
              reader_properties_.use_threads(), 
static_cast<int>(readers.size()),
-@@ -1224,6 +1387,23 @@ Status FileReaderImpl::GetColumn(int i, 
FileColumnIteratorFactory iterator_facto
+@@ -1224,6 +1399,23 @@ Status FileReaderImpl::GetColumn(int i, 
FileColumnIteratorFactory iterator_facto
    return Status::OK();
  }
 
@@ -403,7 +416,7 @@ index 285e2a5973..52f42cf5b3 100644
                                       const std::vector<int>& column_indices,
                                       std::shared_ptr<Table>* out) {
 diff --git a/cpp/src/parquet/arrow/reader.h b/cpp/src/parquet/arrow/reader.h
-index 6e46ca43f7..e86ff0ef52 100644
+index 6e46ca43f7..e202c6404e 100644
 --- a/cpp/src/parquet/arrow/reader.h
 +++ b/cpp/src/parquet/arrow/reader.h
 @@ -21,6 +21,7 @@
@@ -456,7 +469,7 @@ index 6e46ca43f7..e86ff0ef52 100644
    /// \brief Return arrow schema for all the columns.
    virtual ::arrow::Status GetSchema(std::shared_ptr<::arrow::Schema>* out) = 
0;
 
-@@ -316,6 +342,43 @@ class PARQUET_EXPORT ColumnReader {
+@@ -316,6 +342,51 @@ class PARQUET_EXPORT ColumnReader {
    // the data available in the file.
    virtual ::arrow::Status NextBatch(int64_t batch_size,
                                      std::shared_ptr<::arrow::ChunkedArray>* 
out) = 0;
@@ -468,11 +481,19 @@ index 6e46ca43f7..e86ff0ef52 100644
 +  /// skip/read each leaf independently rather than in lockstep.
 +  virtual std::vector<int> LeafColumnIndices() const { return {}; }
 +
-+  /// \brief Reset the leaf identified by col_idx and reserve space for
-+  /// `reserve` records (in that leaf's post-page-filter compressed space).
-+  /// Must be called before SkipRecords()/ReadRecords() for that leaf, and
-+  /// followed by BuildArray() to get the result.
-+  virtual ::arrow::Status ResetLeaf(int col_idx, int64_t reserve) {
++  /// \brief Reset the leaf identified by col_idx and pre-allocate for the 
read
++  /// that follows. Must be called before SkipRecords()/ReadRecords() for that
++  /// leaf, and followed by BuildArray() to get the result.
++  ///
++  /// `reserve_records` is in that leaf's post-page-filter compressed space,
++  /// because SkipRecords walks the levels it bounds. `reserve_values` and
++  /// `reserve_value_bytes` describe what will actually be APPENDED, which on 
a
++  /// selective read is far less, and only the variable-width readers use the
++  /// byte count. Zero for the latter two means "no estimate" and reserves
++  /// nothing beyond `reserve_records`.
++  virtual ::arrow::Status ResetLeaf(int col_idx, int64_t reserve_records,
++                                    int64_t reserve_values,
++                                    int64_t reserve_value_bytes) {
 +    return ::arrow::Status::NotImplemented("ResetLeaf not implemented");
 +  }
 +
@@ -635,7 +656,7 @@ index 4a1a033a7b..0f13d05e44 100644
    virtual ::arrow::Status Close() = 0;
    virtual ~FileWriter();
 diff --git a/cpp/src/parquet/column_reader.cc 
b/cpp/src/parquet/column_reader.cc
-index ebf9515f27..0abc7d2320 100644
+index ebf9515f27..00b96797d9 100644
 --- a/cpp/src/parquet/column_reader.cc
 +++ b/cpp/src/parquet/column_reader.cc
 @@ -208,6 +208,39 @@ ReaderProperties default_reader_properties() {
@@ -789,8 +810,31 @@ index ebf9515f27..0abc7d2320 100644
        ++page_ordinal_;
        const format::DataPageHeaderV2& header = 
current_page_header_.data_page_header_v2;
 
+@@ -2156,6 +2242,22 @@ class ByteArrayChunkedRecordReader final : public 
TypedRecordReader<ByteArrayTyp
+     return result;
+   }
+
++  void ReserveValueBytes(int64_t num_values, int64_t num_bytes) override {
++    if (num_values > 0) {
++      PARQUET_THROW_NOT_OK(accumulator_.builder->Reserve(num_values));
++    }
++    if (num_bytes > 0) {
++      // Never reserve past the current chunk: ArrowBinaryHelper pushes a 
chunk
++      // when the builder's data would exceed kBinaryMemoryLimit, and
++      // ReserveData() would fail the overflow check on a request past it. The
++      // clamp mirrors the one in ArrowBinaryHelper::Prepare.
++      const int64_t room =
++          ::arrow::kBinaryMemoryLimit - 
accumulator_.builder->value_data_length();
++      PARQUET_THROW_NOT_OK(
++          accumulator_.builder->ReserveData(std::min<int64_t>(num_bytes, 
room)));
++    }
++  }
++
+   void ReadValuesDense(int64_t values_to_read) override {
+     int64_t num_decoded = this->current_decoder_->DecodeArrowNonNull(
+         static_cast<int>(values_to_read), &accumulator_);
 diff --git a/cpp/src/parquet/column_reader.h b/cpp/src/parquet/column_reader.h
-index 29e1b2a25e..386e574644 100644
+index 29e1b2a25e..a7e68b7dd6 100644
 --- a/cpp/src/parquet/column_reader.h
 +++ b/cpp/src/parquet/column_reader.h
 @@ -76,6 +76,18 @@ struct PARQUET_EXPORT DataPageStats {
@@ -846,6 +890,26 @@ index 29e1b2a25e..386e574644 100644
  };
 
  class PARQUET_EXPORT ColumnReader {
+@@ -341,6 +370,19 @@ class PARQUET_EXPORT RecordReader {
+   /// \brief Pre-allocate space for data. Results in better flat read 
performance
+   virtual void Reserve(int64_t num_values) = 0;
+
++  /// \brief Pre-allocate the variable-width value buffers for `num_values`
++  /// values totalling about `num_bytes` bytes.
++  ///
++  /// Fixed-width readers size their values buffer from Reserve() alone, so 
the
++  /// default is a no-op. The BYTE_ARRAY readers build into an
++  /// ::arrow::BinaryBuilder that Reserve() deliberately leaves alone
++  /// (uses_values_ is false for them), which is the only reason this second
++  /// hook exists: without it the builder's offsets and data buffers grow by
++  /// doubling once per decoded batch, and every doubling copies everything 
the
++  /// row group has accumulated so far. Both counts are additive, like
++  /// Reserve(); zero means "no estimate" and reserves nothing.
++  virtual void ReserveValueBytes(int64_t num_values, int64_t num_bytes) {}
++
+   /// \brief Clear consumed values and repetition/definition levels as the
+   /// result of calling ReadRecords
+   /// For FLBA and ByteArray types, call GetBuilderChunks() to reset them.
 diff --git a/cpp/src/parquet/file_reader.cc b/cpp/src/parquet/file_reader.cc
 index 3e9eeea6c6..671ebe4644 100644
 --- a/cpp/src/parquet/file_reader.cc
diff --git a/src/paimon/format/parquet/page_filtered_row_group_reader.cpp 
b/src/paimon/format/parquet/page_filtered_row_group_reader.cpp
index f20f224f..70048b12 100644
--- a/src/paimon/format/parquet/page_filtered_row_group_reader.cpp
+++ b/src/paimon/format/parquet/page_filtered_row_group_reader.cpp
@@ -33,6 +33,7 @@
 #include "fmt/format.h"
 #include "paimon/common/utils/arrow/arrow_utils.h"
 #include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/saturating_cast.h"
 #include "parquet/arrow/reader.h"
 #include "parquet/arrow/reader_internal.h"
 #include "parquet/arrow/schema.h"
@@ -43,6 +44,12 @@ namespace paimon::parquet {
 
 namespace {
 
+/// Ceiling on the value bytes a leaf may reserve up front from column chunk 
metadata alone.
+/// The estimate it caps is a heuristic over footer fields, which are 
attacker-controlled and
+/// need not describe the pages this read touches, so it must not turn into an 
unbounded eager
+/// allocation. Past this size the builder's doubling is already amortized 
against a large read.
+constexpr int64_t kMaxMetadataValueBytesReservation = int64_t{16} * 1024 * 
1024;
+
 struct DataPageLayout {
     int64_t column_chunk_offset;
     int64_t first_data_page_offset;
@@ -234,9 +241,10 @@ std::pair<RowRanges, int64_t> 
PageFilteredRowGroupReader::ComputeCompressedRowRa
 }
 
 Status PageFilteredRowGroupReader::ExecuteSkipReadPattern(
-    int col_idx, const RowRanges& ranges, int64_t total,
-    ::parquet::arrow::ColumnReader* column_reader) {
-    PAIMON_RETURN_NOT_OK_FROM_ARROW(column_reader->ResetLeaf(col_idx, total));
+    int col_idx, const RowRanges& ranges, int64_t total, int64_t 
reserve_values,
+    int64_t reserve_value_bytes, ::parquet::arrow::ColumnReader* 
column_reader) {
+    PAIMON_RETURN_NOT_OK_FROM_ARROW(
+        column_reader->ResetLeaf(col_idx, total, reserve_values, 
reserve_value_bytes));
     int64_t current = 0;
     for (const auto& range : ranges.GetRanges()) {
         int64_t skip = range.from > current ? range.from - current : 0;
@@ -341,14 +349,15 @@ Result<std::shared_ptr<arrow::ChunkedArray>> 
PageFilteredRowGroupReader::ReadFil
     for (int col_idx : column_reader->LeafColumnIndices()) {
         RowRanges effective_ranges = row_ranges;
         int64_t effective_total = row_ranges.IsEmpty() ? 0 : 
row_group_row_count;
-        if (!row_ranges.IsEmpty() && rg_page_index_reader) {
-            auto offset_index = rg_page_index_reader->GetOffsetIndex(col_idx);
-            if (offset_index) {
-                auto row_group_metadata =
-                    
arrow_file_reader->parquet_reader()->metadata()->RowGroup(row_group_index);
-                auto column_chunk = row_group_metadata->ColumnChunk(col_idx);
-                if (MakeDataPageReadPlan(row_ranges, offset_index, 
*column_chunk,
-                                         row_group_row_count)) {
+        std::unique_ptr<::parquet::ColumnChunkMetaData> column_chunk;
+        if (!row_ranges.IsEmpty()) {
+            auto row_group_metadata =
+                
arrow_file_reader->parquet_reader()->metadata()->RowGroup(row_group_index);
+            column_chunk = row_group_metadata->ColumnChunk(col_idx);
+            if (rg_page_index_reader) {
+                auto offset_index = 
rg_page_index_reader->GetOffsetIndex(col_idx);
+                if (offset_index && MakeDataPageReadPlan(row_ranges, 
offset_index, *column_chunk,
+                                                         row_group_row_count)) 
{
                     auto [compressed, total] =
                         ComputeCompressedRowRanges(row_ranges, offset_index, 
row_group_row_count);
                     effective_ranges = std::move(compressed);
@@ -357,7 +366,32 @@ Result<std::shared_ptr<arrow::ChunkedArray>> 
PageFilteredRowGroupReader::ReadFil
             }
         }
 
+        // Rows that will actually be appended, as opposed to effective_total, 
which is the
+        // compressed space SkipRecords walks: a highly selective predicate 
reads a handful of
+        // rows out of a row group, and reserving for the whole group would 
allocate tens of MiB
+        // of offsets nobody writes.
+        const int64_t reserve_values = effective_ranges.RowCount();
+        int64_t reserve_value_bytes = 0;
+        const int64_t chunk_bytes = column_chunk ? 
column_chunk->total_uncompressed_size() : 0;
+        const int64_t chunk_values = column_chunk ? column_chunk->num_values() 
: 0;
+        if (chunk_bytes > 0 && chunk_values > 0 && reserve_values > 0) {
+            // A heuristic, not a bound. total_uncompressed_size is 
uncompressed but still
+            // ENCODED: a dictionary page stores each value once and its data 
pages only
+            // indices, DELTA_BYTE_ARRAY only prefix deltas, so both can 
decode into more Arrow
+            // payload than they occupy, while page headers, levels and 
BYTE_ARRAY length
+            // prefixes pull the other way. The average is also taken over the 
whole chunk,
+            // including the pages this selection skips, so it misleads when 
wide values sit in
+            // skipped pages. Either direction only costs performance — the 
reservation is a
+            // hint the builder grows past when short — but they are why the 
result is capped
+            // instead of trusted. Fixed-width leaves ignore the byte count 
entirely.
+            const double avg = static_cast<double>(chunk_bytes) / 
static_cast<double>(chunk_values);
+            reserve_value_bytes = std::min(
+                {SaturatingDoubleToInteger<int64_t>(avg * 
static_cast<double>(reserve_values)),
+                 chunk_bytes, kMaxMetadataValueBytesReservation});
+        }
+
         PAIMON_RETURN_NOT_OK(ExecuteSkipReadPattern(col_idx, effective_ranges, 
effective_total,
+                                                    reserve_values, 
reserve_value_bytes,
                                                     column_reader.get()));
     }
 
diff --git a/src/paimon/format/parquet/page_filtered_row_group_reader.h 
b/src/paimon/format/parquet/page_filtered_row_group_reader.h
index a143ae5a..0633473c 100644
--- a/src/paimon/format/parquet/page_filtered_row_group_reader.h
+++ b/src/paimon/format/parquet/page_filtered_row_group_reader.h
@@ -111,7 +111,12 @@ class PageFilteredRowGroupReader {
 
     /// Reset the given leaf and replay the skip/read pattern derived from 
`ranges`
     /// directly against the ColumnReader (ResetLeaf + 
SkipRecords/ReadRecords).
+    /// `total` is the leaf's compressed-space size, which bounds the levels
+    /// SkipRecords walks; `reserve_values` / `reserve_value_bytes` describe 
what the
+    /// pattern will actually append, so a variable-width leaf can size its 
Arrow
+    /// builder once instead of doubling it per decoded batch.
     static Status ExecuteSkipReadPattern(int col_idx, const RowRanges& ranges, 
int64_t total,
+                                         int64_t reserve_values, int64_t 
reserve_value_bytes,
                                          ::parquet::arrow::ColumnReader* 
column_reader);
 
     /// Read a field (flat or nested) using ColumnReader tree.

Reply via email to