Coverage Report

Created: 2026-07-15 18:12

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