Coverage Report

Created: 2026-07-20 20:03

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