Coverage Report

Created: 2026-07-07 23:24

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 <limits>
20
#include <memory>
21
#include <ranges>
22
#include <unordered_set>
23
#include <utility>
24
25
#include "common/exception.h"
26
#include "common/status.h"
27
#include "core/assert_cast.h"
28
#include "core/block/block.h"
29
#include "core/column/column_vector.h"
30
#include "exprs/vexpr_context.h"
31
#include "format_v2/parquet/parquet_column_schema.h"
32
#include "format_v2/parquet/parquet_file_context.h"
33
#include "format_v2/parquet/parquet_statistics.h"
34
35
namespace doris::format::parquet {
36
37
namespace {
38
39
200
int64_t column_start_offset(const ::parquet::ColumnChunkMetaData& column_metadata) {
40
200
    return column_metadata.has_dictionary_page()
41
200
                   ? cast_set<int64_t>(column_metadata.dictionary_page_offset())
42
200
                   : cast_set<int64_t>(column_metadata.data_page_offset());
43
200
}
44
45
void collect_all_leaf_column_ids(const ParquetColumnSchema& column_schema,
46
148
                                 std::unordered_set<int>* leaf_column_ids) {
47
148
    DORIS_CHECK(leaf_column_ids != nullptr);
48
148
    if (column_schema.kind == ParquetColumnSchemaKind::PRIMITIVE) {
49
140
        if (column_schema.leaf_column_id >= 0) {
50
140
            leaf_column_ids->insert(column_schema.leaf_column_id);
51
140
        }
52
140
        return;
53
140
    }
54
12
    for (const auto& child : column_schema.children) {
55
12
        DORIS_CHECK(child != nullptr);
56
12
        collect_all_leaf_column_ids(*child, leaf_column_ids);
57
12
    }
58
8
}
59
60
void collect_projected_leaf_column_ids(const ParquetColumnSchema& column_schema,
61
                                       const format::LocalColumnIndex& projection,
62
142
                                       std::unordered_set<int>* leaf_column_ids) {
63
142
    DORIS_CHECK(leaf_column_ids != nullptr);
64
142
    if (projection.project_all_children || projection.children.empty()) {
65
136
        collect_all_leaf_column_ids(column_schema, leaf_column_ids);
66
136
        return;
67
136
    }
68
7
    for (const auto& child_projection : projection.children) {
69
7
        const auto child_it =
70
11
                std::ranges::find_if(column_schema.children, [&](const auto& child_schema) {
71
11
                    return child_schema->local_id == child_projection.local_id();
72
11
                });
73
7
        DORIS_CHECK(child_it != column_schema.children.end());
74
7
        collect_projected_leaf_column_ids(**child_it, child_projection, leaf_column_ids);
75
7
    }
76
6
}
77
78
bool is_row_group_outside_range(const ::parquet::FileMetaData& metadata,
79
204
                                const ParquetScanRange& scan_range, int row_group_idx) {
80
204
    if (scan_range.size < 0) {
81
174
        return false;
82
174
    }
83
30
    const int64_t range_start_offset = scan_range.start_offset;
84
30
    const int64_t range_end_offset = range_start_offset + scan_range.size;
85
30
    DORIS_CHECK(range_start_offset >= 0);
86
30
    DORIS_CHECK(range_end_offset >= range_start_offset);
87
30
    if (range_start_offset == 0 &&
88
30
        (scan_range.file_size < 0 || range_end_offset >= scan_range.file_size)) {
89
0
        return false;
90
0
    }
91
92
30
    auto row_group_metadata = metadata.RowGroup(row_group_idx);
93
30
    DORIS_CHECK(row_group_metadata != nullptr);
94
30
    DORIS_CHECK(row_group_metadata->num_columns() > 0);
95
30
    const auto first_column = row_group_metadata->ColumnChunk(0);
96
30
    const auto last_column = row_group_metadata->ColumnChunk(row_group_metadata->num_columns() - 1);
97
30
    DORIS_CHECK(first_column != nullptr);
98
30
    DORIS_CHECK(last_column != nullptr);
99
30
    const int64_t row_group_start_offset = column_start_offset(*first_column);
100
30
    const int64_t row_group_end_offset =
101
30
            column_start_offset(*last_column) + last_column->total_compressed_size();
102
30
    const int64_t row_group_mid_offset =
103
30
            row_group_start_offset + (row_group_end_offset - row_group_start_offset) / 2;
104
30
    return row_group_mid_offset < range_start_offset || row_group_mid_offset >= range_end_offset;
105
30
}
106
107
98
std::vector<format::LocalColumnIndex> request_scan_columns(const format::FileScanRequest& request) {
108
98
    std::vector<format::LocalColumnIndex> scan_columns;
109
98
    scan_columns.reserve(request.predicate_columns.size() + request.non_predicate_columns.size());
110
98
    scan_columns.insert(scan_columns.end(), request.predicate_columns.begin(),
111
98
                        request.predicate_columns.end());
112
98
    scan_columns.insert(scan_columns.end(), request.non_predicate_columns.begin(),
113
98
                        request.non_predicate_columns.end());
114
98
    return scan_columns;
115
98
}
116
117
std::vector<ParquetPageCacheRange> build_row_group_prefetch_ranges(
118
        const ::parquet::FileMetaData& metadata,
119
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
120
98
        const std::vector<format::LocalColumnIndex>& scan_columns, int row_group_idx) {
121
98
    std::unordered_set<int> leaf_column_ids;
122
162
    for (const auto& projection : scan_columns) {
123
162
        const auto local_id = projection.local_id();
124
162
        if (local_id == format::ROW_POSITION_COLUMN_ID ||
125
162
            local_id == format::GLOBAL_ROWID_COLUMN_ID) {
126
27
            continue;
127
27
        }
128
135
        DORIS_CHECK(local_id >= 0 && local_id < static_cast<int32_t>(file_schema.size()));
129
135
        DORIS_CHECK(file_schema[local_id] != nullptr);
130
        // Prefetch and merge-reader ranges must be physical leaf chunks, not Doris logical slots.
131
        // Example: for a struct column s<a:int,b:string>, projecting only s.a should include only
132
        // the Parquet leaf chunk of a. Projecting the whole struct includes both a and b.
133
135
        collect_projected_leaf_column_ids(*file_schema[local_id], projection, &leaf_column_ids);
134
135
    }
135
136
98
    auto row_group_metadata = metadata.RowGroup(row_group_idx);
137
98
    DORIS_CHECK(row_group_metadata != nullptr);
138
98
    std::vector<int> ordered_leaf_column_ids(leaf_column_ids.begin(), leaf_column_ids.end());
139
98
    std::ranges::sort(ordered_leaf_column_ids);
140
141
98
    std::vector<ParquetPageCacheRange> ranges;
142
98
    ranges.reserve(ordered_leaf_column_ids.size());
143
140
    for (const auto leaf_column_id : ordered_leaf_column_ids) {
144
140
        DORIS_CHECK(leaf_column_id >= 0 && leaf_column_id < row_group_metadata->num_columns());
145
140
        auto column_metadata = row_group_metadata->ColumnChunk(leaf_column_id);
146
140
        DORIS_CHECK(column_metadata != nullptr);
147
140
        const int64_t offset = column_start_offset(*column_metadata);
148
140
        const int64_t size = column_metadata->total_compressed_size();
149
140
        DORIS_CHECK(offset >= 0);
150
140
        if (size > 0) {
151
140
            ranges.push_back(ParquetPageCacheRange {.offset = offset, .size = size});
152
140
        }
153
140
    }
154
98
    return ranges;
155
98
}
156
157
Status select_row_groups_by_scan_range(const ::parquet::FileMetaData& metadata,
158
                                       const ParquetScanRange& scan_range,
159
                                       std::vector<int64_t>* row_group_first_rows,
160
118
                                       std::vector<int>* selected_row_groups) {
161
118
    DORIS_CHECK(row_group_first_rows != nullptr);
162
118
    DORIS_CHECK(selected_row_groups != nullptr);
163
118
    row_group_first_rows->assign(metadata.num_row_groups(), 0);
164
118
    selected_row_groups->clear();
165
118
    selected_row_groups->reserve(metadata.num_row_groups());
166
118
    int64_t next_row_group_first_row = 0;
167
322
    for (int row_group_idx = 0; row_group_idx < metadata.num_row_groups(); ++row_group_idx) {
168
204
        (*row_group_first_rows)[row_group_idx] = next_row_group_first_row;
169
204
        auto row_group_metadata = metadata.RowGroup(row_group_idx);
170
204
        DORIS_CHECK(row_group_metadata != nullptr);
171
204
        const int64_t row_group_rows = row_group_metadata->num_rows();
172
204
        if (row_group_rows < 0) {
173
0
            return Status::Corruption("Invalid negative row count in parquet row group {}",
174
0
                                      row_group_idx);
175
0
        }
176
204
        next_row_group_first_row += row_group_rows;
177
204
        if (!is_row_group_outside_range(metadata, scan_range, row_group_idx)) {
178
183
            selected_row_groups->push_back(row_group_idx);
179
183
        }
180
204
    }
181
118
    return Status::OK();
182
118
}
183
184
Status build_row_group_read_plans(
185
        const ::parquet::FileMetaData& metadata, ::parquet::ParquetFileReader* file_reader,
186
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
187
        const format::FileScanRequest& request, const std::vector<int>& selected_row_groups,
188
        const std::vector<int64_t>& row_group_first_rows, RowGroupScanPlan* plan,
189
118
        const cctz::time_zone* timezone, const RuntimeState* runtime_state) {
190
118
    DORIS_CHECK(plan != nullptr);
191
118
    plan->row_groups.reserve(selected_row_groups.size());
192
147
    for (const auto row_group_idx : selected_row_groups) {
193
147
        DORIS_CHECK(row_group_idx >= 0);
194
147
        DORIS_CHECK(static_cast<size_t>(row_group_idx) < row_group_first_rows.size());
195
147
        auto row_group_metadata = metadata.RowGroup(row_group_idx);
196
147
        DORIS_CHECK(row_group_metadata != nullptr);
197
147
        const int64_t row_group_rows = row_group_metadata->num_rows();
198
147
        if (row_group_rows == 0) {
199
0
            continue;
200
0
        }
201
202
147
        RowGroupReadPlan row_group_plan;
203
147
        row_group_plan.row_group_id = row_group_idx;
204
147
        row_group_plan.first_file_row = row_group_first_rows[row_group_idx];
205
147
        row_group_plan.row_group_rows = row_group_rows;
206
147
        RETURN_IF_ERROR(select_row_group_ranges_by_page_index(
207
147
                file_reader, file_schema, request, row_group_idx, row_group_rows,
208
147
                &row_group_plan.selected_ranges, &row_group_plan.page_skip_plans,
209
147
                &plan->pruning_stats, timezone, runtime_state));
210
147
        if (row_group_plan.selected_ranges.empty()) {
211
1
            continue;
212
1
        }
213
146
        plan->pruning_stats.selected_row_ranges += row_group_plan.selected_ranges.size();
214
146
        plan->row_groups.push_back(std::move(row_group_plan));
215
146
    }
216
118
    return Status::OK();
217
118
}
218
219
} // namespace
220
221
Status plan_parquet_row_groups(const ::parquet::FileMetaData& metadata,
222
                               ::parquet::ParquetFileReader* file_reader,
223
                               const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
224
                               const format::FileScanRequest& request,
225
                               const ParquetScanRange& scan_range, bool enable_bloom_filter,
226
                               RowGroupScanPlan* plan, const cctz::time_zone* timezone,
227
118
                               const RuntimeState* runtime_state) {
228
118
    DORIS_CHECK(plan != nullptr);
229
118
    plan->row_groups.clear();
230
118
    plan->pruning_stats = ParquetPruningStats {};
231
232
    // Row-group planning flow:
233
    //
234
    //     parquet footer row groups
235
    //              |
236
    //              v
237
    //     split byte-range candidates
238
    //              |
239
    //              v
240
    //     row-group metadata pruning
241
    //     statistics/ZoneMap -> dictionary -> bloom filter
242
    //              |
243
    //              v
244
    //     page-index pruning per selected row group
245
    //              |
246
    //              v
247
    //     RowGroupReadPlan with selected row ranges
248
    //
249
    // Metadata pruning removes whole row groups before readers are opened. Page index pruning runs
250
    // only for remaining row groups and produces selected row ranges; the scan scheduler later skips
251
    // gaps between those ranges, while row-level VExpr conjuncts still run on loaded batches for
252
    // correctness.
253
118
    std::vector<int64_t> row_group_first_rows;
254
118
    std::vector<int> scan_range_selected_row_groups;
255
118
    RETURN_IF_ERROR(select_row_groups_by_scan_range(metadata, scan_range, &row_group_first_rows,
256
118
                                                    &scan_range_selected_row_groups));
257
258
118
    std::vector<int> metadata_selected_row_groups;
259
118
    RETURN_IF_ERROR(select_row_groups_by_metadata(
260
118
            metadata, file_reader, file_schema, request, &scan_range_selected_row_groups,
261
118
            &metadata_selected_row_groups, enable_bloom_filter, &plan->pruning_stats, timezone,
262
118
            runtime_state));
263
264
118
    RETURN_IF_ERROR(build_row_group_read_plans(metadata, file_reader, file_schema, request,
265
118
                                               metadata_selected_row_groups, row_group_first_rows,
266
118
                                               plan, timezone, runtime_state));
267
118
    plan->pruning_stats.selected_row_groups = plan->row_groups.size();
268
118
    return Status::OK();
269
118
}
270
271
namespace {
272
273
uint16_t apply_filter_to_selection(const IColumn::Filter& filter, SelectionVector* selection,
274
48
                                   uint16_t selected_rows) {
275
48
    uint16_t new_selected_rows = 0;
276
6.44k
    for (uint16_t selection_idx = 0; selection_idx < selected_rows; ++selection_idx) {
277
6.39k
        const auto row_idx = selection->get_index(selection_idx);
278
6.39k
        if (filter[row_idx] != 0) {
279
4.30k
            selection->set_index(new_selected_rows++, static_cast<SelectionVector::Index>(row_idx));
280
4.30k
        }
281
6.39k
    }
282
48
    return new_selected_rows;
283
48
}
284
285
Status execute_filter_conjuncts(const format::FileScanRequest& request, int64_t batch_rows,
286
                                Block* file_block, SelectionVector* selection,
287
48
                                uint16_t* selected_rows) {
288
48
    for (const auto& conjunct : request.conjuncts) {
289
36
        if (*selected_rows == 0) {
290
0
            break;
291
0
        }
292
36
        DORIS_CHECK(conjunct != nullptr);
293
36
        IColumn::Filter filter(static_cast<size_t>(batch_rows), 1);
294
36
        bool can_filter_all = false;
295
36
        RETURN_IF_ERROR(conjunct->execute_filter(file_block, filter.data(),
296
36
                                                 static_cast<size_t>(batch_rows), false,
297
36
                                                 &can_filter_all));
298
36
        *selected_rows =
299
36
                can_filter_all ? 0 : apply_filter_to_selection(filter, selection, *selected_rows);
300
36
    }
301
48
    return Status::OK();
302
48
}
303
304
Status execute_delete_conjuncts(const format::FileScanRequest& request, int64_t batch_rows,
305
                                Block* file_block, SelectionVector* selection,
306
47
                                uint16_t* selected_rows) {
307
47
    for (const auto& delete_conjunct : request.delete_conjuncts) {
308
13
        if (*selected_rows == 0) {
309
0
            break;
310
0
        }
311
13
        DORIS_CHECK(delete_conjunct != nullptr);
312
13
        int result_column_id = -1;
313
13
        RETURN_IF_ERROR(delete_conjunct->root()->execute(delete_conjunct.get(), file_block,
314
13
                                                         &result_column_id));
315
13
        DORIS_CHECK(result_column_id >= 0 &&
316
13
                    result_column_id < static_cast<int>(file_block->columns()));
317
13
        const auto& delete_filter = assert_cast<const ColumnUInt8&>(
318
13
                                            *file_block->get_by_position(result_column_id).column)
319
13
                                            .get_data();
320
13
        DORIS_CHECK(delete_filter.size() == static_cast<size_t>(batch_rows));
321
13
        IColumn::Filter keep_filter(static_cast<size_t>(batch_rows), 1);
322
13
        bool has_kept_row = false;
323
64
        for (size_t row = 0; row < static_cast<size_t>(batch_rows); ++row) {
324
51
            keep_filter[row] = !delete_filter[row];
325
51
            has_kept_row |= keep_filter[row] != 0;
326
51
        }
327
13
        file_block->erase(result_column_id);
328
13
        *selected_rows =
329
13
                !has_kept_row ? 0
330
13
                              : apply_filter_to_selection(keep_filter, selection, *selected_rows);
331
13
    }
332
47
    return Status::OK();
333
47
}
334
335
} // namespace
336
337
IColumn::Filter selection_to_filter(const SelectionVector& selection, uint16_t selected_rows,
338
28
                                    int64_t batch_rows) {
339
28
    IColumn::Filter filter(static_cast<size_t>(batch_rows), 0);
340
2.13k
    for (uint16_t selection_idx = 0; selection_idx < selected_rows; ++selection_idx) {
341
2.10k
        filter[selection.get_index(selection_idx)] = 1;
342
2.10k
    }
343
28
    return filter;
344
28
}
345
346
Status execute_batch_filters(const format::FileScanRequest& request, int64_t batch_rows,
347
                             Block* file_block, SelectionVector* selection, uint16_t* selected_rows,
348
91
                             int64_t* conjunct_filtered_rows) {
349
91
    if (request.conjuncts.empty() && request.delete_conjuncts.empty()) {
350
43
        return Status::OK();
351
43
    }
352
48
    const auto selected_rows_before_conjunct = *selected_rows;
353
48
    RETURN_IF_ERROR(
354
48
            execute_filter_conjuncts(request, batch_rows, file_block, selection, selected_rows));
355
48
    if (conjunct_filtered_rows != nullptr) {
356
48
        *conjunct_filtered_rows += static_cast<int64_t>(selected_rows_before_conjunct) -
357
48
                                   static_cast<int64_t>(*selected_rows);
358
48
    }
359
48
    if (*selected_rows == 0) {
360
1
        return Status::OK();
361
1
    }
362
47
    return execute_delete_conjuncts(request, batch_rows, file_block, selection, selected_rows);
363
48
}
364
365
namespace {
366
2
int64_t count_range_rows(const std::vector<RowRange>& ranges) {
367
2
    int64_t rows = 0;
368
2
    for (const auto& range : ranges) {
369
2
        rows += range.length;
370
2
    }
371
2
    return rows;
372
2
}
373
374
void append_intersection(const RowRange& left, const RowRange& right,
375
1
                         std::vector<RowRange>* result) {
376
1
    const int64_t start = std::max(left.start, right.start);
377
1
    const int64_t end = std::min(left.start + left.length, right.start + right.length);
378
1
    if (start < end) {
379
1
        result->push_back(RowRange {.start = start, .length = end - start});
380
1
    }
381
1
}
382
383
std::vector<RowRange> filter_ranges_by_condition_cache(const std::vector<RowRange>& ranges,
384
                                                       const std::vector<bool>& cache,
385
                                                       int64_t row_group_first_row,
386
1
                                                       int64_t base_granule) {
387
1
    std::vector<RowRange> result;
388
1
    if (cache.empty()) {
389
0
        return ranges;
390
0
    }
391
392
    // Cache coordinates are file-global granules; RowRange coordinates are row-group-relative.
393
    // Walk every selected range in order and split it by granule. Granules covered by the bitmap
394
    // are kept only when the bit is true. Granules outside the bitmap are kept conservatively, so
395
    // an undersized or old-format cache entry cannot skip valid rows.
396
1
    for (const auto& range : ranges) {
397
1
        const int64_t global_start = row_group_first_row + range.start;
398
1
        const int64_t global_end = global_start + range.length;
399
1
        for (int64_t granule = global_start / ConditionCacheContext::GRANULE_SIZE;
400
3
             granule <= (global_end - 1) / ConditionCacheContext::GRANULE_SIZE; ++granule) {
401
2
            const int64_t cache_idx = granule - base_granule;
402
2
            const bool keep = cache_idx < 0 || static_cast<size_t>(cache_idx) >= cache.size() ||
403
2
                              cache[static_cast<size_t>(cache_idx)];
404
2
            if (!keep) {
405
1
                continue;
406
1
            }
407
1
            const int64_t granule_start = granule * ConditionCacheContext::GRANULE_SIZE;
408
1
            const int64_t granule_end = granule_start + ConditionCacheContext::GRANULE_SIZE;
409
1
            const RowRange file_granule_range {.start = granule_start - row_group_first_row,
410
1
                                               .length = granule_end - granule_start};
411
1
            append_intersection(range, file_granule_range, &result);
412
1
        }
413
1
    }
414
1
    return result;
415
1
}
416
417
} // namespace
418
419
106
void ParquetScanScheduler::set_plan(RowGroupScanPlan plan) {
420
106
    _row_group_plans = std::move(plan.row_groups);
421
106
    _condition_cache_filtered_rows = 0;
422
106
    _predicate_filtered_rows = 0;
423
106
    reset();
424
106
}
425
426
2
void ParquetScanScheduler::set_condition_cache_context(std::shared_ptr<ConditionCacheContext> ctx) {
427
2
    _condition_cache_ctx = std::move(ctx);
428
2
    if (!_condition_cache_ctx || !_condition_cache_ctx->filter_result || _row_group_plans.empty()) {
429
0
        return;
430
0
    }
431
432
2
    _condition_cache_ctx->base_granule =
433
2
            _row_group_plans.front().first_file_row / ConditionCacheContext::GRANULE_SIZE;
434
2
    if (!_condition_cache_ctx->is_hit) {
435
1
        return;
436
1
    }
437
438
1
    std::vector<RowGroupReadPlan> filtered_plans;
439
1
    filtered_plans.reserve(_row_group_plans.size());
440
1
    for (auto& plan : _row_group_plans) {
441
1
        const int64_t old_rows = count_range_rows(plan.selected_ranges);
442
1
        plan.selected_ranges = filter_ranges_by_condition_cache(
443
1
                plan.selected_ranges, *_condition_cache_ctx->filter_result, plan.first_file_row,
444
1
                _condition_cache_ctx->base_granule);
445
1
        const int64_t new_rows = count_range_rows(plan.selected_ranges);
446
1
        _condition_cache_filtered_rows += old_rows - new_rows;
447
1
        if (!plan.selected_ranges.empty()) {
448
1
            filtered_plans.push_back(std::move(plan));
449
1
        }
450
1
    }
451
1
    _row_group_plans = std::move(filtered_plans);
452
1
    reset();
453
1
}
454
455
107
void ParquetScanScheduler::reset() {
456
107
    _next_row_group_plan_idx = 0;
457
107
    _raw_rows_read = 0;
458
107
    reset_current_row_group();
459
107
}
460
461
157
void ParquetScanScheduler::reset_current_row_group() {
462
157
    _current_row_group.reset();
463
157
    _current_predicate_columns.clear();
464
157
    _current_non_predicate_columns.clear();
465
157
    _current_row_group_rows = 0;
466
157
    _current_row_group_id = -1;
467
157
    _current_row_group_rows_read = 0;
468
157
    _current_row_group_first_row = 0;
469
157
    _current_selected_ranges.clear();
470
157
    _current_range_idx = 0;
471
157
    _current_range_rows_read = 0;
472
157
    _current_predicate_prefetched = false;
473
157
    _current_non_predicate_prefetched = false;
474
157
    _current_merge_range_active = false;
475
157
}
476
477
Status ParquetScanScheduler::open_next_row_group(
478
        ParquetFileContext& file_context,
479
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
480
137
        const format::FileScanRequest& request, bool* has_row_group) {
481
137
    *has_row_group = false;
482
137
    if (_next_row_group_plan_idx >= _row_group_plans.size()) {
483
39
        return Status::OK();
484
39
    }
485
98
    const RowGroupReadPlan& row_group_plan = _row_group_plans[_next_row_group_plan_idx++];
486
98
    const int row_group_idx = row_group_plan.row_group_id;
487
98
    _current_merge_range_active =
488
98
            prepare_current_row_group_reader(file_context, file_schema, request, row_group_idx);
489
98
    try {
490
98
        _current_row_group = file_context.file_reader->RowGroup(row_group_idx);
491
98
    } catch (const ::parquet::ParquetException& e) {
492
0
        return Status::Corruption("Failed to open parquet row group {}: {}", row_group_idx,
493
0
                                  e.what());
494
0
    } catch (const std::exception& e) {
495
0
        return Status::InternalError("Failed to open parquet row group {}: {}", row_group_idx,
496
0
                                     e.what());
497
0
    }
498
499
98
    auto row_group_metadata = file_context.metadata->RowGroup(row_group_idx);
500
98
    DORIS_CHECK(row_group_metadata != nullptr);
501
98
    _current_row_group_rows = row_group_metadata->num_rows();
502
98
    DORIS_CHECK(_current_row_group_rows == row_group_plan.row_group_rows);
503
98
    DORIS_CHECK(_current_row_group_rows > 0);
504
98
    _current_row_group_id = row_group_idx;
505
98
    DORIS_CHECK(!row_group_plan.selected_ranges.empty());
506
98
    _current_row_group_first_row = row_group_plan.first_file_row;
507
98
    _current_row_group_rows_read = 0;
508
98
    _current_selected_ranges = row_group_plan.selected_ranges;
509
98
    _current_range_idx = 0;
510
98
    _current_range_rows_read = 0;
511
98
    _current_predicate_columns.clear();
512
98
    _current_non_predicate_columns.clear();
513
514
98
    ParquetColumnReaderFactory column_reader_factory(
515
98
            _current_row_group, file_context.schema->num_columns(), &row_group_plan.page_skip_plans,
516
98
            _page_skip_profile, _timezone, _enable_strict_mode,
517
98
            _scan_profile.column_reader_profile);
518
98
    for (const auto& col : request.predicate_columns) {
519
52
        const auto local_id = col.local_id();
520
52
        if (local_id == format::ROW_POSITION_COLUMN_ID) {
521
11
            _current_predicate_columns[local_id] =
522
11
                    column_reader_factory.create_row_position_column_reader(
523
11
                            _current_row_group_first_row);
524
11
            continue;
525
11
        }
526
41
        if (local_id == format::GLOBAL_ROWID_COLUMN_ID) {
527
0
            DORIS_CHECK(_global_rowid_context.has_value());
528
0
            _current_predicate_columns[local_id] =
529
0
                    column_reader_factory.create_global_rowid_column_reader(
530
0
                            *_global_rowid_context, _current_row_group_first_row);
531
0
            continue;
532
0
        }
533
534
41
        DORIS_CHECK(local_id >= 0 && local_id < static_cast<int32_t>(file_schema.size()));
535
41
        const auto& column_schema = file_schema[local_id];
536
41
        DORIS_CHECK(column_schema != nullptr);
537
41
        std::unique_ptr<ParquetColumnReader> column_reader;
538
41
        RETURN_IF_ERROR(column_reader_factory.create(*column_schema, &col, &column_reader));
539
41
        _current_predicate_columns[local_id] = std::move(column_reader);
540
41
    }
541
    // Start warming filter-column chunks as soon as their row group is selected. Parquet v2 still
542
    // reads through Arrow's random-access reader; this prefetch only warms Doris file cache blocks
543
    // in the background and never changes the row/column materialization order.
544
98
    if (!_current_merge_range_active) {
545
11
        prefetch_current_row_group_columns(file_context, file_schema, request.predicate_columns,
546
11
                                           &_current_predicate_prefetched);
547
11
    }
548
110
    for (const auto& col : request.non_predicate_columns) {
549
110
        const auto local_id = col.local_id();
550
110
        if (local_id == format::ROW_POSITION_COLUMN_ID) {
551
14
            _current_non_predicate_columns[local_id] =
552
14
                    column_reader_factory.create_row_position_column_reader(
553
14
                            _current_row_group_first_row);
554
14
            continue;
555
14
        }
556
96
        if (local_id == format::GLOBAL_ROWID_COLUMN_ID) {
557
2
            DORIS_CHECK(_global_rowid_context.has_value());
558
2
            _current_non_predicate_columns[local_id] =
559
2
                    column_reader_factory.create_global_rowid_column_reader(
560
2
                            *_global_rowid_context, _current_row_group_first_row);
561
2
            continue;
562
2
        }
563
94
        DORIS_CHECK(local_id >= 0 && local_id < static_cast<int32_t>(file_schema.size()));
564
94
        const auto& column_schema = file_schema[local_id];
565
94
        DORIS_CHECK(column_schema != nullptr);
566
94
        std::unique_ptr<ParquetColumnReader> column_reader;
567
94
        RETURN_IF_ERROR(column_reader_factory.create(*column_schema, &col, &column_reader));
568
94
        _current_non_predicate_columns[local_id] = std::move(column_reader);
569
94
    }
570
98
    if (!_current_merge_range_active && request.conjuncts.empty() &&
571
98
        request.delete_conjuncts.empty()) {
572
        // With no row-level filters there is no lazy-read decision to wait for, so start warming
573
        // output chunks immediately after their readers are created. Filtered scans still defer
574
        // this until at least one row survives the predicate phase.
575
11
        prefetch_current_row_group_columns(file_context, file_schema, request.non_predicate_columns,
576
11
                                           &_current_non_predicate_prefetched);
577
11
    }
578
98
    *has_row_group = true;
579
98
    return Status::OK();
580
98
}
581
582
3
Status ParquetScanScheduler::skip_current_row_group_rows(int64_t rows) {
583
3
    DORIS_CHECK(rows >= 0);
584
3
    if (rows == 0) {
585
0
        return Status::OK();
586
0
    }
587
3
    if (_scan_profile.range_gap_skipped_rows != nullptr) {
588
2
        COUNTER_UPDATE(_scan_profile.range_gap_skipped_rows, rows);
589
2
    }
590
3
    for (const auto& column_reader : _current_predicate_columns | std::views::values) {
591
3
        RETURN_IF_ERROR(column_reader->skip(rows));
592
3
    }
593
3
    for (const auto& column_reader : _current_non_predicate_columns | std::views::values) {
594
1
        RETURN_IF_ERROR(column_reader->skip(rows));
595
1
    }
596
3
    _current_row_group_rows_read += rows;
597
3
    return Status::OK();
598
3
}
599
600
Status ParquetScanScheduler::read_filter_columns(int64_t batch_rows,
601
                                                 const format::FileScanRequest& request,
602
                                                 Block* file_block, SelectionVector* selection,
603
                                                 uint16_t* selected_rows,
604
91
                                                 int64_t* conjunct_filtered_rows) {
605
91
    if (!request.conjuncts.empty() || !request.delete_conjuncts.empty()) {
606
48
        selection->resize(static_cast<size_t>(batch_rows));
607
48
    }
608
91
    for (const auto& [fid, column_reader] : _current_predicate_columns) {
609
52
        auto position_it = request.local_positions.find(format::LocalColumnId(fid));
610
52
        DORIS_CHECK(position_it != request.local_positions.end());
611
52
        const auto block_position = position_it->second.value();
612
52
        DCHECK(remove_nullable(column_reader->type())
613
0
                       ->equals(*remove_nullable(file_block->get_by_position(block_position).type)))
614
0
                << column_reader->type()->get_name() << " "
615
0
                << file_block->get_by_position(block_position).type->get_name() << " "
616
0
                << column_reader->name() << " " << file_block->get_by_position(block_position).name;
617
52
        auto column = file_block->get_by_position(block_position).column->assert_mutable();
618
52
        int64_t column_rows = 0;
619
52
        {
620
52
            SCOPED_TIMER(_scan_profile.column_read_time);
621
52
            RETURN_IF_ERROR(column_reader->read(batch_rows, column, &column_rows));
622
52
        }
623
52
        if (column_rows != batch_rows) {
624
0
            return Status::Corruption("Parquet filter column {} returned {} rows, expected {} rows",
625
0
                                      column_reader->name(), column_rows, batch_rows);
626
0
        }
627
52
        file_block->replace_by_position(block_position, std::move(column));
628
52
    }
629
91
    if (_scan_profile.predicate_filter_time == nullptr) {
630
66
        return execute_batch_filters(request, batch_rows, file_block, selection, selected_rows,
631
66
                                     conjunct_filtered_rows);
632
66
    }
633
25
    SCOPED_TIMER(_scan_profile.predicate_filter_time);
634
25
    return execute_batch_filters(request, batch_rows, file_block, selection, selected_rows,
635
25
                                 conjunct_filtered_rows);
636
91
}
637
638
bool ParquetScanScheduler::prepare_current_row_group_reader(
639
        ParquetFileContext& file_context,
640
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
641
98
        const format::FileScanRequest& request, int row_group_idx) {
642
98
    if (file_context.metadata == nullptr) {
643
0
        return false;
644
0
    }
645
98
    const auto ranges = build_row_group_prefetch_ranges(
646
98
            *file_context.metadata, file_schema, request_scan_columns(request), row_group_idx);
647
98
    const size_t avg_io_size = detail::average_prefetch_range_size(ranges);
648
98
    return file_context.set_random_access_ranges(ranges, avg_io_size, _profile,
649
98
                                                 _merge_read_slice_size);
650
98
}
651
652
void ParquetScanScheduler::prefetch_current_row_group_columns(
653
        ParquetFileContext& file_context,
654
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
655
22
        const std::vector<format::LocalColumnIndex>& scan_columns, bool* prefetched) {
656
22
    DORIS_CHECK(prefetched != nullptr);
657
22
    if (_current_merge_range_active || *prefetched || scan_columns.empty() ||
658
22
        _current_row_group_id < 0 || file_context.metadata == nullptr) {
659
22
        return;
660
22
    }
661
0
    *prefetched = true;
662
    // The scanner request separates predicate and non-predicate columns so Parquet can read
663
    // predicate columns first and lazily materialize the rest. Keep the same contract for
664
    // prefetch: callers decide which side to warm, and this helper only translates that selected
665
    // projection into physical column-chunk byte ranges for the current row group.
666
0
    file_context.prefetch_ranges(
667
0
            build_row_group_prefetch_ranges(*file_context.metadata, file_schema, scan_columns,
668
0
                                            _current_row_group_id),
669
0
            nullptr);
670
0
}
671
672
Status ParquetScanScheduler::read_current_row_group_batch(
673
        ParquetFileContext& file_context,
674
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, int64_t batch_rows,
675
        const format::FileScanRequest& request, int64_t batch_first_file_row, Block* file_block,
676
102
        size_t* rows) {
677
102
    if (_scan_profile.total_batches != nullptr) {
678
25
        COUNTER_UPDATE(_scan_profile.total_batches, 1);
679
25
    }
680
102
    if (_scan_profile.raw_rows_read != nullptr) {
681
25
        COUNTER_UPDATE(_scan_profile.raw_rows_read, batch_rows);
682
25
    }
683
102
    _raw_rows_read += batch_rows;
684
102
    if (_current_predicate_columns.empty() && _current_non_predicate_columns.empty()) {
685
11
        *rows = static_cast<size_t>(batch_rows);
686
11
        if (_scan_profile.selected_rows != nullptr) {
687
0
            COUNTER_UPDATE(_scan_profile.selected_rows, batch_rows);
688
0
        }
689
11
        return Status::OK();
690
11
    }
691
91
    SelectionVector selection;
692
91
    DORIS_CHECK(batch_rows <= std::numeric_limits<uint16_t>::max());
693
91
    uint16_t selected_rows = static_cast<uint16_t>(batch_rows);
694
91
    int64_t conjunct_filtered_rows = 0;
695
91
    RETURN_IF_ERROR(read_filter_columns(batch_rows, request, file_block, &selection, &selected_rows,
696
91
                                        &conjunct_filtered_rows));
697
91
    _predicate_filtered_rows += conjunct_filtered_rows;
698
91
    mark_condition_cache_granules(selection, selected_rows, batch_first_file_row);
699
700
91
    const bool need_filter_output = selected_rows != batch_rows;
701
91
    if (_scan_profile.selected_rows != nullptr) {
702
25
        COUNTER_UPDATE(_scan_profile.selected_rows, selected_rows);
703
25
    }
704
91
    if (_scan_profile.rows_filtered_by_conjunct != nullptr) {
705
25
        COUNTER_UPDATE(_scan_profile.rows_filtered_by_conjunct, conjunct_filtered_rows);
706
25
    }
707
91
    if (!_current_non_predicate_columns.empty() &&
708
91
        _scan_profile.lazy_read_filtered_rows != nullptr) {
709
22
        COUNTER_UPDATE(_scan_profile.lazy_read_filtered_rows, batch_rows - selected_rows);
710
22
    }
711
91
    if (selected_rows == 0 && _scan_profile.empty_selection_batches != nullptr) {
712
1
        COUNTER_UPDATE(_scan_profile.empty_selection_batches, 1);
713
1
    }
714
91
    if (need_filter_output) {
715
28
        IColumn::Filter output_filter = selection_to_filter(selection, selected_rows, batch_rows);
716
31
        for (const auto& col : request.predicate_columns) {
717
31
            auto position_it = request.local_positions.find(col.column_id());
718
31
            DORIS_CHECK(position_it != request.local_positions.end());
719
31
            const auto block_position = position_it->second.value();
720
31
            RETURN_IF_CATCH_EXCEPTION(file_block->replace_by_position(
721
31
                    block_position, file_block->get_by_position(block_position)
722
31
                                            .column->filter(output_filter, selected_rows)));
723
31
        }
724
28
    }
725
91
    if (!_current_merge_range_active && selected_rows > 0 &&
726
91
        !_current_non_predicate_columns.empty()) {
727
        // Do not prefetch lazy output columns until at least one row survives filtering. This is
728
        // the same decision point where the v2 reader switches from predicate-only reads to
729
        // materializing non-predicate columns, so fully filtered batches avoid unnecessary IO.
730
0
        prefetch_current_row_group_columns(file_context, file_schema, request.non_predicate_columns,
731
0
                                           &_current_non_predicate_prefetched);
732
0
    }
733
734
91
    {
735
91
        SCOPED_TIMER(_scan_profile.column_read_time);
736
118
        for (const auto& [fid, column_reader] : _current_non_predicate_columns) {
737
118
            auto position_it = request.local_positions.find(format::LocalColumnId(fid));
738
118
            DORIS_CHECK(position_it != request.local_positions.end());
739
118
            const auto block_position = position_it->second.value();
740
118
            auto column = file_block->get_by_position(block_position).column->assert_mutable();
741
118
            DCHECK_EQ(file_block->get_by_position(block_position).type->get_primitive_type(),
742
0
                      column_reader->type()->get_primitive_type())
743
0
                    << type_to_string(file_block->get_by_position(block_position)
744
0
                                              .type->get_primitive_type())
745
0
                    << " " << type_to_string(column_reader->type()->get_primitive_type()) << " "
746
0
                    << column_reader->name() << " " << fid << " " << block_position;
747
118
            if (need_filter_output) {
748
23
                [[maybe_unused]] auto old_size = column->size();
749
23
                RETURN_IF_ERROR(
750
23
                        column_reader->select(selection, selected_rows, batch_rows, column));
751
23
                if (column->size() != old_size + selected_rows) {
752
0
                    return Status::Corruption(
753
0
                            "Parquet selected output column {} returned {} rows, expected {} rows",
754
0
                            column_reader->name(), column->size(), old_size + selected_rows);
755
0
                }
756
95
            } else {
757
95
                int64_t column_rows = 0;
758
95
                RETURN_IF_ERROR(column_reader->read(batch_rows, column, &column_rows));
759
95
                if (column_rows != batch_rows) {
760
0
                    return Status::Corruption(
761
0
                            "Parquet output column {} returned {} rows, expected {} rows",
762
0
                            column_reader->name(), column_rows, batch_rows);
763
0
                }
764
95
            }
765
118
            file_block->replace_by_position(block_position, std::move(column));
766
118
        }
767
91
    }
768
91
    *rows = static_cast<size_t>(selected_rows);
769
91
    return Status::OK();
770
91
}
771
772
void ParquetScanScheduler::mark_condition_cache_granules(const SelectionVector& selection,
773
                                                         uint16_t selected_rows,
774
91
                                                         int64_t batch_first_file_row) {
775
91
    if (!_condition_cache_ctx || _condition_cache_ctx->is_hit ||
776
91
        !_condition_cache_ctx->filter_result) {
777
90
        return;
778
90
    }
779
1
    auto& cache = *_condition_cache_ctx->filter_result;
780
2.04k
    for (uint16_t selection_idx = 0; selection_idx < selected_rows; ++selection_idx) {
781
2.04k
        const int64_t file_row = batch_first_file_row + selection.get_index(selection_idx);
782
2.04k
        const int64_t granule = file_row / ConditionCacheContext::GRANULE_SIZE;
783
2.04k
        const int64_t cache_idx = granule - _condition_cache_ctx->base_granule;
784
2.04k
        if (cache_idx >= 0 && static_cast<size_t>(cache_idx) < cache.size()) {
785
2.04k
            cache[static_cast<size_t>(cache_idx)] = true;
786
2.04k
        }
787
2.04k
    }
788
1
}
789
790
Status ParquetScanScheduler::read_next_batch(
791
        ParquetFileContext& file_context,
792
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
793
140
        const format::FileScanRequest& request, Block* file_block, size_t* rows, bool* eof) {
794
140
    *rows = 0;
795
191
    while (true) {
796
191
        if (_current_row_group == nullptr) {
797
137
            bool has_row_group = false;
798
137
            RETURN_IF_ERROR(
799
137
                    open_next_row_group(file_context, file_schema, request, &has_row_group));
800
137
            if (!has_row_group) {
801
39
                *eof = true;
802
39
                return Status::OK();
803
39
            }
804
137
        }
805
806
152
        if (_current_range_idx >= _current_selected_ranges.size()) {
807
            // Current row group finished, try next row group.
808
50
            reset_current_row_group();
809
50
            continue;
810
50
        }
811
812
102
        const RowRange& current_range = _current_selected_ranges[_current_range_idx];
813
102
        DORIS_CHECK(current_range.start >= 0);
814
102
        DORIS_CHECK(current_range.length > 0);
815
102
        DORIS_CHECK(current_range.start + current_range.length <= _current_row_group_rows);
816
817
102
        if (_current_row_group_rows_read < current_range.start) {
818
            // Skip filtered rows according to row group level pruning.
819
3
            RETURN_IF_ERROR(skip_current_row_group_rows(current_range.start -
820
3
                                                        _current_row_group_rows_read));
821
3
        }
822
102
        DORIS_CHECK(_current_row_group_rows_read == current_range.start + _current_range_rows_read);
823
102
        const int64_t remaining_rows = current_range.length - _current_range_rows_read;
824
102
        if (remaining_rows <= 0) {
825
            // Current range finished, try next range in the same row group.
826
0
            ++_current_range_idx;
827
0
            _current_range_rows_read = 0;
828
0
            continue;
829
0
        }
830
831
102
        const int64_t batch_rows = std::min<int64_t>(_batch_size, remaining_rows);
832
102
        const int64_t physical_rows_read = batch_rows;
833
102
        const int64_t batch_first_file_row =
834
102
                _current_row_group_first_row + _current_row_group_rows_read;
835
102
        RETURN_IF_ERROR(read_current_row_group_batch(file_context, file_schema, batch_rows, request,
836
102
                                                     batch_first_file_row, file_block, rows));
837
102
        _current_row_group_rows_read += physical_rows_read;
838
102
        _current_range_rows_read += physical_rows_read;
839
102
        if (_current_range_rows_read >= current_range.length) {
840
98
            ++_current_range_idx;
841
98
            _current_range_rows_read = 0;
842
98
        }
843
102
        if (*rows == 0) {
844
1
            continue;
845
1
        }
846
101
        *eof = false;
847
101
        return Status::OK();
848
102
    }
849
140
}
850
851
} // namespace doris::format::parquet