Coverage Report

Created: 2026-08-06 20:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format_v2/parquet/parquet_scan.cpp
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//   http://www.apache.org/licenses/LICENSE-2.0
9
// Unless required by applicable law or agreed to in writing,
10
// software distributed under the License is distributed on an
11
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
12
// KIND, either express or implied.  See the License for the
13
// specific language governing permissions and limitations
14
// under the License.
15
16
#include "format_v2/parquet/parquet_scan.h"
17
18
#include <algorithm>
19
#include <iterator>
20
#include <limits>
21
#include <memory>
22
#include <optional>
23
#include <ranges>
24
#include <set>
25
#include <span>
26
#include <unordered_set>
27
#include <utility>
28
29
#include "common/exception.h"
30
#include "common/status.h"
31
#include "core/assert_cast.h"
32
#include "core/block/block.h"
33
#include "core/column/column_const.h"
34
#include "core/column/column_decimal.h"
35
#include "core/column/column_nullable.h"
36
#include "core/column/column_vector.h"
37
#include "core/data_type/data_type_array.h"
38
#include "core/data_type/data_type_map.h"
39
#include "core/data_type/data_type_number.h"
40
#include "core/data_type/data_type_struct.h"
41
#include "exprs/expr_zonemap_filter.h"
42
#include "exprs/vcompound_pred.h"
43
#include "exprs/vectorized_fn_call.h"
44
#include "exprs/vexpr_context.h"
45
#include "format_v2/parquet/parquet_column_schema.h"
46
#include "format_v2/parquet/parquet_file_context.h"
47
#include "format_v2/parquet/parquet_statistics.h"
48
#include "format_v2/parquet/reader/global_rowid_column_reader.h"
49
#include "format_v2/parquet/reader/native/column_chunk_reader.h"
50
#include "format_v2/parquet/reader/native_column_reader.h"
51
#include "format_v2/parquet/reader/row_position_column_reader.h"
52
#include "runtime/runtime_state.h"
53
#include "util/defer_op.h"
54
#include "util/time.h"
55
56
namespace doris::format::parquet {
57
58
namespace detail {
59
60
std::vector<size_t> order_adaptive_predicates(
61
        const std::vector<size_t>& positions,
62
349
        const std::unordered_map<size_t, AdaptivePredicateStats>& stats) {
63
349
    if (std::ranges::any_of(positions, [&](size_t position) {
64
172
            const auto it = stats.find(position);
65
172
            return it == stats.end() || it->second.samples == 0;
66
172
        })) {
67
100
        return positions;
68
100
    }
69
249
    auto ordered = positions;
70
249
    std::stable_sort(ordered.begin(), ordered.end(), [&](size_t left, size_t right) {
71
6
        const auto score = [&](size_t position) {
72
6
            const auto& sample = stats.at(position);
73
6
            return sample.cost_per_input_row_ns / std::max(1.0 - sample.survival_ratio, 0.01);
74
6
        };
75
3
        return score(left) < score(right);
76
3
    });
77
249
    return ordered;
78
349
}
79
80
std::vector<size_t> adaptive_prefetch_prefix(
81
        const std::vector<size_t>& ordered_positions,
82
        const std::unordered_map<size_t, AdaptivePredicateStats>& stats,
83
14
        double minimum_reach_probability) {
84
14
    if (std::ranges::any_of(ordered_positions, [&](size_t position) {
85
4
            const auto it = stats.find(position);
86
4
            return it == stats.end() || it->second.samples == 0;
87
4
        })) {
88
1
        return ordered_positions;
89
1
    }
90
13
    std::vector<size_t> result;
91
13
    double reach_probability = 1;
92
13
    for (const size_t position : ordered_positions) {
93
2
        if (!result.empty() && reach_probability < minimum_reach_probability) {
94
1
            break;
95
1
        }
96
1
        result.push_back(position);
97
1
        reach_probability *= stats.at(position).survival_ratio;
98
1
    }
99
13
    return result;
100
14
}
101
102
183
bool should_sample_adaptive_predicate(size_t samples, size_t batch_sequence) {
103
183
    constexpr size_t WARMUP_SAMPLES = 8;
104
183
    constexpr size_t STEADY_STATE_INTERVAL = 16;
105
183
    return samples < WARMUP_SAMPLES || batch_sequence % STEADY_STATE_INTERVAL == 0;
106
183
}
107
108
} // namespace detail
109
110
namespace {
111
112
detail::PredicateConjunctSchedule build_predicate_conjunct_schedule(
113
        const format::FileScanRequest& request);
114
115
48
bool is_dictionary_data_encoding(tparquet::Encoding::type encoding) {
116
48
    return encoding == tparquet::Encoding::PLAIN_DICTIONARY ||
117
48
           encoding == tparquet::Encoding::RLE_DICTIONARY;
118
48
}
119
120
0
bool is_level_encoding(tparquet::Encoding::type encoding) {
121
0
    return encoding == tparquet::Encoding::RLE || encoding == tparquet::Encoding::BIT_PACKED;
122
0
}
123
124
254
bool types_equal_ignoring_nested_nullability(const DataTypePtr& left, const DataTypePtr& right) {
125
254
    const auto left_type = remove_nullable(left);
126
254
    const auto right_type = remove_nullable(right);
127
254
    if (left_type->get_primitive_type() != right_type->get_primitive_type()) {
128
0
        return false;
129
0
    }
130
131
254
    switch (left_type->get_primitive_type()) {
132
0
    case TYPE_ARRAY: {
133
0
        const auto& left_array = assert_cast<const DataTypeArray&>(*left_type);
134
0
        const auto& right_array = assert_cast<const DataTypeArray&>(*right_type);
135
0
        return types_equal_ignoring_nested_nullability(left_array.get_nested_type(),
136
0
                                                       right_array.get_nested_type());
137
0
    }
138
0
    case TYPE_MAP: {
139
0
        const auto& left_map = assert_cast<const DataTypeMap&>(*left_type);
140
0
        const auto& right_map = assert_cast<const DataTypeMap&>(*right_type);
141
0
        return types_equal_ignoring_nested_nullability(left_map.get_key_type(),
142
0
                                                       right_map.get_key_type()) &&
143
0
               types_equal_ignoring_nested_nullability(left_map.get_value_type(),
144
0
                                                       right_map.get_value_type());
145
0
    }
146
8
    case TYPE_STRUCT: {
147
8
        const auto& left_struct = assert_cast<const DataTypeStruct&>(*left_type);
148
8
        const auto& right_struct = assert_cast<const DataTypeStruct&>(*right_type);
149
8
        if (left_struct.get_elements().size() != right_struct.get_elements().size()) {
150
0
            return false;
151
0
        }
152
16
        for (size_t i = 0; i < left_struct.get_elements().size(); ++i) {
153
8
            if (!types_equal_ignoring_nested_nullability(left_struct.get_element(i),
154
8
                                                         right_struct.get_element(i))) {
155
0
                return false;
156
0
            }
157
8
        }
158
8
        return true;
159
8
    }
160
246
    default:
161
246
        return left_type->equals(*right_type);
162
254
    }
163
254
}
164
165
96
bool is_data_page_type(tparquet::PageType::type page_type) {
166
96
    return page_type == tparquet::PageType::DATA_PAGE ||
167
96
           page_type == tparquet::PageType::DATA_PAGE_V2;
168
96
}
169
170
58
bool is_fully_dictionary_encoded_chunk(const tparquet::ColumnMetaData& column_metadata) {
171
58
    if (!column_metadata.__isset.dictionary_page_offset ||
172
58
        column_metadata.dictionary_page_offset < 0) {
173
10
        return false;
174
10
    }
175
176
48
    const auto& encoding_stats = column_metadata.encoding_stats;
177
48
    if (!encoding_stats.empty()) {
178
48
        bool has_dictionary_data_page = false;
179
96
        for (const auto& encoding_stat : encoding_stats) {
180
96
            if (!is_data_page_type(encoding_stat.page_type) || encoding_stat.count <= 0) {
181
48
                continue;
182
48
            }
183
48
            if (!is_dictionary_data_encoding(encoding_stat.encoding)) {
184
0
                return false;
185
0
            }
186
48
            has_dictionary_data_page = true;
187
48
        }
188
48
        return has_dictionary_data_page;
189
48
    }
190
191
0
    bool has_dictionary_encoding = false;
192
0
    for (const auto encoding : column_metadata.encodings) {
193
0
        if (is_dictionary_data_encoding(encoding)) {
194
0
            has_dictionary_encoding = true;
195
0
            continue;
196
0
        }
197
0
        if (!is_level_encoding(encoding)) {
198
0
            return false;
199
0
        }
200
0
    }
201
0
    return has_dictionary_encoding;
202
0
}
203
204
bool supports_row_level_dictionary_filter(const ParquetColumnSchema& column_schema,
205
58
                                          const tparquet::ColumnMetaData& column_metadata) {
206
58
    if (column_schema.kind != ParquetColumnSchemaKind::PRIMITIVE || column_schema.type == nullptr ||
207
58
        column_schema.max_repetition_level > 0) {
208
0
        return false;
209
0
    }
210
58
    bool is_supported_physical_type = false;
211
58
    switch (column_metadata.type) {
212
26
    case tparquet::Type::BYTE_ARRAY:
213
26
        is_supported_physical_type = column_schema.type_descriptor.is_string_like;
214
26
        break;
215
26
    case tparquet::Type::INT32:
216
28
    case tparquet::Type::INT64:
217
29
    case tparquet::Type::INT96:
218
30
    case tparquet::Type::FLOAT:
219
31
    case tparquet::Type::DOUBLE:
220
32
    case tparquet::Type::FIXED_LEN_BYTE_ARRAY:
221
32
        is_supported_physical_type = true;
222
32
        break;
223
0
    case tparquet::Type::BOOLEAN:
224
        // Parquet booleans are PLAIN encoded and cannot have a dictionary page.
225
0
        break;
226
58
    }
227
58
    if (!is_supported_physical_type) {
228
0
        return false;
229
0
    }
230
58
    if (remove_nullable(column_schema.type)->get_primitive_type() == TYPE_VARBINARY) {
231
        // A table STRING predicate can be rewritten through a raw VARBINARY file slot. Evaluating
232
        // it on dictionary Fields before the mapping expression is neither type-safe nor exact.
233
0
        return false;
234
0
    }
235
    // The row filter consumes dictionary ids rather than decoded values, so a plain data page
236
    // cannot resume this reader without changing its output domain. Keep mixed chunks on the
237
    // normal decoded-value path to preserve one representation for the complete column chunk.
238
58
    return is_fully_dictionary_encoded_chunk(column_metadata);
239
58
}
240
241
void collect_all_leaf_column_ids(const ParquetColumnSchema& column_schema,
242
1.30k
                                 std::unordered_set<int>* leaf_column_ids) {
243
1.30k
    DORIS_CHECK(leaf_column_ids != nullptr);
244
1.30k
    if (column_schema.kind == ParquetColumnSchemaKind::PRIMITIVE) {
245
1.19k
        if (column_schema.leaf_column_id >= 0) {
246
1.19k
            leaf_column_ids->insert(column_schema.leaf_column_id);
247
1.19k
        }
248
1.19k
        return;
249
1.19k
    }
250
139
    for (const auto& child : column_schema.children) {
251
139
        DORIS_CHECK(child != nullptr);
252
139
        collect_all_leaf_column_ids(*child, leaf_column_ids);
253
139
    }
254
108
}
255
256
void collect_projected_leaf_column_ids(const ParquetColumnSchema& column_schema,
257
                                       const format::LocalColumnIndex& projection,
258
1.19k
                                       std::unordered_set<int>* leaf_column_ids) {
259
1.19k
    DORIS_CHECK(leaf_column_ids != nullptr);
260
1.19k
    if (projection.project_all_children || projection.children.empty()) {
261
1.16k
        collect_all_leaf_column_ids(column_schema, leaf_column_ids);
262
1.16k
        return;
263
1.16k
    }
264
28
    for (const auto& child_projection : projection.children) {
265
28
        const auto child_it =
266
46
                std::ranges::find_if(column_schema.children, [&](const auto& child_schema) {
267
46
                    return child_schema->local_id == child_projection.local_id();
268
46
                });
269
28
        DORIS_CHECK(child_it != column_schema.children.end());
270
28
        collect_projected_leaf_column_ids(**child_it, child_projection, leaf_column_ids);
271
28
    }
272
26
}
273
274
611
std::vector<format::LocalColumnIndex> request_scan_columns(const format::FileScanRequest& request) {
275
611
    std::vector<format::LocalColumnIndex> scan_columns;
276
611
    scan_columns.reserve(request.predicate_columns.size() + request.non_predicate_columns.size());
277
611
    scan_columns.insert(scan_columns.end(), request.predicate_columns.begin(),
278
611
                        request.predicate_columns.end());
279
882
    for (const auto& column : request.non_predicate_columns) {
280
882
        if (!request.is_count_star_placeholder(column.column_id())) {
281
874
            scan_columns.push_back(column);
282
874
        }
283
882
    }
284
611
    return scan_columns;
285
611
}
286
287
std::vector<format::LocalColumnIndex> physical_non_predicate_columns(
288
12
        const format::FileScanRequest& request) {
289
12
    std::vector<format::LocalColumnIndex> columns;
290
12
    columns.reserve(request.non_predicate_columns.size());
291
12
    for (const auto& column : request.non_predicate_columns) {
292
0
        if (!request.is_count_star_placeholder(column.column_id())) {
293
0
            columns.push_back(column);
294
0
        }
295
0
    }
296
12
    return columns;
297
12
}
298
299
void materialize_count_star_placeholders(const format::FileScanRequest& request, size_t rows,
300
317
                                         Block* file_block) {
301
317
    DORIS_CHECK(file_block != nullptr);
302
480
    for (const auto& column : request.non_predicate_columns) {
303
480
        if (!request.is_count_star_placeholder(column.column_id())) {
304
479
            continue;
305
479
        }
306
1
        const auto block_position = request.local_positions.at(column.column_id()).value();
307
1
        auto placeholder = file_block->get_by_position(block_position).column->assert_mutable();
308
1
        DCHECK(placeholder->empty());
309
1
        placeholder->insert_many_defaults(rows);
310
1
        file_block->replace_by_position(block_position, std::move(placeholder));
311
1
    }
312
317
}
313
314
} // namespace
315
316
namespace detail {
317
318
Status build_native_prefetch_ranges(
319
        const tparquet::FileMetaData& metadata,
320
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
321
        const std::vector<format::LocalColumnIndex>& scan_columns, int row_group_idx,
322
291
        size_t file_size, bool parquet_816_padding, std::vector<ParquetPageCacheRange>* ranges) {
323
291
    DORIS_CHECK(ranges != nullptr);
324
291
    ranges->clear();
325
291
    std::unordered_set<int> leaf_column_ids;
326
626
    for (const auto& projection : scan_columns) {
327
626
        const auto local_id = projection.local_id();
328
626
        if (local_id == format::ROW_POSITION_COLUMN_ID ||
329
626
            local_id == format::GLOBAL_ROWID_COLUMN_ID) {
330
48
            continue;
331
48
        }
332
578
        if (local_id < 0 || local_id >= static_cast<int32_t>(file_schema.size()) ||
333
578
            file_schema[local_id] == nullptr) {
334
0
            return Status::Corruption("Invalid Parquet projected column id {}", local_id);
335
0
        }
336
        // Prefetch and merge-reader ranges must be physical leaf chunks, not Doris logical slots.
337
        // Example: for a struct column s<a:int,b:string>, projecting only s.a should include only
338
        // the Parquet leaf chunk of a. Projecting the whole struct includes both a and b.
339
578
        collect_projected_leaf_column_ids(*file_schema[local_id], projection, &leaf_column_ids);
340
578
    }
341
342
291
    if (row_group_idx < 0 || row_group_idx >= static_cast<int>(metadata.row_groups.size())) {
343
0
        return Status::Corruption("Invalid Parquet row group index {}", row_group_idx);
344
0
    }
345
291
    const auto& row_group_metadata = metadata.row_groups[row_group_idx];
346
291
    std::vector<int> ordered_leaf_column_ids(leaf_column_ids.begin(), leaf_column_ids.end());
347
291
    std::ranges::sort(ordered_leaf_column_ids);
348
349
291
    ranges->reserve(ordered_leaf_column_ids.size());
350
594
    for (const auto leaf_column_id : ordered_leaf_column_ids) {
351
594
        if (leaf_column_id < 0 ||
352
594
            leaf_column_id >= static_cast<int>(row_group_metadata.columns.size())) {
353
0
            return Status::Corruption("Invalid Parquet leaf column id {}", leaf_column_id);
354
0
        }
355
594
        const auto& chunk = row_group_metadata.columns[leaf_column_id];
356
594
        if (!chunk.__isset.meta_data) {
357
0
            return Status::Corruption("Parquet leaf column {} has no chunk metadata",
358
0
                                      leaf_column_id);
359
0
        }
360
594
        native::ColumnChunkRange chunk_range;
361
594
        RETURN_IF_ERROR(native::compute_column_chunk_range(chunk.meta_data, file_size,
362
594
                                                           parquet_816_padding, &chunk_range));
363
593
        if (chunk_range.length > 0) {
364
593
            if (chunk_range.offset > static_cast<size_t>(std::numeric_limits<int64_t>::max()) ||
365
593
                chunk_range.length > static_cast<size_t>(std::numeric_limits<int64_t>::max())) {
366
0
                return Status::Corruption("Parquet column chunk range exceeds int64 coordinates");
367
0
            }
368
            // Prefetch must use the same checked chunk extent as the decoder, including the
369
            // PARQUET-816 compatibility padding, so warm-up cannot target different bytes.
370
593
            ranges->push_back(
371
593
                    ParquetPageCacheRange {.offset = static_cast<int64_t>(chunk_range.offset),
372
593
                                           .size = static_cast<int64_t>(chunk_range.length)});
373
593
        }
374
593
    }
375
290
    return Status::OK();
376
291
}
377
378
} // namespace detail
379
380
namespace detail {
381
382
Status select_native_row_groups_by_scan_range(const tparquet::FileMetaData& metadata,
383
                                              const ParquetScanRange& scan_range,
384
                                              std::vector<int64_t>* row_group_first_rows,
385
297
                                              std::vector<int>* selected_row_groups) {
386
297
    DORIS_CHECK(row_group_first_rows != nullptr && selected_row_groups != nullptr);
387
297
    if (scan_range.start_offset < 0 || scan_range.size < -1 ||
388
297
        (scan_range.size >= 0 &&
389
297
         scan_range.start_offset > std::numeric_limits<int64_t>::max() - scan_range.size)) {
390
0
        return Status::Corruption("Invalid Parquet scan range [{}, {})", scan_range.start_offset,
391
0
                                  scan_range.size);
392
0
    }
393
297
    const uint64_t range_start = static_cast<uint64_t>(scan_range.start_offset);
394
297
    const uint64_t range_end = scan_range.size < 0
395
297
                                       ? std::numeric_limits<uint64_t>::max()
396
297
                                       : range_start + static_cast<uint64_t>(scan_range.size);
397
297
    const size_t file_size = scan_range.file_size < 0 ? std::numeric_limits<size_t>::max()
398
297
                                                      : static_cast<size_t>(scan_range.file_size);
399
297
    const bool full_file_range =
400
297
            scan_range.size < 0 || (range_start == 0 && scan_range.file_size >= 0 &&
401
9
                                    range_end >= static_cast<uint64_t>(scan_range.file_size));
402
297
    const auto compat = native::parquet_reader_compat(
403
297
            metadata.__isset.created_by ? metadata.created_by : std::string {});
404
297
    row_group_first_rows->assign(metadata.row_groups.size(), 0);
405
297
    selected_row_groups->clear();
406
297
    selected_row_groups->reserve(metadata.row_groups.size());
407
297
    int64_t next_first_row = 0;
408
682
    for (size_t row_group_idx = 0; row_group_idx < metadata.row_groups.size(); ++row_group_idx) {
409
385
        (*row_group_first_rows)[row_group_idx] = next_first_row;
410
385
        const auto& row_group = metadata.row_groups[row_group_idx];
411
385
        if (row_group.num_rows < 0) {
412
0
            return Status::Corruption("Invalid negative row count in parquet row group {}",
413
0
                                      row_group_idx);
414
0
        }
415
385
        if (row_group.num_rows > std::numeric_limits<int64_t>::max() - next_first_row) {
416
0
            return Status::Corruption("Parquet row counts overflow at row group {}", row_group_idx);
417
0
        }
418
385
        next_first_row += row_group.num_rows;
419
385
        bool selected = full_file_range;
420
385
        if (!full_file_range) {
421
23
            if (row_group.columns.empty()) {
422
0
                return Status::Corruption("Parquet row group {} has no column chunks",
423
0
                                          row_group_idx);
424
0
            }
425
23
            size_t group_start = std::numeric_limits<size_t>::max();
426
23
            size_t group_end = 0;
427
76
            for (size_t column_idx = 0; column_idx < row_group.columns.size(); ++column_idx) {
428
53
                const auto& chunk = row_group.columns[column_idx];
429
53
                if (!chunk.__isset.meta_data) {
430
0
                    return Status::Corruption("Parquet row group {} column {} has no metadata",
431
0
                                              row_group_idx, column_idx);
432
0
                }
433
53
                native::ColumnChunkRange chunk_range;
434
53
                RETURN_IF_ERROR(native::compute_column_chunk_range(
435
53
                        chunk.meta_data, file_size, compat.parquet_816_padding, &chunk_range));
436
53
                group_start = std::min(group_start, chunk_range.offset);
437
53
                group_end = std::max(group_end, chunk_range.offset + chunk_range.length);
438
53
            }
439
            // Checked chunk ranges make end >= start; this midpoint form cannot overflow even
440
            // when footer offsets are close to the host coordinate limit.
441
23
            const uint64_t group_mid =
442
23
                    static_cast<uint64_t>(group_start) + (group_end - group_start) / 2;
443
23
            selected = group_mid >= range_start && group_mid < range_end;
444
23
        }
445
385
        if (selected) {
446
370
            selected_row_groups->push_back(cast_set<int>(row_group_idx));
447
370
        }
448
385
    }
449
297
    return Status::OK();
450
297
}
451
452
} // namespace detail
453
454
namespace {
455
456
std::vector<RowRange> intersect_row_ranges(const std::vector<RowRange>& left,
457
323
                                           const std::vector<RowRange>& right) {
458
323
    std::vector<RowRange> result;
459
323
    size_t left_idx = 0;
460
323
    size_t right_idx = 0;
461
646
    while (left_idx < left.size() && right_idx < right.size()) {
462
323
        const int64_t left_end = left[left_idx].start + left[left_idx].length;
463
323
        const int64_t right_end = right[right_idx].start + right[right_idx].length;
464
323
        const int64_t start = std::max(left[left_idx].start, right[right_idx].start);
465
323
        const int64_t end = std::min(left_end, right_end);
466
323
        if (start < end) {
467
323
            result.push_back({.start = start, .length = end - start});
468
323
        }
469
323
        if (left_end < right_end) {
470
0
            ++left_idx;
471
323
        } else {
472
323
            ++right_idx;
473
323
        }
474
323
    }
475
323
    return result;
476
323
}
477
478
Status finalize_native_row_group_read_plan(
479
        const NativeParquetMetadata& metadata,
480
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
481
        const format::FileScanRequest& request, bool enable_bloom_filter,
482
        RowGroupReadPlan* row_group_plan, ParquetPruningStats* pruning_stats,
483
        const cctz::time_zone* timezone, const RuntimeState* runtime_state,
484
        ParquetFileContext* file_context, const ParquetColumnReaderProfile& column_reader_profile,
485
355
        bool* selected) {
486
355
    DORIS_CHECK(row_group_plan != nullptr && pruning_stats != nullptr && file_context != nullptr &&
487
355
                selected != nullptr);
488
355
    *selected = true;
489
355
    if (!row_group_plan->expensive_pruning_pending) {
490
10
        return Status::OK();
491
10
    }
492
345
    row_group_plan->expensive_pruning_pending = false;
493
345
    const auto& thrift = metadata.to_thrift();
494
345
    const std::vector<int> candidate {row_group_plan->row_group_id};
495
345
    std::vector<int> metadata_selected;
496
345
    RETURN_IF_ERROR(select_row_groups_by_metadata(
497
345
            thrift, file_schema, request, &candidate, &metadata_selected, enable_bloom_filter,
498
345
            pruning_stats, timezone, runtime_state, file_context, column_reader_profile,
499
345
            ParquetMetadataProbeMode::EXPENSIVE_ONLY));
500
345
    if (metadata_selected.empty()) {
501
22
        *selected = false;
502
22
        return Status::OK();
503
22
    }
504
505
323
    std::unordered_set<int> requested_leaf_ids;
506
633
    for (const auto& projection : request_scan_columns(request)) {
507
633
        const auto local_id = projection.local_id();
508
633
        if (local_id < 0 || local_id >= static_cast<int32_t>(file_schema.size())) {
509
47
            continue;
510
47
        }
511
586
        collect_projected_leaf_column_ids(*file_schema[local_id], projection, &requested_leaf_ids);
512
586
    }
513
323
    std::unordered_map<int, NativeParquetPageIndex> page_indexes;
514
323
    if (can_use_parquet_page_index(request, runtime_state)) {
515
75
        RETURN_IF_ERROR(file_context->load_native_page_indexes(
516
75
                row_group_plan->row_group_id, requested_leaf_ids, &page_indexes,
517
75
                &pruning_stats->read_page_index_time, &pruning_stats->parse_page_index_time));
518
75
    }
519
323
    std::vector<RowRange> page_selected_ranges;
520
323
    std::map<int, ParquetPageSkipPlan> page_skip_plans;
521
323
    RETURN_IF_ERROR(select_row_group_ranges_by_native_page_index(
522
323
            thrift, page_indexes, file_schema, request, row_group_plan->row_group_rows,
523
323
            &page_selected_ranges, &page_skip_plans, pruning_stats, timezone, runtime_state));
524
323
    row_group_plan->selected_ranges =
525
323
            intersect_row_ranges(row_group_plan->selected_ranges, page_selected_ranges);
526
323
    row_group_plan->page_skip_plans = std::move(page_skip_plans);
527
323
    for (auto& [leaf_column_id, indexes] : page_indexes) {
528
59
        row_group_plan->offset_indexes.emplace(leaf_column_id, std::move(indexes.offset_index));
529
59
    }
530
323
    if (row_group_plan->selected_ranges.empty()) {
531
0
        *selected = false;
532
0
        return Status::OK();
533
0
    }
534
323
    pruning_stats->selected_row_ranges += row_group_plan->selected_ranges.size();
535
323
    return Status::OK();
536
323
}
537
538
Status build_native_row_group_read_plans(
539
        const NativeParquetMetadata& metadata,
540
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
541
        const format::FileScanRequest& request, const std::vector<int>& selected_row_groups,
542
        const std::vector<int64_t>& row_group_first_rows, RowGroupScanPlan* plan,
543
        const cctz::time_zone* timezone, const RuntimeState* runtime_state,
544
295
        ParquetFileContext* file_context) {
545
295
    DORIS_CHECK(plan != nullptr && file_context != nullptr);
546
295
    const auto& thrift = metadata.to_thrift();
547
295
    plan->row_groups.reserve(selected_row_groups.size());
548
351
    for (const int row_group_idx : selected_row_groups) {
549
351
        const auto& row_group = thrift.row_groups[row_group_idx];
550
351
        if (row_group.num_rows == 0) {
551
0
            continue;
552
0
        }
553
351
        RowGroupReadPlan row_group_plan;
554
351
        row_group_plan.row_group_id = row_group_idx;
555
351
        row_group_plan.first_file_row = row_group_first_rows[row_group_idx];
556
351
        row_group_plan.row_group_rows = row_group.num_rows;
557
351
        row_group_plan.selected_ranges = {{.start = 0, .length = row_group.num_rows}};
558
351
        row_group_plan.expensive_pruning_pending = true;
559
351
        plan->row_groups.push_back(std::move(row_group_plan));
560
351
    }
561
295
    return Status::OK();
562
295
}
563
564
} // namespace
565
566
Status plan_parquet_row_groups(const NativeParquetMetadata& metadata,
567
                               const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
568
                               const format::FileScanRequest& request,
569
                               const ParquetScanRange& scan_range, bool enable_bloom_filter,
570
                               RowGroupScanPlan* plan, const cctz::time_zone* timezone,
571
                               const RuntimeState* runtime_state, ParquetFileContext* file_context,
572
295
                               const ParquetColumnReaderProfile& column_reader_profile) {
573
295
    DORIS_CHECK(plan != nullptr && file_context != nullptr);
574
295
    plan->row_groups.clear();
575
295
    plan->pruning_stats = {};
576
295
    plan->enable_bloom_filter = enable_bloom_filter;
577
295
    std::vector<int64_t> row_group_first_rows;
578
295
    std::vector<int> scan_range_selected;
579
295
    RETURN_IF_ERROR(detail::select_native_row_groups_by_scan_range(
580
295
            metadata.to_thrift(), scan_range, &row_group_first_rows, &scan_range_selected));
581
295
    std::vector<int> metadata_selected;
582
295
    RETURN_IF_ERROR(select_row_groups_by_metadata(
583
295
            metadata.to_thrift(), file_schema, request, &scan_range_selected, &metadata_selected,
584
295
            enable_bloom_filter, &plan->pruning_stats, timezone, runtime_state, file_context,
585
295
            column_reader_profile, ParquetMetadataProbeMode::FOOTER_ONLY));
586
295
    RETURN_IF_ERROR(build_native_row_group_read_plans(metadata, file_schema, request,
587
295
                                                      metadata_selected, row_group_first_rows, plan,
588
295
                                                      timezone, runtime_state, file_context));
589
295
    plan->pruning_stats.selected_row_groups = plan->row_groups.size();
590
295
    return Status::OK();
591
295
}
592
593
Status finalize_parquet_row_group_plans(
594
        const NativeParquetMetadata& metadata,
595
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
596
        const format::FileScanRequest& request, bool enable_bloom_filter, RowGroupScanPlan* plan,
597
        const cctz::time_zone* timezone, const RuntimeState* runtime_state,
598
        ParquetFileContext* file_context, const ParquetColumnReaderProfile& column_reader_profile,
599
28
        const ParquetProfile* parquet_profile) {
600
28
    DORIS_CHECK(plan != nullptr && file_context != nullptr);
601
28
    std::vector<RowGroupReadPlan> selected_plans;
602
28
    selected_plans.reserve(plan->row_groups.size());
603
45
    for (auto& row_group_plan : plan->row_groups) {
604
45
        ParquetPruningStats deferred_stats;
605
45
        bool selected = false;
606
45
        RETURN_IF_ERROR(finalize_native_row_group_read_plan(
607
45
                metadata, file_schema, request, enable_bloom_filter, &row_group_plan,
608
45
                &deferred_stats, timezone, runtime_state, file_context, column_reader_profile,
609
45
                &selected));
610
45
        if (parquet_profile != nullptr) {
611
5
            parquet_profile->update_deferred_pruning_stats(deferred_stats, selected);
612
5
        }
613
45
        if (selected) {
614
45
            selected_plans.push_back(std::move(row_group_plan));
615
45
        }
616
45
    }
617
28
    plan->row_groups = std::move(selected_plans);
618
28
    plan->pruning_stats.selected_row_groups = plan->row_groups.size();
619
28
    return Status::OK();
620
28
}
621
622
namespace {
623
624
using OwnedExpressionConjunct = std::pair<VExprContextSPtr, VExprSPtr>;
625
using OwnedExpressionConjuncts = std::vector<OwnedExpressionConjunct>;
626
627
1.37k
void update_counter_if_not_null(RuntimeProfile::Counter* counter, int64_t value) {
628
1.37k
    if (counter != nullptr) {
629
1.03k
        COUNTER_UPDATE(counter, value);
630
1.03k
    }
631
1.37k
}
632
633
uint16_t apply_filter_to_selection(const IColumn::Filter& filter, SelectionVector* selection,
634
6
                                   uint16_t selected_rows) {
635
6
    return cast_set<uint16_t>(selection->compact_with_row_filter(filter.data(), selected_rows));
636
6
}
637
638
Status execute_compact_filter_conjuncts(const VExprContextSPtrs& conjuncts, size_t rows,
639
                                        Block* file_block, IColumn::Filter* compact_filter,
640
61
                                        bool* can_filter_all) {
641
61
    DORIS_CHECK(compact_filter != nullptr);
642
61
    DORIS_CHECK(can_filter_all != nullptr);
643
61
    compact_filter->resize_fill(rows, 1);
644
61
    *can_filter_all = false;
645
61
    for (const auto& conjunct : conjuncts) {
646
61
        DORIS_CHECK(conjunct != nullptr);
647
61
        IColumn::Filter filter(rows, 1);
648
61
        bool conjunct_can_filter_all = false;
649
61
        RETURN_IF_ERROR(conjunct->execute_filter(file_block, filter.data(), rows, false,
650
61
                                                 &conjunct_can_filter_all));
651
61
        if (conjunct_can_filter_all) {
652
30
            std::ranges::fill(*compact_filter, 0);
653
30
            *can_filter_all = true;
654
30
            break;
655
30
        }
656
9.93k
        for (size_t row = 0; row < rows; ++row) {
657
9.90k
            (*compact_filter)[row] &= filter[row];
658
9.90k
        }
659
31
    }
660
61
    return Status::OK();
661
61
}
662
663
Status execute_compact_owned_conjuncts(std::span<const OwnedExpressionConjunct> conjuncts,
664
                                       size_t rows, Block* file_block,
665
14
                                       IColumn::Filter* compact_filter, bool* can_filter_all) {
666
14
    DORIS_CHECK(compact_filter != nullptr);
667
14
    DORIS_CHECK(can_filter_all != nullptr);
668
14
    compact_filter->resize_fill(rows, 1);
669
14
    *can_filter_all = false;
670
14
    for (const auto& [owner_context, residual_expr] : conjuncts) {
671
14
        DORIS_CHECK(owner_context != nullptr);
672
14
        DORIS_CHECK(residual_expr != nullptr);
673
14
        IColumn::Filter filter(rows, 1);
674
14
        bool conjunct_can_filter_all = false;
675
14
        RETURN_IF_ERROR(residual_expr->execute_filter(owner_context.get(), file_block,
676
14
                                                      filter.data(), rows, false,
677
14
                                                      &conjunct_can_filter_all));
678
14
        if (conjunct_can_filter_all) {
679
2
            std::ranges::fill(*compact_filter, 0);
680
2
            *can_filter_all = true;
681
2
            break;
682
2
        }
683
62
        for (size_t row = 0; row < rows; ++row) {
684
50
            (*compact_filter)[row] &= filter[row];
685
50
        }
686
12
    }
687
14
    return Status::OK();
688
14
}
689
690
Status execute_compact_delete_conjuncts(const VExprContextSPtrs& delete_conjuncts, size_t rows,
691
                                        Block* file_block, IColumn::Filter* compact_filter,
692
48
                                        bool* can_filter_all) {
693
48
    DORIS_CHECK(compact_filter != nullptr);
694
48
    DORIS_CHECK(can_filter_all != nullptr);
695
48
    compact_filter->resize_fill(rows, 1);
696
48
    *can_filter_all = false;
697
48
    for (const auto& delete_conjunct : delete_conjuncts) {
698
48
        DORIS_CHECK(delete_conjunct != nullptr);
699
48
        const size_t original_columns = file_block->columns();
700
48
        int result_column_id = -1;
701
48
        RETURN_IF_ERROR(delete_conjunct->root()->execute(delete_conjunct.get(), file_block,
702
48
                                                         &result_column_id));
703
48
        RETURN_IF_ERROR(detail::validate_ephemeral_expr_result_column(
704
48
                original_columns, result_column_id, file_block->columns()));
705
48
        const auto& delete_filter = assert_cast<const ColumnUInt8&>(
706
48
                                            *file_block->get_by_position(result_column_id).column)
707
48
                                            .get_data();
708
48
        DORIS_CHECK(delete_filter.size() == rows);
709
48
        bool has_kept_row = false;
710
204
        for (size_t row = 0; row < rows; ++row) {
711
156
            (*compact_filter)[row] &= !delete_filter[row];
712
156
            has_kept_row |= (*compact_filter)[row] != 0;
713
156
        }
714
48
        file_block->erase(result_column_id);
715
48
        if (!has_kept_row) {
716
9
            *can_filter_all = true;
717
9
            break;
718
9
        }
719
48
    }
720
48
    return Status::OK();
721
48
}
722
723
Status execute_filter_conjuncts(const format::FileScanRequest& request, int64_t batch_rows,
724
                                Block* file_block, SelectionVector* selection,
725
5
                                uint16_t* selected_rows) {
726
7
    for (const auto& conjunct : request.conjuncts) {
727
7
        if (*selected_rows == 0) {
728
0
            break;
729
0
        }
730
7
        DORIS_CHECK(conjunct != nullptr);
731
7
        IColumn::Filter filter(static_cast<size_t>(batch_rows), 1);
732
7
        bool can_filter_all = false;
733
7
        RETURN_IF_ERROR(conjunct->execute_filter(file_block, filter.data(),
734
7
                                                 static_cast<size_t>(batch_rows), false,
735
7
                                                 &can_filter_all));
736
6
        *selected_rows =
737
6
                can_filter_all ? 0 : apply_filter_to_selection(filter, selection, *selected_rows);
738
6
    }
739
4
    return Status::OK();
740
5
}
741
742
Status execute_delete_conjuncts(const format::FileScanRequest& request, int64_t batch_rows,
743
                                Block* file_block, SelectionVector* selection,
744
4
                                uint16_t* selected_rows) {
745
4
    for (const auto& delete_conjunct : request.delete_conjuncts) {
746
0
        if (*selected_rows == 0) {
747
0
            break;
748
0
        }
749
0
        DORIS_CHECK(delete_conjunct != nullptr);
750
0
        const size_t original_columns = file_block->columns();
751
0
        int result_column_id = -1;
752
0
        RETURN_IF_ERROR(delete_conjunct->root()->execute(delete_conjunct.get(), file_block,
753
0
                                                         &result_column_id));
754
0
        RETURN_IF_ERROR(detail::validate_ephemeral_expr_result_column(
755
0
                original_columns, result_column_id, file_block->columns()));
756
0
        const auto& delete_filter = assert_cast<const ColumnUInt8&>(
757
0
                                            *file_block->get_by_position(result_column_id).column)
758
0
                                            .get_data();
759
0
        DORIS_CHECK(delete_filter.size() == static_cast<size_t>(batch_rows));
760
0
        IColumn::Filter keep_filter(static_cast<size_t>(batch_rows), 1);
761
0
        bool has_kept_row = false;
762
0
        for (size_t row = 0; row < static_cast<size_t>(batch_rows); ++row) {
763
0
            keep_filter[row] = !delete_filter[row];
764
0
            has_kept_row |= keep_filter[row] != 0;
765
0
        }
766
0
        file_block->erase(result_column_id);
767
0
        *selected_rows =
768
0
                !has_kept_row ? 0
769
0
                              : apply_filter_to_selection(keep_filter, selection, *selected_rows);
770
0
    }
771
4
    return Status::OK();
772
4
}
773
774
} // namespace
775
776
Status detail::validate_ephemeral_expr_result_column(size_t original_columns, int result_column_id,
777
51
                                                     size_t current_columns) {
778
    // Delete predicates may erase only a temporary expression result. A bare SlotRef returns an
779
    // input column id, which must remain in the block for later predicates and materialization.
780
51
    if (UNLIKELY(result_column_id < 0 || static_cast<size_t>(result_column_id) < original_columns ||
781
51
                 static_cast<size_t>(result_column_id) >= current_columns)) {
782
2
        return Status::InternalError(
783
2
                "Delete conjunct result column {} is not ephemeral (original={}, current={})",
784
2
                result_column_id, original_columns, current_columns);
785
2
    }
786
49
    return Status::OK();
787
51
}
788
789
uint16_t apply_compact_filter_to_selection(const IColumn::Filter& filter,
790
111
                                           SelectionVector* selection, uint16_t selected_rows) {
791
111
    DORIS_CHECK(selection != nullptr);
792
111
    DORIS_CHECK(filter.size() == selected_rows);
793
111
    return cast_set<uint16_t>(
794
111
            selection->compact_with_selection_filter(filter.data(), selected_rows));
795
111
}
796
797
IColumn::Filter selection_to_filter(const SelectionVector& selection, uint16_t selected_rows,
798
4
                                    int64_t batch_rows) {
799
4
    IColumn::Filter filter(static_cast<size_t>(batch_rows), 0);
800
13
    for (uint16_t selection_idx = 0; selection_idx < selected_rows; ++selection_idx) {
801
9
        filter[selection.get_index(selection_idx)] = 1;
802
9
    }
803
4
    return filter;
804
4
}
805
806
Status execute_batch_filters(const format::FileScanRequest& request, int64_t batch_rows,
807
                             Block* file_block, SelectionVector* selection, uint16_t* selected_rows,
808
5
                             int64_t* conjunct_filtered_rows) {
809
5
    if (request.conjuncts.empty() && request.delete_conjuncts.empty()) {
810
0
        return Status::OK();
811
0
    }
812
5
    const auto selected_rows_before_conjunct = *selected_rows;
813
5
    RETURN_IF_ERROR(
814
5
            execute_filter_conjuncts(request, batch_rows, file_block, selection, selected_rows));
815
4
    if (conjunct_filtered_rows != nullptr) {
816
4
        *conjunct_filtered_rows += static_cast<int64_t>(selected_rows_before_conjunct) -
817
4
                                   static_cast<int64_t>(*selected_rows);
818
4
    }
819
4
    if (*selected_rows == 0) {
820
0
        return Status::OK();
821
0
    }
822
4
    return execute_delete_conjuncts(request, batch_rows, file_block, selection, selected_rows);
823
4
}
824
825
namespace {
826
6
int64_t count_range_rows(const std::vector<RowRange>& ranges) {
827
6
    int64_t rows = 0;
828
6
    for (const auto& range : ranges) {
829
6
        rows += range.length;
830
6
    }
831
6
    return rows;
832
6
}
833
834
void append_intersection(const RowRange& left, const RowRange& right,
835
4
                         std::vector<RowRange>& result) {
836
4
    const int64_t start = std::max(left.start, right.start);
837
4
    const int64_t end = std::min(left.start + left.length, right.start + right.length);
838
4
    if (start < end) {
839
        // Cache granules are only filter coordinates. Merge adjacent survivors so cache hits
840
        // preserve the original read-range batch boundaries, matching V1 RowRanges semantics.
841
4
        if (!result.empty() && result.back().start + result.back().length == start) {
842
1
            result.back().length = end - result.back().start;
843
1
            return;
844
1
        }
845
3
        result.push_back(RowRange {.start = start, .length = end - start});
846
3
    }
847
4
}
848
849
std::vector<RowRange> filter_ranges_by_condition_cache(const std::vector<RowRange>& ranges,
850
                                                       const std::vector<bool>& cache,
851
                                                       int64_t row_group_first_row,
852
3
                                                       int64_t base_granule) {
853
3
    std::vector<RowRange> result;
854
3
    if (cache.empty()) {
855
0
        return ranges;
856
0
    }
857
858
    // Cache coordinates are file-global granules; RowRange coordinates are row-group-relative.
859
    // Walk every selected range in order and split it by granule. Granules covered by the bitmap
860
    // are kept only when the bit is true. Granules outside the bitmap are kept conservatively, so
861
    // an undersized or old-format cache entry cannot skip valid rows.
862
3
    for (const auto& range : ranges) {
863
3
        const int64_t global_start = row_group_first_row + range.start;
864
3
        const int64_t global_end = global_start + range.length;
865
3
        for (int64_t granule = global_start / ConditionCacheContext::GRANULE_SIZE;
866
8
             granule <= (global_end - 1) / ConditionCacheContext::GRANULE_SIZE; ++granule) {
867
5
            const int64_t cache_idx = granule - base_granule;
868
5
            const bool keep = cache_idx < 0 || static_cast<size_t>(cache_idx) >= cache.size() ||
869
5
                              cache[static_cast<size_t>(cache_idx)];
870
5
            if (!keep) {
871
1
                continue;
872
1
            }
873
4
            const int64_t granule_start = granule * ConditionCacheContext::GRANULE_SIZE;
874
4
            const int64_t granule_end = granule_start + ConditionCacheContext::GRANULE_SIZE;
875
4
            const RowRange file_granule_range {.start = granule_start - row_group_first_row,
876
4
                                               .length = granule_end - granule_start};
877
4
            append_intersection(range, file_granule_range, result);
878
4
        }
879
3
    }
880
3
    return result;
881
3
}
882
883
} // namespace
884
885
296
void ParquetScanScheduler::set_plan(RowGroupScanPlan plan) {
886
296
    _enable_bloom_filter = plan.enable_bloom_filter;
887
296
    _row_group_plans = std::move(plan.row_groups);
888
296
    _condition_cache_filtered_rows = 0;
889
296
    _predicate_filtered_rows = 0;
890
296
    _remaining_plans_need_replanning = false;
891
296
    reset();
892
296
}
893
894
4
void ParquetScanScheduler::set_condition_cache_context(std::shared_ptr<ConditionCacheContext> ctx) {
895
4
    _condition_cache_ctx = std::move(ctx);
896
4
    if (!_condition_cache_ctx || !_condition_cache_ctx->filter_result || _row_group_plans.empty()) {
897
0
        return;
898
0
    }
899
900
4
    if (!_condition_cache_ctx->is_hit) {
901
1
        _condition_cache_ctx->base_granule =
902
1
                _row_group_plans.front().first_file_row / ConditionCacheContext::GRANULE_SIZE;
903
1
        const auto& last_plan = _row_group_plans.back();
904
1
        const int64_t end_granule = (last_plan.first_file_row + last_plan.row_group_rows +
905
1
                                     ConditionCacheContext::GRANULE_SIZE - 1) /
906
1
                                    ConditionCacheContext::GRANULE_SIZE;
907
1
        DORIS_CHECK(end_granule > _condition_cache_ctx->base_granule);
908
1
        _condition_cache_ctx->num_granules =
909
1
                std::min(_condition_cache_ctx->filter_result->size(),
910
1
                         static_cast<size_t>(end_granule - _condition_cache_ctx->base_granule));
911
1
        return;
912
1
    }
913
914
3
    std::vector<RowGroupReadPlan> filtered_plans;
915
3
    filtered_plans.reserve(_row_group_plans.size());
916
3
    for (auto& plan : _row_group_plans) {
917
3
        const int64_t old_rows = count_range_rows(plan.selected_ranges);
918
3
        plan.selected_ranges = filter_ranges_by_condition_cache(
919
3
                plan.selected_ranges, *_condition_cache_ctx->filter_result, plan.first_file_row,
920
3
                _condition_cache_ctx->base_granule);
921
3
        const int64_t new_rows = count_range_rows(plan.selected_ranges);
922
3
        _condition_cache_filtered_rows += old_rows - new_rows;
923
3
        if (!plan.selected_ranges.empty()) {
924
3
            filtered_plans.push_back(std::move(plan));
925
3
        }
926
3
    }
927
3
    _row_group_plans = std::move(filtered_plans);
928
3
    reset();
929
3
}
930
931
299
void ParquetScanScheduler::reset() {
932
299
    _next_row_group_plan_idx = 0;
933
299
    _raw_rows_read = 0;
934
299
    _predicate_schedule_request = nullptr;
935
299
    _predicate_schedule = {};
936
299
    _predicate_positions_scratch.clear();
937
299
    _predicate_indices_by_position_scratch.clear();
938
299
    _materialized_predicate_positions_scratch.clear();
939
299
    _ordered_predicate_positions_scratch.clear();
940
299
    _predicate_batch_sequence = 0;
941
299
    reset_current_row_group();
942
299
}
943
944
296
void ParquetScanScheduler::set_scan_request(std::shared_ptr<format::FileScanRequest> request) {
945
296
    DORIS_CHECK(request != nullptr);
946
296
    _active_request = std::move(request);
947
296
    _pending_request.reset();
948
296
    _predicate_schedule_request = nullptr;
949
296
}
950
951
3
void ParquetScanScheduler::queue_scan_request(std::shared_ptr<format::FileScanRequest> request) {
952
3
    DORIS_CHECK(request != nullptr);
953
3
    _pending_request = std::move(request);
954
3
}
955
956
430
void ParquetScanScheduler::activate_pending_scan_request_at_row_group_boundary() {
957
430
    if (_has_current_row_group || !_pending_predicate_selection.empty() ||
958
430
        _pending_request == nullptr) {
959
427
        return;
960
427
    }
961
    // Column readers and predicate schedules retain request-derived state for one row group. Swap
962
    // only after they are gone; the refreshed request may promote a lazy column to a predicate.
963
3
    _active_request = std::move(_pending_request);
964
3
    _predicate_schedule_request = nullptr;
965
    // Footer plans and adaptive ordering describe the previous predicate snapshot. Reusing either
966
    // after a late runtime filter would miss pruning or bias the new predicate order with stale data.
967
3
    _remaining_plans_need_replanning = true;
968
3
    _predicate_schedule = {};
969
3
    _predicate_positions_scratch.clear();
970
3
    _predicate_indices_by_position_scratch.clear();
971
3
    _materialized_predicate_positions_scratch.clear();
972
3
    _ordered_predicate_positions_scratch.clear();
973
3
    _predicate_runtime_stats.clear();
974
3
    _predicate_batch_sequence = 0;
975
3
    _predicate_survival_ratio = -1;
976
3
}
977
978
615
void ParquetScanScheduler::reset_current_row_group() {
979
    // RuntimeProfile updates are amortized on the batch path, but a row-group transition destroys
980
    // the reader tree. Force the final delta out before clearing it so short row groups and early
981
    // EOF paths cannot lose their last decode/IO timings.
982
615
    flush_current_reader_profiles();
983
615
    _batches_since_profile_flush = 0;
984
615
    _has_current_row_group = false;
985
615
    _current_predicate_columns.clear();
986
615
    _current_non_predicate_columns.clear();
987
615
    _current_dictionary_filters.clear();
988
615
    _current_dictionary_residual_conjuncts.clear();
989
615
    _current_row_group_rows = 0;
990
615
    _current_row_group_id = -1;
991
615
    _current_row_group_rows_read = 0;
992
615
    _current_row_group_first_row = 0;
993
615
    _current_selected_ranges.clear();
994
615
    _current_offset_indexes.clear();
995
615
    _current_range_idx = 0;
996
615
    _current_range_rows_read = 0;
997
    // Readers are row-group scoped. If every remaining row was filtered, no future output can
998
    // observe the non-predicate readers' position, so dropping them together with their pending lag
999
    // avoids a useless end-of-row-group SkipRecords call. Example: predicate readers advance from 0
1000
    // to 10,000 while lazy readers stay at 0; clearing both readers here is sufficient because the
1001
    // next row group constructs a new set starting at its own row 0.
1002
615
    _pending_non_predicate_skip_rows = 0;
1003
615
    _pending_predicate_batch_rows = 0;
1004
615
    _pending_predicate_batch_rows_consumed = 0;
1005
615
    _pending_predicate_selected_offset = 0;
1006
615
    _pending_predicate_selection.clear();
1007
615
    _pending_predicate_columns.clear();
1008
615
    _pending_output_selection.clear();
1009
615
    _current_predicate_prefetched = false;
1010
615
    _current_non_predicate_prefetched = false;
1011
615
    _current_merge_range_active = false;
1012
615
}
1013
1014
906
void ParquetScanScheduler::flush_current_reader_profiles() {
1015
906
    for (const auto& reader : _current_predicate_columns | std::views::values) {
1016
309
        reader->flush_profile();
1017
309
    }
1018
906
    for (const auto& reader : _current_non_predicate_columns | std::views::values) {
1019
671
        reader->flush_profile();
1020
671
    }
1021
906
}
1022
1023
353
bool ParquetScanScheduler::finish_current_reader_batch_profiles() {
1024
353
    bool crossed_page = false;
1025
    // A scheduler batch is counted once even when several projected leaves cross page boundaries.
1026
353
    for (const auto& reader : _current_predicate_columns | std::views::values) {
1027
250
        crossed_page |= reader->crossed_page_since_last_batch();
1028
250
    }
1029
525
    for (const auto& reader : _current_non_predicate_columns | std::views::values) {
1030
525
        crossed_page |= reader->crossed_page_since_last_batch();
1031
525
    }
1032
353
    return crossed_page;
1033
353
}
1034
1035
const detail::PredicateConjunctSchedule& ParquetScanScheduler::predicate_conjunct_schedule(
1036
470
        const format::FileScanRequest& request) {
1037
470
    if (_predicate_schedule_request == &request) {
1038
202
        return _predicate_schedule;
1039
202
    }
1040
1041
    // FileScanRequest is frozen by ParquetReader::open(). Its address therefore identifies both
1042
    // the conjunct set and local-position mapping for the scheduler lifetime.
1043
268
    _predicate_schedule = build_predicate_conjunct_schedule(request);
1044
268
    _predicate_schedule_request = &request;
1045
268
    _predicate_positions_scratch.clear();
1046
268
    _predicate_indices_by_position_scratch.clear();
1047
268
    _materialized_predicate_positions_scratch.clear();
1048
268
    _predicate_positions_scratch.reserve(request.predicate_columns.size());
1049
268
    _predicate_indices_by_position_scratch.reserve(request.predicate_columns.size());
1050
268
    _materialized_predicate_positions_scratch.reserve(request.predicate_columns.size());
1051
450
    for (size_t idx = 0; idx < request.predicate_columns.size(); ++idx) {
1052
182
        const auto position_it =
1053
182
                request.local_positions.find(request.predicate_columns[idx].column_id());
1054
182
        DORIS_CHECK(position_it != request.local_positions.end());
1055
182
        const size_t position = position_it->second.value();
1056
182
        _predicate_positions_scratch.push_back(position);
1057
182
        _predicate_indices_by_position_scratch.emplace(position, idx);
1058
182
    }
1059
268
    return _predicate_schedule;
1060
470
}
1061
1062
std::vector<format::LocalColumnIndex> ParquetScanScheduler::adaptive_predicate_prefetch_columns(
1063
13
        const format::FileScanRequest& request) {
1064
13
    std::vector<size_t> positions;
1065
13
    std::unordered_map<size_t, const format::LocalColumnIndex*> columns_by_position;
1066
13
    columns_by_position.reserve(request.predicate_columns.size());
1067
13
    for (const auto& column : request.predicate_columns) {
1068
1
        const auto position_it = request.local_positions.find(column.column_id());
1069
1
        DORIS_CHECK(position_it != request.local_positions.end());
1070
1
        const size_t position = position_it->second.value();
1071
1
        columns_by_position.emplace(position, &column);
1072
1
    }
1073
13
    const auto& schedule = predicate_conjunct_schedule(request);
1074
13
    if (!schedule.supports_lazy_materialization) {
1075
0
        positions.reserve(request.predicate_columns.size());
1076
0
        for (const auto& column : request.predicate_columns) {
1077
0
            positions.push_back(request.local_positions.at(column.column_id()).value());
1078
0
        }
1079
13
    } else if (!schedule.single_column_conjuncts.empty()) {
1080
0
        positions.reserve(schedule.single_column_conjuncts.size());
1081
0
        for (const auto& column : request.predicate_columns) {
1082
0
            const size_t position = request.local_positions.at(column.column_id()).value();
1083
0
            if (schedule.single_column_conjuncts.contains(position)) {
1084
                // Cold adaptive statistics intentionally preserve request order; iterating the
1085
                // hash map here would make the first decoded predicate depend on bucket layout.
1086
0
                positions.push_back(position);
1087
0
            }
1088
0
        }
1089
13
    } else if (!schedule.remaining_stages.empty()) {
1090
        // Match execution's first reachable stage. Warming columns owned only by later residuals
1091
        // would turn lazy decode into eager remote IO before an earlier conjunct can reject rows.
1092
0
        positions = schedule.remaining_stages.front().required_positions;
1093
13
    } else {
1094
13
        positions.reserve(request.predicate_columns.size());
1095
13
        for (const auto& column : request.predicate_columns) {
1096
1
            positions.push_back(request.local_positions.at(column.column_id()).value());
1097
1
        }
1098
13
    }
1099
13
    auto ordered = detail::order_adaptive_predicates(positions, _predicate_runtime_stats);
1100
13
    ordered = detail::adaptive_prefetch_prefix(ordered, _predicate_runtime_stats, 0.25);
1101
13
    std::vector<format::LocalColumnIndex> result;
1102
13
    result.reserve(ordered.size());
1103
13
    for (const size_t position : ordered) {
1104
1
        result.push_back(*columns_by_position.at(position));
1105
1
    }
1106
13
    return result;
1107
13
}
1108
1109
Status ParquetScanScheduler::open_next_row_group(
1110
        ParquetFileContext& file_context,
1111
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
1112
428
        const format::FileScanRequest& request, bool* has_row_group) {
1113
428
    *has_row_group = false;
1114
428
    RowGroupReadPlan* selected_plan = nullptr;
1115
452
    while (_next_row_group_plan_idx < _row_group_plans.size()) {
1116
312
        RowGroupReadPlan& candidate_plan = _row_group_plans[_next_row_group_plan_idx++];
1117
        // Probe only the row group about to execute. This keeps LIMIT/cancellation latency
1118
        // independent of the number of later remote row groups while preserving eager footer
1119
        // statistics pruning during open.
1120
312
        file_context.reset_random_access_ranges();
1121
312
        _current_merge_range_active = false;
1122
312
        ParquetPruningStats deferred_stats;
1123
312
        if (_remaining_plans_need_replanning) {
1124
            // A refreshed projection may require different dictionary, Bloom, or page-index
1125
            // metadata. Preserve already-safe selected ranges, but rebuild every request-shaped
1126
            // artifact before opening this row group.
1127
4
            candidate_plan.expensive_pruning_pending = true;
1128
4
            candidate_plan.page_skip_plans.clear();
1129
4
            candidate_plan.offset_indexes.clear();
1130
4
            const std::vector<int> candidate {candidate_plan.row_group_id};
1131
4
            std::vector<int> footer_selected;
1132
4
            RETURN_IF_ERROR(select_row_groups_by_metadata(
1133
4
                    file_context.native_metadata->to_thrift(), file_schema, request, &candidate,
1134
4
                    &footer_selected, _enable_bloom_filter, &deferred_stats, _timezone,
1135
4
                    _runtime_state, &file_context, _scan_profile.column_reader_profile,
1136
4
                    ParquetMetadataProbeMode::FOOTER_ONLY));
1137
4
            if (footer_selected.empty()) {
1138
2
                if (_parquet_profile != nullptr) {
1139
1
                    _parquet_profile->update_deferred_pruning_stats(deferred_stats, false);
1140
1
                }
1141
2
                continue;
1142
2
            }
1143
4
        }
1144
310
        bool selected = false;
1145
310
        RETURN_IF_ERROR(finalize_native_row_group_read_plan(
1146
310
                *file_context.native_metadata, file_schema, request, _enable_bloom_filter,
1147
310
                &candidate_plan, &deferred_stats, _timezone, _runtime_state, &file_context,
1148
310
                _scan_profile.column_reader_profile, &selected));
1149
310
        if (_parquet_profile != nullptr) {
1150
201
            _parquet_profile->update_deferred_pruning_stats(deferred_stats, selected);
1151
201
        }
1152
310
        if (!selected) {
1153
22
            continue;
1154
22
        }
1155
288
        selected_plan = &candidate_plan;
1156
288
        break;
1157
310
    }
1158
428
    if (selected_plan == nullptr) {
1159
        // The last row group's native readers have already been released by
1160
        // reset_current_row_group(). Flush the shared merge reader now so its counters are visible
1161
        // when EOF is returned and its bounded scratch does not survive until file close.
1162
140
        file_context.reset_random_access_ranges();
1163
140
        _current_merge_range_active = false;
1164
140
        return Status::OK();
1165
140
    }
1166
288
    RowGroupReadPlan& row_group_plan = *selected_plan;
1167
288
    const int row_group_idx = row_group_plan.row_group_id;
1168
    // Dictionary probes and data-page readers share the native metadata tree. Reset the previous
1169
    // row-group merge reader before probing because dictionary-page offsets are not scan ordered.
1170
288
    file_context.reset_random_access_ranges();
1171
288
    _current_merge_range_active = false;
1172
1173
288
    const auto& row_group_metadata =
1174
288
            file_context.native_metadata->to_thrift().row_groups[row_group_idx];
1175
288
    _current_row_group_rows = row_group_metadata.num_rows;
1176
288
    DORIS_CHECK(_current_row_group_rows == row_group_plan.row_group_rows);
1177
288
    DORIS_CHECK(_current_row_group_rows > 0);
1178
288
    _current_row_group_id = row_group_idx;
1179
288
    _has_current_row_group = true;
1180
288
    DORIS_CHECK(!row_group_plan.selected_ranges.empty());
1181
288
    _current_row_group_first_row = row_group_plan.first_file_row;
1182
288
    _current_row_group_rows_read = 0;
1183
288
    _current_selected_ranges = row_group_plan.selected_ranges;
1184
288
    _current_offset_indexes = std::move(row_group_plan.offset_indexes);
1185
    // Condition Cache and split planning can narrow logical ranges without a physical OffsetIndex.
1186
    // Native readers must keep the sequential level/value cursor path valid in that case; only a
1187
    // PageIndex-derived skip plan requires the transferred indexes below.
1188
288
    for (const auto& [leaf_column_id, skip_plan] : row_group_plan.page_skip_plans) {
1189
3
        if (!_current_offset_indexes.contains(leaf_column_id)) {
1190
0
            continue;
1191
0
        }
1192
51
        for (size_t page = 0; page < skip_plan.skipped_pages.size(); ++page) {
1193
48
            if (!skip_plan.should_skip_page(page)) {
1194
24
                continue;
1195
24
            }
1196
24
            if (_page_skip_profile.skipped_pages != nullptr) {
1197
24
                COUNTER_UPDATE(_page_skip_profile.skipped_pages, 1);
1198
24
            }
1199
24
            if (_page_skip_profile.skipped_bytes != nullptr) {
1200
24
                COUNTER_UPDATE(_page_skip_profile.skipped_bytes,
1201
24
                               skip_plan.skipped_page_compressed_size(page));
1202
24
            }
1203
24
        }
1204
3
    }
1205
288
    _current_range_idx = 0;
1206
288
    _current_range_rows_read = 0;
1207
288
    _current_predicate_columns.clear();
1208
288
    _current_non_predicate_columns.clear();
1209
288
    _current_dictionary_filters.clear();
1210
288
    RETURN_IF_ERROR(prepare_current_dictionary_filters(file_context, file_schema, request,
1211
288
                                                       row_group_idx, row_group_metadata));
1212
    // Dictionary probing is complete, so the native data-page readers can now share the same
1213
    // row-group-scoped MergeRangeFileReader policy as v1. Sharing one wrapper is important: a
1214
    // separate merge reader per leaf would duplicate its 128MB scratch capacity and defeat lazy
1215
    // materialization for wide schemas.
1216
288
    const auto& thrift_metadata = file_context.native_metadata->to_thrift();
1217
288
    const auto compat = native::parquet_reader_compat(
1218
288
            thrift_metadata.__isset.created_by ? thrift_metadata.created_by : std::string {});
1219
288
    std::vector<ParquetPageCacheRange> native_ranges;
1220
288
    RETURN_IF_ERROR(detail::build_native_prefetch_ranges(
1221
288
            thrift_metadata, file_schema, request_scan_columns(request), row_group_idx,
1222
288
            file_context.native_file->size(), compat.parquet_816_padding, &native_ranges));
1223
288
    _current_merge_range_active = file_context.set_native_random_access_ranges(
1224
288
            native_ranges, detail::average_prefetch_range_size(native_ranges), _profile,
1225
288
            _merge_read_slice_size);
1226
1227
288
    for (const auto& col : request.predicate_columns) {
1228
191
        const auto local_id = col.column_id();
1229
191
        if (_current_predicate_columns.contains(local_id)) {
1230
48
            continue;
1231
48
        }
1232
143
        if (local_id == format::LocalColumnId(format::ROW_POSITION_COLUMN_ID)) {
1233
30
            _current_predicate_columns[local_id] = std::make_unique<RowPositionColumnReader>(
1234
30
                    _current_row_group_first_row, _scan_profile.column_reader_profile);
1235
30
            continue;
1236
30
        }
1237
113
        if (local_id == format::LocalColumnId(format::GLOBAL_ROWID_COLUMN_ID)) {
1238
1
            DORIS_CHECK(_global_rowid_context.has_value());
1239
1
            _current_predicate_columns[local_id] = std::make_unique<GlobalRowIdColumnReader>(
1240
1
                    *_global_rowid_context, _current_row_group_first_row,
1241
1
                    _scan_profile.column_reader_profile);
1242
1
            continue;
1243
1
        }
1244
1245
112
        DORIS_CHECK(local_id.is_valid() &&
1246
112
                    local_id.value() < static_cast<int32_t>(file_schema.size()));
1247
112
        const auto& column_schema = file_schema[local_id.value()];
1248
112
        DORIS_CHECK(column_schema != nullptr);
1249
112
        std::unique_ptr<ParquetColumnReader> column_reader;
1250
112
        RETURN_IF_ERROR(NativeColumnReader::create(
1251
112
                *column_schema, &col, file_context.native_data_file(), file_context.native_metadata,
1252
112
                row_group_idx, _current_selected_ranges, _current_offset_indexes, _timezone,
1253
112
                _int96_timezone, file_context.native_io_ctx, _runtime_state,
1254
112
                file_context.native_page_cache_enabled, file_context.native_page_cache_file_key,
1255
112
                _current_dictionary_filters.contains(local_id), _scan_profile.column_reader_profile,
1256
112
                &column_reader));
1257
112
        _current_predicate_columns[local_id] = std::move(column_reader);
1258
112
    }
1259
    // Start warming filter-column chunks as soon as their row group is selected. The native
1260
    // BufferedFileStreamReader later consumes the same Doris file-cache blocks; prefetch never
1261
    // changes row/column materialization order.
1262
288
    if (!_current_merge_range_active) {
1263
13
        const auto prefetch_columns = adaptive_predicate_prefetch_columns(request);
1264
13
        RETURN_IF_ERROR(prefetch_current_row_group_columns(
1265
13
                file_context, file_schema, prefetch_columns, &_current_predicate_prefetched));
1266
13
    }
1267
433
    for (const auto& col : request.non_predicate_columns) {
1268
433
        const auto local_id = col.column_id();
1269
433
        if (request.is_count_star_placeholder(col.column_id())) {
1270
1
            continue;
1271
1
        }
1272
432
        if (local_id == format::LocalColumnId(format::ROW_POSITION_COLUMN_ID)) {
1273
14
            _current_non_predicate_columns[local_id] = std::make_unique<RowPositionColumnReader>(
1274
14
                    _current_row_group_first_row, _scan_profile.column_reader_profile);
1275
14
            continue;
1276
14
        }
1277
418
        if (local_id == format::LocalColumnId(format::GLOBAL_ROWID_COLUMN_ID)) {
1278
2
            DORIS_CHECK(_global_rowid_context.has_value());
1279
2
            _current_non_predicate_columns[local_id] = std::make_unique<GlobalRowIdColumnReader>(
1280
2
                    *_global_rowid_context, _current_row_group_first_row,
1281
2
                    _scan_profile.column_reader_profile);
1282
2
            continue;
1283
2
        }
1284
416
        DORIS_CHECK(local_id.is_valid() &&
1285
416
                    local_id.value() < static_cast<int32_t>(file_schema.size()));
1286
416
        const auto& column_schema = file_schema[local_id.value()];
1287
416
        DORIS_CHECK(column_schema != nullptr);
1288
416
        std::unique_ptr<ParquetColumnReader> column_reader;
1289
416
        RETURN_IF_ERROR(NativeColumnReader::create(
1290
416
                *column_schema, &col, file_context.native_data_file(), file_context.native_metadata,
1291
416
                row_group_idx, _current_selected_ranges, _current_offset_indexes, _timezone,
1292
416
                _int96_timezone, file_context.native_io_ctx, _runtime_state,
1293
416
                file_context.native_page_cache_enabled, file_context.native_page_cache_file_key,
1294
416
                false, _scan_profile.column_reader_profile, &column_reader));
1295
416
        _current_non_predicate_columns[local_id] = std::move(column_reader);
1296
416
    }
1297
288
    if (!_current_merge_range_active &&
1298
288
        ((request.conjuncts.empty() && request.delete_conjuncts.empty()) ||
1299
13
         _predicate_survival_ratio >= 0.8)) {
1300
        // With no row-level filters there is no lazy-read decision to wait for, so start warming
1301
        // output chunks immediately after their readers are created. Filtered scans still defer
1302
        // this until at least one row survives the predicate phase.
1303
12
        RETURN_IF_ERROR(prefetch_current_row_group_columns(file_context, file_schema,
1304
12
                                                           physical_non_predicate_columns(request),
1305
12
                                                           &_current_non_predicate_prefetched));
1306
12
    }
1307
288
    *has_row_group = true;
1308
288
    return Status::OK();
1309
288
}
1310
1311
3
Status ParquetScanScheduler::skip_current_row_group_rows(int64_t rows) {
1312
3
    DORIS_CHECK(rows >= 0);
1313
3
    if (rows == 0) {
1314
0
        return Status::OK();
1315
0
    }
1316
3
    if (_scan_profile.range_gap_skipped_rows != nullptr) {
1317
2
        COUNTER_UPDATE(_scan_profile.range_gap_skipped_rows, rows);
1318
2
    }
1319
3
    for (const auto& column_reader : _current_predicate_columns | std::views::values) {
1320
3
        RETURN_IF_ERROR(column_reader->skip(rows));
1321
3
    }
1322
    // Keep page-index/condition-cache gaps pending for lazy columns as well. For example, after a
1323
    // fully filtered [0, 32) batch and a pruned [32, 96) gap, predicate readers are at 96 while lazy
1324
    // readers remain at 0; one later skip(96) is cheaper than skip(32) followed by skip(64).
1325
3
    DORIS_CHECK(_pending_non_predicate_skip_rows <= std::numeric_limits<int64_t>::max() - rows);
1326
3
    _pending_non_predicate_skip_rows += rows;
1327
3
    _current_row_group_rows_read += rows;
1328
3
    return Status::OK();
1329
3
}
1330
1331
305
Status ParquetScanScheduler::flush_pending_non_predicate_skip_rows() {
1332
305
    if (_pending_non_predicate_skip_rows == 0) {
1333
298
        return Status::OK();
1334
298
    }
1335
8
    for (const auto& column_reader : _current_non_predicate_columns | std::views::values) {
1336
8
        RETURN_IF_ERROR(column_reader->skip(_pending_non_predicate_skip_rows));
1337
8
    }
1338
7
    _pending_non_predicate_skip_rows = 0;
1339
7
    return Status::OK();
1340
7
}
1341
1342
namespace {
1343
1344
bool append_residual_stages(const VExprContextSPtr& owner_context, const VExprSPtr& expression,
1345
                            const std::unordered_set<size_t>& predicate_block_positions,
1346
12
                            std::vector<detail::PredicateConjunctStage>* stages) {
1347
12
    DORIS_CHECK(owner_context != nullptr);
1348
12
    DORIS_CHECK(expression != nullptr);
1349
12
    DORIS_CHECK(stages != nullptr);
1350
12
    const auto* compound_predicate = dynamic_cast<const VCompoundPred*>(expression.get());
1351
12
    if (compound_predicate != nullptr && compound_predicate->op() == TExprOpcode::COMPOUND_AND) {
1352
4
        for (const auto& child : expression->children()) {
1353
4
            if (!append_residual_stages(owner_context, child, predicate_block_positions, stages)) {
1354
0
                return false;
1355
0
            }
1356
4
        }
1357
2
        return true;
1358
2
    }
1359
1360
10
    std::set<int> referenced_positions;
1361
10
    expression->collect_slot_column_ids(referenced_positions);
1362
10
    auto& stage = stages->emplace_back();
1363
10
    stage.owner_context = owner_context;
1364
10
    stage.expression = expression;
1365
20
    for (const int position : referenced_positions) {
1366
20
        if (position < 0 || !predicate_block_positions.contains(cast_set<size_t>(position))) {
1367
0
            stages->pop_back();
1368
0
            return false;
1369
0
        }
1370
20
        stage.required_positions.push_back(cast_set<size_t>(position));
1371
20
    }
1372
10
    return true;
1373
10
}
1374
1375
detail::PredicateConjunctSchedule build_predicate_conjunct_schedule(
1376
268
        const format::FileScanRequest& request) {
1377
268
    std::unordered_set<size_t> predicate_block_positions;
1378
268
    predicate_block_positions.reserve(request.predicate_columns.size());
1379
268
    for (const auto& col : request.predicate_columns) {
1380
182
        const auto position_it = request.local_positions.find(col.column_id());
1381
182
        DORIS_CHECK(position_it != request.local_positions.end());
1382
182
        predicate_block_positions.insert(position_it->second.value());
1383
182
    }
1384
1385
268
    detail::PredicateConjunctSchedule schedule;
1386
268
    for (const auto& conjunct : request.conjuncts) {
1387
126
        DORIS_CHECK(conjunct != nullptr);
1388
126
        DORIS_CHECK(conjunct->root() != nullptr);
1389
126
        if (!conjunct->root()->is_safe_to_execute_on_selected_rows()) {
1390
            // Round-by-round filtering can compact later predicate columns before evaluating
1391
            // remaining expressions. Stateful functions such as random(1) and error-preserving
1392
            // functions such as assert_true() must see the same full batch they saw before this
1393
            // optimization, so any unsafe conjunct disables the per-column schedule for the batch.
1394
5
            schedule.remaining_conjuncts = request.conjuncts;
1395
5
            schedule.single_column_conjuncts.clear();
1396
5
            schedule.remaining_stages.clear();
1397
5
            schedule.supports_lazy_materialization = false;
1398
5
            return schedule;
1399
5
        }
1400
121
        std::set<int> referenced_positions;
1401
121
        conjunct->root()->collect_slot_column_ids(referenced_positions);
1402
121
        if (referenced_positions.size() != 1) {
1403
8
            schedule.remaining_conjuncts.push_back(conjunct);
1404
8
            if (!append_residual_stages(conjunct, conjunct->root(), predicate_block_positions,
1405
8
                                        &schedule.remaining_stages)) {
1406
0
                schedule.supports_lazy_materialization = false;
1407
0
                schedule.remaining_conjuncts = request.conjuncts;
1408
0
                schedule.single_column_conjuncts.clear();
1409
0
                schedule.remaining_stages.clear();
1410
0
                return schedule;
1411
0
            }
1412
8
            continue;
1413
8
        }
1414
113
        const auto block_position = static_cast<size_t>(*referenced_positions.begin());
1415
113
        if (!predicate_block_positions.contains(block_position)) {
1416
0
            schedule.supports_lazy_materialization = false;
1417
0
            schedule.remaining_conjuncts = request.conjuncts;
1418
0
            schedule.single_column_conjuncts.clear();
1419
0
            schedule.remaining_stages.clear();
1420
0
            return schedule;
1421
0
        }
1422
113
        schedule.single_column_conjuncts[block_position].push_back(conjunct);
1423
113
    }
1424
263
    return schedule;
1425
268
}
1426
1427
114
bool can_evaluate_all_with_dictionary(const VExprContextSPtrs& conjuncts) {
1428
114
    if (conjuncts.empty()) {
1429
0
        return false;
1430
0
    }
1431
116
    return std::ranges::all_of(conjuncts, [](const auto& conjunct) {
1432
116
        return conjunct != nullptr && conjunct->root() != nullptr &&
1433
116
               conjunct->root()->can_evaluate_dictionary_filter();
1434
116
    });
1435
114
}
1436
1437
61
bool can_evaluate_dictionary_exactly(const VExprSPtr& expr) {
1438
61
    DORIS_CHECK(expr != nullptr);
1439
61
    if (expr->is_topn_filter()) {
1440
        // A row-group bitmap snapshots one bound, while TopN can publish or tighten it between
1441
        // batches. Keep the row expression as a residual; the cached bitmap remains a safe
1442
        // monotonic prefilter and the residual observes the current bound on every batch.
1443
2
        return false;
1444
2
    }
1445
59
    const auto* compound_pred = dynamic_cast<const VCompoundPred*>(expr.get());
1446
59
    if (compound_pred == nullptr) {
1447
56
        return expr->can_evaluate_dictionary_filter();
1448
56
    }
1449
3
    if (compound_pred->op() != TExprOpcode::COMPOUND_AND &&
1450
3
        compound_pred->op() != TExprOpcode::COMPOUND_OR) {
1451
0
        return false;
1452
0
    }
1453
3
    return !expr->children().empty() &&
1454
6
           std::ranges::all_of(expr->children(), [](const auto& child) {
1455
6
               return can_evaluate_dictionary_exactly(child);
1456
6
           });
1457
3
}
1458
1459
void collect_dictionary_residual_exprs(const VExprContextSPtr& owner_context, const VExprSPtr& expr,
1460
55
                                       OwnedExpressionConjuncts* residual_conjuncts) {
1461
55
    DORIS_CHECK(owner_context != nullptr);
1462
55
    DORIS_CHECK(expr != nullptr);
1463
55
    DORIS_CHECK(residual_conjuncts != nullptr);
1464
1465
55
    if (can_evaluate_dictionary_exactly(expr)) {
1466
47
        return;
1467
47
    }
1468
1469
    // VCompoundPred dictionary evaluation is a conservative prefilter for AND when only some
1470
    // children are dictionary-aware. Split AND so exact dictionary children are not executed again
1471
    // on materialized rows. Do not split a non-exact OR: its branches cannot be evaluated
1472
    // independently after a dictionary prefilter without changing the original boolean semantics.
1473
8
    const auto* compound_pred = dynamic_cast<const VCompoundPred*>(expr.get());
1474
8
    if (compound_pred != nullptr && compound_pred->op() == TExprOpcode::COMPOUND_AND) {
1475
6
        for (const auto& child : expr->children()) {
1476
6
            collect_dictionary_residual_exprs(owner_context, child, residual_conjuncts);
1477
6
        }
1478
3
        return;
1479
3
    }
1480
1481
5
    residual_conjuncts->emplace_back(owner_context, expr);
1482
5
}
1483
1484
48
OwnedExpressionConjuncts build_dictionary_residual_conjuncts(const VExprContextSPtrs& conjuncts) {
1485
48
    OwnedExpressionConjuncts residual_conjuncts;
1486
49
    for (const auto& conjunct : conjuncts) {
1487
49
        DORIS_CHECK(conjunct != nullptr);
1488
49
        collect_dictionary_residual_exprs(conjunct, conjunct->root(), &residual_conjuncts);
1489
49
    }
1490
48
    return residual_conjuncts;
1491
48
}
1492
1493
141
uint16_t count_selected_rows(const IColumn::Filter& filter) {
1494
141
    uint16_t selected_rows = 0;
1495
10.3k
    for (const auto value : filter) {
1496
10.3k
        selected_rows += value != 0;
1497
10.3k
    }
1498
141
    return selected_rows;
1499
141
}
1500
1501
enum class DictionaryEntryFilterKernel {
1502
    GENERIC,
1503
    TYPED_FIXED_WIDTH,
1504
    TYPED_STRING,
1505
    VECTORIZED_RUNTIME_FILTER,
1506
};
1507
1508
template <typename ColumnType>
1509
bool get_fixed_dictionary_raw_values(const IColumn& dictionary, const uint8_t** values,
1510
24
                                     size_t* value_width) {
1511
24
    const auto* typed_dictionary = check_and_get_column<ColumnType>(dictionary);
1512
24
    if (typed_dictionary == nullptr) {
1513
0
        return false;
1514
0
    }
1515
24
    *values = reinterpret_cast<const uint8_t*>(typed_dictionary->get_data().data());
1516
24
    *value_width = sizeof(typename ColumnType::value_type);
1517
24
    return true;
1518
24
}
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_12ColumnVectorILNS_13PrimitiveTypeE2EEEEEbRKNS_7IColumnEPPKhPm
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_12ColumnVectorILNS_13PrimitiveTypeE3EEEEEbRKNS_7IColumnEPPKhPm
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_12ColumnVectorILNS_13PrimitiveTypeE4EEEEEbRKNS_7IColumnEPPKhPm
parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_12ColumnVectorILNS_13PrimitiveTypeE5EEEEEbRKNS_7IColumnEPPKhPm
Line
Count
Source
1510
23
                                     size_t* value_width) {
1511
23
    const auto* typed_dictionary = check_and_get_column<ColumnType>(dictionary);
1512
23
    if (typed_dictionary == nullptr) {
1513
0
        return false;
1514
0
    }
1515
23
    *values = reinterpret_cast<const uint8_t*>(typed_dictionary->get_data().data());
1516
23
    *value_width = sizeof(typename ColumnType::value_type);
1517
23
    return true;
1518
23
}
parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_12ColumnVectorILNS_13PrimitiveTypeE6EEEEEbRKNS_7IColumnEPPKhPm
Line
Count
Source
1510
1
                                     size_t* value_width) {
1511
1
    const auto* typed_dictionary = check_and_get_column<ColumnType>(dictionary);
1512
1
    if (typed_dictionary == nullptr) {
1513
0
        return false;
1514
0
    }
1515
1
    *values = reinterpret_cast<const uint8_t*>(typed_dictionary->get_data().data());
1516
1
    *value_width = sizeof(typename ColumnType::value_type);
1517
1
    return true;
1518
1
}
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_12ColumnVectorILNS_13PrimitiveTypeE7EEEEEbRKNS_7IColumnEPPKhPm
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_12ColumnVectorILNS_13PrimitiveTypeE8EEEEEbRKNS_7IColumnEPPKhPm
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_12ColumnVectorILNS_13PrimitiveTypeE9EEEEEbRKNS_7IColumnEPPKhPm
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_12ColumnVectorILNS_13PrimitiveTypeE11EEEEEbRKNS_7IColumnEPPKhPm
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_12ColumnVectorILNS_13PrimitiveTypeE12EEEEEbRKNS_7IColumnEPPKhPm
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_12ColumnVectorILNS_13PrimitiveTypeE25EEEEEbRKNS_7IColumnEPPKhPm
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_12ColumnVectorILNS_13PrimitiveTypeE26EEEEEbRKNS_7IColumnEPPKhPm
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_12ColumnVectorILNS_13PrimitiveTypeE42EEEEEbRKNS_7IColumnEPPKhPm
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_13ColumnDecimalILNS_13PrimitiveTypeE28EEEEEbRKNS_7IColumnEPPKhPm
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_13ColumnDecimalILNS_13PrimitiveTypeE29EEEEEbRKNS_7IColumnEPPKhPm
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_13ColumnDecimalILNS_13PrimitiveTypeE20EEEEEbRKNS_7IColumnEPPKhPm
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_13ColumnDecimalILNS_13PrimitiveTypeE30EEEEEbRKNS_7IColumnEPPKhPm
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_13ColumnDecimalILNS_13PrimitiveTypeE35EEEEEbRKNS_7IColumnEPPKhPm
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_12ColumnVectorILNS_13PrimitiveTypeE36EEEEEbRKNS_7IColumnEPPKhPm
Unexecuted instantiation: parquet_scan.cpp:_ZN5doris6format7parquet12_GLOBAL__N_131get_fixed_dictionary_raw_valuesINS_12ColumnVectorILNS_13PrimitiveTypeE37EEEEEbRKNS_7IColumnEPPKhPm
1519
1520
bool get_typed_dictionary_raw_values(PrimitiveType primitive_type, const IColumn& dictionary,
1521
24
                                     const uint8_t** values, size_t* value_width) {
1522
24
    switch (primitive_type) {
1523
0
#define GET_TYPED_DICTIONARY_VALUES(TYPE)                                                       \
1524
24
    case TYPE:                                                                                  \
1525
24
        return get_fixed_dictionary_raw_values<typename PrimitiveTypeTraits<TYPE>::ColumnType>( \
1526
24
                dictionary, values, value_width)
1527
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_BOOLEAN);
1528
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_TINYINT);
1529
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_SMALLINT);
1530
23
        GET_TYPED_DICTIONARY_VALUES(TYPE_INT);
