Coverage Report

Created: 2026-07-23 15:27

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