Coverage Report

Created: 2026-08-04 17:23

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