Coverage Report

Created: 2026-07-20 13:34

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 <parquet/encoding.h>
19
20
#include <algorithm>
21
#include <limits>
22
#include <memory>
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
39
namespace doris::format::parquet {
40
41
namespace {
42
43
296
int64_t column_start_offset(const ::parquet::ColumnChunkMetaData& column_metadata) {
44
296
    return column_metadata.has_dictionary_page()
45
296
                   ? cast_set<int64_t>(column_metadata.dictionary_page_offset())
46
296
                   : cast_set<int64_t>(column_metadata.data_page_offset());
47
296
}
48
49
12
bool is_dictionary_data_encoding(::parquet::Encoding::type encoding) {
50
12
    return encoding == ::parquet::Encoding::PLAIN_DICTIONARY ||
51
12
           encoding == ::parquet::Encoding::RLE_DICTIONARY;
52
12
}
53
54
0
bool is_level_encoding(::parquet::Encoding::type encoding) {
55
0
    return encoding == ::parquet::Encoding::RLE || encoding == ::parquet::Encoding::BIT_PACKED;
56
0
}
57
58
24
bool is_data_page_type(::parquet::PageType::type page_type) {
59
24
    return page_type == ::parquet::PageType::DATA_PAGE ||
60
24
           page_type == ::parquet::PageType::DATA_PAGE_V2;
61
24
}
62
63
12
bool is_fully_dictionary_encoded_chunk(const ::parquet::ColumnChunkMetaData& column_metadata) {
64
12
    if (!column_metadata.has_dictionary_page()) {
65
0
        return false;
66
0
    }
67
68
12
    const auto& encoding_stats = column_metadata.encoding_stats();
69
12
    if (!encoding_stats.empty()) {
70
12
        bool has_dictionary_data_page = false;
71
24
        for (const auto& encoding_stat : encoding_stats) {
72
24
            if (!is_data_page_type(encoding_stat.page_type) || encoding_stat.count <= 0) {
73
12
                continue;
74
12
            }
75
12
            if (!is_dictionary_data_encoding(encoding_stat.encoding)) {
76
0
                return false;
77
0
            }
78
12
            has_dictionary_data_page = true;
79
12
        }
80
12
        return has_dictionary_data_page;
81
12
    }
82
83
0
    bool has_dictionary_encoding = false;
84
0
    for (const auto encoding : column_metadata.encodings()) {
85
0
        if (is_dictionary_data_encoding(encoding)) {
86
0
            has_dictionary_encoding = true;
87
0
            continue;
88
0
        }
89
0
        if (!is_level_encoding(encoding)) {
90
0
            return false;
91
0
        }
92
0
    }
93
0
    return has_dictionary_encoding;
94
0
}
95
96
bool supports_row_level_dictionary_filter(const ParquetColumnSchema& column_schema,
97
12
                                          const ::parquet::ColumnChunkMetaData& column_metadata) {
98
12
    if (column_schema.kind != ParquetColumnSchemaKind::PRIMITIVE ||
99
12
        column_schema.descriptor == nullptr || column_schema.type == nullptr ||
100
12
        column_schema.max_repetition_level > 0) {
101
0
        return false;
102
0
    }
103
12
    if (!column_schema.type_descriptor.is_string_like ||
104
12
        column_metadata.type() != ::parquet::Type::BYTE_ARRAY) {
105
0
        return false;
106
0
    }
107
    // Row-level dictionary filtering consumes dictionary ids from DATA_PAGE payloads. It is exact
108
    // only when every data page is dictionary encoded. Mixed dictionary/plain chunks are left on
109
    // the normal decoded-value path, matching the safety rule used by StarRocks and Doris v1.
110
12
    return is_fully_dictionary_encoded_chunk(column_metadata);
111
12
}
112
113
void collect_all_leaf_column_ids(const ParquetColumnSchema& column_schema,
114
245
                                 std::unordered_set<int>* leaf_column_ids) {
115
245
    DORIS_CHECK(leaf_column_ids != nullptr);
116
245
    if (column_schema.kind == ParquetColumnSchemaKind::PRIMITIVE) {
117
236
        if (column_schema.leaf_column_id >= 0) {
118
236
            leaf_column_ids->insert(column_schema.leaf_column_id);
119
236
        }
120
236
        return;
121
236
    }
122
13
    for (const auto& child : column_schema.children) {
123
13
        DORIS_CHECK(child != nullptr);
124
13
        collect_all_leaf_column_ids(*child, leaf_column_ids);
125
13
    }
126
9
}
127
128
void collect_projected_leaf_column_ids(const ParquetColumnSchema& column_schema,
129
                                       const format::LocalColumnIndex& projection,
130
239
                                       std::unordered_set<int>* leaf_column_ids) {
131
239
    DORIS_CHECK(leaf_column_ids != nullptr);
132
239
    if (projection.project_all_children || projection.children.empty()) {
133
232
        collect_all_leaf_column_ids(column_schema, leaf_column_ids);
134
232
        return;
135
232
    }
136
8
    for (const auto& child_projection : projection.children) {
137
8
        const auto child_it =
138
13
                std::ranges::find_if(column_schema.children, [&](const auto& child_schema) {
139
13
                    return child_schema->local_id == child_projection.local_id();
140
13
                });
141
8
        DORIS_CHECK(child_it != column_schema.children.end());
142
8
        collect_projected_leaf_column_ids(**child_it, child_projection, leaf_column_ids);
143
8
    }
144
7
}
145
146
bool is_row_group_outside_range(const ::parquet::FileMetaData& metadata,
147
277
                                const ParquetScanRange& scan_range, int row_group_idx) {
148
277
    if (scan_range.size < 0) {
149
247
        return false;
150
247
    }
151
30
    const int64_t range_start_offset = scan_range.start_offset;
152
30
    const int64_t range_end_offset = range_start_offset + scan_range.size;
153
30
    DORIS_CHECK(range_start_offset >= 0);
154
30
    DORIS_CHECK(range_end_offset >= range_start_offset);
155
30
    if (range_start_offset == 0 &&
156
30
        (scan_range.file_size < 0 || range_end_offset >= scan_range.file_size)) {
157
0
        return false;
158
0
    }
159
160
30
    auto row_group_metadata = metadata.RowGroup(row_group_idx);
161
30
    DORIS_CHECK(row_group_metadata != nullptr);
162
30
    DORIS_CHECK(row_group_metadata->num_columns() > 0);
163
30
    const auto first_column = row_group_metadata->ColumnChunk(0);
164
30
    const auto last_column = row_group_metadata->ColumnChunk(row_group_metadata->num_columns() - 1);
165
30
    DORIS_CHECK(first_column != nullptr);
166
30
    DORIS_CHECK(last_column != nullptr);
167
30
    const int64_t row_group_start_offset = column_start_offset(*first_column);
168
30
    const int64_t row_group_end_offset =
169
30
            column_start_offset(*last_column) + last_column->total_compressed_size();
170
30
    const int64_t row_group_mid_offset =
171
30
            row_group_start_offset + (row_group_end_offset - row_group_start_offset) / 2;
172
30
    return row_group_mid_offset < range_start_offset || row_group_mid_offset >= range_end_offset;
173
30
}
174
175
165
std::vector<format::LocalColumnIndex> request_scan_columns(const format::FileScanRequest& request) {
176
165
    std::vector<format::LocalColumnIndex> scan_columns;
177
165
    scan_columns.reserve(request.predicate_columns.size() + request.non_predicate_columns.size());
178
165
    scan_columns.insert(scan_columns.end(), request.predicate_columns.begin(),
179
165
                        request.predicate_columns.end());
180
175
    for (const auto& column : request.non_predicate_columns) {
181
175
        if (!request.is_count_star_placeholder(column.column_id())) {
182
174
            scan_columns.push_back(column);
183
174
        }
184
175
    }
185
165
    return scan_columns;
186
165
}
187
188
std::vector<format::LocalColumnIndex> physical_non_predicate_columns(
189
11
        const format::FileScanRequest& request) {
190
11
    std::vector<format::LocalColumnIndex> columns;
191
11
    columns.reserve(request.non_predicate_columns.size());
192
11
    for (const auto& column : request.non_predicate_columns) {
193
0
        if (!request.is_count_star_placeholder(column.column_id())) {
194
0
            columns.push_back(column);
195
0
        }
196
0
    }
197
11
    return columns;
198
11
}
199
200
void materialize_count_star_placeholders(const format::FileScanRequest& request, size_t rows,
201
159
                                         Block* file_block) {
202
159
    DORIS_CHECK(file_block != nullptr);
203
175
    for (const auto& column : request.non_predicate_columns) {
204
175
        if (!request.is_count_star_placeholder(column.column_id())) {
205
174
            continue;
206
174
        }
207
1
        const auto block_position = request.local_positions.at(column.column_id()).value();
208
1
        auto placeholder = file_block->get_by_position(block_position).column->assert_mutable();
209
1
        DCHECK(placeholder->empty());
210
1
        placeholder->insert_many_defaults(rows);
211
1
        file_block->replace_by_position(block_position, std::move(placeholder));
212
1
    }
213
159
}
214
215
std::vector<ParquetPageCacheRange> build_row_group_prefetch_ranges(
216
        const ::parquet::FileMetaData& metadata,
217
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
218
166
        const std::vector<format::LocalColumnIndex>& scan_columns, int row_group_idx) {
219
166
    std::unordered_set<int> leaf_column_ids;
220
273
    for (const auto& projection : scan_columns) {
221
273
        const auto local_id = projection.local_id();
222
273
        if (local_id == format::ROW_POSITION_COLUMN_ID ||
223
273
            local_id == format::GLOBAL_ROWID_COLUMN_ID) {
224
42
            continue;
225
42
        }
226
231
        DORIS_CHECK(local_id >= 0 && local_id < static_cast<int32_t>(file_schema.size()));
227
231
        DORIS_CHECK(file_schema[local_id] != nullptr);
228
        // Prefetch and merge-reader ranges must be physical leaf chunks, not Doris logical slots.
229
        // Example: for a struct column s<a:int,b:string>, projecting only s.a should include only
230
        // the Parquet leaf chunk of a. Projecting the whole struct includes both a and b.
231
231
        collect_projected_leaf_column_ids(*file_schema[local_id], projection, &leaf_column_ids);
232
231
    }
233
234
166
    auto row_group_metadata = metadata.RowGroup(row_group_idx);
235
166
    DORIS_CHECK(row_group_metadata != nullptr);
236
166
    std::vector<int> ordered_leaf_column_ids(leaf_column_ids.begin(), leaf_column_ids.end());
237
166
    std::ranges::sort(ordered_leaf_column_ids);
238
239
166
    std::vector<ParquetPageCacheRange> ranges;
240
166
    ranges.reserve(ordered_leaf_column_ids.size());
241
236
    for (const auto leaf_column_id : ordered_leaf_column_ids) {
242
236
        DORIS_CHECK(leaf_column_id >= 0 && leaf_column_id < row_group_metadata->num_columns());
243
236
        auto column_metadata = row_group_metadata->ColumnChunk(leaf_column_id);
244
236
        DORIS_CHECK(column_metadata != nullptr);
245
236
        const int64_t offset = column_start_offset(*column_metadata);
246
236
        const int64_t size = column_metadata->total_compressed_size();
247
236
        DORIS_CHECK(offset >= 0);
248
236
        if (size > 0) {
249
236
            ranges.push_back(ParquetPageCacheRange {.offset = offset, .size = size});
250
236
        }
251
236
    }
252
166
    return ranges;
253
166
}
254
255
Status select_row_groups_by_scan_range(const ::parquet::FileMetaData& metadata,
256
                                       const ParquetScanRange& scan_range,
257
                                       std::vector<int64_t>* row_group_first_rows,
258
186
                                       std::vector<int>* selected_row_groups) {
259
186
    DORIS_CHECK(row_group_first_rows != nullptr);
260
186
    DORIS_CHECK(selected_row_groups != nullptr);
261
186
    row_group_first_rows->assign(metadata.num_row_groups(), 0);
262
186
    selected_row_groups->clear();
263
186
    selected_row_groups->reserve(metadata.num_row_groups());
264
186
    int64_t next_row_group_first_row = 0;
265
463
    for (int row_group_idx = 0; row_group_idx < metadata.num_row_groups(); ++row_group_idx) {
266
277
        (*row_group_first_rows)[row_group_idx] = next_row_group_first_row;
267
277
        auto row_group_metadata = metadata.RowGroup(row_group_idx);
268
277
        DORIS_CHECK(row_group_metadata != nullptr);
269
277
        const int64_t row_group_rows = row_group_metadata->num_rows();
270
277
        if (row_group_rows < 0) {
271
0
            return Status::Corruption("Invalid negative row count in parquet row group {}",
272
0
                                      row_group_idx);
273
0
        }
274
277
        next_row_group_first_row += row_group_rows;
275
277
        if (!is_row_group_outside_range(metadata, scan_range, row_group_idx)) {
276
256
            selected_row_groups->push_back(row_group_idx);
277
256
        }
278
277
    }
279
186
    return Status::OK();
280
186
}
281
282
Status build_row_group_read_plans(
283
        const ::parquet::FileMetaData& metadata, ::parquet::ParquetFileReader* file_reader,
284
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
285
        const format::FileScanRequest& request, const std::vector<int>& selected_row_groups,
286
        const std::vector<int64_t>& row_group_first_rows, RowGroupScanPlan* plan,
287
186
        const cctz::time_zone* timezone, const RuntimeState* runtime_state) {
288
186
    DORIS_CHECK(plan != nullptr);
289
186
    plan->row_groups.reserve(selected_row_groups.size());
290
220
    for (const auto row_group_idx : selected_row_groups) {
291
220
        DORIS_CHECK(row_group_idx >= 0);
292
220
        DORIS_CHECK(static_cast<size_t>(row_group_idx) < row_group_first_rows.size());
293
220
        auto row_group_metadata = metadata.RowGroup(row_group_idx);
294
220
        DORIS_CHECK(row_group_metadata != nullptr);
295
220
        const int64_t row_group_rows = row_group_metadata->num_rows();
296
220
        if (row_group_rows == 0) {
297
0
            continue;
298
0
        }
299
300
220
        RowGroupReadPlan row_group_plan;
301
220
        row_group_plan.row_group_id = row_group_idx;
302
220
        row_group_plan.first_file_row = row_group_first_rows[row_group_idx];
303
220
        row_group_plan.row_group_rows = row_group_rows;
304
220
        RETURN_IF_ERROR(select_row_group_ranges_by_page_index(
305
220
                file_reader, file_schema, request, row_group_idx, row_group_rows,
306
220
                &row_group_plan.selected_ranges, &row_group_plan.page_skip_plans,
307
220
                &plan->pruning_stats, timezone, runtime_state));
308
220
        if (row_group_plan.selected_ranges.empty()) {
309
1
            continue;
310
1
        }
311
219
        plan->pruning_stats.selected_row_ranges += row_group_plan.selected_ranges.size();
312
219
        plan->row_groups.push_back(std::move(row_group_plan));
313
219
    }
314
186
    return Status::OK();
315
186
}
316
317
} // namespace
318
319
Status plan_parquet_row_groups(const ::parquet::FileMetaData& metadata,
320
                               ::parquet::ParquetFileReader* file_reader,
321
                               const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
322
                               const format::FileScanRequest& request,
323
                               const ParquetScanRange& scan_range, bool enable_bloom_filter,
324
                               RowGroupScanPlan* plan, const cctz::time_zone* timezone,
325
186
                               const RuntimeState* runtime_state) {
326
186
    DORIS_CHECK(plan != nullptr);
327
186
    plan->row_groups.clear();
328
186
    plan->pruning_stats = ParquetPruningStats {};
329
330
    // Row-group planning flow:
331
    //
332
    //     parquet footer row groups
333
    //              |
334
    //              v
335
    //     split byte-range candidates
336
    //              |
337
    //              v
338
    //     row-group metadata pruning
339
    //     statistics/ZoneMap -> dictionary -> bloom filter
340
    //              |
341
    //              v
342
    //     page-index pruning per selected row group
343
    //              |
344
    //              v
345
    //     RowGroupReadPlan with selected row ranges
346
    //
347
    // Metadata pruning removes whole row groups before readers are opened. Page index pruning runs
348
    // only for remaining row groups and produces selected row ranges; the scan scheduler later skips
349
    // gaps between those ranges, while row-level VExpr conjuncts still run on loaded batches for
350
    // correctness.
351
186
    std::vector<int64_t> row_group_first_rows;
352
186
    std::vector<int> scan_range_selected_row_groups;
353
186
    RETURN_IF_ERROR(select_row_groups_by_scan_range(metadata, scan_range, &row_group_first_rows,
354
186
                                                    &scan_range_selected_row_groups));
355
356
186
    std::vector<int> metadata_selected_row_groups;
357
186
    RETURN_IF_ERROR(select_row_groups_by_metadata(
358
186
            metadata, file_reader, file_schema, request, &scan_range_selected_row_groups,
359
186
            &metadata_selected_row_groups, enable_bloom_filter, &plan->pruning_stats, timezone,
360
186
            runtime_state));
361
362
186
    RETURN_IF_ERROR(build_row_group_read_plans(metadata, file_reader, file_schema, request,
363
186
                                               metadata_selected_row_groups, row_group_first_rows,
364
186
                                               plan, timezone, runtime_state));
365
186
    plan->pruning_stats.selected_row_groups = plan->row_groups.size();
366
186
    return Status::OK();
367
186
}
368
369
namespace {
370
371
using DictionaryResidualConjunct = std::pair<VExprContextSPtr, VExprSPtr>;
372
using DictionaryResidualConjuncts = std::vector<DictionaryResidualConjunct>;
373
374
36
void update_counter_if_not_null(RuntimeProfile::Counter* counter, int64_t value) {
375
36
    if (counter != nullptr) {
376
15
        COUNTER_UPDATE(counter, value);
377
15
    }
378
36
}
379
380
uint16_t apply_filter_to_selection(const IColumn::Filter& filter, SelectionVector* selection,
381
35
                                   uint16_t selected_rows) {
382
35
    uint16_t new_selected_rows = 0;
383
161
    for (uint16_t selection_idx = 0; selection_idx < selected_rows; ++selection_idx) {
384
126
        const auto row_idx = selection->get_index(selection_idx);
385
126
        if (filter[row_idx] != 0) {
386
83
            selection->set_index(new_selected_rows++, static_cast<SelectionVector::Index>(row_idx));
387
83
        }
388
126
    }
389
35
    return new_selected_rows;
390
35
}
391
392
Status execute_compact_filter_conjuncts(const VExprContextSPtrs& conjuncts, size_t rows,
393
                                        Block* file_block, IColumn::Filter* compact_filter,
394
46
                                        bool* can_filter_all) {
395
46
    DORIS_CHECK(compact_filter != nullptr);
396
46
    DORIS_CHECK(can_filter_all != nullptr);
397
46
    compact_filter->resize_fill(rows, 1);
398
46
    *can_filter_all = false;
399
46
    for (const auto& conjunct : conjuncts) {
400
46
        DORIS_CHECK(conjunct != nullptr);
401
46
        IColumn::Filter filter(rows, 1);
402
46
        bool conjunct_can_filter_all = false;
403
46
        RETURN_IF_ERROR(conjunct->execute_filter(file_block, filter.data(), rows, false,
404
46
                                                 &conjunct_can_filter_all));
405
46
        if (conjunct_can_filter_all) {
406
11
            std::ranges::fill(*compact_filter, 0);
407
11
            *can_filter_all = true;
408
11
            break;
409
11
        }
410
6.37k
        for (size_t row = 0; row < rows; ++row) {
411
6.34k
            (*compact_filter)[row] &= filter[row];
412
6.34k
        }
413
35
    }
414
46
    return Status::OK();
415
46
}
416
417
Status execute_compact_dictionary_residual_conjuncts(const DictionaryResidualConjuncts& conjuncts,
418
                                                     size_t rows, Block* file_block,
419
                                                     IColumn::Filter* compact_filter,
420
3
                                                     bool* can_filter_all) {
421
3
    DORIS_CHECK(compact_filter != nullptr);
422
3
    DORIS_CHECK(can_filter_all != nullptr);
423
3
    compact_filter->resize_fill(rows, 1);
424
3
    *can_filter_all = false;
425
3
    for (const auto& [owner_context, residual_expr] : conjuncts) {
426
3
        DORIS_CHECK(owner_context != nullptr);
427
3
        DORIS_CHECK(residual_expr != nullptr);
428
3
        IColumn::Filter filter(rows, 1);
429
3
        bool conjunct_can_filter_all = false;
430
3
        RETURN_IF_ERROR(residual_expr->execute_filter(owner_context.get(), file_block,
431
3
                                                      filter.data(), rows, false,
432
3
                                                      &conjunct_can_filter_all));
433
3
        if (conjunct_can_filter_all) {
434
1
            std::ranges::fill(*compact_filter, 0);
435
1
            *can_filter_all = true;
436
1
            break;
437
1
        }
438
6
        for (size_t row = 0; row < rows; ++row) {
439
4
            (*compact_filter)[row] &= filter[row];
440
4
        }
441
2
    }
442
3
    return Status::OK();
443
3
}
444
445
Status execute_compact_delete_conjuncts(const VExprContextSPtrs& delete_conjuncts, size_t rows,
446
                                        Block* file_block, IColumn::Filter* compact_filter,
447
1
                                        bool* can_filter_all) {
448
1
    DORIS_CHECK(compact_filter != nullptr);
449
1
    DORIS_CHECK(can_filter_all != nullptr);
450
1
    compact_filter->resize_fill(rows, 1);
451
1
    *can_filter_all = false;
452
1
    for (const auto& delete_conjunct : delete_conjuncts) {
453
1
        DORIS_CHECK(delete_conjunct != nullptr);
454
1
        int result_column_id = -1;
455
1
        RETURN_IF_ERROR(delete_conjunct->root()->execute(delete_conjunct.get(), file_block,
456
1
                                                         &result_column_id));
457
1
        DORIS_CHECK(result_column_id >= 0 &&
458
1
                    result_column_id < static_cast<int>(file_block->columns()));
459
1
        const auto& delete_filter = assert_cast<const ColumnUInt8&>(
460
1
                                            *file_block->get_by_position(result_column_id).column)
461
1
                                            .get_data();
462
1
        DORIS_CHECK(delete_filter.size() == rows);
463
1
        bool has_kept_row = false;
464
4
        for (size_t row = 0; row < rows; ++row) {
465
3
            (*compact_filter)[row] &= !delete_filter[row];
466
3
            has_kept_row |= (*compact_filter)[row] != 0;
467
3
        }
468
1
        file_block->erase(result_column_id);
469
1
        if (!has_kept_row) {
470
0
            *can_filter_all = true;
471
0
            break;
472
0
        }
473
1
    }
474
1
    return Status::OK();
475
1
}
476
477
Status execute_filter_conjuncts(const format::FileScanRequest& request, int64_t batch_rows,
478
                                Block* file_block, SelectionVector* selection,
479
39
                                uint16_t* selected_rows) {
480
39
    for (const auto& conjunct : request.conjuncts) {
481
7
        if (*selected_rows == 0) {
482
0
            break;
483
0
        }
484
7
        DORIS_CHECK(conjunct != nullptr);
485
7
        IColumn::Filter filter(static_cast<size_t>(batch_rows), 1);
486
7
        bool can_filter_all = false;
487
7
        RETURN_IF_ERROR(conjunct->execute_filter(file_block, filter.data(),
488
7
                                                 static_cast<size_t>(batch_rows), false,
489
7
                                                 &can_filter_all));
490
7
        *selected_rows =
491
7
                can_filter_all ? 0 : apply_filter_to_selection(filter, selection, *selected_rows);
492
7
    }
493
39
    return Status::OK();
494
39
}
495
496
Status execute_delete_conjuncts(const format::FileScanRequest& request, int64_t batch_rows,
497
                                Block* file_block, SelectionVector* selection,
498
39
                                uint16_t* selected_rows) {
499
39
    for (const auto& delete_conjunct : request.delete_conjuncts) {
500
34
        if (*selected_rows == 0) {
501
0
            break;
502
0
        }
503
34
        DORIS_CHECK(delete_conjunct != nullptr);
504
34
        int result_column_id = -1;
505
34
        RETURN_IF_ERROR(delete_conjunct->root()->execute(delete_conjunct.get(), file_block,
506
34
                                                         &result_column_id));
507
34
        DORIS_CHECK(result_column_id >= 0 &&
508
34
                    result_column_id < static_cast<int>(file_block->columns()));
509
34
        const auto& delete_filter = assert_cast<const ColumnUInt8&>(
510
34
                                            *file_block->get_by_position(result_column_id).column)
511
34
                                            .get_data();
512
34
        DORIS_CHECK(delete_filter.size() == static_cast<size_t>(batch_rows));
513
34
        IColumn::Filter keep_filter(static_cast<size_t>(batch_rows), 1);
514
34
        bool has_kept_row = false;
515
148
        for (size_t row = 0; row < static_cast<size_t>(batch_rows); ++row) {
516
114
            keep_filter[row] = !delete_filter[row];
517
114
            has_kept_row |= keep_filter[row] != 0;
518
114
        }
519
34
        file_block->erase(result_column_id);
520
34
        *selected_rows =
521
34
                !has_kept_row ? 0
522
34
                              : apply_filter_to_selection(keep_filter, selection, *selected_rows);
523
34
    }
524
39
    return Status::OK();
525
39
}
526
527
} // namespace
528
529
uint16_t apply_compact_filter_to_selection(const IColumn::Filter& filter,
530
25
                                           SelectionVector* selection, uint16_t selected_rows) {
531
25
    DORIS_CHECK(selection != nullptr);
532
25
    DORIS_CHECK(filter.size() == selected_rows);
533
25
    uint16_t new_selected_rows = 0;
534
4.22k
    for (uint16_t selection_idx = 0; selection_idx < selected_rows; ++selection_idx) {
535
4.19k
        if (filter[selection_idx] != 0) {
536
2.09k
            selection->set_index(new_selected_rows++, static_cast<SelectionVector::Index>(
537
2.09k
                                                              selection->get_index(selection_idx)));
538
2.09k
        }
539
4.19k
    }
540
25
    return new_selected_rows;
541
25
}
542
543
IColumn::Filter selection_to_filter(const SelectionVector& selection, uint16_t selected_rows,
544
38
                                    int64_t batch_rows) {
545
38
    IColumn::Filter filter(static_cast<size_t>(batch_rows), 0);
546
112
    for (uint16_t selection_idx = 0; selection_idx < selected_rows; ++selection_idx) {
547
74
        filter[selection.get_index(selection_idx)] = 1;
548
74
    }
549
38
    return filter;
550
38
}
551
552
Status execute_batch_filters(const format::FileScanRequest& request, int64_t batch_rows,
553
                             Block* file_block, SelectionVector* selection, uint16_t* selected_rows,
554
109
                             int64_t* conjunct_filtered_rows) {
555
109
    if (request.conjuncts.empty() && request.delete_conjuncts.empty()) {
556
70
        return Status::OK();
557
70
    }
558
39
    const auto selected_rows_before_conjunct = *selected_rows;
559
39
    RETURN_IF_ERROR(
560
39
            execute_filter_conjuncts(request, batch_rows, file_block, selection, selected_rows));
561
39
    if (conjunct_filtered_rows != nullptr) {
562
39
        *conjunct_filtered_rows += static_cast<int64_t>(selected_rows_before_conjunct) -
563
39
                                   static_cast<int64_t>(*selected_rows);
564
39
    }
565
39
    if (*selected_rows == 0) {
566
0
        return Status::OK();
567
0
    }
568
39
    return execute_delete_conjuncts(request, batch_rows, file_block, selection, selected_rows);
569
39
}
570
571
namespace {
572
4
int64_t count_range_rows(const std::vector<RowRange>& ranges) {
573
4
    int64_t rows = 0;
574
4
    for (const auto& range : ranges) {
575
4
        rows += range.length;
576
4
    }
577
4
    return rows;
578
4
}
579
580
void append_intersection(const RowRange& left, const RowRange& right,
581
2
                         std::vector<RowRange>* result) {
582
2
    const int64_t start = std::max(left.start, right.start);
583
2
    const int64_t end = std::min(left.start + left.length, right.start + right.length);
584
2
    if (start < end) {
585
2
        result->push_back(RowRange {.start = start, .length = end - start});
586
2
    }
587
2
}
588
589
std::vector<RowRange> filter_ranges_by_condition_cache(const std::vector<RowRange>& ranges,
590
                                                       const std::vector<bool>& cache,
591
                                                       int64_t row_group_first_row,
592
2
                                                       int64_t base_granule) {
593
2
    std::vector<RowRange> result;
594
2
    if (cache.empty()) {
595
0
        return ranges;
596
0
    }
597
598
    // Cache coordinates are file-global granules; RowRange coordinates are row-group-relative.
599
    // Walk every selected range in order and split it by granule. Granules covered by the bitmap
600
    // are kept only when the bit is true. Granules outside the bitmap are kept conservatively, so
601
    // an undersized or old-format cache entry cannot skip valid rows.
602
2
    for (const auto& range : ranges) {
603
2
        const int64_t global_start = row_group_first_row + range.start;
604
2
        const int64_t global_end = global_start + range.length;
605
2
        for (int64_t granule = global_start / ConditionCacheContext::GRANULE_SIZE;
606
5
             granule <= (global_end - 1) / ConditionCacheContext::GRANULE_SIZE; ++granule) {
607
3
            const int64_t cache_idx = granule - base_granule;
608
3
            const bool keep = cache_idx < 0 || static_cast<size_t>(cache_idx) >= cache.size() ||
609
3
                              cache[static_cast<size_t>(cache_idx)];
610
3
            if (!keep) {
611
1
                continue;
612
1
            }
613
2
            const int64_t granule_start = granule * ConditionCacheContext::GRANULE_SIZE;
614
2
            const int64_t granule_end = granule_start + ConditionCacheContext::GRANULE_SIZE;
615
2
            const RowRange file_granule_range {.start = granule_start - row_group_first_row,
616
2
                                               .length = granule_end - granule_start};
617
2
            append_intersection(range, file_granule_range, &result);
618
2
        }
619
2
    }
620
2
    return result;
621
2
}
622
623
} // namespace
624
625
175
void ParquetScanScheduler::set_plan(RowGroupScanPlan plan) {
626
175
    _row_group_plans = std::move(plan.row_groups);
627
175
    _condition_cache_filtered_rows = 0;
628
175
    _predicate_filtered_rows = 0;
629
175
    reset();
630
175
}
631
632
3
void ParquetScanScheduler::set_condition_cache_context(std::shared_ptr<ConditionCacheContext> ctx) {
633
3
    _condition_cache_ctx = std::move(ctx);
634
3
    if (!_condition_cache_ctx || !_condition_cache_ctx->filter_result || _row_group_plans.empty()) {
635
0
        return;
636
0
    }
637
638
3
    if (!_condition_cache_ctx->is_hit) {
639
1
        _condition_cache_ctx->base_granule =
640
1
                _row_group_plans.front().first_file_row / ConditionCacheContext::GRANULE_SIZE;
641
1
        const auto& last_plan = _row_group_plans.back();
642
1
        const int64_t end_granule = (last_plan.first_file_row + last_plan.row_group_rows +
643
1
                                     ConditionCacheContext::GRANULE_SIZE - 1) /
644
1
                                    ConditionCacheContext::GRANULE_SIZE;
645
1
        DORIS_CHECK(end_granule > _condition_cache_ctx->base_granule);
646
1
        _condition_cache_ctx->num_granules =
647
1
                std::min(_condition_cache_ctx->filter_result->size(),
648
1
                         static_cast<size_t>(end_granule - _condition_cache_ctx->base_granule));
649
1
        return;
650
1
    }
651
652
2
    std::vector<RowGroupReadPlan> filtered_plans;
653
2
    filtered_plans.reserve(_row_group_plans.size());
654
2
    for (auto& plan : _row_group_plans) {
655
2
        const int64_t old_rows = count_range_rows(plan.selected_ranges);
656
2
        plan.selected_ranges = filter_ranges_by_condition_cache(
657
2
                plan.selected_ranges, *_condition_cache_ctx->filter_result, plan.first_file_row,
658
2
                _condition_cache_ctx->base_granule);
659
2
        const int64_t new_rows = count_range_rows(plan.selected_ranges);
660
2
        _condition_cache_filtered_rows += old_rows - new_rows;
661
2
        if (!plan.selected_ranges.empty()) {
662
2
            filtered_plans.push_back(std::move(plan));
663
2
        }
664
2
    }
665
2
    _row_group_plans = std::move(filtered_plans);
666
2
    reset();
667
2
}
668
669
177
void ParquetScanScheduler::reset() {
670
177
    _next_row_group_plan_idx = 0;
671
177
    _raw_rows_read = 0;
672
177
    reset_current_row_group();
673
177
}
674
675
287
void ParquetScanScheduler::reset_current_row_group() {
676
287
    _current_row_group.reset();
677
287
    _current_predicate_columns.clear();
678
287
    _current_non_predicate_columns.clear();
679
287
    _current_dictionary_filters.clear();
680
287
    _current_dictionary_residual_conjuncts.clear();
681
287
    _current_row_group_rows = 0;
682
287
    _current_row_group_id = -1;
683
287
    _current_row_group_rows_read = 0;
684
287
    _current_row_group_first_row = 0;
685
287
    _current_selected_ranges.clear();
686
287
    _current_range_idx = 0;
687
287
    _current_range_rows_read = 0;
688
    // Readers are row-group scoped. If every remaining row was filtered, no future output can
689
    // observe the non-predicate readers' position, so dropping them together with their pending lag
690
    // avoids a useless end-of-row-group SkipRecords call. Example: predicate readers advance from 0
691
    // to 10,000 while lazy readers stay at 0; clearing both readers here is sufficient because the
692
    // next row group constructs a new set starting at its own row 0.
693
287
    _pending_non_predicate_skip_rows = 0;
694
287
    _current_predicate_prefetched = false;
695
287
    _current_non_predicate_prefetched = false;
696
287
    _current_merge_range_active = false;
697
287
}
698
699
Status ParquetScanScheduler::open_next_row_group(
700
        ParquetFileContext& file_context,
701
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
702
258
        const format::FileScanRequest& request, bool* has_row_group) {
703
258
    *has_row_group = false;
704
258
    if (_next_row_group_plan_idx >= _row_group_plans.size()) {
705
93
        return Status::OK();
706
93
    }
707
165
    const RowGroupReadPlan& row_group_plan = _row_group_plans[_next_row_group_plan_idx++];
708
165
    const int row_group_idx = row_group_plan.row_group_id;
709
    // Row-level dictionary filters read dictionary pages before Arrow RecordReaders are created.
710
    // Keep that probe on the base reader: MergeRangeFileReader expects each registered range to be
711
    // consumed as one forward pass, while the later RecordReader opens the same column chunk again
712
    // for the data-page stream.
713
165
    file_context.reset_random_access_ranges();
714
165
    _current_merge_range_active = false;
715
165
    try {
716
165
        _current_row_group = file_context.file_reader->RowGroup(row_group_idx);
717
165
    } catch (const ::parquet::ParquetException& e) {
718
0
        return Status::Corruption("Failed to open parquet row group {}: {}", row_group_idx,
719
0
                                  e.what());
720
0
    } catch (const std::exception& e) {
721
0
        return Status::InternalError("Failed to open parquet row group {}: {}", row_group_idx,
722
0
                                     e.what());
723
0
    }
724
725
165
    auto row_group_metadata = file_context.metadata->RowGroup(row_group_idx);
726
165
    DORIS_CHECK(row_group_metadata != nullptr);
727
165
    _current_row_group_rows = row_group_metadata->num_rows();
728
165
    DORIS_CHECK(_current_row_group_rows == row_group_plan.row_group_rows);
729
165
    DORIS_CHECK(_current_row_group_rows > 0);
730
165
    _current_row_group_id = row_group_idx;
731
165
    DORIS_CHECK(!row_group_plan.selected_ranges.empty());
732
165
    _current_row_group_first_row = row_group_plan.first_file_row;
733
165
    _current_row_group_rows_read = 0;
734
165
    _current_selected_ranges = row_group_plan.selected_ranges;
735
165
    _current_range_idx = 0;
736
165
    _current_range_rows_read = 0;
737
165
    _current_predicate_columns.clear();
738
165
    _current_non_predicate_columns.clear();
739
165
    _current_dictionary_filters.clear();
740
165
    RETURN_IF_ERROR(prepare_current_dictionary_filters(file_context, file_schema, request,
741
165
                                                       row_group_idx, *row_group_metadata));
742
165
    _current_merge_range_active =
743
165
            prepare_current_row_group_reader(file_context, file_schema, request, row_group_idx);
744
745
165
    ParquetColumnReaderFactory column_reader_factory(
746
165
            _current_row_group, file_context.schema->num_columns(), &row_group_plan.page_skip_plans,
747
165
            _page_skip_profile, _timezone, _enable_strict_mode,
748
165
            _scan_profile.column_reader_profile);
749
165
    for (const auto& col : request.predicate_columns) {
750
98
        const auto local_id = col.local_id();
751
98
        if (local_id == format::ROW_POSITION_COLUMN_ID) {
752
25
            _current_predicate_columns[local_id] =
753
25
                    column_reader_factory.create_row_position_column_reader(
754
25
                            _current_row_group_first_row);
755
25
            continue;
756
25
        }
757
73
        if (local_id == format::GLOBAL_ROWID_COLUMN_ID) {
758
0
            DORIS_CHECK(_global_rowid_context.has_value());
759
0
            _current_predicate_columns[local_id] =
760
0
                    column_reader_factory.create_global_rowid_column_reader(
761
0
                            *_global_rowid_context, _current_row_group_first_row);
762
0
            continue;
763
0
        }
764
765
73
        DORIS_CHECK(local_id >= 0 && local_id < static_cast<int32_t>(file_schema.size()));
766
73
        const auto& column_schema = file_schema[local_id];
767
73
        DORIS_CHECK(column_schema != nullptr);
768
73
        std::unique_ptr<ParquetColumnReader> column_reader;
769
73
        RETURN_IF_ERROR(
770
73
                column_reader_factory.create(*column_schema, &col, &column_reader,
771
73
                                             _current_dictionary_filters.contains(local_id)));
772
73
        _current_predicate_columns[local_id] = std::move(column_reader);
773
73
    }
774
    // Start warming filter-column chunks as soon as their row group is selected. Parquet v2 still
775
    // reads through Arrow's random-access reader; this prefetch only warms Doris file cache blocks
776
    // in the background and never changes the row/column materialization order.
777
165
    if (!_current_merge_range_active) {
778
12
        prefetch_current_row_group_columns(file_context, file_schema, request.predicate_columns,
779
12
                                           &_current_predicate_prefetched);
780
12
    }
781
175
    for (const auto& col : request.non_predicate_columns) {
782
175
        const auto local_id = col.local_id();
783
175
        if (request.is_count_star_placeholder(col.column_id())) {
784
1
            continue;
785
1
        }
786
174
        if (local_id == format::ROW_POSITION_COLUMN_ID) {
787
14
            _current_non_predicate_columns[local_id] =
788
14
                    column_reader_factory.create_row_position_column_reader(
789
14
                            _current_row_group_first_row);
790
14
            continue;
791
14
        }
792
160
        if (local_id == format::GLOBAL_ROWID_COLUMN_ID) {
793
2
            DORIS_CHECK(_global_rowid_context.has_value());
794
2
            _current_non_predicate_columns[local_id] =
795
2
                    column_reader_factory.create_global_rowid_column_reader(
796
2
                            *_global_rowid_context, _current_row_group_first_row);
797
2
            continue;
798
2
        }
799
158
        DORIS_CHECK(local_id >= 0 && local_id < static_cast<int32_t>(file_schema.size()));
800
158
        const auto& column_schema = file_schema[local_id];
801
158
        DORIS_CHECK(column_schema != nullptr);
802
158
        std::unique_ptr<ParquetColumnReader> column_reader;
803
158
        RETURN_IF_ERROR(column_reader_factory.create(*column_schema, &col, &column_reader));
804
158
        _current_non_predicate_columns[local_id] = std::move(column_reader);
805
158
    }
806
165
    if (!_current_merge_range_active && request.conjuncts.empty() &&
807
165
        request.delete_conjuncts.empty()) {
808
        // With no row-level filters there is no lazy-read decision to wait for, so start warming
809
        // output chunks immediately after their readers are created. Filtered scans still defer
810
        // this until at least one row survives the predicate phase.
811
11
        prefetch_current_row_group_columns(file_context, file_schema,
812
11
                                           physical_non_predicate_columns(request),
813
11
                                           &_current_non_predicate_prefetched);
814
11
    }
815
165
    *has_row_group = true;
816
165
    return Status::OK();
817
165
}
818
819
3
Status ParquetScanScheduler::skip_current_row_group_rows(int64_t rows) {
820
3
    DORIS_CHECK(rows >= 0);
821
3
    if (rows == 0) {
822
0
        return Status::OK();
823
0
    }
824
3
    if (_scan_profile.range_gap_skipped_rows != nullptr) {
825
2
        COUNTER_UPDATE(_scan_profile.range_gap_skipped_rows, rows);
826
2
    }
827
3
    for (const auto& column_reader : _current_predicate_columns | std::views::values) {
828
3
        RETURN_IF_ERROR(column_reader->skip(rows));
829
3
    }
830
    // Keep page-index/condition-cache gaps pending for lazy columns as well. For example, after a
831
    // fully filtered [0, 32) batch and a pruned [32, 96) gap, predicate readers are at 96 while lazy
832
    // readers remain at 0; one later skip(96) is cheaper than skip(32) followed by skip(64).
833
3
    DORIS_CHECK(_pending_non_predicate_skip_rows <= std::numeric_limits<int64_t>::max() - rows);
834
3
    _pending_non_predicate_skip_rows += rows;
835
3
    _current_row_group_rows_read += rows;
836
3
    return Status::OK();
837
3
}
838
839
148
Status ParquetScanScheduler::flush_pending_non_predicate_skip_rows() {
840
148
    if (_pending_non_predicate_skip_rows == 0) {
841
144
        return Status::OK();
842
144
    }
843
4
    for (const auto& column_reader : _current_non_predicate_columns | std::views::values) {
844
2
        RETURN_IF_ERROR(column_reader->skip(_pending_non_predicate_skip_rows));
845
2
    }
846
4
    _pending_non_predicate_skip_rows = 0;
847
4
    return Status::OK();
848
4
}
849
850
namespace {
851
852
struct PredicateConjunctSchedule {
853
    std::map<size_t, VExprContextSPtrs> single_column_conjuncts;
854
    VExprContextSPtrs remaining_conjuncts;
855
};
856
857
PredicateConjunctSchedule build_predicate_conjunct_schedule(
858
220
        const format::FileScanRequest& request) {
859
220
    std::unordered_set<size_t> predicate_block_positions;
860
220
    predicate_block_positions.reserve(request.predicate_columns.size());
861
220
    for (const auto& col : request.predicate_columns) {
862
169
        const auto position_it = request.local_positions.find(col.column_id());
863
169
        DORIS_CHECK(position_it != request.local_positions.end());
864
169
        predicate_block_positions.insert(position_it->second.value());
865
169
    }
866
867
220
    PredicateConjunctSchedule schedule;
868
220
    for (const auto& conjunct : request.conjuncts) {
869
126
        DORIS_CHECK(conjunct != nullptr);
870
126
        DORIS_CHECK(conjunct->root() != nullptr);
871
126
        if (!conjunct->root()->is_safe_to_execute_on_selected_rows()) {
872
            // Round-by-round filtering can compact later predicate columns before evaluating
873
            // remaining expressions. Stateful functions such as random(1) and error-preserving
874
            // functions such as assert_true() must see the same full batch they saw before this
875
            // optimization, so any unsafe conjunct disables the per-column schedule for the batch.
876
4
            schedule.remaining_conjuncts = request.conjuncts;
877
4
            schedule.single_column_conjuncts.clear();
878
4
            return schedule;
879
4
        }
880
122
        std::set<int> referenced_positions;
881
122
        conjunct->root()->collect_slot_column_ids(referenced_positions);
882
122
        if (referenced_positions.size() != 1) {
883
6
            schedule.remaining_conjuncts.push_back(conjunct);
884
6
            continue;
885
6
        }
886
116
        const auto block_position = static_cast<size_t>(*referenced_positions.begin());
887
116
        if (!predicate_block_positions.contains(block_position)) {
888
0
            schedule.remaining_conjuncts.push_back(conjunct);
889
0
            continue;
890
0
        }
891
116
        schedule.single_column_conjuncts[block_position].push_back(conjunct);
892
116
    }
893
216
    return schedule;
894
220
}
895
896
52
bool can_evaluate_all_with_dictionary(const VExprContextSPtrs& conjuncts) {
897
52
    if (conjuncts.empty()) {
898
0
        return false;
899
0
    }
900
52
    return std::ranges::all_of(conjuncts, [](const auto& conjunct) {
901
52
        return conjunct != nullptr && conjunct->root() != nullptr &&
902
52
               conjunct->root()->can_evaluate_dictionary_filter();
903
52
    });
904
52
}
905
906
24
bool can_evaluate_dictionary_exactly(const VExprSPtr& expr) {
907
24
    DORIS_CHECK(expr != nullptr);
908
24
    const auto* compound_pred = dynamic_cast<const VCompoundPred*>(expr.get());
909
24
    if (compound_pred == nullptr) {
910
21
        return expr->can_evaluate_dictionary_filter();
911
21
    }
912
3
    if (compound_pred->op() != TExprOpcode::COMPOUND_AND &&
913
3
        compound_pred->op() != TExprOpcode::COMPOUND_OR) {
914
0
        return false;
915
0
    }
916
3
    return !expr->children().empty() &&
917
6
           std::ranges::all_of(expr->children(), [](const auto& child) {
918
6
               return can_evaluate_dictionary_exactly(child);
919
6
           });
920
3
}
921
922
void collect_dictionary_residual_exprs(const VExprContextSPtr& owner_context, const VExprSPtr& expr,
923
18
                                       DictionaryResidualConjuncts* residual_conjuncts) {
924
18
    DORIS_CHECK(owner_context != nullptr);
925
18
    DORIS_CHECK(expr != nullptr);
926
18
    DORIS_CHECK(residual_conjuncts != nullptr);
927
928
18
    if (can_evaluate_dictionary_exactly(expr)) {
929
12
        return;
930
12
    }
931
932
    // VCompoundPred dictionary evaluation is a conservative prefilter for AND when only some
933
    // children are dictionary-aware. Split AND so exact dictionary children are not executed again
934
    // on materialized rows. Do not split a non-exact OR: its branches cannot be evaluated
935
    // independently after a dictionary prefilter without changing the original boolean semantics.
936
6
    const auto* compound_pred = dynamic_cast<const VCompoundPred*>(expr.get());
937
6
    if (compound_pred != nullptr && compound_pred->op() == TExprOpcode::COMPOUND_AND) {
938
6
        for (const auto& child : expr->children()) {
939
6
            collect_dictionary_residual_exprs(owner_context, child, residual_conjuncts);
940
6
        }
941
3
        return;
942
3
    }
943
944
3
    residual_conjuncts->emplace_back(owner_context, expr);
945
3
}
946
947
DictionaryResidualConjuncts build_dictionary_residual_conjuncts(
948
12
        const VExprContextSPtrs& conjuncts) {
949
12
    DictionaryResidualConjuncts residual_conjuncts;
950
12
    for (const auto& conjunct : conjuncts) {
951
12
        DORIS_CHECK(conjunct != nullptr);
952
12
        collect_dictionary_residual_exprs(conjunct, conjunct->root(), &residual_conjuncts);
953
12
    }
954
12
    return residual_conjuncts;
955
12
}
956
957
50
uint16_t count_selected_rows(const IColumn::Filter& filter) {
958
50
    uint16_t selected_rows = 0;
959
6.39k
    for (const auto value : filter) {
960
6.39k
        selected_rows += value != 0;
961
6.39k
    }
962
50
    return selected_rows;
963
50
}
964
965
Status filter_read_predicate_columns(Block* file_block, const std::vector<uint32_t>& positions,
966
36
                                     const IColumn::Filter& compact_filter) {
967
36
    if (positions.empty()) {
968
8
        return Status::OK();
969
8
    }
970
28
    RETURN_IF_CATCH_EXCEPTION(Block::filter_block_internal(file_block, positions, compact_filter));
971
28
    return Status::OK();
972
28
}
973
974
IColumn::Filter build_dictionary_entry_filter(size_t block_position,
975
                                              const ParquetColumnSchema& column_schema,
976
                                              const VExprContextSPtrs& conjuncts,
977
12
                                              const ParquetDictionaryWords& dict_words) {
978
12
    auto fields = dictionary_fields_from_words(dict_words);
979
12
    IColumn::Filter dictionary_filter(fields.size(), 1);
980
12
    DictionaryEvalContext ctx;
981
12
    auto& slot = ctx.slots
982
12
                         .emplace(static_cast<int>(block_position),
983
12
                                  DictionaryEvalContext::SlotDictionary {
984
12
                                          .data_type = column_schema.type, .values = {}})
985
12
                         .first->second;
986
12
    slot.values.reserve(1);
987
988
57
    for (size_t dict_idx = 0; dict_idx < fields.size(); ++dict_idx) {
989
45
        slot.values.clear();
990
45
        slot.values.push_back(fields[dict_idx]);
991
45
        dictionary_filter[dict_idx] = VExprContext::evaluate_dictionary_filter(conjuncts, ctx) ==
992
45
                                                      ZoneMapFilterResult::kNoMatch
993
45
                                              ? 0
994
45
                                              : 1;
995
45
    }
996
12
    return dictionary_filter;
997
12
}
998
999
} // namespace
1000
1001
Status ParquetScanScheduler::prepare_current_dictionary_filters(
1002
        ParquetFileContext& file_context,
1003
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
1004
        const format::FileScanRequest& request, int row_group_idx,
1005
165
        const ::parquet::RowGroupMetaData& row_group_metadata) {
1006
165
    _current_dictionary_filters.clear();
1007
165
    _current_dictionary_residual_conjuncts.clear();
1008
165
    if (request.conjuncts.empty()) {
1009
111
        return Status::OK();
1010
111
    }
1011
54
    PredicateConjunctSchedule schedule;
1012
54
    {
1013
54
        SCOPED_TIMER(_scan_profile.dict_filter_expr_rewrite_time);
1014
54
        schedule = build_predicate_conjunct_schedule(request);
1015
54
    }
1016
54
    if (schedule.single_column_conjuncts.empty()) {
1017
5
        return Status::OK();
1018
5
    }
1019
1020
49
    SCOPED_TIMER(_scan_profile.dict_filter_rewrite_time);
1021
53
    for (const auto& col : request.predicate_columns) {
1022
53
        const auto local_id = col.local_id();
1023
53
        if (local_id < 0 || local_id >= static_cast<int32_t>(file_schema.size())) {
1024
1
            continue;
1025
1
        }
1026
52
        const auto position_it = request.local_positions.find(col.column_id());
1027
52
        DORIS_CHECK(position_it != request.local_positions.end());
1028
52
        const auto block_position = static_cast<size_t>(position_it->second.value());
1029
52
        const auto conjunct_it = schedule.single_column_conjuncts.find(block_position);
1030
52
        if (conjunct_it == schedule.single_column_conjuncts.end() ||
1031
52
            !can_evaluate_all_with_dictionary(conjunct_it->second)) {
1032
40
            continue;
1033
40
        }
1034
12
        update_counter_if_not_null(_scan_profile.dict_filter_candidate_columns, 1);
1035
1036
        // This optimization is deliberately limited to single-column predicates with a dictionary
1037
        // evaluable part. Mixed AND predicates are split so dictionary-covered children run as a
1038
        // dict-id prefilter and residual children keep the normal row-level expression path.
1039
12
        const auto& column_schema = file_schema[local_id];
1040
12
        DORIS_CHECK(column_schema != nullptr);
1041
12
        if (column_schema->leaf_column_id < 0 ||
1042
12
            column_schema->leaf_column_id >= row_group_metadata.num_columns()) {
1043
0
            update_counter_if_not_null(_scan_profile.dict_filter_unsupported_columns, 1);
1044
0
            continue;
1045
0
        }
1046
12
        auto column_chunk = row_group_metadata.ColumnChunk(column_schema->leaf_column_id);
1047
12
        if (column_chunk == nullptr ||
1048
12
            !supports_row_level_dictionary_filter(*column_schema, *column_chunk)) {
1049
0
            update_counter_if_not_null(_scan_profile.dict_filter_unsupported_columns, 1);
1050
0
            continue;
1051
0
        }
1052
1053
12
        ParquetDictionaryWords dict_words;
1054
12
        {
1055
12
            SCOPED_TIMER(_scan_profile.dict_filter_read_dict_time);
1056
12
            if (!read_dictionary_words(file_context.file_reader.get(), row_group_idx,
1057
12
                                       column_schema->leaf_column_id, *column_schema,
1058
12
                                       &dict_words)) {
1059
0
                update_counter_if_not_null(_scan_profile.dict_filter_read_failures, 1);
1060
0
                continue;
1061
0
            }
1062
12
        }
1063
1064
        // Build a safe dictionary prefilter from the dictionary-filter interface instead of
1065
        // executing the row expression on a temporary dictionary block. For compound AND,
1066
        // VCompoundPred intentionally evaluates only dictionary-capable children, so residual
1067
        // predicates still run later on surviving rows.
1068
12
        IColumn::Filter dictionary_filter;
1069
12
        DictionaryResidualConjuncts residual_conjuncts;
1070
12
        {
1071
12
            SCOPED_TIMER(_scan_profile.dict_filter_build_time);
1072
12
            dictionary_filter = build_dictionary_entry_filter(block_position, *column_schema,
1073
12
                                                              conjunct_it->second, dict_words);
1074
12
            residual_conjuncts = build_dictionary_residual_conjuncts(conjunct_it->second);
1075
12
        }
1076
1077
        // The bitmap is keyed by Parquet dictionary id. Later data-page reads evaluate the
1078
        // predicate with an integer lookup and only materialize STRING values for surviving rows.
1079
12
        _current_dictionary_filters.emplace(local_id, std::move(dictionary_filter));
1080
12
        _current_dictionary_residual_conjuncts.emplace(local_id, std::move(residual_conjuncts));
1081
12
        update_counter_if_not_null(_scan_profile.dict_filter_columns, 1);
1082
12
    }
1083
49
    return Status::OK();
1084
54
}
1085
1086
Status ParquetScanScheduler::read_filter_columns(int64_t batch_rows,
1087
                                                 const format::FileScanRequest& request,
1088
                                                 Block* file_block, SelectionVector* selection,
1089
                                                 uint16_t* selected_rows,
1090
                                                 int64_t* conjunct_filtered_rows,
1091
166
                                                 bool* predicate_columns_filtered) {
1092
166
    DORIS_CHECK(predicate_columns_filtered != nullptr);
1093
166
    *predicate_columns_filtered = false;
1094
166
    if (!request.conjuncts.empty() || !request.delete_conjuncts.empty()) {
1095
96
        selection->resize(static_cast<size_t>(batch_rows));
1096
96
    }
1097
166
    const auto schedule = build_predicate_conjunct_schedule(request);
1098
166
    const bool can_read_predicate_columns_round_by_round =
1099
166
            !schedule.single_column_conjuncts.empty();
1100
166
    std::vector<uint32_t> read_column_positions;
1101
166
    read_column_positions.reserve(request.predicate_columns.size());
1102
1103
166
    auto read_predicate_column = [&](ParquetColumnReader* column_reader, size_t block_position,
1104
166
                                     ColumnId local_id, bool* used_dictionary_filter) -> Status {
1105
104
        DORIS_CHECK(used_dictionary_filter != nullptr);
1106
104
        *used_dictionary_filter = false;
1107
104
        DCHECK(remove_nullable(column_reader->type())
1108
0
                       ->equals(*remove_nullable(file_block->get_by_position(block_position).type)))
1109
0
                << column_reader->type()->get_name() << " "
1110
0
                << file_block->get_by_position(block_position).type->get_name() << " "
1111
0
                << column_reader->name() << " " << file_block->get_by_position(block_position).name;
1112
104
        auto column = file_block->get_by_position(block_position).column->assert_mutable();
1113
104
        SCOPED_TIMER(_scan_profile.column_read_time);
1114
104
        const auto dictionary_filter_it = _current_dictionary_filters.find(local_id);
1115
104
        if (dictionary_filter_it != _current_dictionary_filters.end()) {
1116
12
            const uint16_t selected_rows_before = *selected_rows;
1117
12
            IColumn::Filter compact_filter;
1118
12
            bool used_filter = false;
1119
12
            RETURN_IF_ERROR(column_reader->select_with_dictionary_filter(
1120
12
                    *selection, *selected_rows, batch_rows, dictionary_filter_it->second, column,
1121
12
                    &compact_filter, &used_filter));
1122
12
            if (used_filter) {
1123
12
                DORIS_CHECK(compact_filter.size() == selected_rows_before);
1124
12
                const uint16_t new_selected_rows = count_selected_rows(compact_filter);
1125
12
                const auto filtered_rows = static_cast<int64_t>(selected_rows_before) -
1126
12
                                           static_cast<int64_t>(new_selected_rows);
1127
12
                if (conjunct_filtered_rows != nullptr) {
1128
12
                    *conjunct_filtered_rows += filtered_rows;
1129
12
                }
1130
12
                update_counter_if_not_null(_scan_profile.rows_filtered_by_dict_filter,
1131
12
                                           filtered_rows);
1132
12
                if (new_selected_rows != selected_rows_before) {
1133
                    // The dictionary reader has already appended only surviving values for the
1134
                    // current column. Apply the compact row filter only to columns read before this
1135
                    // one, then update the shared selection for later predicate/lazy columns.
1136
8
                    RETURN_IF_ERROR(filter_read_predicate_columns(file_block, read_column_positions,
1137
8
                                                                  compact_filter));
1138
8
                    *selected_rows = apply_compact_filter_to_selection(compact_filter, selection,
1139
8
                                                                       selected_rows_before);
1140
8
                    *predicate_columns_filtered = true;
1141
8
                }
1142
12
                file_block->replace_by_position(block_position, std::move(column));
1143
12
                read_column_positions.push_back(cast_set<uint32_t>(block_position));
1144
12
                *used_dictionary_filter = true;
1145
12
                return Status::OK();
1146
12
            }
1147
12
        }
1148
1149
92
        if (*selected_rows == batch_rows) {
1150
90
            int64_t column_rows = 0;
1151
90
            RETURN_IF_ERROR(column_reader->read(batch_rows, column, &column_rows));
1152
90
            if (column_rows != batch_rows) {
1153
0
                return Status::Corruption(
1154
0
                        "Parquet filter column {} returned {} rows, expected {} rows",
1155
0
                        column_reader->name(), column_rows, batch_rows);
1156
0
            }
1157
90
        } else {
1158
2
            [[maybe_unused]] auto old_size = column->size();
1159
2
            RETURN_IF_ERROR(column_reader->select(*selection, *selected_rows, batch_rows, column));
1160
2
            if (column->size() != old_size + *selected_rows) {
1161
0
                return Status::Corruption(
1162
0
                        "Parquet selected filter column {} returned {} rows, expected {} rows",
1163
0
                        column_reader->name(), column->size(), old_size + *selected_rows);
1164
0
            }
1165
2
            *predicate_columns_filtered = true;
1166
2
        }
1167
92
        file_block->replace_by_position(block_position, std::move(column));
1168
92
        read_column_positions.push_back(cast_set<uint32_t>(block_position));
1169
92
        return Status::OK();
1170
92
    };
1171
1172
166
    auto execute_scheduled_conjuncts = [&](const VExprContextSPtrs& conjuncts) -> Status {
1173
103
        if (conjuncts.empty() || *selected_rows == 0) {
1174
57
            return Status::OK();
1175
57
        }
1176
46
        const uint16_t selected_rows_before = *selected_rows;
1177
46
        IColumn::Filter compact_filter;
1178
46
        bool can_filter_all = false;
1179
46
        RETURN_IF_ERROR(execute_compact_filter_conjuncts(
1180
46
                conjuncts, selected_rows_before, file_block, &compact_filter, &can_filter_all));
1181
46
        if (can_filter_all) {
1182
11
            compact_filter.resize_fill(selected_rows_before, 0);
1183
11
        }
1184
46
        const uint16_t new_selected_rows = can_filter_all ? 0 : count_selected_rows(compact_filter);
1185
46
        if (conjunct_filtered_rows != nullptr) {
1186
46
            *conjunct_filtered_rows += static_cast<int64_t>(selected_rows_before) -
1187
46
                                       static_cast<int64_t>(new_selected_rows);
1188
46
        }
1189
46
        if (new_selected_rows != selected_rows_before) {
1190
            // All columns read so far are already compacted to the current selection. Apply the
1191
            // compact filter to those columns and the selection vector together, so later predicate
1192
            // columns can read only rows that survived previous predicate rounds.
1193
24
            RETURN_IF_ERROR(filter_read_predicate_columns(file_block, read_column_positions,
1194
24
                                                          compact_filter));
1195
24
            *selected_rows = can_filter_all
1196
24
                                     ? 0
1197
24
                                     : apply_compact_filter_to_selection(compact_filter, selection,
1198
13
                                                                         selected_rows_before);
1199
24
            *predicate_columns_filtered = true;
1200
24
        }
1201
46
        return Status::OK();
1202
46
    };
1203
1204
166
    auto execute_scheduled_dictionary_residual_conjuncts =
1205
166
            [&](const DictionaryResidualConjuncts& conjuncts) -> Status {
1206
12
        if (conjuncts.empty() || *selected_rows == 0) {
1207
9
            return Status::OK();
1208
9
        }
1209
3
        const uint16_t selected_rows_before = *selected_rows;
1210
3
        IColumn::Filter compact_filter;
1211
3
        bool can_filter_all = false;
1212
3
        RETURN_IF_ERROR(execute_compact_dictionary_residual_conjuncts(
1213
3
                conjuncts, selected_rows_before, file_block, &compact_filter, &can_filter_all));
1214
3
        if (can_filter_all) {
1215
1
            compact_filter.resize_fill(selected_rows_before, 0);
1216
1
        }
1217
3
        const uint16_t new_selected_rows = can_filter_all ? 0 : count_selected_rows(compact_filter);
1218
3
        if (conjunct_filtered_rows != nullptr) {
1219
3
            *conjunct_filtered_rows += static_cast<int64_t>(selected_rows_before) -
1220
3
                                       static_cast<int64_t>(new_selected_rows);
1221
3
        }
1222
3
        if (new_selected_rows != selected_rows_before) {
1223
            // Dictionary-covered children have already reduced the compact block. Apply only the
1224
            // residual child filters here, then keep the same compacted-column invariant as the
1225
            // normal conjunct path for later predicate rounds.
1226
3
            RETURN_IF_ERROR(filter_read_predicate_columns(file_block, read_column_positions,
1227
3
                                                          compact_filter));
1228
3
            *selected_rows = can_filter_all
1229
3
                                     ? 0
1230
3
                                     : apply_compact_filter_to_selection(compact_filter, selection,
1231
2
                                                                         selected_rows_before);
1232
3
            *predicate_columns_filtered = true;
1233
3
        }
1234
3
        return Status::OK();
1235
3
    };
1236
1237
166
    auto execute_scheduled_conjuncts_with_profile =
1238
166
            [&](const VExprContextSPtrs& conjuncts) -> Status {
1239
103
        if (_scan_profile.predicate_filter_time == nullptr) {
1240
55
            return execute_scheduled_conjuncts(conjuncts);
1241
55
        }
1242
48
        SCOPED_TIMER(_scan_profile.predicate_filter_time);
1243
48
        return execute_scheduled_conjuncts(conjuncts);
1244
103
    };
1245
1246
166
    auto execute_scheduled_dictionary_residual_conjuncts_with_profile =
1247
166
            [&](const DictionaryResidualConjuncts& conjuncts) -> Status {
1248
12
        if (_scan_profile.predicate_filter_time == nullptr) {
1249
7
            return execute_scheduled_dictionary_residual_conjuncts(conjuncts);
1250
7
        }
1251
5
        SCOPED_TIMER(_scan_profile.predicate_filter_time);
1252
5
        return execute_scheduled_dictionary_residual_conjuncts(conjuncts);
1253
12
    };
1254
1255
166
    auto execute_scheduled_delete_conjuncts = [&]() -> Status {
1256
57
        if (request.delete_conjuncts.empty() || *selected_rows == 0) {
1257
56
            return Status::OK();
1258
56
        }
1259
1
        const uint16_t selected_rows_before = *selected_rows;
1260
1
        IColumn::Filter compact_filter;
1261
1
        bool can_filter_all = false;
1262
1
        RETURN_IF_ERROR(execute_compact_delete_conjuncts(request.delete_conjuncts,
1263
1
                                                         selected_rows_before, file_block,
1264
1
                                                         &compact_filter, &can_filter_all));
1265
1
        if (can_filter_all) {
1266
0
            compact_filter.resize_fill(selected_rows_before, 0);
1267
0
        }
1268
1
        if (can_filter_all || count_selected_rows(compact_filter) != selected_rows_before) {
1269
1
            RETURN_IF_ERROR(filter_read_predicate_columns(file_block, read_column_positions,
1270
1
                                                          compact_filter));
1271
1
            *selected_rows = can_filter_all
1272
1
                                     ? 0
1273
1
                                     : apply_compact_filter_to_selection(compact_filter, selection,
1274
1
                                                                         selected_rows_before);
1275
1
            *predicate_columns_filtered = true;
1276
1
        }
1277
1
        return Status::OK();
1278
1
    };
1279
1280
166
    auto read_all_predicate_columns = [&]() -> Status {
1281
109
        for (const auto& [fid, column_reader] : _current_predicate_columns) {
1282
45
            auto position_it = request.local_positions.find(format::LocalColumnId(fid));
1283
45
            DORIS_CHECK(position_it != request.local_positions.end());
1284
45
            bool used_dictionary_filter = false;
1285
45
            RETURN_IF_ERROR(read_predicate_column(column_reader.get(), position_it->second.value(),
1286
45
                                                  fid, &used_dictionary_filter));
1287
45
        }
1288
109
        return Status::OK();
1289
109
    };
1290
1291
166
    if (!can_read_predicate_columns_round_by_round) {
1292
109
        RETURN_IF_ERROR(read_all_predicate_columns());
1293
109
        if (_scan_profile.predicate_filter_time == nullptr) {
1294
43
            return execute_batch_filters(request, batch_rows, file_block, selection, selected_rows,
1295
43
                                         conjunct_filtered_rows);
1296
43
        }
1297
66
        SCOPED_TIMER(_scan_profile.predicate_filter_time);
1298
66
        return execute_batch_filters(request, batch_rows, file_block, selection, selected_rows,
1299
66
                                     conjunct_filtered_rows);
1300
109
    }
1301
1302
57
    auto read_round_by_round = [&]() -> Status {
1303
        // Single-column conjuncts can be evaluated immediately after their column is read. Once
1304
        // selection shrinks, later predicate columns use ParquetColumnReader::select() so the
1305
        // reader skips rows already rejected by earlier predicates instead of materializing them.
1306
104
        for (size_t idx = 0; idx < request.predicate_columns.size(); ++idx) {
1307
59
            const auto& col = request.predicate_columns[idx];
1308
59
            const auto fid = col.local_id();
1309
59
            auto reader_it = _current_predicate_columns.find(fid);
1310
59
            DORIS_CHECK(reader_it != _current_predicate_columns.end());
1311
59
            auto position_it = request.local_positions.find(col.column_id());
1312
59
            DORIS_CHECK(position_it != request.local_positions.end());
1313
59
            const auto block_position = position_it->second.value();
1314
59
            bool used_dictionary_filter = false;
1315
59
            RETURN_IF_ERROR(read_predicate_column(reader_it->second.get(), block_position, fid,
1316
59
                                                  &used_dictionary_filter));
1317
59
            if (*selected_rows == 0) {
1318
0
                for (size_t remaining_idx = idx + 1;
1319
0
                     remaining_idx < request.predicate_columns.size(); ++remaining_idx) {
1320
0
                    const auto remaining_fid = request.predicate_columns[remaining_idx].local_id();
1321
0
                    auto remaining_reader_it = _current_predicate_columns.find(remaining_fid);
1322
0
                    DORIS_CHECK(remaining_reader_it != _current_predicate_columns.end());
1323
0
                    RETURN_IF_ERROR(remaining_reader_it->second->skip(batch_rows));
1324
0
                }
1325
0
                return Status::OK();
1326
0
            }
1327
59
            const auto conjunct_it = schedule.single_column_conjuncts.find(block_position);
1328
59
            if (conjunct_it == schedule.single_column_conjuncts.end()) {
1329
1
                continue;
1330
1
            }
1331
58
            if (used_dictionary_filter) {
1332
12
                const auto residual_it = _current_dictionary_residual_conjuncts.find(fid);
1333
12
                DORIS_CHECK(residual_it != _current_dictionary_residual_conjuncts.end());
1334
12
                RETURN_IF_ERROR(execute_scheduled_dictionary_residual_conjuncts_with_profile(
1335
12
                        residual_it->second));
1336
46
            } else {
1337
46
                RETURN_IF_ERROR(execute_scheduled_conjuncts_with_profile(conjunct_it->second));
1338
46
            }
1339
58
            if (*selected_rows != 0) {
1340
46
                continue;
1341
46
            }
1342
14
            for (size_t remaining_idx = idx + 1; remaining_idx < request.predicate_columns.size();
1343
12
                 ++remaining_idx) {
1344
2
                const auto remaining_fid = request.predicate_columns[remaining_idx].local_id();
1345
2
                auto remaining_reader_it = _current_predicate_columns.find(remaining_fid);
1346
2
                DORIS_CHECK(remaining_reader_it != _current_predicate_columns.end());
1347
2
                RETURN_IF_ERROR(remaining_reader_it->second->skip(batch_rows));
1348
2
            }
1349
12
            return Status::OK();
1350
12
        }
1351
45
        return Status::OK();
1352
57
    };
1353
1354
57
    RETURN_IF_ERROR(read_round_by_round());
1355
57
    RETURN_IF_ERROR(execute_scheduled_conjuncts_with_profile(schedule.remaining_conjuncts));
1356
57
    if (_scan_profile.predicate_filter_time == nullptr) {
1357
31
        return execute_scheduled_delete_conjuncts();
1358
31
    }
1359
26
    SCOPED_TIMER(_scan_profile.predicate_filter_time);
1360
26
    return execute_scheduled_delete_conjuncts();
1361
57
}
1362
1363
bool ParquetScanScheduler::prepare_current_row_group_reader(
1364
        ParquetFileContext& file_context,
1365
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
1366
165
        const format::FileScanRequest& request, int row_group_idx) {
1367
165
    if (file_context.metadata == nullptr) {
1368
0
        return false;
1369
0
    }
1370
165
    const auto ranges = build_row_group_prefetch_ranges(
1371
165
            *file_context.metadata, file_schema, request_scan_columns(request), row_group_idx);
1372
165
    const size_t avg_io_size = detail::average_prefetch_range_size(ranges);
1373
165
    return file_context.set_random_access_ranges(ranges, avg_io_size, _profile,
1374
165
                                                 _merge_read_slice_size);
1375
165
}
1376
1377
void ParquetScanScheduler::prefetch_current_row_group_columns(
1378
        ParquetFileContext& file_context,
1379
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
1380
23
        const std::vector<format::LocalColumnIndex>& scan_columns, bool* prefetched) {
1381
23
    DORIS_CHECK(prefetched != nullptr);
1382
23
    if (_current_merge_range_active || *prefetched || scan_columns.empty() ||
1383
23
        _current_row_group_id < 0 || file_context.metadata == nullptr) {
1384
22
        return;
1385
22
    }
1386
1
    *prefetched = true;
1387
    // The scanner request separates predicate and non-predicate columns so Parquet can read
1388
    // predicate columns first and lazily materialize the rest. Keep the same contract for
1389
    // prefetch: callers decide which side to warm, and this helper only translates that selected
1390
    // projection into physical column-chunk byte ranges for the current row group.
1391
1
    file_context.prefetch_ranges(
1392
1
            build_row_group_prefetch_ranges(*file_context.metadata, file_schema, scan_columns,
1393
1
                                            _current_row_group_id),
1394
1
            nullptr);
1395
1
}
1396
1397
Status ParquetScanScheduler::read_current_row_group_batch(
1398
        ParquetFileContext& file_context,
1399
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema, int64_t batch_rows,
1400
        const format::FileScanRequest& request, int64_t batch_first_file_row, Block* file_block,
1401
177
        size_t* rows) {
1402
177
    if (_scan_profile.total_batches != nullptr) {
1403
92
        COUNTER_UPDATE(_scan_profile.total_batches, 1);
1404
92
    }
1405
177
    if (_scan_profile.raw_rows_read != nullptr) {
1406
92
        COUNTER_UPDATE(_scan_profile.raw_rows_read, batch_rows);
1407
92
    }
1408
177
    _raw_rows_read += batch_rows;
1409
177
    if (_current_predicate_columns.empty() && _current_non_predicate_columns.empty()) {
1410
11
        *rows = static_cast<size_t>(batch_rows);
1411
11
        materialize_count_star_placeholders(request, *rows, file_block);
1412
11
        if (_scan_profile.selected_rows != nullptr) {
1413
0
            COUNTER_UPDATE(_scan_profile.selected_rows, batch_rows);
1414
0
        }
1415
11
        return Status::OK();
1416
11
    }
1417
166
    SelectionVector selection;
1418
166
    DORIS_CHECK(batch_rows <= std::numeric_limits<uint16_t>::max());
1419
166
    uint16_t selected_rows = static_cast<uint16_t>(batch_rows);
1420
166
    int64_t conjunct_filtered_rows = 0;
1421
166
    bool predicate_columns_filtered = false;
1422
166
    RETURN_IF_ERROR(read_filter_columns(batch_rows, request, file_block, &selection, &selected_rows,
1423
166
                                        &conjunct_filtered_rows, &predicate_columns_filtered));
1424
166
    _predicate_filtered_rows += conjunct_filtered_rows;
1425
166
    mark_condition_cache_granules(selection, selected_rows, batch_first_file_row);
1426
1427
166
    const bool need_filter_output = selected_rows != batch_rows;
1428
166
    if (_scan_profile.selected_rows != nullptr) {
1429
92
        COUNTER_UPDATE(_scan_profile.selected_rows, selected_rows);
1430
92
    }
1431
166
    if (_scan_profile.rows_filtered_by_conjunct != nullptr) {
1432
92
        COUNTER_UPDATE(_scan_profile.rows_filtered_by_conjunct, conjunct_filtered_rows);
1433
92
    }
1434
166
    if (!_current_non_predicate_columns.empty() &&
1435
166
        _scan_profile.lazy_read_filtered_rows != nullptr) {
1436
77
        COUNTER_UPDATE(_scan_profile.lazy_read_filtered_rows, batch_rows - selected_rows);
1437
77
    }
1438
166
    if (selected_rows == 0 && _scan_profile.empty_selection_batches != nullptr) {
1439
18
        COUNTER_UPDATE(_scan_profile.empty_selection_batches, 1);
1440
18
    }
1441
166
    if (need_filter_output && !predicate_columns_filtered) {
1442
38
        IColumn::Filter output_filter = selection_to_filter(selection, selected_rows, batch_rows);
1443
43
        for (const auto& col : request.predicate_columns) {
1444
43
            auto position_it = request.local_positions.find(col.column_id());
1445
43
            DORIS_CHECK(position_it != request.local_positions.end());
1446
43
            const auto block_position = position_it->second.value();
1447
43
            RETURN_IF_CATCH_EXCEPTION(file_block->replace_by_position(
1448
43
                    block_position, file_block->get_by_position(block_position)
1449
43
                                            .column->filter(output_filter, selected_rows)));
1450
43
        }
1451
38
    }
1452
166
    if (selected_rows == 0) {
1453
        // Predicate readers have consumed this physical batch, but touching every lazy column here
1454
        // turns a long rejected prefix into `empty_batches * lazy_columns` Arrow calls. Record only
1455
        // the positional lag. If [0, 32), [32, 64), and [64, 96) are empty, the first surviving
1456
        // batch performs one skip(96) per lazy column. If the row group ends instead, reset drops the
1457
        // lazy readers without flushing because no value from them can be observed.
1458
18
        DORIS_CHECK(_pending_non_predicate_skip_rows <=
1459
18
                    std::numeric_limits<int64_t>::max() - batch_rows);
1460
18
        _pending_non_predicate_skip_rows += batch_rows;
1461
18
        *rows = 0;
1462
18
        return Status::OK();
1463
18
    }
1464
148
    if (!_current_merge_range_active && selected_rows > 0 &&
1465
148
        !_current_non_predicate_columns.empty()) {
1466
        // Do not prefetch lazy output columns until at least one row survives filtering. This is
1467
        // the same decision point where the v2 reader switches from predicate-only reads to
1468
        // materializing non-predicate columns, so fully filtered batches avoid unnecessary IO.
1469
0
        prefetch_current_row_group_columns(file_context, file_schema,
1470
0
                                           physical_non_predicate_columns(request),
1471
0
                                           &_current_non_predicate_prefetched);
1472
0
    }
1473
1474
148
    {
1475
148
        SCOPED_TIMER(_scan_profile.column_read_time);
1476
        // Bring lazy readers to the first row of the current physical batch before interpreting its
1477
        // selection vector. This also merges pending range gaps with fully filtered batches.
1478
148
        RETURN_IF_ERROR(flush_pending_non_predicate_skip_rows());
1479
174
        for (const auto& [fid, column_reader] : _current_non_predicate_columns) {
1480
174
            auto position_it = request.local_positions.find(format::LocalColumnId(fid));
1481
174
            DORIS_CHECK(position_it != request.local_positions.end());
1482
174
            const auto block_position = position_it->second.value();
1483
174
            auto column = file_block->get_by_position(block_position).column->assert_mutable();
1484
174
            DCHECK_EQ(file_block->get_by_position(block_position).type->get_primitive_type(),
1485
0
                      column_reader->type()->get_primitive_type())
1486
0
                    << type_to_string(file_block->get_by_position(block_position)
1487
0
                                              .type->get_primitive_type())
1488
0
                    << " " << type_to_string(column_reader->type()->get_primitive_type()) << " "
1489
0
                    << column_reader->name() << " " << fid << " " << block_position;
1490
174
            if (need_filter_output) {
1491
36
                [[maybe_unused]] auto old_size = column->size();
1492
36
                RETURN_IF_ERROR(
1493
36
                        column_reader->select(selection, selected_rows, batch_rows, column));
1494
36
                if (column->size() != old_size + selected_rows) {
1495
0
                    return Status::Corruption(
1496
0
                            "Parquet selected output column {} returned {} rows, expected {} rows",
1497
0
                            column_reader->name(), column->size(), old_size + selected_rows);
1498
0
                }
1499
138
            } else {
1500
138
                int64_t column_rows = 0;
1501
138
                RETURN_IF_ERROR(column_reader->read(batch_rows, column, &column_rows));
1502
138
                if (column_rows != batch_rows) {
1503
0
                    return Status::Corruption(
1504
0
                            "Parquet output column {} returned {} rows, expected {} rows",
1505
0
                            column_reader->name(), column_rows, batch_rows);
1506
0
                }
1507
138
            }
1508
174
            file_block->replace_by_position(block_position, std::move(column));
1509
174
        }
1510
148
    }
1511
148
    materialize_count_star_placeholders(request, selected_rows, file_block);
1512
148
    *rows = static_cast<size_t>(selected_rows);
1513
148
    return Status::OK();
1514
148
}
1515
1516
void ParquetScanScheduler::mark_condition_cache_granules(const SelectionVector& selection,
1517
                                                         uint16_t selected_rows,
1518
166
                                                         int64_t batch_first_file_row) {
1519
166
    if (!_condition_cache_ctx || _condition_cache_ctx->is_hit ||
1520
166
        !_condition_cache_ctx->filter_result) {
1521
165
        return;
1522
165
    }
1523
1
    auto& cache = *_condition_cache_ctx->filter_result;
1524
2.04k
    for (uint16_t selection_idx = 0; selection_idx < selected_rows; ++selection_idx) {
1525
2.04k
        const int64_t file_row = batch_first_file_row + selection.get_index(selection_idx);
1526
2.04k
        const int64_t granule = file_row / ConditionCacheContext::GRANULE_SIZE;
1527
2.04k
        const int64_t cache_idx = granule - _condition_cache_ctx->base_granule;
1528
2.04k
        if (cache_idx >= 0 && static_cast<size_t>(cache_idx) < cache.size()) {
1529
2.04k
            cache[static_cast<size_t>(cache_idx)] = true;
1530
2.04k
        }
1531
2.04k
    }
1532
1
}
1533
1534
Status ParquetScanScheduler::read_next_batch(
1535
        ParquetFileContext& file_context,
1536
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
1537
252
        const format::FileScanRequest& request, Block* file_block, size_t* rows, bool* eof) {
1538
252
    *rows = 0;
1539
380
    while (true) {
1540
380
        if (_current_row_group == nullptr) {
1541
258
            bool has_row_group = false;
1542
258
            RETURN_IF_ERROR(
1543
258
                    open_next_row_group(file_context, file_schema, request, &has_row_group));
1544
258
            if (!has_row_group) {
1545
93
                *eof = true;
1546
93
                return Status::OK();
1547
93
            }
1548
258
        }
1549
1550
287
        if (_current_range_idx >= _current_selected_ranges.size()) {
1551
            // Current row group finished, try next row group.
1552
110
            reset_current_row_group();
1553
110
            continue;
1554
110
        }
1555
1556
177
        const RowRange& current_range = _current_selected_ranges[_current_range_idx];
1557
177
        DORIS_CHECK(current_range.start >= 0);
1558
177
        DORIS_CHECK(current_range.length > 0);
1559
177
        DORIS_CHECK(current_range.start + current_range.length <= _current_row_group_rows);
1560
1561
177
        if (_current_row_group_rows_read < current_range.start) {
1562
            // Skip filtered rows according to row group level pruning.
1563
3
            RETURN_IF_ERROR(skip_current_row_group_rows(current_range.start -
1564
3
                                                        _current_row_group_rows_read));
1565
3
        }
1566
177
        DORIS_CHECK(_current_row_group_rows_read == current_range.start + _current_range_rows_read);
1567
177
        const int64_t remaining_rows = current_range.length - _current_range_rows_read;
1568
177
        if (remaining_rows <= 0) {
1569
            // Current range finished, try next range in the same row group.
1570
0
            ++_current_range_idx;
1571
0
            _current_range_rows_read = 0;
1572
0
            continue;
1573
0
        }
1574
1575
177
        const int64_t batch_rows = std::min<int64_t>(_batch_size, remaining_rows);
1576
177
        const int64_t physical_rows_read = batch_rows;
1577
177
        const int64_t batch_first_file_row =
1578
177
                _current_row_group_first_row + _current_row_group_rows_read;
1579
177
        RETURN_IF_ERROR(read_current_row_group_batch(file_context, file_schema, batch_rows, request,
1580
177
                                                     batch_first_file_row, file_block, rows));
1581
177
        _current_row_group_rows_read += physical_rows_read;
1582
177
        _current_range_rows_read += physical_rows_read;
1583
177
        if (_current_range_rows_read >= current_range.length) {
1584
165
            ++_current_range_idx;
1585
165
            _current_range_rows_read = 0;
1586
165
        }
1587
177
        if (*rows == 0) {
1588
18
            continue;
1589
18
        }
1590
159
        *eof = false;
1591
159
        return Status::OK();
1592
177
    }
1593
252
}
1594
1595
} // namespace doris::format::parquet