1531
1
        GET_TYPED_DICTIONARY_VALUES(TYPE_BIGINT);
1532
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_LARGEINT);
1533
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_FLOAT);
1534
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_DOUBLE);
1535
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_DATE);
1536
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_DATETIME);
1537
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_DATEV2);
1538
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_DATETIMEV2);
1539
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_TIMESTAMPTZ);
1540
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_DECIMAL32);
1541
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_DECIMAL64);
1542
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_DECIMALV2);
1543
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_DECIMAL128I);
1544
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_DECIMAL256);
1545
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_IPV4);
1546
0
        GET_TYPED_DICTIONARY_VALUES(TYPE_IPV6);
1547
0
#undef GET_TYPED_DICTIONARY_VALUES
1548
0
    default:
1549
0
        return false;
1550
24
    }
1551
24
}
1552
1553
Status try_apply_runtime_filters_to_dictionary(size_t block_position,
1554
                                               const ParquetColumnSchema& column_schema,
1555
                                               const VExprContextSPtrs& conjuncts,
1556
                                               const IColumn& dictionary,
1557
22
                                               IColumn::Filter* dictionary_filter, bool* applied) {
1558
22
    DORIS_CHECK(dictionary_filter != nullptr);
1559
22
    DORIS_CHECK(applied != nullptr);
1560
22
    *applied = false;
1561
23
    if (!std::ranges::all_of(conjuncts, [](const auto& conjunct) {
1562
23
            return conjunct != nullptr && conjunct->root() != nullptr &&
1563
23
                   conjunct->root()->is_rf_wrapper() &&
1564
23
                   conjunct->root()->can_evaluate_dictionary_filter() &&
1565
23
                   conjunct->root()->get_impl() != nullptr;
1566
23
        })) {
1567
19
        return Status::OK();
1568
19
    }
1569
1570
3
    Block dictionary_block;
1571
3
    const size_t dictionary_size = dictionary.size();
1572
3
    const auto dummy_type = std::make_shared<DataTypeUInt8>();
1573
3
    for (size_t position = 0; position < block_position; ++position) {
1574
        // Slot position is metadata, not a reason to allocate position*rows bytes. Every unused
1575
        // slot shares the same one-value constant representation while preserving block indexing.
1576
0
        dictionary_block.insert({ColumnConst::create(ColumnUInt8::create(1, 0), dictionary_size),
1577
0
                                 dummy_type, "dictionary_dummy"});
1578
0
    }
1579
3
    ColumnPtr dictionary_column = dictionary.get_ptr();
1580
3
    if (column_schema.type->is_nullable()) {
1581
0
        dictionary_column =
1582
0
                ColumnNullable::create(dictionary_column, ColumnUInt8::create(dictionary_size, 0));
1583
0
    }
1584
3
    dictionary_block.insert({std::move(dictionary_column), column_schema.type, "dictionary_value"});
1585
1586
3
    dictionary_filter->clear();
1587
3
    dictionary_filter->resize_fill(dictionary_size, 1);
1588
4
    for (const auto& conjunct : conjuncts) {
1589
4
        bool can_filter_all = false;
1590
        // Execute the wrapped implementation directly. Sampling the tiny dictionary through the
1591
        // RF wrapper could mark the filter ineffective for the much larger row batches that follow.
1592
4
        RETURN_IF_ERROR(conjunct->root()->get_impl()->execute_filter(
1593
4
                conjunct.get(), &dictionary_block, dictionary_filter->data(), dictionary_size,
1594
4
                false, &can_filter_all));
1595
4
        if (can_filter_all) {
1596
0
            break;
1597
0
        }
1598
4
    }
1599
3
    *applied = true;
1600
3
    return Status::OK();
1601
3
}
1602
1603
enum class StringDictionaryCompareOp {
1604
    EQ,
1605
    NE,
1606
    LT,
1607
    LE,
1608
    GT,
1609
    GE,
1610
};
1611
1612
std::optional<StringDictionaryCompareOp> string_dictionary_compare_op(std::string_view name,
1613
2
                                                                      bool reverse) {
1614
2
    StringDictionaryCompareOp op;
1615
2
    if (name == "eq") {
1616
0
        op = StringDictionaryCompareOp::EQ;
1617
2
    } else if (name == "ne") {
1618
0
        op = StringDictionaryCompareOp::NE;
1619
2
    } else if (name == "lt") {
1620
1
        op = StringDictionaryCompareOp::LT;
1621
1
    } else if (name == "le") {
1622
0
        op = StringDictionaryCompareOp::LE;
1623
1
    } else if (name == "gt") {
1624
1
        op = StringDictionaryCompareOp::GT;
1625
1
    } else if (name == "ge") {
1626
0
        op = StringDictionaryCompareOp::GE;
1627
0
    } else {
1628
0
        return std::nullopt;
1629
0
    }
1630
2
    if (!reverse || op == StringDictionaryCompareOp::EQ || op == StringDictionaryCompareOp::NE) {
1631
1
        return op;
1632
1
    }
1633
1
    switch (op) {
1634
1
    case StringDictionaryCompareOp::LT:
1635
1
        return StringDictionaryCompareOp::GT;
1636
0
    case StringDictionaryCompareOp::LE:
1637
0
        return StringDictionaryCompareOp::GE;
1638
0
    case StringDictionaryCompareOp::GT:
1639
0
        return StringDictionaryCompareOp::LT;
1640
0
    case StringDictionaryCompareOp::GE:
1641
0
        return StringDictionaryCompareOp::LE;
1642
0
    default:
1643
0
        __builtin_unreachable();
1644
1
    }
1645
1
}
1646
1647
8
bool string_compare_matches(int comparison, StringDictionaryCompareOp op) {
1648
8
    switch (op) {
1649
0
    case StringDictionaryCompareOp::EQ:
1650
0
        return comparison == 0;
1651
0
    case StringDictionaryCompareOp::NE:
1652
0
        return comparison != 0;
1653
0
    case StringDictionaryCompareOp::LT:
1654
0
        return comparison < 0;
1655
0
    case StringDictionaryCompareOp::LE:
1656
0
        return comparison <= 0;
1657
8
    case StringDictionaryCompareOp::GT:
1658
8
        return comparison > 0;
1659
0
    case StringDictionaryCompareOp::GE:
1660
0
        return comparison >= 0;
1661
8
    }
1662
0
    __builtin_unreachable();
1663
8
}
1664
1665
bool try_apply_string_dictionary_conjunct(size_t block_position, const DataTypePtr& column_type,
1666
                                          const VExprSPtr& root, const IColumn& dictionary,
1667
24
                                          IColumn::Filter* dictionary_filter) {
1668
24
    const auto fn = std::dynamic_pointer_cast<VectorizedFnCall>(root);
1669
24
    if (fn == nullptr || (!dictionary.is_column_string() && !dictionary.is_column_string64())) {
1670
19
        return false;
1671
19
    }
1672
5
    const auto slot_literal = expr_zonemap::extract_slot_and_literal(fn->children());
1673
5
    if (!slot_literal.has_value() || slot_literal->slot_index != block_position ||
1674
5
        slot_literal->literal.get_type() != TYPE_STRING ||
1675
5
        !remove_nullable(slot_literal->slot_type)->equals(*remove_nullable(column_type)) ||
1676
5
        !remove_nullable(slot_literal->literal_type)->equals(*remove_nullable(column_type))) {
1677
3
        return false;
1678
3
    }
1679
2
    const auto op =
1680
2
            string_dictionary_compare_op(fn->function_name(), slot_literal->literal_on_left);
1681
2
    if (!op.has_value()) {
1682
0
        return false;
1683
0
    }
1684
2
    const auto& literal = slot_literal->literal.get<TYPE_STRING>();
1685
2
    const StringRef literal_ref(literal.data(), literal.size());
1686
10
    for (size_t dictionary_id = 0; dictionary_id < dictionary.size(); ++dictionary_id) {
1687
8
        const int comparison = dictionary.get_data_at(dictionary_id).compare(literal_ref);
1688
8
        (*dictionary_filter)[dictionary_id] &= string_compare_matches(comparison, *op) ? 1 : 0;
1689
8
    }
1690
2
    return true;
1691
2
}
1692
1693
Status build_dictionary_entry_filter(size_t block_position,
1694
                                     const ParquetColumnSchema& column_schema,
1695
                                     const VExprContextSPtrs& conjuncts, const IColumn& dictionary,
1696
                                     IColumn::Filter* dictionary_filter,
1697
48
                                     DictionaryEntryFilterKernel* kernel) {
1698
48
    DORIS_CHECK(dictionary_filter != nullptr);
1699
48
    DORIS_CHECK(kernel != nullptr);
1700
48
    dictionary_filter->clear();
1701
48
    dictionary_filter->resize_fill(dictionary.size(), 1);
1702
48
    *kernel = DictionaryEntryFilterKernel::GENERIC;
1703
    // Block positions are expression slot IDs here; validate the narrowing once so every
1704
    // dictionary evaluation path uses the same representable ID.
1705
48
    const int expression_column_id = cast_set<int>(block_position);
1706
48
    const auto typed_data_type = remove_nullable(column_schema.type);
1707
48
    const uint8_t* raw_values = nullptr;
1708
48
    size_t value_width = 0;
1709
48
    if (std::ranges::all_of(conjuncts,
1710
48
                            [&](const auto& conjunct) {
1711
48
                                return conjunct->root()->can_execute_on_raw_fixed_values(
1712
48
                                        column_schema.type, expression_column_id);
1713
48
                            }) &&
1714
48
        get_typed_dictionary_raw_values(typed_data_type->get_primitive_type(), dictionary,
1715
24
                                        &raw_values, &value_width)) {
1716
        // A dictionary is immutable for the row group, so compare its contiguous typed values once
1717
        // and reuse the resulting id bitmap for every data page.
1718
24
        for (const auto& conjunct : conjuncts) {
1719
24
            RETURN_IF_ERROR(conjunct->root()->execute_on_raw_fixed_values(
1720
24
                    raw_values, dictionary.size(), value_width, column_schema.type,
1721
24
                    expression_column_id, dictionary_filter->data()));
1722
24
        }
1723
24
        *kernel = DictionaryEntryFilterKernel::TYPED_FIXED_WIDTH;
1724
24
        return Status::OK();
1725
24
    }
1726
1727
24
    if (std::ranges::all_of(conjuncts, [&](const auto& conjunct) {
1728
24
            return try_apply_string_dictionary_conjunct(block_position, column_schema.type,
1729
24
                                                        conjunct->root(), dictionary,
1730
24
                                                        dictionary_filter);
1731
24
        })) {
1732
2
        *kernel = DictionaryEntryFilterKernel::TYPED_STRING;
1733
2
        return Status::OK();
1734
2
    }
1735
1736
22
    bool applied_runtime_filters = false;
1737
22
    RETURN_IF_ERROR(try_apply_runtime_filters_to_dictionary(
1738
22
            block_position, column_schema, conjuncts, dictionary, dictionary_filter,
1739
22
            &applied_runtime_filters));
1740
22
    if (applied_runtime_filters) {
1741
3
        *kernel = DictionaryEntryFilterKernel::VECTORIZED_RUNTIME_FILTER;
1742
3
        return Status::OK();
1743
3
    }
1744
1745
19
    dictionary_filter->clear();
1746
19
    dictionary_filter->resize_fill(dictionary.size(), 1);
1747
19
    DictionaryEvalContext ctx;
1748
19
    auto& slot = ctx.slots
1749
19
                         .emplace(expression_column_id,
1750
19
                                  DictionaryEvalContext::SlotDictionary {
1751
19
                                          .data_type = column_schema.type, .values = {}})
1752
19
                         .first->second;
1753
19
    slot.values.reserve(1);
1754
84
    for (size_t dictionary_id = 0; dictionary_id < dictionary.size(); ++dictionary_id) {
1755
65
        Field value;
1756
65
        dictionary.get(dictionary_id, value);
1757
65
        slot.values.clear();
1758
65
        slot.values.push_back(std::move(value));
1759
65
        (*dictionary_filter)[dictionary_id] =
1760
65
                VExprContext::evaluate_dictionary_filter(conjuncts, ctx) ==
1761
65
                                ZoneMapFilterResult::kNoMatch
1762
65
                        ? 0
1763
65
                        : 1;
1764
65
    }
1765
19
    return Status::OK();
1766
22
}
1767
1768
} // namespace
1769
1770
Status ParquetScanScheduler::prepare_current_dictionary_filters(
1771
        ParquetFileContext& file_context,
1772
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
1773
        const format::FileScanRequest& request, int row_group_idx,
1774
288
        const tparquet::RowGroup& row_group_metadata) {
1775
288
    _current_dictionary_filters.clear();
1776
288
    _current_dictionary_residual_conjuncts.clear();
1777
288
    if (request.conjuncts.empty()) {
1778
170
        return Status::OK();
1779
170
    }
1780
118
    detail::PredicateConjunctSchedule schedule;
1781
118
    {
1782
118
        SCOPED_TIMER(_scan_profile.dict_filter_expr_rewrite_time);
1783
118
        schedule = predicate_conjunct_schedule(request);
1784
118
    }
1785
118
    if (schedule.single_column_conjuncts.empty()) {
1786
10
        return Status::OK();
1787
10
    }
1788
1789
108
    SCOPED_TIMER(_scan_profile.dict_filter_rewrite_time);
1790
117
    for (const auto& col : request.predicate_columns) {
1791
117
        const auto local_id = col.column_id();
1792
117
        if (!local_id.is_valid() || local_id.value() >= static_cast<int32_t>(file_schema.size())) {
1793
2
            continue;
1794
2
        }
1795
115
        const auto position_it = request.local_positions.find(col.column_id());
1796
115
        DORIS_CHECK(position_it != request.local_positions.end());
1797
115
        const auto block_position = static_cast<size_t>(position_it->second.value());
1798
115
        const auto conjunct_it = schedule.single_column_conjuncts.find(block_position);
1799
115
        if (conjunct_it == schedule.single_column_conjuncts.end() ||
1800
115
            !can_evaluate_all_with_dictionary(conjunct_it->second)) {
1801
57
            continue;
1802
57
        }
1803
58
        update_counter_if_not_null(_scan_profile.dict_filter_candidate_columns, 1);
1804
1805
        // This optimization is deliberately limited to single-column predicates with a dictionary
1806
        // evaluable part. Mixed AND predicates are split so dictionary-covered children run as a
1807
        // dict-id prefilter and residual children keep the normal row-level expression path.
1808
58
        const auto& column_schema = file_schema[local_id.value()];
1809
58
        DORIS_CHECK(column_schema != nullptr);
1810
58
        if (column_schema->leaf_column_id < 0 ||
1811
58
            column_schema->leaf_column_id >= static_cast<int>(row_group_metadata.columns.size())) {
1812
0
            update_counter_if_not_null(_scan_profile.dict_filter_unsupported_columns, 1);
1813
0
            continue;
1814
0
        }
1815
58
        const auto& column_chunk = row_group_metadata.columns[column_schema->leaf_column_id];
1816
58
        if (!column_chunk.__isset.meta_data ||
1817
58
            !supports_row_level_dictionary_filter(*column_schema, column_chunk.meta_data)) {
1818
10
            update_counter_if_not_null(_scan_profile.dict_filter_unsupported_columns, 1);
1819
10
            continue;
1820
10
        }
1821
1822
48
        std::unique_ptr<ParquetColumnReader> column_reader;
1823
48
        RETURN_IF_ERROR(NativeColumnReader::create(
1824
48
                *column_schema, &col, file_context.native_file, file_context.native_metadata,
1825
48
                row_group_idx, _current_selected_ranges, _current_offset_indexes, _timezone,
1826
48
                _int96_timezone, file_context.native_io_ctx, _runtime_state,
1827
48
                file_context.native_page_cache_enabled, file_context.native_page_cache_file_key,
1828
48
                true, _scan_profile.column_reader_profile, &column_reader));
1829
48
        MutableColumnPtr dictionary_values;
1830
48
        {
1831
48
            SCOPED_TIMER(_scan_profile.dict_filter_read_dict_time);
1832
48
            auto dictionary_result = column_reader->dictionary_values();
1833
48
            if (!dictionary_result.has_value()) {
1834
0
                update_counter_if_not_null(_scan_profile.dict_filter_read_failures, 1);
1835
                // Dictionary filtering is optional: a probe failure must not reject a file that
1836
                // the normal native read path can still decode.
1837
0
                continue;
1838
0
            }
1839
48
            dictionary_values = std::move(dictionary_result).value();
1840
48
        }
1841
1842
        // Build a safe dictionary prefilter from the dictionary-filter interface instead of
1843
        // executing the row expression on a temporary dictionary block. For compound AND,
1844
        // VCompoundPred intentionally evaluates only dictionary-capable children, so residual
1845
        // predicates still run later on surviving rows.
1846
0
        IColumn::Filter dictionary_filter;
1847
48
        OwnedExpressionConjuncts residual_conjuncts;
1848
48
        {
1849
48
            SCOPED_TIMER(_scan_profile.dict_filter_build_time);
1850
48
            DictionaryEntryFilterKernel filter_kernel = DictionaryEntryFilterKernel::GENERIC;
1851
48
            RETURN_IF_ERROR(build_dictionary_entry_filter(block_position, *column_schema,
1852
48
                                                          conjunct_it->second, *dictionary_values,
1853
48
                                                          &dictionary_filter, &filter_kernel));
1854
48
            if (filter_kernel == DictionaryEntryFilterKernel::TYPED_FIXED_WIDTH) {
1855
24
                update_counter_if_not_null(_scan_profile.dict_filter_typed_compare_columns, 1);
1856
24
            } else if (filter_kernel == DictionaryEntryFilterKernel::TYPED_STRING) {
1857
2
                update_counter_if_not_null(_scan_profile.dict_filter_string_compare_columns, 1);
1858
22
            } else if (filter_kernel == DictionaryEntryFilterKernel::VECTORIZED_RUNTIME_FILTER) {
1859
3
                update_counter_if_not_null(
1860
3
                        _scan_profile.dict_filter_vectorized_runtime_filter_columns, 1);
1861
3
            }
1862
48
            residual_conjuncts = build_dictionary_residual_conjuncts(conjunct_it->second);
1863
48
        }
1864
1865
        // The bitmap is keyed by Parquet dictionary id. Later data-page reads evaluate the
1866
        // predicate with an integer lookup and materialize typed values only for surviving rows.
1867
0
        _current_dictionary_filters.emplace(local_id, std::move(dictionary_filter));
1868
48
        _current_dictionary_residual_conjuncts.emplace(local_id, std::move(residual_conjuncts));
1869
48
        _current_predicate_columns.emplace(local_id, std::move(column_reader));
1870
48
        update_counter_if_not_null(_scan_profile.dict_filter_columns, 1);
1871
48
    }
1872
108
    return Status::OK();
1873
108
}
1874
1875
Status ParquetScanScheduler::read_filter_columns(int64_t batch_rows,
1876
                                                 const format::FileScanRequest& request,
1877
                                                 Block* file_block, SelectionVector* selection,
1878
                                                 uint16_t* selected_rows,
1879
                                                 int64_t* conjunct_filtered_rows,
1880
339
                                                 bool* predicate_columns_filtered) {
1881
339
    DORIS_CHECK(predicate_columns_filtered != nullptr);
1882
339
    *predicate_columns_filtered = false;
1883
339
    if (!request.conjuncts.empty() || !request.delete_conjuncts.empty()) {
1884
223
        selection->resize(static_cast<size_t>(batch_rows));
1885
223
    }
1886
339
    const auto& schedule = predicate_conjunct_schedule(request);
1887
339
    std::unordered_set<size_t> residual_predicate_positions;
1888
678
    auto remember_residual_positions = [&](const VExprContextSPtrs& conjuncts) {
1889
678
        for (const auto& conjunct : conjuncts) {
1890
63
            std::set<int> positions;
1891
63
            conjunct->root()->collect_slot_column_ids(positions);
1892
64
            for (const int position : positions) {
1893
64
                if (position >= 0) {
1894
64
                    residual_predicate_positions.insert(cast_set<size_t>(position));
1895
64
                }
1896
64
            }
1897
63
        }
1898
678
    };
1899
339
    remember_residual_positions(schedule.remaining_conjuncts);
1900
339
    remember_residual_positions(request.delete_conjuncts);
1901
339
    const size_t predicate_batch_sequence = _predicate_batch_sequence++;
1902
339
    const bool can_read_predicate_columns_round_by_round = schedule.supports_lazy_materialization;
1903
339
    auto& read_column_positions = _read_column_positions_scratch;
1904
339
    read_column_positions.clear();
1905
339
    read_column_positions.reserve(request.predicate_columns.size());
1906
339
    auto& materialized_positions = _materialized_predicate_positions_scratch;
1907
339
    materialized_positions.clear();
1908
339
    for (auto& rows : _predicate_column_selection_scratch | std::views::values) {
1909
67
        rows.clear();
1910
67
    }
1911
    // A generation becomes dirty only when filtering changes SelectionVector. Columns read after
1912
    // an all-pass stage already share its coordinates, so rewalking every prior mapping is wasted.
1913
339
    bool predicate_columns_need_alignment = false;
1914
1915
410
    auto remember_column_selection = [&](uint32_t position) {
1916
410
        auto& rows = _predicate_column_selection_scratch[position];
1917
410
        rows.resize(*selected_rows);
1918
80.0k
        for (uint16_t row = 0; row < *selected_rows; ++row) {
1919
            // SelectionVector and the scanner batch contract both bound row ordinals to uint16_t;
1920
            // keep the checked conversion explicit when persisting the coordinate mapping.
1921
79.6k
            rows[row] = cast_set<uint16_t>(selection->get_index(row));
1922
79.6k
        }
1923
410
    };
1924
1925
339
    auto compact_predicate_columns = [&](bool discard_predicate_only_payload) -> Status {
1926
334
        bool compacted = false;
1927
334
        int64_t compacted_bytes = 0;
1928
334
        update_counter_if_not_null(_scan_profile.predicate_alignment_columns,
1929
334
                                   cast_set<int64_t>(read_column_positions.size()));
1930
334
        for (const uint32_t position : read_column_positions) {
1931
240
            auto& source_rows = _predicate_column_selection_scratch[position];
1932
240
            const auto& old_column = file_block->get_by_position(position).column;
1933
240
            if (old_column->size() != source_rows.size()) {
1934
0
                return Status::Corruption(
1935
0
                        "Predicate column {} has {} values but {} remembered source rows", position,
1936
0
                        old_column->size(), source_rows.size());
1937
0
            }
1938
240
            bool predicate_only = false;
1939
240
            if (discard_predicate_only_payload) {
1940
233
                predicate_only = std::ranges::any_of(
1941
233
                        request.predicate_only_columns, [&](format::LocalColumnId local_id) {
1942
69
                            const auto position_it = request.local_positions.find(local_id);
1943
69
                            return position_it != request.local_positions.end() &&
1944
69
                                   position_it->second.value() == position;
1945
69
                        });
1946
233
            }
1947
240
            if (predicate_only) {
1948
68
                auto placeholder = old_column->clone_empty();
1949
                // Hidden predicate values are dead after the last filter, but every file-block
1950
                // column must retain the selected row count until TableReader drops hidden slots.
1951
68
                placeholder->insert_many_defaults(*selected_rows);
1952
68
                file_block->replace_by_position(position, std::move(placeholder));
1953
68
                remember_column_selection(position);
1954
68
                continue;
1955
68
            }
1956
172
            bool already_compact = source_rows.size() == *selected_rows &&
1957
172
                                   old_column->size() == static_cast<size_t>(*selected_rows);
1958
2.95k
            for (uint16_t row = 0; already_compact && row < *selected_rows; ++row) {
1959
2.78k
                already_compact = source_rows[row] == selection->get_index(row);
1960
2.78k
            }
1961
172
            if (already_compact) {
1962
73
                continue;
1963
73
            }
1964
99
            auto& filter = _predicate_compaction_filter_scratch;
1965
            // resize_fill() preserves bytes when the next predicate column is smaller. Clear the
1966
            // whole reusable mask so survivors from an earlier coordinate space cannot reappear.
1967
99
            filter.resize(source_rows.size());
1968
99
            std::ranges::fill(filter, 0);
1969
99
            size_t source_idx = 0;
1970
99
            uint16_t selected_idx = 0;
1971
7.53k
            while (source_idx < source_rows.size() && selected_idx < *selected_rows) {
1972
7.43k
                const auto source_row = source_rows[source_idx];
1973
7.43k
                const auto selected_row = selection->get_index(selected_idx);
1974
7.43k
                if (source_row < selected_row) {
1975
5.26k
                    ++source_idx;
1976
5.26k
                    continue;
1977
5.26k
                }
1978
2.17k
                DORIS_CHECK_EQ(source_row, selected_row);
1979
2.17k
                filter[source_idx++] = 1;
1980
2.17k
                ++selected_idx;
1981
2.17k
            }
1982
99
            DORIS_CHECK_EQ(selected_idx, *selected_rows);
1983
99
            compacted_bytes += static_cast<int64_t>(old_column->byte_size());
1984
99
            RETURN_IF_CATCH_EXCEPTION(file_block->replace_by_position(
1985
99
                    position, old_column->filter(filter, *selected_rows)));
1986
99
            remember_column_selection(position);
1987
99
            compacted = true;
1988
99
        }
1989
334
        if (compacted) {
1990
93
            update_counter_if_not_null(_scan_profile.predicate_compaction_bytes, compacted_bytes);
1991
93
            update_counter_if_not_null(_scan_profile.predicate_compaction_count, 1);
1992
93
        }
1993
        // The output path must not apply a batch-coordinate filter to columns that now use compact
1994
        // coordinates. The loop above establishes this invariant even when no bytes moved because
1995
        // every column was already aligned.
1996
334
        *predicate_columns_filtered = !read_column_positions.empty();
1997
334
        return Status::OK();
1998
334
    };
1999
2000
339
    auto read_predicate_column =
2001
339
            [&](ParquetColumnReader* column_reader, size_t block_position,
2002
339
                format::LocalColumnId local_id, const VExprContextSPtrs* single_column_conjuncts,
2003
339
                bool* used_dictionary_filter, bool* used_direct_reader_filter) -> Status {
2004
246
        DORIS_CHECK(used_dictionary_filter != nullptr);
2005
246
        DORIS_CHECK(used_direct_reader_filter != nullptr);
2006
246
        *used_dictionary_filter = false;
2007
246
        *used_direct_reader_filter = false;
2008
        // External table schemas may make required Parquet descendants nullable. Preserve the
2009
        // recursive type and shape checks while ignoring only nullability at every nesting level.
2010
246
        DCHECK(types_equal_ignoring_nested_nullability(
2011
0
                column_reader->type(), file_block->get_by_position(block_position).type))
2012
0
                << column_reader->type()->get_name() << " "
2013
0
                << file_block->get_by_position(block_position).type->get_name() << " "
2014
0
                << column_reader->name() << " " << file_block->get_by_position(block_position).name;
2015
246
        auto column = file_block->get_by_position(block_position).column->assert_mutable();
2016
246
        SCOPED_TIMER(_scan_profile.column_read_time);
2017
246
        const auto dictionary_filter_it = _current_dictionary_filters.find(local_id);
2018
246
        const bool dictionary_predicate_accepts_null =
2019
246
                single_column_conjuncts != nullptr && !single_column_conjuncts->empty() &&
2020
246
                std::ranges::all_of(*single_column_conjuncts, [](const auto& conjunct) {
2021
171
                    return conjunct != nullptr && conjunct->root() != nullptr &&
2022
171
                           conjunct->root()->raw_predicate_result_for_null();
2023
171
                });
2024
246
        if (dictionary_filter_it != _current_dictionary_filters.end() &&
2025
246
            !dictionary_predicate_accepts_null) {
2026
            // Dictionary ids have no entry for a physical NULL. Until an unbound TopN publishes
2027
            // its first bound, keep the materializing residual path so the all-pass invariant can
2028
            // preserve those rows; later batches can resume dictionary-id pruning safely.
2029
48
            const uint16_t selected_rows_before = *selected_rows;
2030
48
            IColumn::Filter compact_filter;
2031
48
            uint16_t new_selected_rows = 0;
2032
48
            bool used_filter = false;
2033
48
            const auto residual_it = _current_dictionary_residual_conjuncts.find(local_id);
2034
48
            const bool has_dictionary_residual =
2035
48
                    residual_it != _current_dictionary_residual_conjuncts.end() &&
2036
48
                    !residual_it->second.empty();
2037
48
            const bool predicate_only =
2038
48
                    request.is_predicate_only(local_id) && !has_dictionary_residual;
2039
            // Dictionary ids are sufficient for predicate-only slots; skipping typed survivor
2040
            // gathers preserves the block row shape without materializing an unobservable payload.
2041
48
            IColumn* projected_column = predicate_only ? nullptr : column.get();
2042
48
            RETURN_IF_ERROR(column_reader->select_with_dictionary_filter(
2043
48
                    *selection, *selected_rows, batch_rows, dictionary_filter_it->second,
2044
48
                    projected_column, &compact_filter, &new_selected_rows, &used_filter));
2045
48
            if (used_filter) {
2046
48
                DORIS_CHECK(compact_filter.size() == selected_rows_before);
2047
48
                DORIS_CHECK(new_selected_rows <= selected_rows_before);
2048
48
                update_counter_if_not_null(_scan_profile.dictionary_predicate_direct_batches, 1);
2049
48
                update_counter_if_not_null(_scan_profile.dictionary_predicate_direct_rows,
2050
48
                                           selected_rows_before);
2051
                // The decoder already observes every keep bit while producing compact_filter, so
2052
                // reuse its count instead of adding another full filter scan at this boundary.
2053
48
                if (!predicate_only) {
2054
39
                    update_counter_if_not_null(_scan_profile.dictionary_predicate_projected_rows,
2055
39
                                               new_selected_rows);
2056
39
                }
2057
48
                const auto filtered_rows = static_cast<int64_t>(selected_rows_before) -
2058
48
                                           static_cast<int64_t>(new_selected_rows);
2059
48
                if (conjunct_filtered_rows != nullptr) {
2060
48
                    *conjunct_filtered_rows += filtered_rows;
2061
48
                }
2062
48
                update_counter_if_not_null(_scan_profile.rows_filtered_by_dict_filter,
2063
48
                                           filtered_rows);
2064
48
                if (new_selected_rows != selected_rows_before) {
2065
                    // The dictionary reader already appended only survivors for this column. Keep
2066
                    // older predicate columns in their original coordinate spaces and compact all
2067
                    // of them once at the expression/output boundary below.
2068
29
                    *selected_rows = apply_compact_filter_to_selection(compact_filter, selection,
2069
29
                                                                       selected_rows_before);
2070
29
                }
2071
48
                if (predicate_only) {
2072
9
                    auto placeholder = column->clone_empty();
2073
9
                    placeholder->insert_many_defaults(*selected_rows);
2074
9
                    file_block->replace_by_position(block_position, std::move(placeholder));
2075
39
                } else {
2076
39
                    file_block->replace_by_position(block_position, std::move(column));
2077
39
                }
2078
48
                read_column_positions.push_back(cast_set<uint32_t>(block_position));
2079
48
                remember_column_selection(cast_set<uint32_t>(block_position));
2080
48
                *used_dictionary_filter = true;
2081
48
                return Status::OK();
2082
48
            }
2083
48
        }
2084
2085
198
        if (single_column_conjuncts != nullptr &&
2086
198
            !residual_predicate_positions.contains(block_position)) {
2087
122
            VExprSPtrs direct_conjuncts;
2088
122
            direct_conjuncts.reserve(single_column_conjuncts->size());
2089
122
            std::ranges::transform(*single_column_conjuncts, std::back_inserter(direct_conjuncts),
2090
126
                                   [](const auto& context) { return context->root(); });
2091
122
            if (!direct_conjuncts.empty()) {
2092
122
                const uint16_t selected_rows_before = *selected_rows;
2093
122
                IColumn::Filter compact_filter;
2094
122
                bool used_filter = false;
2095
122
                DirectPredicateExecutionKind execution_kind = DirectPredicateExecutionKind::NONE;
2096
122
                const bool predicate_only = request.is_predicate_only(local_id);
2097
                // The raw decoder cannot rewind after evaluating encoded fixed-width values.
2098
                // Project survivors in that pass when output still needs the predicate column.
2099
122
                IColumn* projected_column = predicate_only ? nullptr : column.get();
2100
122
                RETURN_IF_ERROR(column_reader->select_with_fixed_width_filter(
2101
122
                        *selection, *selected_rows, batch_rows, direct_conjuncts,
2102
122
                        cast_set<int>(block_position), projected_column, &compact_filter,
2103
122
                        &used_filter, &execution_kind));
2104
120
                if (used_filter) {
2105
58
                    DORIS_CHECK_EQ(compact_filter.size(), selected_rows_before);
2106
58
                    if (execution_kind == DirectPredicateExecutionKind::RAW_FIXED ||
2107
58
                        execution_kind == DirectPredicateExecutionKind::RAW_BINARY ||
2108
58
                        execution_kind == DirectPredicateExecutionKind::CONVERTED_FIXED) {
2109
53
                        update_counter_if_not_null(_scan_profile.raw_value_predicate_direct_batches,
2110
53
                                                   1);
2111
53
                        update_counter_if_not_null(_scan_profile.raw_value_predicate_direct_rows,
2112
53
                                                   selected_rows_before);
2113
53
                    }
2114
58
                    if (execution_kind == DirectPredicateExecutionKind::RAW_FIXED ||
2115
58
                        execution_kind == DirectPredicateExecutionKind::CONVERTED_FIXED) {
2116
42
                        update_counter_if_not_null(
2117
42
                                _scan_profile.fixed_width_predicate_direct_batches, 1);
2118
42
                        update_counter_if_not_null(_scan_profile.fixed_width_predicate_direct_rows,
2119
42
                                                   selected_rows_before);
2120
42
                    }
2121
58
                    const uint16_t new_selected_rows = count_selected_rows(compact_filter);
2122
58
                    const auto filtered_rows = static_cast<int64_t>(selected_rows_before) -
2123
58
                                               static_cast<int64_t>(new_selected_rows);
2124
58
                    if (conjunct_filtered_rows != nullptr) {
2125
58
                        *conjunct_filtered_rows += filtered_rows;
2126
58
                    }
2127
58
                    if (new_selected_rows != selected_rows_before) {
2128
25
                        *selected_rows = apply_compact_filter_to_selection(
2129
25
                                compact_filter, selection, selected_rows_before);
2130
25
                    }
2131
58
                    if (predicate_only) {
2132
                        // This slot is absent from every residual/delete conjunct, so no later
2133
                        // expression can observe its payload. Keep only the block row-shape contract.
2134
54
                        auto placeholder = column->clone_empty();
2135
54
                        placeholder->insert_many_defaults(*selected_rows);
2136
54
                        file_block->replace_by_position(block_position, std::move(placeholder));
2137
54
                    } else {
2138
4
                        file_block->replace_by_position(block_position, std::move(column));
2139
4
                    }
2140
58
                    read_column_positions.push_back(cast_set<uint32_t>(block_position));
2141
58
                    remember_column_selection(cast_set<uint32_t>(block_position));
2142
58
                    *predicate_columns_filtered = true;
2143
58
                    *used_direct_reader_filter = true;
2144
58
                    return Status::OK();
2145
58
                }
2146
2147
62
                RETURN_IF_ERROR(column_reader->select_with_runtime_filter(
2148
62
                        *selection, *selected_rows, batch_rows, *single_column_conjuncts,
2149
62
                        cast_set<int>(block_position), predicate_only ? nullptr : &column,
2150
62
                        &compact_filter, &used_filter));
2151
62
                if (used_filter) {
2152
1
                    DORIS_CHECK_EQ(compact_filter.size(), selected_rows_before);
2153
1
                    update_counter_if_not_null(_scan_profile.typed_runtime_filter_direct_batches,
2154
1
                                               1);
2155
1
                    update_counter_if_not_null(_scan_profile.typed_runtime_filter_direct_rows,
2156
1
                                               selected_rows_before);
2157
1
                    const uint16_t new_selected_rows = count_selected_rows(compact_filter);
2158
1
                    const auto filtered_rows = static_cast<int64_t>(selected_rows_before) -
2159
1
                                               static_cast<int64_t>(new_selected_rows);
2160
1
                    if (conjunct_filtered_rows != nullptr) {
2161
1
                        *conjunct_filtered_rows += filtered_rows;
2162
1
                    }
2163
1
                    if (new_selected_rows != selected_rows_before) {
2164
1
                        *selected_rows = apply_compact_filter_to_selection(
2165
1
                                compact_filter, selection, selected_rows_before);
2166
1
                    }
2167
1
                    if (predicate_only) {
2168
0
                        auto placeholder = column->clone_empty();
2169
0
                        placeholder->insert_many_defaults(*selected_rows);
2170
0
                        file_block->replace_by_position(block_position, std::move(placeholder));
2171
1
                    } else {
2172
1
                        file_block->replace_by_position(block_position, std::move(column));
2173
1
                    }
2174
1
                    read_column_positions.push_back(cast_set<uint32_t>(block_position));
2175
1
                    remember_column_selection(cast_set<uint32_t>(block_position));
2176
1
                    *predicate_columns_filtered = true;
2177
1
                    *used_direct_reader_filter = true;
2178
1
                    return Status::OK();
2179
1
                }
2180
62
            }
2181
122
        }
2182
2183
137
        if (*selected_rows == batch_rows) {
2184
133
            int64_t column_rows = 0;
2185
133
            RETURN_IF_ERROR(column_reader->read(batch_rows, column, &column_rows));
2186
132
            if (column_rows != batch_rows) {
2187
0
                return Status::Corruption(
2188
0
                        "Parquet filter column {} returned {} rows, expected {} rows",
2189
0
                        column_reader->name(), column_rows, batch_rows);
2190
0
            }
2191
132
        } else {
2192
4
            [[maybe_unused]] auto old_size = column->size();
2193
4
            RETURN_IF_ERROR(column_reader->select(*selection, *selected_rows, batch_rows, column));
2194
4
            if (column->size() != old_size + *selected_rows) {
2195
0
                return Status::Corruption(
2196
0
                        "Parquet selected filter column {} returned {} rows, expected {} rows",
2197
0
                        column_reader->name(), column->size(), old_size + *selected_rows);
2198
0
            }
2199
4
            *predicate_columns_filtered = true;
2200
4
        }
2201
136
        file_block->replace_by_position(block_position, std::move(column));
2202
136
        read_column_positions.push_back(cast_set<uint32_t>(block_position));
2203
136
        remember_column_selection(cast_set<uint32_t>(block_position));
2204
136
        return Status::OK();
2205
137
    };
2206
2207
339
    auto execute_scheduled_conjuncts = [&](const VExprContextSPtrs& conjuncts) -> Status {
2208
61
        if (conjuncts.empty() || *selected_rows == 0) {
2209
0
            return Status::OK();
2210
0
        }
2211
61
        const uint16_t selected_rows_before = *selected_rows;
2212
61
        IColumn::Filter compact_filter;
2213
61
        bool can_filter_all = false;
2214
61
        RETURN_IF_ERROR(execute_compact_filter_conjuncts(
2215
61
                conjuncts, selected_rows_before, file_block, &compact_filter, &can_filter_all));
2216
61
        if (can_filter_all) {
2217
30
            compact_filter.resize_fill(selected_rows_before, 0);
2218
30
        }
2219
61
        const uint16_t new_selected_rows = can_filter_all ? 0 : count_selected_rows(compact_filter);
2220
61
        if (conjunct_filtered_rows != nullptr) {
2221
61
            *conjunct_filtered_rows += static_cast<int64_t>(selected_rows_before) -
2222
61
                                       static_cast<int64_t>(new_selected_rows);
2223
61
        }
2224
61
        if (new_selected_rows != selected_rows_before) {
2225
44
            predicate_columns_need_alignment = true;
2226
44
            *selected_rows = can_filter_all
2227
44
                                     ? 0
2228
44
                                     : apply_compact_filter_to_selection(compact_filter, selection,
2229
14
                                                                         selected_rows_before);
2230
44
        }
2231
61
        return Status::OK();
2232
61
    };
2233
2234
339
    auto execute_scheduled_owned_conjuncts =
2235
339
            [&](std::span<const OwnedExpressionConjunct> conjuncts) -> Status {
2236
57
        if (conjuncts.empty() || *selected_rows == 0) {
2237
43
            return Status::OK();
2238
43
        }
2239
14
        const uint16_t selected_rows_before = *selected_rows;
2240
14
        IColumn::Filter compact_filter;
2241
14
        bool can_filter_all = false;
2242
14
        RETURN_IF_ERROR(execute_compact_owned_conjuncts(conjuncts, selected_rows_before, file_block,
2243
14
                                                        &compact_filter, &can_filter_all));
2244
14
        if (can_filter_all) {
2245
2
            compact_filter.resize_fill(selected_rows_before, 0);
2246
2
        }
2247
14
        const uint16_t new_selected_rows = can_filter_all ? 0 : count_selected_rows(compact_filter);
2248
14
        if (conjunct_filtered_rows != nullptr) {
2249
14
            *conjunct_filtered_rows += static_cast<int64_t>(selected_rows_before) -
2250
14
                                       static_cast<int64_t>(new_selected_rows);
2251
14
        }
2252
14
        if (new_selected_rows != selected_rows_before) {
2253
8
            predicate_columns_need_alignment = true;
2254
8
            *selected_rows = can_filter_all
2255
8
                                     ? 0
2256
8
                                     : apply_compact_filter_to_selection(compact_filter, selection,
2257
6
                                                                         selected_rows_before);
2258
8
        }
2259
14
        return Status::OK();
2260
14
    };
2261
2262
339
    auto execute_scheduled_conjuncts_with_profile =
2263
339
            [&](const VExprContextSPtrs& conjuncts) -> Status {
2264
61
        if (_scan_profile.predicate_filter_time == nullptr) {
2265
12
            return execute_scheduled_conjuncts(conjuncts);
2266
12
        }
2267
49
        SCOPED_TIMER(_scan_profile.predicate_filter_time);
2268
49
        return execute_scheduled_conjuncts(conjuncts);
2269
61
    };
2270
2271
339
    auto execute_scheduled_owned_conjuncts_with_profile =
2272
339
            [&](std::span<const OwnedExpressionConjunct> conjuncts) -> Status {
2273
57
        if (_scan_profile.predicate_filter_time == nullptr) {
2274
22
            return execute_scheduled_owned_conjuncts(conjuncts);
2275
22
        }
2276
35
        SCOPED_TIMER(_scan_profile.predicate_filter_time);
2277
35
        return execute_scheduled_owned_conjuncts(conjuncts);
2278
57
    };
2279
2280
339
    auto execute_scheduled_delete_conjuncts = [&]() -> Status {
2281
298
        if (request.delete_conjuncts.empty() || *selected_rows == 0) {
2282
250
            return Status::OK();
2283
250
        }
2284
48
        const uint16_t selected_rows_before = *selected_rows;
2285
48
        IColumn::Filter compact_filter;
2286
48
        bool can_filter_all = false;
2287
48
        RETURN_IF_ERROR(execute_compact_delete_conjuncts(request.delete_conjuncts,
2288
48
                                                         selected_rows_before, file_block,
2289
48
                                                         &compact_filter, &can_filter_all));
2290
48
        if (can_filter_all) {
2291
9
            compact_filter.resize_fill(selected_rows_before, 0);
2292
9
        }
2293
48
        if (can_filter_all || count_selected_rows(compact_filter) != selected_rows_before) {
2294
44
            predicate_columns_need_alignment = true;
2295
44
            *selected_rows = can_filter_all
2296
44
                                     ? 0
2297
44
                                     : apply_compact_filter_to_selection(compact_filter, selection,
2298
35
                                                                         selected_rows_before);
2299
44
        }
2300
48
        return Status::OK();
2301
48
    };
2302
2303
339
    auto read_all_predicate_columns = [&]() -> Status {
2304
10
        for (const auto& [fid, column_reader] : _current_predicate_columns) {
2305
10
            auto position_it = request.local_positions.find(fid);
2306
10
            DORIS_CHECK(position_it != request.local_positions.end());
2307
10
            bool used_dictionary_filter = false;
2308
10
            bool used_direct_reader_filter = false;
2309
10
            RETURN_IF_ERROR(read_predicate_column(column_reader.get(), position_it->second.value(),
2310
10
                                                  fid, nullptr, &used_dictionary_filter,
2311
10
                                                  &used_direct_reader_filter));
2312
10
            materialized_positions.insert(position_it->second.value());
2313
10
        }
2314
5
        return Status::OK();
2315
5
    };
2316
2317
339
    if (!can_read_predicate_columns_round_by_round) {
2318
5
        RETURN_IF_ERROR(read_all_predicate_columns());
2319
5
        if (_scan_profile.predicate_filter_time == nullptr) {
2320
2
            return execute_batch_filters(request, batch_rows, file_block, selection, selected_rows,
2321
2
                                         conjunct_filtered_rows);
2322
2
        }
2323
3
        SCOPED_TIMER(_scan_profile.predicate_filter_time);
2324
3
        return execute_batch_filters(request, batch_rows, file_block, selection, selected_rows,
2325
3
                                     conjunct_filtered_rows);
2326
5
    }
2327
2328
334
    auto read_round_by_round = [&]() -> Status {
2329
        // Single-column conjuncts can be evaluated immediately after their column is read. Once
2330
        // selection shrinks, later predicate columns use ParquetColumnReader::select() so the
2331
        // reader skips rows already rejected by earlier predicates instead of materializing them.
2332
334
        _ordered_predicate_positions_scratch.clear();
2333
334
        _ordered_predicate_positions_scratch.reserve(schedule.single_column_conjuncts.size());
2334
334
        for (const auto& column : request.predicate_columns) {
2335
239
            const size_t position = request.local_positions.at(column.column_id()).value();
2336
239
            if (schedule.single_column_conjuncts.contains(position)) {
2337
                // The request order is the stable cold-start policy until measured costs can
2338
                // reorder predicates; unordered-map iteration can defeat an early selective filter.
2339
173
                _ordered_predicate_positions_scratch.push_back(position);
2340
173
            }
2341
239
        }
2342
334
        _ordered_predicate_positions_scratch = detail::order_adaptive_predicates(
2343
334
                _ordered_predicate_positions_scratch, _predicate_runtime_stats);
2344
334
        const auto& ordered_positions = _ordered_predicate_positions_scratch;
2345
470
        for (size_t order_idx = 0; order_idx < ordered_positions.size(); ++order_idx) {
2346
171
            const size_t position = ordered_positions[order_idx];
2347
171
            const size_t idx = _predicate_indices_by_position_scratch.at(position);
2348
171
            const auto& col = request.predicate_columns[idx];
2349
171
            const auto fid = col.column_id();
2350
171
            auto reader_it = _current_predicate_columns.find(fid);
2351
171
            DORIS_CHECK(reader_it != _current_predicate_columns.end());
2352
171
            auto position_it = request.local_positions.find(col.column_id());
2353
171
            DORIS_CHECK(position_it != request.local_positions.end());
2354
171
            const auto block_position = position_it->second.value();
2355
171
            const uint16_t rows_before = *selected_rows;
2356
171
            auto& stats = _predicate_runtime_stats[position];
2357
171
            const bool sample = detail::should_sample_adaptive_predicate(stats.samples,
2358
171
                                                                         predicate_batch_sequence);
2359
171
            const int64_t start_ns = sample ? MonotonicNanos() : 0;
2360
171
            bool used_dictionary_filter = false;
2361
171
            bool used_direct_reader_filter = false;
2362
171
            const auto conjunct_it = schedule.single_column_conjuncts.find(block_position);
2363
171
            const VExprContextSPtrs* column_conjuncts =
2364
171
                    conjunct_it == schedule.single_column_conjuncts.end() ? nullptr
2365
171
                                                                          : &conjunct_it->second;
2366
171
            RETURN_IF_ERROR(read_predicate_column(reader_it->second.get(), block_position, fid,
2367
171
                                                  column_conjuncts, &used_dictionary_filter,
2368
171
                                                  &used_direct_reader_filter));
2369
168
            materialized_positions.insert(block_position);
2370
168
            if (*selected_rows != 0 && conjunct_it != schedule.single_column_conjuncts.end()) {
2371
167
                if (used_dictionary_filter) {
2372
48
                    const auto residual_it = _current_dictionary_residual_conjuncts.find(fid);
2373
48
                    DORIS_CHECK(residual_it != _current_dictionary_residual_conjuncts.end());
2374
48
                    RETURN_IF_ERROR(
2375
48
                            execute_scheduled_owned_conjuncts_with_profile(residual_it->second));
2376
119
                } else if (!used_direct_reader_filter) {
2377
61
                    RETURN_IF_ERROR(execute_scheduled_conjuncts_with_profile(conjunct_it->second));
2378
61
                }
2379
167
            }
2380
168
            if (*selected_rows != rows_before) {
2381
99
                predicate_columns_need_alignment = true;
2382
99
            }
2383
168
            if (sample) {
2384
134
                const double cost_per_row = static_cast<double>(MonotonicNanos() - start_ns) /
2385
134
                                            std::max<uint16_t>(rows_before, 1);
2386
134
                const double survival =
2387
134
                        static_cast<double>(*selected_rows) / std::max<uint16_t>(rows_before, 1);
2388
134
                constexpr double ADAPTIVE_ALPHA = 0.25;
2389
134
                if (stats.samples == 0) {
2390
101
                    stats.cost_per_input_row_ns = cost_per_row;
2391
101
                    stats.survival_ratio = survival;
2392
101
                } else {
2393
33
                    stats.cost_per_input_row_ns =
2394
33
                            ADAPTIVE_ALPHA * cost_per_row +
2395
33
                            (1 - ADAPTIVE_ALPHA) * stats.cost_per_input_row_ns;
2396
33
                    stats.survival_ratio =
2397
33
                            ADAPTIVE_ALPHA * survival + (1 - ADAPTIVE_ALPHA) * stats.survival_ratio;
2398
33
                }
2399
134
                ++stats.samples;
2400
134
            }
2401
168
            if (*selected_rows != 0) {
2402
136
                continue;
2403
136
            }
2404
32
            return Status::OK();
2405
168
        }
2406
299
        return Status::OK();
2407
334
    };
2408
2409
346
    auto materialize_predicate_positions = [&](const std::vector<size_t>& positions) -> Status {
2410
346
        for (const size_t position : positions) {
2411
256
            if (materialized_positions.contains(position)) {
2412
191
                continue;
2413
191
            }
2414
65
            const auto index_it = _predicate_indices_by_position_scratch.find(position);
2415
65
            DORIS_CHECK(index_it != _predicate_indices_by_position_scratch.end());
2416
65
            const auto fid = request.predicate_columns[index_it->second].column_id();
2417
65
            const auto reader_it = _current_predicate_columns.find(fid);
2418
65
            DORIS_CHECK(reader_it != _current_predicate_columns.end());
2419
65
            bool used_dictionary_filter = false;
2420
65
            bool used_direct_reader_filter = false;
2421
65
            RETURN_IF_ERROR(read_predicate_column(reader_it->second.get(), position, fid, nullptr,
2422
65
                                                  &used_dictionary_filter,
2423
65
                                                  &used_direct_reader_filter));
2424
65
            materialized_positions.insert(position);
2425
65
        }
2426
346
        return Status::OK();
2427
346
    };
2428
2429
334
    auto skip_unmaterialized_predicate_columns = [&]() -> Status {
2430
46
        for (const auto& col : request.predicate_columns) {
2431
46
            const auto position_it = request.local_positions.find(col.column_id());
2432
46
            DORIS_CHECK(position_it != request.local_positions.end());
2433
46
            if (materialized_positions.contains(position_it->second.value())) {
2434
43
                continue;
2435
43
            }
2436
3
            const auto reader_it = _current_predicate_columns.find(col.column_id());
2437
3
            DORIS_CHECK(reader_it != _current_predicate_columns.end());
2438
3
            RETURN_IF_ERROR(reader_it->second->skip(batch_rows));
2439
3
        }
2440
        // Every skipped column has an empty payload in the block. Suppress the caller's
2441
        // batch-coordinate filter because there is no materialized batch-sized column left.
2442
42
        *predicate_columns_filtered = true;
2443
42
        return Status::OK();
2444
42
    };
2445
2446
334
    auto compact_predicate_columns_with_profile =
2447
388
            [&](bool discard_predicate_only_payload) -> Status {
2448
388
        if (!discard_predicate_only_payload && !predicate_columns_need_alignment) {
2449
54
            return Status::OK();
2450
54
        }
2451
334
        const int64_t start_ns = MonotonicNanos();
2452
334
        auto status = compact_predicate_columns(discard_predicate_only_payload);
2453
334
        update_counter_if_not_null(_scan_profile.predicate_compaction_time,
2454
334
                                   MonotonicNanos() - start_ns);
2455
334
        if (status.ok()) {
2456
334
            predicate_columns_need_alignment = false;
2457
334
        }
2458
334
        return status;
2459
388
    };
2460
2461
334
    RETURN_IF_ERROR(read_round_by_round());
2462
331
    if (*selected_rows == 0) {
2463
32
        RETURN_IF_ERROR(skip_unmaterialized_predicate_columns());
2464
32
        return compact_predicate_columns_with_profile(true);
2465
32
    }
2466
2467
    // Complex residuals keep their original conjunct order. Materialize only the columns needed
2468
    // by the next reachable expression, then compact previously read columns into the same row
2469
    // space before evaluating it. This is the scanner-side equivalent of expression-triggered
2470
    // lazy columns: a conjunct that rejects the batch prevents later-only columns from decoding.
2471
299
    for (const auto& stage : schedule.remaining_stages) {
2472
9
        RETURN_IF_ERROR(materialize_predicate_positions(stage.required_positions));
2473
9
        RETURN_IF_ERROR(compact_predicate_columns_with_profile(false));
2474
9
        const OwnedExpressionConjunct stage_conjunct {stage.owner_context, stage.expression};
2475
9
        RETURN_IF_ERROR(execute_scheduled_owned_conjuncts_with_profile(
2476
9
                std::span<const OwnedExpressionConjunct>(&stage_conjunct, 1)));
2477
9
        if (*selected_rows == 0) {
2478
1
            RETURN_IF_ERROR(skip_unmaterialized_predicate_columns());
2479
1
            return compact_predicate_columns_with_profile(true);
2480
1
        }
2481
9
    }
2482
2483
298
    if (!request.delete_conjuncts.empty()) {
2484
48
        std::set<int> delete_positions;
2485
48
        for (const auto& conjunct : request.delete_conjuncts) {
2486
48
            DORIS_CHECK(conjunct != nullptr && conjunct->root() != nullptr);
2487
48
            conjunct->root()->collect_slot_column_ids(delete_positions);
2488
48
        }
2489
48
        std::vector<size_t> required_delete_positions;
2490
48
        required_delete_positions.reserve(delete_positions.size());
2491
48
        for (const int position : delete_positions) {
2492
36
            DORIS_CHECK(position >= 0);
2493
36
            required_delete_positions.push_back(cast_set<size_t>(position));
2494
36
        }
2495
48
        if (required_delete_positions.empty() && !_predicate_positions_scratch.empty()) {
2496
            // An all-literal equality-delete predicate has no slot dependency, but its hidden
2497
            // row-count carrier must still be materialized so the result matches selected_rows.
2498
12
            required_delete_positions.push_back(_predicate_positions_scratch.front());
2499
12
        }
2500
48
        RETURN_IF_ERROR(materialize_predicate_positions(required_delete_positions));
2501
48
        RETURN_IF_ERROR(compact_predicate_columns_with_profile(false));
2502
48
    }
2503
298
    if (_scan_profile.predicate_filter_time == nullptr) {
2504
90
        RETURN_IF_ERROR(execute_scheduled_delete_conjuncts());
2505
208
    } else {
2506
208
        SCOPED_TIMER(_scan_profile.predicate_filter_time);
2507
208
        RETURN_IF_ERROR(execute_scheduled_delete_conjuncts());
2508
208
    }
2509
298
    if (*selected_rows == 0) {
2510
9
        RETURN_IF_ERROR(skip_unmaterialized_predicate_columns());
2511
9
        return compact_predicate_columns_with_profile(true);
2512
9
    }
2513
289
    RETURN_IF_ERROR(materialize_predicate_positions(_predicate_positions_scratch));
2514
289
    return compact_predicate_columns_with_profile(true);
2515
289
}
2516
2517
Status ParquetScanScheduler::prefetch_current_row_group_columns(
2518
        ParquetFileContext& file_context,
2519
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
2520
25
        const std::vector<format::LocalColumnIndex>& scan_columns, bool* prefetched) {
2521
25
    DORIS_CHECK(prefetched != nullptr);
2522
25
    if (_current_merge_range_active || *prefetched || scan_columns.empty() ||
2523
25
        _current_row_group_id < 0 || file_context.native_metadata == nullptr) {
2524
24
        return Status::OK();
2525
24
    }
2526
1
    *prefetched = true;
2527
    // The scanner request separates predicate and non-predicate columns so Parquet can read
2528
    // predicate columns first and lazily materialize the rest. Keep the same contract for
2529
    // prefetch: callers decide which side to warm, and this helper only translates that selected
2530
    // projection into physical column-chunk byte ranges for the current row group.
2531
1
    const auto& metadata = file_context.native_metadata->to_thrift();
2532
1
    const auto compat = native::parquet_reader_compat(
2533
1
            metadata.__isset.created_by ? metadata.created_by : std::string {});
2534
1
    std::vector<ParquetPageCacheRange> ranges;
2535
1
    RETURN_IF_ERROR(detail::build_native_prefetch_ranges(
2536
1
            metadata, file_schema, scan_columns, _current_row_group_id,
2537
1
            file_context.native_file->size(), compat.parquet_816_padding, &ranges));
2538
1
    file_context.prefetch_ranges(ranges, nullptr);
2539
1
    return Status::OK();
2540
1
}
2541
2542
Status ParquetScanScheduler::read_current_row_group_batch(
2543
        ParquetFileContext& file_context,
2544
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, int64_t batch_rows,
2545
        const format::FileScanRequest& request, int64_t batch_first_file_row, Block* file_block,
2546
351
        size_t* rows) {
2547
    // Reader statistics are cumulative plain integers. Publishing their delta recursively for
2548
    // every tiny batch is measurable on wide/nested scans, so flush periodically and force the
2549
    // tail at row-group reset/close.
2550
351
    Defer profile_flush {[this, batch_rows]() {
2551
        // A widened predicate batch can be emitted in several output slices. Its lazy readers
2552
        // have not consumed the whole physical batch until the last slice is drained.
2553
351
        if (_pending_predicate_selection.empty() && finish_current_reader_batch_profiles() &&
2554
351
            _scan_profile.column_reader_profile.page_crossing_batches != nullptr) {
2555
7
            COUNTER_UPDATE(_scan_profile.column_reader_profile.page_crossing_batches, 1);
2556
7
        }
2557
351
        const bool finishes_row_group = _current_range_idx + 1 == _current_selected_ranges.size() &&
2558
351
                                        _current_range_rows_read + batch_rows ==
2559
351
                                                _current_selected_ranges[_current_range_idx].length;
2560
351
        if (++_batches_since_profile_flush >= PROFILE_FLUSH_BATCH_INTERVAL || finishes_row_group) {
2561
290
            flush_current_reader_profiles();
2562
290
            _batches_since_profile_flush = 0;
2563
290
        }
2564
351
    }};
2565
351
    if (_scan_profile.total_batches != nullptr) {
2566
248
        COUNTER_UPDATE(_scan_profile.total_batches, 1);
2567
248
    }
2568
351
    if (_scan_profile.raw_rows_read != nullptr) {
2569
248
        COUNTER_UPDATE(_scan_profile.raw_rows_read, batch_rows);
2570
248
    }
2571
351
    _raw_rows_read += batch_rows;
2572
351
    if (_current_predicate_columns.empty() && _current_non_predicate_columns.empty()) {
2573
12
        *rows = static_cast<size_t>(batch_rows);
2574
12
        materialize_count_star_placeholders(request, *rows, file_block);
2575
12
        if (_scan_profile.selected_rows != nullptr) {
2576
1
            COUNTER_UPDATE(_scan_profile.selected_rows, batch_rows);
2577
1
        }
2578
12
        return Status::OK();
2579
12
    }
2580
339
    auto& selection = _selection;
2581
339
    DORIS_CHECK(batch_rows <= std::numeric_limits<uint16_t>::max());
2582
339
    uint16_t selected_rows = static_cast<uint16_t>(batch_rows);
2583
339
    int64_t conjunct_filtered_rows = 0;
2584
339
    bool predicate_columns_filtered = false;
2585
339
    RETURN_IF_ERROR(read_filter_columns(batch_rows, request, file_block, &selection, &selected_rows,
2586
339
                                        &conjunct_filtered_rows, &predicate_columns_filtered));
2587
335
    _predicate_filtered_rows += conjunct_filtered_rows;
2588
335
    mark_condition_cache_granules(selection, selected_rows, batch_first_file_row);
2589
2590
335
    const bool need_filter_output = selected_rows != batch_rows;
2591
335
    const double batch_survival = static_cast<double>(selected_rows) / batch_rows;
2592
335
    _predicate_survival_ratio = _predicate_survival_ratio < 0
2593
335
                                        ? batch_survival
2594
335
                                        : 0.25 * batch_survival + 0.75 * _predicate_survival_ratio;
2595
335
    if (_scan_profile.selected_rows != nullptr) {
2596
243
        COUNTER_UPDATE(_scan_profile.selected_rows, selected_rows);
2597
243
    }
2598
335
    if (_scan_profile.rows_filtered_by_conjunct != nullptr) {
2599
243
        COUNTER_UPDATE(_scan_profile.rows_filtered_by_conjunct, conjunct_filtered_rows);
2600
243
    }
2601
335
    if (!_current_non_predicate_columns.empty() &&
2602
335
        _scan_profile.lazy_read_filtered_rows != nullptr) {
2603
223
        COUNTER_UPDATE(_scan_profile.lazy_read_filtered_rows, batch_rows - selected_rows);
2604
223
    }
2605
335
    if (selected_rows == 0 && _scan_profile.empty_selection_batches != nullptr) {
2606
42
        COUNTER_UPDATE(_scan_profile.empty_selection_batches, 1);
2607
293
    } else if (static_cast<int64_t>(selected_rows) == batch_rows &&
2608
293
               _scan_profile.dense_batches != nullptr) {
2609
112
        COUNTER_UPDATE(_scan_profile.dense_batches, 1);
2610
181
    } else if (_scan_profile.selected_batches != nullptr) {
2611
89
        COUNTER_UPDATE(_scan_profile.selected_batches, 1);
2612
89
    }
2613
335
    if (need_filter_output && !predicate_columns_filtered) {
2614
4
        IColumn::Filter output_filter = selection_to_filter(selection, selected_rows, batch_rows);
2615
7
        for (const auto& col : request.predicate_columns) {
2616
7
            auto position_it = request.local_positions.find(col.column_id());
2617
7
            DORIS_CHECK(position_it != request.local_positions.end());
2618
7
            const auto block_position = position_it->second.value();
2619
7
            RETURN_IF_CATCH_EXCEPTION(file_block->replace_by_position(
2620
7
                    block_position, file_block->get_by_position(block_position)
2621
7
                                            .column->filter(output_filter, selected_rows)));
2622
7
        }
2623
4
    }
2624
335
    if (selected_rows == 0) {
2625
        // Predicate readers have consumed this physical batch, but touching every lazy column here
2626
        // turns a long rejected prefix into `empty_batches * lazy_columns` native calls. Record only
2627
        // the positional lag. If [0, 32), [32, 64), and [64, 96) are empty, the first surviving
2628
        // batch performs one skip(96) per lazy column. If the row group ends instead, reset drops the
2629
        // lazy readers without flushing because no value from them can be observed.
2630
42
        DORIS_CHECK(_pending_non_predicate_skip_rows <=
2631
42
                    std::numeric_limits<int64_t>::max() - batch_rows);
2632
42
        _pending_non_predicate_skip_rows += batch_rows;
2633
42
        *rows = 0;
2634
42
        return Status::OK();
2635
42
    }
2636
293
    if (!_current_merge_range_active && selected_rows > 0 &&
2637
293
        !_current_non_predicate_columns.empty()) {
2638
        // Do not prefetch lazy output columns until at least one row survives filtering. This is
2639
        // the same decision point where the v2 reader switches from predicate-only reads to
2640
        // materializing non-predicate columns, so fully filtered batches avoid unnecessary IO.
2641
0
        RETURN_IF_ERROR(prefetch_current_row_group_columns(file_context, file_schema,
2642
0
                                                           physical_non_predicate_columns(request),
2643
0
                                                           &_current_non_predicate_prefetched));
2644
0
    }
2645
2646
293
    if (selected_rows > _batch_size) {
2647
4
        DORIS_CHECK(_pending_predicate_selection.empty());
2648
4
        _pending_predicate_batch_rows = batch_rows;
2649
4
        _pending_predicate_batch_rows_consumed = 0;
2650
4
        _pending_predicate_selected_offset = 0;
2651
4
        _pending_predicate_selection.resize(selected_rows);
2652
267
        for (uint16_t idx = 0; idx < selected_rows; ++idx) {
2653
263
            _pending_predicate_selection[idx] =
2654
263
                    static_cast<SelectionVector::Index>(selection.get_index(idx));
2655
263
        }
2656
4
        for (const auto& col : request.predicate_columns) {
2657
4
            const auto position_it = request.local_positions.find(col.column_id());
2658
4
            DORIS_CHECK(position_it != request.local_positions.end());
2659
4
            const size_t block_position = position_it->second.value();
2660
4
            const auto& column = file_block->get_by_position(block_position).column;
2661
4
            DORIS_CHECK_EQ(column->size(), selected_rows);
2662
4
            _pending_predicate_columns.emplace(block_position, column);
2663
4
        }
2664
4
        return materialize_pending_predicate_batch(request, file_block, rows);
2665
4
    }
2666
2667
289
    {
2668
289
        SCOPED_TIMER(_scan_profile.column_read_time);
2669
        // Bring lazy readers to the first row of the current physical batch before interpreting its
2670
        // selection vector. This also merges pending range gaps with fully filtered batches.
2671
289
        RETURN_IF_ERROR(flush_pending_non_predicate_skip_rows());
2672
455
        for (const auto& [fid, column_reader] : _current_non_predicate_columns) {
2673
455
            auto position_it = request.local_positions.find(fid);
2674
455
            DORIS_CHECK(position_it != request.local_positions.end());
2675
455
            const auto block_position = position_it->second.value();
2676
455
            auto column = file_block->get_by_position(block_position).column->assert_mutable();
2677
455
            DCHECK_EQ(file_block->get_by_position(block_position).type->get_primitive_type(),
2678
0
                      column_reader->type()->get_primitive_type())
2679
0
                    << type_to_string(file_block->get_by_position(block_position)
2680
0
                                              .type->get_primitive_type())
2681
0
                    << " " << type_to_string(column_reader->type()->get_primitive_type()) << " "
2682
0
                    << column_reader->name() << " " << fid << " " << block_position;
2683
455
            if (need_filter_output) {
2684
216
                [[maybe_unused]] auto old_size = column->size();
2685
216
                RETURN_IF_ERROR(
2686
216
                        column_reader->select(selection, selected_rows, batch_rows, column));
2687
216
                if (column->size() != old_size + selected_rows) {
2688
0
                    return Status::Corruption(
2689
0
                            "Parquet selected output column {} returned {} rows, expected {} rows",
2690
0
                            column_reader->name(), column->size(), old_size + selected_rows);
2691
0
                }
2692
239
            } else {
2693
239
                int64_t column_rows = 0;
2694
239
                RETURN_IF_ERROR(column_reader->read(batch_rows, column, &column_rows));
2695
239
                if (column_rows != batch_rows) {
2696
0
                    return Status::Corruption(
2697
0
                            "Parquet output column {} returned {} rows, expected {} rows",
2698
0
                            column_reader->name(), column_rows, batch_rows);
2699
0
                }
2700
239
            }
2701
455
            file_block->replace_by_position(block_position, std::move(column));
2702
455
        }
2703
289
    }
2704
289
    materialize_count_star_placeholders(request, selected_rows, file_block);
2705
289
    *rows = static_cast<size_t>(selected_rows);
2706
289
    return Status::OK();
2707
289
}
2708
2709
Status ParquetScanScheduler::materialize_pending_predicate_batch(
2710
16
        const format::FileScanRequest& request, Block* file_block, size_t* rows) {
2711
16
    DORIS_CHECK(!_pending_predicate_selection.empty());
2712
16
    DORIS_CHECK(_pending_predicate_selected_offset < _pending_predicate_selection.size());
2713
16
    const size_t remaining_selected =
2714
16
            _pending_predicate_selection.size() - _pending_predicate_selected_offset;
2715
16
    const size_t output_rows =
2716
16
            std::min<size_t>(static_cast<size_t>(_batch_size), remaining_selected);
2717
16
    const size_t output_end = _pending_predicate_selected_offset + output_rows;
2718
16
    const int64_t physical_end =
2719
16
            output_end == _pending_predicate_selection.size()
2720
16
                    ? _pending_predicate_batch_rows
2721
16
                    : static_cast<int64_t>(_pending_predicate_selection[output_end - 1]) + 1;
2722
16
    DORIS_CHECK(physical_end > _pending_predicate_batch_rows_consumed);
2723
16
    const int64_t physical_rows = physical_end - _pending_predicate_batch_rows_consumed;
2724
2725
16
    _pending_output_selection.resize(output_rows);
2726
281
    for (size_t idx = 0; idx < output_rows; ++idx) {
2727
265
        const int64_t physical_row =
2728
265
                _pending_predicate_selection[_pending_predicate_selected_offset + idx];
2729
265
        DORIS_CHECK(physical_row >= _pending_predicate_batch_rows_consumed);
2730
265
        _pending_output_selection.set_index(
2731
265
                idx, static_cast<SelectionVector::Index>(physical_row -
2732
265
                                                         _pending_predicate_batch_rows_consumed));
2733
265
    }
2734
2735
16
    for (const auto& [block_position, column] : _pending_predicate_columns) {
2736
14
        file_block->replace_by_position(
2737
14
                block_position, column->cut(_pending_predicate_selected_offset, output_rows));
2738
14
    }
2739
16
    {
2740
16
        SCOPED_TIMER(_scan_profile.column_read_time);
2741
16
        RETURN_IF_ERROR(flush_pending_non_predicate_skip_rows());
2742
26
        for (const auto& [fid, column_reader] : _current_non_predicate_columns) {
2743
26
            auto position_it = request.local_positions.find(fid);
2744
26
            DORIS_CHECK(position_it != request.local_positions.end());
2745
26
            const auto block_position = position_it->second.value();
2746
26
            auto column = file_block->get_by_position(block_position).column->assert_mutable();
2747
26
            [[maybe_unused]] const auto old_size = column->size();
2748
26
            RETURN_IF_ERROR(column_reader->select(_pending_output_selection,
2749
26
                                                  static_cast<uint16_t>(output_rows), physical_rows,
2750
26
                                                  column));
2751
26
            if (column->size() != old_size + output_rows) {
2752
0
                return Status::Corruption(
2753
0
                        "Parquet pending output column {} returned {} rows, expected {} rows",
2754
0
                        column_reader->name(), column->size(), old_size + output_rows);
2755
0
            }
2756
26
            file_block->replace_by_position(block_position, std::move(column));
2757
26
        }
2758
16
    }
2759
16
    materialize_count_star_placeholders(request, output_rows, file_block);
2760
16
    *rows = output_rows;
2761
16
    _pending_predicate_batch_rows_consumed = physical_end;
2762
16
    _pending_predicate_selected_offset = output_end;
2763
16
    if (_pending_predicate_selected_offset == _pending_predicate_selection.size()) {
2764
5
        DORIS_CHECK_EQ(_pending_predicate_batch_rows_consumed, _pending_predicate_batch_rows);
2765
5
        if (finish_current_reader_batch_profiles() &&
2766
5
            _scan_profile.column_reader_profile.page_crossing_batches != nullptr) {
2767
1
            COUNTER_UPDATE(_scan_profile.column_reader_profile.page_crossing_batches, 1);
2768
1
        }
2769
5
        _pending_predicate_batch_rows = 0;
2770
5
        _pending_predicate_batch_rows_consumed = 0;
2771
5
        _pending_predicate_selected_offset = 0;
2772
5
        _pending_predicate_selection.clear();
2773
5
        _pending_predicate_columns.clear();
2774
5
        _pending_output_selection.clear();
2775
5
    }
2776
16
    return Status::OK();
2777
16
}
2778
2779
void ParquetScanScheduler::mark_condition_cache_granules(const SelectionVector& selection,
2780
                                                         uint16_t selected_rows,
2781
335
                                                         int64_t batch_first_file_row) {
2782
335
    if (!_condition_cache_ctx || _condition_cache_ctx->is_hit ||
2783
335
        !_condition_cache_ctx->filter_result) {
2784
334
        return;
2785
334
    }
2786
1
    auto& cache = *_condition_cache_ctx->filter_result;
2787
2.04k
    for (uint16_t selection_idx = 0; selection_idx < selected_rows; ++selection_idx) {
2788
2.04k
        const int64_t file_row = batch_first_file_row + selection.get_index(selection_idx);
2789
2.04k
        const int64_t granule = file_row / ConditionCacheContext::GRANULE_SIZE;
2790
2.04k
        const int64_t cache_idx = granule - _condition_cache_ctx->base_granule;
2791
2.04k
        if (cache_idx >= 0 && static_cast<size_t>(cache_idx) < cache.size()) {
2792
2.04k
            cache[static_cast<size_t>(cache_idx)] = true;
2793
2.04k
        }
2794
2.04k
    }
2795
1
}
2796
2797
Status ParquetScanScheduler::read_next_batch(
2798
        ParquetFileContext& file_context,
2799
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, Block* file_block,
2800
459
        size_t* rows, bool* eof) {
2801
459
    DORIS_CHECK(_active_request != nullptr);
2802
459
    *rows = 0;
2803
459
    if (!_pending_predicate_selection.empty()) {
2804
10
        RETURN_IF_ERROR(materialize_pending_predicate_batch(*_active_request, file_block, rows));
2805
10
        *eof = false;
2806
10
        return Status::OK();
2807
10
    }
2808
449
    int64_t predicate_batch_rows = _batch_size;
2809
449
    const int64_t max_predicate_batch_rows = std::min<int64_t>(
2810
449
            std::numeric_limits<uint16_t>::max(),
2811
449
            std::max<int64_t>(DEFAULT_READ_BATCH_SIZE, _runtime_state == nullptr
2812
449
                                                               ? DEFAULT_READ_BATCH_SIZE
2813
449
                                                               : _runtime_state->batch_size()));
2814
449
    auto grow_empty_predicate_batch = [max_predicate_batch_rows](int64_t current) {
2815
42
        for (const int64_t target :
2816
142
             {int64_t {256}, int64_t {1024}, int64_t {4096}, max_predicate_batch_rows}) {
2817
142
            if (current < target) {
2818
11
                return std::min(target, max_predicate_batch_rows);
2819
11
            }
2820
142
        }
2821
31
        return max_predicate_batch_rows;
2822
42
    };
2823
651
    while (true) {
2824
651
        if (!_has_current_row_group) {
2825
428
            activate_pending_scan_request_at_row_group_boundary();
2826
428
            bool has_row_group = false;
2827
428
            RETURN_IF_ERROR(open_next_row_group(file_context, file_schema, *_active_request,
2828
428
                                                &has_row_group));
2829
428
            if (!has_row_group) {
2830
140
                *eof = true;
2831
140
                return Status::OK();
2832
140
            }
2833
428
        }
2834
2835
511
        if (_current_range_idx >= _current_selected_ranges.size()) {
2836
            // Current row group finished, try next row group.
2837
160
            reset_current_row_group();
2838
160
            continue;
2839
160
        }
2840
2841
351
        const RowRange& current_range = _current_selected_ranges[_current_range_idx];
2842
351
        DORIS_CHECK(current_range.start >= 0);
2843
351
        DORIS_CHECK(current_range.length > 0);
2844
351
        DORIS_CHECK(current_range.start + current_range.length <= _current_row_group_rows);
2845
2846
351
        if (_current_row_group_rows_read < current_range.start) {
2847
            // Skip filtered rows according to row group level pruning.
2848
3
            RETURN_IF_ERROR(skip_current_row_group_rows(current_range.start -
2849
3
                                                        _current_row_group_rows_read));
2850
3
        }
2851
351
        DORIS_CHECK(_current_row_group_rows_read == current_range.start + _current_range_rows_read);
2852
351
        const int64_t remaining_rows = current_range.length - _current_range_rows_read;
2853
351
        if (remaining_rows <= 0) {
2854
            // Current range finished, try next range in the same row group.
2855
0
            ++_current_range_idx;
2856
0
            _current_range_rows_read = 0;
2857
0
            continue;
2858
0
        }
2859
2860
351
        const int64_t batch_rows = std::min<int64_t>(predicate_batch_rows, remaining_rows);
2861
351
        const int64_t physical_rows_read = batch_rows;
2862
351
        const int64_t batch_first_file_row =
2863
351
                _current_row_group_first_row + _current_row_group_rows_read;
2864
351
        RETURN_IF_ERROR(read_current_row_group_batch(file_context, file_schema, batch_rows,
2865
351
                                                     *_active_request, batch_first_file_row,
2866
351
                                                     file_block, rows));
2867
347
        _current_row_group_rows_read += physical_rows_read;
2868
347
        _current_range_rows_read += physical_rows_read;
2869
347
        if (_current_range_rows_read >= current_range.length) {
2870
284
            ++_current_range_idx;
2871
284
            _current_range_rows_read = 0;
2872
284
        }
2873
347
        if (*rows == 0) {
2874
            // Fully rejected probes carry no output-width sample. Widen predicate work to cross
2875
            // long empty prefixes cheaply; a later non-empty probe is sliced before lazy columns
2876
            // are materialized, so this internal width cannot escape the caller's row cap.
2877
42
            predicate_batch_rows = grow_empty_predicate_batch(predicate_batch_rows);
2878
42
            continue;
2879
42
        }
2880
305
        *eof = false;
2881
305
        return Status::OK();
2882
347
    }
2883
449
}
2884
2885
} // namespace doris::format::parquet