SteNicholas commented on code in PR #490:
URL: https://github.com/apache/paimon-cpp/pull/490#discussion_r4453150246


##########
src/paimon/common/reader/complete_index_score_file_batch_reader_test.cpp:
##########
@@ -190,4 +192,160 @@ TEST_F(CompleteIndexScoreFileBatchReaderTest, 
TestSetReadSchemaReordersScoreColu
     ASSERT_TRUE(expected_array->Equals(*result_array)) << 
result_array->ToString();
 }
 
+TEST_F(CompleteIndexScoreFileBatchReaderTest, 
TestMultipleScoresFollowSelectedRowsAndProjection) {
+    arrow::FieldVector fields = {arrow::field("score_z", arrow::float32()),
+                                 arrow::field("f0", arrow::int32()),
+                                 arrow::field("score_a", arrow::float32())};
+    auto type = arrow::struct_(fields);
+    auto data = arrow::ipc::internal::json::ArrayFromJSON(type, R"([
+        [null, 10, null], [null, 11, null], [null, 12, null],
+        [null, 13, null], [null, 14, null], [null, 15, null],
+        [null, 16, null], [null, 17, null], [null, 18, null],
+        [null, 19, null], [null, 20, null], [null, 21, null]
+    ])")
+                    .ValueOrDie();
+    auto selected = RoaringBitmap32::From({0, 2, 8, 9, 10, 11});
+    auto inner = std::make_unique<MockFileBatchReader>(data, type, selected,
+                                                       /*read_batch_size=*/3);
+    inner->EnableRandomizeBatchSize(false);
+    std::map<std::string, std::vector<float>> scores = {
+        {"score_a", {1.25f, 2.5f, 3.75f, 5.0f, 6.25f, 7.5f}},
+        {"score_z", {12.5f, 25.0f, 37.5f, 50.0f, 62.5f, 75.0f}}};
+    CompleteIndexScoreFileBatchReader reader(std::move(inner), scores, 
GetArrowPool(pool_));
+
+    ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatchWithBitmap first, 
reader.NextBatchWithBitmap());
+    ASSERT_EQ(RoaringBitmap32::From({0, 2}), first.second);
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> first_array,
+                         
ReadResultCollector::GetArray(std::move(first.first)));
+    auto expected_first = arrow::ipc::internal::json::ArrayFromJSON(
+                              type, R"([[12.5, 10, 1.25], [null, 11, null], 
[25, 12, 2.5]])")
+                              .ValueOrDie();
+    ASSERT_TRUE(expected_first->Equals(*first_array)) << 
first_array->ToString();
+
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result_array,
+                         ReadResultCollector::CollectResult(&reader));
+    std::shared_ptr<arrow::ChunkedArray> expected_array;
+    ASSERT_TRUE(arrow::ipc::internal::json::ChunkedArrayFromJSON(
+                    type, {R"([[37.5, 18, 3.75], [50, 19, 5], [62.5, 20, 
6.25], [75, 21, 7.5]])"},
+                    &expected_array)
+                    .ok());
+    ASSERT_TRUE(expected_array->Equals(*result_array)) << 
result_array->ToString();
+
+    arrow::FieldVector reordered_fields = {fields[2], fields[0], fields[1]};
+    ::ArrowSchema read_schema;
+    ASSERT_TRUE(arrow::ExportSchema(*arrow::schema(reordered_fields), 
&read_schema).ok());
+    ASSERT_OK(reader.SetReadSchema(&read_schema, /*predicate=*/nullptr,
+                                   /*selection_bitmap=*/selected));
+    ASSERT_OK_AND_ASSIGN(result_array, 
ReadResultCollector::CollectResult(&reader));
+    ASSERT_TRUE(arrow::ipc::internal::json::ChunkedArrayFromJSON(
+                    arrow::struct_(reordered_fields),
+                    {R"([[1.25, 12.5, 10], [2.5, 25, 12], [3.75, 37.5, 18],
+                         [5, 50, 19], [6.25, 62.5, 20], [7.5, 75, 21]])"},
+                    &expected_array)
+                    .ok());
+    ASSERT_TRUE(expected_array->Equals(*result_array)) << 
result_array->ToString();
+}
+
+TEST_F(CompleteIndexScoreFileBatchReaderTest, 
TestMultipleScoresWithEmptyResult) {
+    auto type = arrow::struct_(
+        {arrow::field("score_a", arrow::float32()), arrow::field("score_b", 
arrow::float32())});
+    for (const std::string& json : {"[]", "[[null, null]]"}) {

Review Comment:
   Please avoid binding `const std::string&` to elements of an initializer list 
of string literals here and in the equivalent loops at lines 270 and 290. Each 
element is a `const char*`, so the implicit conversion creates a temporary 
`std::string` and GCC reports `-Werror=range-loop-construct`. This already 
fails the [GCC CI 
build](https://github.com/apache/paimon-cpp/actions/runs/41423557423/job/125204618398),
 and I reproduced the diagnostic with GCC locally. An explicitly typed string 
container or a `const std::string` loop variable fixes the build.



##########
src/paimon/core/operation/file_index_read_executor.cpp:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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/core/operation/file_index_read_executor.h"
+
+#include <algorithm>
+#include <cmath>
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "arrow/c/abi.h"
+#include "arrow/type.h"
+#include "fmt/format.h"
+#include "paimon/common/reader/complete_index_score_file_batch_reader.h"
+#include "paimon/common/reader/predicate_batch_reader.h"
+#include "paimon/common/types/data_field.h"
+#include "paimon/common/utils/object_utils.h"
+#include "paimon/common/utils/scope_guard.h"
+#include "paimon/core/deletionvectors/bitmap64_deletion_vector.h"
+#include "paimon/core/deletionvectors/bitmap_deletion_vector.h"
+#include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/io/data_file_path_factory.h"
+#include "paimon/core/io/file_index_evaluator.h"
+#include "paimon/core/io/file_index_options.h"
+#include "paimon/core/operation/internal_read_context.h"
+#include "paimon/core/operation/raw_file_split_read.h"
+#include "paimon/core/schema/schema_manager.h"
+#include "paimon/core/schema/table_schema.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/file_index/scored_file_index_result.h"
+#include "paimon/format/reader_builder.h"
+#include "paimon/predicate/compound_predicate.h"
+#include "paimon/predicate/full_text_search.h"
+#include "paimon/predicate/function.h"
+#include "paimon/predicate/leaf_predicate.h"
+#include "paimon/predicate/vector_search.h"
+#include "paimon/utils/roaring_bitmap64.h"
+
+namespace paimon {
+namespace {
+
+Result<std::string> ResolveSearchFieldName(
+    const std::string& field_name, const std::map<std::string, std::string>& 
field_name_mapping,
+    const std::string& file_name) {
+    auto iter = field_name_mapping.find(field_name);
+    if (iter == field_name_mapping.end()) {
+        return Status::Invalid(
+            fmt::format("Search field '{}' has no matching field ID in data 
schema for file '{}'",
+                        field_name, file_name));
+    }
+    return iter->second;
+}
+
+}  // namespace
+
+Result<std::unique_ptr<FileIndexReadExecutor>> FileIndexReadExecutor::Create(
+    const RawFileSplitRead& split_read, const BinaryRow& partition,
+    const std::shared_ptr<DataFileMeta>& file, const FieldMappingBuilder* 
field_mapping_builder,
+    const std::shared_ptr<DataFilePathFactory>& path_factory,
+    const std::map<std::string, std::string>& extra_format_options) {
+    std::unique_ptr<FileIndexReadExecutor> executor(new FileIndexReadExecutor(
+        split_read, partition, file, field_mapping_builder, path_factory, 
extra_format_options));
+    PAIMON_RETURN_NOT_OK(executor->Prepare());
+    return executor;
+}
+
+FileIndexReadExecutor::FileIndexReadExecutor(
+    const RawFileSplitRead& split_read, const BinaryRow& partition,
+    const std::shared_ptr<DataFileMeta>& file, const FieldMappingBuilder* 
field_mapping_builder,
+    const std::shared_ptr<DataFilePathFactory>& path_factory,
+    const std::map<std::string, std::string>& extra_format_options)
+    : split_read_(split_read),
+      partition_(partition),
+      file_(file),
+      field_mapping_builder_(field_mapping_builder),
+      path_factory_(path_factory),
+      extra_format_options_(extra_format_options) {}
+
+Status FileIndexReadExecutor::Prepare() {
+    std::shared_ptr<TableSchema> table_schema = 
split_read_.context_->GetTableSchema();
+    if (file_->schema_id != table_schema->Id()) {
+        PAIMON_ASSIGN_OR_RAISE(table_schema,
+                               
split_read_.schema_manager_->ReadSchema(file_->schema_id));
+    }
+    const auto data_schema = 
DataField::ConvertDataFieldsToArrowSchema(table_schema->Fields());
+    if (file_->row_count < 0 || file_->row_count > RoaringBitmap32::MAX_VALUE) 
{
+        return Status::NotImplemented(
+            "Precise file-index evaluation requires bitmap32 physical row 
positions");
+    }
+    PAIMON_ASSIGN_OR_RAISE(std::vector<DataField> data_fields,
+                           
DataField::ConvertArrowSchemaToDataFields(data_schema));
+    PAIMON_ASSIGN_OR_RAISE(
+        std::vector<DataField> read_fields,
+        
DataField::ConvertArrowSchemaToDataFields(split_read_.context_->GetReadSchema()));
+    auto field_names = FieldMappingBuilder::CreateFieldNameMapping(
+        split_read_.context_->GetTableSchema()->Fields(), data_fields);
+    std::map<std::string, std::string> index_options;
+    PAIMON_ASSIGN_OR_RAISE(index_options,
+                           FileIndexOptions::RewriteFieldNames(
+                               split_read_.context_->GetCoreOptions().ToMap(), 
field_names));
+    PAIMON_ASSIGN_OR_RAISE(indexes_, FileIndexEvaluator::Create(
+                                         data_schema, index_options, 
path_factory_, file_,
+                                         
split_read_.context_->GetCoreOptions().GetFileSystem(),
+                                         /*only_use_embedding_index=*/false, 
split_read_.pool_));
+
+    std::map<std::string, DataField> read_by_name;
+    for (const auto& field : read_fields) {
+        read_by_name.emplace(field.Name(), field);
+    }
+    std::map<int32_t, std::pair<int32_t, DataField>> data_by_id;
+    for (size_t i = 0; i < data_fields.size(); ++i) {
+        data_by_id.emplace(data_fields[i].Id(), std::make_pair(i, 
data_fields[i]));
+    }
+    // Precheck the entire tree before an empty candidate set can 
short-circuit it.
+    PAIMON_RETURN_NOT_OK(
+        PreparePredicate(split_read_.context_->GetPredicate(), read_by_name, 
data_by_id));
+    return PrepareSearch(field_names);
+}
+
+Status FileIndexReadExecutor::PreparePredicate(
+    const std::shared_ptr<Predicate>& predicate,
+    const std::map<std::string, DataField>& read_by_name,
+    const std::map<int32_t, std::pair<int32_t, DataField>>& data_by_id) {
+    if (!predicate) {
+        return Status::OK();
+    }
+    if (auto compound = 
std::dynamic_pointer_cast<CompoundPredicate>(predicate)) {
+        const Function::Type type = compound->GetFunction().GetType();
+        if (type != Function::Type::AND && type != Function::Type::OR) {
+            return Status::Invalid(
+                fmt::format("unsupported compound file predicate {}", 
predicate->ToString()));
+        }
+        for (const auto& child : compound->Children()) {
+            PAIMON_RETURN_NOT_OK(PreparePredicate(child, read_by_name, 
data_by_id));
+        }
+        return Status::OK();
+    }
+    auto leaf = std::dynamic_pointer_cast<LeafPredicate>(predicate);
+    if (!leaf) {
+        return Status::Invalid("File predicate must contain leaf or compound 
predicates");
+    }
+    bool full_text = leaf->GetFunction().GetType() == 
Function::Type::FULL_TEXT_MATCH;
+    if (!full_text &&
+        ObjectUtils::Contains(split_read_.context_->GetPartitionKeys(), 
leaf->FieldName())) {
+        mapped_leaves_.emplace(leaf, nullptr);
+        return Status::OK();
+    }
+    PAIMON_ASSIGN_OR_RAISE(
+        std::optional<std::shared_ptr<Predicate>> mapped,
+        FieldMappingBuilder::ReconstructPredicateWithDataFields(leaf, 
read_by_name, data_by_id));
+    std::shared_ptr<LeafPredicate> mapped_leaf;
+    if (mapped) {
+        mapped_leaf = std::dynamic_pointer_cast<LeafPredicate>(*mapped);
+        if (!mapped_leaf) {
+            return Status::Invalid("Mapped file predicate must be a leaf");
+        }
+        if (full_text) {
+            PAIMON_ASSIGN_OR_RAISE(
+                [[maybe_unused]] std::shared_ptr<FileIndexReader> reader,
+                indexes_->GetSearchReader(mapped_leaf->FieldName(),
+                                          
FileIndexReader::SearchType::FULL_TEXT_MATCH));
+        }
+    }
+    mapped_leaves_.emplace(leaf, std::move(mapped_leaf));
+    return Status::OK();
+}
+
+Status FileIndexReadExecutor::PrepareSearch(const std::map<std::string, 
std::string>& field_names) {
+    if (split_read_.context_->GetVectorSearch()) {
+        vector_search_ = 
std::make_shared<VectorSearch>(*split_read_.context_->GetVectorSearch());
+        PAIMON_ASSIGN_OR_RAISE(
+            vector_search_->field_name,
+            ResolveSearchFieldName(vector_search_->field_name, field_names, 
file_->file_name));
+        PAIMON_ASSIGN_OR_RAISE(
+            [[maybe_unused]] std::shared_ptr<FileIndexReader> reader,
+            indexes_->GetSearchReader(vector_search_->field_name,
+                                      
FileIndexReader::SearchType::VECTOR_SEARCH));
+    }
+    if (split_read_.context_->GetFullTextSearch()) {
+        full_text_search_ =
+            
std::make_shared<FullTextSearch>(*split_read_.context_->GetFullTextSearch());
+        PAIMON_ASSIGN_OR_RAISE(
+            full_text_search_->field_name,
+            ResolveSearchFieldName(full_text_search_->field_name, field_names, 
file_->file_name));
+        PAIMON_ASSIGN_OR_RAISE(
+            [[maybe_unused]] std::shared_ptr<FileIndexReader> reader,
+            indexes_->GetSearchReader(full_text_search_->field_name,
+                                      
FileIndexReader::SearchType::FULL_TEXT_SEARCH));
+    }
+    return Status::OK();
+}
+
+Result<RoaringBitmap32> FileIndexReadExecutor::CreateCandidates(
+    const DeletionVector::Factory& dv_factory,
+    const std::optional<std::vector<Range>>& row_ranges) const {
+    RoaringBitmap32 candidates;
+    if (row_ranges) {
+        for (const auto& range : *row_ranges) {
+            candidates.AddRange(static_cast<int32_t>(range.from),
+                                static_cast<int32_t>(range.to + 1));
+        }
+    } else {
+        candidates.AddRange(0, static_cast<int32_t>(file_->row_count));
+    }
+    if (dv_factory) {
+        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<DeletionVector> dv, 
dv_factory(file_->file_name));
+        if (dynamic_cast<Bitmap64DeletionVector*>(dv.get())) {
+            return Status::NotImplemented(
+                "Precise file-index evaluation does not support bitmap64 
deletion vectors");
+        }
+        if (auto bitmap = dynamic_cast<BitmapDeletionVector*>(dv.get())) {
+            candidates -= *bitmap->GetBitmap();
+        } else if (dv) {
+            return Status::NotImplemented(
+                "Precise file-index evaluation requires bitmap32 deletion 
vectors");
+        }
+    }
+    return candidates;
+}
+
+Result<FileIndexReadExecutor::Evaluation> FileIndexReadExecutor::Evaluate(
+    const std::shared_ptr<Predicate>& predicate, const RoaringBitmap32& 
candidates) const {
+    if (!predicate || candidates.IsEmpty()) {
+        return Evaluation{candidates, {}};
+    }
+    if (auto leaf = std::dynamic_pointer_cast<LeafPredicate>(predicate)) {
+        return EvaluateLeaf(leaf, candidates);
+    }
+    if (auto compound = 
std::dynamic_pointer_cast<CompoundPredicate>(predicate)) {
+        return EvaluateCompound(compound, candidates);
+    }
+    return Status::Invalid(fmt::format("unsupported file predicate {}", 
predicate->ToString()));
+}
+
+Result<FileIndexReadExecutor::Evaluation> 
FileIndexReadExecutor::EvaluateCompound(
+    const std::shared_ptr<CompoundPredicate>& predicate, const 
RoaringBitmap32& candidates) const {
+    const Function::Type type = predicate->GetFunction().GetType();
+    if (type == Function::Type::OR) {
+        Evaluation result;
+        for (const auto& child : predicate->Children()) {
+            // Every branch must see the same input, including rows matched by 
earlier branches.
+            PAIMON_ASSIGN_OR_RAISE(Evaluation child_result, Evaluate(child, 
candidates));
+            result.rows |= child_result.rows;
+            result.scores.insert(child_result.scores.begin(), 
child_result.scores.end());
+        }
+        return result;
+    }
+    if (type != Function::Type::AND) {
+        return Status::Invalid(
+            fmt::format("unsupported compound file predicate {}", 
predicate->ToString()));
+    }
+
+    struct Child {
+        std::shared_ptr<Predicate> predicate;
+        std::optional<double> selectivity;
+    };
+    std::vector<Child> children;
+    children.reserve(predicate->Children().size());
+    for (const auto& child : predicate->Children()) {
+        PAIMON_ASSIGN_OR_RAISE(std::optional<double> selectivity, 
EstimateSelectivity(child));
+        children.push_back({child, selectivity});
+    }
+    std::stable_sort(children.begin(), children.end(), [](const Child& lhs, 
const Child& rhs) {
+        if (!lhs.selectivity) {
+            return false;
+        }
+        return !rhs.selectivity || *lhs.selectivity < *rhs.selectivity;
+    });
+
+    Evaluation result{candidates, {}};
+    for (const auto& child : children) {
+        PAIMON_ASSIGN_OR_RAISE(Evaluation child_result, 
Evaluate(child.predicate, result.rows));
+        result.rows = std::move(child_result.rows);
+        result.scores.insert(child_result.scores.begin(), 
child_result.scores.end());
+        if (result.rows.IsEmpty()) {
+            break;
+        }
+    }
+    return result;
+}
+
+Result<std::optional<double>> FileIndexReadExecutor::EstimateSelectivity(
+    const std::shared_ptr<Predicate>& predicate) const {
+    if (!predicate) {
+        return std::optional<double>();
+    }
+    if (auto leaf = std::dynamic_pointer_cast<LeafPredicate>(predicate)) {
+        PAIMON_ASSIGN_OR_RAISE(std::optional<double> estimate, 
EstimateLeafSelectivity(leaf));
+        if (estimate && (!std::isfinite(*estimate) || *estimate < 0.0 || 
*estimate > 1.0)) {
+            return std::optional<double>();
+        }
+        return estimate;
+    }
+    auto compound = std::dynamic_pointer_cast<CompoundPredicate>(predicate);
+    if (!compound) {
+        return Status::Invalid(fmt::format("unsupported file predicate {}", 
predicate->ToString()));
+    }
+    const Function::Type type = compound->GetFunction().GetType();
+    if (type != Function::Type::AND && type != Function::Type::OR) {
+        return Status::Invalid(
+            fmt::format("unsupported compound file predicate {}", 
predicate->ToString()));
+    }
+    std::optional<double> estimate =
+        type == Function::Type::AND ? std::nullopt : std::make_optional(0.0);
+    // These are scheduling hints. Continue visiting all children to propagate 
errors.
+    for (const auto& child : compound->Children()) {
+        PAIMON_ASSIGN_OR_RAISE(std::optional<double> child_estimate, 
EstimateSelectivity(child));
+        if (type == Function::Type::AND) {
+            if (child_estimate) {
+                if (estimate.has_value()) {
+                    estimate = std::min(*estimate, *child_estimate);
+                } else {
+                    estimate = *child_estimate;
+                }
+            }
+        } else if (estimate && child_estimate) {
+            estimate = std::min(1.0, *estimate + *child_estimate);
+        } else {
+            estimate = std::nullopt;
+        }
+    }
+    return estimate;
+}
+
+Result<std::optional<double>> FileIndexReadExecutor::EstimateLeafSelectivity(
+    const std::shared_ptr<LeafPredicate>& predicate) const {
+    auto iter = mapped_leaves_.find(predicate);
+    if (iter == mapped_leaves_.end()) {
+        return Status::Invalid("File predicate mapping was not prepared");
+    }
+    if (!iter->second) {
+        return std::optional<double>();
+    }
+    return indexes_->EstimateSelectivity(iter->second);
+}
+
+Result<FileIndexReadExecutor::Evaluation> FileIndexReadExecutor::EvaluateLeaf(
+    const std::shared_ptr<LeafPredicate>& predicate, const RoaringBitmap32& 
candidates) const {
+    bool is_full_text = predicate->GetFunction().GetType() == 
Function::Type::FULL_TEXT_MATCH;
+    bool needs_scores =
+        is_full_text &&
+        split_read_.context_->NeedsScores(
+            {InternalReadContext::IndexScoreSource::Type::FULL_TEXT_MATCH, 
predicate->FieldName()});
+    std::shared_ptr<FileIndexResult> index_result = FileIndexResult::Remain();
+    auto iter = mapped_leaves_.find(predicate);
+    if (iter == mapped_leaves_.end()) {
+        return Status::Invalid("File predicate mapping was not prepared");
+    }
+    if (iter->second) {
+        PAIMON_ASSIGN_OR_RAISE(index_result, 
indexes_->EvaluateLeaf(iter->second, needs_scores));
+    }
+    if (!index_result) {
+        return Status::Invalid("file predicate index returned a null result");
+    }
+    Evaluation result{candidates, {}};
+    if (needs_scores) {
+        auto scored = 
std::dynamic_pointer_cast<ScoredFileIndexResult>(index_result);
+        if (!scored) {
+            return Status::Invalid(
+                fmt::format("full-text index for field '{}' did not return 
requested scores",
+                            predicate->FieldName()));
+        }
+        result.rows &= scored->GetRowPositions();
+        result.scores.emplace(predicate->FieldName(), std::move(scored));
+        return result;
+    }
+
+    if (auto bitmap = 
std::dynamic_pointer_cast<BitmapIndexResult>(index_result)) {
+        PAIMON_ASSIGN_OR_RAISE(const RoaringBitmap32* rows, 
bitmap->GetBitmap());
+        result.rows &= *rows;
+    } else if (is_full_text) {
+        return Status::Invalid(fmt::format("full-text index for field '{}' did 
not return a bitmap",
+                                           predicate->FieldName()));
+    } else if (std::dynamic_pointer_cast<Skip>(index_result)) {
+        return Evaluation{};
+    } else if (!std::dynamic_pointer_cast<Remain>(index_result)) {
+        return Status::Invalid(
+            fmt::format("invalid index result for predicate {}", 
predicate->ToString()));
+    }
+    // TODO(lisizhuo.lsz): Whether to ScanScalar depends on whether the index 
returns exact results.
+    if (!is_full_text && !result.rows.IsEmpty()) {
+        // Scalar indexes may contain false positives, even if they return a 
bitmap.
+        PAIMON_ASSIGN_OR_RAISE(RoaringBitmap32 exact_rows, 
ScanScalar(predicate, result.rows));
+        result.rows &= exact_rows;
+    }
+    return result;
+}
+
+Result<std::unique_ptr<FileBatchReader>> 
FileIndexReadExecutor::CreateSelectedFileReader(
+    const RoaringBitmap32& selection, const FieldMappingBuilder* 
field_mapping_builder) const {
+    PAIMON_ASSIGN_OR_RAISE(std::string format, file_->FileFormat());
+    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ReaderBuilder> builder,
+                           split_read_.PrepareReaderBuilder(format, 
extra_format_options_));
+    return split_read_.CreateFieldMappingReader(path_factory_->ToPath(file_), 
file_, partition_,
+                                                std::move(builder), 
field_mapping_builder, nullptr,
+                                                std::nullopt, path_factory_, 
&selection);
+}
+
+Result<RoaringBitmap32> FileIndexReadExecutor::ScanScalar(
+    const std::shared_ptr<LeafPredicate>& predicate, const RoaringBitmap32& 
input) const {
+    auto field = 
split_read_.context_->GetReadSchema()->GetFieldByName(predicate->FieldName());
+    if (!field) {
+        return Status::Invalid(fmt::format(
+            "Scalar predicate field '{}' is missing from the read schema", 
predicate->FieldName()));
+    }
+    PAIMON_ASSIGN_OR_RAISE(
+        std::unique_ptr<FieldMappingBuilder> field_mapping_builder,
+        FieldMappingBuilder::Create(arrow::schema({field}),
+                                    split_read_.context_->GetPartitionKeys(), 
nullptr));
+    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileBatchReader> reader,
+                           CreateSelectedFileReader(input, 
field_mapping_builder.get()));

Review Comment:
   Please reject predicates on virtual score columns before this path, or 
evaluate them against the computed index scores. A valid projection can contain 
`_body_INDEX_SCORE`, but this single-column file reader treats that field as 
absent from the physical schema and fills it with null; the real scores are 
only attached later by `CompleteIndexScoreFileBatchReader`. Consequently, 
`FullTextMatch(body, query) AND _body_INDEX_SCORE > 0.5` silently returns no 
rows even when matching scores exceed the threshold, while `IS NULL` can retain 
rows incorrectly. Please add regression coverage for both comparison and null 
predicates on score columns.



##########
src/paimon/core/operation/file_index_read_executor.cpp:
##########
@@ -0,0 +1,553 @@
+/*
+ * 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/core/operation/file_index_read_executor.h"
+
+#include <algorithm>
+#include <cmath>
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "arrow/c/abi.h"
+#include "arrow/type.h"
+#include "fmt/format.h"
+#include "paimon/common/reader/complete_index_score_file_batch_reader.h"
+#include "paimon/common/reader/predicate_batch_reader.h"
+#include "paimon/common/types/data_field.h"
+#include "paimon/common/utils/object_utils.h"
+#include "paimon/common/utils/scope_guard.h"
+#include "paimon/core/deletionvectors/bitmap64_deletion_vector.h"
+#include "paimon/core/deletionvectors/bitmap_deletion_vector.h"
+#include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/io/data_file_path_factory.h"
+#include "paimon/core/io/file_index_evaluator.h"
+#include "paimon/core/io/file_index_options.h"
+#include "paimon/core/operation/internal_read_context.h"
+#include "paimon/core/operation/raw_file_split_read.h"
+#include "paimon/core/schema/schema_manager.h"
+#include "paimon/core/schema/table_schema.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/file_index/scored_file_index_result.h"
+#include "paimon/format/reader_builder.h"
+#include "paimon/predicate/compound_predicate.h"
+#include "paimon/predicate/full_text_search.h"
+#include "paimon/predicate/function.h"
+#include "paimon/predicate/leaf_predicate.h"
+#include "paimon/predicate/vector_search.h"
+#include "paimon/utils/roaring_bitmap64.h"
+
+namespace paimon {
+namespace {
+
+Result<std::string> ResolveSearchFieldName(
+    const std::string& field_name, const std::map<std::string, std::string>& 
field_name_mapping,
+    const std::string& file_name) {
+    auto iter = field_name_mapping.find(field_name);
+    if (iter == field_name_mapping.end()) {
+        return Status::Invalid(
+            fmt::format("Search field '{}' has no matching field ID in data 
schema for file '{}'",
+                        field_name, file_name));
+    }
+    return iter->second;
+}
+
+}  // namespace
+
+Result<std::unique_ptr<FileIndexReadExecutor>> FileIndexReadExecutor::Create(
+    const RawFileSplitRead& split_read, const BinaryRow& partition,
+    const std::shared_ptr<DataFileMeta>& file, const FieldMappingBuilder* 
field_mapping_builder,
+    const std::shared_ptr<DataFilePathFactory>& path_factory,
+    const std::map<std::string, std::string>& extra_format_options) {
+    std::unique_ptr<FileIndexReadExecutor> executor(new FileIndexReadExecutor(
+        split_read, partition, file, field_mapping_builder, path_factory, 
extra_format_options));
+    PAIMON_RETURN_NOT_OK(executor->Prepare());
+    return executor;
+}
+
+FileIndexReadExecutor::FileIndexReadExecutor(
+    const RawFileSplitRead& split_read, const BinaryRow& partition,
+    const std::shared_ptr<DataFileMeta>& file, const FieldMappingBuilder* 
field_mapping_builder,
+    const std::shared_ptr<DataFilePathFactory>& path_factory,
+    const std::map<std::string, std::string>& extra_format_options)
+    : split_read_(split_read),
+      partition_(partition),
+      file_(file),
+      field_mapping_builder_(field_mapping_builder),
+      path_factory_(path_factory),
+      extra_format_options_(extra_format_options) {}
+
+Status FileIndexReadExecutor::Prepare() {
+    std::shared_ptr<TableSchema> table_schema = 
split_read_.context_->GetTableSchema();
+    if (file_->schema_id != table_schema->Id()) {
+        PAIMON_ASSIGN_OR_RAISE(table_schema,
+                               
split_read_.schema_manager_->ReadSchema(file_->schema_id));
+    }
+    const auto data_schema = 
DataField::ConvertDataFieldsToArrowSchema(table_schema->Fields());
+    if (file_->row_count < 0 || file_->row_count > RoaringBitmap32::MAX_VALUE) 
{
+        return Status::NotImplemented(
+            "Precise file-index evaluation requires bitmap32 physical row 
positions");
+    }
+    PAIMON_ASSIGN_OR_RAISE(std::vector<DataField> data_fields,
+                           
DataField::ConvertArrowSchemaToDataFields(data_schema));
+    PAIMON_ASSIGN_OR_RAISE(
+        std::vector<DataField> read_fields,
+        
DataField::ConvertArrowSchemaToDataFields(split_read_.context_->GetReadSchema()));
+    auto field_names = FieldMappingBuilder::CreateFieldNameMapping(
+        split_read_.context_->GetTableSchema()->Fields(), data_fields);
+    std::map<std::string, std::string> index_options;
+    PAIMON_ASSIGN_OR_RAISE(index_options,
+                           FileIndexOptions::RewriteFieldNames(
+                               split_read_.context_->GetCoreOptions().ToMap(), 
field_names));
+    PAIMON_ASSIGN_OR_RAISE(indexes_, FileIndexEvaluator::Create(
+                                         data_schema, index_options, 
path_factory_, file_,
+                                         
split_read_.context_->GetCoreOptions().GetFileSystem(),
+                                         /*only_use_embedding_index=*/false, 
split_read_.pool_));
+
+    std::map<std::string, DataField> read_by_name;
+    for (const auto& field : read_fields) {
+        read_by_name.emplace(field.Name(), field);
+    }
+    std::map<int32_t, std::pair<int32_t, DataField>> data_by_id;
+    for (size_t i = 0; i < data_fields.size(); ++i) {
+        data_by_id.emplace(data_fields[i].Id(), std::make_pair(i, 
data_fields[i]));
+    }
+    // Precheck the entire tree before an empty candidate set can 
short-circuit it.
+    PAIMON_RETURN_NOT_OK(
+        PreparePredicate(split_read_.context_->GetPredicate(), read_by_name, 
data_by_id));
+    return PrepareSearch(field_names);
+}
+
+Status FileIndexReadExecutor::PreparePredicate(
+    const std::shared_ptr<Predicate>& predicate,
+    const std::map<std::string, DataField>& read_by_name,
+    const std::map<int32_t, std::pair<int32_t, DataField>>& data_by_id) {
+    if (!predicate) {
+        return Status::OK();
+    }
+    if (auto compound = 
std::dynamic_pointer_cast<CompoundPredicate>(predicate)) {
+        const Function::Type type = compound->GetFunction().GetType();
+        if (type != Function::Type::AND && type != Function::Type::OR) {
+            return Status::Invalid(
+                fmt::format("unsupported compound file predicate {}", 
predicate->ToString()));
+        }
+        for (const auto& child : compound->Children()) {
+            PAIMON_RETURN_NOT_OK(PreparePredicate(child, read_by_name, 
data_by_id));
+        }
+        return Status::OK();
+    }
+    auto leaf = std::dynamic_pointer_cast<LeafPredicate>(predicate);
+    if (!leaf) {
+        return Status::Invalid("File predicate must contain leaf or compound 
predicates");
+    }
+    bool full_text = leaf->GetFunction().GetType() == 
Function::Type::FULL_TEXT_MATCH;
+    if (!full_text &&
+        ObjectUtils::Contains(split_read_.context_->GetPartitionKeys(), 
leaf->FieldName())) {
+        mapped_leaves_.emplace(leaf, nullptr);
+        return Status::OK();
+    }
+    PAIMON_ASSIGN_OR_RAISE(
+        std::optional<std::shared_ptr<Predicate>> mapped,
+        FieldMappingBuilder::ReconstructPredicateWithDataFields(leaf, 
read_by_name, data_by_id));
+    std::shared_ptr<LeafPredicate> mapped_leaf;
+    if (mapped) {
+        mapped_leaf = std::dynamic_pointer_cast<LeafPredicate>(*mapped);
+        if (!mapped_leaf) {
+            return Status::Invalid("Mapped file predicate must be a leaf");
+        }
+        if (full_text) {
+            PAIMON_ASSIGN_OR_RAISE(
+                [[maybe_unused]] std::shared_ptr<FileIndexReader> reader,
+                indexes_->GetSearchReader(mapped_leaf->FieldName(),
+                                          
FileIndexReader::SearchType::FULL_TEXT_MATCH));
+        }
+    }
+    mapped_leaves_.emplace(leaf, std::move(mapped_leaf));
+    return Status::OK();
+}
+
+Status FileIndexReadExecutor::PrepareSearch(const std::map<std::string, 
std::string>& field_names) {
+    if (split_read_.context_->GetVectorSearch()) {
+        vector_search_ = 
std::make_shared<VectorSearch>(*split_read_.context_->GetVectorSearch());
+        PAIMON_ASSIGN_OR_RAISE(
+            vector_search_->field_name,
+            ResolveSearchFieldName(vector_search_->field_name, field_names, 
file_->file_name));
+        PAIMON_ASSIGN_OR_RAISE(
+            [[maybe_unused]] std::shared_ptr<FileIndexReader> reader,
+            indexes_->GetSearchReader(vector_search_->field_name,
+                                      
FileIndexReader::SearchType::VECTOR_SEARCH));
+    }
+    if (split_read_.context_->GetFullTextSearch()) {
+        full_text_search_ =
+            
std::make_shared<FullTextSearch>(*split_read_.context_->GetFullTextSearch());
+        PAIMON_ASSIGN_OR_RAISE(
+            full_text_search_->field_name,
+            ResolveSearchFieldName(full_text_search_->field_name, field_names, 
file_->file_name));
+        PAIMON_ASSIGN_OR_RAISE(
+            [[maybe_unused]] std::shared_ptr<FileIndexReader> reader,
+            indexes_->GetSearchReader(full_text_search_->field_name,
+                                      
FileIndexReader::SearchType::FULL_TEXT_SEARCH));
+    }
+    return Status::OK();
+}
+
+Result<RoaringBitmap32> FileIndexReadExecutor::CreateCandidates(
+    const DeletionVector::Factory& dv_factory,
+    const std::optional<std::vector<Range>>& row_ranges) const {
+    RoaringBitmap32 candidates;
+    if (row_ranges) {
+        for (const auto& range : *row_ranges) {
+            candidates.AddRange(static_cast<int32_t>(range.from),
+                                static_cast<int32_t>(range.to + 1));
+        }
+    } else {
+        candidates.AddRange(0, static_cast<int32_t>(file_->row_count));
+    }
+    if (dv_factory) {
+        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<DeletionVector> dv, 
dv_factory(file_->file_name));
+        if (dynamic_cast<Bitmap64DeletionVector*>(dv.get())) {
+            return Status::NotImplemented(
+                "Precise file-index evaluation does not support bitmap64 
deletion vectors");
+        }
+        if (auto bitmap = dynamic_cast<BitmapDeletionVector*>(dv.get())) {
+            candidates -= *bitmap->GetBitmap();
+        } else if (dv) {
+            return Status::NotImplemented(
+                "Precise file-index evaluation requires bitmap32 deletion 
vectors");
+        }
+    }
+    return candidates;
+}
+
+Result<FileIndexReadExecutor::Evaluation> FileIndexReadExecutor::Evaluate(
+    const std::shared_ptr<Predicate>& predicate, const RoaringBitmap32& 
candidates) const {
+    if (!predicate || candidates.IsEmpty()) {
+        return Evaluation{candidates, {}};
+    }
+    if (auto leaf = std::dynamic_pointer_cast<LeafPredicate>(predicate)) {
+        return EvaluateLeaf(leaf, candidates);
+    }
+    if (auto compound = 
std::dynamic_pointer_cast<CompoundPredicate>(predicate)) {
+        return EvaluateCompound(compound, candidates);
+    }
+    return Status::Invalid(fmt::format("unsupported file predicate {}", 
predicate->ToString()));
+}
+
+Result<FileIndexReadExecutor::Evaluation> 
FileIndexReadExecutor::EvaluateCompound(
+    const std::shared_ptr<CompoundPredicate>& predicate, const 
RoaringBitmap32& candidates) const {
+    const Function::Type type = predicate->GetFunction().GetType();
+    if (type == Function::Type::OR) {
+        Evaluation result;
+        for (const auto& child : predicate->Children()) {
+            // Every branch must see the same input, including rows matched by 
earlier branches.
+            PAIMON_ASSIGN_OR_RAISE(Evaluation child_result, Evaluate(child, 
candidates));
+            result.rows |= child_result.rows;
+            result.scores.insert(child_result.scores.begin(), 
child_result.scores.end());
+        }
+        return result;
+    }
+    if (type != Function::Type::AND) {
+        return Status::Invalid(
+            fmt::format("unsupported compound file predicate {}", 
predicate->ToString()));
+    }
+
+    struct Child {
+        std::shared_ptr<Predicate> predicate;
+        std::optional<double> selectivity;
+    };
+    std::vector<Child> children;
+    children.reserve(predicate->Children().size());
+    for (const auto& child : predicate->Children()) {
+        PAIMON_ASSIGN_OR_RAISE(std::optional<double> selectivity, 
EstimateSelectivity(child));
+        children.push_back({child, selectivity});
+    }
+    std::stable_sort(children.begin(), children.end(), [](const Child& lhs, 
const Child& rhs) {
+        if (!lhs.selectivity) {
+            return false;
+        }
+        return !rhs.selectivity || *lhs.selectivity < *rhs.selectivity;
+    });
+
+    Evaluation result{candidates, {}};
+    for (const auto& child : children) {
+        PAIMON_ASSIGN_OR_RAISE(Evaluation child_result, 
Evaluate(child.predicate, result.rows));
+        result.rows = std::move(child_result.rows);
+        result.scores.insert(child_result.scores.begin(), 
child_result.scores.end());
+        if (result.rows.IsEmpty()) {
+            break;
+        }
+    }
+    return result;
+}
+
+Result<std::optional<double>> FileIndexReadExecutor::EstimateSelectivity(
+    const std::shared_ptr<Predicate>& predicate) const {
+    if (!predicate) {
+        return std::optional<double>();
+    }
+    if (auto leaf = std::dynamic_pointer_cast<LeafPredicate>(predicate)) {
+        PAIMON_ASSIGN_OR_RAISE(std::optional<double> estimate, 
EstimateLeafSelectivity(leaf));
+        if (estimate && (!std::isfinite(*estimate) || *estimate < 0.0 || 
*estimate > 1.0)) {
+            return std::optional<double>();
+        }
+        return estimate;
+    }
+    auto compound = std::dynamic_pointer_cast<CompoundPredicate>(predicate);
+    if (!compound) {
+        return Status::Invalid(fmt::format("unsupported file predicate {}", 
predicate->ToString()));
+    }
+    const Function::Type type = compound->GetFunction().GetType();
+    if (type != Function::Type::AND && type != Function::Type::OR) {
+        return Status::Invalid(
+            fmt::format("unsupported compound file predicate {}", 
predicate->ToString()));
+    }
+    std::optional<double> estimate =
+        type == Function::Type::AND ? std::nullopt : std::make_optional(0.0);
+    // These are scheduling hints. Continue visiting all children to propagate 
errors.
+    for (const auto& child : compound->Children()) {
+        PAIMON_ASSIGN_OR_RAISE(std::optional<double> child_estimate, 
EstimateSelectivity(child));
+        if (type == Function::Type::AND) {
+            if (child_estimate) {
+                if (estimate.has_value()) {
+                    estimate = std::min(*estimate, *child_estimate);
+                } else {
+                    estimate = *child_estimate;
+                }
+            }
+        } else if (estimate && child_estimate) {
+            estimate = std::min(1.0, *estimate + *child_estimate);
+        } else {
+            estimate = std::nullopt;
+        }
+    }
+    return estimate;
+}
+
+Result<std::optional<double>> FileIndexReadExecutor::EstimateLeafSelectivity(
+    const std::shared_ptr<LeafPredicate>& predicate) const {
+    auto iter = mapped_leaves_.find(predicate);
+    if (iter == mapped_leaves_.end()) {
+        return Status::Invalid("File predicate mapping was not prepared");
+    }
+    if (!iter->second) {
+        return std::optional<double>();
+    }
+    return indexes_->EstimateSelectivity(iter->second);
+}
+
+Result<FileIndexReadExecutor::Evaluation> FileIndexReadExecutor::EvaluateLeaf(
+    const std::shared_ptr<LeafPredicate>& predicate, const RoaringBitmap32& 
candidates) const {
+    bool is_full_text = predicate->GetFunction().GetType() == 
Function::Type::FULL_TEXT_MATCH;
+    bool needs_scores =
+        is_full_text &&
+        split_read_.context_->NeedsScores(
+            {InternalReadContext::IndexScoreSource::Type::FULL_TEXT_MATCH, 
predicate->FieldName()});
+    std::shared_ptr<FileIndexResult> index_result = FileIndexResult::Remain();
+    auto iter = mapped_leaves_.find(predicate);
+    if (iter == mapped_leaves_.end()) {
+        return Status::Invalid("File predicate mapping was not prepared");
+    }
+    if (iter->second) {
+        PAIMON_ASSIGN_OR_RAISE(index_result, 
indexes_->EvaluateLeaf(iter->second, needs_scores));
+    }
+    if (!index_result) {
+        return Status::Invalid("file predicate index returned a null result");
+    }
+    Evaluation result{candidates, {}};
+    if (needs_scores) {
+        auto scored = 
std::dynamic_pointer_cast<ScoredFileIndexResult>(index_result);
+        if (!scored) {
+            return Status::Invalid(
+                fmt::format("full-text index for field '{}' did not return 
requested scores",
+                            predicate->FieldName()));
+        }
+        result.rows &= scored->GetRowPositions();
+        result.scores.emplace(predicate->FieldName(), std::move(scored));
+        return result;
+    }
+
+    if (auto bitmap = 
std::dynamic_pointer_cast<BitmapIndexResult>(index_result)) {
+        PAIMON_ASSIGN_OR_RAISE(const RoaringBitmap32* rows, 
bitmap->GetBitmap());
+        result.rows &= *rows;
+    } else if (is_full_text) {
+        return Status::Invalid(fmt::format("full-text index for field '{}' did 
not return a bitmap",
+                                           predicate->FieldName()));
+    } else if (std::dynamic_pointer_cast<Skip>(index_result)) {
+        return Evaluation{};
+    } else if (!std::dynamic_pointer_cast<Remain>(index_result)) {
+        return Status::Invalid(
+            fmt::format("invalid index result for predicate {}", 
predicate->ToString()));
+    }
+    // TODO(lisizhuo.lsz): Whether to ScanScalar depends on whether the index 
returns exact results.
+    if (!is_full_text && !result.rows.IsEmpty()) {
+        // Scalar indexes may contain false positives, even if they return a 
bitmap.
+        PAIMON_ASSIGN_OR_RAISE(RoaringBitmap32 exact_rows, 
ScanScalar(predicate, result.rows));
+        result.rows &= exact_rows;
+    }
+    return result;
+}
+
+Result<std::unique_ptr<FileBatchReader>> 
FileIndexReadExecutor::CreateSelectedFileReader(
+    const RoaringBitmap32& selection, const FieldMappingBuilder* 
field_mapping_builder) const {
+    PAIMON_ASSIGN_OR_RAISE(std::string format, file_->FileFormat());
+    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ReaderBuilder> builder,
+                           split_read_.PrepareReaderBuilder(format, 
extra_format_options_));
+    return split_read_.CreateFieldMappingReader(path_factory_->ToPath(file_), 
file_, partition_,
+                                                std::move(builder), 
field_mapping_builder, nullptr,
+                                                std::nullopt, path_factory_, 
&selection);
+}
+
+Result<RoaringBitmap32> FileIndexReadExecutor::ScanScalar(
+    const std::shared_ptr<LeafPredicate>& predicate, const RoaringBitmap32& 
input) const {
+    auto field = 
split_read_.context_->GetReadSchema()->GetFieldByName(predicate->FieldName());
+    if (!field) {
+        return Status::Invalid(fmt::format(
+            "Scalar predicate field '{}' is missing from the read schema", 
predicate->FieldName()));
+    }
+    PAIMON_ASSIGN_OR_RAISE(
+        std::unique_ptr<FieldMappingBuilder> field_mapping_builder,
+        FieldMappingBuilder::Create(arrow::schema({field}),
+                                    split_read_.context_->GetPartitionKeys(), 
nullptr));
+    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileBatchReader> reader,
+                           CreateSelectedFileReader(input, 
field_mapping_builder.get()));
+    FileBatchReader& file_reader = *reader;
+    PAIMON_ASSIGN_OR_RAISE(
+        std::unique_ptr<PredicateBatchReader> filtered_reader,
+        PredicateBatchReader::Create(std::move(reader), predicate,
+                                     
split_read_.context_->GetArrowMemoryPool()));
+    ScopeGuard close([&]() { filtered_reader->Close(); });
+    RoaringBitmap32 matches;
+    while (true) {
+        PAIMON_ASSIGN_OR_RAISE(BatchReader::ReadBatchWithBitmap batch,
+                               filtered_reader->NextBatchWithBitmap());
+        if (BatchReader::IsEofBatch(batch)) {
+            break;
+        }

Review Comment:
   Please release the returned Arrow C array and schema for every non-EOF 
batch, including error exits below. This loop only consumes the bitmap and row 
positions: it neither imports the batch into Arrow objects nor invokes the C 
Data Interface release callbacks. `ReadBatch` uses ordinary 
`std::unique_ptr<ArrowArray>` / `std::unique_ptr<ArrowSchema>`, whose 
destructors delete only the outer structs, so the exported buffers and schema 
allocations leak for every matching batch. Closing the reader does not reclaim 
these exported batches. A scope guard around both release callbacks would cover 
normal iteration and early returns; a multi-batch allocation/release regression 
test would help verify it.



##########
src/paimon/core/operation/abstract_split_read.cpp:
##########
@@ -204,10 +204,12 @@ Result<std::unique_ptr<FileBatchReader>> 
AbstractSplitRead::ApplyIndexAndDvReade
     std::shared_ptr<FileIndexResult> file_index_result;
     if (options_.FileIndexReadEnabled() &&
         dynamic_cast<Bitmap64DeletionVector*>(deletion_vector.get()) == 
nullptr) {
-        PAIMON_ASSIGN_OR_RAISE(file_index_result,
-                               FileIndexEvaluator::Evaluate(data_schema, 
file_index_options,
-                                                            predicate, 
data_file_path_factory, file,
-                                                            
options_.GetFileSystem(), pool_));
+        PAIMON_ASSIGN_OR_RAISE(
+            std::unique_ptr<FileIndexEvaluator> evaluator,
+            FileIndexEvaluator::Create(data_schema, file_index_options, 
data_file_path_factory,
+                                       file, options_.GetFileSystem(),
+                                       /*only_use_embedding_index=*/false, 
pool_));
+        PAIMON_ASSIGN_OR_RAISE(file_index_result, 
evaluator->Evaluate(predicate));

Review Comment:
   Please preserve the no-predicate fast path before creating the evaluator, or 
make index loading lazy. Previously `FileIndexEvaluator::Evaluate(nullptr)` 
returned before opening an index, but `Create()` now opens and parses the index 
container before the null-predicate check in `Evaluate()`. Since file-index 
reads are enabled by default, ordinary full scans now access every external 
index even with precise evaluation disabled; an unreadable or corrupt index can 
also fail a scan that does not need it. Please add a regression test asserting 
that a read without a predicate does not open its external index.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to