Coverage Report

Created: 2026-07-10 14:32

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