Skip to content

Commit f634436

Browse files
committed
refactor(manifest): reuse late materialization for bucket filtering
1 parent 46d0d85 commit f634436

11 files changed

Lines changed: 449 additions & 212 deletions

‎src/paimon/common/reader/late_materializing_file_batch_reader.cpp‎

Lines changed: 134 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "paimon/common/reader/late_materializing_file_batch_reader.h"
2121

2222
#include <cassert>
23+
#include <limits>
2324
#include <map>
2425
#include <set>
2526
#include <string>
@@ -45,28 +46,50 @@
4546
namespace paimon {
4647

4748
Result<std::unique_ptr<LateMaterializingFileBatchReader>> LateMaterializingFileBatchReader::Create(
48-
std::unique_ptr<FileBatchReader> inner, const std::shared_ptr<arrow::MemoryPool>& arrow_pool) {
49+
std::unique_ptr<FileBatchReader> inner, const std::shared_ptr<arrow::MemoryPool>& arrow_pool,
50+
std::optional<ProbeValidation> validation) {
4951
if (arrow_pool == nullptr) {
5052
return Status::Invalid("arrow pool could not be nullptr.");
5153
}
5254
if (inner == nullptr) {
5355
return Status::Invalid("inner could not be nullptr.");
5456
}
57+
if (validation && (!validation->validate || validation->fields.empty())) {
58+
return Status::Invalid("probe validation requires fields and a callback");
59+
}
5560
auto* prefetch_inner = dynamic_cast<PrefetchFileBatchReader*>(inner.get());
5661
auto reader = std::unique_ptr<LateMaterializingFileBatchReader>(
5762
new LateMaterializingFileBatchReader(std::move(inner), prefetch_inner, arrow_pool));
63+
reader->validation_ = std::move(validation);
5864
return reader;
5965
}
6066

6167
Result<FileBatchReader::ReadBatch> LateMaterializingFileBatchReader::NextBatch() {
68+
if (state_ == kInit && validation_) {
69+
return Status::Invalid("SetReadSchema is required before probe validation");
70+
}
6271
if (state_ == kInit) {
6372
// SetReadSchema has not been called: read with the file schema, matching the
6473
// FileBatchReader contract for schema-less reads.
6574
state_ = kNoLatMat;
6675
}
6776
if (state_ == kProbing) {
6877
PAIMON_RETURN_NOT_OK(ReadAndFilterProbeData());
69-
if (matched_bitmap_.IsEmpty()) {
78+
if (!row_ids_fit_) {
79+
// Rebind to full batches and apply the residual predicate without a file bitmap.
80+
if (predicate_) {
81+
std::map<std::string, int32_t> name_to_idx;
82+
for (int32_t i = 0; i < full_schema_->num_fields(); ++i) {
83+
name_to_idx.emplace(full_schema_->field(i)->name(), i);
84+
}
85+
PAIMON_ASSIGN_OR_RAISE(
86+
std::shared_ptr<Predicate> bound,
87+
PredicateUtils::CreatePickedFieldFilter(predicate_, name_to_idx));
88+
probe_filter_ = std::dynamic_pointer_cast<PredicateFilter>(bound);
89+
}
90+
PAIMON_RETURN_NOT_OK(SetInnerReadSchema(full_schema_, nullptr, std::nullopt));
91+
state_ = kFiltering;
92+
} else if (matched_bitmap_.IsEmpty()) {
7093
state_ = kEOF;
7194
} else {
7295
// payload pass reads only the matched rows (matched_bitmap_ is non-empty here).
@@ -78,6 +101,8 @@ Result<FileBatchReader::ReadBatch> LateMaterializingFileBatchReader::NextBatch()
78101

79102
if (state_ == kNoLatMat) {
80103
return inner_->NextBatch();
104+
} else if (state_ == kFiltering) {
105+
return ReadFilteredBatch();
81106
} else if (state_ == kRunning) {
82107
return ReadPayloadBatch();
83108
} else if (state_ == kEOF) {
@@ -90,8 +115,10 @@ Result<RoaringBitmap32> LateMaterializingFileBatchReader::FilterProbeBatch(
90115
const std::shared_ptr<arrow::Array>& array,
91116
const std::shared_ptr<PredicateFilter>& bound_filter) {
92117
// TODO(zhouhonfeng.zhf): use arrow::compute::Filter instead of PredicateFilter
93-
PAIMON_ASSIGN_OR_RAISE(std::vector<char> results,
94-
bound_filter->Test(*array, arrow_pool_.get()));
118+
std::vector<char> results(array->length(), true);
119+
if (bound_filter) {
120+
PAIMON_ASSIGN_OR_RAISE(results, bound_filter->Test(*array, arrow_pool_.get()));
121+
}
95122
if (results.size() != static_cast<size_t>(array->length())) {
96123
return Status::Invalid(
97124
fmt::format("predicate result size {} does not match probe batch length {}",
@@ -106,7 +133,12 @@ Result<RoaringBitmap32> LateMaterializingFileBatchReader::FilterProbeBatch(
106133
// map batch offset to file row id
107134
PAIMON_ASSIGN_OR_RAISE(uint64_t file_row,
108135
inner_->GetPreviousBatchFileRowId(static_cast<uint64_t>(i)));
109-
if (selection_ && !selection_->Contains(static_cast<int32_t>(file_row))) {
136+
if (selection_ && (file_row > std::numeric_limits<uint32_t>::max() ||
137+
!selection_->Contains(static_cast<uint32_t>(file_row)))) {
138+
continue;
139+
}
140+
if (file_row > std::numeric_limits<uint32_t>::max()) {
141+
row_ids_fit_ = false;
110142
continue;
111143
}
112144
batch_matched.Add(static_cast<uint32_t>(i));
@@ -127,8 +159,20 @@ Status LateMaterializingFileBatchReader::ReadAndFilterProbeData() {
127159
auto& [c_array, c_schema] = batch;
128160
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
129161
arrow::ImportArray(c_array.get(), c_schema.get()));
162+
if (validation_) {
163+
if (!array || array->type_id() != arrow::Type::STRUCT) {
164+
return Status::Invalid("probe validation requires a struct array");
165+
}
166+
PAIMON_RETURN_NOT_OK(
167+
validation_->validate(std::static_pointer_cast<arrow::StructArray>(array)));
168+
}
130169
PAIMON_ASSIGN_OR_RAISE(RoaringBitmap32 batch_matched,
131170
FilterProbeBatch(array, probe_filter_));
171+
if (!row_ids_fit_) {
172+
probe_arrays.clear();
173+
matched_bitmap_ = RoaringBitmap32();
174+
continue;
175+
}
132176
// Compact each probe batch down to its matched rows so probe_data_ aligns row-for-row
133177
// (ascending file order) with matched_bitmap_ and the later payload output.
134178
if (!batch_matched.IsEmpty()) {
@@ -151,6 +195,53 @@ Status LateMaterializingFileBatchReader::ReadAndFilterProbeData() {
151195
return Status::OK();
152196
}
153197

198+
Result<FileBatchReader::ReadBatch> LateMaterializingFileBatchReader::ReadFilteredBatch() {
199+
while (true) {
200+
PAIMON_ASSIGN_OR_RAISE(FileBatchReader::ReadBatch batch, inner_->NextBatch());
201+
if (BatchReader::IsEofBatch(batch)) {
202+
state_ = kEOF;
203+
return batch;
204+
}
205+
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
206+
std::shared_ptr<arrow::Array> array,
207+
arrow::ImportArray(batch.first.get(), batch.second.get()));
208+
std::vector<char> results(array->length(), true);
209+
if (probe_filter_) {
210+
PAIMON_ASSIGN_OR_RAISE(results, probe_filter_->Test(*array, arrow_pool_.get()));
211+
}
212+
if (results.size() != static_cast<size_t>(array->length())) {
213+
return Status::Invalid("predicate result size does not match full batch length");
214+
}
215+
RoaringBitmap32 selected;
216+
row_mapping_.clear();
217+
for (int64_t i = 0; i < array->length(); ++i) {
218+
if (!results[i]) {
219+
continue;
220+
}
221+
PAIMON_ASSIGN_OR_RAISE(uint64_t file_row, inner_->GetPreviousBatchFileRowId(i));
222+
if (selection_ && (file_row > std::numeric_limits<uint32_t>::max() ||
223+
!selection_->Contains(static_cast<uint32_t>(file_row)))) {
224+
continue;
225+
}
226+
selected.Add(static_cast<uint32_t>(i));
227+
row_mapping_.push_back(file_row);
228+
}
229+
if (selected.IsEmpty()) {
230+
continue;
231+
}
232+
PAIMON_ASSIGN_OR_RAISE(arrow::ArrayVector slices,
233+
ReaderUtils::GenerateFilteredArrayVector(array, selected));
234+
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> filtered,
235+
arrow::Concatenate(slices, arrow_pool_.get()));
236+
auto c_array = std::make_unique<ArrowArray>();
237+
auto c_schema = std::make_unique<ArrowSchema>();
238+
PAIMON_RETURN_NOT_OK_FROM_ARROW(
239+
arrow::ExportArray(*filtered, c_array.get(), c_schema.get()));
240+
PAIMON_RETURN_NOT_OK(AddArrowArrayLifetime(c_array.get(), c_schema.get(), arrow_pool_));
241+
return std::make_pair(std::move(c_array), std::move(c_schema));
242+
}
243+
}
244+
154245
Result<FileBatchReader::ReadBatch> LateMaterializingFileBatchReader::ReadPayloadBatch() {
155246
while (true) {
156247
PAIMON_ASSIGN_OR_RAISE(FileBatchReader::ReadBatchWithBitmap batch_with_bitmap,
@@ -258,9 +349,19 @@ Status LateMaterializingFileBatchReader::SetReadSchema(
258349
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(full_schema_, arrow::ImportSchema(read_schema));
259350
predicate_ = predicate;
260351
selection_ = selection_bitmap;
261-
if (predicate_ != nullptr) {
352+
if (predicate_ != nullptr || validation_) {
262353
std::set<std::string> probe_names;
263-
PAIMON_RETURN_NOT_OK(PredicateUtils::GetAllNames(predicate_, &probe_names));
354+
if (predicate_) {
355+
PAIMON_RETURN_NOT_OK(PredicateUtils::GetAllNames(predicate_, &probe_names));
356+
}
357+
if (validation_) {
358+
for (const auto& name : validation_->fields) {
359+
if (!full_schema_->GetFieldByName(name)) {
360+
return Status::Invalid("probe validation field missing: ", name);
361+
}
362+
probe_names.insert(name);
363+
}
364+
}
264365
arrow::FieldVector probe_fields;
265366
arrow::FieldVector payload_fields;
266367
for (const auto& field : full_schema_->fields()) {
@@ -271,30 +372,36 @@ Status LateMaterializingFileBatchReader::SetReadSchema(
271372
}
272373
}
273374
// probing only pays off when the predicate fields are a strict subset of the read schema
274-
if (!probe_fields.empty() && !payload_fields.empty()) {
375+
if (!probe_fields.empty() && (!payload_fields.empty() || validation_)) {
376+
if (payload_fields.empty()) {
377+
payload_fields = full_schema_->fields();
378+
}
275379
probe_schema_ = arrow::schema(probe_fields, full_schema_->metadata());
276380
payload_schema_ = arrow::schema(payload_fields, full_schema_->metadata());
277-
PAIMON_RETURN_NOT_OK(PredicateValidator::ValidatePredicateWithSchema(
278-
*probe_schema_, predicate_, /*validate_field_idx=*/false));
279-
std::map<std::string, int32_t> name_to_idx;
280-
for (int32_t i = 0; i < probe_schema_->num_fields(); ++i) {
281-
name_to_idx.emplace(probe_schema_->field(i)->name(), i);
282-
}
283-
PAIMON_ASSIGN_OR_RAISE(
284-
std::shared_ptr<Predicate> bound_predicate,
285-
PredicateUtils::CreatePickedFieldFilter(predicate_, name_to_idx));
286-
probe_filter_ = std::dynamic_pointer_cast<PredicateFilter>(bound_predicate);
287-
if (!probe_filter_) {
288-
return Status::Invalid("failed to bind predicate to probe schema");
381+
if (predicate_) {
382+
PAIMON_RETURN_NOT_OK(PredicateValidator::ValidatePredicateWithSchema(
383+
*probe_schema_, predicate_, /*validate_field_idx=*/false));
384+
std::map<std::string, int32_t> name_to_idx;
385+
for (int32_t i = 0; i < probe_schema_->num_fields(); ++i) {
386+
name_to_idx.emplace(probe_schema_->field(i)->name(), i);
387+
}
388+
PAIMON_ASSIGN_OR_RAISE(
389+
std::shared_ptr<Predicate> bound_predicate,
390+
PredicateUtils::CreatePickedFieldFilter(predicate_, name_to_idx));
391+
probe_filter_ = std::dynamic_pointer_cast<PredicateFilter>(bound_predicate);
392+
if (!probe_filter_) {
393+
return Status::Invalid("failed to bind predicate to probe schema");
394+
}
289395
}
290396
}
291397
}
292398

293-
if (predicate_ == nullptr || probe_schema_ == nullptr) {
399+
if (probe_schema_ == nullptr) {
294400
PAIMON_RETURN_NOT_OK(SetInnerReadSchema(full_schema_, predicate_, selection_));
295401
state_ = kNoLatMat;
296402
} else {
297-
PAIMON_RETURN_NOT_OK(SetInnerReadSchema(probe_schema_, predicate_, selection_));
403+
PAIMON_RETURN_NOT_OK(SetInnerReadSchema(probe_schema_, validation_ ? nullptr : predicate_,
404+
validation_ ? std::nullopt : selection_));
298405
state_ = kProbing;
299406
}
300407
return Status::OK();
@@ -319,6 +426,10 @@ Status LateMaterializingFileBatchReader::SeekToRow(uint64_t row_number) {
319426
PAIMON_ASSIGN_OR_RAISE(PrefetchFileBatchReader * prefetch_reader,
320427
GetPrefetchReaderOrRaise("SeekToRow"));
321428
PAIMON_RETURN_NOT_OK(prefetch_reader->SeekToRow(row_number));
429+
if (!row_ids_fit_) {
430+
state_ = kFiltering;
431+
return Status::OK();
432+
}
322433
if (state_ == kRunning || state_ == kEOF) {
323434
if (matched_bitmap_.IsEmpty()) {
324435
state_ = kEOF;
@@ -350,6 +461,7 @@ Status LateMaterializingFileBatchReader::SetReadRanges(
350461

351462
void LateMaterializingFileBatchReader::Reset() {
352463
state_ = kInit;
464+
row_ids_fit_ = true;
353465
matched_bitmap_ = RoaringBitmap32();
354466
probe_data_.reset();
355467
probe_cursor_ = 0;

‎src/paimon/common/reader/late_materializing_file_batch_reader.h‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
#include <arrow/c/abi.h>
2424

2525
#include <cstdint>
26+
#include <functional>
2627
#include <memory>
28+
#include <optional>
29+
#include <string>
2730
#include <string_view>
2831
#include <utility>
2932
#include <vector>
@@ -38,12 +41,20 @@ class PredicateFilter;
3841
// For convenience, we abbreviate `Later Materializing` as `LatMat`.
3942
// This reader is installed below the prefetch layer (see
4043
// AbstractSplitRead::CreateFileBatchReader) and performs probe/payload two-phase reads when a
41-
// predicate is pushed down through SetReadSchema; without a predicate it is a plain passthrough.
44+
// predicate is passed through SetReadSchema. Without a predicate or validation it is a passthrough.
4245
class LateMaterializingFileBatchReader : public PrefetchFileBatchReader {
4346
public:
47+
// Validate every file row before filtering. These fields join the probe projection;
48+
// neither predicates nor selections are pushed into the inner probe reader.
49+
struct ProbeValidation {
50+
std::vector<std::string> fields;
51+
std::function<Status(const std::shared_ptr<arrow::StructArray>&)> validate;
52+
};
53+
4454
static Result<std::unique_ptr<LateMaterializingFileBatchReader>> Create(
4555
std::unique_ptr<FileBatchReader> inner,
46-
const std::shared_ptr<arrow::MemoryPool>& arrow_pool);
56+
const std::shared_ptr<arrow::MemoryPool>& arrow_pool,
57+
std::optional<ProbeValidation> validation = std::nullopt);
4758

4859
Result<FileBatchReader::ReadBatch> NextBatch() override;
4960

@@ -114,9 +125,10 @@ class LateMaterializingFileBatchReader : public PrefetchFileBatchReader {
114125

115126
enum LatMatState {
116127
kInit,
117-
kProbing, // schema is set, probing is in progress
118-
kNoLatMat, // no need to late materialization
119-
kRunning, // Lat-mat is enabled and the payload reader is reading data
128+
kProbing, // schema is set, probing is in progress
129+
kNoLatMat, // no need to late materialization
130+
kFiltering, // full batches with residual filtering when file row IDs exceed the bitmap
131+
kRunning, // Lat-mat is enabled and the payload reader is reading data
120132
kEOF
121133
};
122134

@@ -130,6 +142,7 @@ class LateMaterializingFileBatchReader : public PrefetchFileBatchReader {
130142

131143
/// Read one payload batch with bitmap (matched rows only)
132144
Result<FileBatchReader::ReadBatch> ReadPayloadBatch();
145+
Result<FileBatchReader::ReadBatch> ReadFilteredBatch();
133146

134147
/// Combine the compacted payload columns and the selected probe columns into a single struct
135148
/// array following full_schema_'s field order.
@@ -163,6 +176,7 @@ class LateMaterializingFileBatchReader : public PrefetchFileBatchReader {
163176
/// inner_ is never reassigned, so the cast is resolved once in Create().
164177
PrefetchFileBatchReader* prefetch_inner_ = nullptr;
165178
std::shared_ptr<arrow::MemoryPool> arrow_pool_;
179+
std::optional<ProbeValidation> validation_;
166180
LatMatState state_ = kInit;
167181
std::shared_ptr<arrow::Schema> full_schema_;
168182
// projection holding only the predicate fields; nullptr when probing is not applicable
@@ -176,6 +190,7 @@ class LateMaterializingFileBatchReader : public PrefetchFileBatchReader {
176190
// the probe_data_ is sliced and compacted with the matched_bitmap_
177191
std::shared_ptr<arrow::StructArray> probe_data_;
178192
RoaringBitmap32 matched_bitmap_;
193+
bool row_ids_fit_ = true;
179194
// read cursor into probe_data_ for the payload phase
180195
int64_t probe_cursor_ = 0;
181196
// to support GetPreviousBatchFileRowId

0 commit comments

Comments
 (0)