Coverage Report

Created: 2026-08-16 13:33

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