Coverage Report

Created: 2026-10-29 12:42

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
    static inline bool _is_ascii(uint8_t byte) { return byte < 128; }
44
45
    static int _common_prefix(const std::string& encoding_min, const std::string& encoding_max) {
46
        size_t min_length = std::min(encoding_min.size(), encoding_max.size());
47
        int common_length = 0;
48
        while (common_length < min_length &&
49
               encoding_min[common_length] == encoding_max[common_length]) {
50
            common_length++;
51
        }
52
        return common_length;
53
    }
54
55
0
    static bool _try_read_old_utf8_stats(std::string& encoding_min, std::string& encoding_max) {
56
0
        if (encoding_min == encoding_max) {
57
0
            // If min = max, then there is a single value only
58
0
            // No need to modify, just use min
59
0
            encoding_max = encoding_min;
60
0
            return true;
61
0
        } else {
62
0
            int common_prefix_length = _common_prefix(encoding_min, encoding_max);
63
0
64
0
            // For min we can retain all-ASCII, because this produces a strictly lower value.
65
0
            int min_good_length = common_prefix_length;
66
0
            while (min_good_length < encoding_min.size() &&
67
0
                   _is_ascii(static_cast<uint8_t>(encoding_min[min_good_length]))) {
68
0
                min_good_length++;
69
0
            }
70
0
71
0
            // 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
0
            int max_good_length = common_prefix_length;
73
0
            if (max_good_length < encoding_max.size() && max_good_length < encoding_min.size() &&
74
0
                _is_ascii(static_cast<uint8_t>(encoding_min[max_good_length])) &&
75
0
                _is_ascii(static_cast<uint8_t>(encoding_max[max_good_length]))) {
76
0
                max_good_length++;
77
0
            }
78
0
            // Incrementing 127 would overflow. Incrementing within non-ASCII can have side-effects.
79
0
            while (max_good_length > 0 &&
80
0
                   (static_cast<uint8_t>(encoding_max[max_good_length - 1]) == 127 ||
81
0
                    !_is_ascii(static_cast<uint8_t>(encoding_max[max_good_length - 1])))) {
82
0
                max_good_length--;
83
0
            }
84
0
            if (max_good_length == 0) {
85
0
                // We can return just min bound, but code downstream likely expects both are present or both are absent.
86
0
                return false;
87
0
            }
88
0
89
0
            encoding_min.resize(min_good_length);
90
0
            encoding_max.resize(max_good_length);
91
0
            if (max_good_length > 0) {
92
0
                encoding_max[max_good_length - 1]++;
93
0
            }
94
0
            return true;
95
0
        }
96
0
    }
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