Coverage Report

Created: 2026-07-29 21:56

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