Coverage Report

Created: 2026-08-04 17:23

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format_v2/parquet/parquet_statistics.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
//   http://www.apache.org/licenses/LICENSE-2.0
9
// Unless required by applicable law or agreed to in writing,
10
// software distributed under the License is distributed on an
11
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
12
// KIND, either express or implied.  See the License for the
13
// specific language governing permissions and limitations
14
// under the License.
15
16
#include "format_v2/parquet/parquet_statistics.h"
17
18
#include <algorithm>
19
#include <cmath>
20
#include <cstddef>
21
#include <cstring>
22
#include <exception>
23
#include <limits>
24
#include <map>
25
#include <memory>
26
#include <optional>
27
#include <set>
28
#include <string>
29
#include <type_traits>
30
#include <utility>
31
#include <vector>
32
33
#include "common/cast_set.h"
34
#include "common/config.h"
35
#include "core/data_type/data_type.h"
36
#include "core/data_type/data_type_nullable.h"
37
#include "core/data_type_serde/data_type_serde.h"
38
#include "core/field.h"
39
#include "exprs/expr_zonemap_filter.h"
40
#include "exprs/vectorized_fn_call.h"
41
#include "exprs/vexpr_context.h"
42
#include "exprs/vliteral.h"
43
#include "exprs/vslot_ref.h"
44
#include "format_v2/parquet/parquet_column_schema.h"
45
#include "format_v2/parquet/parquet_file_context.h"
46
#include "format_v2/parquet/reader/native/block_split_bloom_filter.h"
47
#include "format_v2/parquet/reader/native_column_reader.h"
48
#include "format_v2/timestamp_statistics.h"
49
#include "runtime/runtime_profile.h"
50
#include "storage/index/bloom_filter/bloom_filter.h"
51
#include "storage/index/zone_map/zone_map_index.h"
52
#include "storage/index/zone_map/zonemap_eval_context.h"
53
#include "util/thrift_util.h"
54
#include "util/unaligned.h"
55
56
namespace doris::format::parquet {
57
58
namespace detail {
59
60
Status validate_native_bloom_filter_layout(int64_t offset, uint32_t header_size,
61
                                           int64_t payload_size, int64_t declared_length,
62
1
                                           size_t file_size) {
63
1
    if (offset < 0 || header_size == 0 || payload_size < segment_v2::BloomFilter::MINIMUM_BYTES ||
64
1
        payload_size > segment_v2::BloomFilter::MAXIMUM_BYTES || payload_size % 32 != 0) {
65
0
        return Status::Corruption(
66
0
                "Invalid Parquet Bloom filter layout: offset {}, header {}, payload {}", offset,
67
0
                header_size, payload_size);
68
0
    }
69
1
    const uint64_t unsigned_offset = static_cast<uint64_t>(offset);
70
1
    const uint64_t total_size = static_cast<uint64_t>(header_size) + payload_size;
71
1
    if (unsigned_offset > file_size || total_size > file_size - unsigned_offset) {
72
0
        return Status::Corruption("Parquet Bloom filter range exceeds file size {}", file_size);
73
0
    }
74
1
    if (declared_length >= 0) {
75
1
        const uint64_t unsigned_declared_length = static_cast<uint64_t>(declared_length);
76
1
        if (unsigned_declared_length < total_size ||
77
1
            unsigned_declared_length > file_size - unsigned_offset) {
78
0
            return Status::Corruption(
79
0
                    "Parquet Bloom filter requires {} bytes, metadata declares {}, file has {}",
80
0
                    total_size, declared_length, file_size - unsigned_offset);
81
0
        }
82
1
    }
83
1
    return Status::OK();
84
1
}
85
86
117
bool has_supported_type_defined_order(const tparquet::FileMetaData& metadata, int leaf_column_id) {
87
117
    return leaf_column_id >= 0 && metadata.__isset.column_orders &&
88
117
           leaf_column_id < static_cast<int>(metadata.column_orders.size()) &&
89
117
           metadata.column_orders[leaf_column_id].__isset.TYPE_ORDER;
90
117
}
91
92
tparquet::Statistics sanitize_native_footer_statistics(const ParquetTypeDescriptor& type_descriptor,
93
                                                       const tparquet::Statistics& statistics,
94
109
                                                       bool has_type_defined_order) {
95
109
    auto sanitized = statistics;
96
109
    if (!has_type_defined_order || !sanitized.__isset.min_value || !sanitized.__isset.max_value) {
97
6
        sanitized.__isset.min_value = false;
98
6
        sanitized.__isset.max_value = false;
99
6
        sanitized.min_value.clear();
100
6
        sanitized.max_value.clear();
101
6
    }
102
109
    const bool binary = type_descriptor.physical_type == tparquet::Type::BYTE_ARRAY ||
103
109
                        type_descriptor.physical_type == tparquet::Type::FIXED_LEN_BYTE_ARRAY;
104
109
    if (!sanitized.__isset.min || !sanitized.__isset.max ||
105
109
        (binary && sanitized.min != sanitized.max)) {
106
12
        sanitized.__isset.min = false;
107
12
        sanitized.__isset.max = false;
108
12
        sanitized.min.clear();
109
12
        sanitized.max.clear();
110
12
    }
111
109
    return sanitized;
112
109
}
113
114
bool can_use_native_footer_min_max(const ParquetTypeDescriptor& type_descriptor,
115
                                   const tparquet::Statistics& statistics,
116
25
                                   bool has_type_defined_order) {
117
    // Inexact bounds remain useful for pruning, but returning them as aggregate values changes the
118
    // query result. Missing exactness fields are legacy-compatible; only an explicit false rejects.
119
25
    if ((statistics.__isset.is_min_value_exact && !statistics.is_min_value_exact) ||
120
25
        (statistics.__isset.is_max_value_exact && !statistics.is_max_value_exact)) {
121
2
        return false;
122
2
    }
123
23
    const auto sanitized =
124
23
            sanitize_native_footer_statistics(type_descriptor, statistics, has_type_defined_order);
125
23
    return (sanitized.__isset.min_value && sanitized.__isset.max_value) ||
126
23
           (sanitized.__isset.min && sanitized.__isset.max);
127
25
}
128
129
} // namespace detail
130
131
namespace {
132
133
bool build_native_page_statistics(const tparquet::ColumnIndex& column_index,
134
                                  const ParquetColumnSchema& column_schema, size_t page_idx,
135
                                  int64_t page_rows, ParquetColumnStatistics* page_statistics,
136
                                  const cctz::time_zone* timezone);
137
138
enum class ParquetRowGroupPruneReason {
139
    NONE,         // cannot prune; must read
140
    STATISTICS,   // excluded by ZoneMap statistics
141
    DICTIONARY,   // excluded by dictionary
142
    BLOOM_FILTER, // excluded by bloom filter
143
};
144
145
Status read_native_bloom_filter(const tparquet::ColumnMetaData& metadata,
146
                                const io::FileReaderSPtr& file, io::IOContext* io_ctx,
147
2
                                std::unique_ptr<native::BlockSplitBloomFilter>* result) {
148
2
    if (result == nullptr || file == nullptr || !metadata.__isset.bloom_filter_offset) {
149
1
        return Status::NotSupported("Parquet Bloom filter is unavailable");
150
1
    }
151
1
    constexpr size_t MAX_BLOOM_HEADER_BYTES = 64;
152
1
    if (metadata.bloom_filter_offset < 0 ||
153
1
        (metadata.__isset.bloom_filter_length && metadata.bloom_filter_length <= 0)) {
154
0
        return Status::Corruption("Invalid Parquet Bloom filter offset or declared length");
155
0
    }
156
1
    const uint64_t bloom_offset = static_cast<uint64_t>(metadata.bloom_filter_offset);
157
1
    if (bloom_offset >= file->size()) {
158
0
        return Status::Corruption("Parquet Bloom filter offset exceeds file size {}", file->size());
159
0
    }
160
1
    const size_t available = file->size() - bloom_offset;
161
1
    const size_t declared_available =
162
1
            metadata.__isset.bloom_filter_length
163
1
                    ? std::min<size_t>(metadata.bloom_filter_length, available)
164
1
                    : available;
165
1
    const size_t header_read_size = std::min(declared_available, MAX_BLOOM_HEADER_BYTES);
166
1
    std::vector<uint8_t> header_buffer(header_read_size);
167
1
    size_t bytes_read = 0;
168
1
    RETURN_IF_ERROR(file->read_at(metadata.bloom_filter_offset,
169
1
                                  Slice(header_buffer.data(), header_buffer.size()), &bytes_read,
170
1
                                  io_ctx));
171
1
    tparquet::BloomFilterHeader header;
172
1
    uint32_t header_size = cast_set<uint32_t>(bytes_read);
173
1
    RETURN_IF_ERROR(deserialize_thrift_msg(header_buffer.data(), &header_size, true, &header));
174
1
    if (!header.algorithm.__isset.BLOCK || !header.compression.__isset.UNCOMPRESSED ||
175
1
        !header.hash.__isset.XXHASH || header.numBytes <= 0) {
176
0
        return Status::NotSupported("Unsupported Parquet Bloom filter encoding");
177
0
    }
178
179
    // Validate the complete split-block layout before allocating or adding footer-controlled
180
    // offsets; BloomFilter::init() otherwise receives a truncated or oversized backing buffer.
181
1
    RETURN_IF_ERROR(detail::validate_native_bloom_filter_layout(
182
1
            metadata.bloom_filter_offset, header_size, header.numBytes,
183
1
            metadata.__isset.bloom_filter_length ? metadata.bloom_filter_length : -1,
184
1
            file->size()));
185
186
1
    std::vector<uint8_t> data(cast_set<size_t>(header.numBytes));
187
1
    RETURN_IF_ERROR(file->read_at(static_cast<size_t>(metadata.bloom_filter_offset) + header_size,
188
1
                                  Slice(data.data(), data.size()), &bytes_read, io_ctx));
189
1
    if (bytes_read != data.size()) {
190
0
        return Status::Corruption("Truncated Parquet Bloom filter payload");
191
0
    }
192
1
    auto bloom_filter = std::make_unique<native::BlockSplitBloomFilter>();
193
1
    RETURN_IF_ERROR(bloom_filter->init(reinterpret_cast<const char*>(data.data()), data.size(),
194
1
                                       segment_v2::HashStrategyPB::XX_HASH_64));
195
1
    *result = std::move(bloom_filter);
196
1
    return Status::OK();
197
1
}
198
199
5
bool bloom_logical_type_supported(const ParquetColumnSchema& column_schema) {
200
5
    if (column_schema.type == nullptr) {
201
0
        return false;
202
0
    }
203
5
    switch (remove_nullable(column_schema.type)->get_primitive_type()) {
204
0
    case TYPE_BOOLEAN:
205
0
    case TYPE_INT:
206
4
    case TYPE_BIGINT:
207
4
    case TYPE_FLOAT:
208
4
    case TYPE_DOUBLE:
209
5
    case TYPE_STRING:
210
5
        return true;
211
0
    default:
212
0
        return false;
213
5
    }
214
5
}
215
216
344
DecodedTimeUnit decoded_time_unit(ParquetTimeUnit time_unit) {
217
344
    switch (time_unit) {
218
0
    case ParquetTimeUnit::MILLIS:
219
0
        return DecodedTimeUnit::MILLIS;
220
0
    case ParquetTimeUnit::MICROS:
221
0
        return DecodedTimeUnit::MICROS;
222
0
    case ParquetTimeUnit::NANOS:
223
0
        return DecodedTimeUnit::NANOS;
224
344
    default:
225
344
        return DecodedTimeUnit::UNKNOWN;
226
344
    }
227
344
}
228
229
Status read_decoded_field(const ParquetColumnSchema& column_schema, DecodedColumnView view,
230
344
                          Field* field, const cctz::time_zone* timezone) {
231
344
    DORIS_CHECK(column_schema.type != nullptr);
232
344
    DORIS_CHECK(field != nullptr);
233
344
    constexpr uint8_t not_null = 0;
234
344
    view.row_count = 1;
235
344
    view.null_map = &not_null;
236
344
    view.time_unit = decoded_time_unit(column_schema.type_descriptor.time_unit);
237
344
    view.logical_integer_bit_width = column_schema.type_descriptor.integer_bit_width;
238
344
    view.logical_integer_is_signed = !column_schema.type_descriptor.is_unsigned_integer;
239
344
    view.decimal_precision = column_schema.type_descriptor.decimal_precision;
240
344
    view.decimal_scale = column_schema.type_descriptor.decimal_scale;
241
344
    view.fixed_length = column_schema.type_descriptor.fixed_length;
242
344
    view.timestamp_is_adjusted_to_utc = column_schema.type_descriptor.timestamp_is_adjusted_to_utc;
243
344
    view.timezone = timezone;
244
    // Statistics are pruning proofs, not row materialization. A malformed non-NULL bound must
245
    // disable pruning instead of being converted to NULL under permissive scan semantics.
246
344
    view.enable_strict_mode = true;
247
344
    RETURN_IF_ERROR(column_schema.type->get_serde()->read_field_from_decoded_value(
248
344
            *column_schema.type, field, view));
249
338
    if (field->is_null()) {
250
0
        return Status::DataQualityError("Non-NULL Parquet statistic decoded as NULL");
251
0
    }
252
338
    return Status::OK();
253
338
}
254
255
template <typename NativeType>
256
bool set_decoded_field(const ParquetColumnSchema& column_schema, DecodedValueKind value_kind,
257
344
                       const NativeType& value, Field* field, const cctz::time_zone* timezone) {
258
344
    DecodedColumnView view;
259
344
    view.value_kind = value_kind;
260
344
    view.values = reinterpret_cast<const uint8_t*>(&value);
261
344
    return read_decoded_field(column_schema, view, field, timezone).ok();
262
344
}
parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_117set_decoded_fieldIhEEbRKNS1_19ParquetColumnSchemaENS_16DecodedValueKindERKT_PNS_5FieldEPKN4cctz9time_zoneE
Line
Count
Source
257
4
                       const NativeType& value, Field* field, const cctz::time_zone* timezone) {
258
4
    DecodedColumnView view;
259
4
    view.value_kind = value_kind;
260
4
    view.values = reinterpret_cast<const uint8_t*>(&value);
261
4
    return read_decoded_field(column_schema, view, field, timezone).ok();
262
4
}
parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_117set_decoded_fieldIiEEbRKNS1_19ParquetColumnSchemaENS_16DecodedValueKindERKT_PNS_5FieldEPKN4cctz9time_zoneE
Line
Count
Source
257
340
                       const NativeType& value, Field* field, const cctz::time_zone* timezone) {
258
340
    DecodedColumnView view;
259
340
    view.value_kind = value_kind;
260
340
    view.values = reinterpret_cast<const uint8_t*>(&value);
261
340
    return read_decoded_field(column_schema, view, field, timezone).ok();
262
340
}
Unexecuted instantiation: parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_117set_decoded_fieldIlEEbRKNS1_19ParquetColumnSchemaENS_16DecodedValueKindERKT_PNS_5FieldEPKN4cctz9time_zoneE
Unexecuted instantiation: parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_117set_decoded_fieldIfEEbRKNS1_19ParquetColumnSchemaENS_16DecodedValueKindERKT_PNS_5FieldEPKN4cctz9time_zoneE
Unexecuted instantiation: parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_117set_decoded_fieldIdEEbRKNS1_19ParquetColumnSchemaENS_16DecodedValueKindERKT_PNS_5FieldEPKN4cctz9time_zoneE
263
264
0
int64_t floor_timestamp_seconds(int64_t value, ParquetTimeUnit time_unit) {
265
0
    int64_t units_per_second = 1;
266
0
    switch (time_unit) {
267
0
    case ParquetTimeUnit::MILLIS:
268
0
        units_per_second = 1000;
269
0
        break;
270
0
    case ParquetTimeUnit::MICROS:
271
0
        units_per_second = 1000000;
272
0
        break;
273
0
    case ParquetTimeUnit::NANOS:
274
0
        units_per_second = 1000000000;
275
0
        break;
276
0
    default:
277
0
        DORIS_CHECK(false);
278
0
    }
279
0
    return format::floor_epoch_seconds(value, units_per_second);
280
0
}
281
282
bool timestamp_min_max_is_safe(const ParquetColumnSchema& column_schema, int64_t min_value,
283
0
                               int64_t max_value, const cctz::time_zone* timezone) {
284
0
    if (min_value > max_value) {
285
0
        return false;
286
0
    }
287
0
    if (!column_schema.type_descriptor.is_timestamp ||
288
0
        !column_schema.type_descriptor.timestamp_is_adjusted_to_utc || timezone == nullptr ||
289
0
        remove_nullable(column_schema.type)->get_primitive_type() == TYPE_TIMESTAMPTZ) {
290
        // TIMESTAMPTZ keeps the original UTC ordering, so local civil-time rollback does not make
291
        // its converted min/max non-monotonic.
292
0
        return true;
293
0
    }
294
0
    return format::utc_timestamp_range_is_monotonic(
295
0
            floor_timestamp_seconds(min_value, column_schema.type_descriptor.time_unit),
296
0
            floor_timestamp_seconds(max_value, column_schema.type_descriptor.time_unit), *timezone);
297
0
}
298
299
template <typename NativeType>
300
175
bool valid_min_max(const NativeType& min_value, const NativeType& max_value) {
301
175
    if constexpr (std::is_floating_point_v<NativeType>) {
302
        // Parquet requires readers to ignore min/max statistics if either bound is NaN.
303
0
        if (std::isnan(min_value) || std::isnan(max_value)) {
304
0
            return false;
305
0
        }
306
0
    }
307
0
    return true;
308
175
}
parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_113valid_min_maxIhEEbRKT_S6_
Line
Count
Source
300
2
bool valid_min_max(const NativeType& min_value, const NativeType& max_value) {
301
    if constexpr (std::is_floating_point_v<NativeType>) {
302
        // Parquet requires readers to ignore min/max statistics if either bound is NaN.
303
        if (std::isnan(min_value) || std::isnan(max_value)) {
304
            return false;
305
        }
306
    }
307
2
    return true;
308
2
}
parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_113valid_min_maxIiEEbRKT_S6_
Line
Count
Source
300
173
bool valid_min_max(const NativeType& min_value, const NativeType& max_value) {
301
    if constexpr (std::is_floating_point_v<NativeType>) {
302
        // Parquet requires readers to ignore min/max statistics if either bound is NaN.
303
        if (std::isnan(min_value) || std::isnan(max_value)) {
304
            return false;
305
        }
306
    }
307
173
    return true;
308
173
}
Unexecuted instantiation: parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_113valid_min_maxIlEEbRKT_S6_
Unexecuted instantiation: parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_113valid_min_maxIfEEbRKT_S6_
Unexecuted instantiation: parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_113valid_min_maxIdEEbRKT_S6_
309
310
169
bool decoded_min_max_is_ordered(const ParquetColumnStatistics& column_statistics) {
311
169
    return !(column_statistics.max_value < column_statistics.min_value);
312
169
}
313
314
bool set_decoded_binary_field(const ParquetColumnSchema& column_schema, DecodedValueKind value_kind,
315
                              const StringRef& value, Field* field,
316
0
                              const cctz::time_zone* timezone) {
317
0
    std::vector<StringRef> binary_values {value};
318
0
    DecodedColumnView view;
319
0
    view.value_kind = value_kind;
320
0
    view.binary_values = &binary_values;
321
0
    return read_decoded_field(column_schema, view, field, timezone).ok();
322
0
}
323
324
template <typename T>
325
3
T load_predicate_value(const char* data) {
326
3
    T value;
327
3
    memcpy(&value, data, sizeof(T));
328
3
    return value;
329
3
}
Unexecuted instantiation: parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_120load_predicate_valueIaEET_PKc
Unexecuted instantiation: parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_120load_predicate_valueIsEET_PKc
Unexecuted instantiation: parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_120load_predicate_valueIiEET_PKc
parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_120load_predicate_valueIlEET_PKc
Line
Count
Source
325
3
T load_predicate_value(const char* data) {
326
3
    T value;
327
3
    memcpy(&value, data, sizeof(T));
328
3
    return value;
329
3
}
330
331
3
std::optional<int64_t> load_predicate_integral_value(const char* buf, size_t size) {
332
3
    switch (size) {
333
0
    case sizeof(int8_t):
334
0
        return static_cast<int64_t>(load_predicate_value<int8_t>(buf));
335
0
    case sizeof(int16_t):
336
0
        return static_cast<int64_t>(load_predicate_value<int16_t>(buf));
337
0
    case sizeof(int32_t):
338
0
        return static_cast<int64_t>(load_predicate_value<int32_t>(buf));
339
3
    case sizeof(int64_t):
340
3
        return load_predicate_value<int64_t>(buf);
341
0
    default:
342
0
        return std::nullopt;
343
3
    }
344
3
}
345
346
bool logical_integer_fits_physical_int32(const ParquetTypeDescriptor& type_descriptor,
347
3
                                         int64_t value) {
348
3
    const int bit_width =
349
3
            type_descriptor.integer_bit_width > 0 ? type_descriptor.integer_bit_width : 32;
350
3
    if (type_descriptor.is_unsigned_integer) {
351
3
        const uint64_t max_value = bit_width >= 32 ? std::numeric_limits<uint32_t>::max()
352
3
                                                   : ((uint64_t {1} << bit_width) - 1);
353
3
        return value >= 0 && static_cast<uint64_t>(value) <= max_value;
354
3
    }
355
0
    const int64_t min_value = bit_width >= 32 ? std::numeric_limits<int32_t>::min()
356
0
                                              : -(int64_t {1} << (bit_width - 1));
357
0
    const int64_t max_value = bit_width >= 32 ? std::numeric_limits<int32_t>::max()
358
0
                                              : ((int64_t {1} << (bit_width - 1)) - 1);
359
0
    return value >= min_value && value <= max_value;
360
3
}
361
362
std::optional<int32_t> convert_logical_integer_to_physical_int32(
363
3
        const ParquetTypeDescriptor& type_descriptor, int64_t value) {
364
3
    if (!logical_integer_fits_physical_int32(type_descriptor, value)) {
365
1
        return std::nullopt;
366
1
    }
367
2
    if (!type_descriptor.is_unsigned_integer) {
368
0
        return static_cast<int32_t>(value);
369
0
    }
370
2
    const auto unsigned_value = static_cast<uint32_t>(value);
371
2
    int32_t physical_value;
372
2
    memcpy(&physical_value, &unsigned_value, sizeof(physical_value));
373
2
    return physical_value;
374
2
}
375
376
class NativeParquetBloomFilterAdapter final : public segment_v2::BloomFilter {
377
public:
378
    NativeParquetBloomFilterAdapter(const ParquetColumnSchema& column_schema,
379
                                    const segment_v2::BloomFilter& bloom_filter)
380
3
            : _column_schema(column_schema), _bloom_filter(bloom_filter) {}
381
382
0
    void add_bytes(const char*, size_t) override { DORIS_CHECK(false); }
383
384
3
    bool test_bytes(const char* buf, size_t size) const override {
385
3
        if (buf == nullptr ||
386
3
            _column_schema.type_descriptor.physical_type != tparquet::Type::INT32) {
387
0
            return _bloom_filter.test_bytes(buf, size);
388
0
        }
389
3
        const auto logical_value = load_predicate_integral_value(buf, size);
390
3
        if (!logical_value.has_value()) {
391
0
            return true;
392
0
        }
393
3
        const auto physical_value = convert_logical_integer_to_physical_int32(
394
3
                _column_schema.type_descriptor, *logical_value);
395
3
        if (!physical_value.has_value()) {
396
1
            return false;
397
1
        }
398
        // Native file Bloom bytes are hashed from the Parquet physical carrier, not the wider
399
        // Doris logical literal used by VExpr (for example UINT32 is exposed as BIGINT).
400
2
        return _bloom_filter.test_bytes(reinterpret_cast<const char*>(&*physical_value),
401
2
                                        sizeof(*physical_value));
402
3
    }
403
404
0
    void set_has_null(bool has_null) override { DORIS_CHECK(!has_null); }
405
0
    bool has_null() const override { return false; }
406
0
    void add_hash(uint64_t) override { DORIS_CHECK(false); }
407
0
    bool test_hash(uint64_t hash) const override { return _bloom_filter.test_hash(hash); }
408
409
private:
410
    const ParquetColumnSchema& _column_schema;
411
    const segment_v2::BloomFilter& _bloom_filter;
412
};
413
414
5
bool bloom_filter_supported(const ParquetColumnSchema& column_schema) {
415
5
    if (!bloom_logical_type_supported(column_schema)) {
416
0
        return false;
417
0
    }
418
5
    switch (column_schema.type_descriptor.physical_type) {
419
0
    case tparquet::Type::BOOLEAN:
420
4
    case tparquet::Type::INT32:
421
4
    case tparquet::Type::INT64:
422
4
    case tparquet::Type::FLOAT:
423
4
    case tparquet::Type::DOUBLE:
424
5
    case tparquet::Type::BYTE_ARRAY:
425
5
        return true;
426
0
    case tparquet::Type::FIXED_LEN_BYTE_ARRAY:
427
0
        return column_schema.type_descriptor.is_string_like &&
428
0
               column_schema.type_descriptor.fixed_length > 0;
429
0
    default:
430
0
        return false;
431
5
    }
432
5
}
433
434
const ParquetColumnSchema* resolve_local_leaf_schema(
435
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& schema,
436
194
        const format::LocalColumnId file_column_id) {
437
194
    if (!file_column_id.is_valid() || file_column_id.value() >= static_cast<int>(schema.size())) {
438
0
        return nullptr;
439
0
    }
440
194
    const ParquetColumnSchema* column_schema = schema[file_column_id.value()].get();
441
194
    if (column_schema == nullptr || column_schema->kind != ParquetColumnSchemaKind::PRIMITIVE ||
442
194
        column_schema->leaf_column_id < 0 || column_schema->max_repetition_level > 0) {
443
0
        return nullptr;
444
0
    }
445
194
    return column_schema;
446
194
}
447
448
std::optional<format::LocalColumnId> file_column_id_by_block_position(
449
213
        const format::FileScanRequest& request, int block_position) {
450
534
    for (const auto& [file_column_id, local_index] : request.local_positions) {
451
534
        if (local_index.value() == block_position) {
452
213
            return file_column_id;
453
213
        }
454
534
    }
455
0
    return std::nullopt;
456
213
}
457
458
enum class VariantComparisonOp { EQ, NE, LT, LE, GT, GE };
459
460
struct VariantShreddedPredicate {
461
    int slot_index = -1;
462
    std::vector<std::string> path;
463
    DataTypePtr comparison_type;
464
    DataTypePtr literal_type;
465
    Field literal;
466
    VariantComparisonOp op = VariantComparisonOp::EQ;
467
};
468
469
245
std::string callable_name(const VExprSPtr& expr) {
470
245
    if (const auto function = std::dynamic_pointer_cast<VectorizedFnCall>(expr);
471
245
        function != nullptr) {
472
75
        return function->function_name();
473
75
    }
474
170
    return expr == nullptr ? std::string {} : expr->expr_name();
475
245
}
476
477
115
std::optional<VariantComparisonOp> variant_comparison_op(std::string_view name) {
478
115
    if (name == "eq") {
479
9
        return VariantComparisonOp::EQ;
480
9
    }
481
106
    if (name == "ne") {
482
0
        return VariantComparisonOp::NE;
483
0
    }
484
106
    if (name == "lt") {
485
1
        return VariantComparisonOp::LT;
486
1
    }
487
105
    if (name == "le") {
488
0
        return VariantComparisonOp::LE;
489
0
    }
490
105
    if (name == "gt") {
491
88
        return VariantComparisonOp::GT;
492
88
    }
493
17
    if (name == "ge") {
494
0
        return VariantComparisonOp::GE;
495
0
    }
496
17
    return std::nullopt;
497
17
}
498
499
1
VariantComparisonOp reverse_variant_comparison(VariantComparisonOp op) {
500
1
    switch (op) {
501
0
    case VariantComparisonOp::EQ:
502
0
    case VariantComparisonOp::NE:
503
0
        return op;
504
1
    case VariantComparisonOp::LT:
505
1
        return VariantComparisonOp::GT;
506
0
    case VariantComparisonOp::LE:
507
0
        return VariantComparisonOp::GE;
508
0
    case VariantComparisonOp::GT:
509
0
        return VariantComparisonOp::LT;
510
0
    case VariantComparisonOp::GE:
511
0
        return VariantComparisonOp::LE;
512
1
    }
513
0
    __builtin_unreachable();
514
1
}
515
516
150
std::optional<std::pair<Field, DataTypePtr>> variant_literal(const VExprSPtr& expr) {
517
150
    const auto literal = std::dynamic_pointer_cast<VLiteral>(expr);
518
150
    if (literal == nullptr || !literal->get_column_ptr() || literal->get_column_ptr()->empty()) {
519
19
        return std::nullopt;
520
19
    }
521
131
    Field value;
522
131
    literal->get_column_ptr()->get(0, value);
523
131
    if (value.is_null()) {
524
0
        return std::nullopt;
525
0
    }
526
131
    return std::make_pair(std::move(value), literal->get_data_type());
527
131
}
528
529
std::optional<VariantShreddedPredicate> extract_variant_shredded_predicate(
530
328
        const VExprContextSPtr& conjunct) {
531
328
    if (conjunct == nullptr || conjunct->root() == nullptr ||
532
328
        conjunct->root()->get_num_children() != 2) {
533
213
        return std::nullopt;
534
213
    }
535
115
    auto op = variant_comparison_op(callable_name(conjunct->root()));
536
115
    if (!op.has_value()) {
537
17
        return std::nullopt;
538
17
    }
539
540
98
    VExprSPtr value_expr;
541
98
    std::optional<std::pair<Field, DataTypePtr>> literal;
542
98
    if ((literal = variant_literal(conjunct->root()->get_child(1))).has_value()) {
543
88
        value_expr = conjunct->root()->get_child(0);
544
88
    } else if ((literal = variant_literal(conjunct->root()->get_child(0))).has_value()) {
545
1
        value_expr = conjunct->root()->get_child(1);
546
1
        op = reverse_variant_comparison(*op);
547
9
    } else {
548
9
        return std::nullopt;
549
9
    }
550
551
89
    const auto comparison_type = value_expr->data_type();
552
132
    while (value_expr->node_type() == TExprNodeType::CAST_EXPR &&
553
132
           value_expr->get_num_children() == 1) {
554
44
        if (!expr_zonemap::data_types_compatible(value_expr->data_type(), comparison_type)) {
555
            // Every removed cast must preserve the comparison domain. Otherwise bounds for the
556
            // raw typed leaf could skip rows whose value changes in an intermediate narrowing cast.
557
1
            return std::nullopt;
558
1
        }
559
43
        value_expr = value_expr->get_child(0);
560
43
    }
561
562
88
    std::vector<std::string> reverse_path;
563
130
    while (callable_name(value_expr) == "element_at" && value_expr->get_num_children() == 2) {
564
42
        const auto key = variant_literal(value_expr->get_child(1));
565
42
        if (!key.has_value() || key->first.get_type() != TYPE_STRING) {
566
            // Repeated array shredding has no single scalar page range, so only object keys are
567
            // eligible for this file-level optimization.
568
0
            return std::nullopt;
569
0
        }
570
42
        reverse_path.push_back(key->first.get<TYPE_STRING>());
571
42
        value_expr = value_expr->get_child(0);
572
42
    }
573
88
    const auto slot = std::dynamic_pointer_cast<VSlotRef>(value_expr);
574
88
    if (slot == nullptr || reverse_path.empty() || comparison_type == nullptr ||
575
88
        remove_nullable(slot->data_type())->get_primitive_type() != TYPE_VARIANT ||
576
88
        !expr_zonemap::data_types_compatible(comparison_type, literal->second)) {
577
46
        return std::nullopt;
578
46
    }
579
42
    std::ranges::reverse(reverse_path);
580
42
    return VariantShreddedPredicate {.slot_index = slot->column_id(),
581
42
                                     .path = std::move(reverse_path),
582
42
                                     .comparison_type = comparison_type,
583
42
                                     .literal_type = literal->second,
584
42
                                     .literal = std::move(literal->first),
585
42
                                     .op = *op};
586
88
}
587
588
701
bool has_variant_shredded_filter(const format::FileScanRequest& request) {
589
701
    return std::ranges::any_of(request.conjuncts, [](const auto& conjunct) {
590
216
        return extract_variant_shredded_predicate(conjunct).has_value();
591
216
    });
592
701
}
593
594
76
const ParquetColumnSchema* child_named(const ParquetColumnSchema& parent, std::string_view name) {
595
133
    const auto it = std::ranges::find_if(parent.children, [&](const auto& child) {
596
133
        return child != nullptr && child->name == name;
597
133
    });
598
76
    return it == parent.children.end() ? nullptr : it->get();
599
76
}
600
601
struct ResolvedVariantShredding {
602
    const ParquetColumnSchema* fallback_value = nullptr;
603
    const ParquetColumnSchema* typed_value = nullptr;
604
};
605
606
16
bool metadata_cast_is_order_preserving(const DataTypePtr& source, const DataTypePtr& target) {
607
16
    if (expr_zonemap::data_types_compatible(source, target)) {
608
14
        return true;
609
14
    }
610
2
    const auto source_type = remove_nullable(source);
611
2
    const auto target_type = remove_nullable(target);
612
2
    const auto source_primitive = source_type->get_primitive_type();
613
2
    const auto target_primitive = target_type->get_primitive_type();
614
    // Metadata bounds may cross only exact widening domains. This mirrors the residual CAST while
615
    // excluding rounding, overflow, and narrowing cases that could reverse a pruning decision.
616
2
    if (source_primitive == TYPE_FLOAT && target_primitive == TYPE_DOUBLE) {
617
0
        return true;
618
0
    }
619
2
    if (is_int(source_primitive) && source_primitive != TYPE_LARGEINT &&
620
2
        is_decimalv3(target_primitive)) {
621
2
        const uint32_t required_integer_digits = source_primitive == TYPE_TINYINT    ? 3
622
2
                                                 : source_primitive == TYPE_SMALLINT ? 5
623
2
                                                 : source_primitive == TYPE_INT      ? 10
624
2
                                                                                     : 19;
625
2
        return target_type->get_precision() >= target_type->get_scale() &&
626
2
               target_type->get_precision() - target_type->get_scale() >= required_integer_digits;
627
2
    }
628
0
    if (is_decimalv3(source_primitive) && is_decimalv3(target_primitive)) {
629
0
        const uint32_t source_integer_digits =
630
0
                source_type->get_precision() - source_type->get_scale();
631
0
        const uint32_t target_integer_digits =
632
0
                target_type->get_precision() - target_type->get_scale();
633
0
        return target_integer_digits >= source_integer_digits &&
634
0
               target_type->get_scale() >= source_type->get_scale();
635
0
    }
636
0
    return false;
637
0
}
638
639
std::optional<Field> cast_metadata_field(const Field& value, const DataTypePtr& source,
640
6
                                         const DataTypePtr& target) {
641
6
    if (expr_zonemap::data_types_compatible(source, target)) {
642
0
        return value;
643
0
    }
644
6
    const auto source_type = remove_nullable(source);
645
6
    const auto target_type = remove_nullable(target);
646
6
    if (source_type->get_primitive_type() == TYPE_FLOAT &&
647
6
        target_type->get_primitive_type() == TYPE_DOUBLE) {
648
0
        return Field::create_field<TYPE_DOUBLE>(static_cast<double>(value.get<TYPE_FLOAT>()));
649
0
    }
650
6
    try {
651
6
        auto source_column = source_type->create_column();
652
6
        source_column->insert(value);
653
6
        DataTypeSerDe::FormatOptions options = DataTypeSerDe::get_default_format_options();
654
6
        options.converted_from_string = true;
655
6
        std::string text = source_type->to_string(*source_column, 0, options);
656
6
        StringRef input(text.data(), text.size());
657
6
        auto target_column = target_type->create_column();
658
6
        if (!target_type->get_serde()
659
6
                     ->from_string_strict_mode(input, *target_column, options)
660
6
                     .ok() ||
661
6
            target_column->size() != 1) {
662
0
            return std::nullopt;
663
0
        }
664
6
        Field result;
665
6
        target_column->get(0, result);
666
6
        return result;
667
6
    } catch (...) {
668
0
        return std::nullopt;
669
0
    }
670
6
}
671
672
std::optional<ParquetColumnStatistics> normalize_variant_statistics(
673
        const VariantShreddedPredicate& predicate, const ParquetColumnSchema& typed_value,
674
42
        const ParquetColumnStatistics& statistics) {
675
42
    if (!statistics.has_min_max ||
676
42
        expr_zonemap::data_types_compatible(typed_value.type, predicate.comparison_type)) {
677
39
        return statistics;
678
39
    }
679
3
    auto min_value =
680
3
            cast_metadata_field(statistics.min_value, typed_value.type, predicate.comparison_type);
681
3
    auto max_value =
682
3
            cast_metadata_field(statistics.max_value, typed_value.type, predicate.comparison_type);
683
3
    if (!min_value.has_value() || !max_value.has_value()) {
684
0
        return std::nullopt;
685
0
    }
686
3
    auto normalized = statistics;
687
3
    normalized.min_value = std::move(*min_value);
688
3
    normalized.max_value = std::move(*max_value);
689
3
    return normalized;
690
3
}
691
692
std::optional<ResolvedVariantShredding> resolve_variant_shredding(
693
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
694
19
        const format::FileScanRequest& request, const VariantShreddedPredicate& predicate) {
695
19
    const auto local_id = file_column_id_by_block_position(request, predicate.slot_index);
696
19
    if (!local_id.has_value() || local_id->value() < 0 ||
697
19
        local_id->value() >= static_cast<int>(file_schema.size())) {
698
0
        return std::nullopt;
699
0
    }
700
19
    const ParquetColumnSchema* wrapper = file_schema[local_id->value()].get();
701
19
    if (wrapper == nullptr || wrapper->kind != ParquetColumnSchemaKind::VARIANT) {
702
0
        return std::nullopt;
703
0
    }
704
19
    for (const auto& component : predicate.path) {
705
19
        const auto* typed_object = child_named(*wrapper, "typed_value");
706
19
        if (typed_object == nullptr || typed_object->kind != ParquetColumnSchemaKind::STRUCT) {
707
0
            return std::nullopt;
708
0
        }
709
19
        wrapper = child_named(*typed_object, component);
710
19
        if (wrapper == nullptr || wrapper->kind != ParquetColumnSchemaKind::STRUCT) {
711
0
            return std::nullopt;
712
0
        }
713
19
    }
714
19
    const auto* fallback = child_named(*wrapper, "value");
715
19
    const auto* typed = child_named(*wrapper, "typed_value");
716
19
    const auto typed_primitive = typed == nullptr || typed->type == nullptr
717
19
                                         ? INVALID_TYPE
718
19
                                         : remove_nullable(typed->type)->get_primitive_type();
719
19
    if (fallback == nullptr || typed == nullptr ||
720
19
        fallback->kind != ParquetColumnSchemaKind::PRIMITIVE ||
721
19
        typed->kind != ParquetColumnSchemaKind::PRIMITIVE || typed->max_repetition_level != 0 ||
722
        // Parquet float statistics do not prove that a page contains no NaN. Min/max pruning in
723
        // the presence of NaN is not order preserving, so keep those pages until such proof exists.
724
19
        typed_primitive == TYPE_FLOAT || typed_primitive == TYPE_DOUBLE ||
725
19
        !metadata_cast_is_order_preserving(typed->type, predicate.comparison_type) ||
726
19
        !expr_zonemap::data_types_compatible(predicate.comparison_type, predicate.literal_type)) {
727
3
        return std::nullopt;
728
3
    }
729
16
    return ResolvedVariantShredding {.fallback_value = fallback, .typed_value = typed};
730
19
}
731
732
bool fallback_is_all_null(const tparquet::RowGroup& row_group,
733
16
                          const ParquetColumnSchema& fallback) {
734
16
    if (fallback.max_repetition_level != 0 || fallback.leaf_column_id < 0 ||
735
16
        fallback.leaf_column_id >= static_cast<int>(row_group.columns.size())) {
736
0
        return false;
737
0
    }
738
16
    const auto& chunk = row_group.columns[fallback.leaf_column_id];
739
16
    return row_group.num_rows >= 0 && chunk.__isset.meta_data &&
740
16
           chunk.meta_data.num_values == row_group.num_rows && chunk.meta_data.__isset.statistics &&
741
16
           chunk.meta_data.statistics.__isset.null_count &&
742
16
           chunk.meta_data.statistics.null_count == chunk.meta_data.num_values;
743
16
}
744
745
bool variant_statistics_exclude(const VariantShreddedPredicate& predicate,
746
42
                                const ParquetColumnStatistics& statistics) {
747
42
    if (!statistics.has_any_statistics()) {
748
1
        return false;
749
1
    }
750
41
    if (!statistics.has_not_null) {
751
0
        return true;
752
0
    }
753
41
    if (!statistics.has_min_max) {
754
0
        return false;
755
0
    }
756
41
    const auto& literal = predicate.literal;
757
41
    switch (predicate.op) {
758
0
    case VariantComparisonOp::EQ:
759
0
        return literal < statistics.min_value || statistics.max_value < literal;
760
0
    case VariantComparisonOp::NE:
761
0
        return statistics.min_value == literal && statistics.max_value == literal;
762
0
    case VariantComparisonOp::LT:
763
0
        return statistics.min_value >= literal;
764
0
    case VariantComparisonOp::LE:
765
0
        return statistics.min_value > literal;
766
41
    case VariantComparisonOp::GT:
767
41
        return statistics.max_value <= literal;
768
0
    case VariantComparisonOp::GE:
769
0
        return statistics.max_value < literal;
770
41
    }
771
0
    __builtin_unreachable();
772
41
}
773
774
958
bool has_expr_zonemap_filter(const format::FileScanRequest& request, const RuntimeState*) {
775
    // FileScannerV2 metadata pruning is a fixed part of its scan pipeline and must not inherit
776
    // the legacy scanner's expression ZoneMap session gate.
777
    // TODO: Fence metadata pruning at the first unsafe/error-preserving conjunct so a later
778
    // ZoneMap predicate cannot bypass its row-level evaluation.
779
958
    for (const auto& conjunct : request.conjuncts) {
780
476
        if (conjunct != nullptr && conjunct->root() != nullptr &&
781
476
            conjunct->root()->can_evaluate_zonemap_filter()) {
782
257
            return true;
783
257
        }
784
476
    }
785
701
    return has_variant_shredded_filter(request);
786
958
}
787
788
111
std::set<int> collect_expr_zonemap_slot_indexes(const VExprContextSPtrs& conjuncts) {
789
111
    std::set<int> slot_indexes;
790
118
    for (const auto& conjunct : conjuncts) {
791
118
        if (conjunct != nullptr && conjunct->root() != nullptr &&
792
118
            conjunct->root()->can_evaluate_zonemap_filter()) {
793
104
            conjunct->root()->collect_slot_column_ids(slot_indexes);
794
104
        }
795
118
    }
796
111
    return slot_indexes;
797
111
}
798
799
template <typename SlotIndexSelector>
800
std::map<int, VExprContextSPtrs> collect_conjuncts_by_single_slot(
801
609
        const VExprContextSPtrs& conjuncts, SlotIndexSelector slot_index_selector) {
802
609
    std::map<int, VExprContextSPtrs> conjuncts_by_slot;
803
609
    for (const auto& conjunct : conjuncts) {
804
303
        const auto slot_index = slot_index_selector(conjunct);
805
303
        if (slot_index >= 0) {
806
84
            conjuncts_by_slot[slot_index].push_back(conjunct);
807
84
        }
808
303
    }
809
609
    return conjuncts_by_slot;
810
609
}
811
812
std::shared_ptr<segment_v2::ZoneMap> make_zonemap_from_statistics(
813
153
        const ParquetColumnStatistics& statistics) {
814
153
    if (!statistics.has_null_count && !statistics.has_min_max) {
815
42
        return nullptr;
816
42
    }
817
111
    segment_v2::ZoneMap zone_map;
818
111
    zone_map.has_null = statistics.has_null;
819
111
    zone_map.has_not_null = statistics.has_not_null;
820
111
    if (!statistics.has_not_null) {
821
0
        return std::make_shared<segment_v2::ZoneMap>(std::move(zone_map));
822
0
    }
823
111
    if (!statistics.has_min_max) {
824
        // Null counts remain trustworthy when min/max decoding fails (for example, because a
825
        // floating-point bound is NaN). pass_all prevents range pruning without discarding the
826
        // has_null/has_not_null flags needed by IS NULL and IS NOT NULL predicates.
827
0
        zone_map.pass_all = true;
828
0
        return std::make_shared<segment_v2::ZoneMap>(std::move(zone_map));
829
0
    }
830
111
    zone_map.min_value = statistics.min_value;
831
111
    zone_map.max_value = statistics.max_value;
832
111
    return std::make_shared<segment_v2::ZoneMap>(std::move(zone_map));
833
111
}
834
835
void add_slot_zonemap(ZoneMapEvalContext* ctx, int slot_index, const DataTypePtr& data_type,
836
153
                      std::shared_ptr<segment_v2::ZoneMap> zone_map) {
837
153
    DORIS_CHECK(ctx != nullptr);
838
153
    ZoneMapEvalContext::SlotZoneMap slot_zone_map;
839
153
    slot_zone_map.data_type = data_type;
840
153
    slot_zone_map.zone_map = std::move(zone_map);
841
153
    ctx->slots.emplace(slot_index, std::move(slot_zone_map));
842
153
}
843
844
100
void accumulate_zonemap_stats(const ZoneMapEvalContext& ctx, ParquetPruningStats* pruning_stats) {
845
100
    if (pruning_stats == nullptr) {
846
3
        return;
847
3
    }
848
97
    pruning_stats->expr_zonemap_unusable_evals += ctx.stats.unusable_zonemap_eval_count;
849
97
    pruning_stats->in_zonemap_point_check_count += ctx.stats.in_zonemap_point_check_count;
850
97
    pruning_stats->in_zonemap_range_only_count += ctx.stats.in_zonemap_range_only_count;
851
97
}
852
853
} // namespace
854
855
bool can_use_parquet_page_index(const format::FileScanRequest& request,
856
295
                                const RuntimeState* runtime_state) {
857
295
    return config::enable_parquet_page_index && has_expr_zonemap_filter(request, runtime_state);
858
295
}
859
860
std::shared_ptr<segment_v2::ZoneMap> ParquetStatisticsUtils::MakeZoneMap(
861
153
        const ParquetColumnStatistics& statistics) {
862
153
    return make_zonemap_from_statistics(statistics);
863
153
}
864
865
ParquetColumnStatistics ParquetStatisticsUtils::TransformColumnStatistics(
866
        const ParquetColumnSchema& column_schema, const tparquet::Statistics* statistics,
867
129
        int64_t column_value_count, const cctz::time_zone* timezone) {
868
129
    ParquetColumnStatistics result;
869
129
    if (statistics == nullptr || column_value_count < 0) {
870
39
        return result;
871
39
    }
872
873
90
    if (statistics->__isset.null_count && statistics->null_count > column_value_count) {
874
        // An impossible null count makes all derived min/max and all-null flags untrustworthy;
875
        // disable pruning instead of turning corrupt footer metadata into false negatives.
876
0
        return result;
877
0
    }
878
879
90
    const bool has_null_count = statistics->__isset.null_count && statistics->null_count >= 0;
880
90
    const int64_t null_count = has_null_count ? statistics->null_count : 0;
881
90
    const bool has_not_null = has_null_count ? column_value_count > null_count : true;
882
90
    const std::string* min_value = statistics->__isset.min_value
883
90
                                           ? &statistics->min_value
884
90
                                           : (statistics->__isset.min ? &statistics->min : nullptr);
885
90
    const std::string* max_value = statistics->__isset.max_value
886
90
                                           ? &statistics->max_value
887
90
                                           : (statistics->__isset.max ? &statistics->max : nullptr);
888
889
90
    tparquet::ColumnIndex index;
890
90
    index.__set_null_pages({!has_not_null});
891
90
    index.__set_null_counts({null_count});
892
90
    if (min_value != nullptr && max_value != nullptr) {
893
89
        index.__set_min_values({*min_value});
894
89
        index.__set_max_values({*max_value});
895
89
    }
896
    // Footer statistics and page indexes share the same little-endian physical encoding. Reusing
897
    // one decoder keeps native row-group and page pruning identical for logical types and NaNs.
898
90
    if (!build_native_page_statistics(index, column_schema, 0, column_value_count, &result,
899
90
                                      timezone)) {
900
8
        return {};
901
8
    }
902
82
    if (!has_null_count) {
903
0
        result.has_null_count = false;
904
0
        result.has_null = true;
905
0
    }
906
82
    return result;
907
90
}
908
909
bool ParquetStatisticsUtils::NativeBloomFilterExcludes(
910
        const ParquetColumnSchema& column_schema, int slot_index,
911
3
        const VExprContextSPtrs& conjuncts, const segment_v2::BloomFilter& bloom_filter) {
912
3
    if (!bloom_filter_supported(column_schema)) {
913
0
        return false;
914
0
    }
915
3
    NativeParquetBloomFilterAdapter adapter(column_schema, bloom_filter);
916
3
    BloomFilterEvalContext ctx;
917
3
    ctx.slots.emplace(slot_index, BloomFilterEvalContext::SlotBloomFilter {
918
3
                                          .data_type = column_schema.type,
919
3
                                          .bloom_filter = &adapter,
920
3
                                  });
921
3
    return VExprContext::evaluate_bloom_filter(conjuncts, ctx) == ZoneMapFilterResult::kNoMatch;
922
3
}
923
924
namespace {
925
926
void collect_filtered_leaf_ids(const ParquetColumnSchema& column_schema,
927
                               const format::LocalColumnIndex* projection,
928
71
                               std::set<int>* leaf_column_ids) {
929
71
    if (column_schema.kind == ParquetColumnSchemaKind::PRIMITIVE) {
930
68
        if (column_schema.leaf_column_id >= 0) {
931
68
            leaf_column_ids->insert(column_schema.leaf_column_id);
932
68
        }
933
68
        return;
934
68
    }
935
6
    for (const auto& child_schema : column_schema.children) {
936
6
        if (!format::is_child_projected(projection, child_schema->local_id)) {
937
3
            continue;
938
3
        }
939
        // The leaf set must match the physical projection. A complete Variant projection naturally
940
        // reaches every sibling; a validated typed-leaf projection reads only retained children.
941
3
        const auto* child_projection =
942
3
                format::find_child_projection(projection, child_schema->local_id);
943
3
        collect_filtered_leaf_ids(*child_schema, child_projection, leaf_column_ids);
944
3
    }
945
3
}
946
947
177
bool native_metadata_predicate_is_type_safe(const ParquetColumnSchema& column_schema) {
948
177
    DORIS_CHECK(column_schema.type != nullptr);
949
    // Raw VARBINARY file slots may feed table-side STRING casts. Footer/page metadata is still in
950
    // the pre-cast domain, so using it for a rewritten table predicate can cause false negatives.
951
177
    if (remove_nullable(column_schema.type)->get_primitive_type() == TYPE_VARBINARY) {
952
0
        return false;
953
0
    }
954
    // UUID readers render canonical text, so their physical 16-byte bounds are not STRING bounds.
955
177
    return !column_schema.type_descriptor.is_uuid;
956
177
}
957
958
14
bool variant_metadata_predicate_is_type_safe(const ParquetColumnSchema& column_schema) {
959
14
    if (!native_metadata_predicate_is_type_safe(column_schema)) {
960
2
        return false;
961
2
    }
962
12
    const auto& descriptor = column_schema.type_descriptor;
963
    // An ordinary raw-binary STRING slot preserves its bytes, but Variant reconstruction renders
964
    // the binary identity before the residual STRING cast and therefore changes the domain.
965
12
    return !descriptor.is_string_like || descriptor.is_string_annotation;
966
14
}
967
968
bool check_native_statistics(const tparquet::FileMetaData& metadata,
969
                             const tparquet::RowGroup& row_group,
970
                             const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
971
                             const format::FileScanRequest& request,
972
111
                             ParquetPruningStats* pruning_stats, const cctz::time_zone* timezone) {
973
111
    const auto slot_indexes = collect_expr_zonemap_slot_indexes(request.conjuncts);
974
111
    if (slot_indexes.empty()) {
975
11
        return false;
976
11
    }
977
100
    ZoneMapEvalContext ctx;
978
102
    for (const int slot_index : slot_indexes) {
979
102
        const auto file_column_id = file_column_id_by_block_position(request, slot_index);
980
102
        if (!file_column_id.has_value()) {
981
0
            continue;
982
0
        }
983
102
        const auto* column_schema = resolve_local_leaf_schema(file_schema, *file_column_id);
984
102
        if (column_schema == nullptr || column_schema->type == nullptr ||
985
102
            !native_metadata_predicate_is_type_safe(*column_schema) ||
986
102
            column_schema->leaf_column_id >= static_cast<int>(row_group.columns.size())) {
987
0
            continue;
988
0
        }
989
102
        const auto& chunk = row_group.columns[column_schema->leaf_column_id];
990
102
        std::shared_ptr<segment_v2::ZoneMap> zone_map;
991
102
        if (chunk.__isset.meta_data) {
992
102
            const auto& column_metadata = chunk.meta_data;
993
102
            std::optional<tparquet::Statistics> safe_statistics;
994
102
            if (column_metadata.__isset.statistics) {
995
65
                safe_statistics = detail::sanitize_native_footer_statistics(
996
65
                        column_schema->type_descriptor, column_metadata.statistics,
997
65
                        detail::has_supported_type_defined_order(metadata,
998
65
                                                                 column_schema->leaf_column_id));
999
65
            }
1000
102
            zone_map = ParquetStatisticsUtils::MakeZoneMap(
1001
102
                    ParquetStatisticsUtils::TransformColumnStatistics(
1002
102
                            *column_schema,
1003
102
                            safe_statistics.has_value() ? &*safe_statistics : nullptr,
1004
102
                            column_metadata.num_values, timezone));
1005
102
        }
1006
102
        add_slot_zonemap(&ctx, slot_index, column_schema->type, std::move(zone_map));
1007
102
    }
1008
100
    const auto result = VExprContext::evaluate_zonemap_filter(request.conjuncts, ctx);
1009
100
    accumulate_zonemap_stats(ctx, pruning_stats);
1010
100
    return result == ZoneMapFilterResult::kNoMatch;
1011
111
}
1012
1013
bool check_shredded_variant_statistics(
1014
        const tparquet::FileMetaData& metadata, const tparquet::RowGroup& row_group,
1015
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
1016
89
        const format::FileScanRequest& request, const cctz::time_zone* timezone) {
1017
96
    for (const auto& conjunct : request.conjuncts) {
1018
96
        const auto predicate = extract_variant_shredded_predicate(conjunct);
1019
96
        if (!predicate.has_value()) {
1020
85
            continue;
1021
85
        }
1022
11
        const auto shredding = resolve_variant_shredding(file_schema, request, *predicate);
1023
11
        if (!shredding.has_value() || shredding->typed_value->leaf_column_id < 0 ||
1024
11
            shredding->typed_value->leaf_column_id >= static_cast<int>(row_group.columns.size()) ||
1025
11
            !fallback_is_all_null(row_group, *shredding->fallback_value) ||
1026
11
            !variant_metadata_predicate_is_type_safe(*shredding->typed_value) ||
1027
11
            !detail::has_supported_type_defined_order(metadata,
1028
6
                                                      shredding->typed_value->leaf_column_id)) {
1029
5
            continue;
1030
5
        }
1031
6
        const auto& chunk = row_group.columns[shredding->typed_value->leaf_column_id];
1032
6
        if (!chunk.__isset.meta_data) {
1033
0
            continue;
1034
0
        }
1035
6
        const auto& column_metadata = chunk.meta_data;
1036
6
        if (column_metadata.num_values != row_group.num_rows) {
1037
0
            continue;
1038
0
        }
1039
6
        std::optional<tparquet::Statistics> safe_statistics;
1040
6
        if (column_metadata.__isset.statistics) {
1041
5
            safe_statistics = detail::sanitize_native_footer_statistics(
1042
5
                    shredding->typed_value->type_descriptor, column_metadata.statistics, true);
1043
5
        }
1044
6
        const auto statistics = ParquetStatisticsUtils::TransformColumnStatistics(
1045
6
                *shredding->typed_value, safe_statistics.has_value() ? &*safe_statistics : nullptr,
1046
6
                column_metadata.num_values, timezone);
1047
6
        const auto normalized =
1048
6
                normalize_variant_statistics(*predicate, *shredding->typed_value, statistics);
1049
6
        if (normalized.has_value() && variant_statistics_exclude(*predicate, *normalized)) {
1050
3
            return true;
1051
3
        }
1052
6
    }
1053
86
    return false;
1054
89
}
1055
1056
41
bool is_native_dictionary_data_encoding(tparquet::Encoding::type encoding) {
1057
41
    return encoding == tparquet::Encoding::PLAIN_DICTIONARY ||
1058
41
           encoding == tparquet::Encoding::RLE_DICTIONARY;
1059
41
}
1060
1061
0
bool is_native_level_encoding(tparquet::Encoding::type encoding) {
1062
0
    return encoding == tparquet::Encoding::RLE || encoding == tparquet::Encoding::BIT_PACKED;
1063
0
}
1064
1065
49
bool is_native_dictionary_encoded_chunk(const tparquet::ColumnMetaData& metadata) {
1066
49
    if (!metadata.__isset.dictionary_page_offset || metadata.dictionary_page_offset < 0) {
1067
8
        return false;
1068
8
    }
1069
41
    if (metadata.__isset.encoding_stats && !metadata.encoding_stats.empty()) {
1070
41
        bool has_dictionary_data_page = false;
1071
82
        for (const auto& encoding_stat : metadata.encoding_stats) {
1072
82
            if ((encoding_stat.page_type != tparquet::PageType::DATA_PAGE &&
1073
82
                 encoding_stat.page_type != tparquet::PageType::DATA_PAGE_V2) ||
1074
82
                encoding_stat.count <= 0) {
1075
41
                continue;
1076
41
            }
1077
41
            if (!is_native_dictionary_data_encoding(encoding_stat.encoding)) {
1078
0
                return false;
1079
0
            }
1080
41
            has_dictionary_data_page = true;
1081
41
        }
1082
41
        return has_dictionary_data_page;
1083
41
    }
1084
0
    bool has_dictionary_encoding = false;
1085
0
    for (const auto encoding : metadata.encodings) {
1086
0
        if (is_native_dictionary_data_encoding(encoding)) {
1087
0
            has_dictionary_encoding = true;
1088
0
        } else if (!is_native_level_encoding(encoding)) {
1089
0
            return false;
1090
0
        }
1091
0
    }
1092
0
    return has_dictionary_encoding;
1093
0
}
1094
1095
const format::LocalColumnIndex* find_request_projection(const format::FileScanRequest& request,
1096
80
                                                        format::LocalColumnId file_column_id) {
1097
90
    for (const auto& projection : request.predicate_columns) {
1098
90
        if (projection.local_id() == file_column_id.value()) {
1099
80
            return &projection;
1100
80
        }
1101
90
    }
1102
0
    for (const auto& projection : request.non_predicate_columns) {
1103
0
        if (projection.local_id() == file_column_id.value()) {
1104
0
            return &projection;
1105
0
        }
1106
0
    }
1107
0
    return nullptr;
1108
0
}
1109
1110
ParquetRowGroupPruneReason native_dictionary_prune_reason(
1111
        const tparquet::RowGroup& row_group, int row_group_idx,
1112
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
1113
        const format::FileScanRequest& request, const cctz::time_zone* timezone,
1114
317
        ParquetFileContext* file_context, const ParquetColumnReaderProfile& column_reader_profile) {
1115
317
    if (file_context == nullptr || file_context->native_metadata == nullptr) {
1116
2
        return ParquetRowGroupPruneReason::NONE;
1117
2
    }
1118
315
    const auto conjuncts_by_slot = collect_conjuncts_by_single_slot(
1119
315
            request.conjuncts, expr_zonemap::single_slot_dictionary_index);
1120
315
    for (const auto& [slot_index, conjuncts] : conjuncts_by_slot) {
1121
80
        const auto file_column_id = file_column_id_by_block_position(request, slot_index);
1122
80
        if (!file_column_id.has_value()) {
1123
0
            continue;
1124
0
        }
1125
80
        const auto* column_schema = resolve_local_leaf_schema(file_schema, *file_column_id);
1126
80
        const auto* projection = find_request_projection(request, *file_column_id);
1127
80
        if (column_schema == nullptr || projection == nullptr || column_schema->type == nullptr ||
1128
80
            !column_schema->type_descriptor.is_string_like ||
1129
80
            column_schema->leaf_column_id >= static_cast<int>(row_group.columns.size())) {
1130
31
            continue;
1131
31
        }
1132
49
        if (!native_metadata_predicate_is_type_safe(*column_schema)) {
1133
            // The file-local VARBINARY may feed a table-side STRING cast. Pruning before that cast
1134
            // can compare different Field kinds and incorrectly discard a matching row group.
1135
0
            continue;
1136
0
        }
1137
49
        const auto& chunk = row_group.columns[column_schema->leaf_column_id];
1138
49
        if (!chunk.__isset.meta_data ||
1139
49
            (chunk.meta_data.type != tparquet::Type::BYTE_ARRAY &&
1140
49
             chunk.meta_data.type != tparquet::Type::FIXED_LEN_BYTE_ARRAY) ||
1141
49
            !is_native_dictionary_encoded_chunk(chunk.meta_data)) {
1142
8
            continue;
1143
8
        }
1144
41
        std::unique_ptr<ParquetColumnReader> reader;
1145
41
        const std::vector<RowRange> ranges {{0, row_group.num_rows}};
1146
41
        const std::unordered_map<int, tparquet::OffsetIndex> offset_indexes;
1147
        // Metadata pruning uses the real native reader, so its page work must be attributed to the
1148
        // scan profile even when the row group is eliminated before execution readers are built.
1149
41
        const auto status = NativeColumnReader::create(
1150
41
                *column_schema, projection, file_context->native_file,
1151
41
                file_context->native_metadata, row_group_idx, ranges, offset_indexes, timezone,
1152
41
                file_context->native_io_ctx, nullptr, file_context->native_page_cache_enabled,
1153
41
                file_context->native_page_cache_file_key, true, column_reader_profile, &reader);
1154
41
        if (!status.ok() || reader == nullptr) {
1155
0
            continue;
1156
0
        }
1157
41
        auto dictionary_result = reader->dictionary_values();
1158
41
        if (!dictionary_result.has_value()) {
1159
0
            continue;
1160
0
        }
1161
41
        auto dictionary = std::move(dictionary_result).value();
1162
41
        std::vector<Field> values(dictionary->size());
1163
134
        for (size_t value_idx = 0; value_idx < dictionary->size(); ++value_idx) {
1164
93
            dictionary->get(value_idx, values[value_idx]);
1165
93
        }
1166
41
        DictionaryEvalContext ctx;
1167
41
        ctx.slots.emplace(slot_index, DictionaryEvalContext::SlotDictionary {
1168
41
                                              .data_type = column_schema->type,
1169
41
                                              .values = std::move(values),
1170
41
                                      });
1171
41
        if (VExprContext::evaluate_dictionary_filter(conjuncts, ctx) ==
1172
41
            ZoneMapFilterResult::kNoMatch) {
1173
22
            return ParquetRowGroupPruneReason::DICTIONARY;
1174
22
        }
1175
41
    }
1176
293
    return ParquetRowGroupPruneReason::NONE;
1177
315
}
1178
1179
ParquetRowGroupPruneReason native_bloom_filter_prune_reason(
1180
        const tparquet::RowGroup& row_group,
1181
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
1182
        const format::FileScanRequest& request, ParquetFileContext* file_context,
1183
294
        ParquetPruningStats* pruning_stats) {
1184
294
    if (file_context == nullptr || file_context->native_file == nullptr) {
1185
0
        return ParquetRowGroupPruneReason::NONE;
1186
0
    }
1187
294
    const auto conjuncts_by_slot = collect_conjuncts_by_single_slot(
1188
294
            request.conjuncts, expr_zonemap::single_slot_bloom_filter_index);
1189
294
    for (const auto& [slot_index, conjuncts] : conjuncts_by_slot) {
1190
2
        const auto file_column_id = file_column_id_by_block_position(request, slot_index);
1191
2
        if (!file_column_id.has_value()) {
1192
0
            continue;
1193
0
        }
1194
2
        const auto* column_schema = resolve_local_leaf_schema(file_schema, *file_column_id);
1195
2
        if (column_schema == nullptr || column_schema->type == nullptr ||
1196
2
            !native_metadata_predicate_is_type_safe(*column_schema) ||
1197
2
            !bloom_filter_supported(*column_schema) ||
1198
2
            column_schema->leaf_column_id >= static_cast<int>(row_group.columns.size())) {
1199
0
            continue;
1200
0
        }
1201
2
        const auto& chunk = row_group.columns[column_schema->leaf_column_id];
1202
2
        if (!chunk.__isset.meta_data) {
1203
0
            continue;
1204
0
        }
1205
2
        std::unique_ptr<native::BlockSplitBloomFilter> bloom_filter;
1206
2
        Status status;
1207
2
        {
1208
2
            int64_t timer_sink = 0;
1209
2
            SCOPED_RAW_TIMER(pruning_stats == nullptr ? &timer_sink
1210
2
                                                      : &pruning_stats->bloom_filter_read_time);
1211
2
            status = read_native_bloom_filter(chunk.meta_data, file_context->native_file,
1212
2
                                              file_context->native_io_ctx, &bloom_filter);
1213
2
        }
1214
2
        if (!status.ok() || bloom_filter == nullptr) {
1215
1
            continue;
1216
1
        }
1217
1
        if (ParquetStatisticsUtils::NativeBloomFilterExcludes(*column_schema, slot_index, conjuncts,
1218
1
                                                              *bloom_filter)) {
1219
0
            return ParquetRowGroupPruneReason::BLOOM_FILTER;
1220
0
        }
1221
1
    }
1222
294
    return ParquetRowGroupPruneReason::NONE;
1223
294
}
1224
1225
int64_t native_requested_compressed_bytes(
1226
        const tparquet::RowGroup& row_group,
1227
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
1228
43
        const format::FileScanRequest& request) {
1229
43
    std::set<int> leaf_column_ids;
1230
70
    auto collect_projection = [&](const format::LocalColumnIndex& projection) {
1231
70
        const int32_t local_id = projection.local_id();
1232
70
        if (local_id < 0 || local_id >= static_cast<int32_t>(file_schema.size()) ||
1233
70
            file_schema[local_id] == nullptr) {
1234
2
            return;
1235
2
        }
1236
68
        collect_filtered_leaf_ids(*file_schema[local_id], &projection, &leaf_column_ids);
1237
68
    };
1238
43
    for (const auto& projection : request.predicate_columns) {
1239
38
        collect_projection(projection);
1240
38
    }
1241
43
    for (const auto& projection : request.non_predicate_columns) {
1242
32
        collect_projection(projection);
1243
32
    }
1244
43
    int64_t bytes = 0;
1245
68
    for (const int leaf_column_id : leaf_column_ids) {
1246
68
        if (leaf_column_id < 0 || leaf_column_id >= static_cast<int>(row_group.columns.size())) {
1247
0
            continue;
1248
0
        }
1249
68
        const auto& chunk = row_group.columns[leaf_column_id];
1250
68
        if (chunk.__isset.meta_data && chunk.meta_data.total_compressed_size > 0) {
1251
68
            bytes += chunk.meta_data.total_compressed_size;
1252
68
        }
1253
68
    }
1254
43
    return bytes;
1255
43
}
1256
1257
} // namespace
1258
1259
Status select_row_groups_by_metadata(
1260
        const tparquet::FileMetaData& metadata,
1261
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
1262
        const format::FileScanRequest& request, const std::vector<int>* candidate_row_groups,
1263
        std::vector<int>* selected_row_groups, bool enable_bloom_filter,
1264
        ParquetPruningStats* pruning_stats, const cctz::time_zone* timezone,
1265
        const RuntimeState* runtime_state, ParquetFileContext* file_context,
1266
        const ParquetColumnReaderProfile& column_reader_profile,
1267
598
        ParquetMetadataProbeMode probe_mode) {
1268
598
    int64_t timer_sink = 0;
1269
598
    SCOPED_RAW_TIMER(pruning_stats == nullptr ? &timer_sink
1270
598
                                              : &pruning_stats->row_group_filter_time);
1271
598
    if (selected_row_groups == nullptr) {
1272
0
        return Status::InvalidArgument("selected_row_groups is null");
1273
0
    }
1274
598
    selected_row_groups->clear();
1275
598
    const size_t candidate_size = candidate_row_groups == nullptr ? metadata.row_groups.size()
1276
598
                                                                  : candidate_row_groups->size();
1277
598
    if (pruning_stats != nullptr) {
1278
586
        pruning_stats->total_row_groups = cast_set<int64_t>(candidate_size);
1279
586
    }
1280
598
    selected_row_groups->reserve(candidate_size);
1281
1.27k
    for (size_t candidate_idx = 0; candidate_idx < candidate_size; ++candidate_idx) {
1282
673
        const int row_group_idx = candidate_row_groups == nullptr
1283
673
                                          ? static_cast<int>(candidate_idx)
1284
673
                                          : (*candidate_row_groups)[candidate_idx];
1285
673
        if (row_group_idx < 0 || row_group_idx >= static_cast<int>(metadata.row_groups.size())) {
1286
            // Candidate ids originate in external split metadata; a corrupt id must not terminate
1287
            // the BE while planning an otherwise recoverable file scan.
1288
1
            return Status::Corruption("Invalid Parquet row group candidate {} for {} row groups",
1289
1
                                      row_group_idx, metadata.row_groups.size());
1290
1
        }
1291
672
        const auto& row_group = metadata.row_groups[row_group_idx];
1292
672
        if (row_group.num_rows < 0) {
1293
0
            return Status::Corruption("Parquet row group {} has negative row count {}",
1294
0
                                      row_group_idx, row_group.num_rows);
1295
0
        }
1296
672
        if (row_group.num_rows == 0) {
1297
            // Native metadata probes construct positive row ranges; empty groups contribute no
1298
            // rows and must be discarded before dictionary, statistics, or Bloom reader setup.
1299
1
            continue;
1300
1
        }
1301
671
        ParquetRowGroupPruneReason prune_reason = ParquetRowGroupPruneReason::NONE;
1302
671
        if (probe_mode != ParquetMetadataProbeMode::EXPENSIVE_ONLY &&
1303
671
            has_expr_zonemap_filter(request, runtime_state) &&
1304
671
            (check_native_statistics(metadata, row_group, file_schema, request, pruning_stats,
1305
111
                                     timezone) ||
1306
111
             check_shredded_variant_statistics(metadata, row_group, file_schema, request,
1307
89
                                               timezone))) {
1308
25
            prune_reason = ParquetRowGroupPruneReason::STATISTICS;
1309
25
        }
1310
671
        if (probe_mode != ParquetMetadataProbeMode::FOOTER_ONLY &&
1311
671
            prune_reason == ParquetRowGroupPruneReason::NONE) {
1312
317
            prune_reason =
1313
317
                    native_dictionary_prune_reason(row_group, row_group_idx, file_schema, request,
1314
317
                                                   timezone, file_context, column_reader_profile);
1315
317
        }
1316
671
        if (probe_mode != ParquetMetadataProbeMode::FOOTER_ONLY &&
1317
671
            prune_reason == ParquetRowGroupPruneReason::NONE && enable_bloom_filter) {
1318
294
            prune_reason = native_bloom_filter_prune_reason(row_group, file_schema, request,
1319
294
                                                            file_context, pruning_stats);
1320
294
        }
1321
671
        if (prune_reason == ParquetRowGroupPruneReason::NONE) {
1322
624
            selected_row_groups->push_back(row_group_idx);
1323
624
            continue;
1324
624
        }
1325
47
        if (pruning_stats != nullptr) {
1326
43
            pruning_stats->filtered_group_rows += row_group.num_rows;
1327
43
            pruning_stats->filtered_bytes +=
1328
43
                    native_requested_compressed_bytes(row_group, file_schema, request);
1329
43
            if (prune_reason == ParquetRowGroupPruneReason::STATISTICS) {
1330
21
                ++pruning_stats->filtered_row_groups_by_statistics;
1331
22
            } else if (prune_reason == ParquetRowGroupPruneReason::DICTIONARY) {
1332
22
                ++pruning_stats->filtered_row_groups_by_dictionary;
1333
22
            } else {
1334
0
                ++pruning_stats->filtered_row_groups_by_bloom_filter;
1335
0
            }
1336
43
        }
1337
47
    }
1338
597
    return Status::OK();
1339
598
}
1340
1341
namespace {
1342
1343
std::vector<RowRange> intersect_ranges(const std::vector<RowRange>& left,
1344
10
                                       const std::vector<RowRange>& right) {
1345
10
    std::vector<RowRange> result;
1346
10
    size_t left_idx = 0;
1347
10
    size_t right_idx = 0;
1348
18
    while (left_idx < left.size() && right_idx < right.size()) {
1349
8
        const int64_t left_start = left[left_idx].start;
1350
8
        const int64_t left_end = left_start + left[left_idx].length;
1351
8
        const int64_t right_start = right[right_idx].start;
1352
8
        const int64_t right_end = right_start + right[right_idx].length;
1353
8
        const int64_t start = std::max(left_start, right_start);
1354
8
        const int64_t end = std::min(left_end, right_end);
1355
8
        if (start < end) {
1356
8
            result.push_back(RowRange {start, end - start});
1357
8
        }
1358
8
        if (left_end < right_end) {
1359
0
            ++left_idx;
1360
8
        } else {
1361
8
            ++right_idx;
1362
8
        }
1363
8
    }
1364
10
    return result;
1365
10
}
1366
1367
6
int64_t count_range_rows(const std::vector<RowRange>& ranges) {
1368
6
    int64_t rows = 0;
1369
6
    for (const auto& range : ranges) {
1370
6
        rows += range.length;
1371
6
    }
1372
6
    return rows;
1373
6
}
1374
1375
302
void append_row_range(const RowRange& range, std::vector<RowRange>* ranges) {
1376
302
    if (range.length == 0) {
1377
0
        return;
1378
0
    }
1379
302
    if (!ranges->empty()) {
1380
276
        auto& previous = ranges->back();
1381
276
        if (previous.start + previous.length == range.start) {
1382
276
            previous.length += range.length;
1383
276
            return;
1384
276
        }
1385
276
    }
1386
26
    ranges->push_back(range);
1387
26
}
1388
1389
413
bool ranges_intersect(const std::vector<RowRange>& ranges, const RowRange& range) {
1390
413
    const int64_t range_end = range.start + range.length;
1391
413
    for (const auto& selected_range : ranges) {
1392
413
        const int64_t selected_end = selected_range.start + selected_range.length;
1393
413
        if (selected_end <= range.start) {
1394
0
            continue;
1395
0
        }
1396
413
        if (selected_range.start >= range_end) {
1397
257
            return false;
1398
257
        }
1399
156
        return true;
1400
413
    }
1401
0
    return false;
1402
413
}
1403
1404
void collect_leaf_schemas(const ParquetColumnSchema& column_schema,
1405
                          const format::LocalColumnIndex* projection,
1406
82
                          std::vector<const ParquetColumnSchema*>* leaf_schemas) {
1407
82
    if (column_schema.kind == ParquetColumnSchemaKind::PRIMITIVE) {
1408
52
        leaf_schemas->push_back(&column_schema);
1409
52
        return;
1410
52
    }
1411
63
    for (const auto& child_schema : column_schema.children) {
1412
63
        if (column_schema.kind != ParquetColumnSchemaKind::VARIANT &&
1413
63
            !format::is_child_projected(projection, child_schema->local_id)) {
1414
0
            continue;
1415
0
        }
1416
        // A logical Variant projection materializes every physical sibling; build skip plans for
1417
        // that identical leaf set so shredded columns cannot drift to different row positions.
1418
63
        const auto* child_projection =
1419
63
                column_schema.kind == ParquetColumnSchemaKind::VARIANT
1420
63
                        ? nullptr
1421
63
                        : format::find_child_projection(projection, child_schema->local_id);
1422
63
        collect_leaf_schemas(*child_schema, child_projection, leaf_schemas);
1423
63
    }
1424
30
}
1425
1426
void collect_request_leaf_schemas(
1427
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
1428
        const format::FileScanRequest& request,
1429
16
        std::vector<const ParquetColumnSchema*>* leaf_schemas) {
1430
16
    std::set<int> seen_leaf_ids;
1431
19
    auto collect_projection = [&](const format::LocalColumnIndex& projection) {
1432
19
        const int32_t local_id = projection.local_id();
1433
19
        if (local_id < 0 || local_id >= static_cast<int32_t>(file_schema.size())) {
1434
0
            return;
1435
0
        }
1436
19
        std::vector<const ParquetColumnSchema*> projection_leaf_schemas;
1437
19
        collect_leaf_schemas(*file_schema[local_id], &projection, &projection_leaf_schemas);
1438
52
        for (const auto* leaf_schema : projection_leaf_schemas) {
1439
52
            DORIS_CHECK(leaf_schema != nullptr);
1440
52
            if (seen_leaf_ids.insert(leaf_schema->leaf_column_id).second) {
1441
46
                leaf_schemas->push_back(leaf_schema);
1442
46
            }
1443
52
        }
1444
19
    };
1445
16
    for (const auto& projection : request.predicate_columns) {
1446
16
        collect_projection(projection);
1447
16
    }
1448
16
    for (const auto& projection : request.non_predicate_columns) {
1449
3
        collect_projection(projection);
1450
3
    }
1451
16
}
1452
1453
template <typename ValueType>
1454
bool set_native_page_scalar_min_max(const tparquet::ColumnIndex& column_index,
1455
                                    const ParquetColumnSchema& column_schema, size_t page_idx,
1456
                                    DecodedValueKind kind, ParquetColumnStatistics* page_statistics,
1457
176
                                    const cctz::time_zone* timezone) {
1458
176
    if (page_idx >= column_index.min_values.size() || page_idx >= column_index.max_values.size() ||
1459
176
        column_index.min_values[page_idx].size() != sizeof(ValueType) ||
1460
176
        column_index.max_values[page_idx].size() != sizeof(ValueType)) {
1461
1
        return false;
1462
1
    }
1463
175
    const auto min_value = unaligned_load<ValueType>(column_index.min_values[page_idx].data());
1464
175
    const auto max_value = unaligned_load<ValueType>(column_index.max_values[page_idx].data());
1465
175
    if constexpr (std::is_integral_v<ValueType>) {
1466
175
        if (remove_nullable(column_schema.type)->get_primitive_type() == TYPE_TIMEV2) {
1467
2
            int64_t units_per_day = 0;
1468
2
            switch (column_schema.type_descriptor.time_unit) {
1469
2
            case ParquetTimeUnit::MILLIS:
1470
2
                units_per_day = 86400000;
1471
2
                break;
1472
0
            case ParquetTimeUnit::MICROS:
1473
0
                units_per_day = 86400000000;
1474
0
                break;
1475
0
            case ParquetTimeUnit::NANOS:
1476
0
                units_per_day = 86400000000000;
1477
0
                break;
1478
0
            default:
1479
0
                return false;
1480
2
            }
1481
            // TIME statistics are pruning proofs. Validate the raw carrier before rescaling so an
1482
            // invalid bound at or beyond 24:00 cannot publish a misleading ZoneMap.
1483
2
            if (min_value < 0 || max_value < 0 || min_value >= units_per_day ||
1484
2
                max_value >= units_per_day) {
1485
2
                return false;
1486
2
            }
1487
2
        }
1488
175
    }
1489
173
    if constexpr (std::is_same_v<ValueType, int64_t>) {
1490
0
        if (!timestamp_min_max_is_safe(column_schema, min_value, max_value, timezone)) {
1491
0
            return false;
1492
0
        }
1493
0
    }
1494
175
    if (!valid_min_max(min_value, max_value)) {
1495
0
        return true;
1496
0
    }
1497
175
    if (!set_decoded_field(column_schema, kind, min_value, &page_statistics->min_value, timezone) ||
1498
175
        !set_decoded_field(column_schema, kind, max_value, &page_statistics->max_value, timezone)) {
1499
6
        return false;
1500
6
    }
1501
169
    if (decoded_min_max_is_ordered(*page_statistics)) {
1502
167
        page_statistics->has_min_max = true;
1503
167
    }
1504
169
    return true;
1505
175
}
parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_130set_native_page_scalar_min_maxIiEEbRKN8tparquet11ColumnIndexERKNS1_19ParquetColumnSchemaEmNS_16DecodedValueKindEPNS1_23ParquetColumnStatisticsEPKN4cctz9time_zoneE
Line
Count
Source
1457
176
                                    const cctz::time_zone* timezone) {
1458
176
    if (page_idx >= column_index.min_values.size() || page_idx >= column_index.max_values.size() ||
1459
176
        column_index.min_values[page_idx].size() != sizeof(ValueType) ||
1460
176
        column_index.max_values[page_idx].size() != sizeof(ValueType)) {
1461
1
        return false;
1462
1
    }
1463
175
    const auto min_value = unaligned_load<ValueType>(column_index.min_values[page_idx].data());
1464
175
    const auto max_value = unaligned_load<ValueType>(column_index.max_values[page_idx].data());
1465
175
    if constexpr (std::is_integral_v<ValueType>) {
1466
175
        if (remove_nullable(column_schema.type)->get_primitive_type() == TYPE_TIMEV2) {
1467
2
            int64_t units_per_day = 0;
1468
2
            switch (column_schema.type_descriptor.time_unit) {
1469
2
            case ParquetTimeUnit::MILLIS:
1470
2
                units_per_day = 86400000;
1471
2
                break;
1472
0
            case ParquetTimeUnit::MICROS:
1473
0
                units_per_day = 86400000000;
1474
0
                break;
1475
0
            case ParquetTimeUnit::NANOS:
1476
0
                units_per_day = 86400000000000;
1477
0
                break;
1478
0
            default:
1479
0
                return false;
1480
2
            }
1481
            // TIME statistics are pruning proofs. Validate the raw carrier before rescaling so an
1482
            // invalid bound at or beyond 24:00 cannot publish a misleading ZoneMap.
1483
2
            if (min_value < 0 || max_value < 0 || min_value >= units_per_day ||
1484
2
                max_value >= units_per_day) {
1485
2
                return false;
1486
2
            }
1487
2
        }
1488
175
    }
1489
    if constexpr (std::is_same_v<ValueType, int64_t>) {
1490
        if (!timestamp_min_max_is_safe(column_schema, min_value, max_value, timezone)) {
1491
            return false;
1492
        }
1493
    }
1494
175
    if (!valid_min_max(min_value, max_value)) {
1495
0
        return true;
1496
0
    }
1497
175
    if (!set_decoded_field(column_schema, kind, min_value, &page_statistics->min_value, timezone) ||
1498
175
        !set_decoded_field(column_schema, kind, max_value, &page_statistics->max_value, timezone)) {
1499
6
        return false;
1500
6
    }
1501
169
    if (decoded_min_max_is_ordered(*page_statistics)) {
1502
167
        page_statistics->has_min_max = true;
1503
167
    }
1504
169
    return true;
1505
175
}
Unexecuted instantiation: parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_130set_native_page_scalar_min_maxIlEEbRKN8tparquet11ColumnIndexERKNS1_19ParquetColumnSchemaEmNS_16DecodedValueKindEPNS1_23ParquetColumnStatisticsEPKN4cctz9time_zoneE
Unexecuted instantiation: parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_130set_native_page_scalar_min_maxIfEEbRKN8tparquet11ColumnIndexERKNS1_19ParquetColumnSchemaEmNS_16DecodedValueKindEPNS1_23ParquetColumnStatisticsEPKN4cctz9time_zoneE
Unexecuted instantiation: parquet_statistics.cpp:_ZN5doris6format7parquet12_GLOBAL__N_130set_native_page_scalar_min_maxIdEEbRKN8tparquet11ColumnIndexERKNS1_19ParquetColumnSchemaEmNS_16DecodedValueKindEPNS1_23ParquetColumnStatisticsEPKN4cctz9time_zoneE
1506
1507
bool set_native_page_boolean_min_max(const tparquet::ColumnIndex& column_index,
1508
                                     const ParquetColumnSchema& column_schema, size_t page_idx,
1509
                                     ParquetColumnStatistics* page_statistics,
1510
2
                                     const cctz::time_zone* timezone) {
1511
2
    if (page_idx >= column_index.min_values.size() || page_idx >= column_index.max_values.size() ||
1512
2
        column_index.min_values[page_idx].size() != 1 ||
1513
2
        column_index.max_values[page_idx].size() != 1) {
1514
0
        return false;
1515
0
    }
1516
    // Parquet BOOLEAN statistics use the same one-bit value representation as PLAIN pages; bits
1517
    // outside the value bit are padding and must not change false into true.
1518
2
    const uint8_t min_value = static_cast<uint8_t>(column_index.min_values[page_idx][0]) & 1;
1519
2
    const uint8_t max_value = static_cast<uint8_t>(column_index.max_values[page_idx][0]) & 1;
1520
2
    if (!valid_min_max(min_value, max_value)) {
1521
0
        return true;
1522
0
    }
1523
2
    if (!set_decoded_field(column_schema, DecodedValueKind::BOOL, min_value,
1524
2
                           &page_statistics->min_value, timezone) ||
1525
2
        !set_decoded_field(column_schema, DecodedValueKind::BOOL, max_value,
1526
2
                           &page_statistics->max_value, timezone)) {
1527
0
        return false;
1528
0
    }
1529
2
    if (decoded_min_max_is_ordered(*page_statistics)) {
1530
2
        page_statistics->has_min_max = true;
1531
2
    }
1532
2
    return true;
1533
2
}
1534
1535
bool build_native_page_statistics(const tparquet::ColumnIndex& column_index,
1536
                                  const ParquetColumnSchema& column_schema, size_t page_idx,
1537
                                  int64_t page_rows, ParquetColumnStatistics* page_statistics,
1538
180
                                  const cctz::time_zone* timezone) {
1539
180
    DORIS_CHECK(page_statistics != nullptr);
1540
180
    *page_statistics = {};
1541
180
    if (!column_index.__isset.null_counts || page_idx >= column_index.null_pages.size() ||
1542
180
        page_idx >= column_index.null_counts.size()) {
1543
0
        return false;
1544
0
    }
1545
180
    const int64_t null_count = column_index.null_counts[page_idx];
1546
180
    const bool all_null = column_index.null_pages[page_idx];
1547
180
    if (page_rows < 0 || null_count < 0 || null_count > page_rows ||
1548
180
        all_null != (null_count == page_rows)) {
1549
        // The caller supplies the exact flat page or row-group span. Contradictory optional null
1550
        // metadata must disable pruning instead of turning a partial span into an all-null proof.
1551
2
        return false;
1552
2
    }
1553
178
    page_statistics->has_null_count = true;
1554
178
    page_statistics->has_null = null_count > 0;
1555
178
    page_statistics->has_not_null = !all_null;
1556
178
    if (!page_statistics->has_not_null) {
1557
0
        return true;
1558
0
    }
1559
178
    switch (column_schema.type_descriptor.physical_type) {
1560
2
    case tparquet::Type::BOOLEAN:
1561
2
        return set_native_page_boolean_min_max(column_index, column_schema, page_idx,
1562
2
                                               page_statistics, timezone);
1563
176
    case tparquet::Type::INT32:
1564
176
        return set_native_page_scalar_min_max<int32_t>(
1565
176
                column_index, column_schema, page_idx,
1566
176
                decoded_value_kind(column_schema.type_descriptor), page_statistics, timezone);
1567
0
    case tparquet::Type::INT64:
1568
0
        return set_native_page_scalar_min_max<int64_t>(
1569
0
                column_index, column_schema, page_idx,
1570
0
                decoded_value_kind(column_schema.type_descriptor), page_statistics, timezone);
1571
0
    case tparquet::Type::FLOAT:
1572
0
        return set_native_page_scalar_min_max<float>(column_index, column_schema, page_idx,
1573
0
                                                     DecodedValueKind::FLOAT, page_statistics,
1574
0
                                                     timezone);
1575
0
    case tparquet::Type::DOUBLE:
1576
0
        return set_native_page_scalar_min_max<double>(column_index, column_schema, page_idx,
1577
0
                                                      DecodedValueKind::DOUBLE, page_statistics,
1578
0
                                                      timezone);
1579
0
    case tparquet::Type::BYTE_ARRAY:
1580
0
    case tparquet::Type::FIXED_LEN_BYTE_ARRAY: {
1581
0
        if (page_idx >= column_index.min_values.size() ||
1582
0
            page_idx >= column_index.max_values.size()) {
1583
0
            return false;
1584
0
        }
1585
0
        const auto& min_value = column_index.min_values[page_idx];
1586
0
        const auto& max_value = column_index.max_values[page_idx];
1587
0
        const bool fixed =
1588
0
                column_schema.type_descriptor.physical_type == tparquet::Type::FIXED_LEN_BYTE_ARRAY;
1589
0
        if (fixed &&
1590
0
            (column_schema.type_descriptor.fixed_length <= 0 ||
1591
0
             min_value.size() != static_cast<size_t>(column_schema.type_descriptor.fixed_length) ||
1592
0
             max_value.size() != static_cast<size_t>(column_schema.type_descriptor.fixed_length))) {
1593
0
            return false;
1594
0
        }
1595
0
        const auto kind = fixed ? DecodedValueKind::FIXED_BINARY : DecodedValueKind::BINARY;
1596
0
        if (!set_decoded_binary_field(column_schema, kind,
1597
0
                                      StringRef(min_value.data(), min_value.size()),
1598
0
                                      &page_statistics->min_value, timezone) ||
1599
0
            !set_decoded_binary_field(column_schema, kind,
1600
0
                                      StringRef(max_value.data(), max_value.size()),
1601
0
                                      &page_statistics->max_value, timezone)) {
1602
0
            return false;
1603
0
        }
1604
0
        if (decoded_min_max_is_ordered(*page_statistics)) {
1605
0
            page_statistics->has_min_max = true;
1606
0
        }
1607
0
        return true;
1608
0
    }
1609
0
    default:
1610
0
        return false;
1611
178
    }
1612
178
}
1613
1614
RowRange native_page_row_range(const tparquet::OffsetIndex& offset_index, size_t page_idx,
1615
503
                               int64_t row_group_rows) {
1616
503
    const auto& locations = offset_index.page_locations;
1617
503
    const int64_t start = locations[page_idx].first_row_index;
1618
503
    const int64_t end = page_idx + 1 == locations.size() ? row_group_rows
1619
503
                                                         : locations[page_idx + 1].first_row_index;
1620
503
    return {.start = start, .length = end - start};
1621
503
}
1622
1623
} // namespace
1624
1625
Status select_row_group_ranges_by_native_page_index(
1626
        const tparquet::FileMetaData& metadata, const tparquet::RowGroup& row_group,
1627
        const std::unordered_map<int, NativeParquetPageIndex>& page_indexes,
1628
        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
1629
        const format::FileScanRequest& request, int64_t row_group_rows,
1630
        std::vector<RowRange>* selected_ranges, std::map<int, ParquetPageSkipPlan>* page_skip_plans,
1631
        ParquetPruningStats* pruning_stats, const cctz::time_zone* timezone,
1632
307
        const RuntimeState* runtime_state) {
1633
307
    int64_t filter_time_sink = 0;
1634
307
    SCOPED_RAW_TIMER(pruning_stats == nullptr ? &filter_time_sink
1635
307
                                              : &pruning_stats->page_index_filter_time);
1636
307
    DORIS_CHECK(selected_ranges != nullptr);
1637
307
    selected_ranges->clear();
1638
307
    selected_ranges->push_back({.start = 0, .length = row_group_rows});
1639
307
    if (page_skip_plans != nullptr) {
1640
307
        page_skip_plans->clear();
1641
307
    }
1642
307
    if (row_group_rows <= 0 || !config::enable_parquet_page_index ||
1643
307
        !has_expr_zonemap_filter(request, runtime_state) || page_indexes.empty()) {
1644
289
        return Status::OK();
1645
289
    }
1646
18
    if (pruning_stats != nullptr) {
1647
6
        ++pruning_stats->page_index_read_calls;
1648
6
    }
1649
1650
18
    std::map<int, VExprContextSPtrs> conjuncts_by_slot;
1651
18
    for (const auto& conjunct : request.conjuncts) {
1652
18
        const auto slot_index = expr_zonemap::single_slot_zonemap_index(conjunct);
1653
18
        if (slot_index >= 0) {
1654
10
            conjuncts_by_slot[slot_index].push_back(conjunct);
1655
10
        }
1656
18
    }
1657
18
    for (const auto& [slot_index, conjuncts] : conjuncts_by_slot) {
1658
10
        const auto file_column_id = file_column_id_by_block_position(request, slot_index);
1659
10
        if (!file_column_id.has_value()) {
1660
0
            continue;
1661
0
        }
1662
10
        const auto* column_schema = resolve_local_leaf_schema(file_schema, *file_column_id);
1663
10
        if (column_schema == nullptr || column_schema->type == nullptr ||
1664
10
            !native_metadata_predicate_is_type_safe(*column_schema) ||
1665
10
            !detail::has_supported_type_defined_order(metadata, column_schema->leaf_column_id)) {
1666
1
            continue;
1667
1
        }
1668
9
        const auto index_it = page_indexes.find(column_schema->leaf_column_id);
1669
9
        if (index_it == page_indexes.end()) {
1670
0
            continue;
1671
0
        }
1672
9
        const auto& indexes = index_it->second;
1673
9
        std::vector<RowRange> filter_ranges;
1674
9
        bool usable = true;
1675
60
        for (size_t page_idx = 0; page_idx < indexes.offset_index.page_locations.size();
1676
54
             ++page_idx) {
1677
54
            const auto page_range =
1678
54
                    native_page_row_range(indexes.offset_index, page_idx, row_group_rows);
1679
54
            ParquetColumnStatistics statistics;
1680
54
            if (!build_native_page_statistics(indexes.column_index, *column_schema, page_idx,
1681
54
                                              page_range.length, &statistics, timezone)) {
1682
3
                usable = false;
1683
3
                break;
1684
3
            }
1685
51
            ZoneMapEvalContext ctx;
1686
51
            add_slot_zonemap(&ctx, slot_index, column_schema->type,
1687
51
                             ParquetStatisticsUtils::MakeZoneMap(statistics));
1688
51
            if (VExprContext::evaluate_zonemap_filter(conjuncts, ctx) !=
1689
51
                ZoneMapFilterResult::kNoMatch) {
1690
33
                append_row_range(page_range, &filter_ranges);
1691
33
            }
1692
51
            if (pruning_stats != nullptr) {
1693
48
                pruning_stats->expr_zonemap_unusable_evals += ctx.stats.unusable_zonemap_eval_count;
1694
48
                pruning_stats->in_zonemap_point_check_count +=
1695
48
                        ctx.stats.in_zonemap_point_check_count;
1696
48
                pruning_stats->in_zonemap_range_only_count += ctx.stats.in_zonemap_range_only_count;
1697
48
            }
1698
51
        }
1699
9
        if (!usable) {
1700
3
            continue;
1701
3
        }
1702
6
        *selected_ranges = intersect_ranges(*selected_ranges, filter_ranges);
1703
6
        if (selected_ranges->empty()) {
1704
2
            if (pruning_stats != nullptr) {
1705
0
                pruning_stats->filtered_page_rows += row_group_rows;
1706
0
                ++pruning_stats->filtered_row_groups_by_page_index;
1707
0
            }
1708
2
            return Status::OK();
1709
2
        }
1710
6
    }
1711
1712
16
    for (const auto& conjunct : request.conjuncts) {
1713
16
        const auto predicate = extract_variant_shredded_predicate(conjunct);
1714
16
        if (!predicate.has_value()) {
1715
8
            continue;
1716
8
        }
1717
8
        const auto shredding = resolve_variant_shredding(file_schema, request, *predicate);
1718
8
        if (!shredding.has_value() || shredding->typed_value->leaf_column_id < 0 ||
1719
8
            !fallback_is_all_null(row_group, *shredding->fallback_value) ||
1720
8
            !variant_metadata_predicate_is_type_safe(*shredding->typed_value) ||
1721
8
            !detail::has_supported_type_defined_order(metadata,
1722
4
                                                      shredding->typed_value->leaf_column_id)) {
1723
4
            continue;
1724
4
        }
1725
4
        const auto index_it = page_indexes.find(shredding->typed_value->leaf_column_id);
1726
4
        if (index_it == page_indexes.end()) {
1727
0
            continue;
1728
0
        }
1729
4
        const auto& indexes = index_it->second;
1730
4
        std::vector<RowRange> filter_ranges;
1731
4
        bool usable = true;
1732
40
        for (size_t page_idx = 0; page_idx < indexes.offset_index.page_locations.size();
1733
36
             ++page_idx) {
1734
36
            const auto page_range =
1735
36
                    native_page_row_range(indexes.offset_index, page_idx, row_group_rows);
1736
36
            ParquetColumnStatistics statistics;
1737
36
            if (!build_native_page_statistics(indexes.column_index, *shredding->typed_value,
1738
36
                                              page_idx, page_range.length, &statistics, timezone)) {
1739
0
                usable = false;
1740
0
                break;
1741
0
            }
1742
36
            const auto normalized =
1743
36
                    normalize_variant_statistics(*predicate, *shredding->typed_value, statistics);
1744
36
            if (!normalized.has_value()) {
1745
0
                usable = false;
1746
0
                break;
1747
0
            }
1748
36
            if (!variant_statistics_exclude(*predicate, *normalized)) {
1749
12
                append_row_range(page_range, &filter_ranges);
1750
12
            }
1751
36
        }
1752
4
        if (!usable) {
1753
0
            continue;
1754
0
        }
1755
4
        *selected_ranges = intersect_ranges(*selected_ranges, filter_ranges);
1756
4
        if (selected_ranges->empty()) {
1757
0
            if (pruning_stats != nullptr) {
1758
0
                pruning_stats->filtered_page_rows += row_group_rows;
1759
0
                ++pruning_stats->filtered_row_groups_by_page_index;
1760
0
            }
1761
0
            return Status::OK();
1762
0
        }
1763
4
    }
1764
1765
16
    if (page_skip_plans != nullptr) {
1766
16
        std::vector<const ParquetColumnSchema*> leaves;
1767
16
        collect_request_leaf_schemas(file_schema, request, &leaves);
1768
46
        for (const auto* leaf : leaves) {
1769
46
            const auto index_it = page_indexes.find(leaf->leaf_column_id);
1770
46
            if (index_it == page_indexes.end() || leaf->max_repetition_level != 0) {
1771
18
                continue;
1772
18
            }
1773
28
            const auto& offset_index = index_it->second.offset_index;
1774
28
            ParquetPageSkipPlan skip_plan;
1775
28
            skip_plan.leaf_column_id = leaf->leaf_column_id;
1776
28
            skip_plan.skipped_pages.resize(offset_index.page_locations.size());
1777
28
            skip_plan.skipped_page_compressed_sizes.resize(offset_index.page_locations.size());
1778
441
            for (size_t page_idx = 0; page_idx < offset_index.page_locations.size(); ++page_idx) {
1779
413
                const auto range = native_page_row_range(offset_index, page_idx, row_group_rows);
1780
413
                if (range.length == 0 || ranges_intersect(*selected_ranges, range)) {
1781
156
                    continue;
1782
156
                }
1783
257
                skip_plan.skipped_pages[page_idx] = 1;
1784
257
                skip_plan.skipped_page_compressed_sizes[page_idx] =
1785
257
                        offset_index.page_locations[page_idx].compressed_page_size;
1786
257
                append_row_range(range, &skip_plan.skipped_ranges);
1787
257
            }
1788
28
            if (!skip_plan.empty()) {
1789
18
                page_skip_plans->emplace(skip_plan.leaf_column_id, std::move(skip_plan));
1790
18
            }
1791
28
        }
1792
16
    }
1793
16
    if (pruning_stats != nullptr) {
1794
6
        pruning_stats->filtered_page_rows += row_group_rows - count_range_rows(*selected_ranges);
1795
6
    }
1796
16
    return Status::OK();
1797
16
}
1798
1799
} // namespace doris::format::parquet