Coverage Report

Created: 2026-08-16 13:33

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