Skip to content

Commit 7ff1855

Browse files
authored
feat(read): support file index and predicate pushdown for data evolution (#215)
1 parent 2b7b983 commit 7ff1855

4 files changed

Lines changed: 358 additions & 44 deletions

File tree

src/paimon/core/operation/data_evolution_split_read.cpp

Lines changed: 131 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <cassert>
2323
#include <limits>
2424
#include <map>
25+
#include <set>
2526
#include <string>
2627
#include <string_view>
2728
#include <thread>
@@ -53,8 +54,13 @@
5354
#include "paimon/core/core_options.h"
5455
#include "paimon/core/deletionvectors/apply_deletion_vector_batch_reader.h"
5556
#include "paimon/core/global_index/indexed_split_impl.h"
57+
#include "paimon/core/io/file_index_evaluator.h"
5658
#include "paimon/core/utils/blob_view_lookup.h"
5759
#include "paimon/core/utils/data_evolution_utils.h"
60+
#include "paimon/core/utils/field_mapping.h"
61+
#include "paimon/file_index/bitmap_index_result.h"
62+
#include "paimon/file_index/file_index_result.h"
63+
#include "paimon/predicate/predicate_utils.h"
5864

5965
namespace paimon {
6066
namespace {
@@ -389,12 +395,22 @@ Result<std::unique_ptr<BatchReader>> DataEvolutionSplitRead::InnerCreateReader(
389395
path_factory_->CreateDataFilePathFactory(split_impl->Partition(), split_impl->Bucket()));
390396
auto metas = split_impl->DataFiles();
391397
DeletionVector::Factory split_dv_factory = CreateSplitDvFactory(*split_impl);
398+
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Predicate> push_down_predicate,
399+
CreatePushDownPredicate(context_->GetPredicate(), raw_read_schema_));
392400

393401
PAIMON_ASSIGN_OR_RAISE(std::vector<std::vector<std::shared_ptr<DataFileMeta>>> split_by_row_id,
394402
MergeRangesAndSort(std::move(metas)));
395403

396404
std::vector<std::unique_ptr<BatchReader>> sub_readers;
397405
for (const std::vector<std::shared_ptr<DataFileMeta>>& need_merge_files : split_by_row_id) {
406+
if (need_merge_files.size() > 1) {
407+
PAIMON_ASSIGN_OR_RAISE(
408+
bool skip_group,
409+
SkipByFileIndex(push_down_predicate, need_merge_files, data_file_path_factory));
410+
if (skip_group) {
411+
continue;
412+
}
413+
}
398414
PAIMON_ASSIGN_OR_RAISE(std::optional<GroupDeletionVector> group_dv,
399415
ReadGroupDeletionVector(need_merge_files, split_dv_factory));
400416
PAIMON_ASSIGN_OR_RAISE(DeletionVector::Factory group_dv_factory,
@@ -404,10 +420,15 @@ Result<std::unique_ptr<BatchReader>> DataEvolutionSplitRead::InnerCreateReader(
404420
PAIMON_ASSIGN_OR_RAISE(
405421
std::vector<std::unique_ptr<FileBatchReader>> raw_file_readers,
406422
CreateRawFileReaders(split_impl->Partition(), need_merge_files, raw_read_schema_,
407-
/*predicate=*/nullptr, group_dv_factory, row_ranges,
423+
push_down_predicate, group_dv_factory, row_ranges,
408424
data_file_path_factory,
409425
/*extra_format_options=*/{}));
410-
assert(raw_file_readers.size() == 1);
426+
if (raw_file_readers.empty()) {
427+
continue;
428+
}
429+
if (raw_file_readers.size() != 1) {
430+
return Status::Invalid("Single-file data evolution group created multiple readers");
431+
}
411432
sub_readers.push_back(std::move(raw_file_readers[0]));
412433
} else {
413434
PAIMON_ASSIGN_OR_RAISE(
@@ -424,17 +445,110 @@ Result<std::unique_ptr<BatchReader>> DataEvolutionSplitRead::InnerCreateReader(
424445
return std::make_unique<CompleteRowKindBatchReader>(std::move(batch_reader), pool_);
425446
}
426447

448+
Result<std::shared_ptr<Predicate>> DataEvolutionSplitRead::CreatePushDownPredicate(
449+
const std::shared_ptr<Predicate>& predicate,
450+
const std::shared_ptr<arrow::Schema>& read_schema) {
451+
std::map<std::string, int32_t> picked_field_name_to_idx;
452+
for (int32_t i = 0; i < read_schema->num_fields(); ++i) {
453+
const std::string& field_name = read_schema->field(i)->name();
454+
if (!SpecialFields::IsSystemField(field_name)) {
455+
picked_field_name_to_idx.emplace(field_name, i);
456+
}
457+
}
458+
return PredicateUtils::CreatePickedFieldFilter(predicate, picked_field_name_to_idx);
459+
}
460+
461+
Result<bool> DataEvolutionSplitRead::SkipByFileIndex(
462+
const std::shared_ptr<Predicate>& predicate,
463+
const std::vector<std::shared_ptr<DataFileMeta>>& files,
464+
const std::shared_ptr<DataFilePathFactory>& data_file_path_factory) const {
465+
if (!options_.FileIndexReadEnabled() || !predicate) {
466+
return false;
467+
}
468+
469+
PAIMON_ASSIGN_OR_RAISE(
470+
std::unique_ptr<FieldMappingBuilder> field_mapping_builder,
471+
FieldMappingBuilder::Create(raw_read_schema_, context_->GetPartitionKeys(), predicate));
472+
std::set<int32_t> claimed_field_ids;
473+
for (const auto& file : files) {
474+
// Blob and vector-store files may cover only part of the row range, so their indexes
475+
// cannot prove that the complete merged group misses the predicate.
476+
if (!DataEvolutionUtils::IsNormalFile(file->file_name)) {
477+
continue;
478+
}
479+
480+
std::shared_ptr<TableSchema> data_schema = context_->GetTableSchema();
481+
if (file->schema_id != data_schema->Id()) {
482+
PAIMON_ASSIGN_OR_RAISE(data_schema, schema_manager_->ReadSchema(file->schema_id));
483+
}
484+
std::vector<DataField> written_fields;
485+
if (file->write_cols) {
486+
std::vector<std::string> data_write_cols;
487+
data_write_cols.reserve(file->write_cols->size());
488+
for (const auto& write_col : file->write_cols.value()) {
489+
if (!SpecialFields::IsSystemField(write_col)) {
490+
data_write_cols.push_back(write_col);
491+
}
492+
}
493+
PAIMON_ASSIGN_OR_RAISE(written_fields, data_schema->GetFields(data_write_cols));
494+
} else {
495+
written_fields = data_schema->Fields();
496+
}
497+
498+
std::set<std::string> overwritten_field_names;
499+
for (const auto& field : written_fields) {
500+
if (!claimed_field_ids.insert(field.Id()).second) {
501+
overwritten_field_names.insert(field.Name());
502+
}
503+
}
504+
505+
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FieldMapping> field_mapping,
506+
field_mapping_builder->CreateFieldMapping(written_fields));
507+
std::shared_ptr<Predicate> data_predicate =
508+
field_mapping->non_partition_info.non_partition_filter;
509+
if (!overwritten_field_names.empty()) {
510+
PAIMON_ASSIGN_OR_RAISE(data_predicate, PredicateUtils::ExcludePredicateWithFields(
511+
data_predicate, overwritten_field_names));
512+
}
513+
if (!data_predicate) {
514+
continue;
515+
}
516+
517+
auto written_schema = DataField::ConvertDataFieldsToArrowSchema(written_fields);
518+
PAIMON_ASSIGN_OR_RAISE(
519+
std::shared_ptr<FileIndexResult> index_result,
520+
FileIndexEvaluator::Evaluate(written_schema, data_predicate, data_file_path_factory,
521+
file, options_.GetFileSystem(), pool_));
522+
PAIMON_ASSIGN_OR_RAISE(bool is_remain, index_result->IsRemain());
523+
if (!is_remain) {
524+
return true;
525+
}
526+
}
527+
return false;
528+
}
529+
427530
Result<std::unique_ptr<FileBatchReader>> DataEvolutionSplitRead::ApplyIndexAndDvReaderIfNeeded(
428531
std::unique_ptr<FileBatchReader>&& file_reader, const std::shared_ptr<DataFileMeta>& file,
429532
const std::shared_ptr<arrow::Schema>& data_schema,
430533
const std::shared_ptr<arrow::Schema>& read_schema, const std::shared_ptr<Predicate>& predicate,
431534
DeletionVector::Factory dv_factory, const std::optional<std::vector<Range>>& row_ranges,
432535
const std::shared_ptr<DataFilePathFactory>& data_file_path_factory) const {
433-
if (predicate) {
434-
assert(false);
435-
// as DataEvolutionSplitRead will skip predicate
436-
return Status::Invalid("DataEvolutionSplitRead do not support predicate");
536+
std::shared_ptr<FileIndexResult> file_index_result;
537+
if (options_.FileIndexReadEnabled()) {
538+
PAIMON_ASSIGN_OR_RAISE(
539+
file_index_result,
540+
FileIndexEvaluator::Evaluate(data_schema, predicate, data_file_path_factory, file,
541+
options_.GetFileSystem(), pool_));
542+
PAIMON_ASSIGN_OR_RAISE(bool is_remain, file_index_result->IsRemain());
543+
if (!is_remain) {
544+
return std::unique_ptr<FileBatchReader>();
545+
}
546+
}
547+
const RoaringBitmap32* index_selection = nullptr;
548+
if (auto* bitmap_index = dynamic_cast<BitmapIndexResult*>(file_index_result.get())) {
549+
PAIMON_ASSIGN_OR_RAISE(index_selection, bitmap_index->GetBitmap());
437550
}
551+
438552
// the factory is per row range group and already returns a view taking file-local positions.
439553
// Unlike RawFileSplitRead the vector is not folded into the format reader's selection: it is
440554
// no BitmapDeletionVector, and the blob fallback path's gap segments have no format reader.
@@ -444,10 +558,19 @@ Result<std::unique_ptr<FileBatchReader>> DataEvolutionSplitRead::ApplyIndexAndDv
444558
}
445559
PAIMON_ASSIGN_OR_RAISE(std::optional<RoaringBitmap32> selection_row_ids,
446560
file->ToFileSelection(row_ranges));
561+
if (index_selection) {
562+
if (selection_row_ids) {
563+
selection_row_ids.value() &= *index_selection;
564+
} else {
565+
selection_row_ids = *index_selection;
566+
}
567+
}
568+
if (selection_row_ids && selection_row_ids->IsEmpty()) {
569+
return std::unique_ptr<FileBatchReader>();
570+
}
447571
::ArrowSchema c_read_schema;
448572
PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*read_schema, &c_read_schema));
449-
PAIMON_RETURN_NOT_OK(
450-
file_reader->SetReadSchema(&c_read_schema, /*predicate=*/nullptr, selection_row_ids));
573+
PAIMON_RETURN_NOT_OK(file_reader->SetReadSchema(&c_read_schema, predicate, selection_row_ids));
451574

452575
std::unique_ptr<FileBatchReader> reader;
453576
if (!file_reader->SupportPreciseBitmapSelection() && selection_row_ids) {

src/paimon/core/operation/data_evolution_split_read.h

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,10 @@ struct DeletionFile;
6868
/// ->(DelegatingPrefetchReader)->(PrefetchFileBatchReader)->FormatReader
6969
///
7070
///
71-
/// A union `SplitRead` to read multiple inner files to merge columns, note that this class
72-
/// does not support filtering push down: a predicate would have to be evaluated consistently
73-
/// across the files being merged, which is not implemented here.
71+
/// A union `SplitRead` to read multiple inner files to merge columns. A single-file row range
72+
/// group gets both file-index and format-level predicate pushdown. A merged group only uses file
73+
/// indexes to skip the whole group: filtering its child readers independently would break their
74+
/// positional alignment.
7475
///
7576
/// Deletion vectors are supported: a row range group's vector is maintained against the
7677
/// group's anchor file (DataEvolutionUtils::RetrieveAnchorFile), so its positions are
@@ -172,6 +173,21 @@ class DataEvolutionSplitRead : public AbstractSplitRead {
172173
const std::shared_ptr<DataSplit>& data_split,
173174
const std::optional<std::vector<Range>>& row_ranges) const;
174175

176+
/// Keeps top-level conjuncts whose fields all belong to `read_schema`, excluding conjuncts
177+
/// over system fields. The returned predicate is for pushdown only; the original predicate is
178+
/// still evaluated as a residual filter when requested by the read context.
179+
static Result<std::shared_ptr<Predicate>> CreatePushDownPredicate(
180+
const std::shared_ptr<Predicate>& predicate,
181+
const std::shared_ptr<arrow::Schema>& read_schema);
182+
183+
/// Returns true when file indexes prove that no row in a merged row range group can match.
184+
/// Only normal files are considered, and an older copy of a field is excluded after a newer
185+
/// file has claimed the same field id.
186+
Result<bool> SkipByFileIndex(
187+
const std::shared_ptr<Predicate>& predicate,
188+
const std::vector<std::shared_ptr<DataFileMeta>>& files,
189+
const std::shared_ptr<DataFilePathFactory>& data_file_path_factory) const;
190+
175191
/// Builds the deletion vector factory over the split's deletion files, keyed by data file
176192
/// name. Only anchor files carry one. Returns a null factory when the split has none.
177193
DeletionVector::Factory CreateSplitDvFactory(const DataSplitImpl& split_impl) const;

src/paimon/core/operation/data_evolution_split_read_test.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
#include "gtest/gtest.h"
2727
#include "paimon/common/data/binary_row.h"
28+
#include "paimon/common/table/special_fields.h"
2829
#include "paimon/core/deletionvectors/bitmap_deletion_vector.h"
2930
#include "paimon/core/io/data_file_meta.h"
3031
#include "paimon/core/manifest/file_source.h"
@@ -37,6 +38,8 @@
3738
#include "paimon/executor.h"
3839
#include "paimon/fs/local/local_file_system.h"
3940
#include "paimon/memory/memory_pool.h"
41+
#include "paimon/predicate/literal.h"
42+
#include "paimon/predicate/predicate_builder.h"
4043
#include "paimon/read_context.h"
4144
#include "paimon/status.h"
4245
#include "paimon/testing/utils/testharness.h"
@@ -102,6 +105,30 @@ class DataEvolutionSplitReadTest : public ::testing::Test {
102105
std::shared_ptr<MemoryPool> pool_ = GetDefaultPool();
103106
};
104107

108+
TEST_F(DataEvolutionSplitReadTest, TestCreatePushDownPredicate) {
109+
auto f0_predicate =
110+
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::INT, Literal(1));
111+
auto f1_predicate =
112+
PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1", FieldType::INT, Literal(2));
113+
auto row_id_predicate = PredicateBuilder::Equal(
114+
/*field_index=*/2, SpecialFields::RowId().Name(), FieldType::BIGINT, Literal(3l));
115+
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> predicate,
116+
PredicateBuilder::And({f0_predicate, f1_predicate, row_id_predicate}));
117+
118+
auto read_schema = DataField::ConvertDataFieldsToArrowSchema(
119+
{DataField(0, arrow::field("f0", arrow::int32())), SpecialFields::RowId()});
120+
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> push_down,
121+
DataEvolutionSplitRead::CreatePushDownPredicate(predicate, read_schema));
122+
ASSERT_TRUE(push_down);
123+
ASSERT_EQ(*push_down, *f0_predicate);
124+
125+
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> or_predicate,
126+
PredicateBuilder::Or({f0_predicate, f1_predicate}));
127+
ASSERT_OK_AND_ASSIGN(
128+
push_down, DataEvolutionSplitRead::CreatePushDownPredicate(or_predicate, read_schema));
129+
ASSERT_FALSE(push_down);
130+
}
131+
105132
TEST_F(DataEvolutionSplitReadTest, TestAddSingleBlobEntry) {
106133
auto blob_entry =
107134
CreateBlobFile("blob1", /*first_row_id=*/0, /*row_count=*/100,

0 commit comments

Comments
 (0)