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

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


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new a9641f1770f [enhancement](scan) Optimize Parquet V2 direct predicate 
filtering (#66360)
a9641f1770f is described below

commit a9641f1770fc079f3b422ea36326fe10ca713ef7
Author: Gabriel <[email protected]>
AuthorDate: Mon Aug 3 20:56:05 2026 +0800

    [enhancement](scan) Optimize Parquet V2 direct predicate filtering (#66360)
    
    ## Proposed changes
    
    Backport the selected Parquet V2 direct-predicate filtering changes from
    #66261 to `branch-4.1`, then address the review and performance findings
    on this branch.
    
    - keep identity selection-vector state implicit and compact selected
    rows in bulk
    - retain the selection scratch high-water mark across scanner batches
    and specialize first compaction from implicit identity
    - refresh late runtime-filter requests at safe row-group boundaries
    - re-run footer-statistics pruning and reset adaptive predicate state
    for unopened row groups after a refresh
    - preserve real COUNT(*) carrier values while runtime filters are
    pending
    - initialize refreshed JNI predicates and attribute refresh work to
    TableReader/FileReader/Parquet profiles
    - preserve Hudi/Paimon child-reader predicate state
    - remove query-scoped dictionary-filter cache state
    - share immutable `VDirectInPredicate` pruning materialization across
    split-local expression clones
    - add correctness-checked selection and direct-IN lifecycle
    microbenchmarks
    
    ## Test
    
    - `./run-be-ut.sh --run
    
--filter='FileScannerV2Test.*:*Parquet*:*TableReaderTest.*:Hudi*ReaderTest.*:Paimon*ReaderTest.*:SelectionVectorTest.*:DictionaryFilterCostTest.*'
    -j48`
      - 639 tests from 47 test suites passed under ASAN
    - targeted late-RF, COUNT(*), dictionary-snapshot, shared-IN-state, and
    SelectionVector tests
      - 19 tests from 5 test suites passed under ASAN
    - Release benchmark build and smoke run
    - expected registrations: 228 decoder, 92 kernel, 25 selection, 167
    reader, and 8 expression-lifecycle cases
    - all 25 selection and 8 expression-lifecycle cases executed with zero
    benchmark errors
    - `git diff --check`
    
    ## Selection-vector microbenchmark
    
    The final benchmark source validates every surviving original row ID
    after the timed region. Base, pre-fix PR, and final binaries use the
    same benchmark source and Clang `-O3 -DNDEBUG -mavx2` on the same host.
    Each comparison uses one pinned CPU, three warmups, eight adjacent
    A-B-B-A quartets, and at least 0.3 seconds per invocation. The table
    reports median paired CPU-time ratios; negative values are improvements.
    
    | Operation | Final selectivity | Final vs pre-fix PR | Final vs branch
    base |
    |---|---:|---:|---:|
    | Identity initialization | 100% | -15.23% | -99.12% |
    | Row filter | 1% | -24.23% | -23.76% |
    | Row filter | 50% | -16.10% | -34.50% |
    | Row filter | 90% | -45.91% | -45.95% |
    | Row filter | 100% | -31.72% | -17.32% |
    | Successive filters | 1% | -33.25% | -35.79% |
    | Successive filters | 50% | -29.80% | -35.10% |
    | Successive filters | 90% | -25.27% | -25.16% |
    | Successive filters | 100% | -24.93% | -23.72% |
    
    All final-vs-base paired-ratio CVs are at most 5.85%. The previous
    16.43%/59.94% dense row-filter regressions and 27.92%-61.05%
    successive-filter regressions are no longer present. Retaining `_owned`
    avoids repeated value initialization; the implicit-identity
    specialization removes the remaining source/coordinate branches from the
    first compaction.
    
    ## Direct-IN expression lifecycle microbenchmark
    
    `FileScannerExpr/direct_in_clone_prepare_open` isolates deep-clone,
    prepare, and open for an already prepared direct-IN runtime filter. Set
    construction and the original fragment prepare/open are outside the
    timed region. Shared and forced-rematerialization implementations run in
    the same Release binary on one pinned CPU, with 10 repetitions and at
    least 0.5 seconds per repetition.
    
    | IN values | Rematerialize median | Shared median | Speedup |
    |---:|---:|---:|---:|
    | 128 | 207.470 us | 1.634 us | 126.9x |
    | 1,024 | 1.674 ms | 1.642 us | 1,019.5x |
    | 8,192 | 13.514 ms | 1.672 us | 8,082.2x |
    | 65,536 | 108.337 ms | 1.650 us | 65,663.1x |
    
    The shared path remains approximately constant because split clones
    reuse the immutable, fragment-originated pruning state; the
    rematerialization path scales linearly with runtime-filter cardinality.
    
    ## Reader-level microbenchmark
    
    The earlier reader-level comparison used an identical Parquet-only
    Release benchmark binary on the same host, with one pinned CPU, warm
    cache, three warmups, A-B-B-A order, 10 repetitions, and at least one
    second per repetition. It covers nullable INT32 predicate scans with a
    lazy payload for PLAIN and dictionary encoding.
    
    | Encoding | Selectivity | CPU time vs base | Wall time vs base |
    |---|---:|---:|---:|
    | Dictionary | 1% | +0.44% | +0.46% |
    | Dictionary | 10% | +1.13% | +1.22% |
    | Dictionary | 50% | +1.41% | +1.51% |
    | Dictionary | 90% | +0.41% | +0.53% |
    | PLAIN | 1% | -0.65% | -0.66% |
    | PLAIN | 10% | +0.32% | +0.38% |
    | PLAIN | 50% | -1.21% | -1.18% |
    | PLAIN | 90% | -1.34% | -1.29% |
    
    The reader-level point estimates span -1.34% to +1.41% CPU time with
    mixed signs, so this benchmark did not detect a material aggregate
    regression. It starts at `format::parquet::ParquetReader`; it does not
    cover scanner scheduling or end-to-end SQL execution.
---
 be/benchmark/benchmark_bit_pack.hpp                |  10 +-
 be/benchmark/benchmark_column_array_view.hpp       |   2 +-
 .../benchmark_column_array_view_distance.hpp       |   2 +-
 be/benchmark/benchmark_fastunion.hpp               |   2 +-
 be/benchmark/benchmark_hll_merge.hpp               |   2 +-
 be/benchmark/benchmark_main.cpp                    |  18 +--
 be/benchmark/binary_cast_benchmark.hpp             |   2 +-
 be/benchmark/parquet/AGENTS.md                     |  52 ++++++-
 be/benchmark/parquet/README.md                     |  38 +++++
 .../parquet/benchmark_file_scanner_expr.hpp        | 118 ++++++++++++++
 .../parquet/benchmark_parquet_selection.hpp        | 172 +++++++++++++++++++++
 be/benchmark/parquet/parquet_benchmark_scenarios.h |  33 ++++
 be/src/exec/scan/file_scanner_v2.cpp               |   7 +
 be/src/exec/scan/file_scanner_v2.h                 |   1 +
 be/src/exprs/vdirect_in_predicate.h                |  71 +++++----
 be/src/format_v2/column_mapper.cpp                 |   9 +-
 be/src/format_v2/column_mapper.h                   |   9 +-
 be/src/format_v2/file_reader.h                     |   9 ++
 be/src/format_v2/jni/jni_table_reader.cpp          |  17 ++
 be/src/format_v2/jni/jni_table_reader.h            |   1 +
 be/src/format_v2/parquet/parquet_profile.cpp       |   3 +
 be/src/format_v2/parquet/parquet_profile.h         |   1 +
 be/src/format_v2/parquet/parquet_reader.cpp        |  19 ++-
 be/src/format_v2/parquet/parquet_reader.h          |   4 +
 be/src/format_v2/parquet/parquet_scan.cpp          |  93 ++++++++---
 be/src/format_v2/parquet/parquet_scan.h            |   9 +-
 .../parquet/reader/native_column_reader.cpp        |   2 -
 be/src/format_v2/parquet/selection_vector.h        | 105 ++++++++++++-
 be/src/format_v2/table/hudi_reader.cpp             |  12 ++
 be/src/format_v2/table/hudi_reader.h               |   1 +
 be/src/format_v2/table/paimon_reader.cpp           |  12 ++
 be/src/format_v2/table/paimon_reader.h             |   1 +
 be/src/format_v2/table_reader.cpp                  | 110 +++++++++++++
 be/src/format_v2/table_reader.h                    |  19 ++-
 be/src/runtime/query_context.h                     |   1 -
 be/test/exprs/expr_zonemap_filter_test.cpp         |  44 ++++--
 be/test/format_v2/jni/jni_table_reader_test.cpp    |  26 ++++
 .../parquet/parquet_benchmark_scenarios_test.cpp   |  20 +++
 .../parquet/parquet_reader_control_test.cpp        |  55 +++++++
 be/test/format_v2/parquet/parquet_scan_test.cpp    | 130 +++++++++++++++-
 be/test/format_v2/table/hudi_reader_test.cpp       |  45 ++++++
 be/test/format_v2/table/paimon_reader_test.cpp     |  44 ++++++
 be/test/format_v2/table_reader_test.cpp            | 172 ++++++++++++++++++++-
 43 files changed, 1386 insertions(+), 117 deletions(-)

diff --git a/be/benchmark/benchmark_bit_pack.hpp 
b/be/benchmark/benchmark_bit_pack.hpp
index a4f269dd471..78ba2ebcd97 100644
--- a/be/benchmark/benchmark_bit_pack.hpp
+++ b/be/benchmark/benchmark_bit_pack.hpp
@@ -50,8 +50,10 @@ void bit_pack(const T* input, uint8_t in_num, int bit_width, 
uint8_t* output) {
 }
 
 static void BM_BitPack(benchmark::State& state) {
-    int w = state.range(0);
-    int n = 255;
+    // Registrations cap these dimensions to the destination types; explicit 
casts keep the
+    // benchmark build independent of the compiler's implicit-conversion 
warning policy.
+    const auto w = static_cast<int>(state.range(0));
+    constexpr uint8_t n = 255;
 
     std::default_random_engine e;
     std::uniform_int_distribution<int64_t> u;
@@ -75,8 +77,8 @@ static void BM_BitPack(benchmark::State& state) {
 }
 
 static void BM_BitPackOptimized(benchmark::State& state) {
-    int w = state.range(0);
-    int n = 255;
+    const auto w = static_cast<int>(state.range(0));
+    constexpr uint8_t n = 255;
 
     std::default_random_engine e;
     std::uniform_int_distribution<int64_t> u;
diff --git a/be/benchmark/benchmark_column_array_view.hpp 
b/be/benchmark/benchmark_column_array_view.hpp
index 09baf2bd435..4ba5fc522a7 100644
--- a/be/benchmark/benchmark_column_array_view.hpp
+++ b/be/benchmark/benchmark_column_array_view.hpp
@@ -126,7 +126,7 @@ static ColumnPtr make_string_array_column() {
 
 // Wrap with outer Nullable (no rows are actually null, just the wrapper 
overhead).
 static ColumnPtr wrap_nullable(const ColumnPtr& col) {
-    return ColumnNullable::create(col->assume_mutable(),
+    return ColumnNullable::create(col->assert_mutable(),
                                   ColumnUInt8::create(col->size(), 0));
 }
 
diff --git a/be/benchmark/benchmark_column_array_view_distance.hpp 
b/be/benchmark/benchmark_column_array_view_distance.hpp
index 34fd287f203..567a1b84d5d 100644
--- a/be/benchmark/benchmark_column_array_view_distance.hpp
+++ b/be/benchmark/benchmark_column_array_view_distance.hpp
@@ -264,7 +264,7 @@ 
BENCHMARK(ArrayView_Distance_Const_Plain_Flat)->Unit(benchmark::kNanosecond);
 // ============================================================
 
 static ColumnPtr wrap_nullable_for_dist(const ColumnPtr& col) {
-    return ColumnNullable::create(col->assume_mutable(), 
ColumnUInt8::create(col->size(), 0));
+    return ColumnNullable::create(col->assert_mutable(), 
ColumnUInt8::create(col->size(), 0));
 }
 
 static void Handwritten_Distance_Nullable_Plain(benchmark::State& state) {
diff --git a/be/benchmark/benchmark_fastunion.hpp 
b/be/benchmark/benchmark_fastunion.hpp
index ba469b75fa6..ae574321aec 100644
--- a/be/benchmark/benchmark_fastunion.hpp
+++ b/be/benchmark/benchmark_fastunion.hpp
@@ -19,7 +19,7 @@
 
 #include <string>
 
-#include "util/bitmap_value.h"
+#include "core/value/bitmap_value.h"
 
 using Roaring64Map = doris::detail::Roaring64Map;
 
diff --git a/be/benchmark/benchmark_hll_merge.hpp 
b/be/benchmark/benchmark_hll_merge.hpp
index d923d208fe4..2fc6c47eaa6 100644
--- a/be/benchmark/benchmark_hll_merge.hpp
+++ b/be/benchmark/benchmark_hll_merge.hpp
@@ -17,7 +17,7 @@
 
 #include <benchmark/benchmark.h>
 
-#include "olap/hll.h"
+#include "core/value/hll.h"
 #include "util/hash_util.hpp"
 
 namespace doris {
diff --git a/be/benchmark/benchmark_main.cpp b/be/benchmark/benchmark_main.cpp
index 7c64fa2729c..775f5caffef 100644
--- a/be/benchmark/benchmark_main.cpp
+++ b/be/benchmark/benchmark_main.cpp
@@ -24,19 +24,11 @@
 
 #include "benchmark_arrow_validation.hpp"
 #include "benchmark_bit_pack.hpp"
-#include "benchmark_bits.hpp"
-#include "benchmark_block_bloom_filter.hpp"
 #include "benchmark_column_array_view.hpp"
 #include "benchmark_column_array_view_distance.hpp"
-#include "benchmark_column_view.hpp"
-#include "benchmark_damerau_levenshtein.hpp"
 #include "benchmark_fastunion.hpp"
 #include "benchmark_fmod.hpp"
 #include "benchmark_hll_merge.hpp"
-#include "benchmark_hybrid_set.hpp"
-#include "benchmark_pdep_unpack.hpp"
-#include "benchmark_string.hpp"
-#include "benchmark_string_replace.hpp"
 #include "benchmark_zone_map_index.hpp"
 #include "binary_cast_benchmark.hpp"
 #include "common/config.h"
@@ -44,20 +36,16 @@
 #include "core/column/column_string.h"
 #include "core/data_type/data_type.h"
 #include "core/data_type/data_type_string.h"
+#include "parquet/benchmark_file_scanner_expr.hpp"
 #include "parquet/benchmark_parquet_decoder.hpp"
 #include "parquet/benchmark_parquet_kernels.hpp"
 #include "parquet/benchmark_parquet_reader.hpp"
+#include "parquet/benchmark_parquet_selection.hpp"
 #include "runtime/exec_env.h"
 #include "runtime/memory/mem_tracker_limiter.h"
 #include "runtime/memory/thread_mem_tracker_mgr.h"
 #include "runtime/thread_context.h"
 
-// benchmark_binary_plain_page_v2.hpp must be included LAST: it transitively 
pulls AWS SDK
-// headers (via storage/cache/page_cache.h) whose symbols shadow types used by 
the benchmark
-// headers above (notably binary_cast_benchmark.hpp). Keeping it last avoids 
the clash without
-// disabling any benchmark. (Do not let clang-format reorder it above the 
others.)
-#include "benchmark_binary_plain_page_v2.hpp"
-
 namespace doris { // change if need
 
 static bool init_benchmark_config(const char* executable) {
@@ -118,8 +106,6 @@ int main(int argc, char** argv) {
     if (!doris::init_benchmark_config(argv[0])) {
         return 1;
     }
-    doris::config::enable_bmi2_optimizations = true;
-
     SCOPED_INIT_THREAD_CONTEXT();
     doris::ExecEnv::GetInstance()->init_mem_tracker();
     doris::thread_context()->thread_mem_tracker_mgr->init();
diff --git a/be/benchmark/binary_cast_benchmark.hpp 
b/be/benchmark/binary_cast_benchmark.hpp
index 9949a783b05..fb2650b40f5 100644
--- a/be/benchmark/binary_cast_benchmark.hpp
+++ b/be/benchmark/binary_cast_benchmark.hpp
@@ -21,7 +21,7 @@
 #include <random>
 #include <vector>
 
-#include "util/binary_cast.hpp"
+#include "core/binary_cast.hpp"
 
 namespace doris {
 
diff --git a/be/benchmark/parquet/AGENTS.md b/be/benchmark/parquet/AGENTS.md
index 6c2fb8c85df..4c1d3cf4d5e 100644
--- a/be/benchmark/parquet/AGENTS.md
+++ b/be/benchmark/parquet/AGENTS.md
@@ -6,16 +6,20 @@ benchmark system described in the design document.
 
 ## What exists today
 
-The benchmark binary registers three groups:
+The benchmark binary registers five groups:
 
 - `ParquetDecoder`: native page decoder benchmarks using in-memory encoded 
pages.
 - `ParquetKernel`: isolated SIMD-sensitive decode and predicate kernels.
+- `ParquetSelection`: isolated selection initialization and predicate 
compaction paths.
 - `ParquetReader`: local-file benchmarks that call the format V2 Parquet 
reader directly.
+- `FileScannerExpr`: expression lifecycle benchmarks for split-local clone, 
prepare, and open.
 
 The relevant files are:
 
 - `benchmark_parquet_decoder.hpp`: deterministic page construction and decoder 
registration.
+- `benchmark_parquet_selection.hpp`: selection initialization and compaction 
registration.
 - `benchmark_parquet_reader.hpp`: deterministic local Parquet fixtures and 
reader registration.
+- `benchmark_file_scanner_expr.hpp`: runtime-filter expression lifecycle 
registration.
 - `parquet_benchmark_scenarios.h`: scenario definitions and the selected 
matrix.
 - `README.md`: short human-oriented build and invocation examples.
 - `be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp`: matrix 
invariants.
@@ -23,7 +27,8 @@ The relevant files are:
 Do not describe this suite as end-to-end SQL, `FileScannerV2`, remote I/O, 
V1/V2 comparison, or a
 cross-engine benchmark. The reader benchmark starts at 
`format::parquet::ParquetReader` and does
 not include FE planning, scanner scheduling, `TableReader`, client latency, or 
Runtime Profile
-collection.
+collection. `FileScannerExpr` isolates expression lifecycle work and likewise 
does not execute a
+scanner or read a file.
 
 ## Build and list cases
 
@@ -37,7 +42,10 @@ List all Parquet cases and verify the expected registration 
counts:
 
 ```shell
 be/output/lib/benchmark_test --benchmark_list_tests \
-  | grep -E '^Parquet(Decoder|Kernel|Reader)/'
+  | grep -E '^Parquet(Decoder|Kernel|Selection|Reader)/'
+
+be/output/lib/benchmark_test --benchmark_list_tests \
+  | grep '^FileScannerExpr/'
 
 be/output/lib/benchmark_test --benchmark_list_tests \
   | grep -c '^ParquetDecoder/'  # currently 228
@@ -45,8 +53,14 @@ be/output/lib/benchmark_test --benchmark_list_tests \
 be/output/lib/benchmark_test --benchmark_list_tests \
   | grep -c '^ParquetKernel/'   # currently 92
 
+be/output/lib/benchmark_test --benchmark_list_tests \
+  | grep -c '^ParquetSelection/' # currently 25
+
 be/output/lib/benchmark_test --benchmark_list_tests \
   | grep -c '^ParquetReader/'   # currently 167
+
+be/output/lib/benchmark_test --benchmark_list_tests \
+  | grep -c '^FileScannerExpr/' # currently 8
 ```
 
 When running the binary directly from `be/build_RELEASE/bin`, make sure the 
JVM and third-party
@@ -71,11 +85,23 @@ be/output/lib/benchmark_test \
   --benchmark_out=parquet-kernel-smoke.json \
   --benchmark_out_format=json
 
+be/output/lib/benchmark_test \
+  --benchmark_filter='^ParquetSelection/' \
+  --benchmark_min_time=0.001s \
+  --benchmark_out=parquet-selection-smoke.json \
+  --benchmark_out_format=json
+
 be/output/lib/benchmark_test \
   --benchmark_filter='^ParquetReader/' \
   --benchmark_min_time=0.001s \
   --benchmark_out=parquet-reader-smoke.json \
   --benchmark_out_format=json
+
+be/output/lib/benchmark_test \
+  --benchmark_filter='^FileScannerExpr/' \
+  --benchmark_min_time=0.001s \
+  --benchmark_out=file-scanner-expr-smoke.json \
+  --benchmark_out_format=json
 ```
 
 Reject a smoke run if the process is non-zero, the expected number of JSON 
results is absent, or
@@ -128,6 +154,18 @@ rates with both placement patterns, 0% through 100% 
raw-predicate selectivities,
 legacy and fused implementations in the same binary and validates both against 
an independent
 source-level oracle before timing.
 
+`ParquetSelection` contains 25 cases that isolate the selection-vector work 
used by Parquet
+predicate evaluation. It measures identity initialization, one raw-row filter, 
and two successive
+filters. The filter matrix covers 0%, 1%, 10%, 50%, 90%, and 100% selectivity 
with clustered and
+alternating matches. These cases include `SelectionVector::resize()` in the 
timed region because
+initializing a new batch is part of the production predicate path.
+
+`FileScannerExpr` contains eight cases that clone, prepare, and open an 
already-prepared
+`VDirectInPredicate` with 128, 1,024, 8,192, or 65,536 integer set values. 
Each cardinality registers
+`impl_shared` and `impl_rematerialize` in the same binary. Set construction 
and the original
+fragment-level materialization are outside the timed region. The cases model 
the repeated
+split-local expression lifecycle only; they do not include scanner scheduling 
or file reads.
+
 `ParquetReader` deliberately uses a single-variable matrix rather than a 
Cartesian product. After
 deduplication it contains 167 cases covering:
 
@@ -302,10 +340,10 @@ be simulated by silently changing the local reader 
benchmark.
 
 ## Current validation record
 
-The current expected registration counts are 228 decoder, 92 kernel, and 167 
reader cases. A smoke
-run is an execution record only, not a reviewed performance baseline, because 
repetitions, host
-isolation, warmups, cache control, `perf` data, variance, and before/after 
comparison are not
-collected.
+The current expected registration counts are 228 decoder, 92 kernel, 25 
selection, 167 reader, and
+8 expression-lifecycle cases. A smoke run is an execution record only, not a 
reviewed performance
+baseline, because repetitions, host isolation, warmups, cache control, `perf` 
data, variance, and
+before/after comparison are not collected.
 
 ## Rules for extending the suite
 
diff --git a/be/benchmark/parquet/README.md b/be/benchmark/parquet/README.md
index fcbed4a548b..156302b2423 100644
--- a/be/benchmark/parquet/README.md
+++ b/be/benchmark/parquet/README.md
@@ -21,6 +21,12 @@ List only the Parquet cases:
 be/output/lib/benchmark_test --benchmark_list_tests | grep '^Parquet'
 ```
 
+List the split-local runtime-filter expression lifecycle cases:
+
+```shell
+be/output/lib/benchmark_test --benchmark_list_tests | grep '^FileScannerExpr/'
+```
+
 ## Decoder cases
 
 `ParquetDecoder` measures the native decoder with data generation and encoder 
setup outside the
@@ -75,6 +81,20 @@ taskset -c 8 be/output/lib/benchmark_test \
 # Repeat fused as B2, then legacy as A2, changing only --benchmark_out.
 ```
 
+## Selection compaction cases
+
+`ParquetSelection` isolates the selection-vector paths used after raw and 
expression predicate
+evaluation. It covers implicit identity initialization, a filter indexed by 
source row, and a
+second compact filter applied after an earlier predicate has already made the 
selection sparse.
+
+```shell
+be/output/lib/benchmark_test \
+  
--benchmark_filter='^ParquetSelection/(resize_identity|row_filter|cascade_filter)/'
 \
+  --benchmark_min_time=1s \
+  --benchmark_repetitions=10 \
+  --benchmark_report_aggregates_only=true
+```
+
 ## Local reader cases
 
 `ParquetReader` measures local open-to-first-block, full scan, predicate scan, 
complex residual
@@ -113,3 +133,21 @@ be/output/lib/benchmark_test \
 Every result reports throughput plus `raw_rows`, `selected_rows`, 
`fixture_bytes`, `ns/raw_row`,
 and (when at least one row survives) `ns/selected_row`. Keep CPU frequency, 
build type, compiler,
 machine placement, and benchmark filters fixed when comparing two commits.
+
+## Runtime-filter expression lifecycle cases
+
+`FileScannerExpr` measures only the repeated deep-clone, prepare, and open 
work for an already
+prepared direct-IN runtime filter. Four cardinalities sweep 128 through 65,536 
set values, with
+shared-state and forced-rematerialization implementations registered in the 
same binary. Set
+construction and the original fragment-level prepare/open are outside the 
timed region.
+
+```shell
+be/output/lib/benchmark_test \
+  --benchmark_filter='^FileScannerExpr/direct_in_clone_prepare_open/' \
+  --benchmark_min_time=1s \
+  --benchmark_repetitions=10 \
+  --benchmark_report_aggregates_only=true
+```
+
+These cases do not execute `FileScannerV2`, schedule splits, or read Parquet 
files. They isolate the
+expression lifecycle visible in scanner profiles so it can be compared without 
I/O noise.
diff --git a/be/benchmark/parquet/benchmark_file_scanner_expr.hpp 
b/be/benchmark/parquet/benchmark_file_scanner_expr.hpp
new file mode 100644
index 00000000000..14654472f5e
--- /dev/null
+++ b/be/benchmark/parquet/benchmark_file_scanner_expr.hpp
@@ -0,0 +1,118 @@
+// 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.
+
+#pragma once
+
+#include <benchmark/benchmark.h>
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <string>
+
+#include "core/data_type/data_type_number.h"
+#include "exprs/create_predicate_function.h"
+#include "exprs/vdirect_in_predicate.h"
+#include "exprs/vexpr_context.h"
+#include "exprs/vslot_ref.h"
+#include "runtime/descriptors.h"
+#include "runtime/runtime_state.h"
+
+namespace doris::parquet_benchmark::file_scanner_expr_detail {
+
+inline TExprNode make_direct_in_node() {
+    TExprNode node;
+    node.__set_type(create_type_desc(PrimitiveType::TYPE_BOOLEAN));
+    node.__set_node_type(TExprNodeType::IN_PRED);
+    node.__set_opcode(TExprOpcode::FILTER_IN);
+    node.__set_num_children(1);
+    node.__set_is_nullable(false);
+    node.in_predicate.__set_is_not_in(false);
+    return node;
+}
+
+inline void run_direct_in_clone_prepare_open(benchmark::State& state, size_t 
cardinality,
+                                             bool share_pruning_state) {
+    std::shared_ptr<HybridSetBase> filter(create_set(PrimitiveType::TYPE_INT, 
cardinality, false));
+    for (size_t index = 0; index < cardinality; ++index) {
+        const int32_t value = static_cast<int32_t>(index);
+        filter->insert(&value);
+    }
+    auto root = VDirectInPredicate::create_shared(make_direct_in_node(), 
std::move(filter), true);
+    root->add_child(VSlotRef::create_shared(0, 0, -1, 
std::make_shared<DataTypeInt32>(),
+                                            "runtime_filter_key"));
+    RuntimeState runtime_state {TQueryOptions(), TQueryGlobals()};
+    RowDescriptor row_desc;
+    VExprContext original(root);
+    auto status = original.prepare(&runtime_state, row_desc);
+    if (status.ok()) {
+        status = original.open(&runtime_state);
+    }
+    if (!status.ok()) {
+        const auto error = status.to_string();
+        state.SkipWithError(error.c_str());
+        return;
+    }
+
+    for (auto _ : state) {
+        VExprSPtr cloned_root;
+        if (share_pruning_state) {
+            status = root->deep_clone(&cloned_root);
+        } else {
+            auto rematerialized = 
VDirectInPredicate::create_shared(make_direct_in_node(),
+                                                                    
root->get_set_func(), true);
+            rematerialized->add_child(VSlotRef::create_shared(
+                    0, 0, -1, std::make_shared<DataTypeInt32>(), 
"runtime_filter_key"));
+            cloned_root = std::move(rematerialized);
+            status = Status::OK();
+        }
+        if (status.ok()) {
+            VExprContext cloned(cloned_root);
+            status = cloned.prepare(&runtime_state, row_desc);
+            if (status.ok()) {
+                status = cloned.open(&runtime_state);
+            }
+            benchmark::DoNotOptimize(cloned_root);
+        }
+        if (!status.ok()) {
+            const auto error = status.to_string();
+            state.SkipWithError(error.c_str());
+            return;
+        }
+    }
+    state.counters["set_values"] = static_cast<double>(cardinality);
+}
+
+inline bool register_file_scanner_expr_benchmarks() {
+    for (const size_t cardinality : std::array<size_t, 4> {128, 1024, 8192, 
65536}) {
+        for (const bool share_pruning_state : {false, true}) {
+            const std::string name = 
"FileScannerExpr/direct_in_clone_prepare_open/values_" +
+                                     std::to_string(cardinality) +
+                                     (share_pruning_state ? "/impl_shared" : 
"/impl_rematerialize");
+            benchmark::RegisterBenchmark(name.c_str(), [cardinality, 
share_pruning_state](
+                                                               
benchmark::State& state) {
+                run_direct_in_clone_prepare_open(state, cardinality, 
share_pruning_state);
+            })->Unit(benchmark::kNanosecond);
+        }
+    }
+    return true;
+}
+
+inline const bool FILE_SCANNER_EXPR_BENCHMARKS_REGISTERED = 
register_file_scanner_expr_benchmarks();
+
+} // namespace doris::parquet_benchmark::file_scanner_expr_detail
diff --git a/be/benchmark/parquet/benchmark_parquet_selection.hpp 
b/be/benchmark/parquet/benchmark_parquet_selection.hpp
new file mode 100644
index 00000000000..06c0a338def
--- /dev/null
+++ b/be/benchmark/parquet/benchmark_parquet_selection.hpp
@@ -0,0 +1,172 @@
+// 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.
+
+#pragma once
+
+#include <benchmark/benchmark.h>
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <numeric>
+#include <string>
+#include <vector>
+
+#include "format_v2/parquet/selection_vector.h"
+#include "parquet_benchmark_scenarios.h"
+
+namespace doris::parquet_benchmark::selection_detail {
+
+constexpr size_t SELECTION_ROWS = 1UL << 12;
+constexpr int CASCADE_FIRST_SELECTIVITY = 90;
+
+inline std::vector<uint8_t> make_filter(size_t rows, int selectivity_percent, 
Pattern pattern) {
+    std::vector<uint8_t> filter(rows, 0);
+    const size_t selected_rows =
+            rows * static_cast<size_t>(std::clamp(selectivity_percent, 0, 
100)) / 100;
+    if (pattern == Pattern::CLUSTERED) {
+        std::fill_n(filter.begin(), selected_rows, uint8_t {1});
+    } else if (selected_rows != 0) {
+        for (size_t selected = 0; selected < selected_rows; ++selected) {
+            filter[selected * rows / selected_rows] = 1;
+        }
+    }
+    return filter;
+}
+
+// Keep the benchmark source buildable on revisions before the bulk compaction 
helpers. The
+// fallback mirrors the former Parquet scan loops so one named matrix can 
compare both revisions.
+template <typename Selection>
+size_t compact_with_row_filter(Selection* selection, const uint8_t* filter, 
size_t rows) {
+    if constexpr (requires { selection->compact_with_row_filter(filter, rows); 
}) {
+        return selection->compact_with_row_filter(filter, rows);
+    } else {
+        size_t output = 0;
+        for (size_t position = 0; position < rows; ++position) {
+            const auto row = selection->get_index(position);
+            if (filter[row] != 0) {
+                selection->set_index(output++, row);
+            }
+        }
+        return output;
+    }
+}
+
+template <typename Selection>
+size_t compact_with_selection_filter(Selection* selection, const uint8_t* 
filter, size_t rows) {
+    if constexpr (requires { selection->compact_with_selection_filter(filter, 
rows); }) {
+        return selection->compact_with_selection_filter(filter, rows);
+    } else {
+        size_t output = 0;
+        for (size_t position = 0; position < rows; ++position) {
+            if (filter[position] != 0) {
+                selection->set_index(output++, selection->get_index(position));
+            }
+        }
+        return output;
+    }
+}
+
+inline std::vector<format::parquet::SelectionVector::Index> 
expected_selection_rows(
+        const SelectionScenario& scenario, const std::vector<uint8_t>& 
row_filter,
+        const std::vector<uint8_t>& first_filter, const std::vector<uint8_t>& 
selection_filter) {
+    std::vector<format::parquet::SelectionVector::Index> rows(SELECTION_ROWS);
+    std::iota(rows.begin(), rows.end(), 0);
+    if (scenario.operation == SelectionOperation::RESIZE_IDENTITY) {
+        return rows;
+    }
+    if (scenario.operation == SelectionOperation::ROW_FILTER) {
+        std::erase_if(rows, [&](const auto row) { return row_filter[row] == 0; 
});
+        return rows;
+    }
+    std::erase_if(rows, [&](const auto row) { return first_filter[row] == 0; 
});
+    std::vector<format::parquet::SelectionVector::Index> cascaded;
+    cascaded.reserve(rows.size());
+    for (size_t position = 0; position < rows.size(); ++position) {
+        if (selection_filter[position] != 0) {
+            cascaded.push_back(rows[position]);
+        }
+    }
+    return cascaded;
+}
+
+inline void run_selection(benchmark::State& state, const SelectionScenario& 
scenario) {
+    format::parquet::SelectionVector selection;
+    const auto row_filter =
+            make_filter(SELECTION_ROWS, scenario.selectivity_percent, 
scenario.pattern);
+    const auto first_filter =
+            make_filter(SELECTION_ROWS, CASCADE_FIRST_SELECTIVITY, 
Pattern::ALTERNATING);
+    const size_t first_selected =
+            static_cast<size_t>(std::count(first_filter.begin(), 
first_filter.end(), uint8_t {1}));
+    const auto selection_filter =
+            make_filter(first_selected, scenario.selectivity_percent, 
scenario.pattern);
+    const auto expected_rows =
+            expected_selection_rows(scenario, row_filter, first_filter, 
selection_filter);
+
+    size_t selected_rows = 0;
+    for (auto _ : state) {
+        selection.resize(SELECTION_ROWS);
+        switch (scenario.operation) {
+        case SelectionOperation::RESIZE_IDENTITY:
+            selected_rows = SELECTION_ROWS;
+            break;
+        case SelectionOperation::ROW_FILTER:
+            selected_rows = compact_with_row_filter(&selection, 
row_filter.data(), SELECTION_ROWS);
+            break;
+        case SelectionOperation::CASCADE_FILTER:
+            selected_rows =
+                    compact_with_row_filter(&selection, first_filter.data(), 
SELECTION_ROWS);
+            selected_rows = compact_with_selection_filter(&selection, 
selection_filter.data(),
+                                                          selected_rows);
+            break;
+        }
+        benchmark::DoNotOptimize(selected_rows);
+        benchmark::ClobberMemory();
+    }
+    bool selection_matches = selected_rows == expected_rows.size();
+    for (size_t position = 0; selection_matches && position < selected_rows; 
++position) {
+        selection_matches = selection.get_index(position) == 
expected_rows[position];
+    }
+    if (!selection_matches) {
+        state.SkipWithError("selection compaction produced unexpected row 
indices");
+        return;
+    }
+
+    state.SetItemsProcessed(static_cast<int64_t>(state.iterations() * 
SELECTION_ROWS));
+    state.SetBytesProcessed(static_cast<int64_t>(state.iterations() * 
SELECTION_ROWS));
+    state.counters["raw_rows"] = static_cast<double>(SELECTION_ROWS);
+    state.counters["selected_rows"] = static_cast<double>(selected_rows);
+    state.counters["ns/raw_row"] = benchmark::Counter(
+            static_cast<double>(SELECTION_ROWS),
+            benchmark::Counter::kIsIterationInvariantRate | 
benchmark::Counter::kInvert);
+}
+
+inline bool register_selection_benchmarks() {
+    for (const auto& scenario : selection_scenarios()) {
+        const std::string name = "ParquetSelection/" + 
to_string(scenario.operation) + "/sel_" +
+                                 std::to_string(scenario.selectivity_percent) 
+ "/" +
+                                 to_string(scenario.pattern);
+        benchmark::RegisterBenchmark(name.c_str(), [=](benchmark::State& 
state) {
+            run_selection(state, scenario);
+        })->Unit(benchmark::kNanosecond);
+    }
+    return true;
+}
+
+inline const bool SELECTION_BENCHMARKS_REGISTERED = 
register_selection_benchmarks();
+
+} // namespace doris::parquet_benchmark::selection_detail
diff --git a/be/benchmark/parquet/parquet_benchmark_scenarios.h 
b/be/benchmark/parquet/parquet_benchmark_scenarios.h
index d2655b3380e..a9c58c15d8c 100644
--- a/be/benchmark/parquet/parquet_benchmark_scenarios.h
+++ b/be/benchmark/parquet/parquet_benchmark_scenarios.h
@@ -37,6 +37,7 @@ enum class Encoding {
 enum class ValueType { INT32, INT64, FLOAT, DOUBLE, BYTE_ARRAY, 
FIXED_LEN_BYTE_ARRAY };
 enum class Pattern { CLUSTERED, ALTERNATING };
 enum class Projection { PREDICATE_ONLY, PREDICATE_PROJECTED };
+enum class SelectionOperation { RESIZE_IDENTITY, ROW_FILTER, CASCADE_FILTER };
 enum class ReaderOperation {
     OPEN_TO_FIRST_BLOCK,
     FULL_SCAN,
@@ -82,6 +83,12 @@ struct KernelScenario {
     NestedSelectionImplementation nested_implementation = 
NestedSelectionImplementation::FUSED;
 };
 
+struct SelectionScenario {
+    SelectionOperation operation;
+    int selectivity_percent;
+    Pattern pattern;
+};
+
 struct SelectionRange {
     size_t first;
     size_t count;
@@ -156,6 +163,20 @@ inline std::vector<KernelScenario> kernel_scenarios() {
     return scenarios;
 }
 
+inline std::vector<SelectionScenario> selection_scenarios() {
+    std::vector<SelectionScenario> scenarios {
+            {SelectionOperation::RESIZE_IDENTITY, 100, Pattern::CLUSTERED}};
+    for (const auto operation :
+         {SelectionOperation::ROW_FILTER, SelectionOperation::CASCADE_FILTER}) 
{
+        for (const int selectivity : {0, 1, 10, 50, 90, 100}) {
+            for (const auto pattern : {Pattern::CLUSTERED, 
Pattern::ALTERNATING}) {
+                scenarios.push_back({operation, selectivity, pattern});
+            }
+        }
+    }
+    return scenarios;
+}
+
 inline std::vector<ReaderScenario> reader_scenarios() {
     std::vector<ReaderScenario> scenarios;
     std::set<std::tuple<ReaderOperation, Encoding, int, Pattern, int, 
Projection, int, int,
@@ -347,6 +368,18 @@ inline std::string to_string(Projection value) {
     return value == Projection::PREDICATE_ONLY ? "predicate_only" : 
"predicate_projected";
 }
 
+inline std::string to_string(SelectionOperation value) {
+    switch (value) {
+    case SelectionOperation::RESIZE_IDENTITY:
+        return "resize_identity";
+    case SelectionOperation::ROW_FILTER:
+        return "row_filter";
+    case SelectionOperation::CASCADE_FILTER:
+        return "cascade_filter";
+    }
+    return "unknown";
+}
+
 inline std::string to_string(ReaderOperation value) {
     switch (value) {
     case ReaderOperation::OPEN_TO_FIRST_BLOCK:
diff --git a/be/src/exec/scan/file_scanner_v2.cpp 
b/be/src/exec/scan/file_scanner_v2.cpp
index 6a7bf74571e..5343b034ea2 100644
--- a/be/src/exec/scan/file_scanner_v2.cpp
+++ b/be/src/exec/scan/file_scanner_v2.cpp
@@ -442,6 +442,12 @@ Status FileScannerV2::_get_block_impl(RuntimeState* state, 
Block* block, bool* e
         }
 
         {
+            if (_table_reader_rf_num != _applied_rf_num) {
+                VExprContextSPtrs refreshed_conjuncts;
+                RETURN_IF_ERROR(_build_table_conjuncts(&refreshed_conjuncts));
+                
RETURN_IF_ERROR(_table_reader->refresh_conjuncts(std::move(refreshed_conjuncts)));
+                _table_reader_rf_num = _applied_rf_num;
+            }
             if (_should_run_adaptive_batch_size()) {
                 _table_reader->set_batch_size(_predict_reader_batch_rows());
             }
@@ -549,6 +555,7 @@ Status FileScannerV2::_prepare_next_split(bool* eos) {
         }
         COUNTER_UPDATE(_file_counter, 1);
         _has_prepared_split = true;
+        _table_reader_rf_num = _applied_rf_num;
         *eos = false;
         return Status::OK();
     }
diff --git a/be/src/exec/scan/file_scanner_v2.h 
b/be/src/exec/scan/file_scanner_v2.h
index 658ec44ad7f..87a68e2ab6d 100644
--- a/be/src/exec/scan/file_scanner_v2.h
+++ b/be/src/exec/scan/file_scanner_v2.h
@@ -178,6 +178,7 @@ private:
     std::shared_ptr<SplitSourceConnector> _split_source;
     bool _first_scan_range = false;
     bool _has_prepared_split = false;
+    int _table_reader_rf_num = 0;
     TFileRangeDesc _current_range;
     std::string _current_range_path;
 
diff --git a/be/src/exprs/vdirect_in_predicate.h 
b/be/src/exprs/vdirect_in_predicate.h
index 79d0f996d7c..b272c816edb 100644
--- a/be/src/exprs/vdirect_in_predicate.h
+++ b/be/src/exprs/vdirect_in_predicate.h
@@ -17,6 +17,7 @@
 
 #pragma once
 
+#include <mutex>
 #include <utility>
 #include <vector>
 
@@ -37,6 +38,15 @@ namespace doris {
 class VDirectInPredicate final : public VExpr {
     ENABLE_FACTORY_CREATOR(VDirectInPredicate);
 
+    struct PruningState {
+        std::once_flag materialize_once;
+        Status materialization_status;
+        bool zonemap_materialized = false;
+        std::vector<Field> seg_filter_values;
+        Field seg_filter_min;
+        Field seg_filter_max;
+    };
+
 public:
     // `hybrid_set_values_match_child_type` tells whether values in `filter` 
can be interpreted with
     // the child expression type. Parquet/ORC dictionary-filter rewrites 
evaluate the original
@@ -90,22 +100,24 @@ public:
     std::shared_ptr<HybridSetBase> get_set_func() const override { return 
_filter; }
 
     ZoneMapFilterResult evaluate_zonemap_filter(const ZoneMapEvalContext& ctx) 
const override {
-        return expr_zonemap::eval_in_zonemap(ctx, get_child(0), false, 
_seg_filter_values,
-                                             _seg_filter_min, _seg_filter_max);
+        return expr_zonemap::eval_in_zonemap(
+                ctx, get_child(0), false, _pruning_state->seg_filter_values,
+                _pruning_state->seg_filter_min, 
_pruning_state->seg_filter_max);
     }
 
     bool can_evaluate_zonemap_filter() const override {
-        return _zonemap_materialized &&
+        return _pruning_state->zonemap_materialized &&
                std::dynamic_pointer_cast<VSlotRef>(get_child(0)) != nullptr;
     }
 
     ZoneMapFilterResult evaluate_dictionary_filter(
             const DictionaryEvalContext& ctx) const override {
-        return expr_zonemap::eval_in_dictionary(ctx, get_child(0), false, 
_seg_filter_values);
+        return expr_zonemap::eval_in_dictionary(ctx, get_child(0), false,
+                                                
_pruning_state->seg_filter_values);
     }
 
     bool can_evaluate_dictionary_filter() const override {
-        return _zonemap_materialized &&
+        return _pruning_state->zonemap_materialized &&
                std::dynamic_pointer_cast<VSlotRef>(get_child(0)) != nullptr;
     }
 
@@ -177,8 +189,12 @@ public:
 
     Status clone_node(VExprSPtr* cloned_expr) const override {
         DORIS_CHECK(cloned_expr != nullptr);
-        *cloned_expr = VDirectInPredicate::create_shared(clone_texpr_node(), 
_filter,
-                                                         
_hybrid_set_values_match_child_type);
+        auto cloned = VDirectInPredicate::create_shared(clone_texpr_node(), 
_filter,
+                                                        
_hybrid_set_values_match_child_type);
+        // Runtime-filter sets are immutable after publication, and file-local 
rewrites preserve
+        // the predicate's logical child type, so every split clone must reuse 
this materialization.
+        cloned->_pruning_state = _pruning_state;
+        *cloned_expr = std::move(cloned);
         return Status::OK();
     }
 
@@ -300,21 +316,27 @@ private:
     }
 
     Status _materialize_for_zonemap_filter() {
-        if (!_hybrid_set_values_match_child_type) {
-            _zonemap_materialized = false;
-            return Status::OK();
-        }
-        DORIS_CHECK(_filter != nullptr);
-        auto& filter = *_filter;
-        const auto& data_type = remove_nullable(get_child(0)->data_type());
-        expr_zonemap::InZonemapMaterializedSet materialized;
-        
RETURN_IF_ERROR(expr_zonemap::materialize_hybrid_set_for_zonemap_filter(filter, 
data_type,
-                                                                               
 &materialized));
-        _seg_filter_values = std::move(materialized.values);
-        _seg_filter_min = std::move(materialized.min_value);
-        _seg_filter_max = std::move(materialized.max_value);
-        _zonemap_materialized = true;
-        return Status::OK();
+        const auto pruning_state = _pruning_state;
+        std::call_once(pruning_state->materialize_once, [&] {
+            if (!_hybrid_set_values_match_child_type) {
+                return;
+            }
+            DORIS_CHECK(_filter != nullptr);
+            auto& filter = *_filter;
+            const auto& data_type = remove_nullable(get_child(0)->data_type());
+            expr_zonemap::InZonemapMaterializedSet materialized;
+            pruning_state->materialization_status =
+                    
expr_zonemap::materialize_hybrid_set_for_zonemap_filter(filter, data_type,
+                                                                            
&materialized);
+            if (!pruning_state->materialization_status.ok()) {
+                return;
+            }
+            pruning_state->seg_filter_values = std::move(materialized.values);
+            pruning_state->seg_filter_min = std::move(materialized.min_value);
+            pruning_state->seg_filter_max = std::move(materialized.max_value);
+            pruning_state->zonemap_materialized = true;
+        });
+        return pruning_state->materialization_status;
     }
 
     std::shared_ptr<HybridSetBase> _filter;
@@ -323,10 +345,7 @@ private:
     // literals for zonemap pruning or slot-IN rewrite.
     bool _hybrid_set_values_match_child_type = true;
     std::string _expr_name;
-    bool _zonemap_materialized = false;
-    std::vector<Field> _seg_filter_values;
-    Field _seg_filter_min;
-    Field _seg_filter_max;
+    std::shared_ptr<PruningState> _pruning_state = 
std::make_shared<PruningState>();
 };
 
 #include "common/compile_check_end.h"
diff --git a/be/src/format_v2/column_mapper.cpp 
b/be/src/format_v2/column_mapper.cpp
index c3dbe3fa9e7..7e028370221 100644
--- a/be/src/format_v2/column_mapper.cpp
+++ b/be/src/format_v2/column_mapper.cpp
@@ -2161,13 +2161,20 @@ Status TableColumnMapper::_build_filter_entries(const 
FileScanRequest& file_requ
 Status TableColumnMapper::create_scan_request(
         const std::vector<TableFilter>& table_filters,
         const std::vector<ColumnDefinition>& projected_columns, 
FileScanRequest* file_request,
-        RuntimeState* runtime_state) {
+        RuntimeState* runtime_state,
+        const std::map<LocalColumnId, LocalIndex>* fixed_local_positions) {
     // FileReader evaluates expressions against a file-local block. This 
mapper owns the
     // table-column to file-column conversion, so it also owns the file-local 
block positions.
     file_request->predicate_columns.clear();
     file_request->non_predicate_columns.clear();
     file_request->predicate_only_columns.clear();
     file_request->local_positions.clear();
+    if (fixed_local_positions != nullptr) {
+        // A refreshed predicate may promote a lazy column, but the active 
split's block slots are
+        // immutable. Seed their positions before rebuilding expressions so 
every rewritten SlotRef
+        // continues to address the same physical column.
+        file_request->local_positions = *fixed_local_positions;
+    }
     file_request->conjuncts.clear();
     file_request->delete_conjuncts.clear();
     _filter_entries.clear();
diff --git a/be/src/format_v2/column_mapper.h b/be/src/format_v2/column_mapper.h
index e03f836061e..68d0ff357c0 100644
--- a/be/src/format_v2/column_mapper.h
+++ b/be/src/format_v2/column_mapper.h
@@ -193,10 +193,11 @@ public:
     // Convert a table-level scan request into a file-local scan request. 
table_filters preserve
     // row-level filtering semantics and are rewritten as file-local 
conjuncts. File-layer pruning
     // such as ZoneMap, dictionary, and bloom filter derives from those 
localized VExpr conjuncts.
-    virtual Status create_scan_request(const std::vector<TableFilter>& 
table_filters,
-                                       const std::vector<ColumnDefinition>& 
projected_columns,
-                                       FileScanRequest* file_request,
-                                       RuntimeState* runtime_state = nullptr);
+    virtual Status create_scan_request(
+            const std::vector<TableFilter>& table_filters,
+            const std::vector<ColumnDefinition>& projected_columns, 
FileScanRequest* file_request,
+            RuntimeState* runtime_state = nullptr,
+            const std::map<LocalColumnId, LocalIndex>* fixed_local_positions = 
nullptr);
 
     // Localize table-level filters to the file schema.
     // Trivial mappings can copy structured predicates directly. Type changes 
may be localized with
diff --git a/be/src/format_v2/file_reader.h b/be/src/format_v2/file_reader.h
index 3ff512975d2..65a2d03417c 100644
--- a/be/src/format_v2/file_reader.h
+++ b/be/src/format_v2/file_reader.h
@@ -305,6 +305,15 @@ public:
         return Status::OK();
     }
 
+    // Readers opt in only when they can keep an immutable request for the 
active physical
+    // granule and switch a newer snapshot at a well-defined boundary.
+    virtual bool supports_scan_request_refresh() const { return false; }
+
+    virtual Status queue_scan_request(std::shared_ptr<FileScanRequest> 
request) {
+        (void)request;
+        return Status::NotSupported("FileReader does not support scan request 
refresh");
+    }
+
     virtual Status get_block(Block* file_block, size_t* rows, bool* eof) {
         if (rows != nullptr) {
             *rows = 0;
diff --git a/be/src/format_v2/jni/jni_table_reader.cpp 
b/be/src/format_v2/jni/jni_table_reader.cpp
index b58696fe4ad..dfa00c1568e 100644
--- a/be/src/format_v2/jni/jni_table_reader.cpp
+++ b/be/src/format_v2/jni/jni_table_reader.cpp
@@ -77,6 +77,23 @@ Status JniTableReader::prepare_split(const SplitReadOptions& 
options) {
     return _open_jni_scanner();
 }
 
+Status JniTableReader::refresh_conjuncts(VExprContextSPtrs conjuncts) {
+    if (_scanner_opened) {
+        SCOPED_TIMER(_profile.total_timer);
+        SCOPED_TIMER(_profile.refresh_conjuncts_timer);
+        SCOPED_TIMER(_profile.file_reader_total_timer);
+        SCOPED_TIMER(_profile.file_reader_refresh_timer);
+        RowDescriptor row_desc;
+        for (const auto& conjunct : conjuncts) {
+            // JNI readers bypass TableReader::open_reader(), so a late 
predicate would otherwise
+            // replace the active snapshot without initializing its executable 
function state.
+            RETURN_IF_ERROR(conjunct->prepare(_runtime_state, row_desc));
+            RETURN_IF_ERROR(conjunct->open(_runtime_state));
+        }
+    }
+    return TableReader::refresh_conjuncts(std::move(conjuncts));
+}
+
 Status JniTableReader::get_block(Block* output_block, bool* eos) {
     SCOPED_TIMER(_profile.total_timer);
     SCOPED_TIMER(_profile.exec_timer);
diff --git a/be/src/format_v2/jni/jni_table_reader.h 
b/be/src/format_v2/jni/jni_table_reader.h
index df7edc92f98..76e51c1c059 100644
--- a/be/src/format_v2/jni/jni_table_reader.h
+++ b/be/src/format_v2/jni/jni_table_reader.h
@@ -48,6 +48,7 @@ public:
 
     Status init(TableReadOptions&& options) override;
     Status prepare_split(const SplitReadOptions& options) override;
+    Status refresh_conjuncts(VExprContextSPtrs conjuncts) override;
     Status get_block(Block* block, bool* eos) override;
     Status abort_split() override;
     Status close() override;
diff --git a/be/src/format_v2/parquet/parquet_profile.cpp 
b/be/src/format_v2/parquet/parquet_profile.cpp
index 02b64d7b63c..e6717505cea 100644
--- a/be/src/format_v2/parquet/parquet_profile.cpp
+++ b/be/src/format_v2/parquet/parquet_profile.cpp
@@ -31,6 +31,8 @@ void ParquetProfile::init(RuntimeProfile* profile) {
     static const char* parquet_profile = "ParquetReader";
     total_time =
             ADD_CHILD_TIMER_WITH_LEVEL(profile, parquet_profile, 
file_scan_profile::FILE_READER, 1);
+    refresh_scan_request_time =
+            ADD_CHILD_TIMER_WITH_LEVEL(profile, "RefreshScanRequestTime", 
parquet_profile, 1);
 
     // Row-group counters are part of the long-standing ParquetReader profile 
contract. Keep them
     // below the format node so profile parsers and operators can attribute 
pruning to Parquet.
@@ -256,6 +258,7 @@ void ParquetProfile::update_deferred_pruning_stats(const 
ParquetPruningStats& pr
                                                    bool selected) const {
     const int64_t filtered = selected ? 0 : 1;
     COUNTER_UPDATE(filtered_row_groups, filtered);
+    COUNTER_UPDATE(filtered_row_groups_by_min_max, 
pruning_stats.filtered_row_groups_by_statistics);
     COUNTER_UPDATE(filtered_row_groups_by_dictionary,
                    pruning_stats.filtered_row_groups_by_dictionary);
     COUNTER_UPDATE(filtered_row_groups_by_bloom_filter,
diff --git a/be/src/format_v2/parquet/parquet_profile.h 
b/be/src/format_v2/parquet/parquet_profile.h
index 2282a70db30..57c03c79336 100644
--- a/be/src/format_v2/parquet/parquet_profile.h
+++ b/be/src/format_v2/parquet/parquet_profile.h
@@ -133,6 +133,7 @@ struct ParquetProfile {
     ParquetScanProfile scan_profile() const;
 
     RuntimeProfile::Counter* total_time = nullptr;
+    RuntimeProfile::Counter* refresh_scan_request_time = nullptr;
 
     RuntimeProfile::Counter* filtered_row_groups = nullptr;
     RuntimeProfile::Counter* filtered_row_groups_by_min_max = nullptr;
diff --git a/be/src/format_v2/parquet/parquet_reader.cpp 
b/be/src/format_v2/parquet/parquet_reader.cpp
index 4a80bc0403d..065adc63b66 100644
--- a/be/src/format_v2/parquet/parquet_reader.cpp
+++ b/be/src/format_v2/parquet/parquet_reader.cpp
@@ -471,10 +471,24 @@ Status 
ParquetReader::open(std::shared_ptr<format::FileScanRequest> request) {
     _state->scheduler.set_global_rowid_context(_global_rowid_context);
     _state->scheduler.set_scan_profile(_parquet_profile.scan_profile());
     _state->scheduler.set_plan(std::move(row_group_plan));
+    _state->scheduler.set_scan_request(request_snapshot);
     _eof = _state->scheduler.empty();
     return Status::OK();
 }
 
+Status 
ParquetReader::queue_scan_request(std::shared_ptr<format::FileScanRequest> 
request) {
+    SCOPED_TIMER(_parquet_profile.total_time);
+    SCOPED_TIMER(_parquet_profile.refresh_scan_request_time);
+    if (_state == nullptr || _state->file_context.native_metadata == nullptr) {
+        return Status::Uninitialized("ParquetReader is not open");
+    }
+    DORIS_CHECK(request != nullptr);
+    RETURN_IF_ERROR(validate_requested_columns_supported(_state->file_schema, 
*request));
+    _state->scheduler.queue_scan_request(request);
+    _request = std::move(request);
+    return Status::OK();
+}
+
 Status ParquetReader::get_block(Block* file_block, size_t* rows, bool* eof) {
     SCOPED_TIMER(_parquet_profile.total_time);
     if (_state == nullptr || _state->file_context.native_metadata == nullptr) {
@@ -489,15 +503,14 @@ Status ParquetReader::get_block(Block* file_block, 
size_t* rows, bool* eof) {
         *eof = true;
         return Status::OK();
     }
-    auto request_snapshot = _request;
-    if (request_snapshot == nullptr) {
+    if (_request == nullptr) {
         return Status::Cancelled("ParquetReader is closed");
     }
 
     const auto predicate_filtered_rows_before = 
_state->scheduler.predicate_filtered_rows();
     const auto raw_rows_read_before = _state->scheduler.raw_rows_read();
     Status st = _state->scheduler.read_next_batch(_state->file_context, 
_state->file_schema,
-                                                  *request_snapshot, 
file_block, rows, eof);
+                                                  file_block, rows, eof);
     if (!st.ok()) {
         if (_io_ctx != nullptr && _io_ctx->should_stop) {
             *rows = 0;
diff --git a/be/src/format_v2/parquet/parquet_reader.h 
b/be/src/format_v2/parquet/parquet_reader.h
index da6135b81ae..fe95b93a9e0 100644
--- a/be/src/format_v2/parquet/parquet_reader.h
+++ b/be/src/format_v2/parquet/parquet_reader.h
@@ -60,6 +60,10 @@ public:
 
     Status open(std::shared_ptr<format::FileScanRequest> request) override;
 
+    bool supports_scan_request_refresh() const override { return true; }
+
+    Status queue_scan_request(std::shared_ptr<format::FileScanRequest> 
request) override;
+
     Status get_block(Block* file_block, size_t* rows, bool* eof) override;
 
     Status get_aggregate_result(const format::FileAggregateRequest& request,
diff --git a/be/src/format_v2/parquet/parquet_scan.cpp 
b/be/src/format_v2/parquet/parquet_scan.cpp
index 33ef6a08fed..18b42d90a96 100644
--- a/be/src/format_v2/parquet/parquet_scan.cpp
+++ b/be/src/format_v2/parquet/parquet_scan.cpp
@@ -46,6 +46,7 @@
 #include "format_v2/parquet/reader/native/column_chunk_reader.h"
 #include "format_v2/parquet/reader/native_column_reader.h"
 #include "format_v2/parquet/reader/row_position_column_reader.h"
+#include "runtime/runtime_state.h"
 #include "util/defer_op.h"
 #include "util/time.h"
 
@@ -587,14 +588,7 @@ void update_counter_if_not_null(RuntimeProfile::Counter* 
counter, int64_t value)
 
 uint16_t apply_filter_to_selection(const IColumn::Filter& filter, 
SelectionVector* selection,
                                    uint16_t selected_rows) {
-    uint16_t new_selected_rows = 0;
-    for (uint16_t selection_idx = 0; selection_idx < selected_rows; 
++selection_idx) {
-        const auto row_idx = selection->get_index(selection_idx);
-        if (filter[row_idx] != 0) {
-            selection->set_index(new_selected_rows++, 
static_cast<SelectionVector::Index>(row_idx));
-        }
-    }
-    return new_selected_rows;
+    return 
cast_set<uint16_t>(selection->compact_with_row_filter(filter.data(), 
selected_rows));
 }
 
 Status execute_compact_filter_conjuncts(const VExprContextSPtrs& conjuncts, 
size_t rows,
@@ -752,14 +746,8 @@ uint16_t apply_compact_filter_to_selection(const 
IColumn::Filter& filter,
                                            SelectionVector* selection, 
uint16_t selected_rows) {
     DORIS_CHECK(selection != nullptr);
     DORIS_CHECK(filter.size() == selected_rows);
-    uint16_t new_selected_rows = 0;
-    for (uint16_t selection_idx = 0; selection_idx < selected_rows; 
++selection_idx) {
-        if (filter[selection_idx] != 0) {
-            selection->set_index(new_selected_rows++, 
static_cast<SelectionVector::Index>(
-                                                              
selection->get_index(selection_idx)));
-        }
-    }
-    return new_selected_rows;
+    return cast_set<uint16_t>(
+            selection->compact_with_selection_filter(filter.data(), 
selected_rows));
 }
 
 IColumn::Filter selection_to_filter(const SelectionVector& selection, uint16_t 
selected_rows,
@@ -855,6 +843,7 @@ void ParquetScanScheduler::set_plan(RowGroupScanPlan plan) {
     _row_group_plans = std::move(plan.row_groups);
     _condition_cache_filtered_rows = 0;
     _predicate_filtered_rows = 0;
+    _remaining_plans_need_replanning = false;
     reset();
 }
 
@@ -908,6 +897,40 @@ void ParquetScanScheduler::reset() {
     reset_current_row_group();
 }
 
+void 
ParquetScanScheduler::set_scan_request(std::shared_ptr<format::FileScanRequest> 
request) {
+    DORIS_CHECK(request != nullptr);
+    _active_request = std::move(request);
+    _pending_request.reset();
+    _predicate_schedule_request = nullptr;
+}
+
+void 
ParquetScanScheduler::queue_scan_request(std::shared_ptr<format::FileScanRequest>
 request) {
+    DORIS_CHECK(request != nullptr);
+    _pending_request = std::move(request);
+}
+
+void 
ParquetScanScheduler::activate_pending_scan_request_at_row_group_boundary() {
+    if (_has_current_row_group || !_pending_predicate_selection.empty() ||
+        _pending_request == nullptr) {
+        return;
+    }
+    // Column readers and predicate schedules retain request-derived state for 
one row group. Swap
+    // only after they are gone; the refreshed request may promote a lazy 
column to a predicate.
+    _active_request = std::move(_pending_request);
+    _predicate_schedule_request = nullptr;
+    // Footer plans and adaptive ordering describe the previous predicate 
snapshot. Reusing either
+    // after a late runtime filter would miss pruning or bias the new 
predicate order with stale data.
+    _remaining_plans_need_replanning = true;
+    _predicate_schedule = {};
+    _predicate_positions_scratch.clear();
+    _predicate_indices_by_position_scratch.clear();
+    _materialized_predicate_positions_scratch.clear();
+    _ordered_predicate_positions_scratch.clear();
+    _predicate_runtime_stats.clear();
+    _predicate_batch_sequence = 0;
+    _predicate_survival_ratio = -1;
+}
+
 void ParquetScanScheduler::reset_current_row_group() {
     // RuntimeProfile updates are amortized on the batch path, but a row-group 
transition destroys
     // the reader tree. Force the final delta out before clearing it so short 
row groups and early
@@ -1053,6 +1076,27 @@ Status ParquetScanScheduler::open_next_row_group(
         file_context.reset_random_access_ranges();
         _current_merge_range_active = false;
         ParquetPruningStats deferred_stats;
+        if (_remaining_plans_need_replanning) {
+            // A refreshed projection may require different dictionary, Bloom, 
or page-index
+            // metadata. Preserve already-safe selected ranges, but rebuild 
every request-shaped
+            // artifact before opening this row group.
+            candidate_plan.expensive_pruning_pending = true;
+            candidate_plan.page_skip_plans.clear();
+            candidate_plan.offset_indexes.clear();
+            const std::vector<int> candidate {candidate_plan.row_group_id};
+            std::vector<int> footer_selected;
+            RETURN_IF_ERROR(select_row_groups_by_metadata(
+                    file_context.native_metadata->to_thrift(), file_schema, 
request, &candidate,
+                    &footer_selected, _enable_bloom_filter, &deferred_stats, 
_timezone,
+                    _runtime_state, &file_context, 
_scan_profile.column_reader_profile,
+                    ParquetMetadataProbeMode::FOOTER_ONLY));
+            if (footer_selected.empty()) {
+                if (_parquet_profile != nullptr) {
+                    
_parquet_profile->update_deferred_pruning_stats(deferred_stats, false);
+                }
+                continue;
+            }
+        }
         bool selected = false;
         RETURN_IF_ERROR(finalize_native_row_group_read_plan(
                 *file_context.native_metadata, file_schema, request, 
_enable_bloom_filter,
@@ -2706,11 +2750,12 @@ void 
ParquetScanScheduler::mark_condition_cache_granules(const SelectionVector&
 
 Status ParquetScanScheduler::read_next_batch(
         ParquetFileContext& file_context,
-        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
-        const format::FileScanRequest& request, Block* file_block, size_t* 
rows, bool* eof) {
+        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, 
Block* file_block,
+        size_t* rows, bool* eof) {
+    DORIS_CHECK(_active_request != nullptr);
     *rows = 0;
     if (!_pending_predicate_selection.empty()) {
-        RETURN_IF_ERROR(materialize_pending_predicate_batch(request, 
file_block, rows));
+        RETURN_IF_ERROR(materialize_pending_predicate_batch(*_active_request, 
file_block, rows));
         *eof = false;
         return Status::OK();
     }
@@ -2731,9 +2776,10 @@ Status ParquetScanScheduler::read_next_batch(
     };
     while (true) {
         if (!_has_current_row_group) {
+            activate_pending_scan_request_at_row_group_boundary();
             bool has_row_group = false;
-            RETURN_IF_ERROR(
-                    open_next_row_group(file_context, file_schema, request, 
&has_row_group));
+            RETURN_IF_ERROR(open_next_row_group(file_context, file_schema, 
*_active_request,
+                                                &has_row_group));
             if (!has_row_group) {
                 *eof = true;
                 return Status::OK();
@@ -2769,8 +2815,9 @@ Status ParquetScanScheduler::read_next_batch(
         const int64_t physical_rows_read = batch_rows;
         const int64_t batch_first_file_row =
                 _current_row_group_first_row + _current_row_group_rows_read;
-        RETURN_IF_ERROR(read_current_row_group_batch(file_context, 
file_schema, batch_rows, request,
-                                                     batch_first_file_row, 
file_block, rows));
+        RETURN_IF_ERROR(read_current_row_group_batch(file_context, 
file_schema, batch_rows,
+                                                     *_active_request, 
batch_first_file_row,
+                                                     file_block, rows));
         _current_row_group_rows_read += physical_rows_read;
         _current_range_rows_read += physical_rows_read;
         if (_current_range_rows_read >= current_range.length) {
diff --git a/be/src/format_v2/parquet/parquet_scan.h 
b/be/src/format_v2/parquet/parquet_scan.h
index f0e202d9942..474963ed5d9 100644
--- a/be/src/format_v2/parquet/parquet_scan.h
+++ b/be/src/format_v2/parquet/parquet_scan.h
@@ -188,6 +188,8 @@ public:
         _enable_strict_mode = enable_strict_mode;
     }
     void set_runtime_state(RuntimeState* runtime_state) { _runtime_state = 
runtime_state; }
+    void set_scan_request(std::shared_ptr<format::FileScanRequest> request);
+    void queue_scan_request(std::shared_ptr<format::FileScanRequest> request);
     // Release row-group readers before the owning RuntimeProfile is reported. 
Native readers
     // publish their accumulated page/decode statistics from their destructor.
     void close() { reset_current_row_group(); }
@@ -204,13 +206,13 @@ public:
 
     Status read_next_batch(ParquetFileContext& file_context,
                            const 
std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
-                           const format::FileScanRequest& request, Block* 
file_block, size_t* rows,
-                           bool* eof);
+                           Block* file_block, size_t* rows, bool* eof);
 
 private:
     static constexpr size_t PROFILE_FLUSH_BATCH_INTERVAL = 16;
 
     void reset_current_row_group();
+    void activate_pending_scan_request_at_row_group_boundary();
     void flush_current_reader_profiles();
     bool finish_current_reader_batch_profiles();
     const detail::PredicateConjunctSchedule& predicate_conjunct_schedule(
@@ -310,6 +312,9 @@ private:
     SelectionVector _selection;
     std::vector<uint32_t> _read_column_positions_scratch;
     const format::FileScanRequest* _predicate_schedule_request = nullptr;
+    std::shared_ptr<format::FileScanRequest> _active_request;
+    std::shared_ptr<format::FileScanRequest> _pending_request;
+    bool _remaining_plans_need_replanning = false;
     detail::PredicateConjunctSchedule _predicate_schedule;
     std::vector<size_t> _predicate_positions_scratch;
     std::unordered_map<size_t, size_t> _predicate_indices_by_position_scratch;
diff --git a/be/src/format_v2/parquet/reader/native_column_reader.cpp 
b/be/src/format_v2/parquet/reader/native_column_reader.cpp
index 60eb0dc3d92..6b686597fa6 100644
--- a/be/src/format_v2/parquet/reader/native_column_reader.cpp
+++ b/be/src/format_v2/parquet/reader/native_column_reader.cpp
@@ -738,7 +738,6 @@ Status NativeColumnReader::select_with_fixed_width_filter(
     DORIS_CHECK(used_filter != nullptr);
     DORIS_CHECK(execution_kind != nullptr);
     RETURN_IF_ERROR(validate_selected_span(batch_rows));
-    RETURN_IF_ERROR(selection.verify(selected_rows, batch_rows));
     const uint8_t* filter_data = nullptr;
     RETURN_IF_ERROR(selection.materialize_filter(selected_rows, batch_rows, 
&filter_data));
     int64_t rows_read = 0;
@@ -768,7 +767,6 @@ Status NativeColumnReader::select_with_runtime_filter(
     DORIS_CHECK(row_filter != nullptr);
     DORIS_CHECK(used_filter != nullptr);
     RETURN_IF_ERROR(validate_selected_span(batch_rows));
-    RETURN_IF_ERROR(selection.verify(selected_rows, batch_rows));
     row_filter->clear();
     *used_filter = false;
     if (_nested || conjuncts.empty() || !std::ranges::all_of(conjuncts, 
[&](const auto& conjunct) {
diff --git a/be/src/format_v2/parquet/selection_vector.h 
b/be/src/format_v2/parquet/selection_vector.h
index 033478875fa..99c854d8b4e 100644
--- a/be/src/format_v2/parquet/selection_vector.h
+++ b/be/src/format_v2/parquet/selection_vector.h
@@ -68,17 +68,19 @@ public:
         _data = data;
         _size = count;
         _identity = data == nullptr;
+        _mutable_data_exposed = data != nullptr;
         ++_generation;
     }
 
     void resize(size_t count) {
-        _owned.resize(count);
-        _data = _owned.data();
+        // Identity is the overwhelmingly common initial state. Keep it 
implicit until a caller
+        // actually changes an index, avoiding one write per source row for 
every scanner batch.
+        // Scanner batches repeatedly reuse this object. Retaining the 
initialized high-water mark
+        // avoids value-initializing the entire scratch vector before every 
sparse compaction.
+        _data = nullptr;
         _size = count;
-        for (size_t idx = 0; idx < count; ++idx) {
-            _data[idx] = static_cast<Index>(idx);
-        }
         _identity = true;
+        _mutable_data_exposed = false;
         ++_generation;
     }
 
@@ -87,6 +89,7 @@ public:
         _data = nullptr;
         _size = 0;
         _identity = true;
+        _mutable_data_exposed = false;
         ++_generation;
     }
 
@@ -95,9 +98,11 @@ public:
     bool is_set() const { return _data != nullptr; }
 
     Index* data() {
+        _materialize_identity();
         // A mutable pointer can change indices without set_index(), so 
identity can no longer be
         // proven until resize() rebuilds it. This keeps the O(1) dense fast 
path conservative.
         _identity = false;
+        _mutable_data_exposed = true;
         ++_generation;
         return _data;
     }
@@ -112,6 +117,7 @@ public:
     }
 
     void set_index(size_t idx, Index value) {
+        _materialize_identity();
         _data[idx] = value;
         if (value != idx) {
             _identity = false;
@@ -119,10 +125,18 @@ public:
         ++_generation;
     }
 
+    size_t compact_with_row_filter(const uint8_t* filter, size_t count) {
+        return _compact(filter, count, true);
+    }
+
+    size_t compact_with_selection_filter(const uint8_t* filter, size_t count) {
+        return _compact(filter, count, false);
+    }
+
     Status materialize_filter(size_t count, int64_t batch_rows, const 
uint8_t** filter) const {
         DORIS_CHECK(filter != nullptr);
         if (batch_rows >= 0 && std::cmp_equal(count, batch_rows) && _identity 
&&
-            (_data == nullptr || count <= _size)) {
+            (_size == 0 || count <= _size)) {
             // A proven identity selection is equivalent to no FilterMap. 
Returning nullptr avoids
             // constructing and rescanning one dense byte per source row.
             *filter = nullptr;
@@ -157,6 +171,17 @@ public:
             return Status::InvalidArgument("Parquet selection count {} exceeds 
vector size {}",
                                            count, _size);
         }
+        if (_data == nullptr && _size != 0 && count > _size) {
+            return Status::InvalidArgument("Parquet selection count {} exceeds 
vector size {}",
+                                           count, _size);
+        }
+        if (_identity) {
+            return Status::OK();
+        }
+        if (!_mutable_data_exposed && _verified_generation == _generation &&
+            _verified_count == count && _verified_batch_rows == batch_rows) {
+            return Status::OK();
+        }
         size_t previous = 0;
         for (size_t idx = 0; idx < count; ++idx) {
             const size_t current = get_index(idx);
@@ -173,19 +198,87 @@ public:
             }
             previous = current;
         }
+        if (!_mutable_data_exposed) {
+            _verified_generation = _generation;
+            _verified_count = count;
+            _verified_batch_rows = batch_rows;
+        }
         return Status::OK();
     }
 
 private:
+    void _materialize_identity() {
+        if (_data != nullptr) {
+            return;
+        }
+        if (_owned.size() < _size) {
+            _owned.resize(_size);
+        }
+        _data = _owned.data();
+        for (size_t idx = 0; idx < _size; ++idx) {
+            _data[idx] = static_cast<Index>(idx);
+        }
+    }
+
+    size_t _compact(const uint8_t* filter, size_t count, bool 
filter_uses_row_index) {
+        DORIS_CHECK(filter != nullptr);
+        DORIS_CHECK(count <= _size);
+        Index* source = _data;
+        if (_data == nullptr) {
+            if (_owned.size() < _size) {
+                _owned.resize(_size);
+            }
+            _data = _owned.data();
+            // An implicit identity maps both filter coordinate systems to the 
same position.
+            // Specialize this first compaction so split batches do not pay 
source/coordinate
+            // branches for every row after already avoiding identity 
materialization.
+            size_t output = 0;
+            while (output < count && filter[output] != 0) {
+                _data[output] = static_cast<Index>(output);
+                ++output;
+            }
+            bool remains_identity = true;
+            for (size_t position = output; position < count; ++position) {
+                if (filter[position] != 0) {
+                    _data[output++] = static_cast<Index>(position);
+                    remains_identity = false;
+                }
+            }
+            _identity = remains_identity;
+            ++_generation;
+            return output;
+        }
+        size_t output = 0;
+        bool remains_identity = true;
+        for (size_t position = 0; position < count; ++position) {
+            const Index row = source == nullptr ? static_cast<Index>(position) 
: source[position];
+            const size_t filter_position = filter_uses_row_index ? row : 
position;
+            if (filter[filter_position] != 0) {
+                _data[output] = row;
+                remains_identity &= row == output;
+                ++output;
+            }
+        }
+        // Compaction is one logical mutation. Invalidating caches once is 
important when several
+        // predicates successively refine a wide batch.
+        _identity = remains_identity;
+        ++_generation;
+        return output;
+    }
+
     std::vector<Index> _owned;
     Index* _data = nullptr;
     size_t _size = 0;
     bool _identity = true;
+    bool _mutable_data_exposed = false;
     uint64_t _generation = 0;
     mutable std::vector<uint8_t> _filter;
     mutable uint64_t _filter_generation = std::numeric_limits<uint64_t>::max();
     mutable size_t _filter_count = 0;
     mutable int64_t _filter_batch_rows = -1;
+    mutable uint64_t _verified_generation = 
std::numeric_limits<uint64_t>::max();
+    mutable size_t _verified_count = 0;
+    mutable int64_t _verified_batch_rows = -1;
 };
 
 inline void selection_to_ranges(const SelectionVector& selection, uint16_t 
selected_rows,
diff --git a/be/src/format_v2/table/hudi_reader.cpp 
b/be/src/format_v2/table/hudi_reader.cpp
index ee24d0f9ad7..838d98b5e52 100644
--- a/be/src/format_v2/table/hudi_reader.cpp
+++ b/be/src/format_v2/table/hudi_reader.cpp
@@ -80,6 +80,18 @@ Status HudiHybridReader::prepare_split(const 
format::SplitReadOptions& options)
     return _current_split_reader->prepare_split(options);
 }
 
+Status HudiHybridReader::refresh_conjuncts(VExprContextSPtrs conjuncts) {
+    
RETURN_IF_ERROR(format::TableReader::refresh_conjuncts(std::move(conjuncts)));
+    if (_current_split_reader == nullptr) {
+        return Status::OK();
+    }
+    VExprContextSPtrs child_conjuncts;
+    RETURN_IF_ERROR(_clone_conjuncts(&child_conjuncts));
+    // The hybrid wrapper owns no physical reader; forward a clone so the 
active child, rather than
+    // only the wrapper snapshot, observes late predicates for the remainder 
of this split.
+    return 
_current_split_reader->refresh_conjuncts(std::move(child_conjuncts));
+}
+
 Status HudiHybridReader::get_block(Block* block, bool* eos) {
     DORIS_CHECK(_current_split_reader != nullptr);
     return _current_split_reader->get_block(block, eos);
diff --git a/be/src/format_v2/table/hudi_reader.h 
b/be/src/format_v2/table/hudi_reader.h
index dbb6f5e8231..c06e1b238b6 100644
--- a/be/src/format_v2/table/hudi_reader.h
+++ b/be/src/format_v2/table/hudi_reader.h
@@ -59,6 +59,7 @@ public:
 
     Status init(format::TableReadOptions&& options) override;
     Status prepare_split(const format::SplitReadOptions& options) override;
+    Status refresh_conjuncts(VExprContextSPtrs conjuncts) override;
     Status get_block(Block* block, bool* eos) override;
     bool current_split_pruned() const override;
     bool current_split_uses_metadata_count() const override;
diff --git a/be/src/format_v2/table/paimon_reader.cpp 
b/be/src/format_v2/table/paimon_reader.cpp
index a3f4092a470..93183af178b 100644
--- a/be/src/format_v2/table/paimon_reader.cpp
+++ b/be/src/format_v2/table/paimon_reader.cpp
@@ -115,6 +115,18 @@ Status PaimonHybridReader::prepare_split(const 
format::SplitReadOptions& options
     return _current_split_reader->prepare_split(options);
 }
 
+Status PaimonHybridReader::refresh_conjuncts(VExprContextSPtrs conjuncts) {
+    
RETURN_IF_ERROR(format::TableReader::refresh_conjuncts(std::move(conjuncts)));
+    if (_current_split_reader == nullptr) {
+        return Status::OK();
+    }
+    VExprContextSPtrs child_conjuncts;
+    RETURN_IF_ERROR(_clone_conjuncts(&child_conjuncts));
+    // The hybrid wrapper owns no physical reader; forward a clone so the 
active child, rather than
+    // only the wrapper snapshot, observes late predicates for the remainder 
of this split.
+    return 
_current_split_reader->refresh_conjuncts(std::move(child_conjuncts));
+}
+
 Status PaimonHybridReader::get_block(Block* block, bool* eos) {
     DORIS_CHECK(_current_split_reader != nullptr);
     return _current_split_reader->get_block(block, eos);
diff --git a/be/src/format_v2/table/paimon_reader.h 
b/be/src/format_v2/table/paimon_reader.h
index 823fa6540d2..8570f2efba6 100644
--- a/be/src/format_v2/table/paimon_reader.h
+++ b/be/src/format_v2/table/paimon_reader.h
@@ -65,6 +65,7 @@ public:
 
     Status init(format::TableReadOptions&& options) override;
     Status prepare_split(const format::SplitReadOptions& options) override;
+    Status refresh_conjuncts(VExprContextSPtrs conjuncts) override;
     Status get_block(Block* block, bool* eos) override;
     bool current_split_pruned() const override;
     bool current_split_uses_metadata_count() const override;
diff --git a/be/src/format_v2/table_reader.cpp 
b/be/src/format_v2/table_reader.cpp
index 8a5da295fdd..be4fe0e406e 100644
--- a/be/src/format_v2/table_reader.cpp
+++ b/be/src/format_v2/table_reader.cpp
@@ -686,6 +686,8 @@ Status TableReader::init(TableReadOptions&& options) {
                 ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"PushDownAggTime", table_profile, 1);
         _profile.open_reader_timer =
                 ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, "OpenReaderTime", 
table_profile, 1);
+        _profile.refresh_conjuncts_timer = ADD_CHILD_TIMER_WITH_LEVEL(
+                _scanner_profile, "RefreshConjunctsTime", table_profile, 1);
         _profile.runtime_filter_partition_prune_timer = 
ADD_CHILD_TIMER_WITH_LEVEL(
                 _scanner_profile, 
"FileScannerRuntimeFilterPartitionPruningTime", table_profile, 1);
         _profile.runtime_filter_partition_pruned_range_counter = 
ADD_CHILD_COUNTER_WITH_LEVEL(
@@ -703,6 +705,8 @@ Status TableReader::init(TableReadOptions&& options) {
                 _scanner_profile, "FileReaderCreateColumnMapperTime", 
file_reader_profile, 1);
         _profile.file_reader_open_timer = ADD_CHILD_TIMER_WITH_LEVEL(
                 _scanner_profile, "FileReaderOpenTime", file_reader_profile, 
1);
+        _profile.file_reader_refresh_timer = ADD_CHILD_TIMER_WITH_LEVEL(
+                _scanner_profile, "FileReaderRefreshScanRequestTime", 
file_reader_profile, 1);
         _profile.file_reader_get_block_timer = ADD_CHILD_TIMER_WITH_LEVEL(
                 _scanner_profile, "FileReaderGetBlockTime", 
file_reader_profile, 1);
         _profile.file_reader_aggregate_timer = ADD_CHILD_TIMER_WITH_LEVEL(
@@ -765,6 +769,112 @@ Status TableReader::_build_table_filters_from_conjuncts() 
{
     return Status::OK();
 }
 
+namespace {
+
+bool same_scan_projection(const LocalColumnIndex& lhs, const LocalColumnIndex& 
rhs) {
+    if (lhs.index != rhs.index || lhs.project_all_children != 
rhs.project_all_children ||
+        lhs.children.size() != rhs.children.size()) {
+        return false;
+    }
+    for (size_t index = 0; index < lhs.children.size(); ++index) {
+        if (!same_scan_projection(lhs.children[index], rhs.children[index])) {
+            return false;
+        }
+    }
+    return true;
+}
+
+const LocalColumnIndex* find_scan_projection(const FileScanRequest& request,
+                                             LocalColumnId column_id) {
+    const auto find_by_id = [column_id](const std::vector<LocalColumnIndex>& 
projections) {
+        return std::ranges::find_if(projections, [column_id](const 
LocalColumnIndex& projection) {
+            return projection.column_id() == column_id;
+        });
+    };
+    auto it = find_by_id(request.predicate_columns);
+    if (it != request.predicate_columns.end()) {
+        return &*it;
+    }
+    it = find_by_id(request.non_predicate_columns);
+    return it == request.non_predicate_columns.end() ? nullptr : &*it;
+}
+
+bool same_physical_scan_layout(const FileScanRequest& lhs, const 
FileScanRequest& rhs) {
+    if (lhs.local_positions != rhs.local_positions) {
+        return false;
+    }
+    for (const auto& [column_id, _] : lhs.local_positions) {
+        const auto* lhs_projection = find_scan_projection(lhs, column_id);
+        const auto* rhs_projection = find_scan_projection(rhs, column_id);
+        if (lhs_projection == nullptr || rhs_projection == nullptr ||
+            !same_scan_projection(*lhs_projection, *rhs_projection)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+} // namespace
+
+Status TableReader::refresh_conjuncts(VExprContextSPtrs conjuncts) {
+    SCOPED_TIMER(_profile.total_timer);
+    SCOPED_TIMER(_profile.refresh_conjuncts_timer);
+    _conjuncts = std::move(conjuncts);
+    if (_data_reader.reader == nullptr) {
+        // The split is prepared but its physical reader has not opened yet. 
open_reader() will use
+        // this newest snapshot directly, so no pending request is needed.
+        return Status::OK();
+    }
+    if (!_data_reader.reader->supports_scan_request_refresh()) {
+        return Status::OK();
+    }
+
+    RETURN_IF_ERROR(_build_table_filters_from_conjuncts());
+    // create_scan_request() rebuilds mapping projections in place. Build late 
predicates with an
+    // isolated mapper so the active row group cannot observe an unprepared or 
incompatible mapper
+    // before its physical request reaches the reader's safe activation 
boundary.
+    auto refreshed_mapper = 
_data_reader.reader->create_column_mapper(_mapper_options);
+    DORIS_CHECK(refreshed_mapper != nullptr);
+    RETURN_IF_ERROR(refreshed_mapper->create_mapping(_projected_columns, 
_partition_values,
+                                                     
_data_reader.file_schema));
+    auto refreshed_request = std::make_shared<FileScanRequest>();
+    RETURN_IF_ERROR(refreshed_mapper->create_scan_request(
+            _table_filters, _projected_columns, refreshed_request.get(), 
_runtime_state,
+            _file_scan_request == nullptr ? nullptr : 
&_file_scan_request->local_positions));
+    // A refresh does not prove that every future runtime filter has arrived. 
Keep carrier values
+    // available whenever the split started with pending filters.
+    if (_push_down_agg_type == TPushAggOp::type::COUNT && 
_push_down_count_columns.has_value() &&
+        _push_down_count_columns->empty() && 
_all_runtime_filters_applied_for_split) {
+        for (const auto& column : refreshed_request->non_predicate_columns) {
+            
refreshed_request->count_star_placeholder_columns.push_back(column.column_id());
+        }
+    }
+    RETURN_IF_ERROR(customize_file_scan_request(refreshed_request.get()));
+    if (_file_scan_request == nullptr ||
+        !same_physical_scan_layout(*refreshed_request, *_file_scan_request)) {
+        // A reader cannot reinterpret columns already materialized with 
another block layout.
+        // Keep scanner-level filtering as the correctness fallback for hidden 
slots or nested
+        // projections instead of switching an incompatible physical shape 
mid-file.
+        return Status::OK();
+    }
+    RETURN_IF_ERROR(_open_local_filter_exprs(*refreshed_request));
+
+    if (_condition_cache_ctx != nullptr && !_condition_cache_ctx->is_hit) {
+        // Rows before and after a late RF were evaluated by different 
predicate snapshots. Such a
+        // partial MISS bitmap must never be published under either snapshot's 
cache key.
+        _condition_cache = nullptr;
+        _condition_cache_ctx = nullptr;
+        _data_reader.reader->set_condition_cache_context(nullptr);
+    }
+    {
+        SCOPED_TIMER(_profile.file_reader_total_timer);
+        SCOPED_TIMER(_profile.file_reader_refresh_timer);
+        
RETURN_IF_ERROR(_data_reader.reader->queue_scan_request(refreshed_request));
+    }
+    _file_scan_request = std::move(refreshed_request);
+    return Status::OK();
+}
+
 Status TableReader::_open_local_filter_exprs(const FileScanRequest& 
file_request) {
     RowDescriptor row_desc;
     for (const auto& conjunct : file_request.conjuncts) {
diff --git a/be/src/format_v2/table_reader.h b/be/src/format_v2/table_reader.h
index 6d679ec2d79..9ff2641f581 100644
--- a/be/src/format_v2/table_reader.h
+++ b/be/src/format_v2/table_reader.h
@@ -116,6 +116,7 @@ struct ReadProfile {
     RuntimeProfile::Counter* create_reader_timer = nullptr;
     RuntimeProfile::Counter* pushdown_agg_timer = nullptr;
     RuntimeProfile::Counter* open_reader_timer = nullptr;
+    RuntimeProfile::Counter* refresh_conjuncts_timer = nullptr;
     RuntimeProfile::Counter* runtime_filter_partition_prune_timer = nullptr;
     RuntimeProfile::Counter* runtime_filter_partition_pruned_range_counter = 
nullptr;
     RuntimeProfile::Counter* close_timer = nullptr;
@@ -124,6 +125,7 @@ struct ReadProfile {
     RuntimeProfile::Counter* file_reader_schema_timer = nullptr;
     RuntimeProfile::Counter* file_reader_mapper_timer = nullptr;
     RuntimeProfile::Counter* file_reader_open_timer = nullptr;
+    RuntimeProfile::Counter* file_reader_refresh_timer = nullptr;
     RuntimeProfile::Counter* file_reader_get_block_timer = nullptr;
     RuntimeProfile::Counter* file_reader_aggregate_timer = nullptr;
     RuntimeProfile::Counter* file_reader_close_timer = nullptr;
@@ -223,6 +225,10 @@ public:
     // 2. Parse delete predicates from split/task information, which will be 
used for later dynamic filtering and delete handling.
     virtual Status prepare_split(const SplitReadOptions& options);
 
+    // Refresh row-level predicates for an already prepared split. Physical 
readers that support
+    // this operation decide the safe boundary at which the new immutable 
request becomes active.
+    virtual Status refresh_conjuncts(VExprContextSPtrs conjuncts);
+
     virtual bool current_split_pruned() const { return _current_split_pruned; }
     virtual bool current_split_uses_metadata_count() const {
         return _current_split_uses_metadata_count;
@@ -453,8 +459,11 @@ protected:
         // marker is independent of aggregate eligibility: with position 
deletes, for example,
         // metadata COUNT must fall back to reading rows, but an arbitrary 
unsupported TIME_MILLIS
         // placeholder still must not be validated or decoded merely to carry 
the surviving count.
+        // Pending runtime filters may later target this retained slot, so 
placeholder values are
+        // safe only after every filter for the split has arrived.
         if (_push_down_agg_type == TPushAggOp::type::COUNT &&
-            _push_down_count_columns.has_value() && 
_push_down_count_columns->empty()) {
+            _push_down_count_columns.has_value() && 
_push_down_count_columns->empty() &&
+            _all_runtime_filters_applied_for_split) {
             file_request->count_star_placeholder_columns.reserve(
                     file_request->non_predicate_columns.size());
             for (const auto& column : file_request->non_predicate_columns) {
@@ -465,6 +474,7 @@ protected:
         RETURN_IF_ERROR(_open_local_filter_exprs(*file_request));
         _data_reader.file_block_layout.clear();
         _data_reader.block_template.clear();
+        _file_scan_request.reset();
         
_data_reader.file_block_layout.resize(file_request->local_positions.size());
 
         // 4. Build file block layout from file schema and column mapping. The 
layout describes
@@ -517,7 +527,8 @@ protected:
             SCOPED_TIMER(_profile.file_reader_open_timer);
             RETURN_IF_ERROR(_data_reader.reader->open(file_request));
         }
-        RETURN_IF_ERROR(_init_reader_condition_cache(*file_request));
+        _file_scan_request = std::move(file_request);
+        RETURN_IF_ERROR(_init_reader_condition_cache(*_file_scan_request));
         return Status::OK();
     }
 
@@ -763,6 +774,7 @@ protected:
         _data_reader.file_schema.clear();
         _data_reader.file_block_layout.clear();
         _data_reader.block_template.clear();
+        _file_scan_request.reset();
         _current_task.reset();
         _current_file_description.reset();
         _current_reader_reached_eof = false;
@@ -1877,6 +1889,9 @@ protected:
         Block block_template;
     };
     DataReader _data_reader;
+    // Latest immutable request queued to the physical reader. The file-block 
layout remains fixed
+    // for the split even while predicates are refreshed at a reader-defined 
granule boundary.
+    std::shared_ptr<FileScanRequest> _file_scan_request;
     std::vector<ColumnDefinition> _projected_columns;
     std::unique_ptr<ScanTask> _current_task;
     std::optional<io::FileDescription> _current_file_description;
diff --git a/be/src/runtime/query_context.h b/be/src/runtime/query_context.h
index 3ef113c1fd1..3102cc85296 100644
--- a/be/src/runtime/query_context.h
+++ b/be/src/runtime/query_context.h
@@ -325,7 +325,6 @@ private:
     void _init_query_mem_tracker();
 
     std::unordered_map<int, RuntimePredicate> _runtime_predicates;
-
     std::unique_ptr<RuntimeFilterMgr> _runtime_filter_mgr;
     const TQueryOptions _query_options;
 
diff --git a/be/test/exprs/expr_zonemap_filter_test.cpp 
b/be/test/exprs/expr_zonemap_filter_test.cpp
index 1d80f40531e..668a528b6a1 100644
--- a/be/test/exprs/expr_zonemap_filter_test.cpp
+++ b/be/test/exprs/expr_zonemap_filter_test.cpp
@@ -22,6 +22,7 @@
 #include <array>
 #include <map>
 #include <memory>
+#include <mutex>
 #include <set>
 #include <string>
 #include <vector>
@@ -809,10 +810,12 @@ TEST(ExprZonemapFilterTest, 
DirectInPredicateMaterializesStringSetForZonemap) {
     direct_in_expr.add_child(slot);
     ASSERT_TRUE(direct_in_expr._materialize_for_zonemap_filter().ok());
 
-    EXPECT_TRUE(direct_in_expr._zonemap_materialized);
-    EXPECT_EQ(2, direct_in_expr._seg_filter_values.size());
-    EXPECT_EQ(Field::create_field<TYPE_STRING>("aaa"), 
direct_in_expr._seg_filter_min);
-    EXPECT_EQ(Field::create_field<TYPE_STRING>("zzz"), 
direct_in_expr._seg_filter_max);
+    EXPECT_TRUE(direct_in_expr._pruning_state->zonemap_materialized);
+    EXPECT_EQ(2, direct_in_expr._pruning_state->seg_filter_values.size());
+    EXPECT_EQ(Field::create_field<TYPE_STRING>("aaa"),
+              direct_in_expr._pruning_state->seg_filter_min);
+    EXPECT_EQ(Field::create_field<TYPE_STRING>("zzz"),
+              direct_in_expr._pruning_state->seg_filter_max);
 }
 
 TEST(ExprZonemapFilterTest, 
DirectInPredicateMaterializesZonemapValuesDuringPrepare) {
@@ -841,11 +844,34 @@ TEST(ExprZonemapFilterTest, 
DirectInPredicateMaterializesZonemapValuesDuringPrep
     VExprContext context(direct_in_expr);
     ASSERT_TRUE(context.prepare(&runtime_state, row_desc).ok());
 
-    EXPECT_TRUE(direct_in_expr->_zonemap_materialized);
+    EXPECT_TRUE(direct_in_expr->_pruning_state->zonemap_materialized);
     EXPECT_TRUE(direct_in_expr->can_evaluate_zonemap_filter());
-    EXPECT_EQ(2, direct_in_expr->_seg_filter_values.size());
-    EXPECT_EQ(int_field(1), direct_in_expr->_seg_filter_min);
-    EXPECT_EQ(int_field(30), direct_in_expr->_seg_filter_max);
+    EXPECT_EQ(2, direct_in_expr->_pruning_state->seg_filter_values.size());
+    EXPECT_EQ(int_field(1), direct_in_expr->_pruning_state->seg_filter_min);
+    EXPECT_EQ(int_field(30), direct_in_expr->_pruning_state->seg_filter_max);
+}
+
+TEST(ExprZonemapFilterTest, 
DirectInPredicateDeepCloneReusesMaterializedPruningState) {
+    auto type = int_type();
+    std::shared_ptr<HybridSetBase> filter(create_set(PrimitiveType::TYPE_INT, 
false));
+    int32_t low_value = 1;
+    int32_t high_value = 30;
+    filter->insert(&low_value);
+    filter->insert(&high_value);
+
+    auto direct_in_expr =
+            std::make_shared<VDirectInPredicate>(make_in_predicate_node(false, 
1), filter, true);
+    direct_in_expr->add_child(make_slot(0, type));
+    ASSERT_TRUE(direct_in_expr->_materialize_for_zonemap_filter().ok());
+
+    VExprSPtr cloned_expr;
+    ASSERT_TRUE(direct_in_expr->deep_clone(&cloned_expr).ok());
+    auto cloned_direct_in = 
std::dynamic_pointer_cast<VDirectInPredicate>(cloned_expr);
+    ASSERT_NE(cloned_direct_in, nullptr);
+    EXPECT_EQ(direct_in_expr->_pruning_state, 
cloned_direct_in->_pruning_state);
+    EXPECT_TRUE(cloned_direct_in->can_evaluate_zonemap_filter());
+    EXPECT_EQ(ZoneMapFilterResult::kNoMatch, 
cloned_direct_in->evaluate_zonemap_filter(
+                                                     
make_context(make_int_zonemap(10, 20), type)));
 }
 
 TEST(ExprZonemapFilterTest, DirectInPredicateRewritesStringSetToInPredicate) {
@@ -874,7 +900,7 @@ TEST(ExprZonemapFilterTest, 
DirectInPredicateSkipsMaterializationWhenSetTypeDiff
     direct_in_expr.add_child(slot);
 
     ASSERT_TRUE(direct_in_expr._materialize_for_zonemap_filter().ok());
-    EXPECT_FALSE(direct_in_expr._zonemap_materialized);
+    EXPECT_FALSE(direct_in_expr._pruning_state->zonemap_materialized);
     VExprSPtr in_expr;
     EXPECT_FALSE(direct_in_expr.get_slot_in_expr(in_expr));
 }
diff --git a/be/test/format_v2/jni/jni_table_reader_test.cpp 
b/be/test/format_v2/jni/jni_table_reader_test.cpp
index 2eee5548ad7..a59a4bbdf56 100644
--- a/be/test/format_v2/jni/jni_table_reader_test.cpp
+++ b/be/test/format_v2/jni/jni_table_reader_test.cpp
@@ -27,8 +27,11 @@
 #include <thread>
 #include <vector>
 
+#include "core/data_type/data_type_number.h"
 #include "core/data_type/data_type_string.h"
 #include "core/data_type/data_type_struct.h"
+#include "exprs/vexpr_context.h"
+#include "exprs/vslot_ref.h"
 #include "format/jni/jni_data_bridge.h"
 #include "io/io_common.h"
 
@@ -238,6 +241,29 @@ TEST(JniTableReaderTest, 
AdaptiveProbeSetBeforePrepareControlsFirstJniOpen) {
     EXPECT_TRUE(reader.TEST_scanner_opened());
 }
 
+TEST(JniTableReaderTest, RefreshedConjunctIsReadyBeforeFilteringOpenScanner) {
+    FakeJniTableReader reader;
+    ASSERT_TRUE(init_reader(&reader, nullptr).ok());
+    ASSERT_TRUE(reader.prepare_split({
+                                             .partition_values = {},
+                                             .conjuncts = std::nullopt,
+                                             .partition_prune_conjuncts = {},
+                                             .all_runtime_filters_applied = 
true,
+                                             .condition_cache_digest = 
std::nullopt,
+                                             .cache = nullptr,
+                                             .current_range = {},
+                                             .current_split_format = 
FileFormat::JNI,
+                                             .global_rowid_context = 
std::nullopt,
+                                     })
+                        .ok());
+
+    auto refreshed = VExprContext::create_shared(
+            VSlotRef::create_shared(0, 0, 0, 
std::make_shared<DataTypeUInt8>(), "filter_column"));
+    ASSERT_FALSE(refreshed->root()->ready_status().ok());
+    ASSERT_TRUE(reader.refresh_conjuncts({refreshed}).ok());
+    EXPECT_TRUE(refreshed->root()->ready_status().ok());
+}
+
 TEST(JniTableReaderTest, CommonLifecycleTimersContainJniLifecycleWork) {
     constexpr auto delay = std::chrono::milliseconds(8);
     RuntimeProfile profile("JniLifecycleContainment");
diff --git a/be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp 
b/be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp
index 72ccbf44844..2145b6ab60d 100644
--- a/be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp
+++ b/be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp
@@ -127,6 +127,26 @@ TEST(ParquetBenchmarkScenariosTest, 
NestedSelectionCoversSparseParentSurvivors)
     }
 }
 
+TEST(ParquetBenchmarkScenariosTest, 
SelectionMatrixCoversIdentityAndSuccessiveCompaction) {
+    const auto scenarios = selection_scenarios();
+    EXPECT_EQ(scenarios.size(), size_t {25});
+    EXPECT_TRUE(std::ranges::any_of(scenarios, [](const SelectionScenario& 
scenario) {
+        return scenario.operation == SelectionOperation::RESIZE_IDENTITY;
+    }));
+    for (const auto operation :
+         {SelectionOperation::ROW_FILTER, SelectionOperation::CASCADE_FILTER}) 
{
+        for (const int selectivity : {0, 1, 10, 50, 90, 100}) {
+            for (const auto pattern : {Pattern::CLUSTERED, 
Pattern::ALTERNATING}) {
+                EXPECT_TRUE(std::ranges::any_of(scenarios, [&](const 
SelectionScenario& scenario) {
+                    return scenario.operation == operation &&
+                           scenario.selectivity_percent == selectivity &&
+                           scenario.pattern == pattern;
+                })) << "missing selection compaction shape";
+            }
+        }
+    }
+}
+
 TEST(ParquetBenchmarkScenariosTest, 
ReaderMatrixCoversNullableSparseAndProjectionAxes) {
     const auto scenarios = reader_scenarios();
     // Keep the exact count aligned with the upstream complex-residual 
scenario retained by rebase.
diff --git a/be/test/format_v2/parquet/parquet_reader_control_test.cpp 
b/be/test/format_v2/parquet/parquet_reader_control_test.cpp
index 0c70ddaf01e..e21439e8855 100644
--- a/be/test/format_v2/parquet/parquet_reader_control_test.cpp
+++ b/be/test/format_v2/parquet/parquet_reader_control_test.cpp
@@ -180,6 +180,7 @@ TEST(SelectionVectorTest, 
MaterializedFilterIsReusedUntilSelectionChanges) {
 
 TEST(SelectionVectorTest, IdentitySelectionDoesNotMaterializeFilter) {
     SelectionVector selection(4);
+    EXPECT_FALSE(selection.is_set());
     const uint8_t* filter = reinterpret_cast<const uint8_t*>(1);
     ASSERT_TRUE(selection.materialize_filter(4, 4, &filter).ok());
     EXPECT_EQ(filter, nullptr);
@@ -259,6 +260,36 @@ TEST(NativeNestedSelectionTest, 
PreservesPriorLevelsAcrossPageContinuation) {
     EXPECT_EQ(definition_levels, (std::vector<level_t> {3, 3, 2}));
 }
 
+TEST(SelectionVectorTest, BulkCompactionSupportsBothFilterCoordinates) {
+    SelectionVector selection(6);
+    const uint8_t row_filter[] = {0, 1, 1, 0, 1, 0};
+    ASSERT_EQ(selection.compact_with_row_filter(row_filter, 6), 3);
+    EXPECT_EQ(selection.get_index(0), 1);
+    EXPECT_EQ(selection.get_index(1), 2);
+    EXPECT_EQ(selection.get_index(2), 4);
+
+    const uint8_t compact_filter[] = {1, 0, 1};
+    ASSERT_EQ(selection.compact_with_selection_filter(compact_filter, 3), 2);
+    EXPECT_EQ(selection.get_index(0), 1);
+    EXPECT_EQ(selection.get_index(1), 4);
+    EXPECT_TRUE(selection.verify(2, 6).ok());
+}
+
+TEST(SelectionVectorTest, BatchResetRetainsMaterializedScratchHighWaterMark) {
+    SelectionVector selection(6);
+    ASSERT_NE(selection.data(), nullptr);
+    const uint8_t first_filter[] = {0, 1, 1, 0, 1, 0};
+    ASSERT_EQ(selection.compact_with_row_filter(first_filter, 6), 3);
+
+    selection.resize(6);
+    const uint8_t second_filter[] = {0, 0, 0, 0, 0, 1};
+    ASSERT_EQ(selection.compact_with_row_filter(second_filter, 6), 1);
+    EXPECT_EQ(selection.get_index(0), 5);
+    // Positions beyond the logical result remain reusable scratch. Clearing 
and resizing the
+    // owned vector would value-initialize this slot on every scanner batch.
+    EXPECT_EQ(selection.get_index(5), 5);
+}
+
 TEST(ParquetColumnReaderControlTest, BaseSelectUsesSkipReadRanges) {
     CursorColumnReader reader;
     SelectionVector selection(3);
@@ -318,6 +349,30 @@ TEST(ParquetColumnReaderControlTest, 
SchedulerOrsPageCrossingOncePerBatch) {
     EXPECT_EQ(lazy_ptr->page_crossing_checks(), 1);
 }
 
+TEST(ParquetColumnReaderControlTest, 
PendingRequestActivatesOnlyAtRowGroupBoundary) {
+    ParquetScanScheduler scheduler;
+    auto initial = std::make_shared<format::FileScanRequest>();
+    auto refreshed = std::make_shared<format::FileScanRequest>();
+    refreshed->predicate_only_columns.push_back(format::LocalColumnId(7));
+
+    scheduler.set_scan_request(initial);
+    scheduler._has_current_row_group = true;
+    scheduler.queue_scan_request(refreshed);
+    scheduler.activate_pending_scan_request_at_row_group_boundary();
+    EXPECT_EQ(scheduler._active_request, initial);
+
+    scheduler._has_current_row_group = false;
+    scheduler._predicate_survival_ratio = 0.5;
+    scheduler._predicate_batch_sequence = 3;
+    scheduler._predicate_runtime_stats.emplace(1, 
detail::AdaptivePredicateStats {});
+    scheduler.activate_pending_scan_request_at_row_group_boundary();
+    EXPECT_EQ(scheduler._active_request, refreshed);
+    EXPECT_TRUE(scheduler._remaining_plans_need_replanning);
+    EXPECT_EQ(scheduler._predicate_survival_ratio, -1);
+    EXPECT_EQ(scheduler._predicate_batch_sequence, 0);
+    EXPECT_TRUE(scheduler._predicate_runtime_stats.empty());
+}
+
 TEST(ParquetColumnReaderControlTest, 
PendingOutputDrainsBeforePageCrossingSample) {
     ParquetScanScheduler scheduler;
     scheduler._batch_size = 1;
diff --git a/be/test/format_v2/parquet/parquet_scan_test.cpp 
b/be/test/format_v2/parquet/parquet_scan_test.cpp
index 75733164ed2..8ed63a7fe28 100644
--- a/be/test/format_v2/parquet/parquet_scan_test.cpp
+++ b/be/test/format_v2/parquet/parquet_scan_test.cpp
@@ -1398,6 +1398,22 @@ void write_dictionary_int_pair_parquet_file(const 
std::string& file_path) {
     write_table(file_path, table, 6, true, false, false);
 }
 
+void write_dictionary_int_pair_parquet_file(const std::string& file_path,
+                                            const std::vector<int32_t>& 
dictionary_values) {
+    std::vector<int32_t> scores(dictionary_values.size());
+    for (size_t index = 0; index < scores.size(); ++index) {
+        scores[index] = static_cast<int32_t>((index + 1) * 10);
+    }
+    auto schema = arrow::schema({
+            arrow::field("id", arrow::int32(), false),
+            arrow::field("score", arrow::int32(), false),
+    });
+    auto table = arrow::Table::Make(
+            schema, {build_int32_array(dictionary_values), 
build_int32_array(scores)});
+    write_table(file_path, table, 
static_cast<int64_t>(dictionary_values.size()), true, false,
+                false);
+}
+
 void write_dictionary_bigint_pair_parquet_file(const std::string& file_path) {
     auto schema = arrow::schema({
             arrow::field("id", arrow::int64(), false),
@@ -2268,6 +2284,49 @@ TEST_F(ParquetScanTest, 
NoRequestedColumnsReturnsRowsOnlyAcrossRowGroups) {
     EXPECT_EQ(total_rows, 6);
 }
 
+TEST_F(ParquetScanTest, 
LateRequestReplansUnopenedRowGroupsWithFooterStatistics) {
+    write_int_pair_parquet_file(_file_path, 2);
+    RuntimeProfile profile("profile");
+    auto reader = create_reader(0, -1, &profile);
+    RuntimeState state {TQueryOptions(), TQueryGlobals()};
+    ASSERT_TRUE(reader->init(&state).ok());
+    reader->set_batch_size(2);
+
+    std::vector<format::ColumnDefinition> schema;
+    ASSERT_TRUE(reader->get_schema(&schema).ok());
+    auto initial = std::make_shared<format::FileScanRequest>();
+    format::FileScanRequestBuilder initial_builder(initial.get());
+    
ASSERT_TRUE(initial_builder.add_non_predicate_column(format::LocalColumnId(0)).ok());
+    
ASSERT_TRUE(initial_builder.add_non_predicate_column(format::LocalColumnId(1)).ok());
+    ASSERT_TRUE(reader->open(initial).ok());
+
+    Block first_block = build_file_block(schema);
+    size_t first_rows = 0;
+    bool eof = false;
+    ASSERT_TRUE(reader->get_block(&first_block, &first_rows, &eof).ok());
+    ASSERT_EQ(first_rows, 2);
+    
EXPECT_EQ(int32_data_column(*first_block.get_by_position(0).column).get_data(),
+              (ColumnInt32::Container {1, 2}));
+
+    auto refreshed = std::make_shared<format::FileScanRequest>();
+    format::FileScanRequestBuilder refreshed_builder(refreshed.get());
+    
ASSERT_TRUE(refreshed_builder.add_predicate_column(format::LocalColumnId(0)).ok());
+    
ASSERT_TRUE(refreshed_builder.add_non_predicate_column(format::LocalColumnId(1)).ok());
+    refreshed->conjuncts.push_back(create_int32_zonemap_conjunct(0, 
Int32ZoneMapExpr::Op::GT, 4));
+    ASSERT_TRUE(reader->queue_scan_request(refreshed).ok());
+
+    Block refreshed_block = build_file_block(schema);
+    size_t refreshed_rows = 0;
+    ASSERT_TRUE(reader->get_block(&refreshed_block, &refreshed_rows, 
&eof).ok());
+    ASSERT_EQ(refreshed_rows, 2);
+    
EXPECT_EQ(int32_data_column(*refreshed_block.get_by_position(0).column).get_data(),
+              (ColumnInt32::Container {5, 6}));
+    // The middle row group is rejected from footer statistics before any data 
page is decoded.
+    EXPECT_EQ(counter_value(profile, "RawRowsRead"), 4);
+    EXPECT_EQ(counter_value(profile, "RowGroupsFilteredByMinMax"), 1);
+    EXPECT_NE(profile.get_counter("RefreshScanRequestTime"), nullptr);
+}
+
 TEST_F(ParquetScanTest, PredicateColumnsFilterRoundByRound) {
     write_int_pair_parquet_file(_file_path, 6, false);
     RuntimeProfile profile("profile");
@@ -3163,17 +3222,82 @@ TEST_F(ParquetScanTest, 
PredicateOnlyDictionaryRangeSkipsTypedValueMaterializati
     conjunct->close();
 }
 
+TEST_F(ParquetScanTest, DictionaryFiltersAreBuiltFromEachReaderSnapshot) {
+    struct ScanResult {
+        std::vector<int32_t> scores;
+        int64_t typed_compare_columns = 0;
+    };
+
+    auto scan = [&](int32_t lower_bound) {
+        RuntimeProfile profile("profile");
+        RuntimeState state {TQueryOptions(), TQueryGlobals()};
+        auto reader = create_reader(0, -1, &profile);
+        EXPECT_TRUE(reader->init(&state).ok());
+
+        std::vector<format::ColumnDefinition> schema;
+        EXPECT_TRUE(reader->get_schema(&schema).ok());
+        auto request = std::make_shared<format::FileScanRequest>();
+        format::FileScanRequestBuilder request_builder(request.get());
+        
EXPECT_TRUE(request_builder.add_predicate_column(format::LocalColumnId(0)).ok());
+        
EXPECT_TRUE(request_builder.add_non_predicate_column(format::LocalColumnId(1)).ok());
+        request->predicate_only_columns.push_back(format::LocalColumnId(0));
+        auto conjunct =
+                create_int32_function_conjunct(0, "gt", TExprOpcode::GT, 
lower_bound, false);
+        EXPECT_TRUE(conjunct->prepare(&state, RowDescriptor()).ok());
+        EXPECT_TRUE(conjunct->open(&state).ok());
+        request->conjuncts.push_back(conjunct);
+        EXPECT_TRUE(reader->open(request).ok());
+
+        ScanResult result;
+        bool eof = false;
+        while (!eof) {
+            Block block = build_file_block(schema);
+            size_t rows = 0;
+            EXPECT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+            const auto& score_column = 
int32_data_column(*block.get_by_position(1).column);
+            for (size_t row = 0; row < rows; ++row) {
+                result.scores.push_back(score_column.get_element(row));
+            }
+        }
+        result.typed_compare_columns = counter_value(profile, 
"DictFilterTypedCompareColumns");
+        conjunct->close();
+        EXPECT_TRUE(reader->close().ok());
+        return result;
+    };
+
+    write_dictionary_int_pair_parquet_file(_file_path);
+    const auto first = scan(2);
+    EXPECT_EQ(first.scores, std::vector<int32_t>({30, 40, 50, 60}));
+    EXPECT_EQ(first.typed_compare_columns, 1);
+
+    const auto repeated = scan(2);
+    EXPECT_EQ(repeated.scores, first.scores);
+    EXPECT_EQ(repeated.typed_compare_columns, 1);
+
+    const auto changed_predicate = scan(3);
+    EXPECT_EQ(changed_predicate.scores, std::vector<int32_t>({40, 50, 60}));
+    EXPECT_EQ(changed_predicate.typed_compare_columns, 1);
+
+    write_dictionary_int_pair_parquet_file(_file_path, {7, 1, 8, 2, 9, 3});
+    const auto changed_dictionary = scan(2);
+    EXPECT_EQ(changed_dictionary.scores, std::vector<int32_t>({10, 30, 50, 
60}));
+    EXPECT_EQ(changed_dictionary.typed_compare_columns, 1);
+}
+
 TEST_F(ParquetScanTest, PredicateOnlyDictionaryTopNUsesDictionaryIds) {
     write_dictionary_int_pair_parquet_file(_file_path);
     RuntimeProfile profile("profile");
-    auto reader = create_reader(0, -1, &profile);
     RuntimeState state {TQueryOptions(), TQueryGlobals()};
+    auto prepared =
+            create_topn_conjunct(&state, 0, 
make_nullable(std::make_shared<DataTypeInt32>()),
+                                 Field::create_field<TYPE_INT>(3));
+    ASSERT_NE(state.get_query_ctx(), nullptr);
+    auto reader = create_reader(0, -1, &profile);
     ASSERT_TRUE(reader->init(&state).ok());
 
     std::vector<format::ColumnDefinition> schema;
     ASSERT_TRUE(reader->get_schema(&schema).ok());
-    auto prepared =
-            create_topn_conjunct(&state, 0, schema[0].type, 
Field::create_field<TYPE_INT>(3));
+    
ASSERT_TRUE(schema[0].type->equals(*prepared.conjunct->root()->children()[0]->data_type()));
     auto request = std::make_shared<format::FileScanRequest>();
     format::FileScanRequestBuilder request_builder(request.get());
     
ASSERT_TRUE(request_builder.add_predicate_column(format::LocalColumnId(0)).ok());
diff --git a/be/test/format_v2/table/hudi_reader_test.cpp 
b/be/test/format_v2/table/hudi_reader_test.cpp
index 96126281744..e75eee47be3 100644
--- a/be/test/format_v2/table/hudi_reader_test.cpp
+++ b/be/test/format_v2/table/hudi_reader_test.cpp
@@ -141,6 +141,18 @@ public:
     }
 };
 
+class RefreshTrackingTableReader final : public TableReader {
+public:
+    Status prepare_split(const SplitReadOptions&) override { return 
Status::OK(); }
+
+    Status refresh_conjuncts(VExprContextSPtrs conjuncts) override {
+        ++refresh_count;
+        return TableReader::refresh_conjuncts(std::move(conjuncts));
+    }
+
+    int refresh_count = 0;
+};
+
 // Scenario: FileScannerV2 Hudi native reader uses the split schema id to 
annotate the physical
 // file schema before TableColumnMapper runs. This keeps schema-evolved Hudi 
files on field-id
 // mapping, including renamed nested children.
@@ -269,6 +281,39 @@ TEST(HudiHybridReaderTest, 
AggregatesConditionCacheHitsFromBothChildren) {
     EXPECT_EQ(reader.condition_cache_hit_count(), 9);
 }
 
+TEST(HudiHybridReaderTest, ForwardsLatePredicatesToActiveChild) {
+    RuntimeState state {TQueryOptions(), TQueryGlobals()};
+    TFileScanRangeParams scan_params;
+    scan_params.__set_format_type(TFileFormatType::FORMAT_PARQUET);
+    hudi::HudiHybridReader reader;
+    RefreshTrackingTableReader* child = nullptr;
+    reader.TEST_set_child_reader_factories(
+            [&] {
+                auto tracking = std::make_unique<RefreshTrackingTableReader>();
+                child = tracking.get();
+                return tracking;
+            },
+            [] { return std::make_unique<TableReader>(); });
+    ASSERT_TRUE(reader.init({
+                                    .projected_columns = {},
+                                    .conjuncts = {},
+                                    .format = FileFormat::PARQUET,
+                                    .scan_params = &scan_params,
+                                    .io_ctx = nullptr,
+                                    .runtime_state = &state,
+                                    .scanner_profile = nullptr,
+                            })
+                        .ok());
+
+    SplitReadOptions split;
+    split.current_split_format = FileFormat::PARQUET;
+    split.current_range.__set_format_type(TFileFormatType::FORMAT_PARQUET);
+    ASSERT_TRUE(reader.prepare_split(split).ok());
+    ASSERT_NE(child, nullptr);
+    ASSERT_TRUE(reader.refresh_conjuncts({}).ok());
+    EXPECT_EQ(child->refresh_count, 1);
+}
+
 TEST(HudiHybridReaderTest, 
NativeCountStarReportsMetadataRowsThroughHybridReader) {
     const auto test_dir =
             std::filesystem::temp_directory_path() / 
"doris_hudi_hybrid_count_star_test";
diff --git a/be/test/format_v2/table/paimon_reader_test.cpp 
b/be/test/format_v2/table/paimon_reader_test.cpp
index 6ebec512f42..32b82ab12ac 100644
--- a/be/test/format_v2/table/paimon_reader_test.cpp
+++ b/be/test/format_v2/table/paimon_reader_test.cpp
@@ -79,6 +79,18 @@ public:
     }
 };
 
+class RefreshTrackingTableReader final : public TableReader {
+public:
+    Status prepare_split(const SplitReadOptions&) override { return 
Status::OK(); }
+
+    Status refresh_conjuncts(VExprContextSPtrs conjuncts) override {
+        ++refresh_count;
+        return TableReader::refresh_conjuncts(std::move(conjuncts));
+    }
+
+    int refresh_count = 0;
+};
+
 class SplitFormatTrackingTableReader final : public TableReader {
 public:
     Status prepare_split(const SplitReadOptions& options) override {
@@ -754,6 +766,38 @@ TEST(PaimonHybridReaderTest, 
AggregatesConditionCacheHitsFromBothChildren) {
     EXPECT_EQ(reader.condition_cache_hit_count(), 8);
 }
 
+TEST(PaimonHybridReaderTest, ForwardsLatePredicatesToActiveChild) {
+    RuntimeState state {TQueryOptions(), TQueryGlobals()};
+    auto scan_params = make_local_parquet_scan_params();
+    paimon::PaimonHybridReader reader;
+    RefreshTrackingTableReader* child = nullptr;
+    reader.TEST_set_child_reader_factories(
+            [&] {
+                auto tracking = std::make_unique<RefreshTrackingTableReader>();
+                child = tracking.get();
+                return tracking;
+            },
+            [] { return std::make_unique<TableReader>(); });
+    ASSERT_TRUE(reader.init({
+                                    .projected_columns = {},
+                                    .conjuncts = {},
+                                    .format = FileFormat::PARQUET,
+                                    .scan_params = &scan_params,
+                                    .io_ctx = nullptr,
+                                    .runtime_state = &state,
+                                    .scanner_profile = nullptr,
+                            })
+                        .ok());
+
+    SplitReadOptions split;
+    split.current_split_format = FileFormat::PARQUET;
+    split.current_range = 
make_paimon_native_range(TFileFormatType::FORMAT_PARQUET);
+    ASSERT_TRUE(reader.prepare_split(split).ok());
+    ASSERT_NE(child, nullptr);
+    ASSERT_TRUE(reader.refresh_conjuncts({}).ok());
+    EXPECT_EQ(child->refresh_count, 1);
+}
+
 TEST(PaimonHybridReaderTest, 
NativeCountColumnReportsMetadataRowsThroughHybridReader) {
     const auto test_dir =
             std::filesystem::temp_directory_path() / 
"doris_paimon_hybrid_count_column_test";
diff --git a/be/test/format_v2/table_reader_test.cpp 
b/be/test/format_v2/table_reader_test.cpp
index bb0db9329a2..f4bc1911d17 100644
--- a/be/test/format_v2/table_reader_test.cpp
+++ b/be/test/format_v2/table_reader_test.cpp
@@ -1050,6 +1050,7 @@ struct FakeFileReaderState {
     int init_count = 0;
     int open_count = 0;
     int close_count = 0;
+    int refresh_count = 0;
     int64_t total_rows = 2;
     int64_t aggregate_count = -1;
     int64_t condition_cache_base_granule = 0;
@@ -1060,6 +1061,7 @@ struct FakeFileReaderState {
     bool stop_during_read = false;
     bool not_found_during_init = false;
     std::shared_ptr<FileScanRequest> last_request;
+    std::shared_ptr<FileScanRequest> pending_request;
     std::optional<FileAggregateRequest> last_aggregate_request;
     std::shared_ptr<ConditionCacheContext> condition_cache_ctx;
     std::shared_ptr<io::IOContext> io_ctx;
@@ -1101,6 +1103,14 @@ public:
         return Status::OK();
     }
 
+    bool supports_scan_request_refresh() const override { return true; }
+
+    Status queue_scan_request(std::shared_ptr<FileScanRequest> request) 
override {
+        _state->pending_request = std::move(request);
+        ++_state->refresh_count;
+        return Status::OK();
+    }
+
     Status get_block(Block* file_block, size_t* rows, bool* eof) override {
         DORIS_CHECK(file_block != nullptr);
         DORIS_CHECK(rows != nullptr);
@@ -1220,6 +1230,12 @@ public:
                     std::shared_ptr<FakeFileReaderState> state)
             : _file_schema(std::move(file_schema)), _state(std::move(state)) {}
 
+    VExprContextSPtr TEST_mapping_projection(size_t index) const {
+        DORIS_CHECK(_data_reader.column_mapper != nullptr);
+        DORIS_CHECK_LT(index, _data_reader.column_mapper->mappings().size());
+        return _data_reader.column_mapper->mappings()[index].projection;
+    }
+
 protected:
     Status create_file_reader(std::unique_ptr<FileReader>* reader) override {
         DORIS_CHECK(reader != nullptr);
@@ -1610,6 +1626,101 @@ TEST(TableReaderTest, 
PrepareSplitReplacesInitialConjunctSnapshot) {
     ASSERT_TRUE(reader.close().ok());
 }
 
+TEST(TableReaderTest, ActiveReaderQueuesRefreshedRuntimeFilterRequest) {
+    std::vector<ColumnDefinition> file_schema;
+    file_schema.push_back(make_file_column(0, "id", 
std::make_shared<DataTypeInt32>()));
+    file_schema.push_back(make_file_column(1, "value", 
std::make_shared<DataTypeString>()));
+
+    std::vector<ColumnDefinition> projected_columns;
+    projected_columns.push_back(make_table_column(0, "id", 
std::make_shared<DataTypeInt32>()));
+    projected_columns.push_back(make_table_column(1, "value", 
std::make_shared<DataTypeString>()));
+    set_name_identifiers(&projected_columns);
+
+    RuntimeState state {TQueryOptions(), TQueryGlobals()};
+    RuntimeProfile profile("scanner");
+    auto fake_state = std::make_shared<FakeFileReaderState>();
+    fake_state->eof_with_first_batch = false;
+    FakeTableReader reader(file_schema, fake_state);
+    ASSERT_TRUE(reader.init({
+                                    .projected_columns = projected_columns,
+                                    .conjuncts = {},
+                                    .format = FileFormat::PARQUET,
+                                    .scan_params = nullptr,
+                                    .io_ctx = nullptr,
+                                    .runtime_state = &state,
+                                    .scanner_profile = &profile,
+                            })
+                        .ok());
+
+    SplitReadOptions split_options;
+    split_options.current_range.__set_path("fake-table-reader-input");
+    ASSERT_TRUE(reader.prepare_split(split_options).ok());
+    Block block = build_table_block(projected_columns);
+    bool eos = false;
+    ASSERT_TRUE(reader.get_block(&block, &eos).ok());
+    ASSERT_FALSE(eos);
+
+    VExprContextSPtrs refreshed {VExprContext::create_shared(
+            runtime_filter_wrapper_expr(table_int32_greater_than_expr(0, 0, 
1)))};
+    ASSERT_TRUE(reader.refresh_conjuncts(std::move(refreshed)).ok());
+    ASSERT_EQ(fake_state->refresh_count, 1);
+    ASSERT_NE(fake_state->pending_request, nullptr);
+    EXPECT_EQ(fake_state->pending_request->local_positions,
+              fake_state->last_request->local_positions);
+    EXPECT_EQ(projection_ids(fake_state->pending_request->predicate_columns),
+              std::vector<int32_t>({0}));
+    
EXPECT_EQ(projection_ids(fake_state->pending_request->non_predicate_columns),
+              std::vector<int32_t>({1}));
+    ASSERT_EQ(fake_state->pending_request->conjuncts.size(), 1);
+    
EXPECT_TRUE(fake_state->pending_request->conjuncts.front()->root()->is_rf_wrapper());
+    EXPECT_NE(profile.get_counter("RefreshConjunctsTime"), nullptr);
+    EXPECT_NE(profile.get_counter("FileReaderRefreshScanRequestTime"), 
nullptr);
+    ASSERT_TRUE(reader.close().ok());
+}
+
+TEST(TableReaderTest, RefreshKeepsActiveMappingProjectionSnapshot) {
+    std::vector<ColumnDefinition> file_schema;
+    file_schema.push_back(make_file_column(0, "id", 
std::make_shared<DataTypeInt32>()));
+
+    std::vector<ColumnDefinition> projected_columns;
+    projected_columns.push_back(make_table_column(0, "id", 
std::make_shared<DataTypeInt32>()));
+    set_name_identifiers(&projected_columns);
+
+    RuntimeState state {TQueryOptions(), TQueryGlobals()};
+    auto fake_state = std::make_shared<FakeFileReaderState>();
+    fake_state->eof_with_first_batch = false;
+    FakeTableReader reader(file_schema, fake_state);
+    ASSERT_TRUE(reader.init({
+                                    .projected_columns = projected_columns,
+                                    .conjuncts = {},
+                                    .format = FileFormat::PARQUET,
+                                    .scan_params = nullptr,
+                                    .io_ctx = nullptr,
+                                    .runtime_state = &state,
+                                    .scanner_profile = nullptr,
+                            })
+                        .ok());
+
+    SplitReadOptions split_options;
+    split_options.current_range.__set_path("fake-table-reader-input");
+    ASSERT_TRUE(reader.prepare_split(split_options).ok());
+    Block block = build_table_block(projected_columns);
+    bool eos = false;
+    ASSERT_TRUE(reader.get_block(&block, &eos).ok());
+    ASSERT_FALSE(eos);
+
+    const auto active_projection = reader.TEST_mapping_projection(0);
+    ASSERT_NE(active_projection, nullptr);
+    ASSERT_TRUE(active_projection->root()->ready_status().ok());
+    VExprContextSPtrs refreshed {VExprContext::create_shared(
+            runtime_filter_wrapper_expr(table_int32_greater_than_expr(0, 0, 
1)))};
+    ASSERT_TRUE(reader.refresh_conjuncts(std::move(refreshed)).ok());
+
+    EXPECT_EQ(reader.TEST_mapping_projection(0), active_projection);
+    
EXPECT_TRUE(reader.TEST_mapping_projection(0)->root()->ready_status().ok());
+    ASSERT_TRUE(reader.close().ok());
+}
+
 TEST(TableReaderTest, RefreshedConjunctDisablesTableLevelCount) {
     std::vector<ColumnDefinition> file_schema;
     file_schema.push_back(make_file_column(0, "id", 
std::make_shared<DataTypeInt32>()));
@@ -1692,9 +1803,64 @@ TEST(TableReaderTest, 
PendingRuntimeFilterDisablesTableLevelCount) {
     EXPECT_EQ(fake_state->open_count, 1);
     EXPECT_EQ(block.rows(), 2);
     ASSERT_NE(fake_state->last_request, nullptr);
-    // Aggregate pushdown is disabled while a runtime filter is pending, but 
COUNT(*) semantics do
-    // not change. The retained output slot remains a value-less placeholder 
during row fallback.
-    
EXPECT_TRUE(fake_state->last_request->is_count_star_placeholder(LocalColumnId(0)));
+    // A pending runtime filter may later target the retained output slot. The 
fallback reader must
+    // keep its real values until the refreshed physical request reaches a 
row-group boundary.
+    
EXPECT_FALSE(fake_state->last_request->is_count_star_placeholder(LocalColumnId(0)));
+    ASSERT_TRUE(reader.close().ok());
+}
+
+TEST(TableReaderTest, CountStarFallbackKeepsLateRuntimeFilterCarrierValues) {
+    const auto test_dir =
+            std::filesystem::temp_directory_path() / 
"doris_table_reader_count_star_late_rf_test";
+    std::filesystem::remove_all(test_dir);
+    std::filesystem::create_directories(test_dir);
+    const auto file_path = (test_dir / "split.parquet").string();
+    write_int_pair_parquet_file(file_path, {1, 2, 3, 4, 5, 6}, {10, 20, 30, 
40, 50, 60},
+                                {"one", "two", "three", "four", "five", 
"six"}, 2);
+
+    std::vector<ColumnDefinition> projected_columns;
+    projected_columns.push_back(make_table_column(0, "id", 
std::make_shared<DataTypeInt32>()));
+    set_name_identifiers(&projected_columns);
+
+    TQueryOptions query_options;
+    query_options.__set_batch_size(2);
+    RuntimeState state {query_options, TQueryGlobals()};
+    TableReader reader;
+    ASSERT_TRUE(reader.init({
+                                    .projected_columns = projected_columns,
+                                    .conjuncts = {},
+                                    .format = FileFormat::PARQUET,
+                                    .scan_params = nullptr,
+                                    .io_ctx = nullptr,
+                                    .runtime_state = &state,
+                                    .scanner_profile = nullptr,
+                                    .push_down_agg_type = 
TPushAggOp::type::COUNT,
+                                    .push_down_count_columns = 
std::vector<GlobalIndex> {},
+                            })
+                        .ok());
+    auto split_options = build_split_options(file_path);
+    split_options.all_runtime_filters_applied = false;
+    ASSERT_TRUE(reader.prepare_split(split_options).ok());
+
+    Block first_block = build_table_block(projected_columns);
+    bool eos = false;
+    ASSERT_TRUE(reader.get_block(&first_block, &eos).ok());
+    ASSERT_EQ(first_block.rows(), 2);
+    EXPECT_EQ(assert_cast<const 
ColumnInt32&>(expect_not_null_table_column(first_block, 0))
+                      .get_data(),
+              (ColumnInt32::Container {1, 2}));
+
+    VExprContextSPtrs refreshed {VExprContext::create_shared(
+            runtime_filter_wrapper_expr(table_int32_greater_than_expr(0, 0, 
4)))};
+    ASSERT_TRUE(reader.refresh_conjuncts(std::move(refreshed)).ok());
+    std::vector<int32_t> remaining_ids;
+    while (!eos) {
+        Block block = build_table_block(projected_columns);
+        ASSERT_TRUE(reader.get_block(&block, &eos).ok());
+        const auto& ids = assert_cast<const 
ColumnInt32&>(expect_not_null_table_column(block, 0));
+        remaining_ids.insert(remaining_ids.end(), ids.get_data().begin(), 
ids.get_data().end());
+    }
+    EXPECT_EQ(remaining_ids, std::vector<int32_t>({5, 6}));
     ASSERT_TRUE(reader.close().ok());
 }
 


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

Reply via email to