Coverage Report

Created: 2026-07-15 03:26

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