Coverage Report

Created: 2026-08-07 10:05

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