Coverage Report

Created: 2026-07-23 19:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format_v2/parquet/parquet_reader.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
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#include "format_v2/parquet/parquet_reader.h"
19
20
#include <algorithm>
21
#include <map>
22
#include <memory>
23
#include <optional>
24
#include <ranges>
25
#include <unordered_set>
26
#include <utility>
27
#include <vector>
28
29
#include "common/cast_set.h"
30
#include "core/assert_cast.h"
31
#include "core/block/block.h"
32
#include "core/data_type/data_type_array.h"
33
#include "core/data_type/data_type_factory.hpp"
34
#include "core/data_type/data_type_map.h"
35
#include "core/data_type/data_type_nullable.h"
36
#include "core/data_type/data_type_struct.h"
37
#include "format_v2/column_mapper.h"
38
#include "format_v2/parquet/parquet_column_schema.h"
39
#include "format_v2/parquet/parquet_file_context.h"
40
#include "format_v2/parquet/parquet_scan.h"
41
#include "format_v2/parquet/parquet_statistics.h"
42
#include "format_v2/parquet/reader/count_column_reader.h"
43
#include "io/io_common.h"
44
#include "runtime/runtime_state.h"
45
46
namespace doris::format::parquet {
47
48
struct ParquetReaderScanState {
49
    ParquetFileContext file_context;
50
    std::vector<std::unique_ptr<ParquetColumnSchema>> file_schema;
51
    RowGroupScanPlan scan_plan;
52
    ParquetScanScheduler scheduler;
53
    const RuntimeState* runtime_state = nullptr;
54
    const cctz::time_zone* timezone = nullptr;
55
    bool enable_bloom_filter = false;
56
    bool enable_page_cache = false;
57
    bool enable_strict_mode = false;
58
};
59
60
364
Status validate_all_projected_leaves_supported(const ParquetColumnSchema& column_schema) {
61
364
    if (column_schema.kind == ParquetColumnSchemaKind::PRIMITIVE) {
62
323
        if (!column_schema.type_descriptor.unsupported_reason.empty()) {
63
2
            return Status::NotSupported("Unsupported parquet column '{}': {}", column_schema.name,
64
2
                                        column_schema.type_descriptor.unsupported_reason);
65
2
        }
66
321
        return Status::OK();
67
323
    }
68
59
    for (const auto& child : column_schema.children) {
69
59
        DORIS_CHECK(child != nullptr);
70
59
        RETURN_IF_ERROR(validate_all_projected_leaves_supported(*child));
71
59
    }
72
41
    return Status::OK();
73
41
}
74
75
Status validate_projected_leaves_supported(const ParquetColumnSchema& column_schema,
76
318
                                           const format::LocalColumnIndex& projection) {
77
318
    if (column_schema.kind == ParquetColumnSchemaKind::PRIMITIVE ||
78
318
        projection.project_all_children || projection.children.empty()) {
79
305
        return validate_all_projected_leaves_supported(column_schema);
80
305
    }
81
14
    for (const auto& child_projection : projection.children) {
82
14
        const auto child_it =
83
22
                std::ranges::find_if(column_schema.children, [&](const auto& child_schema) {
84
22
                    return child_schema->local_id == child_projection.local_id();
85
22
                });
86
14
        DORIS_CHECK(child_it != column_schema.children.end());
87
14
        RETURN_IF_ERROR(validate_projected_leaves_supported(**child_it, child_projection));
88
14
    }
89
13
    return Status::OK();
90
13
}
91
92
Status validate_requested_columns_supported(
93
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
94
214
        const format::FileScanRequest& request) {
95
    // Validate the projected native-schema leaves before pruning: checking a physical carrier at
96
    // a later read site can let an unsupported logical type silently pass when every row is pruned.
97
321
    auto validate_scan_column = [&](const format::LocalColumnIndex& projection) -> Status {
98
321
        const auto local_id = projection.local_id();
99
321
        if (local_id == format::ROW_POSITION_COLUMN_ID ||
100
321
            local_id == format::GLOBAL_ROWID_COLUMN_ID) {
101
40
            return Status::OK();
102
40
        }
103
281
        DORIS_CHECK(local_id >= 0 && local_id < static_cast<int32_t>(file_schema.size()));
104
281
        DORIS_CHECK(file_schema[local_id] != nullptr);
105
281
        return validate_projected_leaves_supported(*file_schema[local_id], projection);
106
321
    };
107
214
    for (const auto& column : request.predicate_columns) {
108
112
        RETURN_IF_ERROR(validate_scan_column(column));
109
112
    }
110
214
    for (const auto& column : request.non_predicate_columns) {
111
214
        if (!request.is_count_star_placeholder(column.column_id())) {
112
209
            RETURN_IF_ERROR(validate_scan_column(column));
113
209
        }
114
214
    }
115
213
    return Status::OK();
116
214
}
117
118
const ParquetColumnSchema& projected_root_schema(
119
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
120
9
        const format::LocalColumnIndex& projection) {
121
9
    const auto local_id = projection.local_id();
122
9
    DORIS_CHECK(local_id >= 0 && local_id < static_cast<int32_t>(file_schema.size()));
123
9
    DORIS_CHECK(file_schema[local_id] != nullptr);
124
9
    return *file_schema[local_id];
125
9
}
126
127
int64_t count_loaded_non_null_values(const ParquetColumnSchema& root_schema,
128
7
                                     const CountColumnReader& shape_reader, int64_t expected_rows) {
129
7
    const auto& def_levels = shape_reader.definition_levels();
130
7
    const auto& rep_levels = shape_reader.repetition_levels();
131
7
    const int64_t levels_written = shape_reader.levels_written();
132
7
    DORIS_CHECK(levels_written >= expected_rows);
133
7
    if (root_schema.max_repetition_level == 0) {
134
3
        DORIS_CHECK(levels_written == expected_rows);
135
3
        const int16_t non_null_definition_level = root_schema.nullable_definition_level;
136
3
        int64_t count = 0;
137
12
        for (int64_t level_idx = 0; level_idx < levels_written; ++level_idx) {
138
9
            count += def_levels[level_idx] >= non_null_definition_level ? 1 : 0;
139
9
        }
140
3
        return count;
141
3
    }
142
143
    // For repeated encodings, repetition level zero starts a top-level row. Empty MAP/LIST rows
144
    // have no entries but still carry a level slot; they are non-NULL and must be counted by
145
    // count(col). The root nullable level distinguishes a NULL top-level value from a non-NULL
146
    // value regardless of which repeated leaf represents its shape.
147
4
    const int16_t non_null_definition_level = root_schema.nullable_definition_level;
148
4
    int64_t counted_rows = 0;
149
4
    int64_t non_null_rows = 0;
150
26
    for (int64_t level_idx = 0; level_idx < levels_written && counted_rows < expected_rows;
151
22
         ++level_idx) {
152
22
        if (rep_levels[level_idx] != 0) {
153
2
            continue;
154
2
        }
155
20
        ++counted_rows;
156
20
        non_null_rows += def_levels[level_idx] >= non_null_definition_level ? 1 : 0;
157
20
    }
158
4
    DORIS_CHECK(counted_rows == expected_rows);
159
4
    return non_null_rows;
160
7
}
161
162
0
DataTypePtr nullable_like_original(const DataTypePtr& type, DataTypePtr nested_type) {
163
0
    return type != nullptr && type->is_nullable() ? make_nullable(nested_type) : nested_type;
164
0
}
165
166
3
int timestamp_tz_scale(const ParquetTypeDescriptor& type_descriptor) {
167
3
    switch (type_descriptor.time_unit) {
168
0
    case ParquetTimeUnit::MILLIS:
169
0
        return 3;
170
2
    case ParquetTimeUnit::MICROS:
171
3
    case ParquetTimeUnit::UNKNOWN:
172
3
    default:
173
3
        return 6;
174
3
    }
175
3
}
176
177
4
bool should_map_to_timestamp_tz(const ParquetColumnSchema& column_schema) {
178
4
    const auto& type_descriptor = column_schema.type_descriptor;
179
4
    return type_descriptor.physical_type == tparquet::Type::INT96 ||
180
4
           (type_descriptor.is_timestamp && type_descriptor.timestamp_is_adjusted_to_utc);
181
4
}
182
183
4
DataTypePtr apply_timestamp_tz_mapping(ParquetColumnSchema* column_schema) {
184
4
    DORIS_CHECK(column_schema != nullptr);
185
4
    if (column_schema->kind == ParquetColumnSchemaKind::PRIMITIVE) {
186
4
        if (should_map_to_timestamp_tz(*column_schema)) {
187
3
            const bool nullable =
188
3
                    column_schema->type != nullptr && column_schema->type->is_nullable();
189
3
            const auto scale = timestamp_tz_scale(column_schema->type_descriptor);
190
3
            column_schema->type = DataTypeFactory::instance().create_data_type(TYPE_TIMESTAMPTZ,
191
3
                                                                               nullable, 0, scale);
192
3
            column_schema->type_descriptor.doris_type = column_schema->type;
193
3
        }
194
4
        return column_schema->type;
195
4
    }
196
197
0
    std::vector<DataTypePtr> child_types;
198
0
    child_types.reserve(column_schema->children.size());
199
0
    for (auto& child : column_schema->children) {
200
0
        child_types.push_back(apply_timestamp_tz_mapping(child.get()));
201
0
    }
202
203
0
    if (column_schema->kind == ParquetColumnSchemaKind::LIST) {
204
0
        DORIS_CHECK(child_types.size() == 1);
205
0
        column_schema->type = nullable_like_original(
206
0
                column_schema->type, std::make_shared<DataTypeArray>(child_types[0]));
207
0
    } else if (column_schema->kind == ParquetColumnSchemaKind::MAP) {
208
0
        DORIS_CHECK(child_types.size() == 2);
209
0
        column_schema->type = nullable_like_original(
210
0
                column_schema->type, std::make_shared<DataTypeMap>(make_nullable(child_types[0]),
211
0
                                                                   make_nullable(child_types[1])));
212
0
    } else if (column_schema->kind == ParquetColumnSchemaKind::STRUCT) {
213
0
        Strings child_names;
214
0
        child_names.reserve(column_schema->children.size());
215
0
        for (const auto& child : column_schema->children) {
216
0
            child_names.push_back(child->name);
217
0
        }
218
0
        column_schema->type = nullable_like_original(
219
0
                column_schema->type, std::make_shared<DataTypeStruct>(child_types, child_names));
220
0
    }
221
0
    return column_schema->type;
222
4
}
223
224
static Status find_projected_minmax_leaf(const ParquetColumnSchema& column_schema,
225
                                         const format::LocalColumnIndex& projection,
226
15
                                         const ParquetColumnSchema** leaf_schema) {
227
15
    DORIS_CHECK(leaf_schema != nullptr);
228
15
    if (projection.project_all_children || projection.children.empty()) {
229
13
        if (column_schema.leaf_column_id < 0) {
230
2
            return Status::NotSupported(
231
2
                    "Parquet aggregate pushdown only supports primitive column {}",
232
2
                    column_schema.name);
233
2
        }
234
11
        if (column_schema.max_repetition_level > 0) {
235
0
            return Status::NotSupported(
236
0
                    "Parquet aggregate pushdown does not support repeated column {}",
237
0
                    column_schema.name);
238
0
        }
239
11
        *leaf_schema = &column_schema;
240
11
        return Status::OK();
241
11
    }
242
2
    if (projection.children.size() != 1) {
243
0
        return Status::NotSupported(
244
0
                "Parquet aggregate pushdown only supports a single nested leaf under column {}",
245
0
                column_schema.name);
246
0
    }
247
2
    const auto& child_projection = projection.children[0];
248
2
    const auto child_schema_it =
249
2
            std::ranges::find_if(column_schema.children, [&](const auto& child_schema) {
250
2
                return child_schema->local_id == child_projection.local_id();
251
2
            });
252
2
    if (child_schema_it != column_schema.children.end()) {
253
2
        return find_projected_minmax_leaf(**child_schema_it, child_projection, leaf_schema);
254
2
    }
255
0
    return Status::InvalidArgument("Invalid parquet aggregate projection local id {} for column {}",
256
0
                                   child_projection.local_id(), column_schema.name);
257
2
}
258
259
11
static Status validate_minmax_aggregate_statistics(const ParquetColumnSchema& column_schema) {
260
11
    switch (column_schema.type_descriptor.physical_type) {
261
1
    case tparquet::Type::BYTE_ARRAY:
262
2
    case tparquet::Type::FIXED_LEN_BYTE_ARRAY:
263
        // Arrow 17 does not expose Parquet's min/max exactness flags. Binary statistics may be
264
        // truncated bounds rather than values present in the file, so they are safe for pruning
265
        // but cannot be returned as exact aggregate results.
266
2
        return Status::NotSupported(
267
2
                "Parquet MIN/MAX aggregate pushdown requires exact statistics for column {}",
268
2
                column_schema.name);
269
9
    default:
270
9
        return Status::OK();
271
11
    }
272
11
}
273
274
void ParquetReader::_fill_column_definition(const ParquetColumnSchema& column_schema,
275
472
                                            format::ColumnDefinition* field) const {
276
472
    if (column_schema.parquet_field_id >= 0) {
277
159
        field->identifier = Field::create_field<TYPE_INT>(column_schema.parquet_field_id);
278
313
    } else {
279
313
        field->identifier = Field::create_field<TYPE_STRING>(column_schema.name);
280
313
    }
281
472
    field->local_id = column_schema.local_id;
282
472
    field->name = column_schema.name;
283
472
    field->type = column_schema.type != nullptr && !column_schema.type->is_nullable()
284
472
                          ? make_nullable(column_schema.type)
285
472
                          : column_schema.type;
286
472
    field->children.clear();
287
472
    field->children.reserve(column_schema.children.size());
288
472
    for (const auto& child : column_schema.children) {
289
81
        format::ColumnDefinition child_field;
290
81
        _fill_column_definition(*child, &child_field);
291
81
        field->children.push_back(std::move(child_field));
292
81
    }
293
472
}
294
295
ParquetReader::ParquetReader(std::shared_ptr<io::FileSystemProperties>& system_properties,
296
                             std::unique_ptr<io::FileDescription>& file_description,
297
                             std::shared_ptr<io::IOContext> io_ctx, RuntimeProfile* profile,
298
                             std::optional<format::GlobalRowIdContext> global_rowid_context,
299
                             bool enable_mapping_timestamp_tz, bool enable_mapping_varbinary)
300
224
        : FileReader(system_properties, file_description, io_ctx, profile),
301
224
          _global_rowid_context(global_rowid_context),
302
224
          _enable_mapping_timestamp_tz(enable_mapping_timestamp_tz),
303
224
          _enable_mapping_varbinary(enable_mapping_varbinary) {}
304
305
224
ParquetReader::~ParquetReader() = default;
306
307
223
Status ParquetReader::init(RuntimeState* state) {
308
223
    _init_profile();
309
223
    SCOPED_TIMER(_parquet_profile.total_time);
310
223
    if (_io_ctx != nullptr && _io_ctx->should_stop) {
311
0
        return Status::EndOfFile("stop");
312
0
    }
313
223
    RETURN_IF_ERROR(format::FileReader::init(state));
314
223
    if (_profile != nullptr) {
315
119
        COUNTER_UPDATE(_parquet_profile.file_reader_create_time,
316
119
                       _reader_statistics.file_reader_create_time);
317
119
        COUNTER_UPDATE(_parquet_profile.open_file_num, _reader_statistics.open_file_num);
318
119
    }
319
223
    _state = std::make_unique<ParquetReaderScanState>();
320
223
    _state->enable_bloom_filter =
321
223
            state != nullptr && state->query_options().enable_parquet_filter_by_bloom_filter;
322
223
    _state->enable_page_cache =
323
223
            state != nullptr && state->query_options().enable_parquet_file_page_cache;
324
223
    if (state != nullptr) {
325
223
        _state->runtime_state = state;
326
223
        _state->timezone = &state->timezone_obj();
327
223
        _state->enable_strict_mode = state->enable_strict_mode();
328
223
        _state->scheduler.set_timezone(&state->timezone_obj());
329
223
        _state->scheduler.set_enable_strict_mode(_state->enable_strict_mode);
330
223
        _state->scheduler.set_runtime_state(state);
331
223
    }
332
223
    int64_t merge_read_slice_size = -1;
333
223
    if (state != nullptr && state->query_options().__isset.merge_read_slice_size) {
334
223
        merge_read_slice_size = state->query_options().merge_read_slice_size;
335
223
    }
336
223
    _state->scheduler.set_merge_read_options(_profile, merge_read_slice_size);
337
223
    _state->scheduler.set_batch_size(_batch_size);
338
    // Opening the file parses the footer before any row group can be scheduled. Keep this timer
339
    // around the whole operation so footer/cache latency cannot disappear from a slow profile.
340
223
    {
341
223
        SCOPED_TIMER(_parquet_profile.parse_footer_time);
342
223
        RETURN_IF_ERROR(_state->file_context.open(
343
223
                _tracing_file_reader, _io_ctx.get(), _state->enable_page_cache, *_file_description,
344
223
                _enable_mapping_timestamp_tz, _enable_mapping_varbinary));
345
223
    }
346
223
    if (_profile != nullptr) {
347
119
        COUNTER_UPDATE(_parquet_profile.file_footer_read_calls,
348
119
                       _state->file_context.native_footer_read_calls);
349
119
        COUNTER_UPDATE(_parquet_profile.file_footer_hit_cache,
350
119
                       _state->file_context.native_footer_cache_hits);
351
119
    }
352
    // Build file schema from parquet metadata.
353
    // A file reader may expose raw file identifiers, such as Parquet field_id, through ColumnDefinition::identifier
354
223
    {
355
223
        SCOPED_TIMER(_parquet_profile.parse_meta_time);
356
223
        RETURN_IF_ERROR(build_parquet_column_schema(_state->file_context.native_metadata->schema(),
357
223
                                                    &_state->file_schema));
358
223
        if (_enable_mapping_timestamp_tz) {
359
4
            for (auto& column_schema : _state->file_schema) {
360
4
                apply_timestamp_tz_mapping(column_schema.get());
361
4
            }
362
3
        }
363
223
    }
364
0
    return Status::OK();
365
223
}
366
367
14
void ParquetReader::set_batch_size(size_t batch_size) {
368
14
    _batch_size = std::max<size_t>(1, batch_size);
369
14
    if (_state != nullptr) {
370
5
        _state->scheduler.set_batch_size(_batch_size);
371
5
    }
372
14
}
373
374
201
Status ParquetReader::get_schema(std::vector<format::ColumnDefinition>* file_schema) const {
375
201
    SCOPED_TIMER(_parquet_profile.total_time);
376
201
    if (file_schema == nullptr) {
377
0
        return Status::InvalidArgument("file_schema is null");
378
0
    }
379
201
    file_schema->clear();
380
201
    if (_state == nullptr || _state->file_context.native_metadata == nullptr) {
381
0
        return Status::Uninitialized("ParquetReader is not open");
382
0
    }
383
384
201
    file_schema->reserve(_state->file_schema.size());
385
592
    for (size_t column_idx = 0; column_idx < _state->file_schema.size(); ++column_idx) {
386
391
        format::ColumnDefinition field;
387
391
        _fill_column_definition(*_state->file_schema[column_idx], &field);
388
391
        DORIS_CHECK(field.local_id == static_cast<int32_t>(column_idx));
389
391
        file_schema->push_back(std::move(field));
390
391
    }
391
201
    if (_global_rowid_context.has_value()) {
392
3
        file_schema->push_back(format::global_rowid_column_definition());
393
3
    }
394
201
    return Status::OK();
395
201
}
396
397
std::unique_ptr<format::TableColumnMapper> ParquetReader::create_column_mapper(
398
98
        format::TableColumnMapperOptions options) const {
399
98
    return std::make_unique<format::ParquetColumnMapper>(std::move(options));
400
98
}
401
402
214
Status ParquetReader::open(std::shared_ptr<format::FileScanRequest> request) {
403
214
    SCOPED_TIMER(_parquet_profile.total_time);
404
214
    if (_state == nullptr || _state->file_context.native_metadata == nullptr) {
405
0
        return Status::Uninitialized("ParquetReader is not open");
406
0
    }
407
214
    auto request_snapshot = request;
408
214
    DORIS_CHECK(request_snapshot != nullptr);
409
214
    RETURN_IF_ERROR(format::FileReader::open(std::move(request)));
410
411
    // `local_positions.empty()` means all columns are needed by table reader
412
    // TODO(gabriel): It will happen only for TVF `select *` query.
413
214
    if (request_snapshot->local_positions.empty()) {
414
43
        for (const auto& col : request_snapshot->predicate_columns) {
415
13
            request_snapshot->local_positions.emplace(col.column_id(),
416
13
                                                      format::LocalIndex(col.column_id().value()));
417
13
        }
418
43
        for (const auto& col : request_snapshot->non_predicate_columns) {
419
27
            request_snapshot->local_positions.emplace(col.column_id(),
420
27
                                                      format::LocalIndex(col.column_id().value()));
421
27
        }
422
43
    }
423
424
214
    const auto num_fields = static_cast<int32_t>(_state->file_schema.size());
425
214
    for (const auto& col : request_snapshot->predicate_columns) {
426
112
        DORIS_CHECK(request_snapshot->local_positions.count(col.column_id()) > 0);
427
112
        const auto local_id = col.local_id();
428
112
        if (local_id == format::ROW_POSITION_COLUMN_ID ||
429
112
            local_id == format::GLOBAL_ROWID_COLUMN_ID) {
430
26
            continue;
431
26
        }
432
86
        DORIS_CHECK(local_id >= 0 && local_id < num_fields);
433
86
    }
434
214
    for (const auto& col : request_snapshot->non_predicate_columns) {
435
214
        DORIS_CHECK(request_snapshot->local_positions.count(col.column_id()) > 0);
436
214
        const auto local_id = col.local_id();
437
214
        if (local_id == format::ROW_POSITION_COLUMN_ID ||
438
214
            local_id == format::GLOBAL_ROWID_COLUMN_ID) {
439
14
            continue;
440
14
        }
441
200
        DORIS_CHECK(local_id >= 0 && local_id < num_fields);
442
200
    }
443
444
    // Reject requested unsupported logical leaves before row-group statistics, dictionaries,
445
    // bloom filters or page indexes inspect their physical fallback type. For example, a predicate
446
    // on TIME_MILLIS must fail here even when its INT32 statistics would prune every row group;
447
    // otherwise the same unsupported SELECT could fail or silently succeed depending on data.
448
214
    RETURN_IF_ERROR(validate_requested_columns_supported(_state->file_schema, *request_snapshot));
449
450
213
    RowGroupScanPlan row_group_plan;
451
213
    ParquetScanRange scan_range;
452
213
    scan_range.start_offset = _file_description->range_start_offset;
453
213
    scan_range.size = _file_description->range_size;
454
213
    scan_range.file_size = _file_description->file_size;
455
    // Get selected ranges in row groups according to metadata (Row-Group level index and Page Index including Zonemap, Dictionary, Bloom Filter).
456
213
    RETURN_IF_ERROR(plan_parquet_row_groups(
457
213
            *_state->file_context.native_metadata, _state->file_schema, *request_snapshot,
458
213
            scan_range, _state->enable_bloom_filter, &row_group_plan, _state->timezone,
459
213
            _state->runtime_state, &_state->file_context,
460
213
            _parquet_profile.column_reader_profile()));
461
213
    if (_profile != nullptr) {
462
115
        _parquet_profile.update_pruning_stats(row_group_plan.pruning_stats);
463
115
    }
464
    // Native page readers admit exact validated page payloads to cache. Do not pre-register whole
465
    // column chunks here: footer offsets are untrusted and this obsolete range map is not consumed.
466
213
    _state->scan_plan = row_group_plan;
467
213
    _state->scheduler.set_page_skip_profile(_parquet_profile.page_skip_profile());
468
213
    if (_profile != nullptr) {
469
115
        _state->scheduler.set_pruning_profile(&_parquet_profile);
470
115
    }
471
213
    _state->scheduler.set_global_rowid_context(_global_rowid_context);
472
213
    _state->scheduler.set_scan_profile(_parquet_profile.scan_profile());
473
213
    _state->scheduler.set_plan(std::move(row_group_plan));
474
213
    _eof = _state->scheduler.empty();
475
213
    return Status::OK();
476
213
}
477
478
346
Status ParquetReader::get_block(Block* file_block, size_t* rows, bool* eof) {
479
346
    SCOPED_TIMER(_parquet_profile.total_time);
480
346
    if (_state == nullptr || _state->file_context.native_metadata == nullptr) {
481
0
        return Status::Uninitialized("ParquetReader is not open");
482
0
    }
483
346
    *rows = 0;
484
346
    if (_io_ctx != nullptr && _io_ctx->should_stop) {
485
0
        *eof = true;
486
0
        return Status::OK();
487
0
    }
488
346
    if (_eof) {
489
2
        *eof = true;
490
2
        return Status::OK();
491
2
    }
492
344
    auto request_snapshot = _request;
493
344
    if (request_snapshot == nullptr) {
494
0
        return Status::Cancelled("ParquetReader is closed");
495
0
    }
496
497
344
    const auto predicate_filtered_rows_before = _state->scheduler.predicate_filtered_rows();
498
344
    const auto raw_rows_read_before = _state->scheduler.raw_rows_read();
499
344
    Status st = _state->scheduler.read_next_batch(_state->file_context, _state->file_schema,
500
344
                                                  *request_snapshot, file_block, rows, eof);
501
344
    if (!st.ok()) {
502
1
        if (_io_ctx != nullptr && _io_ctx->should_stop) {
503
0
            *rows = 0;
504
0
            *eof = true;
505
0
            return Status::OK();
506
0
        }
507
1
        return st;
508
1
    }
509
343
    _sync_page_cache_profile();
510
343
    if (_io_ctx != nullptr) {
511
121
        _io_ctx->predicate_filtered_rows +=
512
121
                _state->scheduler.predicate_filtered_rows() - predicate_filtered_rows_before;
513
121
    }
514
343
    const auto raw_rows_read = _state->scheduler.raw_rows_read();
515
343
    DORIS_CHECK(raw_rows_read >= raw_rows_read_before);
516
343
    _record_scan_rows(raw_rows_read - raw_rows_read_before);
517
343
    _eof = *eof;
518
343
    return Status::OK();
519
344
}
520
521
30
bool ParquetReader::_should_stop() const {
522
30
    return _io_ctx != nullptr && _io_ctx->should_stop;
523
30
}
524
525
14
Status ParquetReader::_stop_status_if_requested(const Status& status) const {
526
14
    if (!status.ok() && _should_stop()) {
527
0
        return Status::EndOfFile("stop");
528
0
    }
529
14
    return status;
530
14
}
531
532
466
void ParquetReader::_sync_page_cache_profile() {
533
466
    if (_profile == nullptr || _state == nullptr) {
534
171
        return;
535
171
    }
536
295
    const auto stats = _state->file_context.page_cache_stats();
537
295
    COUNTER_UPDATE(_parquet_profile.page_read_counter,
538
295
                   stats.read_count - _reported_page_cache_stats.read_count);
539
295
    COUNTER_UPDATE(_parquet_profile.page_cache_write_counter,
540
295
                   stats.write_count - _reported_page_cache_stats.write_count);
541
295
    COUNTER_UPDATE(
542
295
            _parquet_profile.page_cache_compressed_write_counter,
543
295
            stats.compressed_write_count - _reported_page_cache_stats.compressed_write_count);
544
295
    COUNTER_UPDATE(_parquet_profile.page_cache_hit_counter,
545
295
                   stats.hit_count - _reported_page_cache_stats.hit_count);
546
295
    COUNTER_UPDATE(_parquet_profile.page_cache_missing_counter,
547
295
                   stats.miss_count - _reported_page_cache_stats.miss_count);
548
295
    COUNTER_UPDATE(_parquet_profile.page_cache_compressed_hit_counter,
549
295
                   stats.compressed_hit_count - _reported_page_cache_stats.compressed_hit_count);
550
295
    _reported_page_cache_stats = stats;
551
295
}
552
553
2
void ParquetReader::set_condition_cache_context(std::shared_ptr<ConditionCacheContext> ctx) {
554
2
    if (_state == nullptr) {
555
0
        return;
556
0
    }
557
2
    _state->scheduler.set_condition_cache_context(std::move(ctx));
558
2
    if (_io_ctx != nullptr) {
559
        // Condition-cache HIT filters row ranges before batch reading, so skipped rows never belong
560
        // to a later get_block() batch. Report the plan-level skipped rows at the same point where
561
        // the scan plan is rewritten.
562
1
        _io_ctx->condition_cache_filtered_rows += _state->scheduler.condition_cache_filtered_rows();
563
1
    }
564
2
}
565
566
0
int64_t ParquetReader::get_total_rows() const {
567
0
    if (_state == nullptr) {
568
0
        return 0;
569
0
    }
570
0
    int64_t rows = 0;
571
0
    for (const auto& row_group_plan : _state->scan_plan.row_groups) {
572
0
        rows += row_group_plan.row_group_rows;
573
0
    }
574
0
    return rows;
575
0
}
576
577
Status ParquetReader::get_aggregate_result(const format::FileAggregateRequest& request,
578
30
                                           format::FileAggregateResult* result) {
579
30
    SCOPED_TIMER(_parquet_profile.total_time);
580
30
    DORIS_CHECK(result != nullptr);
581
30
    if (_state == nullptr || _state->file_context.native_metadata == nullptr) {
582
0
        return Status::Uninitialized("ParquetReader is not open");
583
0
    }
584
30
    if (_should_stop()) {
585
1
        return Status::EndOfFile("stop");
586
1
    }
587
29
    result->count = 0;
588
29
    result->columns.clear();
589
29
    if (request.agg_type != TPushAggOp::type::COUNT &&
590
29
        request.agg_type != TPushAggOp::type::MINMAX) {
591
1
        return Status::NotSupported("Unsupported parquet aggregate pushdown type {}",
592
1
                                    request.agg_type);
593
1
    }
594
595
    // Aggregate pushdown bypasses the scheduler but still requires the exact pruned row-group set.
596
    // Finish lazy remote probes here; normal scans keep them at current-row-group granularity.
597
28
    RETURN_IF_ERROR(finalize_parquet_row_group_plans(
598
28
            *_state->file_context.native_metadata, _state->file_schema, *_request,
599
28
            _state->enable_bloom_filter, &_state->scan_plan, _state->timezone,
600
28
            _state->runtime_state, &_state->file_context, _parquet_profile.column_reader_profile(),
601
28
            _profile == nullptr ? nullptr : &_parquet_profile));
602
603
28
    for (const auto& aggregate_column : request.columns) {
604
24
        const auto local_id = aggregate_column.projection.local_id();
605
24
        if (local_id < 0 || local_id >= static_cast<int32_t>(_state->file_schema.size())) {
606
1
            return Status::InvalidArgument("Invalid parquet aggregate column id {}", local_id);
607
1
        }
608
23
        DORIS_CHECK(_state->file_schema[local_id] != nullptr);
609
        // Aggregate pushdown can return directly from footer statistics without constructing a
610
        // column reader. Validate first so MIN/MAX(TIME_MILLIS), or an all-pruned COUNT request,
611
        // cannot expose the physical INT32 fallback as a supported logical value.
612
23
        RETURN_IF_ERROR(validate_projected_leaves_supported(*_state->file_schema[local_id],
613
23
                                                            aggregate_column.projection));
614
23
    }
615
616
    // Aggregate row count in all selected row groups. For MIN/MAX aggregate, this is used to determine whether there is no row group selected.
617
41
    for (const auto& row_group_plan : _state->scan_plan.row_groups) {
618
41
        const auto& row_group_metadata = _state->file_context.native_metadata->to_thrift()
619
41
                                                 .row_groups[row_group_plan.row_group_id];
620
41
        result->count += row_group_metadata.num_rows;
621
41
    }
622
26
    if (request.agg_type == TPushAggOp::type::COUNT) {
623
15
        if (request.columns.empty()) {
624
6
            return Status::OK();
625
6
        }
626
9
        if (request.columns.size() != 1) {
627
0
            return Status::NotSupported("Parquet COUNT pushdown only supports one count column");
628
0
        }
629
9
        const auto& count_projection = request.columns[0].projection;
630
9
        const auto& root_schema = projected_root_schema(_state->file_schema, count_projection);
631
        // A required primitive COUNT(col) still carries its projection so the unsupported-type
632
        // validation above cannot be bypassed. Once validated, its definition level proves that
633
        // every selected row is non-NULL, so preserve the already-computed footer row count and
634
        // avoid reading definition levels merely to rediscover COUNT(col) == COUNT(*). Complex
635
        // roots continue through the shape reader because their count semantics and read-row
636
        // accounting are derived from nested levels.
637
9
        if (root_schema.kind == ParquetColumnSchemaKind::PRIMITIVE &&
638
9
            root_schema.max_definition_level == 0) {
639
3
            return Status::OK();
640
3
        }
641
6
        result->count = 0;
642
7
        for (const auto& row_group_plan : _state->scan_plan.row_groups) {
643
7
            std::unique_ptr<CountColumnReader> shape_reader;
644
7
            RETURN_IF_ERROR(CountColumnReader::create(
645
7
                    _state->file_context.native_data_file(), _state->file_context.native_metadata,
646
7
                    row_group_plan.row_group_id, root_schema, &count_projection,
647
7
                    _state->file_context.native_io_ctx,
648
7
                    _state->file_context.native_page_cache_enabled,
649
7
                    _state->file_context.native_page_cache_file_key,
650
7
                    _parquet_profile.scan_profile().column_reader_profile, &shape_reader));
651
7
            DORIS_CHECK(shape_reader != nullptr);
652
653
7
            int64_t row_group_cursor = 0;
654
7
            for (const auto& selected_range : row_group_plan.selected_ranges) {
655
7
                DORIS_CHECK(selected_range.start >= row_group_cursor);
656
7
                RETURN_IF_ERROR(_stop_status_if_requested(
657
7
                        shape_reader->skip(selected_range.start - row_group_cursor)));
658
7
                row_group_cursor = selected_range.start;
659
660
7
                int64_t range_rows_read = 0;
661
14
                while (range_rows_read < selected_range.length) {
662
7
                    const int64_t batch_rows =
663
7
                            std::min<int64_t>(_batch_size, selected_range.length - range_rows_read);
664
7
                    int64_t rows_read = 0;
665
7
                    RETURN_IF_ERROR(_stop_status_if_requested(
666
7
                            shape_reader->read_levels(batch_rows, &rows_read)));
667
7
                    if (rows_read != batch_rows) {
668
0
                        return Status::Corruption(
669
0
                                "Parquet COUNT reader returned {} rows, expected {}", rows_read,
670
0
                                batch_rows);
671
0
                    }
672
7
                    _record_scan_rows(rows_read);
673
7
                    result->count +=
674
7
                            count_loaded_non_null_values(root_schema, *shape_reader, rows_read);
675
7
                    range_rows_read += rows_read;
676
7
                    row_group_cursor += rows_read;
677
7
                }
678
7
            }
679
7
        }
680
6
        return Status::OK();
681
6
    }
682
683
11
    result->columns.resize(request.columns.size());
684
19
    for (size_t request_column_idx = 0; request_column_idx < request.columns.size();
685
13
         ++request_column_idx) {
686
13
        const auto file_column_id = request.columns[request_column_idx].projection.local_id();
687
13
        if (file_column_id < 0 ||
688
13
            file_column_id >= static_cast<int32_t>(_state->file_schema.size())) {
689
0
            return Status::InvalidArgument("Invalid parquet aggregate column id {}",
690
0
                                           file_column_id);
691
0
        }
692
13
        const auto& column_schema = _state->file_schema[file_column_id];
693
13
        DORIS_CHECK(column_schema != nullptr);
694
13
        const ParquetColumnSchema* leaf_schema = nullptr;
695
13
        RETURN_IF_ERROR(find_projected_minmax_leaf(
696
13
                *column_schema, request.columns[request_column_idx].projection, &leaf_schema));
697
11
        DORIS_CHECK(leaf_schema != nullptr);
698
11
        RETURN_IF_ERROR(validate_minmax_aggregate_statistics(*leaf_schema));
699
700
9
        auto& aggregate_column = result->columns[request_column_idx];
701
9
        aggregate_column.projection = request.columns[request_column_idx].projection;
702
17
        for (const auto& row_group_plan : _state->scan_plan.row_groups) {
703
17
            const auto& row_group_metadata = _state->file_context.native_metadata->to_thrift()
704
17
                                                     .row_groups[row_group_plan.row_group_id];
705
17
            DORIS_CHECK(leaf_schema->leaf_column_id >= 0 &&
706
17
                        leaf_schema->leaf_column_id <
707
17
                                static_cast<int>(row_group_metadata.columns.size()));
708
17
            const auto& column_chunk = row_group_metadata.columns[leaf_schema->leaf_column_id];
709
17
            DORIS_CHECK(column_chunk.__isset.meta_data);
710
17
            const auto& column_metadata = column_chunk.meta_data;
711
17
            std::optional<tparquet::Statistics> safe_statistics;
712
17
            if (column_metadata.__isset.statistics &&
713
17
                detail::can_use_native_footer_min_max(
714
16
                        leaf_schema->type_descriptor, column_metadata.statistics,
715
16
                        detail::has_supported_type_defined_order(
716
16
                                _state->file_context.native_metadata->to_thrift(),
717
16
                                leaf_schema->leaf_column_id))) {
718
16
                safe_statistics = detail::sanitize_native_footer_statistics(
719
16
                        leaf_schema->type_descriptor, column_metadata.statistics,
720
16
                        detail::has_supported_type_defined_order(
721
16
                                _state->file_context.native_metadata->to_thrift(),
722
16
                                leaf_schema->leaf_column_id));
723
16
            }
724
17
            const auto statistics = ParquetStatisticsUtils::TransformColumnStatistics(
725
17
                    *leaf_schema, safe_statistics.has_value() ? &*safe_statistics : nullptr,
726
17
                    column_metadata.num_values, _state->timezone);
727
17
            if (!statistics.has_min_max) {
728
1
                return Status::NotSupported("Missing parquet min/max statistics for column {}",
729
1
                                            leaf_schema->name);
730
1
            }
731
16
            if (!aggregate_column.has_min || statistics.min_value < aggregate_column.min_value) {
732
8
                aggregate_column.min_value = statistics.min_value;
733
8
                aggregate_column.has_min = true;
734
8
            }
735
16
            if (!aggregate_column.has_max || aggregate_column.max_value < statistics.max_value) {
736
16
                aggregate_column.max_value = statistics.max_value;
737
16
                aggregate_column.has_max = true;
738
16
            }
739
16
        }
740
8
        if (!aggregate_column.has_min || !aggregate_column.has_max) {
741
0
            return Status::NotSupported("No parquet row group selected for min/max pushdown");
742
0
        }
743
8
    }
744
6
    return Status::OK();
745
11
}
746
747
123
Status ParquetReader::close() {
748
123
    SCOPED_TIMER(_parquet_profile.total_time);
749
123
    if (_state != nullptr) {
750
123
        _state->scheduler.close();
751
123
        _sync_page_cache_profile();
752
123
        RETURN_IF_ERROR(_state->file_context.close());
753
123
    }
754
123
    return FileReader::close();
755
123
}
756
757
446
void ParquetReader::_init_profile() {
758
446
    _parquet_profile.init(_profile);
759
446
}
760
761
} // namespace doris::format::parquet