Coverage Report

Created: 2026-07-27 03:07

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