Coverage Report

Created: 2026-07-17 14:16

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