Coverage Report

Created: 2026-07-18 01:24

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