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

Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new f75e66f8810 [improvement](parquet) Prune array_contains with row group 
statistics (#67441)
f75e66f8810 is described below

commit f75e66f8810b1fa369c2f4d4003593be67fdd887
Author: Gabriel <[email protected]>
AuthorDate: Mon Sep 7 10:59:19 2026 +0800

    [improvement](parquet) Prune array_contains with row group statistics 
(#67441)
    
    ## Summary
    
    - Enable `array_contains(array_column, constant)` to participate in
    expression-level ZoneMap evaluation for supported primitive array
    elements.
    - Resolve the logical ARRAY slot to its physical Parquet LIST element
    leaf in File Scanner V2 and use row-group footer min/max statistics to
    reject impossible matches.
    - Keep missing, incompatible, or unsafe statistics conservative, and
    deliberately exclude repeated leaves from page-index pruning because
    pages do not preserve parent-row boundaries.
    - Generalize the nested metadata probe path so Parquet Bloom and ZoneMap
    pruning share the same validated physical leaf resolution.
    - Preserve scalar Page Index pruning when the predicate is wrapped by
    `RuntimeFilterExpr`.
    - Keep Page Index scan test doubles structurally equivalent to
    production scalar comparisons so the physical probe classifier sees
    their slot and literal children.
    
    ## Fix boundary
    
    - The metadata-safe `array_contains` opt-in is limited to signatures
    accepted by the current BE execution dispatch. Complex ARRAY, MAP, and
    STRUCT element signatures remain unsupported and are kept outside the
    safe prefix so metadata pruning cannot suppress their residual runtime
    error.
    - `RuntimeFilterExpr` is unwrapped only for Page Index capability and
    path classification. The original wrapper remains in the scan request
    and is used for actual evaluation.
    - This change does not add Page Index pruning for repeated ARRAY leaves;
    `array_contains` continues to use row-group footer statistics only.
    - The TeamCity follow-up changes only test fixtures. The production Page
    Index admission gate remains conservative.
    
    ## Testing
    
    - Added RED/GREEN coverage for complex-element `array_contains`
    safe-prefix rejection.
    - Added RED/GREEN coverage for runtime-filter-only Page Index admission
    and range selection.
    - Reproduced all three BE UT failures from TeamCity build 1037684
    locally before the test-fixture fix; all three pass afterward.
    - `TableReaderTest.*`, `NativeParquetStatisticsTest.*`,
    `ExprZonemapFilterTest.*`, and `ParquetBloomFilterPruningTest.*` (194
    related tests passed).
    - Page Index coverage from `NewParquetReaderTest` and `ParquetScanTest`
    (130 related tests passed).
    - clang-format 16 dry run on all changed C/C++ files.
    - `build-support/check-build-hygiene.sh`.
    - `git diff --check`.
    
    ## Links
    
    None
---
 be/src/exprs/expr_zonemap_filter.cpp               | 123 +++++++++++--
 be/src/exprs/expr_zonemap_filter.h                 |  27 ++-
 be/src/exprs/function/array/function_array_index.h |  17 ++
 be/src/exprs/vectorized_fn_call.cpp                |  34 +++-
 be/src/exprs/vin_predicate.cpp                     |   4 +-
 be/src/format_v2/parquet/parquet_statistics.cpp    | 112 ++++++++---
 be/test/exprs/expr_zonemap_filter_test.cpp         |  56 +++++-
 be/test/format_v2/parquet/parquet_reader_test.cpp  |   9 +-
 be/test/format_v2/parquet/parquet_scan_test.cpp    |   9 +-
 .../format_v2/parquet/parquet_statistics_test.cpp  | 205 ++++++++++++++++++++-
 be/test/format_v2/table_reader_test.cpp            |  78 ++++++++
 11 files changed, 610 insertions(+), 64 deletions(-)

diff --git a/be/src/exprs/expr_zonemap_filter.cpp 
b/be/src/exprs/expr_zonemap_filter.cpp
index 878f4c8908c..339123c7aad 100644
--- a/be/src/exprs/expr_zonemap_filter.cpp
+++ b/be/src/exprs/expr_zonemap_filter.cpp
@@ -28,6 +28,7 @@
 #include "common/config.h"
 #include "common/logging.h"
 #include "core/column/column.h"
+#include "core/data_type/data_type_array.h"
 #include "core/data_type/data_type_nullable.h"
 #include "core/string_ref.h"
 #include "exprs/hybrid_set.h"
@@ -104,7 +105,7 @@ bool floating_point_bloom_filter_may_contain(const 
segment_v2::BloomFilter& bloo
     return value == T {0} && test_value(-value);
 }
 
-bool bloom_filter_probes_equal(const BloomFilterProbe& lhs, const 
BloomFilterProbe& rhs) {
+bool metadata_probes_equal(const MetadataProbe& lhs, const MetadataProbe& rhs) 
{
     return lhs.slot_index == rhs.slot_index && lhs.path == rhs.path &&
            data_types_compatible(lhs.value_type, rhs.value_type);
 }
@@ -249,12 +250,71 @@ std::optional<SlotLiteral> extract_slot_and_literal(const 
VExprSPtrs& args) {
     return std::nullopt;
 }
 
-std::optional<BloomFilterProbe> extract_bloom_filter_probe(const VExprSPtr& 
expr) {
+std::optional<SlotLiteral> extract_array_contains_slot_and_literal(const 
VExprSPtrs& args) {
+    if (args.size() != 2) {
+        return std::nullopt;
+    }
+    auto probe = extract_metadata_probe(args[0]);
+    auto literal = field_from_literal_expr(args[1]);
+    if (!probe.has_value() || !literal.has_value()) {
+        return std::nullopt;
+    }
+    const auto array_type = remove_nullable(probe->value_type);
+    if (array_type == nullptr || array_type->get_primitive_type() != 
TYPE_ARRAY) {
+        return std::nullopt;
+    }
+    const auto& element_type = assert_cast<const 
DataTypeArray&>(*array_type).get_nested_type();
+    if (element_type == nullptr ||
+        is_complex_type(remove_nullable(element_type)->get_primitive_type())) {
+        return std::nullopt;
+    }
+    auto [literal_value, literal_type] = std::move(*literal);
+    return SlotLiteral {.slot_index = probe->slot_index,
+                        .slot_type = element_type,
+                        .literal = std::move(literal_value),
+                        .literal_type = std::move(literal_type),
+                        .literal_on_left = false};
+}
+
+ZoneMapFilterResult evaluate_array_contains_zonemap(const ZoneMapEvalContext& 
ctx,
+                                                    const VExprSPtrs& args) {
+    auto slot_literal = extract_array_contains_slot_and_literal(args);
+    DORIS_CHECK(slot_literal.has_value());
+    auto slot_type = ctx.data_type(slot_literal->slot_index);
+    if (slot_type == nullptr || !data_types_compatible(slot_type, 
slot_literal->slot_type)) {
+        // Shared Segment and legacy Parquet callers bind the logical ARRAY, 
while Format V2 binds
+        // its repeated element; only the latter context can safely evaluate 
element statistics.
+        return unsupported_zonemap_filter(ctx);
+    }
+    auto zone_map_ref = ctx.zone_map(slot_literal->slot_index);
+    if (zone_map_ref == nullptr) {
+        return unsupported_zonemap_filter(ctx);
+    }
+    const auto& zone_map = *zone_map_ref;
+    if (!zone_map.has_not_null) {
+        return ZoneMapFilterResult::kNoMatch;
+    }
+    if (!range_stats_usable_for_zonemap(zone_map, slot_type)) {
+        return unsupported_zonemap_filter(ctx);
+    }
+    return slot_literal->literal < zone_map.min_value || zone_map.max_value < 
slot_literal->literal
+                   ? ZoneMapFilterResult::kNoMatch
+                   : ZoneMapFilterResult::kMayMatch;
+}
+
+bool can_evaluate_array_contains_zonemap(const VExprSPtrs& args) {
+    auto slot_literal = extract_array_contains_slot_and_literal(args);
+    return slot_literal.has_value() && !slot_literal->literal.is_null() &&
+           !slot_literal->literal.is_nan() &&
+           data_types_compatible(slot_literal->slot_type, 
slot_literal->literal_type);
+}
+
+std::optional<MetadataProbe> extract_metadata_probe(const VExprSPtr& expr) {
     if (expr == nullptr || expr->data_type() == nullptr) {
         return std::nullopt;
     }
     if (auto slot = std::dynamic_pointer_cast<VSlotRef>(expr); slot) {
-        return BloomFilterProbe {
+        return MetadataProbe {
                 .slot_index = slot->column_id(), .value_type = 
slot->data_type(), .path = {}};
     }
     if ((expr->fn().name.function_name != "element_at" &&
@@ -263,7 +323,7 @@ std::optional<BloomFilterProbe> 
extract_bloom_filter_probe(const VExprSPtr& expr
         return std::nullopt;
     }
 
-    auto probe = extract_bloom_filter_probe(expr->get_child(0));
+    auto probe = extract_metadata_probe(expr->get_child(0));
     auto selector = field_from_literal_expr(expr->get_child(1));
     if (!probe.has_value() || !selector.has_value() || 
selector->first.is_null()) {
         return std::nullopt;
@@ -273,10 +333,10 @@ std::optional<BloomFilterProbe> 
extract_bloom_filter_probe(const VExprSPtr& expr
         return std::nullopt;
     }
 
-    BloomFilterPathElement path_element;
+    MetadataPathElement path_element;
     switch (parent_type->get_primitive_type()) {
     case TYPE_STRUCT: {
-        path_element.kind = BloomFilterPathKind::STRUCT_FIELD;
+        path_element.kind = MetadataPathKind::STRUCT_FIELD;
         const auto selector_type = remove_nullable(selector->second);
         if (selector_type == nullptr) {
             return std::nullopt;
@@ -295,7 +355,7 @@ std::optional<BloomFilterProbe> 
extract_bloom_filter_probe(const VExprSPtr& expr
     case TYPE_ARRAY:
         // Array element positions share one repeated Parquet leaf; membership 
in that leaf is a
         // necessary condition for any element_at(array, constant) equality to 
match.
-        path_element.kind = BloomFilterPathKind::LIST_ELEMENT;
+        path_element.kind = MetadataPathKind::LIST_ELEMENT;
         break;
     default:
         return std::nullopt;
@@ -305,11 +365,28 @@ std::optional<BloomFilterProbe> 
extract_bloom_filter_probe(const VExprSPtr& expr
     return probe;
 }
 
-bool collect_unique_bloom_filter_probe(const VExprSPtr& expr,
-                                       std::optional<BloomFilterProbe>* 
result) {
+bool collect_unique_metadata_probe(const VExprSPtr& expr, bool zonemap,
+                                   std::optional<MetadataProbe>* result) {
     DORIS_CHECK(result != nullptr);
-    if (auto probe = extract_bloom_filter_probe(expr); probe.has_value()) {
-        if (result->has_value() && !bloom_filter_probes_equal(**result, 
*probe)) {
+    std::optional<MetadataProbe> probe;
+    if (zonemap && expr != nullptr && expr->fn().name.function_name == 
"array_contains" &&
+        expr->get_num_children() == 2) {
+        probe = extract_metadata_probe(expr->get_child(0));
+        if (probe.has_value()) {
+            const auto array_type = remove_nullable(probe->value_type);
+            if (array_type == nullptr || array_type->get_primitive_type() != 
TYPE_ARRAY) {
+                return false;
+            }
+            probe->value_type = assert_cast<const 
DataTypeArray&>(*array_type).get_nested_type();
+            MetadataPathElement path_element;
+            path_element.kind = MetadataPathKind::LIST_ELEMENT;
+            probe->path.push_back(std::move(path_element));
+        }
+    } else {
+        probe = extract_metadata_probe(expr);
+    }
+    if (probe.has_value()) {
+        if (result->has_value() && !metadata_probes_equal(**result, *probe)) {
             return false;
         }
         *result = std::move(probe);
@@ -323,18 +400,26 @@ bool collect_unique_bloom_filter_probe(const VExprSPtr& 
expr,
         if (child == nullptr || child->is_literal()) {
             continue;
         }
-        // Every Bloom-capable branch must bind to the same leaf; a 
conflicting subtree cannot be
-        // treated like a branch without a probe because the compound 
evaluator would use it.
-        if (!collect_unique_bloom_filter_probe(child, result)) {
+        // Every metadata-capable branch must bind to the same leaf; a 
conflicting subtree cannot
+        // be treated like a branch without a probe because the compound 
evaluator would use it.
+        if (!collect_unique_metadata_probe(child, zonemap, result)) {
             return false;
         }
     }
     return true;
 }
 
-std::optional<BloomFilterProbe> extract_bloom_filter_predicate_probe(const 
VExprSPtr& expr) {
-    std::optional<BloomFilterProbe> result;
-    if (!collect_unique_bloom_filter_probe(expr, &result)) {
+std::optional<MetadataProbe> extract_bloom_filter_predicate_probe(const 
VExprSPtr& expr) {
+    std::optional<MetadataProbe> result;
+    if (!collect_unique_metadata_probe(expr, false, &result)) {
+        return std::nullopt;
+    }
+    return result;
+}
+
+std::optional<MetadataProbe> extract_zonemap_filter_predicate_probe(const 
VExprSPtr& expr) {
+    std::optional<MetadataProbe> result;
+    if (!collect_unique_metadata_probe(expr, true, &result)) {
         return std::nullopt;
     }
     return result;
@@ -345,7 +430,7 @@ std::optional<SlotLiteral> 
extract_bloom_filter_slot_and_literal(const VExprSPtr
         return std::nullopt;
     }
     for (size_t probe_idx = 0; probe_idx < args.size(); ++probe_idx) {
-        auto probe = extract_bloom_filter_probe(args[probe_idx]);
+        auto probe = extract_metadata_probe(args[probe_idx]);
         auto literal = field_from_literal_expr(args[1 - probe_idx]);
         if (!probe.has_value() || !literal.has_value()) {
             continue;
@@ -551,7 +636,7 @@ ZoneMapFilterResult eval_in_bloom_filter(const 
BloomFilterEvalContext& ctx,
     if (is_not_in) {
         return ZoneMapFilterResult::kUnsupported;
     }
-    auto probe = extract_bloom_filter_probe(slot_expr);
+    auto probe = extract_metadata_probe(slot_expr);
     DORIS_CHECK(probe.has_value());
     const auto* slot_filter = ctx.slot(probe->slot_index);
     if (slot_filter == nullptr || slot_filter->data_type == nullptr ||
diff --git a/be/src/exprs/expr_zonemap_filter.h 
b/be/src/exprs/expr_zonemap_filter.h
index fb0a92f7c35..311d0fb8c65 100644
--- a/be/src/exprs/expr_zonemap_filter.h
+++ b/be/src/exprs/expr_zonemap_filter.h
@@ -97,32 +97,41 @@ struct SlotLiteral {
     bool literal_on_left;
 };
 
-enum class BloomFilterPathKind {
+enum class MetadataPathKind {
     STRUCT_FIELD,
     LIST_ELEMENT,
 };
 
-struct BloomFilterPathElement {
-    BloomFilterPathKind kind;
+struct MetadataPathElement {
+    MetadataPathKind kind;
     std::string field_name;
     int32_t field_ordinal = -1;
 
-    bool operator==(const BloomFilterPathElement&) const = default;
+    bool operator==(const MetadataPathElement&) const = default;
 };
 
-struct BloomFilterProbe {
+struct MetadataProbe {
     int slot_index;
     DataTypePtr value_type;
-    std::vector<BloomFilterPathElement> path;
+    std::vector<MetadataPathElement> path;
 
-    bool operator==(const BloomFilterProbe&) const = default;
+    bool operator==(const MetadataProbe&) const = default;
 };
 
 std::optional<SlotLiteral> extract_slot_and_literal(const VExprSPtrs& args);
 
-std::optional<BloomFilterProbe> extract_bloom_filter_probe(const VExprSPtr& 
expr);
+std::optional<SlotLiteral> extract_array_contains_slot_and_literal(const 
VExprSPtrs& args);
 
-std::optional<BloomFilterProbe> extract_bloom_filter_predicate_probe(const 
VExprSPtr& expr);
+ZoneMapFilterResult evaluate_array_contains_zonemap(const ZoneMapEvalContext& 
ctx,
+                                                    const VExprSPtrs& args);
+
+bool can_evaluate_array_contains_zonemap(const VExprSPtrs& args);
+
+std::optional<MetadataProbe> extract_metadata_probe(const VExprSPtr& expr);
+
+std::optional<MetadataProbe> extract_bloom_filter_predicate_probe(const 
VExprSPtr& expr);
+
+std::optional<MetadataProbe> extract_zonemap_filter_predicate_probe(const 
VExprSPtr& expr);
 
 std::optional<SlotLiteral> extract_bloom_filter_slot_and_literal(const 
VExprSPtrs& args);
 
diff --git a/be/src/exprs/function/array/function_array_index.h 
b/be/src/exprs/function/array/function_array_index.h
index 0984e803792..f456b3b1629 100644
--- a/be/src/exprs/function/array/function_array_index.h
+++ b/be/src/exprs/function/array/function_array_index.h
@@ -22,6 +22,7 @@
 #include <stddef.h>
 
 #include <memory>
+#include <type_traits>
 #include <utility>
 
 #include "common/status.h"
@@ -45,6 +46,7 @@
 #include "core/field.h"
 #include "core/string_ref.h"
 #include "core/types.h"
+#include "exprs/expr_zonemap_filter.h"
 #include "exprs/function/function.h"
 #include "storage/index/index_reader_helper.h"
 #include "storage/index/inverted/inverted_index_query_type.h"
@@ -218,6 +220,21 @@ public:
         return _execute_dispatch(block, arguments, result, input_rows_count);
     }
 
+    ZoneMapFilterResult evaluate_zonemap_filter(const ZoneMapEvalContext& ctx,
+                                                const VExprSPtrs& arguments) 
const override {
+        if constexpr (!std::is_same_v<ConcreteAction, ArrayContainsAction>) {
+            return unsupported_zonemap_filter(ctx);
+        }
+        return expr_zonemap::evaluate_array_contains_zonemap(ctx, arguments);
+    }
+
+    bool can_evaluate_zonemap_filter(const VExprSPtrs& arguments) const 
override {
+        if constexpr (!std::is_same_v<ConcreteAction, ArrayContainsAction>) {
+            return false;
+        }
+        return expr_zonemap::can_evaluate_array_contains_zonemap(arguments);
+    }
+
 private:
     template <PrimitiveType PType>
     ColumnPtr _execute_view(const ColumnArrayView<PType>& array_view,
diff --git a/be/src/exprs/vectorized_fn_call.cpp 
b/be/src/exprs/vectorized_fn_call.cpp
index ec6881dbb84..0d1fbd5f77b 100644
--- a/be/src/exprs/vectorized_fn_call.cpp
+++ b/be/src/exprs/vectorized_fn_call.cpp
@@ -36,12 +36,14 @@
 #include "core/assert_cast.h"
 #include "core/block/block.h"
 #include "core/block/column_numbers.h"
+#include "core/call_on_type_index.h"
 #include "core/column/column.h"
 #include "core/column/column_array.h"
 #include "core/column/column_nullable.h"
 #include "core/column/column_vector.h"
 #include "core/data_type/data_type.h"
 #include "core/data_type/data_type_agg_state.h"
+#include "core/data_type/data_type_array.h"
 #include "core/types.h"
 #include "exec/common/util.hpp"
 #include "exec/pipeline/pipeline_task.h"
@@ -684,13 +686,35 @@ bool 
VectorizedFnCall::is_safe_to_execute_on_selected_rows() const {
                                                                     
"is_not_null_pred",
                                                                     
"element_at",
                                                                     
"struct_element"};
+    bool function_is_total = 
TOTAL_PREDICATE_FUNCTIONS.contains(_function_name);
+    if (_function_name == "array_contains" && get_num_children() == 2) {
+        const auto& left_child_type = get_child(0)->data_type();
+        const auto& right_child_type = get_child(1)->data_type();
+        if (left_child_type != nullptr && right_child_type != nullptr) {
+            const auto array_type = remove_nullable(left_child_type);
+            const auto right_type = remove_nullable(right_child_type);
+            if (array_type->get_primitive_type() == TYPE_ARRAY) {
+                const auto element_type = remove_nullable(
+                        assert_cast<const 
DataTypeArray&>(*array_type).get_nested_type());
+                const auto element_primitive_type = 
element_type->get_primitive_type();
+                const auto right_primitive_type = 
right_type->get_primitive_type();
+                const bool types_match = element_primitive_type == 
right_primitive_type ||
+                                         
(is_string_type(element_primitive_type) &&
+                                          
is_string_type(right_primitive_type));
+                // Match the executable dispatch domain so metadata pruning 
cannot hide an
+                // unsupported-signature error from a row rejected by a later 
predicate.
+                function_is_total =
+                        types_match && 
dispatch_switch_all(element_primitive_type,
+                                                           [](const auto&) { 
return true; });
+            }
+        }
+    }
     // Selected-row execution may hide data-dependent errors in rows rejected 
by an earlier
     // predicate. Keep function calls unsafe by default and opt in only 
operations that are total
-    // for their input domain. Accessors return NULL for absent elements, so 
admitting them keeps
-    // nested metadata predicates reachable without crossing an 
error-producing child such as
-    // gt(mod(x, -1), 0).
-    return TOTAL_PREDICATE_FUNCTIONS.contains(_function_name) &&
-           VExpr::is_safe_to_execute_on_selected_rows();
+    // for their input domain. Accessors return NULL for absent elements, and 
supported membership
+    // signatures are total, so admitting them keeps nested metadata 
predicates reachable without
+    // crossing an error-producing child such as gt(mod(x, -1), 0).
+    return function_is_total && VExpr::is_safe_to_execute_on_selected_rows();
 }
 
 bool VectorizedFnCall::equals(const VExpr& other) {
diff --git a/be/src/exprs/vin_predicate.cpp b/be/src/exprs/vin_predicate.cpp
index 0a0c43b7eaf..b7466d959e4 100644
--- a/be/src/exprs/vin_predicate.cpp
+++ b/be/src/exprs/vin_predicate.cpp
@@ -167,7 +167,7 @@ void VInPredicate::_prepare_zonemap_min_max(VExprContext* 
context) {
         return;
     }
 
-    auto bloom_probe = expr_zonemap::extract_bloom_filter_probe(_children[0]);
+    auto bloom_probe = expr_zonemap::extract_metadata_probe(_children[0]);
     if (!bloom_probe.has_value()) {
         return;
     }
@@ -225,7 +225,7 @@ ZoneMapFilterResult 
VInPredicate::evaluate_bloom_filter(const BloomFilterEvalCon
 
 bool VInPredicate::can_evaluate_bloom_filter() const {
     return _zonemap_min_max != nullptr && !_is_not_in &&
-           expr_zonemap::extract_bloom_filter_probe(get_child(0)).has_value();
+           expr_zonemap::extract_metadata_probe(get_child(0)).has_value();
 }
 
 bool VInPredicate::can_execute_on_raw_fixed_values(const DataTypePtr& 
data_type,
diff --git a/be/src/format_v2/parquet/parquet_statistics.cpp 
b/be/src/format_v2/parquet/parquet_statistics.cpp
index a894601a160..79cd23c5bb0 100644
--- a/be/src/format_v2/parquet/parquet_statistics.cpp
+++ b/be/src/format_v2/parquet/parquet_statistics.cpp
@@ -456,9 +456,9 @@ const ParquetColumnSchema* resolve_local_leaf_schema(
     return column_schema;
 }
 
-const ParquetColumnSchema* resolve_bloom_filter_leaf_schema(
+const ParquetColumnSchema* resolve_metadata_leaf_schema(
         const std::vector<std::unique_ptr<ParquetColumnSchema>>& schema,
-        const format::LocalColumnId file_column_id, const 
expr_zonemap::BloomFilterProbe& probe) {
+        const format::LocalColumnId file_column_id, const 
expr_zonemap::MetadataProbe& probe) {
     if (probe.path.empty()) {
         return resolve_local_leaf_schema(schema, file_column_id);
     }
@@ -472,7 +472,7 @@ const ParquetColumnSchema* resolve_bloom_filter_leaf_schema(
         if (column_schema == nullptr) {
             return nullptr;
         }
-        if (path_element.kind == 
expr_zonemap::BloomFilterPathKind::STRUCT_FIELD) {
+        if (path_element.kind == expr_zonemap::MetadataPathKind::STRUCT_FIELD) 
{
             if (column_schema->kind != ParquetColumnSchemaKind::STRUCT) {
                 return nullptr;
             }
@@ -863,6 +863,36 @@ bool has_expr_zonemap_filter(const 
format::FileScanRequest& request, const Runti
     return has_variant_shredded_filter(request);
 }
 
+bool can_evaluate_native_page_index(const VExprSPtr& expr) {
+    if (expr == nullptr || !expr->can_evaluate_zonemap_filter()) {
+        return false;
+    }
+    if (const auto impl = expr->get_impl(); impl != nullptr) {
+        // RuntimeFilterExpr keeps its predicate outside the inherited child 
list. Unwrap only for
+        // shape classification; pruning still evaluates the original wrapper 
and its semantics.
+        return can_evaluate_native_page_index(impl);
+    }
+    if (expr->op() == TExprOpcode::COMPOUND_AND) {
+        return std::ranges::any_of(expr->children(), 
can_evaluate_native_page_index);
+    }
+    if (expr->op() == TExprOpcode::COMPOUND_OR) {
+        return !expr->children().empty() &&
+               std::ranges::all_of(expr->children(), 
can_evaluate_native_page_index);
+    }
+    const auto probe = 
expr_zonemap::extract_zonemap_filter_predicate_probe(expr);
+    return probe.has_value() && probe->path.empty();
+}
+
+bool has_native_page_index_filter(const format::FileScanRequest& request) {
+    const auto conjuncts = metadata_pruning_conjuncts(request);
+    return std::ranges::any_of(conjuncts,
+                               [](const auto& conjunct) {
+                                   return conjunct != nullptr &&
+                                          
can_evaluate_native_page_index(conjunct->root());
+                               }) ||
+           has_variant_shredded_filter(request);
+}
+
 std::set<int> collect_expr_zonemap_slot_indexes(const VExprContextSPtrs& 
conjuncts) {
     std::set<int> slot_indexes;
     for (const auto& conjunct : conjuncts) {
@@ -933,9 +963,10 @@ void accumulate_zonemap_stats(const ZoneMapEvalContext& 
ctx, ParquetPruningStats
 
 } // namespace
 
-bool can_use_parquet_page_index(const format::FileScanRequest& request,
-                                const RuntimeState* runtime_state) {
-    return config::enable_parquet_page_index && 
has_expr_zonemap_filter(request, runtime_state);
+bool can_use_parquet_page_index(const format::FileScanRequest& request, const 
RuntimeState*) {
+    // Footer-only repeated paths cannot map physical value pages back to 
parent rows, so they must
+    // not trigger Page Index I/O unless another predicate can actually use 
the loaded indexes.
+    return config::enable_parquet_page_index && 
has_native_page_index_filter(request);
 }
 
 std::shared_ptr<segment_v2::ZoneMap> ParquetStatisticsUtils::MakeZoneMap(
@@ -1052,21 +1083,13 @@ bool check_native_statistics(const 
tparquet::FileMetaData& metadata,
                              const format::FileScanRequest& request,
                              ParquetPruningStats* pruning_stats, const 
cctz::time_zone* timezone) {
     const auto conjuncts = metadata_pruning_conjuncts(request);
-    const auto slot_indexes = collect_expr_zonemap_slot_indexes(conjuncts);
-    if (slot_indexes.empty()) {
-        return false;
-    }
-    ZoneMapEvalContext ctx;
-    for (const int slot_index : slot_indexes) {
-        const auto file_column_id = file_column_id_by_block_position(request, 
slot_index);
-        if (!file_column_id.has_value()) {
-            continue;
-        }
-        const auto* column_schema = resolve_local_leaf_schema(file_schema, 
*file_column_id);
+    const auto add_column_zonemap = [&](ZoneMapEvalContext* ctx, int 
slot_index,
+                                        const ParquetColumnSchema* 
column_schema) {
+        DORIS_CHECK(ctx != nullptr);
         if (column_schema == nullptr || column_schema->type == nullptr ||
             !native_metadata_predicate_is_type_safe(*column_schema) ||
             column_schema->leaf_column_id >= 
static_cast<int>(row_group.columns.size())) {
-            continue;
+            return;
         }
         const auto& chunk = row_group.columns[column_schema->leaf_column_id];
         std::shared_ptr<segment_v2::ZoneMap> zone_map;
@@ -1085,9 +1108,54 @@ bool check_native_statistics(const 
tparquet::FileMetaData& metadata,
                             safe_statistics.has_value() ? &*safe_statistics : 
nullptr,
                             column_metadata.num_values, timezone));
         }
-        add_slot_zonemap(&ctx, slot_index, column_schema->type, 
std::move(zone_map));
+        add_slot_zonemap(ctx, slot_index, column_schema->type, 
std::move(zone_map));
+    };
+
+    VExprContextSPtrs direct_conjuncts;
+    for (const auto& conjunct : conjuncts) {
+        if (conjunct == nullptr || conjunct->root() == nullptr ||
+            !conjunct->root()->can_evaluate_zonemap_filter()) {
+            direct_conjuncts.push_back(conjunct);
+            continue;
+        }
+        const auto probe = 
expr_zonemap::extract_zonemap_filter_predicate_probe(conjunct->root());
+        if (!probe.has_value() || probe->path.empty()) {
+            direct_conjuncts.push_back(conjunct);
+            continue;
+        }
+        const auto file_column_id = file_column_id_by_block_position(request, 
probe->slot_index);
+        // Row-group statistics safely summarize every repeated LIST value. 
Page-index pruning keeps
+        // using top-level leaves because repeated values do not preserve 
page-to-parent-row bounds.
+        const auto* column_schema =
+                file_column_id.has_value()
+                        ? resolve_metadata_leaf_schema(file_schema, 
*file_column_id, *probe)
+                        : nullptr;
+        ZoneMapEvalContext nested_ctx;
+        if (column_schema != nullptr &&
+            expr_zonemap::data_types_compatible(column_schema->type, 
probe->value_type)) {
+            add_column_zonemap(&nested_ctx, probe->slot_index, column_schema);
+        }
+        const auto result = VExprContext::evaluate_zonemap_filter({conjunct}, 
nested_ctx);
+        accumulate_zonemap_stats(nested_ctx, pruning_stats);
+        if (result == ZoneMapFilterResult::kNoMatch) {
+            return true;
+        }
+    }
+
+    const auto slot_indexes = 
collect_expr_zonemap_slot_indexes(direct_conjuncts);
+    if (slot_indexes.empty()) {
+        return false;
+    }
+    ZoneMapEvalContext ctx;
+    for (const int slot_index : slot_indexes) {
+        const auto file_column_id = file_column_id_by_block_position(request, 
slot_index);
+        if (!file_column_id.has_value()) {
+            continue;
+        }
+        const auto* column_schema = resolve_local_leaf_schema(file_schema, 
*file_column_id);
+        add_column_zonemap(&ctx, slot_index, column_schema);
     }
-    const auto result = VExprContext::evaluate_zonemap_filter(conjuncts, ctx);
+    const auto result = 
VExprContext::evaluate_zonemap_filter(direct_conjuncts, ctx);
     accumulate_zonemap_stats(ctx, pruning_stats);
     return result == ZoneMapFilterResult::kNoMatch;
 }
@@ -1316,9 +1384,7 @@ ParquetRowGroupPruneReason 
native_bloom_filter_prune_reason(
             continue;
         }
         const auto* column_schema =
-                probe->path.empty()
-                        ? resolve_local_leaf_schema(file_schema, 
*file_column_id)
-                        : resolve_bloom_filter_leaf_schema(file_schema, 
*file_column_id, *probe);
+                resolve_metadata_leaf_schema(file_schema, *file_column_id, 
*probe);
         if (column_schema == nullptr ||
             !expr_zonemap::data_types_compatible(column_schema->type, 
probe->value_type)) {
             continue;
diff --git a/be/test/exprs/expr_zonemap_filter_test.cpp 
b/be/test/exprs/expr_zonemap_filter_test.cpp
index f75502e1390..88dee1cb974 100644
--- a/be/test/exprs/expr_zonemap_filter_test.cpp
+++ b/be/test/exprs/expr_zonemap_filter_test.cpp
@@ -832,11 +832,11 @@ TEST(ExprZonemapFilterTest, 
EqualityBloomAcceptsStructAndListLeafAccessors) {
             "element_at", list_type, make_slot(0, nested_type), 
make_string_literal("items"));
     auto nested_leaf = std::make_shared<MetadataAccessorExpr>(
             "element_at", leaf_type, std::move(nested_list), 
make_int_literal(1));
-    auto nested_probe = expr_zonemap::extract_bloom_filter_probe(nested_leaf);
+    auto nested_probe = expr_zonemap::extract_metadata_probe(nested_leaf);
     ASSERT_TRUE(nested_probe.has_value());
     ASSERT_EQ(nested_probe->path.size(), 2);
-    EXPECT_EQ(nested_probe->path[0].kind, 
expr_zonemap::BloomFilterPathKind::STRUCT_FIELD);
-    EXPECT_EQ(nested_probe->path[1].kind, 
expr_zonemap::BloomFilterPathKind::LIST_ELEMENT);
+    EXPECT_EQ(nested_probe->path[0].kind, 
expr_zonemap::MetadataPathKind::STRUCT_FIELD);
+    EXPECT_EQ(nested_probe->path[1].kind, 
expr_zonemap::MetadataPathKind::LIST_ELEMENT);
     EXPECT_EQ(ZoneMapFilterResult::kNoMatch,
               equals.evaluate_bloom_filter(bloom_ctx, {nested_leaf, 
make_int_literal(2)}));
 }
@@ -1035,6 +1035,56 @@ TEST(ExprZonemapFilterTest, 
FunctionStringStartsWithZonemapUsesPrefixRange) {
                                                    {slot, 
make_string_literal(max_byte_prefix)}));
 }
 
+TEST(ExprZonemapFilterTest, FunctionArrayContainsZonemapUsesElementRange) {
+    auto element_type = int_type();
+    auto array_type = 
std::make_shared<DataTypeArray>(make_nullable(element_type));
+    auto slot = make_slot(0, array_type);
+    auto array_contains = SimpleFunctionFactory::instance().get_function(
+            "array_contains",
+            ColumnsWithTypeAndName {{nullptr, array_type, "slot"},
+                                    {nullptr, element_type, "needle"}},
+            std::make_shared<DataTypeUInt8>());
+    ASSERT_NE(array_contains, nullptr);
+    EXPECT_EQ("array_contains", array_contains->get_name());
+
+    EXPECT_TRUE(array_contains->can_evaluate_zonemap_filter({slot, 
make_int_literal(7)}));
+    EXPECT_FALSE(array_contains->can_evaluate_zonemap_filter({slot, 
make_null_int_literal()}));
+    EXPECT_FALSE(array_contains->can_evaluate_zonemap_filter(
+            {make_int_literal(7), make_int_literal(7)}));
+
+    auto below_range_ctx = make_context(make_int_zonemap(10, 20), 
element_type);
+    EXPECT_EQ(ZoneMapFilterResult::kNoMatch, 
array_contains->evaluate_zonemap_filter(
+                                                     below_range_ctx, {slot, 
make_int_literal(7)}));
+
+    auto above_range_ctx = make_context(make_int_zonemap(10, 20), 
element_type);
+    EXPECT_EQ(
+            ZoneMapFilterResult::kNoMatch,
+            array_contains->evaluate_zonemap_filter(above_range_ctx, {slot, 
make_int_literal(21)}));
+
+    auto overlap_ctx = make_context(make_int_zonemap(10, 20), element_type);
+    EXPECT_EQ(ZoneMapFilterResult::kMayMatch,
+              array_contains->evaluate_zonemap_filter(overlap_ctx, {slot, 
make_int_literal(15)}));
+
+    segment_v2::ZoneMap only_null;
+    only_null.has_null = true;
+    auto null_ctx = make_context(only_null, element_type);
+    EXPECT_EQ(ZoneMapFilterResult::kNoMatch,
+              array_contains->evaluate_zonemap_filter(null_ctx, {slot, 
make_int_literal(15)}));
+
+    auto missing_ctx = ZoneMapEvalContext {};
+    EXPECT_EQ(ZoneMapFilterResult::kUnsupported,
+              array_contains->evaluate_zonemap_filter(missing_ctx, {slot, 
make_int_literal(15)}));
+    EXPECT_EQ(1, missing_ctx.stats.unusable_zonemap_eval_count);
+
+    // Segment and legacy Parquet callers bind the logical ARRAY instead of 
the repeated element.
+    // The shared capability must fall back instead of treating that binding 
as an invariant breach.
+    auto logical_array_ctx = make_context(make_int_zonemap(10, 20), 
array_type);
+    EXPECT_EQ(ZoneMapFilterResult::kUnsupported,
+              array_contains->evaluate_zonemap_filter(logical_array_ctx,
+                                                      {slot, 
make_int_literal(15)}));
+    EXPECT_EQ(1, logical_array_ctx.stats.unusable_zonemap_eval_count);
+}
+
 TEST(ExprZonemapFilterTest, CharZonemapUsesTrimmedLogicalBounds) {
     auto char_type = std::make_shared<DataTypeString>(10, TYPE_CHAR);
     auto slot = make_slot(0, char_type);
diff --git a/be/test/format_v2/parquet/parquet_reader_test.cpp 
b/be/test/format_v2/parquet/parquet_reader_test.cpp
index d20a3fe8e94..38b6761c89d 100644
--- a/be/test/format_v2/parquet/parquet_reader_test.cpp
+++ b/be/test/format_v2/parquet/parquet_reader_test.cpp
@@ -114,7 +114,14 @@ public:
     Int32GreaterThanExpr(int column_id, int32_t value)
             : VExpr(std::make_shared<DataTypeUInt8>(), false),
               _column_id(column_id),
-              _value(value) {}
+              _value(value) {
+        _fn.name.function_name = "gt";
+        const auto int_type = std::make_shared<DataTypeInt32>();
+        // Keep the test double structurally equivalent to a scalar comparison 
because Page Index
+        // admission resolves the physical probe from expression children.
+        add_child(VSlotRef::create_shared(column_id, column_id, -1, int_type, 
"c0"));
+        add_child(VLiteral::create_shared(int_type, 
Field::create_field<TYPE_INT>(value)));
+    }
 
     Status execute_column_impl(VExprContext* context, const Block* block, 
const Selector* selector,
                                size_t count, ColumnPtr& result_column) const 
override {
diff --git a/be/test/format_v2/parquet/parquet_scan_test.cpp 
b/be/test/format_v2/parquet/parquet_scan_test.cpp
index 635355826a0..d12f78c9b10 100644
--- a/be/test/format_v2/parquet/parquet_scan_test.cpp
+++ b/be/test/format_v2/parquet/parquet_scan_test.cpp
@@ -174,7 +174,14 @@ public:
             : VExpr(std::make_shared<DataTypeUInt8>(), false),
               _column_id(column_id),
               _op(op),
-              _value(value) {}
+              _value(value) {
+        _fn.name.function_name = op == Op::GE ? "ge" : op == Op::GT ? "gt" : 
"lt";
+        const auto int_type = std::make_shared<DataTypeInt32>();
+        // Keep the test double structurally equivalent to a scalar comparison 
because Page Index
+        // admission resolves the physical probe from expression children.
+        add_child(VSlotRef::create_shared(column_id, column_id, -1, int_type, 
"c0"));
+        add_child(VLiteral::create_shared(int_type, 
Field::create_field<TYPE_INT>(value)));
+    }
 
     const std::string& expr_name() const override { return _expr_name; }
 
diff --git a/be/test/format_v2/parquet/parquet_statistics_test.cpp 
b/be/test/format_v2/parquet/parquet_statistics_test.cpp
index 48e4a157b94..4aa996ab00c 100644
--- a/be/test/format_v2/parquet/parquet_statistics_test.cpp
+++ b/be/test/format_v2/parquet/parquet_statistics_test.cpp
@@ -47,6 +47,7 @@
 #include "exprs/function/functions_comparison.h"
 #include "exprs/hybrid_set.h"
 #include "exprs/hybrid_set_min_max.h"
+#include "exprs/runtime_filter_expr.h"
 #include "exprs/vcompound_pred.h"
 #include "exprs/vexpr.h"
 #include "exprs/vexpr_context.h"
@@ -316,6 +317,39 @@ private:
     const std::string _expr_name = "MetadataAccessorExpr";
 };
 
+class MetadataArrayContainsExpr final : public VExpr {
+public:
+    MetadataArrayContainsExpr(DataTypePtr array_type, int32_t value)
+            : VExpr(std::make_shared<DataTypeUInt8>(), false) {
+        _fn.name.function_name = "array_contains";
+        add_child(VSlotRef::create_shared(0, 0, -1, std::move(array_type), 
"items"));
+        add_child(VLiteral::create_shared(std::make_shared<DataTypeInt32>(),
+                                          
Field::create_field<TYPE_INT>(value)));
+    }
+
+    const std::string& expr_name() const override { return _expr_name; }
+
+    Status execute_column_impl(VExprContext*, const Block*, const Selector*, 
size_t,
+                               ColumnPtr&) const override {
+        return Status::InternalError("MetadataArrayContainsExpr is 
metadata-only");
+    }
+
+    bool can_evaluate_zonemap_filter() const override {
+        return expr_zonemap::can_evaluate_array_contains_zonemap(_children);
+    }
+
+    ZoneMapFilterResult evaluate_zonemap_filter(const ZoneMapEvalContext& ctx) 
const override {
+        return expr_zonemap::evaluate_array_contains_zonemap(ctx, _children);
+    }
+
+    void collect_slot_column_ids(std::set<int>& column_ids) const override {
+        get_child(0)->collect_slot_column_ids(column_ids);
+    }
+
+private:
+    const std::string _expr_name = "MetadataArrayContainsExpr";
+};
+
 class DictionaryStringInExpr final : public VExpr {
 public:
     DictionaryStringInExpr() : VExpr(std::make_shared<DataTypeUInt8>(), false) 
{}
@@ -342,7 +376,12 @@ private:
 class MetadataInt32GreaterThanExpr final : public VExpr {
 public:
     explicit MetadataInt32GreaterThanExpr(int32_t value)
-            : VExpr(std::make_shared<DataTypeUInt8>(), false), _value(value) {}
+            : VExpr(std::make_shared<DataTypeUInt8>(), false), _value(value) {
+        _fn.name.function_name = "gt";
+        add_child(VSlotRef::create_shared(0, 0, -1, 
std::make_shared<DataTypeInt32>(), "c0"));
+        add_child(VLiteral::create_shared(std::make_shared<DataTypeInt32>(),
+                                          
Field::create_field<TYPE_INT>(value)));
+    }
 
     const std::string& expr_name() const override { return _expr_name; }
     Status execute_column_impl(VExprContext*, const Block*, const Selector*, 
size_t,
@@ -1820,6 +1859,170 @@ TEST(NativeParquetStatisticsTest, 
TypeDefinedBoundsRequireSupportedColumnOrder)
     EXPECT_TRUE(selected_ranges.empty());
 }
 
+TEST(NativeParquetStatisticsTest, 
RuntimeFilterWrapperKeepsScalarPageIndexPruning) {
+    auto encode_int32 = [](int32_t value) {
+        std::string bytes(sizeof(value), '\0');
+        memcpy(bytes.data(), &value, sizeof(value));
+        return bytes;
+    };
+
+    auto column_schema = 
std::make_unique<format::parquet::ParquetColumnSchema>();
+    column_schema->kind = format::parquet::ParquetColumnSchemaKind::PRIMITIVE;
+    column_schema->local_id = 0;
+    column_schema->leaf_column_id = 0;
+    column_schema->type = std::make_shared<DataTypeInt32>();
+    column_schema->type_descriptor.doris_type = column_schema->type;
+    column_schema->type_descriptor.physical_type = tparquet::Type::INT32;
+    std::vector<std::unique_ptr<format::parquet::ParquetColumnSchema>> schema;
+    schema.push_back(std::move(column_schema));
+
+    tparquet::ColumnChunk chunk;
+    tparquet::ColumnMetaData column_metadata;
+    column_metadata.__set_type(tparquet::Type::INT32);
+    column_metadata.__set_num_values(1);
+    chunk.__set_meta_data(column_metadata);
+    tparquet::RowGroup row_group;
+    row_group.__set_columns({chunk});
+    row_group.__set_num_rows(1);
+    tparquet::ColumnOrder order;
+    order.__set_TYPE_ORDER(tparquet::TypeDefinedOrder());
+    tparquet::FileMetaData metadata;
+    metadata.__set_column_orders({order});
+    metadata.__set_row_groups({row_group});
+
+    TExprNode runtime_filter_node;
+    
runtime_filter_node.__set_type(std::make_shared<DataTypeUInt8>()->to_thrift());
+    runtime_filter_node.__set_is_nullable(false);
+    auto runtime_filter = RuntimeFilterExpr::create_shared(
+            runtime_filter_node, 
std::make_shared<MetadataInt32GreaterThanExpr>(100), 0.0, false,
+            7);
+    format::FileScanRequest request;
+    request.local_positions.emplace(format::LocalColumnId(0), 
format::LocalIndex(0));
+    request.predicate_columns = 
{format::LocalColumnIndex::top_level(format::LocalColumnId(0))};
+    request.conjuncts = 
{VExprContext::create_shared(std::move(runtime_filter))};
+    EXPECT_TRUE(format::parquet::can_use_parquet_page_index(request, nullptr));
+
+    format::parquet::NativeParquetPageIndex page_index;
+    page_index.column_index.__set_min_values({encode_int32(1)});
+    page_index.column_index.__set_max_values({encode_int32(2)});
+    page_index.column_index.__set_null_pages({false});
+    page_index.column_index.__set_null_counts({0});
+    tparquet::PageLocation location;
+    location.__set_offset(0);
+    location.__set_compressed_page_size(10);
+    location.__set_first_row_index(0);
+    page_index.offset_index.__set_page_locations({location});
+    std::unordered_map<int, format::parquet::NativeParquetPageIndex> 
page_indexes;
+    page_indexes.emplace(0, std::move(page_index));
+    std::vector<format::parquet::RowRange> selected_ranges;
+    std::map<int, format::parquet::ParquetPageSkipPlan> skip_plans;
+    ASSERT_TRUE(format::parquet::select_row_group_ranges_by_native_page_index(
+                        metadata, metadata.row_groups[0], page_indexes, 
schema, request, 1,
+                        &selected_ranges, &skip_plans, nullptr)
+                        .ok());
+    EXPECT_TRUE(selected_ranges.empty());
+}
+
+TEST(NativeParquetStatisticsTest, 
ArrayContainsUsesRepeatedLeafFooterStatisticsOnly) {
+    const auto encode_int32 = [](int32_t value) {
+        std::string bytes(sizeof(value), '\0');
+        memcpy(bytes.data(), &value, sizeof(value));
+        return bytes;
+    };
+
+    const auto leaf_type = std::make_shared<DataTypeInt32>();
+    const auto array_type = std::make_shared<DataTypeArray>(leaf_type);
+    auto root_schema = 
std::make_unique<format::parquet::ParquetColumnSchema>();
+    root_schema->kind = format::parquet::ParquetColumnSchemaKind::LIST;
+    root_schema->local_id = 0;
+    root_schema->name = "items";
+    root_schema->type = array_type;
+    root_schema->max_repetition_level = 1;
+    auto leaf_schema = 
std::make_unique<format::parquet::ParquetColumnSchema>();
+    leaf_schema->kind = format::parquet::ParquetColumnSchemaKind::PRIMITIVE;
+    leaf_schema->local_id = 0;
+    leaf_schema->name = "element";
+    leaf_schema->leaf_column_id = 0;
+    leaf_schema->type = leaf_type;
+    leaf_schema->type_descriptor.doris_type = leaf_type;
+    leaf_schema->type_descriptor.physical_type = tparquet::Type::INT32;
+    leaf_schema->max_repetition_level = 1;
+    root_schema->children.push_back(std::move(leaf_schema));
+    std::vector<std::unique_ptr<format::parquet::ParquetColumnSchema>> schema;
+    schema.push_back(std::move(root_schema));
+
+    tparquet::Statistics statistics;
+    statistics.__set_min_value(encode_int32(10));
+    statistics.__set_max_value(encode_int32(20));
+    statistics.__set_null_count(0);
+    tparquet::ColumnMetaData column_metadata;
+    column_metadata.__set_type(tparquet::Type::INT32);
+    column_metadata.__set_num_values(2);
+    column_metadata.__set_statistics(statistics);
+    tparquet::ColumnChunk chunk;
+    chunk.__set_meta_data(column_metadata);
+    tparquet::RowGroup row_group;
+    row_group.__set_columns({chunk});
+    row_group.__set_num_rows(1);
+    tparquet::ColumnOrder order;
+    order.__set_TYPE_ORDER(tparquet::TypeDefinedOrder());
+    tparquet::FileMetaData metadata;
+    metadata.__set_column_orders({order});
+    metadata.__set_row_groups({row_group});
+
+    const auto select_for_value = [&](int32_t value,
+                                      format::parquet::ParquetPruningStats* 
pruning_stats) {
+        format::FileScanRequest request;
+        request.local_positions.emplace(format::LocalColumnId(0), 
format::LocalIndex(0));
+        request.predicate_columns = 
{format::LocalColumnIndex::top_level(format::LocalColumnId(0))};
+        request.conjuncts = {VExprContext::create_shared(
+                std::make_shared<MetadataArrayContainsExpr>(array_type, 
value))};
+        std::vector<int> selected_row_groups;
+        EXPECT_TRUE(format::parquet::select_row_groups_by_metadata(metadata, 
schema, request,
+                                                                   nullptr, 
&selected_row_groups,
+                                                                   false, 
pruning_stats)
+                            .ok());
+        return selected_row_groups;
+    };
+
+    format::parquet::ParquetPruningStats disjoint_stats;
+    EXPECT_TRUE(select_for_value(7, &disjoint_stats).empty());
+    EXPECT_EQ(disjoint_stats.filtered_row_groups_by_statistics, 1);
+    format::parquet::ParquetPruningStats overlap_stats;
+    EXPECT_EQ(select_for_value(15, &overlap_stats), std::vector<int>({0}));
+    metadata.row_groups[0].columns[0].meta_data.__isset.statistics = false;
+    format::parquet::ParquetPruningStats missing_stats;
+    EXPECT_EQ(select_for_value(7, &missing_stats), std::vector<int>({0}));
+    EXPECT_EQ(missing_stats.expr_zonemap_unusable_evals, 1);
+
+    format::FileScanRequest page_request;
+    page_request.local_positions.emplace(format::LocalColumnId(0), 
format::LocalIndex(0));
+    page_request.conjuncts = {VExprContext::create_shared(
+            std::make_shared<MetadataArrayContainsExpr>(array_type, 7))};
+    EXPECT_FALSE(format::parquet::can_use_parquet_page_index(page_request, 
nullptr));
+    format::parquet::NativeParquetPageIndex page_index;
+    page_index.column_index.__set_min_values({encode_int32(10)});
+    page_index.column_index.__set_max_values({encode_int32(20)});
+    page_index.column_index.__set_null_pages({false});
+    page_index.column_index.__set_null_counts({0});
+    tparquet::PageLocation location;
+    location.__set_offset(0);
+    location.__set_compressed_page_size(10);
+    location.__set_first_row_index(0);
+    page_index.offset_index.__set_page_locations({location});
+    std::unordered_map<int, format::parquet::NativeParquetPageIndex> 
page_indexes;
+    page_indexes.emplace(0, std::move(page_index));
+    std::vector<format::parquet::RowRange> selected_ranges;
+    std::map<int, format::parquet::ParquetPageSkipPlan> skip_plans;
+    ASSERT_TRUE(format::parquet::select_row_group_ranges_by_native_page_index(
+                        metadata, metadata.row_groups[0], page_indexes, 
schema, page_request, 1,
+                        &selected_ranges, &skip_plans, nullptr)
+                        .ok());
+    ASSERT_EQ(selected_ranges.size(), 1);
+    EXPECT_EQ(selected_ranges[0].start, 0);
+    EXPECT_EQ(selected_ranges[0].length, 1);
+}
+
 TEST(NativeParquetStatisticsTest, ZonemapPruningIgnoresDisabledSessionSwitch) {
     auto encode_int32 = [](int32_t value) {
         std::string bytes(sizeof(value), '\0');
diff --git a/be/test/format_v2/table_reader_test.cpp 
b/be/test/format_v2/table_reader_test.cpp
index 800f0040dd6..074479d5718 100644
--- a/be/test/format_v2/table_reader_test.cpp
+++ b/be/test/format_v2/table_reader_test.cpp
@@ -263,6 +263,17 @@ VExprSPtr table_int32_greater_than_expr(int slot_id, int 
column_id, int32_t valu
     return expr;
 }
 
+VExprSPtr table_array_contains_int32_expr(int slot_id, int column_id, const 
DataTypePtr& array_type,
+                                          int32_t value) {
+    const auto int_type = std::make_shared<DataTypeInt32>();
+    auto expr =
+            table_function_expr("array_contains", 
make_nullable(std::make_shared<DataTypeUInt8>()),
+                                {array_type, int_type});
+    expr->add_child(VSlotRef::create_shared(slot_id, column_id, slot_id, 
array_type, "items"));
+    expr->add_child(table_int32_literal(value));
+    return expr;
+}
+
 VExprSPtr table_struct_int32_child_greater_than_expr(int slot_id, int 
column_id,
                                                      const DataTypePtr& 
struct_type,
                                                      int32_t child_ordinal, 
int32_t value) {
@@ -1344,6 +1355,18 @@ public:
                 file_block->replace_by_position(
                         block_position.value(),
                         
make_not_null_nullable_column(std::move(struct_column)));
+            } else if (file_column_id == LocalColumnId(3)) {
+                auto values = ColumnInt32::create();
+                values->insert_value(1);
+                values->insert_value(2);
+                auto nullable_values = 
make_not_null_nullable_column(std::move(values));
+                auto offsets = ColumnArray::ColumnOffsets::create();
+                offsets->get_data().assign({1, 2});
+                auto array_column =
+                        ColumnArray::create(std::move(nullable_values), 
std::move(offsets));
+                file_block->replace_by_position(
+                        block_position.value(),
+                        
make_not_null_nullable_column(std::move(array_column)));
             } else {
                 return Status::InvalidArgument("Unexpected fake file column id 
{}",
                                                file_column_id.value());
@@ -1642,6 +1665,61 @@ TEST(TableReaderTest, UnsafePredicateStaysOnScannerPath) 
{
     ASSERT_TRUE(reader.close().ok());
 }
 
+TEST(TableReaderTest, ArrayContainsReachesMetadataPruningSafeRequestPrefix) {
+    const auto raw_array_type = 
std::make_shared<DataTypeArray>(std::make_shared<DataTypeInt32>());
+    std::vector<ColumnDefinition> file_schema;
+    file_schema.push_back(make_file_column(3, "items", raw_array_type));
+    std::vector<ColumnDefinition> projected_columns;
+    projected_columns.push_back(make_table_column(3, "items", raw_array_type));
+    set_name_identifiers(&projected_columns);
+    const auto array_type = projected_columns[0].type;
+
+    RuntimeState state {TQueryOptions(), TQueryGlobals()};
+    auto fake_state = std::make_shared<FakeFileReaderState>();
+    FakeTableReader reader(file_schema, fake_state);
+    ASSERT_TRUE(reader.init({
+                                    .projected_columns = projected_columns,
+                                    .conjuncts = {prepared_conjunct(
+                                            &state,
+                                            table_array_contains_int32_expr(0, 
0, array_type, 1))},
+                                    .format = FileFormat::PARQUET,
+                                    .scan_params = nullptr,
+                                    .io_ctx = nullptr,
+                                    .runtime_state = &state,
+                                    .scanner_profile = nullptr,
+                            })
+                        .ok());
+
+    SplitReadOptions split;
+    split.current_range.__set_path("fake-table-reader-input");
+    ASSERT_TRUE(reader.prepare_split(split).ok());
+    Block block = build_table_block(projected_columns);
+    bool eos = false;
+    ASSERT_TRUE(reader.get_block(&block, &eos).ok());
+    ASSERT_NE(fake_state->last_request, nullptr);
+    ASSERT_EQ(fake_state->last_request->conjuncts.size(), 1);
+    EXPECT_EQ(fake_state->last_request->metadata_pruning_safe_conjunct_count, 
1);
+    
EXPECT_TRUE(fake_state->last_request->conjuncts[0]->root()->can_evaluate_zonemap_filter());
+    ASSERT_TRUE(reader.close().ok());
+}
+
+TEST(TableReaderTest, ComplexArrayContainsIsUnsafeForMetadataPruning) {
+    const auto int_type = std::make_shared<DataTypeInt32>();
+    const auto inner_array_type = std::make_shared<DataTypeArray>(int_type);
+    const auto outer_array_type = 
std::make_shared<DataTypeArray>(inner_array_type);
+    auto expr = table_function_expr("array_contains", 
std::make_shared<DataTypeUInt8>(),
+                                    {outer_array_type, inner_array_type});
+    expr->add_child(VSlotRef::create_shared(0, 0, 0, outer_array_type, 
"nested_items"));
+    expr->add_child(VLiteral::create_shared(
+            inner_array_type,
+            Field::create_field<TYPE_ARRAY>(Array 
{Field::create_field<TYPE_INT>(1)})));
+
+    RuntimeState state {TQueryOptions(), TQueryGlobals()};
+    auto conjunct = prepared_conjunct(&state, expr);
+    EXPECT_FALSE(conjunct->root()->is_safe_to_execute_on_selected_rows());
+    conjunct->close();
+}
+
 TEST(TableReaderTest, ConstantPruningStopsAtUnsafeSlotlessPredicate) {
     std::vector<ColumnDefinition> projected_columns;
     auto partition_column = make_table_column(0, "part", 
std::make_shared<DataTypeInt32>());


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to