Coverage Report

Created: 2026-10-28 16:17

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format/parquet/parquet_predicate.h
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
#pragma once
19
20
#include <gen_cpp/parquet_types.h>
21
22
#include <cmath>
23
#include <cstring>
24
#include <vector>
25
26
#include "cctz/time_zone.h"
27
#include "core/data_type/data_type_decimal.h"
28
#include "core/data_type/primitive_type.h"
29
#include "exec/common/endian.h"
30
#include "format/format_common.h"
31
#include "format/parquet/parquet_block_split_bloom_filter.h"
32
#include "format/parquet/parquet_column_convert.h"
33
#include "format/parquet/parquet_common.h"
34
#include "format/parquet/schema_desc.h"
35
#include "io/fs/file_reader.h"
36
#include "storage/olap_scan_common.h"
37
#include "storage/segment/row_ranges.h"
38
#include "util/timezone_utils.h"
39
40
namespace doris {
41
class ParquetPredicate {
42
private:
43
43
    static inline bool _is_ascii(uint8_t byte) { return byte < 128; }
44
45
12
    static int _common_prefix(const std::string& encoding_min, const std::string& encoding_max) {
46
12
        size_t min_length = std::min(encoding_min.size(), encoding_max.size());
47
12
        int common_length = 0;
48
37
        while (common_length < min_length &&
49
37
               encoding_min[common_length] == encoding_max[common_length]) {
50
25
            common_length++;
51
25
        }
52
12
        return common_length;
53
12
    }
54
55
13
    static bool _try_read_old_utf8_stats(std::string& encoding_min, std::string& encoding_max) {
56
13
        if (encoding_min == encoding_max) {
57
            // If min = max, then there is a single value only
58
            // No need to modify, just use min
59
1
            encoding_max = encoding_min;
60
1
            return true;
61
12
        } else {
62
12
            int common_prefix_length = _common_prefix(encoding_min, encoding_max);
63
64
            // For min we can retain all-ASCII, because this produces a strictly lower value.
65
12
            int min_good_length = common_prefix_length;
66
24
            while (min_good_length < encoding_min.size() &&
67
24
                   _is_ascii(static_cast<uint8_t>(encoding_min[min_good_length]))) {
68
12
                min_good_length++;
69
12
            }
70
71
            // For max we can be sure only of the part matching the min. When they differ, we can consider only one next, and only if both are ASCII
72
12
            int max_good_length = common_prefix_length;
73
12
            if (max_good_length < encoding_max.size() && max_good_length < encoding_min.size() &&
74
12
                _is_ascii(static_cast<uint8_t>(encoding_min[max_good_length])) &&
75
12
                _is_ascii(static_cast<uint8_t>(encoding_max[max_good_length]))) {
76
5
                max_good_length++;
77
5
            }
78
            // Incrementing 127 would overflow. Incrementing within non-ASCII can have side-effects.
79
17
            while (max_good_length > 0 &&
80
17
                   (static_cast<uint8_t>(encoding_max[max_good_length - 1]) == 127 ||
81
13
                    !_is_ascii(static_cast<uint8_t>(encoding_max[max_good_length - 1])))) {
82
5
                max_good_length--;
83
5
            }
84
12
            if (max_good_length == 0) {
85
                // We can return just min bound, but code downstream likely expects both are present or both are absent.
86
4
                return false;
87
4
            }
88
89
8
            encoding_min.resize(min_good_length);
90
8
            encoding_max.resize(max_good_length);
91
8
            if (max_good_length > 0) {
92
8
                encoding_max[max_good_length - 1]++;
93
8
            }
94
8
            return true;
95
12
        }
96
13
    }
97
98
0
    static SortOrder _determine_sort_order(const tparquet::SchemaElement& parquet_schema) {
99
0
        tparquet::Type::type physical_type = parquet_schema.type;
100
0
        const tparquet::LogicalType& logical_type = parquet_schema.logicalType;
101
0
102
0
        // Assume string type is SortOrder::SIGNED, use ParquetPredicate::_try_read_old_utf8_stats() to handle it.
103
0
        if (logical_type.__isset.STRING &&
104
0
            (physical_type == tparquet::Type::BYTE_ARRAY ||
105
0
             physical_type == tparquet::Type::FIXED_LEN_BYTE_ARRAY)) {
106
0
            return SortOrder::SIGNED;
107
0
        }
108
0
109
0
        if (logical_type.__isset.INTEGER) {
110
0
            if (logical_type.INTEGER.isSigned) {
111
0
                return SortOrder::SIGNED;
112
0
            } else {
113
0
                return SortOrder::UNSIGNED;
114
0
            }
115
0
        } else if (logical_type.__isset.DATE) {
116
0
            return SortOrder::SIGNED;
117
0
        } else if (logical_type.__isset.ENUM) {
118
0
            return SortOrder::UNSIGNED;
119
0
        } else if (logical_type.__isset.BSON) {
120
0
            return SortOrder::UNSIGNED;
121
0
        } else if (logical_type.__isset.JSON) {
122
0
            return SortOrder::UNSIGNED;
123
0
        } else if (logical_type.__isset.STRING) {
124
0
            return SortOrder::UNSIGNED;
125
0
        } else if (logical_type.__isset.DECIMAL) {
126
0
            return SortOrder::UNKNOWN;
127
0
        } else if (logical_type.__isset.MAP) {
128
0
            return SortOrder::UNKNOWN;
129
0
        } else if (logical_type.__isset.LIST) {
130
0
            return SortOrder::UNKNOWN;
131
0
        } else if (logical_type.__isset.TIME) {
132
0
            return SortOrder::SIGNED;
133
0
        } else if (logical_type.__isset.TIMESTAMP) {
134
0
            return SortOrder::SIGNED;
135
0
        } else if (logical_type.__isset.UNKNOWN) {
136
0
            return SortOrder::UNKNOWN;
137
0
        } else {
138
0
            switch (physical_type) {
139
0
            case tparquet::Type::BOOLEAN:
140
0
            case tparquet::Type::INT32:
141
0
            case tparquet::Type::INT64:
142
0
            case tparquet::Type::FLOAT:
143
0
            case tparquet::Type::DOUBLE:
144
0
                return SortOrder::SIGNED;
145
0
            case tparquet::Type::BYTE_ARRAY:
146
0
            case tparquet::Type::FIXED_LEN_BYTE_ARRAY:
147
0
                return SortOrder::UNSIGNED;
148
0
            case tparquet::Type::INT96:
149
0
                return SortOrder::UNKNOWN;
150
0
            default:
151
0
                return SortOrder::UNKNOWN;
152
0
            }
153
0
        }
154
0
    }
155
156
public:
157
    static constexpr int BLOOM_FILTER_MAX_HEADER_LENGTH = 64;
158
    struct ColumnStat {
159
        std::string encoded_min_value;
160
        std::string encoded_max_value;
161
        bool has_null;
162
        bool is_all_null;
163
        const FieldSchema* col_schema;
164
        const cctz::time_zone* ctz;
165
        std::unique_ptr<ParquetBlockSplitBloomFilter> bloom_filter;
166
        std::function<bool(ParquetPredicate::ColumnStat*, const int)>* get_stat_func = nullptr;
167
        std::function<bool(ParquetPredicate::ColumnStat*, const int)>* get_bloom_filter_func =
168
                nullptr;
169
    };
170
171
0
    static bool bloom_filter_supported(PrimitiveType type) {
172
        // Only support types where physical type == logical type (no conversion needed)
173
        // For types like DATEV2, DATETIMEV2, DECIMAL, Parquet stores them in physical format
174
        // (INT32, INT64, etc.) but Doris uses different internal representations.
175
        // Bloom filter works with physical bytes, but we only have logical type values,
176
        // and there's no reverse conversion (logical -> physical) available.
177
        // TINYINT/SMALLINT also need conversion via LittleIntPhysicalConverter.
178
0
        switch (type) {
179
0
        case TYPE_BOOLEAN:
180
0
        case TYPE_INT:
181
0
        case TYPE_BIGINT:
182
0
        case TYPE_FLOAT:
183
0
        case TYPE_DOUBLE:
184
0
        case TYPE_CHAR:
185
0
        case TYPE_VARCHAR:
186
0
        case TYPE_STRING:
187
0
            return true;
188
0
        default:
189
0
            return false;
190
0
        }
191
0
    }
192
193
    struct PageIndexStat {
194
        // Indicates whether the page index information in this column can be used.
195
        bool available = false;
196
        int64_t num_of_pages;
197
        std::vector<std::string> encoded_min_value;
198
        std::vector<std::string> encoded_max_value;
199
        std::vector<bool> has_null;
200
        std::vector<bool> is_all_null;
201
        const FieldSchema* col_schema;
202
203
        // Record the row range corresponding to each page.
204
        std::vector<segment_v2::RowRange> ranges;
205
    };
206
207
    struct CachedPageIndexStat {
208
        const cctz::time_zone* ctz;
209
        std::map<int, PageIndexStat> stats;
210
        std::function<bool(PageIndexStat**, int)> get_stat_func;
211
        RowRange row_group_range;
212
    };
213
214
    // The encoded Parquet min-max value is parsed into `fields`;
215
    // Can be used in row groups and page index statistics.
216
    static Status parse_min_max_value(const FieldSchema* col_schema, const std::string& encoded_min,
217
                                      const std::string& encoded_max, const cctz::time_zone& ctz,
218
51
                                      Field* min_field, Field* max_field) {
219
51
        auto logical_data_type = remove_nullable(col_schema->data_type);
220
        // UUID statistics are used only with Iceberg's binary carrier. Ordinary UUID text
221
        // scans reject these predicates in ParquetReader::_type_matches.
222
51
        auto converter = parquet::PhysicalToLogicalConverter::get_converter(
223
51
                col_schema, logical_data_type, logical_data_type, &ctz, false, true);
224
51
        ColumnPtr physical_column;
225
51
        switch (col_schema->parquet_schema.type) {
226
0
        case tparquet::Type::type::BOOLEAN: {
227
0
            auto physical_col = ColumnUInt8::create();
228
0
            physical_col->get_data().data();
229
0
            physical_col->resize(2);
230
0
            physical_col->get_data()[0] = *reinterpret_cast<const bool*>(encoded_min.data());
231
0
            physical_col->get_data()[1] = *reinterpret_cast<const bool*>(encoded_max.data());
232
0
            physical_column = std::move(physical_col);
233
0
            break;
234
0
        }
235
38
        case tparquet::Type::type::INT32: {
236
38
            auto physical_col = ColumnInt32::create();
237
38
            physical_col->resize(2);
238
239
38
            physical_col->get_data()[0] = *reinterpret_cast<const int32_t*>(encoded_min.data());
240
38
            physical_col->get_data()[1] = *reinterpret_cast<const int32_t*>(encoded_max.data());
241
242
38
            physical_column = std::move(physical_col);
243
38
            break;
244
0
        }
245
0
        case tparquet::Type::type::INT64: {
246
0
            auto physical_col = ColumnInt64::create();
247
0
            physical_col->resize(2);
248
0
            physical_col->get_data()[0] = *reinterpret_cast<const int64_t*>(encoded_min.data());
249
0
            physical_col->get_data()[1] = *reinterpret_cast<const int64_t*>(encoded_max.data());
250
0
            physical_column = std::move(physical_col);
251
0
            break;
252
0
        }
253
13
        case tparquet::Type::type::FLOAT: {
254
13
            auto physical_col = ColumnFloat32::create();
255
13
            physical_col->resize(2);
256
13
            physical_col->get_data()[0] = *reinterpret_cast<const float*>(encoded_min.data());
257
13
            physical_col->get_data()[1] = *reinterpret_cast<const float*>(encoded_max.data());
258
13
            physical_column = std::move(physical_col);
259
13
            break;
260
0
        }
261
0
        case tparquet::Type::type::DOUBLE: {
262
0
            auto physical_col = ColumnFloat64 ::create();
263
0
            physical_col->resize(2);
264
0
            physical_col->get_data()[0] = *reinterpret_cast<const double*>(encoded_min.data());
265
0
            physical_col->get_data()[1] = *reinterpret_cast<const double*>(encoded_max.data());
266
0
            physical_column = std::move(physical_col);
267
0
            break;
268
0
        }
269
0
        case tparquet::Type::type::BYTE_ARRAY: {
270
0
            auto physical_col = ColumnString::create();
271
0
            physical_col->insert_data(encoded_min.data(), encoded_min.size());
272
0
            physical_col->insert_data(encoded_max.data(), encoded_max.size());
273
0
            physical_column = std::move(physical_col);
274
0
            break;
275
0
        }
276
0
        case tparquet::Type::type::FIXED_LEN_BYTE_ARRAY: {
277
0
            auto physical_col = ColumnUInt8::create();
278
0
            physical_col->resize(2 * col_schema->parquet_schema.type_length);
279
0
            DCHECK(col_schema->parquet_schema.type_length == encoded_min.length());
280
0
            DCHECK(col_schema->parquet_schema.type_length == encoded_max.length());
281
282
0
            auto ptr = physical_col->get_data().data();
283
0
            memcpy(ptr, encoded_min.data(), encoded_min.length());
284
0
            memcpy(ptr + encoded_min.length(), encoded_max.data(), encoded_max.length());
285
0
            physical_column = std::move(physical_col);
286
0
            break;
287
0
        }
288
0
        case tparquet::Type::type::INT96: {
289
0
            auto physical_col = ColumnInt8::create();
290
0
            physical_col->resize(2 * sizeof(ParquetInt96));
291
0
            DCHECK(sizeof(ParquetInt96) == encoded_min.length());
292
0
            DCHECK(sizeof(ParquetInt96) == encoded_max.length());
293
294
0
            auto ptr = physical_col->get_data().data();
295
0
            memcpy(ptr, encoded_min.data(), encoded_min.length());
296
0
            memcpy(ptr + encoded_min.length(), encoded_max.data(), encoded_max.length());
297
0
            physical_column = std::move(physical_col);
298
0
            break;
299
0
        }
300
51
        }
301
302
51
        ColumnPtr logical_column;
303
51
        if (converter->is_consistent()) {
304
51
            logical_column = physical_column;
305
51
        } else {
306
0
            logical_column = logical_data_type->create_column();
307
0
            RETURN_IF_ERROR(converter->physical_convert(physical_column, logical_column));
308
0
        }
309
310
51
        DCHECK(logical_column->size() == 2);
311
51
        *min_field = logical_column->operator[](0);
312
51
        *max_field = logical_column->operator[](1);
313
314
51
        auto logical_prim_type = logical_data_type->get_primitive_type();
315
316
51
        if (logical_prim_type == TYPE_FLOAT) {
317
10
            auto& min_value = min_field->get<TYPE_FLOAT>();
318
10
            auto& max_value = max_field->get<TYPE_FLOAT>();
319
320
10
            if (std::isnan(min_value) || std::isnan(max_value)) {
321
1
                return Status::DataQualityError("Can not use this parquet min/max value.");
322
1
            }
323
            // Updating min to -0.0 and max to +0.0 to ensure that no 0.0 values would be skipped
324
9
            if (std::signbit(min_value) == 0 && min_value == 0.0F) {
325
0
                min_value = -0.0F;
326
0
            }
327
9
            if (std::signbit(max_value) != 0 && max_value == -0.0F) {
328
0
                max_value = 0.0F;
329
0
            }
330
41
        } else if (logical_prim_type == TYPE_DOUBLE) {
331
0
            auto& min_value = min_field->get<TYPE_DOUBLE>();
332
0
            auto& max_value = max_field->get<TYPE_DOUBLE>();
333
334
0
            if (std::isnan(min_value) || std::isnan(max_value)) {
335
0
                return Status::DataQualityError("Can not use this parquet min/max value.");
336
0
            }
337
            // Updating min to -0.0 and max to +0.0 to ensure that no 0.0 values would be skipped
338
0
            if (std::signbit(min_value) == 0 && min_value == 0.0F) {
339
0
                min_value = -0.0F;
340
0
            }
341
0
            if (std::signbit(max_value) != 0 && max_value == -0.0F) {
342
0
                max_value = 0.0F;
343
0
            }
344
41
        } else if (col_schema->parquet_schema.type == tparquet::Type::type::INT96 ||
345
41
                   logical_prim_type == TYPE_DATETIMEV2) {
346
0
            auto min_value = min_field->get<TYPE_DATETIMEV2>();
347
0
            auto max_value = min_field->get<TYPE_DATETIMEV2>();
348
349
            // From Trino: Parquet INT96 timestamp values were compared incorrectly
350
            // for the purposes of producing statistics by older parquet writers,
351
            // so PARQUET-1065 deprecated them. The result is that any writer that produced stats
352
            // was producing unusable incorrect values, except the special case where min == max
353
            // and an incorrect ordering would not be material to the result.
354
            // PARQUET-1026 made binary stats available and valid in that special case.
355
0
            if (min_value != max_value) {
356
0
                return Status::DataQualityError("invalid min/max value");
357
0
            }
358
0
        }
359
360
50
        return Status::OK();
361
51
    }
362
363
    static Status read_column_stats(const FieldSchema* col_schema,
364
                                    const tparquet::ColumnMetaData& column_meta_data,
365
                                    std::unordered_map<tparquet::Type::type, bool>* ignored_stats,
366
0
                                    const std::string& file_created_by, ColumnStat* ans_stat) {
367
0
        auto& statistic = column_meta_data.statistics;
368
0
369
0
        if (!statistic.__isset.null_count) [[unlikely]] {
370
0
            return Status::DataQualityError("This parquet Column meta no set null_count.");
371
0
        }
372
0
        ans_stat->has_null = statistic.null_count > 0;
373
0
        ans_stat->is_all_null = statistic.null_count == column_meta_data.num_values;
374
0
        if (ans_stat->is_all_null) {
375
0
            return Status::OK();
376
0
        }
377
0
        auto prim_type = remove_nullable(col_schema->data_type)->get_primitive_type();
378
0
379
0
        // Min-max of statistic is plain-encoded value
380
0
        if (statistic.__isset.min_value && statistic.__isset.max_value) {
381
0
            ColumnOrderName column_order =
382
0
                    col_schema->physical_type == tparquet::Type::INT96 ||
383
0
                                    col_schema->parquet_schema.logicalType.__isset.UNKNOWN
384
0
                            ? ColumnOrderName::UNDEFINED
385
0
                            : ColumnOrderName::TYPE_DEFINED_ORDER;
386
0
            if ((statistic.min_value != statistic.max_value) &&
387
0
                (column_order != ColumnOrderName::TYPE_DEFINED_ORDER)) {
388
0
                return Status::DataQualityError("Can not use this parquet min/max value.");
389
0
            }
390
0
            ans_stat->encoded_min_value = statistic.min_value;
391
0
            ans_stat->encoded_max_value = statistic.max_value;
392
0
393
0
            if (prim_type == TYPE_VARCHAR || prim_type == TYPE_CHAR || prim_type == TYPE_STRING) {
394
0
                auto encoded_min_copy = ans_stat->encoded_min_value;
395
0
                auto encoded_max_copy = ans_stat->encoded_max_value;
396
0
                if (!_try_read_old_utf8_stats(encoded_min_copy, encoded_max_copy)) {
397
0
                    return Status::DataQualityError("Can not use this parquet min/max value.");
398
0
                }
399
0
                ans_stat->encoded_min_value = encoded_min_copy;
400
0
                ans_stat->encoded_max_value = encoded_max_copy;
401
0
            }
402
0
403
0
        } else if (statistic.__isset.min && statistic.__isset.max) {
404
0
            bool max_equals_min = statistic.min == statistic.max;
405
0
406
0
            SortOrder sort_order = _determine_sort_order(col_schema->parquet_schema);
407
0
            bool sort_orders_match = SortOrder::SIGNED == sort_order;
408
0
            if (!sort_orders_match && !max_equals_min) {
409
0
                return Status::NotSupported("Can not use this parquet min/max value.");
410
0
            }
411
0
412
0
            bool should_ignore_corrupted_stats = false;
413
0
            if (ignored_stats != nullptr) {
414
0
                if (ignored_stats->count(col_schema->physical_type) == 0) {
415
0
                    if (CorruptStatistics::should_ignore_statistics(file_created_by,
416
0
                                                                    col_schema->physical_type)) {
417
0
                        ignored_stats->emplace(col_schema->physical_type, true);
418
0
                        should_ignore_corrupted_stats = true;
419
0
                    } else {
420
0
                        ignored_stats->emplace(col_schema->physical_type, false);
421
0
                    }
422
0
                } else if (ignored_stats->at(col_schema->physical_type)) {
423
0
                    should_ignore_corrupted_stats = true;
424
0
                }
425
0
            } else if (CorruptStatistics::should_ignore_statistics(file_created_by,
426
0
                                                                   col_schema->physical_type)) {
427
0
                should_ignore_corrupted_stats = true;
428
0
            }
429
0
430
0
            if (should_ignore_corrupted_stats) {
431
0
                return Status::DataQualityError("Error statistics, should ignore.");
432
0
            }
433
0
434
0
            ans_stat->encoded_min_value = statistic.min;
435
0
            ans_stat->encoded_max_value = statistic.max;
436
0
        } else {
437
0
            return Status::DataQualityError("This parquet file not set min/max value");
438
0
        }
439
0
440
0
        return Status::OK();
441
0
    }
442
443
    static Status read_bloom_filter(const tparquet::ColumnMetaData& column_meta_data,
444
                                    io::FileReaderSPtr file_reader, io::IOContext* io_ctx,
445
8
                                    ColumnStat* ans_stat) {
446
8
        if (!column_meta_data.__isset.bloom_filter_offset) {
447
0
            return Status::NotSupported("Can not use this parquet bloom filter.");
448
0
        }
449
8
        if (column_meta_data.bloom_filter_offset < 0 ||
450
8
            (column_meta_data.__isset.bloom_filter_length &&
451
8
             column_meta_data.bloom_filter_length <= 0)) {
452
1
            return Status::Corruption("Invalid Parquet bloom filter offset or declared length");
453
1
        }
454
455
7
        const uint64_t bloom_offset = static_cast<uint64_t>(column_meta_data.bloom_filter_offset);
456
7
        if (bloom_offset >= file_reader->size()) {
457
0
            return Status::Corruption("Parquet bloom filter offset exceeds file size");
458
0
        }
459
7
        const size_t available = file_reader->size() - bloom_offset;
460
7
        const size_t declared_available =
461
7
                column_meta_data.__isset.bloom_filter_length
462
7
                        ? std::min<size_t>(column_meta_data.bloom_filter_length, available)
463
7
                        : available;
464
7
        const size_t header_read_size =
465
7
                std::min<size_t>(declared_available, BLOOM_FILTER_MAX_HEADER_LENGTH);
466
7
        size_t bytes_read = 0;
467
7
        std::vector<uint8_t> header_buffer(header_read_size);
468
7
        RETURN_IF_ERROR(file_reader->read_at(column_meta_data.bloom_filter_offset,
469
7
                                             Slice(header_buffer.data(), header_buffer.size()),
470
7
                                             &bytes_read, io_ctx));
471
472
7
        tparquet::BloomFilterHeader t_bloom_filter_header;
473
7
        uint32_t t_bloom_filter_header_size = static_cast<uint32_t>(bytes_read);
474
7
        if (!deserialize_thrift_msg(header_buffer.data(), &t_bloom_filter_header_size, true,
475
7
                                    &t_bloom_filter_header)
476
7
                     .ok()) {
477
0
            return Status::Corruption("Malformed Parquet bloom filter header");
478
0
        }
479
480
        // TODO the bloom filter could be encrypted, too, so need to double check that this is NOT the case
481
7
        if (!t_bloom_filter_header.algorithm.__isset.BLOCK ||
482
7
            !t_bloom_filter_header.compression.__isset.UNCOMPRESSED ||
483
7
            !t_bloom_filter_header.hash.__isset.XXHASH || t_bloom_filter_header.numBytes <= 0) {
484
0
            return Status::NotSupported("Can not use this parquet bloom filter.");
485
0
        }
486
487
7
        const int64_t payload_size = t_bloom_filter_header.numBytes;
488
7
        if (payload_size < segment_v2::BloomFilter::MINIMUM_BYTES ||
489
7
            payload_size > segment_v2::BloomFilter::MAXIMUM_BYTES || payload_size % 32 != 0) {
490
2
            return Status::Corruption("Invalid Parquet bloom filter payload size {}", payload_size);
491
2
        }
492
5
        const uint64_t total_size =
493
5
                static_cast<uint64_t>(t_bloom_filter_header_size) + payload_size;
494
5
        if (total_size > available) {
495
1
            return Status::Corruption("Parquet bloom filter range exceeds file size");
496
1
        }
497
4
        const auto expected_declared_length = static_cast<int64_t>(total_size);
498
4
        if (column_meta_data.__isset.bloom_filter_length &&
499
4
            column_meta_data.bloom_filter_length != expected_declared_length) {
500
1
            return Status::Corruption("Invalid Parquet bloom filter declared length");
501
1
        }
502
503
3
        auto bloom_filter = std::make_unique<ParquetBlockSplitBloomFilter>();
504
        // Read directly into one tracked allocation so a valid maximum-size filter is admitted
505
        // against the task budget without doubling its peak memory during initialization.
506
3
        RETURN_IF_ERROR(bloom_filter->init_for_read(static_cast<size_t>(payload_size),
507
3
                                                    segment_v2::HashStrategyPB::XX_HASH_64));
508
3
        RETURN_IF_ERROR(file_reader->read_at(
509
3
                static_cast<size_t>(bloom_offset) + t_bloom_filter_header_size,
510
3
                Slice(bloom_filter->mutable_data(), bloom_filter->size()), &bytes_read, io_ctx));
511
3
        if (bytes_read != bloom_filter->size()) {
512
1
            return Status::Corruption("Truncated Parquet bloom filter payload");
513
1
        }
514
515
2
        ans_stat->bloom_filter = std::move(bloom_filter);
516
517
2
        return Status::OK();
518
3
    }
519
};
520
521
} // namespace doris