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

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


The following commit(s) were added to refs/heads/main by this push:
     new 7ff18554 feat(read): support file index and predicate pushdown for 
data evolution (#215)
7ff18554 is described below

commit 7ff18554271c354601d8f30d7ae32db98e11b02c
Author: lxy <[email protected]>
AuthorDate: Wed Aug 19 19:48:34 2026 +0800

    feat(read): support file index and predicate pushdown for data evolution 
(#215)
---
 .../core/operation/data_evolution_split_read.cpp   | 139 ++++++++++++-
 .../core/operation/data_evolution_split_read.h     |  22 ++-
 .../operation/data_evolution_split_read_test.cpp   |  27 +++
 test/inte/data_evolution_table_test.cpp            | 214 +++++++++++++++++----
 4 files changed, 358 insertions(+), 44 deletions(-)

diff --git a/src/paimon/core/operation/data_evolution_split_read.cpp 
b/src/paimon/core/operation/data_evolution_split_read.cpp
index 52a1bb51..9e71fe47 100644
--- a/src/paimon/core/operation/data_evolution_split_read.cpp
+++ b/src/paimon/core/operation/data_evolution_split_read.cpp
@@ -22,6 +22,7 @@
 #include <cassert>
 #include <limits>
 #include <map>
+#include <set>
 #include <string>
 #include <string_view>
 #include <thread>
@@ -53,8 +54,13 @@
 #include "paimon/core/core_options.h"
 #include "paimon/core/deletionvectors/apply_deletion_vector_batch_reader.h"
 #include "paimon/core/global_index/indexed_split_impl.h"
+#include "paimon/core/io/file_index_evaluator.h"
 #include "paimon/core/utils/blob_view_lookup.h"
 #include "paimon/core/utils/data_evolution_utils.h"
+#include "paimon/core/utils/field_mapping.h"
+#include "paimon/file_index/bitmap_index_result.h"
+#include "paimon/file_index/file_index_result.h"
+#include "paimon/predicate/predicate_utils.h"
 
 namespace paimon {
 namespace {
@@ -389,12 +395,22 @@ Result<std::unique_ptr<BatchReader>> 
DataEvolutionSplitRead::InnerCreateReader(
         path_factory_->CreateDataFilePathFactory(split_impl->Partition(), 
split_impl->Bucket()));
     auto metas = split_impl->DataFiles();
     DeletionVector::Factory split_dv_factory = 
CreateSplitDvFactory(*split_impl);
+    PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Predicate> push_down_predicate,
+                           CreatePushDownPredicate(context_->GetPredicate(), 
raw_read_schema_));
 
     
PAIMON_ASSIGN_OR_RAISE(std::vector<std::vector<std::shared_ptr<DataFileMeta>>> 
split_by_row_id,
                            MergeRangesAndSort(std::move(metas)));
 
     std::vector<std::unique_ptr<BatchReader>> sub_readers;
     for (const std::vector<std::shared_ptr<DataFileMeta>>& need_merge_files : 
split_by_row_id) {
+        if (need_merge_files.size() > 1) {
+            PAIMON_ASSIGN_OR_RAISE(
+                bool skip_group,
+                SkipByFileIndex(push_down_predicate, need_merge_files, 
data_file_path_factory));
+            if (skip_group) {
+                continue;
+            }
+        }
         PAIMON_ASSIGN_OR_RAISE(std::optional<GroupDeletionVector> group_dv,
                                ReadGroupDeletionVector(need_merge_files, 
split_dv_factory));
         PAIMON_ASSIGN_OR_RAISE(DeletionVector::Factory group_dv_factory,
@@ -404,10 +420,15 @@ Result<std::unique_ptr<BatchReader>> 
DataEvolutionSplitRead::InnerCreateReader(
             PAIMON_ASSIGN_OR_RAISE(
                 std::vector<std::unique_ptr<FileBatchReader>> raw_file_readers,
                 CreateRawFileReaders(split_impl->Partition(), 
need_merge_files, raw_read_schema_,
-                                     /*predicate=*/nullptr, group_dv_factory, 
row_ranges,
+                                     push_down_predicate, group_dv_factory, 
row_ranges,
                                      data_file_path_factory,
                                      /*extra_format_options=*/{}));
-            assert(raw_file_readers.size() == 1);
+            if (raw_file_readers.empty()) {
+                continue;
+            }
+            if (raw_file_readers.size() != 1) {
+                return Status::Invalid("Single-file data evolution group 
created multiple readers");
+            }
             sub_readers.push_back(std::move(raw_file_readers[0]));
         } else {
             PAIMON_ASSIGN_OR_RAISE(
@@ -424,17 +445,110 @@ Result<std::unique_ptr<BatchReader>> 
DataEvolutionSplitRead::InnerCreateReader(
     return 
std::make_unique<CompleteRowKindBatchReader>(std::move(batch_reader), pool_);
 }
 
+Result<std::shared_ptr<Predicate>> 
DataEvolutionSplitRead::CreatePushDownPredicate(
+    const std::shared_ptr<Predicate>& predicate,
+    const std::shared_ptr<arrow::Schema>& read_schema) {
+    std::map<std::string, int32_t> picked_field_name_to_idx;
+    for (int32_t i = 0; i < read_schema->num_fields(); ++i) {
+        const std::string& field_name = read_schema->field(i)->name();
+        if (!SpecialFields::IsSystemField(field_name)) {
+            picked_field_name_to_idx.emplace(field_name, i);
+        }
+    }
+    return PredicateUtils::CreatePickedFieldFilter(predicate, 
picked_field_name_to_idx);
+}
+
+Result<bool> DataEvolutionSplitRead::SkipByFileIndex(
+    const std::shared_ptr<Predicate>& predicate,
+    const std::vector<std::shared_ptr<DataFileMeta>>& files,
+    const std::shared_ptr<DataFilePathFactory>& data_file_path_factory) const {
+    if (!options_.FileIndexReadEnabled() || !predicate) {
+        return false;
+    }
+
+    PAIMON_ASSIGN_OR_RAISE(
+        std::unique_ptr<FieldMappingBuilder> field_mapping_builder,
+        FieldMappingBuilder::Create(raw_read_schema_, 
context_->GetPartitionKeys(), predicate));
+    std::set<int32_t> claimed_field_ids;
+    for (const auto& file : files) {
+        // Blob and vector-store files may cover only part of the row range, 
so their indexes
+        // cannot prove that the complete merged group misses the predicate.
+        if (!DataEvolutionUtils::IsNormalFile(file->file_name)) {
+            continue;
+        }
+
+        std::shared_ptr<TableSchema> data_schema = context_->GetTableSchema();
+        if (file->schema_id != data_schema->Id()) {
+            PAIMON_ASSIGN_OR_RAISE(data_schema, 
schema_manager_->ReadSchema(file->schema_id));
+        }
+        std::vector<DataField> written_fields;
+        if (file->write_cols) {
+            std::vector<std::string> data_write_cols;
+            data_write_cols.reserve(file->write_cols->size());
+            for (const auto& write_col : file->write_cols.value()) {
+                if (!SpecialFields::IsSystemField(write_col)) {
+                    data_write_cols.push_back(write_col);
+                }
+            }
+            PAIMON_ASSIGN_OR_RAISE(written_fields, 
data_schema->GetFields(data_write_cols));
+        } else {
+            written_fields = data_schema->Fields();
+        }
+
+        std::set<std::string> overwritten_field_names;
+        for (const auto& field : written_fields) {
+            if (!claimed_field_ids.insert(field.Id()).second) {
+                overwritten_field_names.insert(field.Name());
+            }
+        }
+
+        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FieldMapping> field_mapping,
+                               
field_mapping_builder->CreateFieldMapping(written_fields));
+        std::shared_ptr<Predicate> data_predicate =
+            field_mapping->non_partition_info.non_partition_filter;
+        if (!overwritten_field_names.empty()) {
+            PAIMON_ASSIGN_OR_RAISE(data_predicate, 
PredicateUtils::ExcludePredicateWithFields(
+                                                       data_predicate, 
overwritten_field_names));
+        }
+        if (!data_predicate) {
+            continue;
+        }
+
+        auto written_schema = 
DataField::ConvertDataFieldsToArrowSchema(written_fields);
+        PAIMON_ASSIGN_OR_RAISE(
+            std::shared_ptr<FileIndexResult> index_result,
+            FileIndexEvaluator::Evaluate(written_schema, data_predicate, 
data_file_path_factory,
+                                         file, options_.GetFileSystem(), 
pool_));
+        PAIMON_ASSIGN_OR_RAISE(bool is_remain, index_result->IsRemain());
+        if (!is_remain) {
+            return true;
+        }
+    }
+    return false;
+}
+
 Result<std::unique_ptr<FileBatchReader>> 
DataEvolutionSplitRead::ApplyIndexAndDvReaderIfNeeded(
     std::unique_ptr<FileBatchReader>&& file_reader, const 
std::shared_ptr<DataFileMeta>& file,
     const std::shared_ptr<arrow::Schema>& data_schema,
     const std::shared_ptr<arrow::Schema>& read_schema, const 
std::shared_ptr<Predicate>& predicate,
     DeletionVector::Factory dv_factory, const 
std::optional<std::vector<Range>>& row_ranges,
     const std::shared_ptr<DataFilePathFactory>& data_file_path_factory) const {
-    if (predicate) {
-        assert(false);
-        // as DataEvolutionSplitRead will skip predicate
-        return Status::Invalid("DataEvolutionSplitRead do not support 
predicate");
+    std::shared_ptr<FileIndexResult> file_index_result;
+    if (options_.FileIndexReadEnabled()) {
+        PAIMON_ASSIGN_OR_RAISE(
+            file_index_result,
+            FileIndexEvaluator::Evaluate(data_schema, predicate, 
data_file_path_factory, file,
+                                         options_.GetFileSystem(), pool_));
+        PAIMON_ASSIGN_OR_RAISE(bool is_remain, file_index_result->IsRemain());
+        if (!is_remain) {
+            return std::unique_ptr<FileBatchReader>();
+        }
+    }
+    const RoaringBitmap32* index_selection = nullptr;
+    if (auto* bitmap_index = 
dynamic_cast<BitmapIndexResult*>(file_index_result.get())) {
+        PAIMON_ASSIGN_OR_RAISE(index_selection, bitmap_index->GetBitmap());
     }
+
     // the factory is per row range group and already returns a view taking 
file-local positions.
     // Unlike RawFileSplitRead the vector is not folded into the format 
reader's selection: it is
     // 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
     }
     PAIMON_ASSIGN_OR_RAISE(std::optional<RoaringBitmap32> selection_row_ids,
                            file->ToFileSelection(row_ranges));
+    if (index_selection) {
+        if (selection_row_ids) {
+            selection_row_ids.value() &= *index_selection;
+        } else {
+            selection_row_ids = *index_selection;
+        }
+    }
+    if (selection_row_ids && selection_row_ids->IsEmpty()) {
+        return std::unique_ptr<FileBatchReader>();
+    }
     ::ArrowSchema c_read_schema;
     PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*read_schema, 
&c_read_schema));
-    PAIMON_RETURN_NOT_OK(
-        file_reader->SetReadSchema(&c_read_schema, /*predicate=*/nullptr, 
selection_row_ids));
+    PAIMON_RETURN_NOT_OK(file_reader->SetReadSchema(&c_read_schema, predicate, 
selection_row_ids));
 
     std::unique_ptr<FileBatchReader> reader;
     if (!file_reader->SupportPreciseBitmapSelection() && selection_row_ids) {
diff --git a/src/paimon/core/operation/data_evolution_split_read.h 
b/src/paimon/core/operation/data_evolution_split_read.h
index 983ca29d..94a59fa0 100644
--- a/src/paimon/core/operation/data_evolution_split_read.h
+++ b/src/paimon/core/operation/data_evolution_split_read.h
@@ -68,9 +68,10 @@ struct DeletionFile;
 /// ->(DelegatingPrefetchReader)->(PrefetchFileBatchReader)->FormatReader
 ///
 ///
-/// A union `SplitRead` to read multiple inner files to merge columns, note 
that this class
-/// does not support filtering push down: a predicate would have to be 
evaluated consistently
-/// across the files being merged, which is not implemented here.
+/// A union `SplitRead` to read multiple inner files to merge columns. A 
single-file row range
+/// group gets both file-index and format-level predicate pushdown. A merged 
group only uses file
+/// indexes to skip the whole group: filtering its child readers independently 
would break their
+/// positional alignment.
 ///
 /// Deletion vectors are supported: a row range group's vector is maintained 
against the
 /// group's anchor file (DataEvolutionUtils::RetrieveAnchorFile), so its 
positions are
@@ -172,6 +173,21 @@ class DataEvolutionSplitRead : public AbstractSplitRead {
         const std::shared_ptr<DataSplit>& data_split,
         const std::optional<std::vector<Range>>& row_ranges) const;
 
+    /// Keeps top-level conjuncts whose fields all belong to `read_schema`, 
excluding conjuncts
+    /// over system fields. The returned predicate is for pushdown only; the 
original predicate is
+    /// still evaluated as a residual filter when requested by the read 
context.
+    static Result<std::shared_ptr<Predicate>> CreatePushDownPredicate(
+        const std::shared_ptr<Predicate>& predicate,
+        const std::shared_ptr<arrow::Schema>& read_schema);
+
+    /// Returns true when file indexes prove that no row in a merged row range 
group can match.
+    /// Only normal files are considered, and an older copy of a field is 
excluded after a newer
+    /// file has claimed the same field id.
+    Result<bool> SkipByFileIndex(
+        const std::shared_ptr<Predicate>& predicate,
+        const std::vector<std::shared_ptr<DataFileMeta>>& files,
+        const std::shared_ptr<DataFilePathFactory>& data_file_path_factory) 
const;
+
     /// Builds the deletion vector factory over the split's deletion files, 
keyed by data file
     /// name. Only anchor files carry one. Returns a null factory when the 
split has none.
     DeletionVector::Factory CreateSplitDvFactory(const DataSplitImpl& 
split_impl) const;
diff --git a/src/paimon/core/operation/data_evolution_split_read_test.cpp 
b/src/paimon/core/operation/data_evolution_split_read_test.cpp
index 809933e9..03bc95b6 100644
--- a/src/paimon/core/operation/data_evolution_split_read_test.cpp
+++ b/src/paimon/core/operation/data_evolution_split_read_test.cpp
@@ -25,6 +25,7 @@
 
 #include "gtest/gtest.h"
 #include "paimon/common/data/binary_row.h"
+#include "paimon/common/table/special_fields.h"
 #include "paimon/core/deletionvectors/bitmap_deletion_vector.h"
 #include "paimon/core/io/data_file_meta.h"
 #include "paimon/core/manifest/file_source.h"
@@ -37,6 +38,8 @@
 #include "paimon/executor.h"
 #include "paimon/fs/local/local_file_system.h"
 #include "paimon/memory/memory_pool.h"
+#include "paimon/predicate/literal.h"
+#include "paimon/predicate/predicate_builder.h"
 #include "paimon/read_context.h"
 #include "paimon/status.h"
 #include "paimon/testing/utils/testharness.h"
@@ -102,6 +105,30 @@ class DataEvolutionSplitReadTest : public ::testing::Test {
     std::shared_ptr<MemoryPool> pool_ = GetDefaultPool();
 };
 
+TEST_F(DataEvolutionSplitReadTest, TestCreatePushDownPredicate) {
+    auto f0_predicate =
+        PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", 
FieldType::INT, Literal(1));
+    auto f1_predicate =
+        PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1", 
FieldType::INT, Literal(2));
+    auto row_id_predicate = PredicateBuilder::Equal(
+        /*field_index=*/2, SpecialFields::RowId().Name(), FieldType::BIGINT, 
Literal(3l));
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> predicate,
+                         PredicateBuilder::And({f0_predicate, f1_predicate, 
row_id_predicate}));
+
+    auto read_schema = DataField::ConvertDataFieldsToArrowSchema(
+        {DataField(0, arrow::field("f0", arrow::int32())), 
SpecialFields::RowId()});
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> push_down,
+                         
DataEvolutionSplitRead::CreatePushDownPredicate(predicate, read_schema));
+    ASSERT_TRUE(push_down);
+    ASSERT_EQ(*push_down, *f0_predicate);
+
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> or_predicate,
+                         PredicateBuilder::Or({f0_predicate, f1_predicate}));
+    ASSERT_OK_AND_ASSIGN(
+        push_down, 
DataEvolutionSplitRead::CreatePushDownPredicate(or_predicate, read_schema));
+    ASSERT_FALSE(push_down);
+}
+
 TEST_F(DataEvolutionSplitReadTest, TestAddSingleBlobEntry) {
     auto blob_entry =
         CreateBlobFile("blob1", /*first_row_id=*/0, /*row_count=*/100,
diff --git a/test/inte/data_evolution_table_test.cpp 
b/test/inte/data_evolution_table_test.cpp
index 6232c1e7..13fede80 100644
--- a/test/inte/data_evolution_table_test.cpp
+++ b/test/inte/data_evolution_table_test.cpp
@@ -400,10 +400,11 @@ class DataEvolutionTableTest : public ::testing::Test,
                        const std::shared_ptr<arrow::StructArray>& 
expected_array,
                        const std::shared_ptr<Predicate>& predicate = nullptr,
                        const std::vector<Range>& row_ranges = {},
-                       bool check_scan_plan_when_empty_result = true) const {
+                       bool check_scan_plan_when_empty_result = true,
+                       bool apply_predicate_to_scan = true) const {
         // scan
         ScanContextBuilder scan_context_builder(table_path);
-        scan_context_builder.SetPredicate(predicate);
+        scan_context_builder.SetPredicate(apply_predicate_to_scan ? predicate 
: nullptr);
         if (!row_ranges.empty()) {
             auto global_index_result = 
BitmapGlobalIndexResult::FromRanges(row_ranges);
             scan_context_builder.SetGlobalIndexResult(global_index_result);
@@ -1880,7 +1881,7 @@ TEST_P(DataEvolutionTableTest, TestScanAndReadWithIndex) {
                               expected_array));
     }
     {
-        // first 4 records read with data evolution, ignore index
+        // The old file's f2 index does not contain 102, but the newer file 
owns f2.
         auto predicate = PredicateBuilder::Equal(/*field_index=*/2, 
/*field_name=*/"f2",
                                                  FieldType::INT, Literal(102));
         auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>(
@@ -1892,51 +1893,45 @@ TEST_P(DataEvolutionTableTest, 
TestScanAndReadWithIndex) {
     ])")
                 .ValueOrDie());
         ASSERT_OK(ScanAndRead(table_path, 
arrow::schema(arrow_data_type->fields())->field_names(),
-                              expected_array, predicate));
+                              expected_array, predicate,
+                              /*row_ranges=*/{},
+                              /*check_scan_plan_when_empty_result=*/true,
+                              /*apply_predicate_to_scan=*/false));
     }
     {
-        // f2 has bitmap index, but data evolution scan and read ignore index
+        // The bitmap proves that neither row range group contains f2 = 103.
         auto predicate = PredicateBuilder::Equal(/*field_index=*/2, 
/*field_name=*/"f2",
                                                  FieldType::INT, Literal(103));
-        auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>(
-            arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([
-        ["Lily", 2, 102, 2.1],
-        ["Alice", 4, 104, 3.1],
-        ["Bob", 6, 106, 4.1],
-        ["David", 8, 108, 5.1]
-    ])")
-                .ValueOrDie());
         ASSERT_OK(ScanAndRead(table_path, 
arrow::schema(arrow_data_type->fields())->field_names(),
-                              expected_array, predicate));
+                              /*expected_array=*/nullptr, predicate,
+                              /*row_ranges=*/{},
+                              /*check_scan_plan_when_empty_result=*/false,
+                              /*apply_predicate_to_scan=*/false));
     }
     {
-        // f2 has bitmap index, data evolution scan will ignore index => not 
empty plan
-        // data evolution split read will also ignore index => not empty read 
batch
+        // Scan planning keeps the split, but reader-side indexes skip both 
row range groups.
         auto predicate = PredicateBuilder::Equal(/*field_index=*/2, 
/*field_name=*/"f2",
                                                  FieldType::INT, Literal(203));
-        auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>(
-            arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([
-        [null, null, 202, 6.1],
-        [null, null, 204, 7.1]
-    ])")
-                .ValueOrDie());
         ASSERT_OK(ScanAndRead(table_path, 
arrow::schema(arrow_data_type->fields())->field_names(),
-                              expected_array, predicate,
+                              /*expected_array=*/nullptr, predicate,
                               /*row_ranges=*/{},
-                              /*check_scan_plan_when_empty_result=*/true));
+                              /*check_scan_plan_when_empty_result=*/false,
+                              /*apply_predicate_to_scan=*/false));
     }
     {
-        // f2 has bitmap index, data evolution split read will ignore index
+        // A single-file group applies the exact bitmap row selection.
         auto predicate = PredicateBuilder::Equal(/*field_index=*/2, 
/*field_name=*/"f2",
                                                  FieldType::INT, Literal(202));
         auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>(
             arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([
-        [null, null, 202, 6.1],
-        [null, null, 204, 7.1]
+        [null, null, 202, 6.1]
     ])")
                 .ValueOrDie());
         ASSERT_OK(ScanAndRead(table_path, 
arrow::schema(arrow_data_type->fields())->field_names(),
-                              expected_array, predicate));
+                              expected_array, predicate,
+                              /*row_ranges=*/{},
+                              /*check_scan_plan_when_empty_result=*/true,
+                              /*apply_predicate_to_scan=*/false));
     }
     {
         auto predicate =
@@ -1953,7 +1948,7 @@ TEST_P(DataEvolutionTableTest, TestScanAndReadWithIndex) {
     {
         // test row id with predicate
         std::vector<Range> row_ranges = {Range(0l, 2l)};
-        // row id = {0, 1, 2}, while data evolution split read will ignore 
index
+        // A merged group keeps all selected row ids to preserve column 
alignment.
         auto predicate = PredicateBuilder::Equal(/*field_index=*/2, 
/*field_name=*/"f2",
                                                  FieldType::INT, Literal(106));
         CheckScanResult(table_path, /*predicate=*/predicate, 
/*row_ranges=*/row_ranges,
@@ -1967,26 +1962,179 @@ TEST_P(DataEvolutionTableTest, 
TestScanAndReadWithIndex) {
                 .ValueOrDie());
         ASSERT_OK(ScanAndRead(table_path, 
arrow::schema(arrow_data_type->fields())->field_names(),
                               expected_array, predicate,
-                              /*row_ranges=*/row_ranges));
+                              /*row_ranges=*/row_ranges,
+                              /*check_scan_plan_when_empty_result=*/true,
+                              /*apply_predicate_to_scan=*/false));
     }
     {
         // test row id with predicate
         std::vector<Range> row_ranges = {Range(4l, 5l)};
-        // row id = {4, 5}, data evolution split read will ignore bitmap index
+        // The single-file bitmap selection is intersected with the row-id 
selection.
         auto predicate = PredicateBuilder::Equal(/*field_index=*/2, 
/*field_name=*/"f2",
                                                  FieldType::INT, Literal(204));
         CheckScanResult(table_path, /*predicate=*/predicate, 
/*row_ranges=*/row_ranges,
                         /*expected_first_row_ids=*/{4}, 
/*expected_row_counts=*/{2});
         auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>(
             arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, R"([
-        [null, null, 202, 6.1],
         [null, null, 204, 7.1]
     ])")
                 .ValueOrDie());
         ASSERT_OK(ScanAndRead(table_path, 
arrow::schema(arrow_data_type->fields())->field_names(),
                               expected_array, predicate,
-                              /*row_ranges=*/row_ranges));
+                              /*row_ranges=*/row_ranges,
+                              /*check_scan_plan_when_empty_result=*/true,
+                              /*apply_predicate_to_scan=*/false));
+    }
+}
+
+TEST_P(DataEvolutionTableTest, TestDataEvolutionPredicatePushDownBoundaries) {
+    auto file_format = FileFormat();
+    if (file_format == "avro") {
+        return;
+    }
+    std::string table_path = paimon::test::GetDataDir() + file_format +
+                             
"/data_evolution_with_index.db/data_evolution_with_index";
+
+    {
+        // A file without f0 must not interpret the predicate as f0 = null.
+        auto predicate = PredicateBuilder::Equal(
+            /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
+            Literal(FieldType::STRING, "Lily", 4));
+        auto read_type =
+            arrow::struct_({arrow::field("f0", arrow::utf8()), 
arrow::field("f2", arrow::int32())});
+        auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>(
+            arrow::ipc::internal::json::ArrayFromJSON(read_type, R"([
+        ["Lily", 102],
+        ["Alice", 104],
+        ["Bob", 106],
+        ["David", 108],
+        [null, 202],
+        [null, 204]
+    ])")
+                .ValueOrDie());
+        ASSERT_OK(ScanAndRead(table_path, {"f0", "f2"}, expected_array, 
predicate,
+                              /*row_ranges=*/{},
+                              /*check_scan_plan_when_empty_result=*/true,
+                              /*apply_predicate_to_scan=*/false));
     }
