Coverage Report

Created: 2026-07-13 00:48

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