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

yiguolei 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 46bd26c7f6f [opt](parquet) Fuse fragmented nullable selection planning 
(#66397)
46bd26c7f6f is described below

commit 46bd26c7f6f8431e4ab87cd507f3ecbfba10c2d1
Author: Gabriel <[email protected]>
AuthorDate: Tue Aug 4 09:27:15 2026 +0800

    [opt](parquet) Fuse fragmented nullable selection planning (#66397)
    
    ### What
    
    - Fuse nullable definition-level runs with the row filter in one
    traversal.
    - Produce physical decode ranges, the selected NULL map, and selected
    value counts without first materializing and rescanning a row-wise
    selection map.
    - Reuse the existing selected-decoder strategies and nullable in-place
    expansion.
    - Restrict fusion to batches with at least 1,024 rows, at least 10%
    NULLs, and materially fragmented definition-level runs. No-NULL,
    low-NULL, clustered, nested, and non-expandable shapes keep the legacy
    path.
    
    ### Why the guard is conservative
    
    The full benchmark matrix includes no-NULL, low-NULL, and clustered
    level plans as negative controls. Those shapes do not remove enough
    legacy work to guarantee a win, so this change deliberately leaves them
    unchanged. Decoder selection and encoding-specific materialization are
    not modified.
    
    ### Verification
    
    - ASAN: `NativeNullableSelectionTest.*` and benchmark scenario tests:
    16/16 passed.
    - ASAN: `ParquetV2NativeDecoderTest.*`: 118/118 passed.
    
    #### Release microbenchmark
    
    | Coverage | Legacy/fused pairs | Correctness | Regressions | Mean CPU
    change | Least improvement |
    |---|---:|---|---:|---:|---:|
    | Full scenario matrix | 100 | Identical ranges and NULL maps | N/A
    (includes negative controls) | N/A | N/A |
    | Production-eligible scenarios | 30 | Identical ranges and NULL maps |
    0 | -42.68% | 8.08% |
    
    | Scenario | Repetitions | Legacy median CPU | Fused median CPU | CPU
    change | Legacy CV | Fused CV |
    |---|---:|---:|---:|---:|---:|---:|
    | 10% selectivity / 50% NULL, fragmented | 10 | 400,220 ns | 177,905 ns
    | -55.55% | 0.58% | 1.28% |
    | 99% selectivity / 50% NULL, high-selectivity boundary | 10 | 687,385
    ns | 460,657 ns | -32.98% | 0.60% | 1.32% |
    
    The microbenchmark isolates nullable selection planning; it is not
    presented as an end-to-end query speedup.
---
 be/benchmark/parquet/AGENTS.md                     |  12 +-
 be/benchmark/parquet/README.md                     |  11 +-
 be/benchmark/parquet/benchmark_parquet_kernels.hpp | 202 +++++++++++++++++++++
 be/benchmark/parquet/parquet_benchmark_scenarios.h |  37 ++++
 .../parquet/reader/native/column_chunk_reader.cpp  |  59 ++++++
 .../parquet/reader/native/column_chunk_reader.h    |   8 +
 .../parquet/reader/native/column_reader.cpp        |  43 ++++-
 .../parquet/reader/native/column_reader.h          |   1 +
 be/src/format_v2/parquet/reader/native/common.cpp  | 128 +++++++++++++
 be/src/format_v2/parquet/reader/native/common.h    |  11 ++
 .../parquet/parquet_benchmark_scenarios_test.cpp   |  26 +++
 .../parquet/parquet_reader_control_test.cpp        |  74 ++++++++
 12 files changed, 601 insertions(+), 11 deletions(-)

diff --git a/be/benchmark/parquet/AGENTS.md b/be/benchmark/parquet/AGENTS.md
index 4c1d3cf4d5e..4d8c0610f17 100644
--- a/be/benchmark/parquet/AGENTS.md
+++ b/be/benchmark/parquet/AGENTS.md
@@ -51,7 +51,7 @@ be/output/lib/benchmark_test --benchmark_list_tests \
   | grep -c '^ParquetDecoder/'  # currently 228
 
 be/output/lib/benchmark_test --benchmark_list_tests \
-  | grep -c '^ParquetKernel/'   # currently 92
+  | grep -c '^ParquetKernel/'   # currently 292
 
 be/output/lib/benchmark_test --benchmark_list_tests \
   | grep -c '^ParquetSelection/' # currently 25
@@ -146,13 +146,19 @@ cache to manufacture a cold run.
 | DELTA_LENGTH_BYTE_ARRAY | BYTE_ARRAY |
 | DELTA_BYTE_ARRAY | BYTE_ARRAY |
 
-`ParquetKernel` contains 92 cases across six decode and selection stages: 
BYTE_STREAM_SPLIT,
-DELTA_PREFIX_SUM, DICTIONARY_GATHER, NULLABLE_EXPAND, RAW_PREDICATE, and 
NESTED_SELECTION. It covers
+`ParquetKernel` contains 292 cases across seven decode and selection stages: 
BYTE_STREAM_SPLIT,
+DELTA_PREFIX_SUM, DICTIONARY_GATHER, NULLABLE_EXPAND, NULLABLE_SELECTION, 
RAW_PREDICATE, and
+NESTED_SELECTION. It covers
 the applicable four- and eight-byte types, three dictionary working-set sizes, 
0% through 90% null
 rates with both placement patterns, 0% through 100% raw-predicate 
selectivities, and 1%, 10%, and
 50% nested parent-row selectivities with both placement patterns. Nested 
selection registers the
 legacy and fused implementations in the same binary and validates both against 
an independent
 source-level oracle before timing.
+Nullable selection contributes 200 legacy/fused cases across five 
selectivities, five null rates,
+and independent clustered or alternating selection/null placement. Each pair 
is validated for
+identical physical ranges and null maps before timing. Treat no-NULL, 
low-NULL, and clustered
+level-plan cases as negative controls: production fusion is gated to batches 
with at least 1,024
+rows, at least 10% NULLs, and materially fragmented definition-level runs.
 
 `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
diff --git a/be/benchmark/parquet/README.md b/be/benchmark/parquet/README.md
index 156302b2423..891fd02d837 100644
--- a/be/benchmark/parquet/README.md
+++ b/be/benchmark/parquet/README.md
@@ -45,14 +45,21 @@ be/output/lib/benchmark_test \
 
 ## SIMD kernel cases
 
-`ParquetKernel` isolates six decode and selection stages from reader setup and 
virtual consumer
+`ParquetKernel` isolates seven decode and selection stages from reader setup 
and virtual consumer
 overhead: byte-stream-split transpose, delta prefix sum, numeric dictionary 
gather, nullable
-expansion, raw predicate evaluation, and repeated-level sparse selection. It 
covers the applicable
+expansion, nullable selection planning, raw predicate evaluation, and 
repeated-level sparse
+selection. It covers the applicable
 4-byte and 8-byte integer and floating-point physical types, raw-predicate 
selectivities from 0%
 through 100%, and nullable rates from 0% through 90% with clustered and 
alternating placement.
 Nested selection covers 1%, 10%, and 50% surviving parent rows with both 
placement patterns.
 Each nested-selection scenario registers both `impl_legacy` and `impl_fused`; 
both paths use the
 same source levels and are checked against an independent oracle before timing.
+Nullable selection planning registers legacy and fused pairs across five 
selectivities, five null
+rates, and independent clustered or alternating selection/null placement. Both 
implementations are
+checked for identical physical ranges and null maps before timing. The full 
matrix also acts as a
+negative control: production fusion is limited to batches with at least 1,024 
rows, at least 10%
+NULLs, and fragmented definition-level runs; no-NULL, low-NULL, and clustered 
pages retain the
+legacy planner.
 Dictionary gather uses 32-, 4,096-, and 262,144-entry working sets to separate 
cache-resident and
 cache-miss-dominated behavior.
 
diff --git a/be/benchmark/parquet/benchmark_parquet_kernels.hpp 
b/be/benchmark/parquet/benchmark_parquet_kernels.hpp
index 619d58fd8cf..de3a15c254b 100644
--- a/be/benchmark/parquet/benchmark_parquet_kernels.hpp
+++ b/be/benchmark/parquet/benchmark_parquet_kernels.hpp
@@ -63,6 +63,191 @@ struct NestedSelectionScratch {
     size_t ancestor_null_count = 0;
 };
 
+struct NullableSelectionScratch {
+    format::parquet::native::ColumnSelectVector legacy_selection;
+    ParquetSelection physical_selection;
+    NullMap output_nulls;
+    NullMap selected_nulls;
+    size_t num_filtered = 0;
+};
+
+inline void append_nullable_run(std::vector<uint16_t>* runs, bool is_null, 
size_t run_length,
+                                bool* previous_is_null) {
+    if (runs->empty()) {
+        if (is_null) {
+            runs->push_back(0);
+        }
+    } else if (*previous_is_null == is_null) {
+        runs->push_back(0);
+    }
+    while (run_length > USHRT_MAX) {
+        runs->push_back(USHRT_MAX);
+        runs->push_back(0);
+        run_length -= USHRT_MAX;
+    }
+    runs->push_back(static_cast<uint16_t>(run_length));
+    *previous_is_null = is_null;
+}
+
+inline std::vector<uint16_t> build_nullable_runs(const NullMap& nulls) {
+    std::vector<uint16_t> runs;
+    bool previous_is_null = false;
+    size_t row = 0;
+    while (row < nulls.size()) {
+        const bool is_null = nulls[row] != 0;
+        const size_t begin = row++;
+        while (row < nulls.size() && (nulls[row] != 0) == is_null) {
+            ++row;
+        }
+        append_nullable_run(&runs, is_null, row - begin, &previous_is_null);
+    }
+    return runs;
+}
+
+inline Status run_legacy_nullable_selection(NullableSelectionScratch* scratch,
+                                            const std::vector<uint16_t>& 
null_runs,
+                                            size_t num_values,
+                                            
format::parquet::native::FilterMap* filter) {
+    using ReadType = format::parquet::native::ColumnSelectVector::DataReadType;
+    scratch->output_nulls.clear();
+    scratch->selected_nulls.clear();
+    scratch->physical_selection.ranges.clear();
+    scratch->physical_selection.total_values = 0;
+    scratch->physical_selection.selected_values = 0;
+    RETURN_IF_ERROR(scratch->legacy_selection.init(null_runs, num_values, 
&scratch->output_nulls,
+                                                   filter, 0));
+    scratch->num_filtered = scratch->legacy_selection.num_filtered();
+
+    size_t physical_cursor = 0;
+    ReadType type;
+    while (const size_t run_length = 
scratch->legacy_selection.get_next_run<true>(&type)) {
+        switch (type) {
+        case ReadType::CONTENT:
+            if (!scratch->physical_selection.ranges.empty() &&
+                scratch->physical_selection.ranges.back().first +
+                                
scratch->physical_selection.ranges.back().count ==
+                        physical_cursor) {
+                scratch->physical_selection.ranges.back().count += run_length;
+            } else {
+                scratch->physical_selection.ranges.push_back(
+                        {.first = physical_cursor, .count = run_length});
+            }
+            scratch->physical_selection.selected_values += run_length;
+            scratch->selected_nulls.resize_fill(scratch->selected_nulls.size() 
+ run_length, 0);
+            physical_cursor += run_length;
+            break;
+        case ReadType::NULL_DATA:
+            scratch->selected_nulls.resize_fill(scratch->selected_nulls.size() 
+ run_length, 1);
+            break;
+        case ReadType::FILTERED_CONTENT:
+            physical_cursor += run_length;
+            break;
+        case ReadType::FILTERED_NULL:
+            break;
+        }
+    }
+    scratch->physical_selection.total_values = physical_cursor;
+    return Status::OK();
+}
+
+inline Status run_nullable_selection_once(NullableSelectionScratch* scratch,
+                                          const std::vector<uint16_t>& 
null_runs, size_t num_values,
+                                          size_t num_nulls,
+                                          format::parquet::native::FilterMap* 
filter,
+                                          NullableSelectionImplementation 
implementation) {
+    if (implementation == NullableSelectionImplementation::LEGACY) {
+        return run_legacy_nullable_selection(scratch, null_runs, num_values, 
filter);
+    }
+    scratch->output_nulls.clear();
+    return format::parquet::native::build_filtered_nullable_selection(
+            null_runs, num_values, num_nulls, &scratch->output_nulls, filter, 
0,
+            &scratch->physical_selection, &scratch->selected_nulls, 
&scratch->num_filtered);
+}
+
+inline bool equal_selection(const ParquetSelection& lhs, const 
ParquetSelection& rhs) {
+    if (lhs.total_values != rhs.total_values || lhs.selected_values != 
rhs.selected_values ||
+        lhs.ranges.size() != rhs.ranges.size()) {
+        return false;
+    }
+    for (size_t range = 0; range < lhs.ranges.size(); ++range) {
+        if (lhs.ranges[range].first != rhs.ranges[range].first ||
+            lhs.ranges[range].count != rhs.ranges[range].count) {
+            return false;
+        }
+    }
+    return true;
+}
+
+inline void run_nullable_selection_kernel(benchmark::State& state,
+                                          const NullableSelectionScenario& 
scenario) {
+    using format::parquet::native::FilterMap;
+
+    std::vector<uint8_t> filter_data(KERNEL_ROWS, 0);
+    const auto selected = make_selection_plan(KERNEL_ROWS, 
scenario.selectivity_percent,
+                                              scenario.selection_pattern);
+    visit_selected_rows(selected, [&](size_t row) { filter_data[row] = 1; });
+    FilterMap filter;
+    auto status = filter.init(filter_data.data(), filter_data.size(), false);
+    if (!status.ok()) {
+        state.SkipWithError(status.to_string().c_str());
+        return;
+    }
+
+    NullMap nulls;
+    nulls.resize_fill(KERNEL_ROWS, 0);
+    const auto null_plan =
+            make_selection_plan(KERNEL_ROWS, scenario.null_percent, 
scenario.null_pattern);
+    visit_selected_rows(null_plan, [&](size_t row) { nulls[row] = 1; });
+    const auto null_runs = build_nullable_runs(nulls);
+
+    NullableSelectionScratch legacy;
+    NullableSelectionScratch fused;
+    status = run_nullable_selection_once(&legacy, null_runs, KERNEL_ROWS, 
null_plan.selected_rows,
+                                         &filter, 
NullableSelectionImplementation::LEGACY);
+    if (status.ok()) {
+        status =
+                run_nullable_selection_once(&fused, null_runs, KERNEL_ROWS, 
null_plan.selected_rows,
+                                            &filter, 
NullableSelectionImplementation::FUSED);
+    }
+    if (!status.ok() || !equal_selection(legacy.physical_selection, 
fused.physical_selection) ||
+        legacy.output_nulls != fused.output_nulls ||
+        legacy.selected_nulls != fused.selected_nulls ||
+        legacy.num_filtered != fused.num_filtered) {
+        if (status.ok()) {
+            state.SkipWithError("nullable selection implementations disagree");
+        } else {
+            state.SkipWithError(status.to_string().c_str());
+        }
+        return;
+    }
+
+    NullableSelectionScratch scratch;
+    status = run_nullable_selection_once(&scratch, null_runs, KERNEL_ROWS, 
null_plan.selected_rows,
+                                         &filter, scenario.implementation);
+    if (!status.ok()) {
+        state.SkipWithError(status.to_string().c_str());
+        return;
+    }
+    for (auto _ : state) {
+        status = run_nullable_selection_once(&scratch, null_runs, KERNEL_ROWS,
+                                             null_plan.selected_rows, &filter,
+                                             scenario.implementation);
+        if (!status.ok()) {
+            state.SkipWithError(status.to_string().c_str());
+            return;
+        }
+        benchmark::DoNotOptimize(scratch.physical_selection.ranges.data());
+        benchmark::DoNotOptimize(scratch.selected_nulls.data());
+        benchmark::ClobberMemory();
+    }
+
+    state.SetItemsProcessed(static_cast<int64_t>(state.iterations()) *
+                            static_cast<int64_t>(KERNEL_ROWS));
+    state.counters["rows"] = static_cast<double>(KERNEL_ROWS);
+    state.counters["selected_rows"] = 
static_cast<double>(selected.selected_rows);
+    state.counters["null_rows"] = static_cast<double>(null_plan.selected_rows);
+}
+
 inline NestedSelectionOracle build_nested_selection_oracle(
         const std::vector<NestedLevel>& repetition_levels,
         const std::vector<NestedLevel>& definition_levels,
@@ -493,7 +678,24 @@ inline bool register_kernel_benchmarks() {
     return true;
 }
 
+inline bool register_nullable_selection_benchmarks() {
+    for (const auto& scenario : nullable_selection_scenarios()) {
+        const std::string name = "ParquetKernel/nullable_selection/sel_" +
+                                 std::to_string(scenario.selectivity_percent) 
+ "/null_" +
+                                 std::to_string(scenario.null_percent) + 
"/selection_" +
+                                 to_string(scenario.selection_pattern) + 
"/nulls_" +
+                                 to_string(scenario.null_pattern) + "/impl_" +
+                                 to_string(scenario.implementation);
+        benchmark::RegisterBenchmark(name.c_str(), [=](benchmark::State& 
state) {
+            run_nullable_selection_kernel(state, scenario);
+        })->Unit(benchmark::kNanosecond);
+    }
+    return true;
+}
+
 inline const bool KERNEL_BENCHMARKS_REGISTERED = register_kernel_benchmarks();
+inline const bool NULLABLE_SELECTION_BENCHMARKS_REGISTERED =
+        register_nullable_selection_benchmarks();
 
 } // namespace detail
 } // namespace doris::parquet_benchmark
diff --git a/be/benchmark/parquet/parquet_benchmark_scenarios.h 
b/be/benchmark/parquet/parquet_benchmark_scenarios.h
index a9c58c15d8c..f7eddf0b11a 100644
--- a/be/benchmark/parquet/parquet_benchmark_scenarios.h
+++ b/be/benchmark/parquet/parquet_benchmark_scenarios.h
@@ -55,6 +55,7 @@ enum class Kernel {
     NESTED_SELECTION
 };
 enum class NestedSelectionImplementation { LEGACY, FUSED };
+enum class NullableSelectionImplementation { LEGACY, FUSED };
 
 struct DecoderScenario {
     Encoding encoding;
@@ -89,6 +90,14 @@ struct SelectionScenario {
     Pattern pattern;
 };
 
+struct NullableSelectionScenario {
+    int selectivity_percent;
+    int null_percent;
+    Pattern selection_pattern;
+    Pattern null_pattern;
+    NullableSelectionImplementation implementation;
+};
+
 struct SelectionRange {
     size_t first;
     size_t count;
@@ -177,6 +186,24 @@ inline std::vector<SelectionScenario> 
selection_scenarios() {
     return scenarios;
 }
 
+inline std::vector<NullableSelectionScenario> nullable_selection_scenarios() {
+    std::vector<NullableSelectionScenario> scenarios;
+    for (const int selectivity : {1, 10, 50, 90, 99}) {
+        for (const int null_percent : {0, 1, 10, 50, 90}) {
+            for (const auto selection_pattern : {Pattern::CLUSTERED, 
Pattern::ALTERNATING}) {
+                for (const auto null_pattern : {Pattern::CLUSTERED, 
Pattern::ALTERNATING}) {
+                    for (const auto implementation : 
{NullableSelectionImplementation::LEGACY,
+                                                      
NullableSelectionImplementation::FUSED}) {
+                        scenarios.push_back({selectivity, null_percent, 
selection_pattern,
+                                             null_pattern, implementation});
+                    }
+                }
+            }
+        }
+    }
+    return scenarios;
+}
+
 inline std::vector<ReaderScenario> reader_scenarios() {
     std::vector<ReaderScenario> scenarios;
     std::set<std::tuple<ReaderOperation, Encoding, int, Pattern, int, 
Projection, int, int,
@@ -435,4 +462,14 @@ inline std::string to_string(NestedSelectionImplementation 
value) {
     return "unknown";
 }
 
+inline std::string to_string(NullableSelectionImplementation value) {
+    switch (value) {
+    case NullableSelectionImplementation::LEGACY:
+        return "legacy";
+    case NullableSelectionImplementation::FUSED:
+        return "fused";
+    }
+    return "unknown";
+}
+
 } // namespace doris::parquet_benchmark
diff --git a/be/src/format_v2/parquet/reader/native/column_chunk_reader.cpp 
b/be/src/format_v2/parquet/reader/native/column_chunk_reader.cpp
index f3c3af5ba99..d1677c66676 100644
--- a/be/src/format_v2/parquet/reader/native/column_chunk_reader.cpp
+++ b/be/src/format_v2/parquet/reader/native/column_chunk_reader.cpp
@@ -681,6 +681,12 @@ void remap_nullable_conversion_failures(IColumn::Filter* 
conversion_failure_null
     DORIS_CHECK_EQ(source, 0);
 }
 
+Status decode_prepared_nullable_values(IColumn& column, const DataTypeSerDe& 
serde,
+                                       Decoder& decoder, const 
ParquetDecodeContext& context,
+                                       ParquetMaterializationState& state,
+                                       const NullMap& selected_nulls,
+                                       int64_t* materialization_time);
+
 Status decode_selected_nullable_values(IColumn& column, const DataTypeSerDe& 
serde,
                                        Decoder& decoder, const 
ParquetDecodeContext& context,
                                        ParquetMaterializationState& state,
@@ -722,6 +728,16 @@ Status decode_selected_nullable_values(IColumn& column, 
const DataTypeSerDe& ser
     DORIS_CHECK_EQ(selection.total_values, select_vector.num_values() - 
select_vector.num_nulls());
     DORIS_CHECK_EQ(selected_nulls.size(),
                    select_vector.num_values() - select_vector.num_filtered());
+    return decode_prepared_nullable_values(column, serde, decoder, context, 
state, selected_nulls,
+                                           materialization_time);
+}
+
+Status decode_prepared_nullable_values(IColumn& column, const DataTypeSerDe& 
serde,
+                                       Decoder& decoder, const 
ParquetDecodeContext& context,
+                                       ParquetMaterializationState& state,
+                                       const NullMap& selected_nulls,
+                                       int64_t* materialization_time) {
+    auto& selection = state.selection;
 
     const size_t old_size = column.size();
     SCOPED_RAW_TIMER(materialization_time);
@@ -1735,6 +1751,49 @@ Status ColumnChunkReader<IN_COLLECTION, 
OFFSET_INDEX>::materialize_values(
     return Status::OK();
 }
 
+template <bool IN_COLLECTION, bool OFFSET_INDEX>
+bool ColumnChunkReader<IN_COLLECTION, 
OFFSET_INDEX>::supports_fused_nullable_selection(
+        IColumn& column) const {
+    return visit_nullable_expandable_column(column, [](auto&) {});
+}
+
+template <bool IN_COLLECTION, bool OFFSET_INDEX>
+Status ColumnChunkReader<IN_COLLECTION, 
OFFSET_INDEX>::materialize_fused_nullable_values(
+        MutableColumnPtr& doris_column, const DataTypeSerDe& serde, 
ParquetDecodeContext& context,
+        ParquetMaterializationState& state, size_t num_values, size_t 
num_nulls,
+        const NullMap& selected_nulls) {
+    if (num_values == 0) {
+        return Status::OK();
+    }
+    SCOPED_RAW_TIMER(&_chunk_statistics.decode_value_time);
+    DORIS_CHECK_GT(num_nulls, 0);
+    const size_t physical_values = num_values - num_nulls;
+    DORIS_CHECK_EQ(state.selection.total_values, physical_values);
+    DORIS_CHECK_LE(state.selection.selected_values, selected_nulls.size());
+    if (UNLIKELY(_empty_value_section && physical_values != 0)) {
+        return Status::Corruption(
+                "Parquet definition levels require {} values from an empty 
value section",
+                physical_values);
+    }
+    if (UNLIKELY((doris_column->is_column_dictionary() || 
context.dictionary_index_only) &&
+                 !_has_dict && physical_values != 0)) {
+        return Status::IOError("Not dictionary coded");
+    }
+    if (UNLIKELY(_remaining_num_values < num_values)) {
+        return Status::IOError("Decode too many values in current page");
+    }
+    RETURN_IF_ERROR(translate_value_encoding(_current_encoding, 
&context.encoding));
+
+    ++_chunk_statistics.hybrid_selection_batches;
+    const auto status = decode_prepared_nullable_values(*doris_column, serde, 
*_page_decoder,
+                                                        context, state, 
selected_nulls,
+                                                        
&_chunk_statistics.materialization_time);
+    _chunk_statistics.hybrid_selection_ranges += state.selection.ranges.size();
+    RETURN_IF_ERROR(status);
+    _remaining_num_values -= num_values;
+    return Status::OK();
+}
+
 template <bool IN_COLLECTION, bool OFFSET_INDEX>
 bool ColumnChunkReader<IN_COLLECTION, 
OFFSET_INDEX>::can_filter_fixed_width_values(
         const VExprSPtrs& conjuncts, int column_id, const DataTypeSerDe* serde,
diff --git a/be/src/format_v2/parquet/reader/native/column_chunk_reader.h 
b/be/src/format_v2/parquet/reader/native/column_chunk_reader.h
index 35bf5200d7a..a50b7e4bc08 100644
--- a/be/src/format_v2/parquet/reader/native/column_chunk_reader.h
+++ b/be/src/format_v2/parquet/reader/native/column_chunk_reader.h
@@ -177,6 +177,14 @@ public:
                               ParquetDecodeContext& context, 
ParquetMaterializationState& state,
                               ColumnSelectVector& select_vector);
 
+    bool supports_fused_nullable_selection(IColumn& column) const;
+
+    Status materialize_fused_nullable_values(MutableColumnPtr& doris_column,
+                                             const DataTypeSerDe& serde,
+                                             ParquetDecodeContext& context,
+                                             ParquetMaterializationState& 
state, size_t num_values,
+                                             size_t num_nulls, const NullMap& 
selected_nulls);
+
     static bool supports_raw_fixed_filter_encoding(tparquet::Encoding::type 
encoding,
                                                    tparquet::Type::type 
physical_type) {
         switch (encoding) {
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 f762cb0f9d3..b5ea931b29f 100644
--- a/be/src/format_v2/parquet/reader/native/column_reader.cpp
+++ b/be/src/format_v2/parquet/reader/native/column_reader.cpp
@@ -799,6 +799,8 @@ void ScalarColumnReader<IN_COLLECTION, 
OFFSET_INDEX>::release_batch_scratch(
                                                      max_retained_bytes);
     release_selection |= 
release_vector_if_oversized(&_materialization_state.selection.ranges,
                                                      max_retained_bytes);
+    release_selection |=
+            release_filter_if_oversized(&_fused_nullable_selection_nulls, 
max_retained_bytes);
     release_selection |=
             release_filter_if_oversized(&_fixed_width_predicate_nulls, 
max_retained_bytes);
     release_selection |=
@@ -832,6 +834,7 @@ void ScalarColumnReader<IN_COLLECTION, 
OFFSET_INDEX>::release_batch_scratch(
     release_selection |= 
release_vector_for_aggregate(&_nested_filter_map_data);
     release_selection |= 
release_vector_for_aggregate(&_materialization_state.dictionary_indices);
     release_selection |= 
release_vector_for_aggregate(&_materialization_state.selection.ranges);
+    release_selection |= 
release_filter_for_aggregate(&_fused_nullable_selection_nulls);
     release_selection |= 
release_filter_for_aggregate(&_fixed_width_predicate_nulls);
     release_selection |= 
release_filter_for_aggregate(&_fixed_width_predicate_matches);
     release_selection |= 
release_filter_for_aggregate(&_fixed_width_predicate_conversion_nulls);
@@ -860,7 +863,8 @@ size_t ScalarColumnReader<IN_COLLECTION, 
OFFSET_INDEX>::retained_batch_scratch_b
            _def_levels.capacity() * sizeof(level_t) +
            _null_run_lengths.capacity() * sizeof(uint16_t) +
            _nested_filter_map_data.capacity() * sizeof(uint8_t) +
-           _fixed_width_predicate_nulls.capacity() + 
_fixed_width_predicate_matches.capacity() +
+           _fused_nullable_selection_nulls.capacity() + 
_fixed_width_predicate_nulls.capacity() +
+           _fixed_width_predicate_matches.capacity() +
            _fixed_width_predicate_conversion_nulls.capacity() +
            _materialization_state.dictionary_indices.capacity() * 
sizeof(uint32_t) +
            _materialization_state.selection.ranges.capacity() * 
sizeof(ParquetSelectionRange) +
@@ -875,7 +879,8 @@ size_t ScalarColumnReader<IN_COLLECTION, 
OFFSET_INDEX>::active_batch_scratch_byt
             _serde == nullptr ? 0 : 
_serde->active_parquet_raw_predicate_scratch_bytes();
     return decoder_bytes + serde_bytes + _rep_levels.size() * sizeof(level_t) +
            _def_levels.size() * sizeof(level_t) + _null_run_lengths.size() * 
sizeof(uint16_t) +
-           _nested_filter_map_data.size() * sizeof(uint8_t) + 
_fixed_width_predicate_nulls.size() +
+           _nested_filter_map_data.size() * sizeof(uint8_t) +
+           _fused_nullable_selection_nulls.size() + 
_fixed_width_predicate_nulls.size() +
            _fixed_width_predicate_matches.size() + 
_fixed_width_predicate_conversion_nulls.size() +
            _materialization_state.dictionary_indices.size() * sizeof(uint32_t) 
+
            _materialization_state.selection.ranges.size() * 
sizeof(ParquetSelectionRange) +
@@ -892,6 +897,7 @@ void ScalarColumnReader<IN_COLLECTION, 
OFFSET_INDEX>::reserve_batch_scratch_for_
     _nested_filter_map_data.reserve(elements);
     _materialization_state.dictionary_indices.reserve(elements);
     _materialization_state.selection.ranges.reserve(elements);
+    _fused_nullable_selection_nulls.reserve(elements);
     _ancestor_null_indices.reserve(elements);
 }
 
@@ -955,6 +961,7 @@ Status ScalarColumnReader<IN_COLLECTION, 
OFFSET_INDEX>::_read_values(size_t num_
     }
     MutableColumnPtr data_column;
     _null_run_lengths.clear();
+    size_t num_nulls = 0;
     NullMap* map_data_column = nullptr;
     doris_column = IColumn::mutate(std::move(doris_column));
     if (is_column_nullable(*doris_column)) {
@@ -977,6 +984,9 @@ Status ScalarColumnReader<IN_COLLECTION, 
OFFSET_INDEX>::_read_values(size_t num_
                 }
 
                 bool is_null = def_level < _field_schema->definition_level;
+                if (is_null) {
+                    num_nulls += loop_read;
+                }
                 if (!(prev_is_null ^ is_null)) {
                     _null_run_lengths.emplace_back(0);
                 }
@@ -1006,10 +1016,26 @@ Status ScalarColumnReader<IN_COLLECTION, 
OFFSET_INDEX>::_read_values(size_t num_
         }
         _null_run_lengths.emplace_back((u_short)remaining);
     }
+    const bool use_fused_nullable_selection =
+            map_data_column != nullptr && filter_map.has_filter() && num_nulls 
> 0 &&
+            should_use_fused_nullable_selection(num_values, num_nulls, 
_null_run_lengths.size()) &&
+            _chunk_reader->supports_fused_nullable_selection(*data_column);
     {
         SCOPED_RAW_TIMER(&_decode_null_map_time);
-        RETURN_IF_ERROR(_select_vector.init(_null_run_lengths, num_values, 
map_data_column,
-                                            &filter_map, _filter_map_index));
+        if (use_fused_nullable_selection) {
+            size_t num_filtered = 0;
+            // The fused path owns both the physical ranges and selected NULL 
layout. Restrict it
+            // to fragmented, materially nullable level plans: clustered, 
low-NULL, and no-NULL
+            // pages already collapse into a few cheap legacy runs, while 
fusing them adds planning
+            // branches without removing enough work to guarantee a win.
+            RETURN_IF_ERROR(build_filtered_nullable_selection(
+                    _null_run_lengths, num_values, num_nulls, map_data_column, 
&filter_map,
+                    _filter_map_index, &_materialization_state.selection,
+                    &_fused_nullable_selection_nulls, &num_filtered));
+        } else {
+            RETURN_IF_ERROR(_select_vector.init(_null_run_lengths, num_values, 
map_data_column,
+                                                &filter_map, 
_filter_map_index));
+        }
         _filter_map_index += num_values;
     }
     DORIS_CHECK(_serde != nullptr);
@@ -1020,8 +1046,13 @@ Status ScalarColumnReader<IN_COLLECTION, 
OFFSET_INDEX>::_read_values(size_t num_
             conversion_failure_map(*_field_schema, type, 
_materialization_state.enable_strict_mode,
                                    map_data_column, &compatibility_scratch);
     const size_t materialization_start_row = data_column->size();
-    const auto status = _chunk_reader->materialize_values(data_column, 
*_serde, _decode_context,
-                                                          
_materialization_state, _select_vector);
+    const auto status =
+            use_fused_nullable_selection
+                    ? _chunk_reader->materialize_fused_nullable_values(
+                              data_column, *_serde, _decode_context, 
_materialization_state,
+                              num_values, num_nulls, 
_fused_nullable_selection_nulls)
+                    : _chunk_reader->materialize_values(data_column, *_serde, 
_decode_context,
+                                                        
_materialization_state, _select_vector);
     _materialization_state.conversion_failure_null_map = nullptr;
     if (status.ok()) {
         mark_local_timestamp_defaults(*_field_schema, type,
diff --git a/be/src/format_v2/parquet/reader/native/column_reader.h 
b/be/src/format_v2/parquet/reader/native/column_reader.h
index 18bcdd866f8..594f0ac0921 100644
--- a/be/src/format_v2/parquet/reader/native/column_reader.h
+++ b/be/src/format_v2/parquet/reader/native/column_reader.h
@@ -432,6 +432,7 @@ private:
     std::vector<uint16_t> _null_run_lengths;
     std::unordered_set<size_t> _ancestor_null_indices;
     std::vector<uint8_t> _nested_filter_map_data;
+    NullMap _fused_nullable_selection_nulls;
     NullMap _fixed_width_predicate_nulls;
     IColumn::Filter _fixed_width_predicate_matches;
     IColumn::Filter _fixed_width_predicate_conversion_nulls;
diff --git a/be/src/format_v2/parquet/reader/native/common.cpp 
b/be/src/format_v2/parquet/reader/native/common.cpp
index c50488575e4..020f613c5d2 100644
--- a/be/src/format_v2/parquet/reader/native/common.cpp
+++ b/be/src/format_v2/parquet/reader/native/common.cpp
@@ -17,6 +17,7 @@
 
 #include "format_v2/parquet/reader/native/common.h"
 
+#include <algorithm>
 #include <cstring>
 
 #include "core/types.h"
@@ -68,6 +69,133 @@ bool FilterMap::can_filter_all(size_t remaining_num_values, 
size_t filter_map_in
                    remaining_num_values) == remaining_num_values;
 }
 
+bool should_use_fused_nullable_selection(size_t num_values, size_t num_nulls,
+                                         size_t num_null_runs) {
+    constexpr size_t MIN_BATCH_VALUES = 1024;
+    constexpr size_t MIN_NULL_RUNS = 32;
+    constexpr size_t MAX_AVERAGE_NULL_RUN = 64;
+    constexpr size_t MIN_NULL_RATIO_DENOMINATOR = 10;
+    if (num_values < MIN_BATCH_VALUES || num_nulls < num_values / 
MIN_NULL_RATIO_DENOMINATOR) {
+        return false;
+    }
+    return num_null_runs >= std::max(MIN_NULL_RUNS, num_values / 
MAX_AVERAGE_NULL_RUN);
+}
+
+Status build_filtered_nullable_selection(const std::vector<uint16_t>& 
run_length_null_map,
+                                         size_t num_values, size_t num_nulls,
+                                         NullMap* output_null_map, FilterMap* 
filter_map,
+                                         size_t filter_map_index, 
ParquetSelection* selection,
+                                         NullMap* selected_nulls, size_t* 
num_filtered) {
+    if (output_null_map == nullptr || filter_map == nullptr || selection == 
nullptr ||
+        selected_nulls == nullptr || num_filtered == nullptr) {
+        return Status::InvalidArgument(
+                "Nullable selection planning requires non-null output state");
+    }
+    if (!filter_map->has_filter()) {
+        return Status::InvalidArgument("Nullable selection planning requires a 
row filter");
+    }
+    if (!filter_map->filter_all() &&
+        (filter_map->filter_map_data() == nullptr ||
+         filter_map_index + num_values > filter_map->filter_map_size())) {
+        return Status::InvalidArgument("Nullable selection filter range [{}, 
{}) exceeds size {}",
+                                       filter_map_index, filter_map_index + 
num_values,
+                                       filter_map->filter_map_size());
+    }
+    if (num_nulls > num_values) {
+        return Status::InvalidArgument("Nullable selection has {} nulls for {} 
values", num_nulls,
+                                       num_values);
+    }
+
+    selection->ranges.clear();
+    selection->total_values = num_values - num_nulls;
+    selection->selected_values = 0;
+    selected_nulls->clear();
+    *num_filtered = 0;
+    if (filter_map->filter_all()) {
+        *num_filtered = num_values;
+        return Status::OK();
+    }
+
+    selected_nulls->reserve(num_values);
+    const uint8_t* filter = filter_map->filter_map_data() + filter_map_index;
+    const auto select_physical_values = [&](size_t physical_index, size_t 
count) {
+        if (!selection->ranges.empty() &&
+            selection->ranges.back().first + selection->ranges.back().count == 
physical_index) {
+            selection->ranges.back().count += count;
+        } else {
+            selection->ranges.push_back({.first = physical_index, .count = 
count});
+        }
+        selection->selected_values += count;
+    };
+
+    if (num_nulls == 0) {
+        size_t row = 0;
+        while (row < num_values) {
+            const bool selected = filter[row] != 0;
+            const size_t run_start = row++;
+            while (row < num_values && (filter[row] != 0) == selected) {
+                ++row;
+            }
+            const size_t run_length = row - run_start;
+            if (selected) {
+                select_physical_values(run_start, run_length);
+            } else {
+                *num_filtered += run_length;
+            }
+        }
+        selected_nulls->resize_fill(selection->selected_values, 0);
+    } else {
+        size_t logical_index = 0;
+        size_t physical_index = 0;
+        size_t observed_nulls = 0;
+        bool is_null = false;
+        for (const size_t run_length : run_length_null_map) {
+            if (logical_index + run_length > num_values) {
+                return Status::InvalidArgument("Nullable selection run lengths 
exceed {} values",
+                                               num_values);
+            }
+            const size_t run_end = logical_index + run_length;
+            while (logical_index < run_end) {
+                const bool selected = filter[logical_index] != 0;
+                const size_t filter_run_start = logical_index++;
+                while (logical_index < run_end && (filter[logical_index] != 0) 
== selected) {
+                    ++logical_index;
+                }
+                const size_t filter_run_length = logical_index - 
filter_run_start;
+                if (selected) {
+                    selected_nulls->resize_fill(selected_nulls->size() + 
filter_run_length,
+                                                static_cast<UInt8>(is_null));
+                    if (!is_null) {
+                        select_physical_values(physical_index, 
filter_run_length);
+                    }
+                } else {
+                    *num_filtered += filter_run_length;
+                }
+                if (!is_null) {
+                    physical_index += filter_run_length;
+                } else {
+                    observed_nulls += filter_run_length;
+                }
+            }
+            is_null = !is_null;
+        }
+        if (logical_index != num_values || observed_nulls != num_nulls ||
+            physical_index != selection->total_values) {
+            return Status::InvalidArgument(
+                    "Nullable selection level plan is inconsistent: values={}, 
nulls={}",
+                    logical_index, observed_nulls);
+        }
+    }
+
+    const size_t old_null_size = output_null_map->size();
+    output_null_map->resize(old_null_size + selected_nulls->size());
+    if (!selected_nulls->empty()) {
+        memcpy(output_null_map->data() + old_null_size, selected_nulls->data(),
+               selected_nulls->size());
+    }
+    return Status::OK();
+}
+
 Status FilterMap::generate_nested_filter_map(const std::vector<level_t>& 
rep_levels,
                                              std::vector<uint8_t>& 
nested_filter_map_data,
                                              std::unique_ptr<FilterMap>* 
nested_filter_map,
diff --git a/be/src/format_v2/parquet/reader/native/common.h 
b/be/src/format_v2/parquet/reader/native/common.h
index eb6848ee299..bd687616f22 100644
--- a/be/src/format_v2/parquet/reader/native/common.h
+++ b/be/src/format_v2/parquet/reader/native/common.h
@@ -25,6 +25,7 @@
 
 #include "common/status.h"
 #include "core/column/column_nullable.h"
+#include "core/data_type_serde/parquet_decode_source.h"
 
 namespace doris::format::parquet::native {
 
@@ -116,4 +117,14 @@ private:
     size_t _read_index = 0;
 };
 
+Status build_filtered_nullable_selection(const std::vector<uint16_t>& 
run_length_null_map,
+                                         size_t num_values, size_t num_nulls,
+                                         NullMap* output_null_map, FilterMap* 
filter_map,
+                                         size_t filter_map_index, 
ParquetSelection* selection,
+                                         NullMap* selected_nulls, size_t* 
num_filtered);
+
+// Fusion pays for its additional planning branches only when definition 
levels are materially
+// nullable and fragmented. Keep compact/no-NULL batches on the run-oriented 
legacy path.
+bool should_use_fused_nullable_selection(size_t num_values, size_t num_nulls, 
size_t num_null_runs);
+
 } // namespace doris::format::parquet::native
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 2145b6ab60d..596d076c5f0 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,32 @@ TEST(ParquetBenchmarkScenariosTest, 
NestedSelectionCoversSparseParentSurvivors)
     }
 }
 
+TEST(ParquetBenchmarkScenariosTest, 
NullableSelectionPairsLegacyAndFusedAcrossRowShapes) {
+    const auto scenarios = nullable_selection_scenarios();
+    EXPECT_EQ(scenarios.size(), size_t {200});
+    for (const int selectivity : {1, 10, 50, 90, 99}) {
+        for (const int null_percent : {0, 1, 10, 50, 90}) {
+            for (const auto selection_pattern : {Pattern::CLUSTERED, 
Pattern::ALTERNATING}) {
+                for (const auto null_pattern : {Pattern::CLUSTERED, 
Pattern::ALTERNATING}) {
+                    for (const auto implementation : 
{NullableSelectionImplementation::LEGACY,
+                                                      
NullableSelectionImplementation::FUSED}) {
+                        EXPECT_TRUE(std::ranges::any_of(
+                                scenarios,
+                                [&](const NullableSelectionScenario& scenario) 
{
+                                    return scenario.selectivity_percent == 
selectivity &&
+                                           scenario.null_percent == 
null_percent &&
+                                           scenario.selection_pattern == 
selection_pattern &&
+                                           scenario.null_pattern == 
null_pattern &&
+                                           scenario.implementation == 
implementation;
+                                }))
+                                << "missing nullable selection comparison 
shape";
+                    }
+                }
+            }
+        }
+    }
+}
+
 TEST(ParquetBenchmarkScenariosTest, 
SelectionMatrixCoversIdentityAndSuccessiveCompaction) {
     const auto scenarios = selection_scenarios();
     EXPECT_EQ(scenarios.size(), size_t {25});
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 e21439e8855..37deb32d5bf 100644
--- a/be/test/format_v2/parquet/parquet_reader_control_test.cpp
+++ b/be/test/format_v2/parquet/parquet_reader_control_test.cpp
@@ -27,6 +27,7 @@
 #include "core/column/column_string.h"
 #include "core/column/column_vector.h"
 #include "core/data_type/data_type_number.h"
+#include "core/data_type_serde/parquet_decode_source.h"
 #include "format_v2/parquet/parquet_column_schema.h"
 #include "format_v2/parquet/parquet_scan.h"
 #include "format_v2/parquet/reader/column_reader.h"
@@ -186,6 +187,79 @@ TEST(SelectionVectorTest, 
IdentitySelectionDoesNotMaterializeFilter) {
     EXPECT_EQ(filter, nullptr);
 }
 
+TEST(NativeNullableSelectionTest, 
BuildsPhysicalRangesAndSelectedNullsInOnePass) {
+    using native::FilterMap;
+
+    const std::vector<uint16_t> null_runs {2, 1, 3, 2, 2};
+    const std::vector<uint8_t> filter_data {1, 0, 1, 1, 0, 1, 1, 1, 0, 1};
+    FilterMap filter;
+    ASSERT_TRUE(filter.init(filter_data.data(), filter_data.size(), 
false).ok());
+    ParquetSelection selection;
+    NullMap output_nulls {1};
+    NullMap selected_nulls;
+    size_t num_filtered = 0;
+
+    ASSERT_TRUE(native::build_filtered_nullable_selection(null_runs, 
filter_data.size(), 3,
+                                                          &output_nulls, 
&filter, 0, &selection,
+                                                          &selected_nulls, 
&num_filtered)
+                        .ok());
+
+    EXPECT_EQ(selection.total_values, 7);
+    EXPECT_EQ(selection.selected_values, 4);
+    ASSERT_EQ(selection.ranges.size(), 4);
+    EXPECT_EQ(selection.ranges[0].first, 0);
+    EXPECT_EQ(selection.ranges[0].count, 1);
+    EXPECT_EQ(selection.ranges[1].first, 2);
+    EXPECT_EQ(selection.ranges[1].count, 1);
+    EXPECT_EQ(selection.ranges[2].first, 4);
+    EXPECT_EQ(selection.ranges[2].count, 1);
+    EXPECT_EQ(selection.ranges[3].first, 6);
+    EXPECT_EQ(selection.ranges[3].count, 1);
+    EXPECT_EQ(selected_nulls, (NullMap {0, 1, 0, 0, 1, 1, 0}));
+    EXPECT_EQ(output_nulls, (NullMap {1, 0, 1, 0, 0, 1, 1, 0}));
+    EXPECT_EQ(num_filtered, 3);
+}
+
+TEST(NativeNullableSelectionTest, UsesDirectPhysicalCoordinatesWithoutNulls) {
+    using native::FilterMap;
+
+    const std::vector<uint16_t> no_nulls {10};
+    const std::vector<uint8_t> filter_data {1, 1, 0, 1, 0, 0, 1, 1, 1, 0};
+    FilterMap filter;
+    ASSERT_TRUE(filter.init(filter_data.data(), filter_data.size(), 
false).ok());
+    ParquetSelection selection;
+    NullMap output_nulls;
+    NullMap selected_nulls;
+    size_t num_filtered = 0;
+
+    ASSERT_TRUE(native::build_filtered_nullable_selection(no_nulls, 
filter_data.size(), 0,
+                                                          &output_nulls, 
&filter, 0, &selection,
+                                                          &selected_nulls, 
&num_filtered)
+                        .ok());
+
+    EXPECT_EQ(selection.total_values, 10);
+    EXPECT_EQ(selection.selected_values, 6);
+    ASSERT_EQ(selection.ranges.size(), 3);
+    EXPECT_EQ(selection.ranges[0].first, 0);
+    EXPECT_EQ(selection.ranges[0].count, 2);
+    EXPECT_EQ(selection.ranges[1].first, 3);
+    EXPECT_EQ(selection.ranges[1].count, 1);
+    EXPECT_EQ(selection.ranges[2].first, 6);
+    EXPECT_EQ(selection.ranges[2].count, 3);
+    EXPECT_EQ(selected_nulls, (NullMap {0, 0, 0, 0, 0, 0}));
+    EXPECT_EQ(output_nulls, selected_nulls);
+    EXPECT_EQ(num_filtered, 4);
+}
+
+TEST(NativeNullableSelectionTest, 
EnablesFusionOnlyForMateriallyFragmentedNullableBatches) {
+    EXPECT_FALSE(native::should_use_fused_nullable_selection(65536, 0, 3));
+    EXPECT_FALSE(native::should_use_fused_nullable_selection(65536, 655, 
1311));
+    EXPECT_FALSE(native::should_use_fused_nullable_selection(65536, 32768, 3));
+    EXPECT_FALSE(native::should_use_fused_nullable_selection(512, 256, 512));
+    EXPECT_TRUE(native::should_use_fused_nullable_selection(65536, 6553, 
13107));
+    EXPECT_TRUE(native::should_use_fused_nullable_selection(65536, 32768, 
65536));
+}
+
 TEST(NativeNestedSelectionTest, 
BuildsSelectionAndCompactsSurvivingParentLevels) {
     using native::ColumnSelectVector;
     using native::FilterMap;


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

Reply via email to