+    {
+        // System fields are completed after reading and cannot be pushed into 
data files.
+        auto predicate = PredicateBuilder::Equal(/*field_index=*/1, 
/*field_name=*/"_ROW_ID",
+                                                 FieldType::BIGINT, 
Literal(99l));
+        auto read_type =
+            arrow::struct_({arrow::field("f2", arrow::int32()), 
SpecialFields::RowId().field_});
+        auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>(
+            arrow::ipc::internal::json::ArrayFromJSON(read_type, R"([
+        [102, 0],
+        [104, 1],
+        [106, 2],
+        [108, 3],
+        [202, 4],
+        [204, 5]
+    ])")
+                .ValueOrDie());
+        ASSERT_OK(ScanAndRead(table_path, {"f2", "_ROW_ID"}, expected_array, 
predicate,
+                              /*row_ranges=*/{},
+                              /*check_scan_plan_when_empty_result=*/true,
+                              /*apply_predicate_to_scan=*/false));
+    }
+    {
+        // Dropping a system-field conjunct must not drop a pushable data 
conjunct.
+        auto data_predicate = PredicateBuilder::Equal(
+            /*field_index=*/0, /*field_name=*/"f2", FieldType::INT, 
Literal(103));
+        auto system_predicate = PredicateBuilder::Equal(
+            /*field_index=*/1, /*field_name=*/"_ROW_ID", FieldType::BIGINT, 
Literal(0l));
+        ASSERT_OK_AND_ASSIGN(auto predicate,
+                             PredicateBuilder::And({data_predicate, 
system_predicate}));
+        ASSERT_OK(ScanAndRead(table_path, {"f2", "_ROW_ID"}, 
/*expected_array=*/nullptr, predicate,
+                              /*row_ranges=*/{},
+                              /*check_scan_plan_when_empty_result=*/false,
+                              /*apply_predicate_to_scan=*/false));
+    }
+    {
+        // Bitmap positions compose with row ranges without changing the 
physical row id.
+        auto predicate = PredicateBuilder::Equal(/*field_index=*/0, 
/*field_name=*/"f2",
+                                                 FieldType::INT, Literal(204));
+        auto read_type =
+            arrow::struct_({arrow::field("f2", arrow::int32()), 
SpecialFields::RowId().field_});
+        auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>(
+            arrow::ipc::internal::json::ArrayFromJSON(read_type, R"([
+        [204, 5]
+    ])")
+                .ValueOrDie());
+        ASSERT_OK(ScanAndRead(table_path, {"f2", "_ROW_ID"}, expected_array, 
predicate,
+                              /*row_ranges=*/{Range(4l, 5l)},
+                              /*check_scan_plan_when_empty_result=*/true,
+                              /*apply_predicate_to_scan=*/false));
+    }
+    {
+        // The bitmap selects row id 5 while the global-index selection keeps 
only row id 4.
+        auto predicate = PredicateBuilder::Equal(/*field_index=*/0, 
/*field_name=*/"f2",
+                                                 FieldType::INT, Literal(204));
+        ASSERT_OK(ScanAndRead(table_path, {"f2", "_ROW_ID"}, 
/*expected_array=*/nullptr, predicate,
+                              /*row_ranges=*/{Range(4l, 4l)},
+                              /*check_scan_plan_when_empty_result=*/false,
+                              /*apply_predicate_to_scan=*/false));
+    }
+    {
+        // The predicate keeps row ids {4, 5}; the global-index selection 
keeps {0, 1, 2, 3, 4}.
+        auto equal_202 = PredicateBuilder::Equal(/*field_index=*/0, 
/*field_name=*/"f2",
+                                                 FieldType::INT, Literal(202));
+        auto equal_204 = PredicateBuilder::Equal(/*field_index=*/0, 
/*field_name=*/"f2",
+                                                 FieldType::INT, Literal(204));
+        ASSERT_OK_AND_ASSIGN(auto predicate, PredicateBuilder::Or({equal_202, 
equal_204}));
+        auto read_type =
+            arrow::struct_({arrow::field("f2", arrow::int32()), 
SpecialFields::RowId().field_});
+        auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>(
+            arrow::ipc::internal::json::ArrayFromJSON(read_type, R"([
+        [202, 4]
+    ])")
+                .ValueOrDie());
+        ASSERT_OK(ScanAndRead(table_path, {"f2", "_ROW_ID"}, expected_array, 
predicate,
+                              /*row_ranges=*/{Range(0l, 4l)},
+                              /*check_scan_plan_when_empty_result=*/true,
+                              /*apply_predicate_to_scan=*/false));
+    }
+}
+
+TEST_P(DataEvolutionTableTest, TestFormatPredicatePushDownWithoutFileIndex) {
+    if (FileFormat() == "avro") {
+        return;
+    }
+
+    CreateDataEvolutionTable(
+        /*deletion_vectors_enabled=*/false, 
{{Options::FILE_INDEX_READ_ENABLED, "false"},
+                                             {Options::WRITE_BATCH_SIZE, "1"},
+                                             {"parquet.page.size", "1"},
+                                             {"parquet.enable-dictionary", 
"false"},
+                                             
{"parquet.write.enable-page-index", "true"},
+                                             
{"parquet.read.enable-page-index-filter", "true"},
+                                             {"orc.stripe.size", "1"},
+                                             {"orc.row.index.stride", "1"}});
+    std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
+
+    auto input = std::dynamic_pointer_cast<arrow::StructArray>(
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+        [1, "a", "x"],
+        [2, "b", "y"],
+        [3, "c", "z"],
+        [4, "d", "w"]
+    ])")
+            .ValueOrDie());
+    ASSERT_OK_AND_ASSIGN(auto commit_messages, WriteArray(table_path, {"f0", 
"f1", "f2"}, input));
+    ASSERT_OK(Commit(table_path, commit_messages));
+
+    auto predicate =
+        PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", 
FieldType::INT, Literal(3));
+    auto expected = std::dynamic_pointer_cast<arrow::StructArray>(
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+        [3, "c", "z"]
+    ])")
+            .ValueOrDie());
+    ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2"}, expected, predicate,
+                          /*row_ranges=*/{},
+                          /*check_scan_plan_when_empty_result=*/true,
+                          /*apply_predicate_to_scan=*/true));
 }
 
 TEST_P(DataEvolutionTableTest, TestPredicate) {

Reply via email to