Coverage Report

Created: 2026-07-29 15:18

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