Coverage Report

Created: 2026-08-06 18:25

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