Coverage Report

Created: 2026-07-19 16:21

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