Coverage Report

Created: 2026-07-24 19:38

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