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 c73f0087 feat(reader): support late materialization with probe/payload
two-phase reads (#243)
c73f0087 is described below
commit c73f0087cea6174fbffe813601ed8c7a904599f8
Author: Zhou Hongfeng <[email protected]>
AuthorDate: Thu Aug 27 11:22:08 2026 +0800
feat(reader): support late materialization with probe/payload two-phase
reads (#243)
---
include/paimon/read_context.h | 15 +-
include/paimon/reader/prefetch_file_batch_reader.h | 2 +-
src/paimon/CMakeLists.txt | 2 +
.../late_materializing_file_batch_reader.cpp | 369 ++++++++++++
.../reader/late_materializing_file_batch_reader.h | 184 ++++++
.../late_materializing_file_batch_reader_test.cpp | 670 +++++++++++++++++++++
.../reader/late_materializing_reader_builder.h | 70 +++
.../reader/prefetch_file_batch_reader_impl.cpp | 9 +-
.../reader/prefetch_file_batch_reader_impl.h | 2 +-
src/paimon/common/utils/arrow/arrow_utils.cpp | 11 +-
src/paimon/common/utils/arrow/arrow_utils.h | 3 +
src/paimon/core/operation/abstract_split_read.cpp | 15 +-
src/paimon/core/operation/abstract_split_read.h | 4 +-
.../core/operation/data_evolution_split_read.h | 3 +-
src/paimon/core/operation/internal_read_context.h | 3 +
src/paimon/core/operation/merge_file_split_read.h | 3 +-
.../core/operation/merge_file_split_read_test.cpp | 58 ++
src/paimon/core/operation/raw_file_split_read.h | 3 +-
src/paimon/core/operation/read_context.cpp | 27 +-
src/paimon/format/orc/orc_file_batch_reader.h | 2 +-
.../format/parquet/parquet_file_batch_reader.cpp | 2 +
.../format/parquet/parquet_file_batch_reader.h | 5 +-
src/paimon/testing/mock/mock_file_batch_reader.h | 87 ++-
test/inte/blob_table_inte_test.cpp | 1 +
test/inte/global_index_test.cpp | 3 +-
test/inte/read_inte_test.cpp | 170 ++++++
test/inte/read_inte_with_index_test.cpp | 71 ++-
test/inte/scan_and_read_inte_test.cpp | 92 ++-
28 files changed, 1841 insertions(+), 45 deletions(-)
diff --git a/include/paimon/read_context.h b/include/paimon/read_context.h
index 3e58b1c4..e645645b 100644
--- a/include/paimon/read_context.h
+++ b/include/paimon/read_context.h
@@ -51,7 +51,7 @@ class PAIMON_EXPORT ReadContext {
const std::vector<std::string>& read_field_names,
const std::vector<int32_t>& read_field_ids,
const std::shared_ptr<Predicate>& predicate, bool
enable_predicate_filter,
- bool enable_prefetch, uint32_t prefetch_batch_count,
+ bool enable_prefetch, bool enable_late_materializing, uint32_t
prefetch_batch_count,
uint32_t prefetch_max_parallel_num, bool
enable_multi_thread_row_to_batch,
uint32_t row_to_batch_thread_number, const
std::optional<std::string>& table_schema,
const std::shared_ptr<MemoryPool>& memory_pool,
@@ -97,6 +97,9 @@ class PAIMON_EXPORT ReadContext {
bool EnablePrefetch() const {
return enable_prefetch_;
}
+ bool EnableLateMaterializing() const {
+ return enable_late_materializing_;
+ }
uint32_t GetPrefetchBatchCount() const {
return prefetch_batch_count_;
}
@@ -163,6 +166,7 @@ class PAIMON_EXPORT ReadContext {
std::shared_ptr<Predicate> predicate_;
bool enable_predicate_filter_;
bool enable_prefetch_;
+ bool enable_late_materializing_;
uint32_t prefetch_batch_count_;
uint32_t prefetch_max_parallel_num_;
bool enable_multi_thread_row_to_batch_;
@@ -306,6 +310,15 @@ class PAIMON_EXPORT ReadContextBuilder {
/// @return Reference to this builder for method chaining.
ReadContextBuilder& EnablePrefetch(bool enabled);
+ /// Enable or disable late materialization (probe/payload two-phase
reads). When enabled,
+ /// each parallel reader under the prefetch layer performs a probe read of
predicate
+ /// columns first and only materializes payload columns for matched rows.
+ /// @param enabled Whether to enable late materialization (default: false)
+ /// @return Reference to this builder for method chaining.
+ /// @note Without a pushed-down predicate the late-materializing reader
degrades to a
+ /// plain passthrough.
+ ReadContextBuilder& EnableLateMaterializing(bool enabled);
+
/// Enable or disable the read-ahead cache for read operations.
///
/// A read-ahead cache is used to prebuffer data ranges before they are
needed,
diff --git a/include/paimon/reader/prefetch_file_batch_reader.h
b/include/paimon/reader/prefetch_file_batch_reader.h
index acc7d0bb..5e6313e8 100644
--- a/include/paimon/reader/prefetch_file_batch_reader.h
+++ b/include/paimon/reader/prefetch_file_batch_reader.h
@@ -40,7 +40,7 @@ class PAIMON_EXPORT PrefetchFileBatchReader : public
FileBatchReader {
/// Retrieves the row number of the next row to be read.
/// This method indicates the current read position within the file.
/// @return The row number of the next row to read.
- virtual uint64_t GetNextRowToRead() const = 0;
+ virtual Result<uint64_t> GetNextRowToRead() const = 0;
/// Generates a list of row ranges to be read in batches.
/// Each range specifies the start and end row numbers for a batch,
diff --git a/src/paimon/CMakeLists.txt b/src/paimon/CMakeLists.txt
index 61fd7c96..8fb6c17d 100644
--- a/src/paimon/CMakeLists.txt
+++ b/src/paimon/CMakeLists.txt
@@ -134,6 +134,7 @@ set(PAIMON_COMMON_SRCS
common/predicate/starts_with.cpp
common/reader/batch_reader.cpp
common/reader/concat_batch_reader.cpp
+ common/reader/late_materializing_file_batch_reader.cpp
common/reader/predicate_batch_reader.cpp
common/reader/prefetch_file_batch_reader_impl.cpp
common/reader/reader_utils.cpp
@@ -606,6 +607,7 @@ if(PAIMON_BUILD_TESTS)
common/predicate/predicate_utils_test.cpp
common/predicate/predicate_validator_test.cpp
common/reader/concat_batch_reader_test.cpp
+ common/reader/late_materializing_file_batch_reader_test.cpp
common/reader/predicate_batch_reader_test.cpp
common/reader/prefetch_file_batch_reader_impl_test.cpp
common/reader/reader_utils_test.cpp
diff --git a/src/paimon/common/reader/late_materializing_file_batch_reader.cpp
b/src/paimon/common/reader/late_materializing_file_batch_reader.cpp
new file mode 100644
index 00000000..ed7fa304
--- /dev/null
+++ b/src/paimon/common/reader/late_materializing_file_batch_reader.cpp
@@ -0,0 +1,369 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "paimon/common/reader/late_materializing_file_batch_reader.h"
+
+#include <cassert>
+#include <map>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "arrow/array/concatenate.h"
+#include "arrow/array/util.h"
+#include "arrow/c/bridge.h"
+#include "arrow/memory_pool.h"
+#include "arrow/type.h"
+#include "arrow/util/checked_cast.h"
+#include "fmt/format.h"
+#include "paimon/common/predicate/predicate_filter.h"
+#include "paimon/common/predicate/predicate_validator.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/predicate/predicate_utils.h"
+#include "paimon/status.h"
+
+namespace paimon {
+
+Result<std::unique_ptr<LateMaterializingFileBatchReader>>
LateMaterializingFileBatchReader::Create(
+ std::unique_ptr<FileBatchReader> inner, std::shared_ptr<MemoryPool> pool) {
+ // The reader's own compaction allocations go through an arrow pool;
bridge the paimon pool
+ // once here so the accounting matches the rest of the read path.
+ if (pool == nullptr) {
+ return Status::Invalid("pool could not be nullptr.");
+ }
+ if (inner == nullptr) {
+ return Status::Invalid("inner could not be nullptr.");
+ }
+ auto* prefetch_inner = dynamic_cast<PrefetchFileBatchReader*>(inner.get());
+ std::shared_ptr<arrow::MemoryPool> arrow_pool = GetArrowPool(pool);
+ auto reader =
+ std::unique_ptr<LateMaterializingFileBatchReader>(new
LateMaterializingFileBatchReader(
+ std::move(inner), prefetch_inner, std::move(arrow_pool)));
+ return reader;
+}
+
+Result<FileBatchReader::ReadBatch>
LateMaterializingFileBatchReader::NextBatch() {
+ if (state_ == kInit) {
+ // SetReadSchema has not been called: read with the file schema,
matching the
+ // FileBatchReader contract for schema-less reads.
+ state_ = kNoLatMat;
+ }
+ if (state_ == kProbing) {
+ PAIMON_RETURN_NOT_OK(ReadAndFilterProbeData());
+ if (matched_bitmap_.IsEmpty()) {
+ state_ = kEOF;
+ } else {
+ // payload pass reads only the matched rows (matched_bitmap_ is
non-empty here).
+ PAIMON_RETURN_NOT_OK(
+ SetInnerReadSchema(payload_schema_, /*predicate=*/nullptr,
matched_bitmap_));
+ state_ = kRunning;
+ }
+ }
+
+ if (state_ == kNoLatMat) {
+ return inner_->NextBatch();
+ } else if (state_ == kRunning) {
+ return ReadPayloadBatch();
+ } else if (state_ == kEOF) {
+ return MakeEofBatch();
+ }
+ return Status::Invalid("invalid state when calling NextBatch: " +
std::to_string(state_));
+}
+
+Result<RoaringBitmap32> LateMaterializingFileBatchReader::FilterProbeBatch(
+ const std::shared_ptr<arrow::Array>& array,
+ const std::shared_ptr<PredicateFilter>& bound_filter) {
+ // TODO(zhouhonfeng.zhf): use arrow::compute::Filter instead of
PredicateFilter
+ PAIMON_ASSIGN_OR_RAISE(std::vector<char> results,
bound_filter->Test(*array));
+ if (results.size() != static_cast<size_t>(array->length())) {
+ return Status::Invalid(
+ fmt::format("predicate result size {} does not match probe batch
length {}",
+ results.size(), array->length()));
+ }
+ // batch-local offsets of the rows passing both the predicate and the
selection
+ RoaringBitmap32 batch_matched;
+ for (int64_t i = 0; i < array->length(); ++i) {
+ if (!results[static_cast<size_t>(i)]) {
+ continue;
+ }
+ // map batch offset to file row id
+ PAIMON_ASSIGN_OR_RAISE(uint64_t file_row,
+
inner_->GetPreviousBatchFileRowId(static_cast<uint64_t>(i)));
+ if (selection_ &&
!selection_->Contains(static_cast<int32_t>(file_row))) {
+ continue;
+ }
+ batch_matched.Add(static_cast<uint32_t>(i));
+ matched_bitmap_.Add(file_row);
+ }
+ return batch_matched;
+}
+
+Status LateMaterializingFileBatchReader::ReadAndFilterProbeData() {
+ matched_bitmap_ = RoaringBitmap32();
+ probe_cursor_ = 0;
+ arrow::ArrayVector probe_arrays;
+ while (true) {
+ PAIMON_ASSIGN_OR_RAISE(FileBatchReader::ReadBatch batch,
inner_->NextBatch());
+ if (BatchReader::IsEofBatch(batch)) {
+ break;
+ }
+ auto& [c_array, c_schema] = batch;
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
+ arrow::ImportArray(c_array.get(),
c_schema.get()));
+ PAIMON_ASSIGN_OR_RAISE(RoaringBitmap32 batch_matched,
+ FilterProbeBatch(array, probe_filter_));
+ // Compact each probe batch down to its matched rows so probe_data_
aligns row-for-row
+ // (ascending file order) with matched_bitmap_ and the later payload
output.
+ if (!batch_matched.IsEmpty()) {
+ PAIMON_ASSIGN_OR_RAISE(arrow::ArrayVector matched_slices,
+
ReaderUtils::GenerateFilteredArrayVector(array, batch_matched));
+ probe_arrays.insert(probe_arrays.end(),
std::make_move_iterator(matched_slices.begin()),
+ std::make_move_iterator(matched_slices.end()));
+ }
+ }
+
+ std::shared_ptr<arrow::Array> probe_array;
+ if (probe_arrays.empty()) {
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+ probe_array,
arrow::MakeEmptyArray(arrow::struct_(probe_schema_->fields())));
+ } else {
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(probe_array,
+ arrow::Concatenate(probe_arrays,
arrow_pool_.get()));
+ }
+ probe_data_ =
arrow::internal::checked_pointer_cast<arrow::StructArray>(probe_array);
+ return Status::OK();
+}
+
+Result<FileBatchReader::ReadBatch>
LateMaterializingFileBatchReader::ReadPayloadBatch() {
+ while (true) {
+ PAIMON_ASSIGN_OR_RAISE(FileBatchReader::ReadBatchWithBitmap
batch_with_bitmap,
+ inner_->NextBatchWithBitmap());
+ if (BatchReader::IsEofBatch(batch_with_bitmap)) {
+ state_ = kEOF;
+ if (probe_cursor_ != probe_data_->length()) {
+ return Status::Invalid(
+ fmt::format("probe cursor {} does not match probe data
length {}",
+ probe_cursor_, probe_data_->length()));
+ }
+ return MakeEofBatch();
+ }
+ auto& [batch, bitmap] = batch_with_bitmap;
+ if (bitmap.IsEmpty()) {
+ ReaderUtils::ReleaseReadBatch(std::move(batch));
+ return Status::Invalid("inner read bitmap is empty.");
+ }
+ auto& [c_array, c_schema] = batch;
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array>
payload_array,
+ arrow::ImportArray(c_array.get(),
c_schema.get()));
+
+ // Generate the valid bitmap and row_mapping_
+ RoaringBitmap32 valid;
+ row_mapping_.clear();
+ for (auto it = bitmap.Begin(); it != bitmap.End(); ++it) {
+ auto offset = static_cast<uint64_t>(*it);
+ PAIMON_ASSIGN_OR_RAISE(uint64_t file_row,
inner_->GetPreviousBatchFileRowId(offset));
+ if (!matched_bitmap_.Contains(file_row)) {
+ continue;
+ }
+ valid.Add(static_cast<uint32_t>(offset));
+ row_mapping_.push_back(file_row);
+ }
+ if (valid.IsEmpty()) {
+ ReaderUtils::ReleaseReadBatch(std::move(batch));
+ continue;
+ }
+
+ // Compact the payload superset down to the matched rows (ascending
file row order).
+ PAIMON_ASSIGN_OR_RAISE(arrow::ArrayVector payload_slices,
+
ReaderUtils::GenerateFilteredArrayVector(payload_array, valid));
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array>
payload_compacted,
+ arrow::Concatenate(payload_slices,
arrow_pool_.get()));
+
+ auto card = static_cast<int64_t>(valid.Cardinality());
+ if (probe_cursor_ + card > probe_data_->length()) {
+ return Status::Invalid(
+ fmt::format("probe cache underflow: cursor {} + {} exceeds
probe rows {}",
+ probe_cursor_, card, probe_data_->length()));
+ }
+ std::shared_ptr<arrow::Array> probe_selected =
probe_data_->Slice(probe_cursor_, card);
+ PAIMON_ASSIGN_OR_RAISE(
+ probe_selected, ArrowUtils::NormalizeArrayOffsets(probe_selected,
arrow_pool_.get()));
+ probe_cursor_ += card;
+
+ PAIMON_ASSIGN_OR_RAISE(FileBatchReader::ReadBatch assembled,
+ AssembleFullBatch(payload_compacted,
probe_selected));
+ return assembled;
+ }
+}
+
+Result<FileBatchReader::ReadBatch>
LateMaterializingFileBatchReader::AssembleFullBatch(
+ const std::shared_ptr<arrow::Array>& payload_array,
+ const std::shared_ptr<arrow::Array>& probe_array) {
+ auto payload_struct =
arrow::internal::checked_pointer_cast<arrow::StructArray>(payload_array);
+ auto probe_struct =
arrow::internal::checked_pointer_cast<arrow::StructArray>(probe_array);
+ arrow::ArrayVector children;
+ children.reserve(full_schema_->num_fields());
+ for (const auto& field : full_schema_->fields()) {
+ std::shared_ptr<arrow::Array> col =
payload_struct->GetFieldByName(field->name());
+ if (!col) {
+ col = probe_struct->GetFieldByName(field->name());
+ }
+ if (!col) {
+ return Status::Invalid(
+ fmt::format("field {} missing in both payload and probe
columns", field->name()));
+ }
+ PAIMON_ASSIGN_OR_RAISE(col, ArrowUtils::NormalizeArrayOffsets(col,
arrow_pool_.get()));
+ children.push_back(std::move(col));
+ }
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::StructArray>
full_struct,
+ arrow::StructArray::Make(children,
full_schema_->fields()));
+ std::unique_ptr<::ArrowArray> c_array = std::make_unique<::ArrowArray>();
+ std::unique_ptr<::ArrowSchema> c_schema =
std::make_unique<::ArrowSchema>();
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(
+ arrow::ExportArray(*full_struct, c_array.get(), c_schema.get()));
+ return std::make_pair(std::move(c_array), std::move(c_schema));
+}
+
+Status LateMaterializingFileBatchReader::SetInnerReadSchema(
+ const std::shared_ptr<arrow::Schema>& read_schema, const
std::shared_ptr<Predicate>& predicate,
+ const std::optional<RoaringBitmap32>& selection) {
+ ::ArrowSchema c_read_schema;
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*read_schema,
&c_read_schema));
+ PAIMON_RETURN_NOT_OK(inner_->SetReadSchema(&c_read_schema, predicate,
selection));
+ return Status::OK();
+}
+
+Status LateMaterializingFileBatchReader::SetReadSchema(
+ ::ArrowSchema* read_schema, const std::shared_ptr<Predicate>& predicate,
+ const std::optional<RoaringBitmap32>& selection_bitmap) {
+ Reset();
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(full_schema_,
arrow::ImportSchema(read_schema));
+ predicate_ = predicate;
+ selection_ = selection_bitmap;
+ if (predicate_ != nullptr) {
+ std::set<std::string> probe_names;
+ PAIMON_RETURN_NOT_OK(PredicateUtils::GetAllNames(predicate_,
&probe_names));
+ arrow::FieldVector probe_fields;
+ arrow::FieldVector payload_fields;
+ for (const auto& field : full_schema_->fields()) {
+ if (probe_names.count(field->name()) > 0) {
+ probe_fields.push_back(field);
+ } else {
+ payload_fields.push_back(field);
+ }
+ }
+ // probing only pays off when the predicate fields are a strict subset
of the read schema
+ if (!probe_fields.empty() && !payload_fields.empty()) {
+ probe_schema_ = arrow::schema(probe_fields,
full_schema_->metadata());
+ payload_schema_ = arrow::schema(payload_fields,
full_schema_->metadata());
+
PAIMON_RETURN_NOT_OK(PredicateValidator::ValidatePredicateWithSchema(
+ *probe_schema_, predicate_, /*validate_field_idx=*/false));
+ std::map<std::string, int32_t> name_to_idx;
+ for (int32_t i = 0; i < probe_schema_->num_fields(); ++i) {
+ name_to_idx.emplace(probe_schema_->field(i)->name(), i);
+ }
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<Predicate> bound_predicate,
+ PredicateUtils::CreatePickedFieldFilter(predicate_,
name_to_idx));
+ probe_filter_ =
std::dynamic_pointer_cast<PredicateFilter>(bound_predicate);
+ if (!probe_filter_) {
+ return Status::Invalid("failed to bind predicate to probe
schema");
+ }
+ }
+ }
+
+ if (predicate_ == nullptr || probe_schema_ == nullptr) {
+ PAIMON_RETURN_NOT_OK(SetInnerReadSchema(full_schema_, predicate_,
selection_));
+ state_ = kNoLatMat;
+ } else {
+ PAIMON_RETURN_NOT_OK(SetInnerReadSchema(probe_schema_, predicate_,
selection_));
+ state_ = kProbing;
+ }
+ return Status::OK();
+}
+
+Result<uint64_t> LateMaterializingFileBatchReader::GetPreviousBatchFileRowId(
+ uint64_t batch_row_id) const {
+ if (state_ == kNoLatMat) {
+ return inner_->GetPreviousBatchFileRowId(batch_row_id);
+ }
+ // In kRunning the emitted batch is compacted/reassembled, so row ids come
from row_mapping_
+ // instead of the inner reader.
+ if (batch_row_id >= row_mapping_.size()) {
+ return Status::Invalid(
+ fmt::format("batch_row_id {} is out of range, last batch row count
is {}", batch_row_id,
+ row_mapping_.size()));
+ }
+ return row_mapping_[batch_row_id];
+}
+
+Status LateMaterializingFileBatchReader::SeekToRow(uint64_t row_number) {
+ PAIMON_ASSIGN_OR_RAISE(PrefetchFileBatchReader * prefetch_reader,
+ GetPrefetchReaderOrRaise("SeekToRow"));
+ PAIMON_RETURN_NOT_OK(prefetch_reader->SeekToRow(row_number));
+ if (state_ == kRunning || state_ == kEOF) {
+ if (matched_bitmap_.IsEmpty()) {
+ state_ = kEOF;
+ return Status::OK();
+ }
+ int64_t cursor = 0;
+ for (auto it = matched_bitmap_.Begin(); it != matched_bitmap_.End();
++it) {
+ if (static_cast<uint64_t>(*it) >= row_number) {
+ break;
+ }
+ ++cursor;
+ }
+ probe_cursor_ = cursor;
+ // a seek after EOF re-activates payload reading
+ state_ = kRunning;
+ }
+ return Status::OK();
+}
+
+Status LateMaterializingFileBatchReader::SetReadRanges(
+ const std::vector<std::pair<uint64_t, uint64_t>>& read_ranges) {
+ if (prefetch_inner_ == nullptr) {
+ // Only the format reader can act on this hint, and the
PrefetchFileBatchReader contract
+ // lets an implementation that cannot honor it ignore the hint.
+ return Status::OK();
+ }
+ return prefetch_inner_->SetReadRanges(read_ranges);
+}
+
+void LateMaterializingFileBatchReader::Reset() {
+ state_ = kInit;
+ matched_bitmap_ = RoaringBitmap32();
+ probe_data_.reset();
+ probe_cursor_ = 0;
+ row_mapping_.clear();
+ probe_schema_.reset();
+ payload_schema_.reset();
+ full_schema_.reset();
+ probe_filter_.reset();
+ predicate_.reset();
+ selection_.reset();
+ probe_cursor_ = 0;
+ row_mapping_.clear();
+}
+
+} // namespace paimon
diff --git a/src/paimon/common/reader/late_materializing_file_batch_reader.h
b/src/paimon/common/reader/late_materializing_file_batch_reader.h
new file mode 100644
index 00000000..231625db
--- /dev/null
+++ b/src/paimon/common/reader/late_materializing_file_batch_reader.h
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+#include <arrow/array/array_nested.h>
+#include <arrow/c/abi.h>
+
+#include <cstdint>
+#include <memory>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "fmt/format.h"
+#include "paimon/reader/prefetch_file_batch_reader.h"
+
+namespace paimon {
+
+class PredicateFilter;
+
+// For convenience, we abbreviate `Later Materializing` as `LatMat`.
+// This reader is installed below the prefetch layer (see
+// AbstractSplitRead::CreateFileBatchReader) and performs probe/payload
two-phase reads when a
+// predicate is pushed down through SetReadSchema; without a predicate it is a
plain passthrough.
+class LateMaterializingFileBatchReader : public PrefetchFileBatchReader {
+ public:
+ static Result<std::unique_ptr<LateMaterializingFileBatchReader>> Create(
+ std::unique_ptr<FileBatchReader> inner, std::shared_ptr<MemoryPool>
pool);
+
+ Result<FileBatchReader::ReadBatch> NextBatch() override;
+
+ std::shared_ptr<Metrics> GetReaderMetrics() const override {
+ return inner_->GetReaderMetrics();
+ };
+
+ void Close() override {
+ Reset();
+ inner_->Close();
+ }
+
+ Result<std::unique_ptr<::ArrowSchema>> GetFileSchema() const override {
+ return inner_->GetFileSchema();
+ }
+
+ Status SetReadSchema(::ArrowSchema* read_schema, const
std::shared_ptr<Predicate>& predicate,
+ const std::optional<RoaringBitmap32>&
selection_bitmap) override;
+
+ Result<uint64_t> GetPreviousBatchFileRowId(uint64_t batch_row_id) const
override;
+
+ Result<uint64_t> GetNumberOfRows() const override {
+ return inner_->GetNumberOfRows();
+ }
+
+ bool SupportPreciseBitmapSelection() const override {
+ // When probe_schema_ or payload_schema_ is null, lat-mat does not
take effect.
+ // Here we simply pass through the inner reader's support.
+ return inner_->SupportPreciseBitmapSelection();
+ }
+
+ Status SeekToRow(uint64_t row_number) override;
+
+ Result<uint64_t> GetNextRowToRead() const override {
+ PAIMON_ASSIGN_OR_RAISE(PrefetchFileBatchReader * prefetch_reader,
+ GetPrefetchReaderOrRaise("GetNextRowToRead"));
+ return prefetch_reader->GetNextRowToRead();
+ }
+
+ Result<std::vector<std::pair<uint64_t, uint64_t>>> GenReadRanges(
+ bool* need_prefetch) const override {
+ PAIMON_ASSIGN_OR_RAISE(PrefetchFileBatchReader * prefetch_reader,
+ GetPrefetchReaderOrRaise("GenReadRanges"));
+ return prefetch_reader->GenReadRanges(need_prefetch);
+ }
+
+ Status SetReadRanges(const std::vector<std::pair<uint64_t, uint64_t>>&
read_ranges) override;
+
+ Result<std::vector<std::pair<uint64_t, uint64_t>>> PreBufferRange()
override {
+ // TODO(zhouhongfeng.zhf): PrebufferRange (called by
PrefetchFileBatchReader) only read the
+ // probe data, consider read the payload data as well.
+ if (prefetch_inner_ == nullptr) {
+ return std::vector<std::pair<uint64_t, uint64_t>>{};
+ }
+ return prefetch_inner_->PreBufferRange();
+ }
+
+ private:
+ LateMaterializingFileBatchReader(std::unique_ptr<FileBatchReader> inner,
+ PrefetchFileBatchReader* prefetch_inner,
+ std::shared_ptr<arrow::MemoryPool>
arrow_pool)
+ : inner_(std::move(inner)),
+ prefetch_inner_(prefetch_inner),
+ arrow_pool_(std::move(arrow_pool)) {}
+
+ /// Reset the state of the late materializing reader, does not close inner
reader.
+ void Reset();
+
+ enum LatMatState {
+ kInit,
+ kProbing, // schema is set, probing is in progress
+ kNoLatMat, // no need to late materialization
+ kRunning, // Lat-mat is enabled and the payload reader is reading
data
+ kEOF
+ };
+
+ /// Read the probe projection once (whole file) and evaluating the
predicate batch by batch.
+ /// This function updates matched_bitmap_ and probe_data_.
+ /// TODO(zhouhongfeng.zhf): Read the probe data batch by batch to save
memory.
+ Status ReadAndFilterProbeData();
+
+ Result<RoaringBitmap32> FilterProbeBatch(const
std::shared_ptr<arrow::Array>& array,
+ const
std::shared_ptr<PredicateFilter>& bound_filter);
+
+ /// Read one payload batch with bitmap (matched rows only)
+ Result<FileBatchReader::ReadBatch> ReadPayloadBatch();
+
+ /// Combine the compacted payload columns and the selected probe columns
into a single struct
+ /// array following full_schema_'s field order.
+ Result<FileBatchReader::ReadBatch> AssembleFullBatch(
+ const std::shared_ptr<arrow::Array>& payload_array,
+ const std::shared_ptr<arrow::Array>& probe_array);
+
+ Status SetInnerReadSchema(const std::shared_ptr<arrow::Schema>&
read_schema,
+ const std::shared_ptr<Predicate>& predicate,
+ const std::optional<RoaringBitmap32>& selection);
+
+ /// Returns the inner reader's prefetch interface, or an error when the
format reader does not
+ /// implement it (avro and blob do not).
+ Result<PrefetchFileBatchReader*> GetPrefetchReaderOrRaise(
+ std::string_view function_name) const {
+ if (prefetch_inner_ == nullptr) {
+ return Status::NotImplemented(
+ fmt::format("format reader is not a prefetch reader, function
{} not supported",
+ function_name));
+ }
+ return prefetch_inner_;
+ }
+
+ /// The probe/payload logic needs nothing beyond FileBatchReader, so the
inner reader is held as
+ /// the base type: parquet and orc readers implement
PrefetchFileBatchReader, while avro and
+ /// blob readers only implement FileBatchReader. The prefetch-only methods
are rejected for the
+ /// latter; AbstractSplitRead::CreateFileBatchReader keeps those formats
out of the prefetch
+ /// layer so nothing calls them.
+ std::unique_ptr<FileBatchReader> inner_;
+ /// Non-owning view of inner_ when it implements the prefetch interface,
nullptr otherwise.
+ /// inner_ is never reassigned, so the cast is resolved once in Create().
+ PrefetchFileBatchReader* prefetch_inner_ = nullptr;
+ std::shared_ptr<arrow::MemoryPool> arrow_pool_;
+ LatMatState state_ = kInit;
+ std::shared_ptr<arrow::Schema> full_schema_;
+ // projection holding only the predicate fields; nullptr when probing is
not applicable
+ std::shared_ptr<arrow::Schema> probe_schema_;
+ // projection holding the payload (non-probe) fields; nullptr when probing
is not applicable
+ std::shared_ptr<arrow::Schema> payload_schema_;
+ std::shared_ptr<Predicate> predicate_;
+ // predicate bound to probe_schema_'s field indices; null when probing is
not applicable
+ std::shared_ptr<PredicateFilter> probe_filter_;
+ std::optional<RoaringBitmap32> selection_;
+ // the probe_data_ is sliced and compacted with the matched_bitmap_
+ std::shared_ptr<arrow::StructArray> probe_data_;
+ RoaringBitmap32 matched_bitmap_;
+ // read cursor into probe_data_ for the payload phase
+ int64_t probe_cursor_ = 0;
+ // to support GetPreviousBatchFileRowId
+ std::vector<uint64_t> row_mapping_;
+};
+
+} // namespace paimon
diff --git
a/src/paimon/common/reader/late_materializing_file_batch_reader_test.cpp
b/src/paimon/common/reader/late_materializing_file_batch_reader_test.cpp
new file mode 100644
index 00000000..ff571a28
--- /dev/null
+++ b/src/paimon/common/reader/late_materializing_file_batch_reader_test.cpp
@@ -0,0 +1,670 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "paimon/common/reader/late_materializing_file_batch_reader.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <limits>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "arrow/api.h"
+#include "arrow/array/builder_nested.h"
+#include "arrow/c/bridge.h"
+#include "gtest/gtest.h"
+#include "paimon/common/reader/late_materializing_reader_builder.h"
+#include "paimon/common/reader/prefetch_file_batch_reader_impl.h"
+#include "paimon/common/reader/reader_utils.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/checked_cast.h"
+#include "paimon/common/utils/read_ahead_cache.h"
+#include "paimon/executor.h"
+#include "paimon/format/reader_builder.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/predicate/literal.h"
+#include "paimon/predicate/predicate.h"
+#include "paimon/predicate/predicate_builder.h"
+#include "paimon/reader/prefetch_file_batch_reader.h"
+#include "paimon/status.h"
+#include "paimon/testing/mock/mock_file_batch_reader.h"
+#include "paimon/testing/mock/mock_file_system.h"
+#include "paimon/testing/mock/mock_format_reader_builder.h"
+#include "paimon/testing/utils/read_result_collector.h"
+#include "paimon/testing/utils/testharness.h"
+#include "paimon/utils/roaring_bitmap32.h"
+
+namespace paimon::test {
+
+class LateMaterializingFileBatchReaderTest : public ::testing::Test {
+ public:
+ void SetUp() override {
+ k_field_ = arrow::field("k", arrow::int64());
+ v_field_ = arrow::field("v", arrow::utf8());
+ full_fields_ = {k_field_, v_field_};
+ full_type_ = arrow::struct_(full_fields_);
+ }
+
+ // Build a struct array with column k (int64, values = ks) and column v
(utf8, "v_<index>").
+ std::shared_ptr<arrow::Array> BuildData(const std::vector<int64_t>& ks) {
+ arrow::StructBuilder builder(
+ full_type_, arrow::default_memory_pool(),
+ {std::make_shared<arrow::Int64Builder>(),
std::make_shared<arrow::StringBuilder>()});
+ auto* k_builder =
checked_cast<arrow::Int64Builder*>(builder.field_builder(0));
+ auto* v_builder =
checked_cast<arrow::StringBuilder*>(builder.field_builder(1));
+ for (size_t i = 0; i < ks.size(); ++i) {
+ EXPECT_TRUE(builder.Append().ok());
+ EXPECT_TRUE(k_builder->Append(ks[i]).ok());
+ EXPECT_TRUE(v_builder->Append("v_" + std::to_string(i)).ok());
+ }
+ std::shared_ptr<arrow::Array> array;
+ EXPECT_TRUE(builder.Finish(&array).ok());
+ return array;
+ }
+
+ struct Row {
+ int64_t k;
+ std::string v;
+ uint64_t file_row;
+ };
+
+ // Drive the reader through NextBatchWithBitmap to EOF, decoding the
full-schema output rows.
+ Result<std::vector<Row>> Collect(LateMaterializingFileBatchReader* reader)
{
+ std::vector<Row> rows;
+ while (true) {
+ PAIMON_ASSIGN_OR_RAISE(BatchReader::ReadBatchWithBitmap
batch_with_bitmap,
+ reader->NextBatchWithBitmap());
+ if (BatchReader::IsEofBatch(batch_with_bitmap)) {
+ break;
+ }
+ auto& [batch, bitmap] = batch_with_bitmap;
+ auto& [c_array, c_schema] = batch;
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array>
array,
+
arrow::ImportArray(c_array.get(), c_schema.get()));
+ auto struct_array =
arrow::internal::checked_pointer_cast<arrow::StructArray>(array);
+ EXPECT_EQ(bitmap.Cardinality(),
static_cast<int32_t>(struct_array->length()));
+ auto k_array =
arrow::internal::checked_pointer_cast<arrow::Int64Array>(
+ struct_array->GetFieldByName("k"));
+ if (!k_array) {
+ return Status::Invalid("output batch missing k column");
+ }
+ // v is only present when it belongs to the read schema (payload
projection).
+ auto v_array =
arrow::internal::checked_pointer_cast<arrow::StringArray>(
+ struct_array->GetFieldByName("v"));
+ for (int64_t i = 0; i < struct_array->length(); ++i) {
+ PAIMON_ASSIGN_OR_RAISE(uint64_t file_row,
+
reader->GetPreviousBatchFileRowId(static_cast<uint64_t>(i)));
+ rows.push_back(Row{k_array->Value(i),
+ v_array ? v_array->GetString(i) :
std::string(), file_row});
+ }
+ }
+ return rows;
+ }
+
+ Status SetReadSchema(LateMaterializingFileBatchReader* reader,
+ const std::shared_ptr<arrow::Schema>& schema,
+ const std::shared_ptr<Predicate>& predicate,
+ const std::optional<RoaringBitmap32>& selection) {
+ ::ArrowSchema c_schema;
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*schema,
&c_schema));
+ return reader->SetReadSchema(&c_schema, predicate, selection);
+ }
+
+ // Collect all output rows as a single concatenated struct array (for
schema/nested checks),
+ // reusing the shared collector so the batch-offset and bitmap contracts
are checked too.
+ Result<std::shared_ptr<arrow::StructArray>> CollectStruct(FileBatchReader*
reader) {
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::ChunkedArray> chunked,
+ ReadResultCollector::CollectResult(reader));
+ if (chunked == nullptr) {
+ return std::shared_ptr<arrow::StructArray>();
+ }
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array>
combined,
+
arrow::Concatenate(chunked->chunks()));
+ return
arrow::internal::checked_pointer_cast<arrow::StructArray>(combined);
+ }
+
+ // Build a struct with 5 columns [a:int64, b:utf8, c:int64, d:utf8,
e:int64], each carrying a
+ // distinct value pattern so any column reordering is detected.
+ std::shared_ptr<arrow::Array> BuildMultiFieldData(int32_t n) {
+ auto type =
+ arrow::struct_({arrow::field("a", arrow::int64()),
arrow::field("b", arrow::utf8()),
+ arrow::field("c", arrow::int64()),
arrow::field("d", arrow::utf8()),
+ arrow::field("e", arrow::int64())});
+ arrow::StructBuilder builder(
+ type, arrow::default_memory_pool(),
+ {std::make_shared<arrow::Int64Builder>(),
std::make_shared<arrow::StringBuilder>(),
+ std::make_shared<arrow::Int64Builder>(),
std::make_shared<arrow::StringBuilder>(),
+ std::make_shared<arrow::Int64Builder>()});
+ auto* a = checked_cast<arrow::Int64Builder*>(builder.field_builder(0));
+ auto* b =
checked_cast<arrow::StringBuilder*>(builder.field_builder(1));
+ auto* c = checked_cast<arrow::Int64Builder*>(builder.field_builder(2));
+ auto* d =
checked_cast<arrow::StringBuilder*>(builder.field_builder(3));
+ auto* e = checked_cast<arrow::Int64Builder*>(builder.field_builder(4));
+ for (int32_t i = 0; i < n; ++i) {
+ EXPECT_TRUE(builder.Append().ok());
+ EXPECT_TRUE(a->Append(i).ok());
+ EXPECT_TRUE(b->Append("b_" + std::to_string(i)).ok());
+ EXPECT_TRUE(c->Append(static_cast<int64_t>(i) * 100).ok());
+ EXPECT_TRUE(d->Append("d_" + std::to_string(i)).ok());
+ EXPECT_TRUE(e->Append(static_cast<int64_t>(i) * 10000).ok());
+ }
+ std::shared_ptr<arrow::Array> array;
+ EXPECT_TRUE(builder.Finish(&array).ok());
+ return array;
+ }
+
+ // Build a struct with a nested payload column [k:int64, arr:list<int64>,
tag:utf8].
+ std::shared_ptr<arrow::Array> BuildNestedData(int32_t n) {
+ auto type = arrow::struct_({arrow::field("k", arrow::int64()),
+ arrow::field("arr",
arrow::list(arrow::int64())),
+ arrow::field("tag", arrow::utf8())});
+ auto arr_value_builder = std::make_shared<arrow::Int64Builder>();
+ arrow::StructBuilder builder(
+ type, arrow::default_memory_pool(),
+ {std::make_shared<arrow::Int64Builder>(),
+
std::make_shared<arrow::ListBuilder>(arrow::default_memory_pool(),
arr_value_builder),
+ std::make_shared<arrow::StringBuilder>()});
+ auto* k = checked_cast<arrow::Int64Builder*>(builder.field_builder(0));
+ auto* arr =
checked_cast<arrow::ListBuilder*>(builder.field_builder(1));
+ auto* arr_values =
checked_cast<arrow::Int64Builder*>(arr->value_builder());
+ auto* tag =
checked_cast<arrow::StringBuilder*>(builder.field_builder(2));
+ for (int32_t i = 0; i < n; ++i) {
+ EXPECT_TRUE(builder.Append().ok());
+ EXPECT_TRUE(k->Append(i).ok());
+ EXPECT_TRUE(arr->Append().ok());
+ EXPECT_TRUE(arr_values->Append(i).ok());
+ EXPECT_TRUE(arr_values->Append(i + 1).ok());
+ EXPECT_TRUE(tag->Append("t_" + std::to_string(i)).ok());
+ }
+ std::shared_ptr<arrow::Array> array;
+ EXPECT_TRUE(builder.Finish(&array).ok());
+ return array;
+ }
+
+ protected:
+ std::shared_ptr<arrow::Field> k_field_;
+ std::shared_ptr<arrow::Field> v_field_;
+ arrow::FieldVector full_fields_;
+ std::shared_ptr<arrow::DataType> full_type_;
+};
+
+// No predicate: the reader must pass through the inner reader unchanged (all
rows, all columns).
+TEST_F(LateMaterializingFileBatchReaderTest, PassThroughWhenNoPredicate) {
+ auto data = BuildData({0, 1, 2, 3, 4});
+ auto mock = std::make_unique<MockFileBatchReader>(data, full_type_,
/*batch_size=*/2);
+ ASSERT_OK_AND_ASSIGN(
+ auto reader, LateMaterializingFileBatchReader::Create(std::move(mock),
GetDefaultPool()));
+ ASSERT_OK(SetReadSchema(reader.get(), arrow::schema(full_fields_),
/*predicate=*/nullptr,
+ std::nullopt));
+
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> rows, Collect(reader.get()));
+ ASSERT_EQ(rows.size(), 5u);
+ for (int64_t i = 0; i < 5; ++i) {
+ EXPECT_EQ(rows[i].k, i);
+ EXPECT_EQ(rows[i].v, "v_" + std::to_string(i));
+ EXPECT_EQ(rows[i].file_row, static_cast<uint64_t>(i));
+ }
+}
+
+// The predicate references every projected column, so the payload set is
empty: no late
+// materialization, plain pass-through.
+TEST_F(LateMaterializingFileBatchReaderTest, PassThroughWhenPayloadEmpty) {
+ auto data = BuildData({0, 1, 2, 3, 4});
+ auto mock = std::make_unique<MockFileBatchReader>(data, full_type_,
/*batch_size=*/2);
+ ASSERT_OK_AND_ASSIGN(
+ auto reader, LateMaterializingFileBatchReader::Create(std::move(mock),
GetDefaultPool()));
+ // read schema is just {k}; the predicate on k covers all columns ->
payload empty
+ auto predicate = PredicateBuilder::GreaterOrEqual(/*field_index=*/0,
/*field_name=*/"k",
+ FieldType::BIGINT,
Literal(0l));
+ ASSERT_OK(SetReadSchema(reader.get(), arrow::schema({k_field_}),
predicate, std::nullopt));
+
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> rows, Collect(reader.get()));
+ ASSERT_EQ(rows.size(), 5u);
+ for (size_t idx = 0; idx < rows.size(); ++idx) {
+ EXPECT_EQ(rows[idx].k, static_cast<int64_t>(idx));
+ // v is outside the read schema, so the pass-through output must not
carry it
+ EXPECT_EQ(rows[idx].v, "");
+ EXPECT_EQ(rows[idx].file_row, static_cast<uint64_t>(idx));
+ }
+}
+
+// Contiguous matched subset spanning multiple batches.
+TEST_F(LateMaterializingFileBatchReaderTest, ContiguousSubsetAcrossBatches) {
+ auto data = BuildData({0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
+ auto mock = std::make_unique<MockFileBatchReader>(data, full_type_,
/*batch_size=*/3);
+ ASSERT_OK_AND_ASSIGN(
+ auto reader, LateMaterializingFileBatchReader::Create(std::move(mock),
GetDefaultPool()));
+ auto predicate = PredicateBuilder::GreaterThan(/*field_index=*/0,
/*field_name=*/"k",
+ FieldType::BIGINT,
Literal(4l));
+ ASSERT_OK(SetReadSchema(reader.get(), arrow::schema(full_fields_),
predicate, std::nullopt));
+
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> rows, Collect(reader.get()));
+ ASSERT_EQ(rows.size(), 5u); // k = 5..9
+ for (size_t idx = 0; idx < rows.size(); ++idx) {
+ int64_t expected = 5 + static_cast<int64_t>(idx);
+ EXPECT_EQ(rows[idx].k, expected);
+ EXPECT_EQ(rows[idx].v, "v_" + std::to_string(expected));
+ EXPECT_EQ(rows[idx].file_row, static_cast<uint64_t>(expected));
+ }
+}
+
+// Scattered (alternating) matched rows: predicate matches every other row.
+TEST_F(LateMaterializingFileBatchReaderTest, ScatteredAlternatingMatch) {
+ // k = 0,1,0,1,... ; predicate k == 1 matches all odd file rows.
+ std::vector<int64_t> ks;
+ for (int i = 0; i < 12; ++i) {
+ ks.push_back(i % 2);
+ }
+ auto data = BuildData(ks);
+ auto mock = std::make_unique<MockFileBatchReader>(data, full_type_,
/*batch_size=*/3);
+ ASSERT_OK_AND_ASSIGN(
+ auto reader, LateMaterializingFileBatchReader::Create(std::move(mock),
GetDefaultPool()));
+ auto predicate = PredicateBuilder::Equal(/*field_index=*/0,
/*field_name=*/"k",
+ FieldType::BIGINT, Literal(1l));
+ ASSERT_OK(SetReadSchema(reader.get(), arrow::schema(full_fields_),
predicate, std::nullopt));
+
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> rows, Collect(reader.get()));
+ ASSERT_EQ(rows.size(), 6u); // odd rows 1,3,5,7,9,11
+ for (size_t idx = 0; idx < rows.size(); ++idx) {
+ uint64_t expected_row = 2 * idx + 1;
+ EXPECT_EQ(rows[idx].k, 1);
+ EXPECT_EQ(rows[idx].v, "v_" + std::to_string(expected_row));
+ EXPECT_EQ(rows[idx].file_row, expected_row);
+ }
+}
+
+// The selection bitmap further restricts the matched rows: matched must be a
subset of selection.
+TEST_F(LateMaterializingFileBatchReaderTest, MatchedIntersectsSelection) {
+ std::vector<int64_t> ks;
+ for (int i = 0; i < 12; ++i) {
+ ks.push_back(i % 2);
+ }
+ auto data = BuildData(ks);
+ auto mock = std::make_unique<MockFileBatchReader>(data, full_type_,
/*batch_size=*/4);
+ ASSERT_OK_AND_ASSIGN(
+ auto reader, LateMaterializingFileBatchReader::Create(std::move(mock),
GetDefaultPool()));
+ auto predicate = PredicateBuilder::Equal(/*field_index=*/0,
/*field_name=*/"k",
+ FieldType::BIGINT, Literal(1l));
+ // predicate hits {1,3,5,7,9,11}; selection keeps only {1,5,9}
+ RoaringBitmap32 selection;
+ selection.Add(1);
+ selection.Add(5);
+ selection.Add(9);
+ ASSERT_OK(SetReadSchema(reader.get(), arrow::schema(full_fields_),
predicate,
+ std::optional<RoaringBitmap32>(selection)));
+
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> rows, Collect(reader.get()));
+ ASSERT_EQ(rows.size(), 3u);
+ const std::vector<uint64_t> expected_rows = {1u, 5u, 9u};
+ for (size_t idx = 0; idx < rows.size(); ++idx) {
+ EXPECT_EQ(rows[idx].k, 1);
+ EXPECT_EQ(rows[idx].v, "v_" + std::to_string(expected_rows[idx]));
+ EXPECT_EQ(rows[idx].file_row, expected_rows[idx]);
+ }
+}
+
+// No matched rows: the reader returns EOF immediately.
+TEST_F(LateMaterializingFileBatchReaderTest, EmptyMatchReturnsEof) {
+ auto data = BuildData({0, 1, 2, 3, 4});
+ auto mock = std::make_unique<MockFileBatchReader>(data, full_type_,
/*batch_size=*/2);
+ ASSERT_OK_AND_ASSIGN(
+ auto reader, LateMaterializingFileBatchReader::Create(std::move(mock),
GetDefaultPool()));
+ auto predicate = PredicateBuilder::GreaterThan(/*field_index=*/0,
/*field_name=*/"k",
+ FieldType::BIGINT,
Literal(100l));
+ ASSERT_OK(SetReadSchema(reader.get(), arrow::schema(full_fields_),
predicate, std::nullopt));
+
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> rows, Collect(reader.get()));
+ ASSERT_TRUE(rows.empty());
+}
+
+// SeekToRow during payload emission must re-align the probe cursor so
probe/payload stay matched.
+TEST_F(LateMaterializingFileBatchReaderTest, SeekToRowRealignsProbeCursor) {
+ auto data = BuildData({0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
+ auto mock = std::make_unique<MockFileBatchReader>(data, full_type_,
/*batch_size=*/4);
+ ASSERT_OK_AND_ASSIGN(
+ auto reader, LateMaterializingFileBatchReader::Create(std::move(mock),
GetDefaultPool()));
+ auto predicate = PredicateBuilder::GreaterOrEqual(/*field_index=*/0,
/*field_name=*/"k",
+ FieldType::BIGINT,
Literal(5l));
+ ASSERT_OK(SetReadSchema(reader.get(), arrow::schema(full_fields_),
predicate, std::nullopt));
+
+ // First payload batch triggers the probe scan; matched rows are 5..9.
+ ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatchWithBitmap first,
reader->NextBatchWithBitmap());
+ ASSERT_FALSE(BatchReader::IsEofBatch(first));
+ ReaderUtils::ReleaseReadBatch(std::move(first.first));
+
+ // Seek forward to file row 8: subsequent output must be exactly rows 8
and 9, correctly paired.
+ ASSERT_OK(reader->SeekToRow(8));
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> rows, Collect(reader.get()));
+ ASSERT_EQ(rows.size(), 2u);
+ EXPECT_EQ(rows[0].k, 8);
+ EXPECT_EQ(rows[0].v, "v_8");
+ EXPECT_EQ(rows[0].file_row, 8u);
+ EXPECT_EQ(rows[1].k, 9);
+ EXPECT_EQ(rows[1].v, "v_9");
+ EXPECT_EQ(rows[1].file_row, 9u);
+}
+
+// SetReadRanges must be cached and re-forwarded to the inner reader across
the probe/payload
+// schema switches (SetReadSchema resets the inner reader's ranges).
+TEST_F(LateMaterializingFileBatchReaderTest, ReadRangesForwardedAcrossPhases) {
+ auto data = BuildData({0, 1, 2, 3, 4, 5, 6, 7});
+ auto mock = std::make_unique<MockFileBatchReader>(data, full_type_,
/*batch_size=*/4);
+ auto* mock_ptr = mock.get();
+ ASSERT_OK_AND_ASSIGN(
+ auto reader, LateMaterializingFileBatchReader::Create(std::move(mock),
GetDefaultPool()));
+ auto predicate = PredicateBuilder::GreaterOrEqual(/*field_index=*/0,
/*field_name=*/"k",
+ FieldType::BIGINT,
Literal(2l));
+ ASSERT_OK(SetReadSchema(reader.get(), arrow::schema(full_fields_),
predicate, std::nullopt));
+
+ std::vector<std::pair<uint64_t, uint64_t>> ranges = {{0, 8}};
+ ASSERT_OK(reader->SetReadRanges(ranges));
+
+ // Drive to EOF; this performs the probe pass and the payload schema
switch.
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> rows, Collect(reader.get()));
+ ASSERT_EQ(rows.size(), 6u); // k = 2..7
+ for (size_t idx = 0; idx < rows.size(); ++idx) {
+ int64_t expected = 2 + static_cast<int64_t>(idx);
+ EXPECT_EQ(rows[idx].k, expected);
+ EXPECT_EQ(rows[idx].v, "v_" + std::to_string(expected));
+ EXPECT_EQ(rows[idx].file_row, static_cast<uint64_t>(expected));
+ }
+
+ // The inner reader must have received the cached ranges again after the
payload switch.
+ // SetReadSchema (invoked on the payload switch) clears the inner reader's
ranges, so the
+ // cached ranges still being present at the end proves LM re-forwarded
them after the switch.
+ ASSERT_EQ(mock_ptr->GetReadRanges(), ranges);
+}
+
+// SetReadSchema is re-entrant: a second call with a different predicate
resets probe state.
+TEST_F(LateMaterializingFileBatchReaderTest, ReentrantSetReadSchema) {
+ auto data = BuildData({0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
+ auto mock = std::make_unique<MockFileBatchReader>(data, full_type_,
/*batch_size=*/3);
+ ASSERT_OK_AND_ASSIGN(
+ auto reader, LateMaterializingFileBatchReader::Create(std::move(mock),
GetDefaultPool()));
+
+ auto predicate1 = PredicateBuilder::GreaterThan(/*field_index=*/0,
/*field_name=*/"k",
+ FieldType::BIGINT,
Literal(7l));
+ ASSERT_OK(SetReadSchema(reader.get(), arrow::schema(full_fields_),
predicate1, std::nullopt));
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> rows1, Collect(reader.get()));
+ ASSERT_EQ(rows1.size(), 2u); // k = 8,9
+ for (size_t idx = 0; idx < rows1.size(); ++idx) {
+ int64_t expected = 8 + static_cast<int64_t>(idx);
+ EXPECT_EQ(rows1[idx].k, expected);
+ EXPECT_EQ(rows1[idx].v, "v_" + std::to_string(expected));
+ EXPECT_EQ(rows1[idx].file_row, static_cast<uint64_t>(expected));
+ }
+
+ auto predicate2 = PredicateBuilder::LessThan(/*field_index=*/0,
/*field_name=*/"k",
+ FieldType::BIGINT,
Literal(3l));
+ ASSERT_OK(SetReadSchema(reader.get(), arrow::schema(full_fields_),
predicate2, std::nullopt));
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> rows2, Collect(reader.get()));
+ ASSERT_EQ(rows2.size(), 3u); // k = 0,1,2
+ for (size_t idx = 0; idx < rows2.size(); ++idx) {
+ EXPECT_EQ(rows2[idx].k, static_cast<int64_t>(idx));
+ EXPECT_EQ(rows2[idx].v, "v_" + std::to_string(idx));
+ EXPECT_EQ(rows2[idx].file_row, static_cast<uint64_t>(idx));
+ }
+}
+
+// Forwarded metadata accessors should reflect the inner reader.
+TEST_F(LateMaterializingFileBatchReaderTest, ForwardsRowCountAndFileSchema) {
+ auto data = BuildData({0, 1, 2, 3});
+ auto mock = std::make_unique<MockFileBatchReader>(data, full_type_,
/*batch_size=*/2);
+ ASSERT_OK_AND_ASSIGN(
+ auto reader, LateMaterializingFileBatchReader::Create(std::move(mock),
GetDefaultPool()));
+
+ ASSERT_OK_AND_ASSIGN(uint64_t num_rows, reader->GetNumberOfRows());
+ EXPECT_EQ(num_rows, 4u);
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<::ArrowSchema> c_file_schema,
reader->GetFileSchema());
+ auto import_result = arrow::ImportType(c_file_schema.get());
+ ASSERT_TRUE(import_result.ok());
+ EXPECT_TRUE(import_result.ValueOrDie()->Equals(full_type_));
+}
+
+// With many columns and a predicate over two non-adjacent probe columns, the
output must keep the
+// full read-schema field order (and each probe/payload column's values must
not be scrambled).
+TEST_F(LateMaterializingFileBatchReaderTest, MultiFieldPreservesColumnOrder) {
+ auto data = BuildMultiFieldData(10);
+ auto type = data->type();
+ auto mock = std::make_unique<MockFileBatchReader>(data, type,
/*batch_size=*/3);
+ ASSERT_OK_AND_ASSIGN(
+ auto reader, LateMaterializingFileBatchReader::Create(std::move(mock),
GetDefaultPool()));
+ // probe columns = {a (idx0), c (idx2)}; payload columns = {b, d, e}
+ auto pred_a =
+ PredicateBuilder::GreaterOrEqual(/*field_index=*/0, "a",
FieldType::BIGINT, Literal(3l));
+ auto pred_c =
+ PredicateBuilder::LessThan(/*field_index=*/2, "c", FieldType::BIGINT,
Literal(700l));
+ ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({pred_a,
pred_c}));
+ ASSERT_OK(SetReadSchema(reader.get(), arrow::schema(type->fields()),
predicate, std::nullopt));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::StructArray> result,
CollectStruct(reader.get()));
+ ASSERT_TRUE(result);
+ // a >= 3 and c(=i*100) < 700 -> i in {3,4,5,6}
+ ASSERT_EQ(result->length(), 4);
+ // output field order must equal the requested full schema order
+ ASSERT_EQ(result->num_fields(), 5);
+ auto out_type =
arrow::internal::checked_pointer_cast<arrow::StructType>(result->type());
+ EXPECT_EQ(out_type->field(0)->name(), "a");
+ EXPECT_EQ(out_type->field(1)->name(), "b");
+ EXPECT_EQ(out_type->field(2)->name(), "c");
+ EXPECT_EQ(out_type->field(3)->name(), "d");
+ EXPECT_EQ(out_type->field(4)->name(), "e");
+ auto a =
arrow::internal::checked_pointer_cast<arrow::Int64Array>(result->GetFieldByName("a"));
+ auto b =
arrow::internal::checked_pointer_cast<arrow::StringArray>(result->GetFieldByName("b"));
+ auto c =
arrow::internal::checked_pointer_cast<arrow::Int64Array>(result->GetFieldByName("c"));
+ auto d =
arrow::internal::checked_pointer_cast<arrow::StringArray>(result->GetFieldByName("d"));
+ auto e =
arrow::internal::checked_pointer_cast<arrow::Int64Array>(result->GetFieldByName("e"));
+ const std::vector<int64_t> expected = {3, 4, 5, 6};
+ for (size_t j = 0; j < expected.size(); ++j) {
+ int64_t i = expected[j];
+ EXPECT_EQ(a->Value(j), i);
+ EXPECT_EQ(b->GetString(j), "b_" + std::to_string(i));
+ EXPECT_EQ(c->Value(j), i * 100);
+ EXPECT_EQ(d->GetString(j), "d_" + std::to_string(i));
+ EXPECT_EQ(e->Value(j), i * 10000);
+ }
+}
+
+// A nested (list) payload column must round-trip unchanged for the matched
rows.
+TEST_F(LateMaterializingFileBatchReaderTest, NestedPayloadColumn) {
+ auto data = BuildNestedData(8);
+ auto type = data->type();
+ auto mock = std::make_unique<MockFileBatchReader>(data, type,
/*batch_size=*/3);
+ ASSERT_OK_AND_ASSIGN(
+ auto reader, LateMaterializingFileBatchReader::Create(std::move(mock),
GetDefaultPool()));
+ // probe = {k}; payload = {arr (list<int64>), tag}
+ auto predicate =
+ PredicateBuilder::GreaterOrEqual(/*field_index=*/0, "k",
FieldType::BIGINT, Literal(5l));
+ ASSERT_OK(SetReadSchema(reader.get(), arrow::schema(type->fields()),
predicate, std::nullopt));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::StructArray> result,
CollectStruct(reader.get()));
+ ASSERT_TRUE(result);
+ ASSERT_EQ(result->length(), 3); // k = 5,6,7
+ auto k =
arrow::internal::checked_pointer_cast<arrow::Int64Array>(result->GetFieldByName("k"));
+ auto arr =
+
arrow::internal::checked_pointer_cast<arrow::ListArray>(result->GetFieldByName("arr"));
+ auto tag =
+
arrow::internal::checked_pointer_cast<arrow::StringArray>(result->GetFieldByName("tag"));
+ ASSERT_TRUE(k && arr && tag);
+ for (int64_t j = 0; j < result->length(); ++j) {
+ int64_t i = 5 + j;
+ EXPECT_EQ(k->Value(j), i);
+ EXPECT_EQ(tag->GetString(j), "t_" + std::to_string(i));
+ auto sub =
arrow::internal::checked_pointer_cast<arrow::Int64Array>(arr->value_slice(j));
+ ASSERT_EQ(sub->length(), 2);
+ EXPECT_EQ(sub->Value(0), i);
+ EXPECT_EQ(sub->Value(1), i + 1);
+ }
+}
+
+// The late-materialization reader must work correctly as an inner reader
driven by
+// PrefetchFileBatchReaderImpl (schema broadcast, range dispatch, seek, row-id
tracking).
+TEST_F(LateMaterializingFileBatchReaderTest, WorksAsInnerOfPrefetchReader) {
+ auto data = BuildData({0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
+ LateMaterializingReaderBuilder builder(
+ std::make_unique<MockFormatReaderBuilder>(data, full_type_,
/*batch_size=*/3),
+ GetDefaultPool());
+ auto mock_fs = std::make_shared<MockFileSystem>();
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Executor> executor,
CreateDefaultExecutor(2));
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<PrefetchFileBatchReaderImpl> impl,
+ PrefetchFileBatchReaderImpl::Create(
+ /*data_file_path=*/"", /*data_file_size=*/0, &builder, mock_fs,
+ /*prefetch_max_parallel_num=*/1, /*batch_size=*/3,
/*prefetch_batch_count=*/2,
+ /*enable_adaptive_prefetch_strategy=*/false, executor,
+ /*initialize_read_ranges=*/false,
/*read_ahead_cache_enabled=*/false, CacheConfig(),
+ GetDefaultPool()));
+ auto predicate =
+ PredicateBuilder::GreaterOrEqual(/*field_index=*/0, "k",
FieldType::BIGINT, Literal(4l));
+ ::ArrowSchema c_schema;
+ ASSERT_TRUE(arrow::ExportSchema(*arrow::schema(full_fields_),
&c_schema).ok());
+ ASSERT_OK(impl->SetReadSchema(&c_schema, predicate, std::nullopt));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::StructArray> result,
CollectStruct(impl.get()));
+ ASSERT_TRUE(result);
+ ASSERT_EQ(result->length(), 6); // k = 4..9
+ auto k =
arrow::internal::checked_pointer_cast<arrow::Int64Array>(result->GetFieldByName("k"));
+ auto v =
arrow::internal::checked_pointer_cast<arrow::StringArray>(result->GetFieldByName("v"));
+ ASSERT_TRUE(k && v);
+ for (int64_t j = 0; j < result->length(); ++j) {
+ EXPECT_EQ(k->Value(j), 4 + j);
+ EXPECT_EQ(v->GetString(j), "v_" + std::to_string(4 + j));
+ }
+ impl->Close();
+}
+
+// Re-setting the read schema on the prefetch impl (which re-broadcasts to the
inner LM readers and
+// re-plans ranges) must reset the probe state and produce correct results for
the new predicate.
+TEST_F(LateMaterializingFileBatchReaderTest,
PrefetchInnerReentrantSetReadSchema) {
+ auto data = BuildData({0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
+ LateMaterializingReaderBuilder builder(
+ std::make_unique<MockFormatReaderBuilder>(data, full_type_,
/*batch_size=*/3),
+ GetDefaultPool());
+ auto mock_fs = std::make_shared<MockFileSystem>();
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Executor> executor,
CreateDefaultExecutor(2));
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<PrefetchFileBatchReaderImpl> impl,
+ PrefetchFileBatchReaderImpl::Create(
+ /*data_file_path=*/"", /*data_file_size=*/0, &builder, mock_fs,
+ /*prefetch_max_parallel_num=*/1, /*batch_size=*/3,
/*prefetch_batch_count=*/2,
+ /*enable_adaptive_prefetch_strategy=*/false, executor,
+ /*initialize_read_ranges=*/false,
/*read_ahead_cache_enabled=*/false, CacheConfig(),
+ GetDefaultPool()));
+
+ auto full_schema = arrow::schema(full_fields_);
+ auto predicate1 =
+ PredicateBuilder::GreaterThan(/*field_index=*/0, "k",
FieldType::BIGINT, Literal(6l));
+ ::ArrowSchema c_schema1;
+ ASSERT_TRUE(arrow::ExportSchema(*full_schema, &c_schema1).ok());
+ ASSERT_OK(impl->SetReadSchema(&c_schema1, predicate1, std::nullopt));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::StructArray> result1,
CollectStruct(impl.get()));
+ ASSERT_TRUE(result1);
+ ASSERT_EQ(result1->length(), 3); // k = 7,8,9
+ auto k1 =
+
arrow::internal::checked_pointer_cast<arrow::Int64Array>(result1->GetFieldByName("k"));
+ auto v1 =
+
arrow::internal::checked_pointer_cast<arrow::StringArray>(result1->GetFieldByName("v"));
+ ASSERT_TRUE(k1 && v1);
+ for (int64_t j = 0; j < result1->length(); ++j) {
+ EXPECT_EQ(k1->Value(j), 7 + j);
+ EXPECT_EQ(v1->GetString(j), "v_" + std::to_string(7 + j));
+ }
+
+ auto predicate2 =
+ PredicateBuilder::LessThan(/*field_index=*/0, "k", FieldType::BIGINT,
Literal(3l));
+ ::ArrowSchema c_schema2;
+ ASSERT_TRUE(arrow::ExportSchema(*full_schema, &c_schema2).ok());
+ ASSERT_OK(impl->SetReadSchema(&c_schema2, predicate2, std::nullopt));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::StructArray> result2,
CollectStruct(impl.get()));
+ ASSERT_TRUE(result2);
+ ASSERT_EQ(result2->length(), 3); // k = 0,1,2
+ auto k2 =
+
arrow::internal::checked_pointer_cast<arrow::Int64Array>(result2->GetFieldByName("k"));
+ auto v2 =
+
arrow::internal::checked_pointer_cast<arrow::StringArray>(result2->GetFieldByName("v"));
+ for (int64_t j = 0; j < result2->length(); ++j) {
+ EXPECT_EQ(k2->Value(j), j);
+ EXPECT_EQ(v2->GetString(j), "v_" + std::to_string(j));
+ }
+ impl->Close();
+}
+
+// With multiple parallel inner readers and per-batch ranges, the prefetch
impl dispatches disjoint
+// ranges to each LM reader and drives them via
EnsureReaderPosition/SeekToRow. The merged output
+// must still be exactly the matched rows in ascending file order.
+TEST_F(LateMaterializingFileBatchReaderTest,
PrefetchInnerParallelReadersWithSeek) {
+ std::vector<int64_t> ks;
+ for (int i = 0; i < 20; ++i) {
+ ks.push_back(i);
+ }
+ auto data = BuildData(ks);
+ // Per-batch ranges (the mock's default) let the impl split work across
the parallel readers,
+ // and each range-honoring reader only reads its assigned slice.
+ LateMaterializingReaderBuilder builder(
+ std::make_unique<MockFormatReaderBuilder>(data, full_type_,
/*batch_size=*/3),
+ GetDefaultPool());
+ auto mock_fs = std::make_shared<MockFileSystem>();
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Executor> executor,
CreateDefaultExecutor(3));
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<PrefetchFileBatchReaderImpl> impl,
+ PrefetchFileBatchReaderImpl::Create(
+ /*data_file_path=*/"", /*data_file_size=*/0, &builder, mock_fs,
+ /*prefetch_max_parallel_num=*/3, /*batch_size=*/3,
/*prefetch_batch_count=*/6,
+ /*enable_adaptive_prefetch_strategy=*/false, executor,
+ /*initialize_read_ranges=*/false,
/*read_ahead_cache_enabled=*/false, CacheConfig(),
+ GetDefaultPool()));
+ auto predicate =
+ PredicateBuilder::GreaterOrEqual(/*field_index=*/0, "k",
FieldType::BIGINT, Literal(5l));
+ ::ArrowSchema c_schema;
+ ASSERT_TRUE(arrow::ExportSchema(*arrow::schema(full_fields_),
&c_schema).ok());
+ ASSERT_OK(impl->SetReadSchema(&c_schema, predicate, std::nullopt));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::StructArray> result,
CollectStruct(impl.get()));
+ ASSERT_TRUE(result);
+ ASSERT_EQ(result->length(), 15); // k = 5..19
+ auto k =
arrow::internal::checked_pointer_cast<arrow::Int64Array>(result->GetFieldByName("k"));
+ auto v =
arrow::internal::checked_pointer_cast<arrow::StringArray>(result->GetFieldByName("v"));
+ ASSERT_TRUE(k && v);
+ for (int64_t j = 0; j < result->length(); ++j) {
+ EXPECT_EQ(k->Value(j), 5 + j);
+ EXPECT_EQ(v->GetString(j), "v_" + std::to_string(5 + j));
+ }
+ impl->Close();
+}
+
+// When the predicate's field type does not match the probe schema, the
+// ValidatePredicateWithSchema check must fail with a clear error instead
+// of silently producing incorrect results.
+TEST_F(LateMaterializingFileBatchReaderTest, FailsOnPredicateTypeMismatch) {
+ auto data = BuildData({0, 1, 2, 3, 4});
+ auto mock = std::make_unique<MockFileBatchReader>(data, full_type_,
/*batch_size=*/2);
+ ASSERT_OK_AND_ASSIGN(
+ auto reader, LateMaterializingFileBatchReader::Create(std::move(mock),
GetDefaultPool()));
+ // k is int64 in the schema, but the predicate claims FieldType::INT
(int32).
+ auto predicate =
+ PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"k",
FieldType::INT, Literal(10));
+ ASSERT_NOK_WITH_MSG(
+ SetReadSchema(reader.get(), arrow::schema(full_fields_), predicate,
std::nullopt),
+ "mismatches");
+}
+
+} // namespace paimon::test
diff --git a/src/paimon/common/reader/late_materializing_reader_builder.h
b/src/paimon/common/reader/late_materializing_reader_builder.h
new file mode 100644
index 00000000..5c7be507
--- /dev/null
+++ b/src/paimon/common/reader/late_materializing_reader_builder.h
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+#include <memory>
+#include <utility>
+
+#include "paimon/common/reader/late_materializing_file_batch_reader.h"
+#include "paimon/format/reader_builder.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/reader/prefetch_file_batch_reader.h"
+#include "paimon/result.h"
+
+namespace paimon {
+
+class LateMaterializingReaderBuilder : public ReaderBuilder {
+ public:
+ LateMaterializingReaderBuilder(std::unique_ptr<ReaderBuilder> inner,
+ std::shared_ptr<MemoryPool> pool)
+ : inner_(std::move(inner)), pool_(std::move(pool)) {}
+
+ ReaderBuilder* WithMemoryPool(const std::shared_ptr<MemoryPool>& pool)
override {
+ pool_ = pool;
+ inner_->WithMemoryPool(pool);
+ return this;
+ }
+
+ ReaderBuilder* WithCache(const std::shared_ptr<Cache>& cache) override {
+ inner_->WithCache(cache);
+ return this;
+ }
+
+ ReaderBuilder* WithReadHints(const std::optional<ReadHints>& hints)
override {
+ inner_->WithReadHints(hints);
+ return this;
+ }
+
+ Result<std::unique_ptr<FileBatchReader>> Build(
+ const std::shared_ptr<InputStream>& stream) const override {
+ PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileBatchReader> format_reader,
+ inner_->Build(stream));
+ PAIMON_ASSIGN_OR_RAISE(
+ std::unique_ptr<LateMaterializingFileBatchReader> reader,
+ LateMaterializingFileBatchReader::Create(std::move(format_reader),
pool_));
+ return std::unique_ptr<FileBatchReader>(std::move(reader));
+ }
+
+ private:
+ std::unique_ptr<ReaderBuilder> inner_;
+ std::shared_ptr<MemoryPool> pool_;
+};
+
+} // namespace paimon
diff --git a/src/paimon/common/reader/prefetch_file_batch_reader_impl.cpp
b/src/paimon/common/reader/prefetch_file_batch_reader_impl.cpp
index 12e38966..369bb58f 100644
--- a/src/paimon/common/reader/prefetch_file_batch_reader_impl.cpp
+++ b/src/paimon/common/reader/prefetch_file_batch_reader_impl.cpp
@@ -411,7 +411,8 @@ std::optional<std::pair<uint64_t, uint64_t>>
PrefetchFileBatchReaderImpl::GetCur
Status PrefetchFileBatchReaderImpl::EnsureReaderPosition(
size_t reader_idx, const std::pair<uint64_t, uint64_t>&
current_read_range) const {
uint64_t pos = std::max(readers_pos_[reader_idx]->load(),
current_read_range.first);
- if (readers_[reader_idx]->GetNextRowToRead() != pos) {
+ PAIMON_ASSIGN_OR_RAISE(uint64_t next_row_to_read,
readers_[reader_idx]->GetNextRowToRead());
+ if (next_row_to_read != pos) {
return readers_[reader_idx]->SeekToRow(pos);
}
return Status::OK();
@@ -480,7 +481,9 @@ Status PrefetchFileBatchReaderImpl::HandleReadResult(
} else {
// all within the range, data before
readers_[reader_idx]->GetNextRowToRead() has been
// effectively consumed
-
readers_pos_[reader_idx]->store(readers_[reader_idx]->GetNextRowToRead());
+ PAIMON_ASSIGN_OR_RAISE(uint64_t next_row_to_read,
+ readers_[reader_idx]->GetNextRowToRead());
+ readers_pos_[reader_idx]->store(next_row_to_read);
}
if (bitmap.IsEmpty()) {
ReaderUtils::ReleaseReadBatch(std::move(read_batch));
@@ -646,7 +649,7 @@ Result<uint64_t>
PrefetchFileBatchReaderImpl::GetNumberOfRows() const {
return readers_[0]->GetNumberOfRows();
}
-uint64_t PrefetchFileBatchReaderImpl::GetNextRowToRead() const {
+Result<uint64_t> PrefetchFileBatchReaderImpl::GetNextRowToRead() const {
assert(false);
return -1;
}
diff --git a/src/paimon/common/reader/prefetch_file_batch_reader_impl.h
b/src/paimon/common/reader/prefetch_file_batch_reader_impl.h
index c21856d0..4750501e 100644
--- a/src/paimon/common/reader/prefetch_file_batch_reader_impl.h
+++ b/src/paimon/common/reader/prefetch_file_batch_reader_impl.h
@@ -80,7 +80,7 @@ class PrefetchFileBatchReaderImpl : public
PrefetchFileBatchReader {
Status SeekToRow(uint64_t row_number) override;
Result<uint64_t> GetPreviousBatchFileRowId(uint64_t batch_row_id) const
override;
Result<uint64_t> GetNumberOfRows() const override;
- uint64_t GetNextRowToRead() const override;
+ Result<uint64_t> GetNextRowToRead() const override;
void Close() override;
Status SetReadRanges(const std::vector<std::pair<uint64_t, uint64_t>>&
read_ranges) override;
diff --git a/src/paimon/common/utils/arrow/arrow_utils.cpp
b/src/paimon/common/utils/arrow/arrow_utils.cpp
index f29e1d11..c6a07b2b 100644
--- a/src/paimon/common/utils/arrow/arrow_utils.cpp
+++ b/src/paimon/common/utils/arrow/arrow_utils.cpp
@@ -449,9 +449,7 @@ Result<std::shared_ptr<arrow::RecordBatch>>
ArrowUtils::NormalizeRecordBatchOffs
if (normalized_columns.empty()) {
normalized_columns = record_batch->columns();
}
- PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::ArrayData>
normalized_data,
- RebaseToZeroOffset(column->data(), pool));
- normalized_columns[i] = arrow::MakeArray(normalized_data);
+ PAIMON_ASSIGN_OR_RAISE(normalized_columns[i],
NormalizeArrayOffsets(column, pool));
}
if (normalized_columns.empty()) {
return record_batch;
@@ -460,6 +458,13 @@ Result<std::shared_ptr<arrow::RecordBatch>>
ArrowUtils::NormalizeRecordBatchOffs
std::move(normalized_columns));
}
+Result<std::shared_ptr<arrow::Array>> ArrowUtils::NormalizeArrayOffsets(
+ const std::shared_ptr<arrow::Array>& array, arrow::MemoryPool* pool) {
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::ArrayData> normalized_data,
+ RebaseToZeroOffset(array->data(), pool));
+ return arrow::MakeArray(normalized_data);
+}
+
Result<arrow::Compression::type> ArrowUtils::GetCompressionType(const
std::string& compression) {
std::string normalized = StringUtils::ToLowerCase(compression);
if (normalized.empty() || normalized == "none") {
diff --git a/src/paimon/common/utils/arrow/arrow_utils.h
b/src/paimon/common/utils/arrow/arrow_utils.h
index 326b3889..13bd8154 100644
--- a/src/paimon/common/utils/arrow/arrow_utils.h
+++ b/src/paimon/common/utils/arrow/arrow_utils.h
@@ -57,6 +57,9 @@ class PAIMON_EXPORT ArrowUtils {
static Result<std::shared_ptr<arrow::RecordBatch>>
NormalizeRecordBatchOffsets(
const std::shared_ptr<arrow::RecordBatch>& record_batch,
arrow::MemoryPool* pool);
+ static Result<std::shared_ptr<arrow::Array>> NormalizeArrayOffsets(
+ const std::shared_ptr<arrow::Array>& array, arrow::MemoryPool* pool);
+
static bool EqualsIgnoreNullable(const std::shared_ptr<arrow::DataType>&
type,
const std::shared_ptr<arrow::DataType>&
other_type);
diff --git a/src/paimon/core/operation/abstract_split_read.cpp
b/src/paimon/core/operation/abstract_split_read.cpp
index d057e1d7..89f5071e 100644
--- a/src/paimon/core/operation/abstract_split_read.cpp
+++ b/src/paimon/core/operation/abstract_split_read.cpp
@@ -33,6 +33,7 @@
#include "paimon/common/data/shredding/shredding_file_reader.h"
#include "paimon/common/data/variant/variant_shredding_read_plan_factory.h"
#include "paimon/common/reader/delegating_prefetch_reader.h"
+#include "paimon/common/reader/late_materializing_reader_builder.h"
#include "paimon/common/reader/predicate_batch_reader.h"
#include "paimon/common/reader/prefetch_file_batch_reader_impl.h"
#include "paimon/common/table/special_fields.h"
@@ -96,7 +97,7 @@ Result<std::vector<std::unique_ptr<FileBatchReader>>>
AbstractSplitRead::CreateR
PrepareReaderBuilder(data_file_identifier,
extra_format_options));
PAIMON_ASSIGN_OR_RAISE(
std::unique_ptr<FileBatchReader> file_reader,
- CreateFieldMappingReader(data_file_path, file, partition,
reader_builder.get(),
+ CreateFieldMappingReader(data_file_path, file, partition,
std::move(reader_builder),
field_mapping_builder.get(), dv_factory,
row_ranges,
data_file_path_factory));
if (file_reader) {
@@ -151,13 +152,17 @@ Result<std::unique_ptr<ReaderBuilder>>
AbstractSplitRead::PrepareReaderBuilder(
Result<std::unique_ptr<FileBatchReader>>
AbstractSplitRead::CreateFileBatchReader(
const std::string& file_format_identifier, const std::string&
data_file_path,
- int64_t data_file_size, const ReaderBuilder* reader_builder) const {
+ int64_t data_file_size, std::unique_ptr<ReaderBuilder> reader_builder)
const {
+ if (context_->EnableLateMaterializing()) {
+ reader_builder =
+
std::make_unique<LateMaterializingReaderBuilder>(std::move(reader_builder),
pool_);
+ }
if (context_->EnablePrefetch() && file_format_identifier != "blob" &&
file_format_identifier != "avro") {
PAIMON_ASSIGN_OR_RAISE(
std::unique_ptr<PrefetchFileBatchReaderImpl> prefetch_reader,
PrefetchFileBatchReaderImpl::Create(
- data_file_path, data_file_size, reader_builder,
options_.GetFileSystem(),
+ data_file_path, data_file_size, reader_builder.get(),
options_.GetFileSystem(),
context_->GetPrefetchMaxParallelNum(),
options_.GetReadBatchSize(),
context_->GetPrefetchBatchCount(),
options_.EnableAdaptivePrefetchStrategy(),
executor_,
@@ -174,7 +179,7 @@ Result<std::unique_ptr<FileBatchReader>>
AbstractSplitRead::CreateFileBatchReade
Result<std::unique_ptr<FileBatchReader>>
AbstractSplitRead::CreateFieldMappingReader(
const std::string& data_file_path, const std::shared_ptr<DataFileMeta>&
file_meta,
- const BinaryRow& partition, const ReaderBuilder* reader_builder,
+ const BinaryRow& partition, std::unique_ptr<ReaderBuilder> reader_builder,
const FieldMappingBuilder* field_mapping_builder, DeletionVector::Factory
dv_factory,
const std::optional<std::vector<Range>>& row_ranges,
const std::shared_ptr<DataFilePathFactory>& data_file_path_factory) const {
@@ -214,7 +219,7 @@ Result<std::unique_ptr<FileBatchReader>>
AbstractSplitRead::CreateFieldMappingRe
PAIMON_ASSIGN_OR_RAISE(std::string file_format_identifier,
file_meta->FileFormat());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileBatchReader> file_reader,
CreateFileBatchReader(file_format_identifier,
data_file_path,
- file_meta->file_size,
reader_builder));
+ file_meta->file_size,
std::move(reader_builder)));
if (VectorFileBatchReader::ContainsVector(read_schema)) {
file_reader =
std::make_unique<VectorFileBatchReader>(std::move(file_reader), pool_);
}
diff --git a/src/paimon/core/operation/abstract_split_read.h
b/src/paimon/core/operation/abstract_split_read.h
index a56b48fd..a02ed5fb 100644
--- a/src/paimon/core/operation/abstract_split_read.h
+++ b/src/paimon/core/operation/abstract_split_read.h
@@ -107,12 +107,12 @@ class AbstractSplitRead : public SplitRead {
Result<std::unique_ptr<FileBatchReader>> CreateFileBatchReader(
const std::string& file_format_identifier, const std::string&
data_file_path,
- int64_t data_file_size, const ReaderBuilder* reader_builder) const;
+ int64_t data_file_size, std::unique_ptr<ReaderBuilder> reader_builder)
const;
// return nullptr if data file is skipped by index or dv
Result<std::unique_ptr<FileBatchReader>> CreateFieldMappingReader(
const std::string& data_file_path, const
std::shared_ptr<DataFileMeta>& file_meta,
- const BinaryRow& partition, const ReaderBuilder* reader_builder,
+ const BinaryRow& partition, std::unique_ptr<ReaderBuilder>
reader_builder,
const FieldMappingBuilder* field_mapping_builder,
DeletionVector::Factory dv_factory,
const std::optional<std::vector<Range>>& row_ranges,
const std::shared_ptr<DataFilePathFactory>& data_file_path_factory)
const;
diff --git a/src/paimon/core/operation/data_evolution_split_read.h
b/src/paimon/core/operation/data_evolution_split_read.h
index fad0e674..2568eae4 100644
--- a/src/paimon/core/operation/data_evolution_split_read.h
+++ b/src/paimon/core/operation/data_evolution_split_read.h
@@ -64,7 +64,8 @@ struct DeletionFile;
/// ->(ConcatBatchReader across blob files | BlobFallbackBatchReader across
blob sequence layers)
///
->FieldMappingReader->(ApplyDeletionVectorBatchReader)->(ApplyBitmapIndexBatchReader)
/// ->(CompleteRowTrackingFieldsBatchReader)->(ShreddingFileReader)
-///
->(VectorFileBatchReader)->(DelegatingPrefetchReader)->(PrefetchFileBatchReader)->FormatReader
+///
->(VectorFileBatchReader)->(DelegatingPrefetchReader)->(PrefetchFileBatchReader)
+/// ->(LateMaterializingFileBatchReader)->FormatReader
///
///
/// A union `SplitRead` to read multiple inner files to merge columns. A
single-file row range
diff --git a/src/paimon/core/operation/internal_read_context.h
b/src/paimon/core/operation/internal_read_context.h
index 8ef9f2d2..8e773cdc 100644
--- a/src/paimon/core/operation/internal_read_context.h
+++ b/src/paimon/core/operation/internal_read_context.h
@@ -74,6 +74,9 @@ class InternalReadContext {
bool EnablePrefetch() const {
return read_context_->EnablePrefetch();
}
+ bool EnableLateMaterializing() const {
+ return read_context_->EnableLateMaterializing();
+ }
uint32_t GetPrefetchBatchCount() const {
return read_context_->GetPrefetchBatchCount();
}
diff --git a/src/paimon/core/operation/merge_file_split_read.h
b/src/paimon/core/operation/merge_file_split_read.h
index 5003cb55..11dcd0b3 100644
--- a/src/paimon/core/operation/merge_file_split_read.h
+++ b/src/paimon/core/operation/merge_file_split_read.h
@@ -74,7 +74,8 @@ class MergeFunctionWrapper;
/// files->KeyValueProjectionReader/AsyncKeyValueProjectionReader
///
->DropDeleteReader->SortMergeReader->ConcatKeyValueRecordReader->KeyValueDataFileRecordReader
///
->FieldMappingReader->(ApplyDeletionVectorBatchReader)->(ShreddingFileReader)
-/// ->(DelegatingPrefetchReader)->(PrefetchFileBatchReader)->FormatReader
+/// ->(DelegatingPrefetchReader)->(PrefetchFileBatchReader)
+/// ->(LateMaterializingFileBatchReader)->FormatReader
class MergeFileSplitRead : public AbstractSplitRead {
public:
static Result<std::unique_ptr<MergeFileSplitRead>> Create(
diff --git a/src/paimon/core/operation/merge_file_split_read_test.cpp
b/src/paimon/core/operation/merge_file_split_read_test.cpp
index 911cf796..19cd96d6 100644
--- a/src/paimon/core/operation/merge_file_split_read_test.cpp
+++ b/src/paimon/core/operation/merge_file_split_read_test.cpp
@@ -798,6 +798,7 @@ TEST_P(MergeFileSplitReadTest, TestReadWithPredicate) {
context_builder.SetOptions({{Options::SEQUENCE_FIELD, "s0,s1"},
{Options::MERGE_ENGINE, "deduplicate"},
{Options::IGNORE_DELETE, "true"}});
+ context_builder.EnableLateMaterializing(false);
AddOptions(&context_builder);
// less_than will be ignore as it is partition predicate
@@ -842,6 +843,63 @@ TEST_P(MergeFileSplitReadTest, TestReadWithPredicate) {
CheckResult(result_array, expected_array, read_schema);
}
+TEST_P(MergeFileSplitReadTest, TestReadWithPredicateAndLateMaterializing) {
+ std::string path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_with_mor.db/pk_table_with_mor";
+ ReadContextBuilder context_builder(path);
+
+ std::vector<DataField> raw_read_fields = {DataField(1, arrow::field("k1",
arrow::int32())),
+ DataField(3, arrow::field("p1",
arrow::int32())),
+ DataField(5, arrow::field("s1",
arrow::utf8())),
+ DataField(4, arrow::field("s0",
arrow::utf8())),
+ DataField(6, arrow::field("v0",
arrow::float64())),
+ DataField(7, arrow::field("v1",
arrow::boolean()))};
+ auto read_schema =
DataField::ConvertDataFieldsToArrowSchema(raw_read_fields);
+ ASSERT_TRUE(read_schema);
+
+ context_builder.SetReadFieldNames({"k1", "p1", "s1", "s0", "v0", "v1"});
+ context_builder.SetOptions({{Options::SEQUENCE_FIELD, "s0,s1"},
+ {Options::MERGE_ENGINE, "deduplicate"},
+ {Options::IGNORE_DELETE, "true"}});
+ AddOptions(&context_builder);
+ context_builder.EnableLateMaterializing(true);
+ // key predicate, always pushed down into the data files
+ auto greater_or_equal =
PredicateBuilder::GreaterOrEqual(/*field_index=*/0, /*field_name=*/"k1",
+ FieldType::INT,
Literal(1));
+ // value predicate, only pushed down when a section holds a single sorted
run
+ auto greater_than = PredicateBuilder::GreaterThan(/*field_index=*/4,
/*field_name=*/"v0",
+ FieldType::DOUBLE,
Literal(12.0));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> predicate_result,
+ PredicateBuilder::And({greater_or_equal,
greater_than}));
+ context_builder.SetPredicate(predicate_result);
+ context_builder.EnablePredicateFilter(true).EnableLateMaterializing(true);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<ReadContext> read_context,
context_builder.Finish());
+
+ auto internal_context = CreateInternalReadContext(read_context);
+ ASSERT_OK_AND_ASSIGN(auto batch_reader, CreateReader(internal_context,
PrepareDataSplit()));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result_array,
+
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ auto fields_with_row_kind = read_schema->fields();
+ fields_with_row_kind.insert(fields_with_row_kind.begin(),
+ arrow::field("_VALUE_KIND", arrow::int8()));
+
+ // Only the merged rows with k1 >= 1 and v0 > 12.0 remain.
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
+
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow::struct_(fields_with_row_kind),
{R"([
+ [0, 1, 0, "!", "driver", 13.3, false],
+ [0, 2, 0, "!", "driver", 13.3, false],
+ [0, 200, 0, "number", "max", 140.4, false],
+ [0, 1, 1, "you", "zoo", 130.0, false]
+
+ ])"},
+ &expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckResult(result_array, expected_array, read_schema);
+}
+
TEST_P(MergeFileSplitReadTest, TestReadWithAlterTable) {
std::string path =
paimon::test::GetDataDir() +
"/parquet/pk_table_with_mor.db/pk_table_with_mor";
diff --git a/src/paimon/core/operation/raw_file_split_read.h
b/src/paimon/core/operation/raw_file_split_read.h
index 93eab550..646f24ac 100644
--- a/src/paimon/core/operation/raw_file_split_read.h
+++ b/src/paimon/core/operation/raw_file_split_read.h
@@ -55,7 +55,8 @@ struct DeletionFile;
/// ->ConcatBatchReader across
///
files->FieldMappingReader->(ApplyBitmapIndexBatchReader)->(CompleteRowTrackingFieldsBatchReader)
/// ->(ShreddingFileReader)->(VectorFileBatchReader)
-/// ->(DelegatingPrefetchReader)->(PrefetchFileBatchReader)->FormatReader
+/// ->(DelegatingPrefetchReader)->(PrefetchFileBatchReader)
+/// ->(LateMaterializingFileBatchReader)->FormatReader
class RawFileSplitRead : public AbstractSplitRead {
public:
diff --git a/src/paimon/core/operation/read_context.cpp
b/src/paimon/core/operation/read_context.cpp
index 08a854d8..deacfa78 100644
--- a/src/paimon/core/operation/read_context.cpp
+++ b/src/paimon/core/operation/read_context.cpp
@@ -35,10 +35,10 @@ ReadContext::ReadContext(
const std::string& path, const std::string& branch,
const std::vector<std::string>& read_field_names, const
std::vector<int32_t>& read_field_ids,
const std::shared_ptr<Predicate>& predicate, bool enable_predicate_filter,
bool enable_prefetch,
- uint32_t prefetch_batch_count, uint32_t prefetch_max_parallel_num,
- bool enable_multi_thread_row_to_batch, uint32_t row_to_batch_thread_number,
- const std::optional<std::string>& table_schema, const
std::shared_ptr<MemoryPool>& memory_pool,
- const std::shared_ptr<Executor>& executor,
+ bool enable_late_materializing, uint32_t prefetch_batch_count,
+ uint32_t prefetch_max_parallel_num, bool enable_multi_thread_row_to_batch,
+ uint32_t row_to_batch_thread_number, const std::optional<std::string>&
table_schema,
+ const std::shared_ptr<MemoryPool>& memory_pool, const
std::shared_ptr<Executor>& executor,
const std::shared_ptr<FileSystem>& specific_file_system,
const std::map<std::string, std::string>& fs_scheme_to_identifier_map,
const std::shared_ptr<RealtimeContext>& realtime_context,
@@ -51,6 +51,7 @@ ReadContext::ReadContext(
predicate_(predicate),
enable_predicate_filter_(enable_predicate_filter),
enable_prefetch_(enable_prefetch),
+ enable_late_materializing_(enable_late_materializing),
prefetch_batch_count_(prefetch_batch_count),
prefetch_max_parallel_num_(prefetch_max_parallel_num),
enable_multi_thread_row_to_batch_(enable_multi_thread_row_to_batch),
@@ -97,6 +98,7 @@ class ReadContextBuilder::Impl {
predicate_.reset();
enable_predicate_filter_ = false;
enable_prefetch_ = false;
+ enable_late_materializing_ = false;
read_ahead_cache_enabled_ = true;
prefetch_batch_count_ = 600;
prefetch_max_parallel_num_ = 3;
@@ -122,6 +124,7 @@ class ReadContextBuilder::Impl {
std::shared_ptr<Predicate> predicate_;
bool enable_predicate_filter_ = false;
bool enable_prefetch_ = false;
+ bool enable_late_materializing_ = false;
uint32_t prefetch_batch_count_ = 600;
uint32_t prefetch_max_parallel_num_ = 3;
bool enable_multi_thread_row_to_batch_ = false;
@@ -191,6 +194,11 @@ ReadContextBuilder&
ReadContextBuilder::EnablePrefetch(bool enabled) {
return *this;
}
+ReadContextBuilder& ReadContextBuilder::EnableLateMaterializing(bool enabled) {
+ impl_->enable_late_materializing_ = enabled;
+ return *this;
+}
+
ReadContextBuilder& ReadContextBuilder::SetPrefetchBatchCount(uint32_t
batch_count) {
impl_->prefetch_batch_count_ = batch_count;
return *this;
@@ -297,11 +305,12 @@ Result<std::unique_ptr<ReadContext>>
ReadContextBuilder::Finish() {
auto ctx = std::make_unique<ReadContext>(
impl_->path_, impl_->branch_, impl_->read_field_names_,
impl_->read_field_ids_,
impl_->predicate_, impl_->enable_predicate_filter_,
impl_->enable_prefetch_,
- impl_->prefetch_batch_count_, impl_->prefetch_max_parallel_num_,
- impl_->enable_multi_thread_row_to_batch_,
impl_->row_to_batch_thread_number_,
- impl_->table_schema_, impl_->memory_pool_, impl_->executor_,
impl_->specific_file_system_,
- impl_->fs_scheme_to_identifier_map_, impl_->realtime_context_,
impl_->options_,
- impl_->read_ahead_cache_enabled_, impl_->cache_config_, impl_->cache_);
+ impl_->enable_late_materializing_, impl_->prefetch_batch_count_,
+ impl_->prefetch_max_parallel_num_,
impl_->enable_multi_thread_row_to_batch_,
+ impl_->row_to_batch_thread_number_, impl_->table_schema_,
impl_->memory_pool_,
+ impl_->executor_, impl_->specific_file_system_,
impl_->fs_scheme_to_identifier_map_,
+ impl_->realtime_context_, impl_->options_,
impl_->read_ahead_cache_enabled_,
+ impl_->cache_config_, impl_->cache_);
if (impl_->read_schema_ && impl_->read_schema_->release) {
ctx->SetReadSchema(std::move(impl_->read_schema_));
}
diff --git a/src/paimon/format/orc/orc_file_batch_reader.h
b/src/paimon/format/orc/orc_file_batch_reader.h
index 85673a93..b48f7cdb 100644
--- a/src/paimon/format/orc/orc_file_batch_reader.h
+++ b/src/paimon/format/orc/orc_file_batch_reader.h
@@ -85,7 +85,7 @@ class OrcFileBatchReader : public PrefetchFileBatchReader {
return reader_->GetNumberOfRows();
}
- uint64_t GetNextRowToRead() const override {
+ Result<uint64_t> GetNextRowToRead() const override {
return reader_->GetNextRowToRead();
}
diff --git a/src/paimon/format/parquet/parquet_file_batch_reader.cpp
b/src/paimon/format/parquet/parquet_file_batch_reader.cpp
index 0c9d065e..7605c424 100644
--- a/src/paimon/format/parquet/parquet_file_batch_reader.cpp
+++ b/src/paimon/format/parquet/parquet_file_batch_reader.cpp
@@ -154,6 +154,7 @@ ParquetFileBatchReader::ParquetFileBatchReader(
arrow_pool_(arrow_pool),
input_stream_(std::move(input_stream)),
reader_(std::move(reader)),
+ read_ranges_(reader_->GetAllRowGroupRanges()),
metrics_(std::make_shared<MetricsImpl>()),
storage_read_bytes_(std::move(storage_read_bytes)),
logger_(Logger::GetLogger("ParquetFileBatchReader")) {}
@@ -309,6 +310,7 @@ Status ParquetFileBatchReader::SetReadSchema(
PAIMON_RETURN_NOT_OK(UpdateAllTargetRowRanges(target_row_groups));
PAIMON_RETURN_NOT_OK(reader_->PrepareForReadingLazy(target_row_groups,
column_indices));
+ PAIMON_RETURN_NOT_OK(reader_->ApplyReadRanges(read_ranges_));
}
PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("ParquetFileBatchReader::SetReadSchema")
return Status::OK();
diff --git a/src/paimon/format/parquet/parquet_file_batch_reader.h
b/src/paimon/format/parquet/parquet_file_batch_reader.h
index daa18f04..2b1097fb 100644
--- a/src/paimon/format/parquet/parquet_file_batch_reader.h
+++ b/src/paimon/format/parquet/parquet_file_batch_reader.h
@@ -128,12 +128,13 @@ class ParquetFileBatchReader : public
PrefetchFileBatchReader {
return reader_->GetNumberOfRows();
}
- uint64_t GetNextRowToRead() const override {
+ Result<uint64_t> GetNextRowToRead() const override {
assert(reader_);
return reader_->GetNextRowToRead();
}
Status SetReadRanges(const std::vector<std::pair<uint64_t, uint64_t>>&
read_ranges) override {
+ read_ranges_ = read_ranges;
return reader_->ApplyReadRanges(read_ranges);
}
@@ -261,6 +262,8 @@ class ParquetFileBatchReader : public
PrefetchFileBatchReader {
std::shared_ptr<arrow::DataType> read_data_type_;
+ std::vector<std::pair<uint64_t, uint64_t>> read_ranges_;
+
std::shared_ptr<Metrics> metrics_;
// storageReadBytes counter shared with the underlying
ArrowInputStreamAdapter.
std::shared_ptr<std::atomic<uint64_t>> storage_read_bytes_;
diff --git a/src/paimon/testing/mock/mock_file_batch_reader.h
b/src/paimon/testing/mock/mock_file_batch_reader.h
index f05a2347..4566289f 100644
--- a/src/paimon/testing/mock/mock_file_batch_reader.h
+++ b/src/paimon/testing/mock/mock_file_batch_reader.h
@@ -29,6 +29,7 @@
#include "arrow/api.h"
#include "arrow/c/bridge.h"
+#include "arrow/util/checked_cast.h"
#include "paimon/common/metrics/metrics_impl.h"
#include "paimon/common/reader/reader_utils.h"
#include "paimon/common/utils/arrow/status_utils.h"
@@ -78,11 +79,14 @@ class MockFileBatchReader : public PrefetchFileBatchReader {
Status SetReadSchema(::ArrowSchema* read_schema, const
std::shared_ptr<Predicate>& predicate,
const std::optional<RoaringBitmap32>&
selection_bitmap) override {
- // Noted that SetReadSchema only change inner read_schema_, but take
no effective on
- // NextBatch
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Schema>
arrow_schema,
arrow::ImportSchema(read_schema));
read_schema_ = arrow_schema;
+ // A real FileBatchReader restarts from the first row and drops its
assigned read ranges
+ // when the read schema is (re)set. Readers that switch schemas
mid-file, such as the
+ // late-materialization reader moving from its probe pass to its
payload pass, rely on it.
+ current_pos_ = 0;
+ previous_batch_first_row_num_ = std::numeric_limits<uint64_t>::max();
return Status::OK();
}
@@ -119,8 +123,29 @@ class MockFileBatchReader : public PrefetchFileBatchReader
{
Result<ReadBatchWithBitmap> NextBatchWithBitmap() override {
while (true) {
PAIMON_RETURN_NOT_OK(next_batch_status_);
- if (current_pos_ >= read_end_pos_) {
- previous_batch_first_row_num_ = current_pos_;
+ int32_t begin_pos = current_pos_;
+ int32_t range_end_pos = read_end_pos_;
+ if (!read_ranges_.empty()) {
+ // Reading is restricted to the assigned ranges (ascending and
half-open), like a
+ // real format reader, so that a prefetch reader may dispatch
disjoint ranges to
+ // parallel readers. An empty range set means the whole file
may be read.
+ const std::pair<uint64_t, uint64_t>* selected = nullptr;
+ for (const auto& range : read_ranges_) {
+ if (static_cast<int64_t>(range.second) > begin_pos) {
+ selected = ⦥
+ break;
+ }
+ }
+ if (selected == nullptr) {
+ previous_batch_first_row_num_ =
ToReaderRowNumber(begin_pos);
+ return BatchReader::MakeEofBatchWithBitmap();
+ }
+ // Skip the gap in front of the first range that has not been
read yet.
+ begin_pos = std::max(begin_pos,
static_cast<int32_t>(selected->first));
+ range_end_pos = std::min(range_end_pos,
static_cast<int32_t>(selected->second));
+ }
+ if (begin_pos >= read_end_pos_) {
+ previous_batch_first_row_num_ = ToReaderRowNumber(begin_pos);
return BatchReader::MakeEofBatchWithBitmap();
}
int32_t actual_batch_size = batch_size_;
@@ -128,21 +153,23 @@ class MockFileBatchReader : public
PrefetchFileBatchReader {
std::uniform_int_distribution<int32_t> distribution(1,
batch_size_);
actual_batch_size = distribution(random_engine_);
}
- int32_t batch_end_pos = std::min(read_end_pos_, current_pos_ +
actual_batch_size);
- auto slice = data_->Slice(current_pos_, batch_end_pos -
current_pos_);
+ int32_t batch_end_pos =
+ std::min({read_end_pos_, range_end_pos, begin_pos +
actual_batch_size});
+ auto slice = data_->Slice(begin_pos, batch_end_pos - begin_pos);
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
std::shared_ptr<arrow::Array> concat_slice,
arrow::Concatenate({slice}, arrow::default_memory_pool()));
RoaringBitmap32 bitmap;
- for (auto iter = bitmap_.EqualOrLarger(current_pos_);
+ for (auto iter = bitmap_.EqualOrLarger(begin_pos);
iter != bitmap_.End() && *iter < batch_end_pos; ++iter) {
- bitmap.Add(*iter - current_pos_);
+ bitmap.Add(*iter - begin_pos);
}
- previous_batch_first_row_num_ = current_pos_;
+ previous_batch_first_row_num_ = ToReaderRowNumber(begin_pos);
current_pos_ = batch_end_pos;
if (bitmap.IsEmpty()) {
continue;
}
+ PAIMON_ASSIGN_OR_RAISE(concat_slice, ProjectBatch(concat_slice));
std::unique_ptr<ArrowArray> c_array =
std::make_unique<ArrowArray>();
std::unique_ptr<ArrowSchema> c_schema =
std::make_unique<ArrowSchema>();
PAIMON_RETURN_NOT_OK_FROM_ARROW(
@@ -168,7 +195,7 @@ class MockFileBatchReader : public PrefetchFileBatchReader {
Result<uint64_t> GetNumberOfRows() const override {
return ToReaderRowNumber(read_end_pos_);
}
- uint64_t GetNextRowToRead() const override {
+ Result<uint64_t> GetNextRowToRead() const override {
return ToReaderRowNumber(current_pos_);
}
void Close() override {}
@@ -181,7 +208,7 @@ class MockFileBatchReader : public PrefetchFileBatchReader {
return false;
}
- private:
+ protected:
static uint64_t ToReaderRowNumber(int32_t row_number) {
if (row_number < 0) {
return std::numeric_limits<uint64_t>::max();
@@ -189,6 +216,44 @@ class MockFileBatchReader : public PrefetchFileBatchReader
{
return static_cast<uint64_t>(row_number);
}
+ /// Pick the columns requested by `read_schema_` out of `batch`, in the
requested order.
+ ///
+ /// `batch` is returned as is unless the requested schema is a genuine
re-selection of the
+ /// columns this file has. Requesting a field the file does not have means
the read schema is a
+ /// logical view over some other physical layout, as the shredding and the
row tracking readers
+ /// do, and those map the raw batch themselves.
+ /// `batch` is expected to have a zero offset, so its validity buffer can
be reused as is.
+ Result<std::shared_ptr<arrow::Array>> ProjectBatch(
+ const std::shared_ptr<arrow::Array>& batch) const {
+ auto struct_batch =
std::dynamic_pointer_cast<arrow::StructArray>(batch);
+ if (struct_batch == nullptr) {
+ return batch;
+ }
+ arrow::ArrayVector children;
+ arrow::FieldVector fields;
+ for (const auto& field : read_schema_->fields()) {
+ std::shared_ptr<arrow::Array> column =
struct_batch->GetFieldByName(field->name());
+ if (column == nullptr) {
+ return batch;
+ }
+ children.push_back(column);
+ fields.push_back(field);
+ }
+ const arrow::FieldVector& batch_fields =
struct_batch->type()->fields();
+ bool keeps_every_column = fields.size() == batch_fields.size();
+ for (size_t i = 0; keeps_every_column && i < fields.size(); i++) {
+ keeps_every_column = fields[i]->name() == batch_fields[i]->name();
+ }
+ if (keeps_every_column) {
+ return batch;
+ }
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+ std::shared_ptr<arrow::StructArray> projected,
+ arrow::StructArray::Make(children, fields,
struct_batch->null_bitmap(),
+ struct_batch->null_count()));
+ return projected;
+ }
+
std::shared_ptr<arrow::Array> data_;
std::shared_ptr<arrow::DataType> file_schema_;
std::shared_ptr<arrow::Schema> read_schema_;
diff --git a/test/inte/blob_table_inte_test.cpp
b/test/inte/blob_table_inte_test.cpp
index ea951a31..b3c5cb23 100644
--- a/test/inte/blob_table_inte_test.cpp
+++ b/test/inte/blob_table_inte_test.cpp
@@ -299,6 +299,7 @@ class BlobTableInteTest : public testing::Test, public
::testing::WithParamInter
auto splits = plan->Splits();
ReadContextBuilder read_context_builder(table_path);
read_context_builder.SetReadFieldNames(read_schema).SetPredicate(predicate);
+ read_context_builder.EnableLateMaterializing(false);
if (!options.empty()) {
read_context_builder.SetOptions(options);
}
diff --git a/test/inte/global_index_test.cpp b/test/inte/global_index_test.cpp
index ccb9ce32..84ab22df 100644
--- a/test/inte/global_index_test.cpp
+++ b/test/inte/global_index_test.cpp
@@ -209,7 +209,8 @@ class GlobalIndexTest : public ::testing::Test, public
::testing::WithParamInter
ReadContextBuilder read_context_builder(table_path);
read_context_builder.SetReadFieldNames(read_schema)
.SetPredicate(predicate)
- .WithFileSystem(fs_);
+ .WithFileSystem(fs_)
+ .EnableLateMaterializing(false);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ReadContext> read_context,
read_context_builder.Finish());
PAIMON_ASSIGN_OR_RAISE(auto table_read,
TableRead::Create(std::move(read_context)));
diff --git a/test/inte/read_inte_test.cpp b/test/inte/read_inte_test.cpp
index 1e0952e0..a2a52d34 100644
--- a/test/inte/read_inte_test.cpp
+++ b/test/inte/read_inte_test.cpp
@@ -2209,6 +2209,7 @@ TEST_P(ReadInteTest,
TestAppendReadWithPredicateOnlyPushdown) {
.AddOption("test.enable-adaptive-prefetch-strategy",
param.enable_adaptive_prefetch_strategy)
.SetPredicate(predicate)
+ .EnableLateMaterializing(false)
.EnablePrefetch(param.enable_prefetch);
ASSERT_OK_AND_ASSIGN(auto read_context, context_builder.Finish());
@@ -2265,6 +2266,89 @@ TEST_P(ReadInteTest,
TestAppendReadWithPredicateOnlyPushdown) {
ASSERT_TRUE(result_array->Equals(*expected_array)) <<
result_array->ToString();
}
+// Late materialization reads the predicate columns first and only
materializes the remaining
+// columns for matched rows. Combined with the top-level predicate filter, the
read path returns
+// the exact user-predicate match set.
+TEST_P(ReadInteTest, TestAppendReadWithLateMaterializing) {
+ std::vector<DataField> read_fields = {DataField(3, arrow::field("f3",
arrow::float64())),
+ DataField(0, arrow::field("f0",
arrow::utf8())),
+ DataField(1, arrow::field("f1",
arrow::int32()))};
+ ASSERT_OK_AND_ASSIGN(
+ auto predicate,
+ PredicateBuilder::Or(
+ {PredicateBuilder::GreaterThan(/*field_index=*/0,
/*field_name=*/"f3",
+ FieldType::DOUBLE,
Literal(static_cast<double>(15.0))),
+ PredicateBuilder::IsNull(/*field_index=*/0, /*field_name=*/"f3",
FieldType::DOUBLE)}));
+
+ auto param = GetParam();
+ std::string path =
+ paimon::test::GetDataDir() + "/" + param.file_format +
"/append_09.db/append_09";
+
+ ReadContextBuilder context_builder(path);
+ context_builder.SetReadAheadCacheEnabled(param.read_ahead_cache_enabled);
+ context_builder.SetReadFieldNames({"f3", "f0", "f1"});
+ context_builder.AddOption(Options::FILE_FORMAT, param.file_format)
+ .AddOption("read.batch-size", "2")
+ .AddOption("test.enable-adaptive-prefetch-strategy",
+ param.enable_adaptive_prefetch_strategy)
+ .SetPredicate(predicate)
+ .EnableLateMaterializing(true)
+ .EnablePrefetch(param.enable_prefetch);
+ ASSERT_OK_AND_ASSIGN(auto read_context, context_builder.Finish());
+ ASSERT_OK_AND_ASSIGN(auto table_read,
TableRead::Create(std::move(read_context)));
+
+ std::vector<std::string> file_list_0;
+ std::vector<std::string> file_list_1;
+ std::vector<std::string> file_list_2;
+ if (param.file_format == "orc") {
+ file_list_0 = {"data-d41fd7d1-b3e4-4905-aad9-b20a780e90a2-0.orc"};
+ file_list_1 = {"data-4e30d6c0-f109-4300-a010-4ba03047dd9d-0.orc",
+ "data-10b9eea8-241d-4e4b-8ab8-2a82d72d79a2-0.orc",
+ "data-e2bb59ee-ae25-4e5b-9bcc-257250bc5fdd-0.orc",
+ "data-2d5ea1ea-77c1-47ff-bb87-19a509962a37-0.orc"};
+ file_list_2 = {"data-db2b44c0-0d73-449d-82a0-4075bd2cb6e3-0.orc",
+ "data-b913a160-a4d1-4084-af2a-18333c35668e-0.orc"};
+ } else if (param.file_format == "parquet") {
+ file_list_0 = {"data-46e27d5b-4850-4d1e-abb6-b3aabbbc08cb-0.parquet"};
+ file_list_1 = {"data-864a052b-a938-4e04-b32c-6c72699a0c92-0.parquet",
+ "data-c0401350-64a3-4a54-a143-dd125ad9a8e5-0.parquet",
+ "data-7a912f84-04b7-4bbb-8dc6-53f4a292ea25-0.parquet",
+ "data-bb891df7-ea12-4b7e-9017-41aabe08c8ec-0.parquet"};
+ file_list_2 = {"data-b446f78a-2cfb-4b3b-add8-31295d24a277-0.parquet",
+ "data-fd72a479-53ae-42f7-aec0-e982ee555928-0.parquet"};
+ }
+
+ DataSplitsSimple input_data_splits = {
+ {paimon::test::GetDataDir() + "/" + param.file_format +
+ "/append_09.db/append_09/f1=10/bucket-0",
+ BinaryRowGenerator::GenerateRow({10}, pool_.get()), file_list_0},
+ {paimon::test::GetDataDir() + "/" + param.file_format +
+ "/append_09.db/append_09/f1=10/bucket-1",
+ BinaryRowGenerator::GenerateRow({10}, pool_.get()), file_list_1},
+ {paimon::test::GetDataDir() + "/" + param.file_format +
+ "/append_09.db/append_09/f1=20/bucket-0",
+ BinaryRowGenerator::GenerateRow({20}, pool_.get()), file_list_2}};
+
+ auto data_splits = CreateDataSplits(input_data_splits, /*snapshot_id=*/4);
+ ASSERT_OK_AND_ASSIGN(auto batch_reader,
table_read->CreateReader(data_splits));
+ ASSERT_OK_AND_ASSIGN(auto result_array,
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ auto fields_with_row_kind = read_fields;
+ fields_with_row_kind.insert(fields_with_row_kind.begin(),
SpecialFields::ValueKind());
+ std::shared_ptr<arrow::DataType> arrow_data_type =
+ DataField::ConvertDataFieldsToArrowStructType(fields_with_row_kind);
+
+ // "Bob" (f3 = 12.1) is the only row that does not match the predicate.
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow_data_type, {R"([
+ [0, 15.1, "Emily", 10], [0, 16.1, "Alex", 10], [0, 17.1, "David", 10],
+ [0, 17.1, "Lily", 10], [0, null, "Paul", 20]
+ ])"},
+
&expected_array);
+ ASSERT_TRUE(array_status.ok());
+ ASSERT_TRUE(result_array->Equals(*expected_array)) <<
result_array->ToString();
+}
+
TEST_P(ReadInteTest, TestAppendReadWithPredicateAllFiltered) {
std::vector<DataField> read_fields = {DataField(3, arrow::field("f3",
arrow::float64())),
DataField(0, arrow::field("f0",
arrow::utf8())),
@@ -3109,6 +3193,7 @@ TEST_P(ReadInteTest,
TestPkReadSnapshot6WithSchemaEvolutionWithPredicateOnlyPush
context_builder.SetReadAheadCacheEnabled(param.read_ahead_cache_enabled);
context_builder.SetPredicate(predicate);
context_builder.EnablePrefetch(param.enable_prefetch)
+ .EnableLateMaterializing(false)
.AddOption("test.enable-adaptive-prefetch-strategy",
param.enable_adaptive_prefetch_strategy);
ASSERT_OK_AND_ASSIGN(auto read_context, context_builder.Finish());
@@ -3166,6 +3251,91 @@ TEST_P(ReadInteTest,
TestPkReadSnapshot6WithSchemaEvolutionWithPredicateOnlyPush
ASSERT_TRUE(result_array->Equals(*expected_array));
}
+TEST_P(ReadInteTest,
TestPkReadSnapshot6WithSchemaEvolutionWithLateMaterializing) {
+ std::vector<DataField> read_fields = {DataField(1, arrow::field("key1",
arrow::int32())),
+ DataField(7, arrow::field("k",
arrow::utf8())),
+ DataField(2, arrow::field("key_2",
arrow::int32())),
+ DataField(4, arrow::field("c",
arrow::int32())),
+ DataField(8, arrow::field("d",
arrow::int32())),
+ DataField(6, arrow::field("a",
arrow::int32())),
+ DataField(0, arrow::field("key0",
arrow::int32())),
+ DataField(9, arrow::field("e",
arrow::int32()))};
+ auto param = GetParam();
+ std::string path = paimon::test::GetDataDir() + "/" + param.file_format +
+
"/pk_table_with_alter_table.db/pk_table_with_alter_table/";
+ // equal is a partition filter and is not pushed into the data files;
less_than is pushed down
+ // and only matches the column added by schema evolution, where the older
files yield nulls.
+ auto equal = PredicateBuilder::Equal(/*field_index=*/6,
/*field_name=*/"key0", FieldType::INT,
+ Literal(0));
+ auto less_than = PredicateBuilder::LessThan(/*field_index=*/7,
/*field_name=*/"e",
+ FieldType::INT, Literal(510));
+ ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({equal,
less_than}));
+
+ ReadContextBuilder context_builder(path);
+ context_builder.SetReadFieldNames({{"key1", "k", "key_2", "c", "d", "a",
"key0", "e"}});
+ context_builder.AddOption(Options::FILE_FORMAT, param.file_format)
+ .AddOption("read.batch-size", "2");
+ context_builder.SetReadAheadCacheEnabled(param.read_ahead_cache_enabled);
+ context_builder.SetPredicate(predicate);
+ context_builder.EnableLateMaterializing(true)
+ .EnablePrefetch(param.enable_prefetch)
+ .AddOption("test.enable-adaptive-prefetch-strategy",
+ param.enable_adaptive_prefetch_strategy);
+ ASSERT_OK_AND_ASSIGN(auto read_context, context_builder.Finish());
+ ASSERT_OK_AND_ASSIGN(auto table_read,
TableRead::Create(std::move(read_context)));
+
+ std::vector<std::string> file_list_0;
+ std::vector<std::string> file_list_1;
+ std::string deletion_file;
+ if (param.file_format == "orc") {
+ file_list_0 = {"data-3842c1d6-6b34-4b2c-a648-9e95b4fb941b-0.orc",
+ "data-d6d370f3-242b-45c9-8739-44bf31b2b449-0.orc"};
+ file_list_1 = {"data-7b538b91-5dbb-4e16-a639-1b5c0696db8c-0.orc"};
+ deletion_file = "index-51804749-ed6c-4e7b-b3e9-337cfe38499c-1";
+ } else if (param.file_format == "parquet") {
+ file_list_0 = {"data-8969384c-d715-4113-b663-2248c9a8c8d9-0.parquet",
+ "data-f2f38e80-7d28-4d51-90b3-c28951e5cdc0-0.parquet"};
+ file_list_1 = {"data-d7a33230-223e-4d65-8e39-bc7ed26bdd32-0.parquet"};
+ deletion_file = "index-c93829f3-1a72-4d88-8401-70663ce46426-1";
+ }
+
+ DataSplitsSchemaDv input_data_splits = {
+ {path + "key0=1/key1=1/bucket-0",
+ BinaryRowGenerator::GenerateRow({1, 1}, pool_.get()),
+ file_list_0,
+ /*schema ids*/ {0, 1},
+ /*deletion file*/
+ {DeletionFile(path + "index/" + deletion_file,
+ /*offset=*/1, /*length=*/26,
/*cardinality=*/std::nullopt),
+ std::nullopt}},
+ {path + "key0=0/key1=1/bucket-0", BinaryRowGenerator::GenerateRow({0,
1}, pool_.get()),
+ file_list_1,
+ /*schema ids*/ {1},
+ /*deletion file*/ {std::nullopt}}};
+
+ auto data_splits = CreateDataSplits(input_data_splits, /*snapshot_id=*/6);
+ ASSERT_OK_AND_ASSIGN(auto batch_reader,
table_read->CreateReader(data_splits));
+ ASSERT_OK_AND_ASSIGN(auto result_array,
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ auto fields_with_row_kind = read_fields;
+ fields_with_row_kind.insert(fields_with_row_kind.begin(),
SpecialFields::ValueKind());
+ std::shared_ptr<arrow::DataType> arrow_data_type =
+ DataField::ConvertDataFieldsToArrowStructType(fields_with_row_kind);
+
+ // "Paul" is the only row in partition key0 = 0 whose e is not null and
matches e < 510.
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow_data_type, {R"([
+ [0, 1, "Bob", 22, 24, null, 26, 1, null],
+ [0, 1, "Emily", 32, 34, null, 36, 1, null],
+ [0, 1, "David", 62, 64, null, 66, 1, null],
+ [0, 1, "Whether I shall turn out to be the hero of my own life.", 72,
74, null, 76, 1, null],
+ [0, 1, "Paul", 502, 504, 508, 506, 0, 509]
+])"},
+
&expected_array);
+ ASSERT_TRUE(array_status.ok());
+ ASSERT_TRUE(result_array->Equals(*expected_array)) <<
result_array->ToString();
+}
+
TEST_P(ReadInteTest,
TestPkReadSnapshot6WithSchemaEvolutionWithPredicateFilter) {
std::vector<DataField> read_fields = {DataField(1, arrow::field("key1",
arrow::int32())),
DataField(7, arrow::field("k",
arrow::utf8())),
diff --git a/test/inte/read_inte_with_index_test.cpp
b/test/inte/read_inte_with_index_test.cpp
index f1316c6c..7a443973 100644
--- a/test/inte/read_inte_with_index_test.cpp
+++ b/test/inte/read_inte_with_index_test.cpp
@@ -84,7 +84,8 @@ class ReadInteWithIndexTest : public testing::Test,
ReadContextBuilder context_builder(table_path);
context_builder.AddOption("read.batch-size", "2")
.AddOption("test.enable-adaptive-prefetch-strategy", "false")
- .SetPredicate(predicate);
+ .SetPredicate(predicate)
+ .EnableLateMaterializing(false);
if (enable_prefetch) {
context_builder.EnablePrefetch(true).SetPrefetchBatchCount(3);
}
@@ -1232,6 +1233,74 @@ TEST_P(ReadInteWithIndexTest,
TestNoEmbeddingBitmapIndex) {
CheckResultForBitmapWithSingleRowGroup(path, arrow_data_type, split);
}
+TEST_P(ReadInteWithIndexTest, TestBitmapIndexWithLateMaterializing) {
+ auto [file_format, enable_prefetch] = GetParam();
+ std::string path = GetDataDir() + "/" + file_format +
+
"/append_with_bitmap_no_embedding.db/append_with_bitmap_no_embedding/";
+ std::string file_name;
+ if (file_format == "orc") {
+ file_name = "data-414509f5-e40c-4245-b992-bbf486778ac9-0.orc";
+ } else if (file_format == "parquet") {
+ file_name = "data-783929b2-49d4-4006-a898-194a62e3278d-0.parquet";
+ }
+
+ std::vector<DataField> read_fields = {SpecialFields::ValueKind(),
+ DataField(0, arrow::field("f0",
arrow::utf8())),
+ DataField(1, arrow::field("f1",
arrow::int32())),
+ DataField(2, arrow::field("f2",
arrow::int32())),
+ DataField(3, arrow::field("f3",
arrow::float64()))};
+ std::shared_ptr<arrow::DataType> arrow_data_type =
+ DataField::ConvertDataFieldsToArrowStructType(read_fields);
+
+ auto data_file_meta = std::make_shared<DataFileMeta>(
+ file_name, /*file_size=*/689,
+ /*row_count=*/8, /*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>>({file_name + ".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);
+ DataSplitImpl::Builder builder(BinaryRow::EmptyRow(), /*bucket=*/0,
+ /*bucket_path=*/path + "bucket-0/",
{data_file_meta});
+ ASSERT_OK_AND_ASSIGN(auto split,
+
builder.WithSnapshot(1).IsStreaming(false).RawConvertible(true).Build());
+
+ std::string literal_str = "Bob";
+ auto predicate = PredicateBuilder::Equal(
+ /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
+ Literal(FieldType::STRING, literal_str.data(), literal_str.size()));
+
+ ReadContextBuilder context_builder(path);
+ context_builder.AddOption("read.batch-size", "2")
+ .AddOption("test.enable-adaptive-prefetch-strategy", "false")
+ .SetPredicate(predicate)
+ .EnablePredicateFilter(true)
+ .EnableLateMaterializing(true);
+ if (enable_prefetch) {
+ context_builder.EnablePrefetch(true).SetPrefetchBatchCount(3);
+ }
+ ASSERT_OK_AND_ASSIGN(auto read_context, context_builder.Finish());
+ ASSERT_OK_AND_ASSIGN(auto table_read,
TableRead::Create(std::move(read_context)));
+ ASSERT_OK_AND_ASSIGN(auto batch_reader,
+
table_read->CreateReader(std::vector<std::shared_ptr<Split>>{split}));
+ ASSERT_OK_AND_ASSIGN(auto result_array,
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ // Only the two "Bob" rows match the predicate.
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow_data_type, {R"([
+[0, "Bob", 10, 1, 12.1],
+[0, "Bob", 10, 1, 16.1]
+ ])"},
+
&expected_array);
+ ASSERT_TRUE(array_status.ok());
+ ASSERT_TRUE(result_array->Equals(*expected_array)) <<
result_array->ToString();
+}
+
TEST_P(ReadInteWithIndexTest, TestNoEmbeddingBitmapIndexWithExternalPath) {
auto [file_format, enable_prefetch] = GetParam();
std::string path = GetDataDir() + "/" + file_format +
diff --git a/test/inte/scan_and_read_inte_test.cpp
b/test/inte/scan_and_read_inte_test.cpp
index 96a7c9a1..537a4cb3 100644
--- a/test/inte/scan_and_read_inte_test.cpp
+++ b/test/inte/scan_and_read_inte_test.cpp
@@ -724,7 +724,7 @@ TEST_P(ScanAndReadInteTest,
TestWithPKWithDvBatchScanSnapshot6WithPredicate) {
ReadContextBuilder read_context_builder(table_path);
AddReadOptionsForPrefetch(&read_context_builder);
- read_context_builder.SetPredicate(predicate);
+
read_context_builder.SetPredicate(predicate).EnableLateMaterializing(false);
ASSERT_OK_AND_ASSIGN(auto read_context, read_context_builder.Finish());
ASSERT_OK_AND_ASSIGN(auto table_read,
TableRead::Create(std::move(read_context)));
@@ -744,6 +744,45 @@ TEST_P(ScanAndReadInteTest,
TestWithPKWithDvBatchScanSnapshot6WithPredicate) {
ASSERT_TRUE(expected->Equals(read_result)) << read_result->ToString();
}
+TEST_P(ScanAndReadInteTest,
TestWithPKWithDvBatchScanSnapshot6WithLateMaterializing) {
+ auto file_format = FileFormat();
+ std::string table_path = paimon::test::GetDataDir() + file_format +
+
"/pk_table_scan_and_read_dv.db/pk_table_scan_and_read_dv/";
+ ScanContextBuilder scan_context_builder(table_path);
+ scan_context_builder.AddOption(Options::SCAN_SNAPSHOT_ID, "6");
+
+ std::string literal_str = "Alice";
+ auto not_equal = PredicateBuilder::NotEqual(
+ /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
+ Literal(FieldType::STRING, literal_str.data(), literal_str.size()));
+ auto greater_than = PredicateBuilder::GreaterThan(/*field_index=*/3,
/*field_name=*/"f3",
+ FieldType::DOUBLE,
Literal(18.0));
+ ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::And({not_equal,
greater_than}));
+ scan_context_builder.SetPredicate(predicate);
+ ASSERT_OK_AND_ASSIGN(auto scan_context,
FinishScanContext(scan_context_builder));
+ ASSERT_OK_AND_ASSIGN(auto table_scan,
TableScan::Create(std::move(scan_context)));
+
+ ReadContextBuilder read_context_builder(table_path);
+ AddReadOptionsForPrefetch(&read_context_builder);
+ read_context_builder.SetPredicate(predicate).EnableLateMaterializing(true);
+ ASSERT_OK_AND_ASSIGN(auto read_context, read_context_builder.Finish());
+ ASSERT_OK_AND_ASSIGN(auto table_read,
TableRead::Create(std::move(read_context)));
+
+ ASSERT_OK_AND_ASSIGN(auto result_plan, table_scan->CreatePlan());
+ ASSERT_EQ(result_plan->SnapshotId().value(), 6);
+ ASSERT_OK_AND_ASSIGN(auto batch_reader,
table_read->CreateReader(result_plan->Splits()));
+ ASSERT_OK_AND_ASSIGN(auto read_result,
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ // check result: "Lucy" (f3 = 14.1) does not match f3 > 18 and is filtered
out.
+ auto expected = std::make_shared<arrow::ChunkedArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type_, R"([
+[0, "Paul", 20, 1, 18.1]
+ ])")
+ .ValueOrDie());
+ ASSERT_TRUE(expected);
+ ASSERT_TRUE(expected->Equals(read_result)) << read_result->ToString();
+}
+
TEST_P(ScanAndReadInteTest, TestWithPKWithDvBatchScanSnapshot4WithPredicate) {
auto file_format = FileFormat();
std::string table_path = paimon::test::GetDataDir() + file_format +
@@ -1251,7 +1290,7 @@ TEST_P(ScanAndReadInteTest,
TestWithPKWithMorBatchScanSnapshot5WithPredicate) {
ReadContextBuilder read_context_builder(table_path);
AddReadOptionsForPrefetch(&read_context_builder);
- read_context_builder.SetPredicate(predicate);
+
read_context_builder.SetPredicate(predicate).EnableLateMaterializing(false);
ASSERT_OK_AND_ASSIGN(auto read_context, read_context_builder.Finish());
ASSERT_OK_AND_ASSIGN(auto table_read,
TableRead::Create(std::move(read_context)));
@@ -1279,6 +1318,55 @@ TEST_P(ScanAndReadInteTest,
TestWithPKWithMorBatchScanSnapshot5WithPredicate) {
ASSERT_TRUE(expected->Equals(read_result)) << read_result->ToString();
}
+// Same coverage as the deletion-vector case above, for the merge-on-read path
where only the
+// key part of the predicate is pushed down into the data files.
+TEST_P(ScanAndReadInteTest,
TestWithPKWithMorBatchScanSnapshot5WithLateMaterializing) {
+ auto file_format = FileFormat();
+ std::string table_path = paimon::test::GetDataDir() + file_format +
+
"/pk_table_scan_and_read_mor.db/pk_table_scan_and_read_mor/";
+
+ ScanContextBuilder scan_context_builder(table_path);
+ scan_context_builder.AddOption(Options::SCAN_SNAPSHOT_ID, "5");
+
+ std::string literal_str = "Alice";
+ auto not_equal = PredicateBuilder::NotEqual(
+ /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
+ Literal(FieldType::STRING, literal_str.data(), literal_str.size()));
+ std::string literal_str2 = "Lucy";
+ auto less_than = PredicateBuilder::LessThan(
+ /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
+ Literal(FieldType::STRING, literal_str2.data(), literal_str2.size()));
+ auto less_or_equal = PredicateBuilder::LessOrEqual(/*field_index=*/3,
/*field_name=*/"f3",
+ FieldType::DOUBLE,
Literal(30.0));
+ ASSERT_OK_AND_ASSIGN(auto predicate,
+ PredicateBuilder::And({not_equal, less_than,
less_or_equal}));
+ scan_context_builder.SetPredicate(predicate);
+ ASSERT_OK_AND_ASSIGN(auto scan_context,
FinishScanContext(scan_context_builder));
+ ASSERT_OK_AND_ASSIGN(auto table_scan,
TableScan::Create(std::move(scan_context)));
+
+ ReadContextBuilder read_context_builder(table_path);
+ AddReadOptionsForPrefetch(&read_context_builder);
+ read_context_builder.SetPredicate(predicate).EnableLateMaterializing(true);
+ ASSERT_OK_AND_ASSIGN(auto read_context, read_context_builder.Finish());
+ ASSERT_OK_AND_ASSIGN(auto table_read,
TableRead::Create(std::move(read_context)));
+
+ ASSERT_OK_AND_ASSIGN(auto result_plan, table_scan->CreatePlan());
+ ASSERT_EQ(result_plan->SnapshotId().value(), 5);
+ ASSERT_OK_AND_ASSIGN(auto batch_reader,
table_read->CreateReader(result_plan->Splits()));
+ ASSERT_OK_AND_ASSIGN(auto read_result,
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ // check result: only the rows before "Lucy" with f3 <= 30.0 remain.
+ auto expected = std::make_shared<arrow::ChunkedArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type_, R"([
+[0, "Bob", 10, 0, 12.1],
+[0, "David", 10, 0, 17.1],
+[0, "Emily", 10, 0, 13.1]
+ ])")
+ .ValueOrDie());
+ ASSERT_TRUE(expected);
+ ASSERT_TRUE(expected->Equals(read_result)) << read_result->ToString();
+}
+
TEST_P(ScanAndReadInteTest, TestWithPKWithMorBatchScanSnapshot3WithPredicate) {
auto file_format = FileFormat();
std::string table_path = paimon::test::GetDataDir() + file_format +