Skip to main content

mito2/sst/parquet/
prefilter.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Prefilter framework for parquet reader.
16//!
17//! Prefilter optimization reduces I/O by reading only a subset of columns first
18//! (the prefilter phase), applying filters to compute a refined row selection,
19//! then reading the remaining columns with the refined selection.
20
21use std::collections::HashSet;
22use std::ops::{BitAnd, Range};
23use std::sync::Arc;
24
25use api::v1::SemanticType;
26use common_recordbatch::filter::SimpleFilterEvaluator;
27use datafusion_common::ScalarValue;
28use datafusion_expr::Expr;
29use datatypes::arrow::array::{Array, BinaryArray, BooleanArray, BooleanBufferBuilder};
30use datatypes::arrow::buffer::BooleanBuffer;
31use datatypes::arrow::datatypes::SchemaRef;
32use datatypes::arrow::record_batch::RecordBatch;
33use datatypes::prelude::ConcreteDataType;
34use datatypes::value::Value;
35use futures::StreamExt;
36use mito_codec::row_converter::{PrimaryKeyCodec, PrimaryKeyFilter, build_primary_key_codec};
37use parquet::arrow::ProjectionMask;
38use parquet::arrow::arrow_reader::{RowSelection, RowSelector};
39use parquet::file::metadata::ParquetMetaData;
40use parquet::schema::types::SchemaDescriptor;
41use smallvec::{SmallVec, smallvec};
42use snafu::{OptionExt, ResultExt};
43use store_api::metadata::{RegionMetadata, RegionMetadataRef};
44use store_api::storage::consts::PRIMARY_KEY_COLUMN_NAME;
45use table::predicate::Predicate;
46
47use crate::cache::PrefilterKey;
48use crate::error::{
49    ComputeArrowSnafu, DecodeSnafu, EvalPartitionFilterSnafu, NewRecordBatchSnafu,
50    RecordBatchSnafu, Result, UnexpectedSnafu,
51};
52use crate::sst::parquet::file_range::PreFilterMode;
53use crate::sst::parquet::flat_format::FlatReadFormat;
54use crate::sst::parquet::format::{PrimaryKeyArray, StatValues};
55use crate::sst::parquet::reader::{
56    MaybeFilter, PhysicalFilterContext, RowGroupBuildContext, RowGroupReaderBuilder,
57    SimpleFilterContext,
58};
59
60pub(crate) fn matching_row_ranges_by_primary_key(
61    input: &RecordBatch,
62    pk_column_index: usize,
63    pk_filter: &mut dyn PrimaryKeyFilter,
64) -> Result<Vec<Range<usize>>> {
65    let pk_column = input.column(pk_column_index);
66    if let Some(pk_dict_array) = pk_column.as_any().downcast_ref::<PrimaryKeyArray>() {
67        matching_row_ranges_from_dict(pk_dict_array, input.num_rows(), pk_filter)
68    } else if let Some(pk_binary_array) = pk_column.as_any().downcast_ref::<BinaryArray>() {
69        matching_row_ranges_from_binary(pk_binary_array, input.num_rows(), pk_filter)
70    } else {
71        UnexpectedSnafu {
72            reason: format!(
73                "Primary key column is neither a dictionary nor a binary array, got {:?}",
74                pk_column.data_type()
75            ),
76        }
77        .fail()
78    }
79}
80
81/// If `pk_filter` matches `pk`, records the row range `start..end`, coalescing
82/// it with the previous range when adjacent.
83fn push_matched_range(
84    matched_row_ranges: &mut Vec<Range<usize>>,
85    pk_filter: &mut dyn PrimaryKeyFilter,
86    pk: &[u8],
87    start: usize,
88    end: usize,
89) -> Result<()> {
90    if pk_filter.matches(pk).context(DecodeSnafu)? {
91        if let Some(last) = matched_row_ranges.last_mut()
92            && last.end == start
93        {
94            last.end = end;
95        } else {
96            matched_row_ranges.push(start..end);
97        }
98    }
99    Ok(())
100}
101
102/// Computes matched row ranges from a dictionary-encoded `__primary_key` column.
103fn matching_row_ranges_from_dict(
104    pk_dict_array: &PrimaryKeyArray,
105    num_rows: usize,
106    pk_filter: &mut dyn PrimaryKeyFilter,
107) -> Result<Vec<Range<usize>>> {
108    let pk_values = pk_dict_array
109        .values()
110        .as_any()
111        .downcast_ref::<BinaryArray>()
112        .context(UnexpectedSnafu {
113            reason: "Primary key values are not binary array",
114        })?;
115    let keys = pk_dict_array.keys();
116    let key_values = keys.values();
117
118    if key_values.is_empty() {
119        return Ok(std::iter::once(0..num_rows).collect());
120    }
121
122    let mut matched_row_ranges: Vec<Range<usize>> = Vec::new();
123    let mut start = 0;
124    while start < key_values.len() {
125        let key = key_values[start];
126        let mut end = start + 1;
127        while end < key_values.len() && key_values[end] == key {
128            end += 1;
129        }
130
131        push_matched_range(
132            &mut matched_row_ranges,
133            pk_filter,
134            pk_values.value(key as usize),
135            start,
136            end,
137        )?;
138
139        start = end;
140    }
141
142    Ok(matched_row_ranges)
143}
144
145/// Computes matched row ranges from a plain binary `__primary_key` column.
146///
147/// The writer falls back to plain binary encoding when the `__primary_key`
148/// chunk exceeds the dictionary page size limit (see `should_read_pk_as_binary`
149/// in the parquet reader), so the prefilter pass must handle this form too.
150fn matching_row_ranges_from_binary(
151    pk_array: &BinaryArray,
152    num_rows: usize,
153    pk_filter: &mut dyn PrimaryKeyFilter,
154) -> Result<Vec<Range<usize>>> {
155    if pk_array.is_empty() {
156        return Ok(std::iter::once(0..num_rows).collect());
157    }
158
159    let mut matched_row_ranges: Vec<Range<usize>> = Vec::new();
160    let mut start = 0;
161    while start < pk_array.len() {
162        let value = pk_array.value(start);
163        let mut end = start + 1;
164        while end < pk_array.len() && pk_array.value(end) == value {
165            end += 1;
166        }
167
168        push_matched_range(&mut matched_row_ranges, pk_filter, value, start, end)?;
169
170        start = end;
171    }
172
173    Ok(matched_row_ranges)
174}
175
176/// Filters a flat-format record batch by primary key, returning only rows whose
177/// primary key matches the filter. Returns `None` if all rows are filtered out.
178pub(crate) fn prefilter_flat_batch_by_primary_key(
179    input: RecordBatch,
180    pk_column_index: usize,
181    pk_filter: &mut dyn PrimaryKeyFilter,
182) -> Result<Option<RecordBatch>> {
183    if input.num_rows() == 0 {
184        return Ok(Some(input));
185    }
186
187    let matched_row_ranges =
188        matching_row_ranges_by_primary_key(&input, pk_column_index, pk_filter)?;
189    if matched_row_ranges.is_empty() {
190        return Ok(None);
191    }
192
193    if matched_row_ranges.len() == 1
194        && matched_row_ranges[0].start == 0
195        && matched_row_ranges[0].end == input.num_rows()
196    {
197        return Ok(Some(input));
198    }
199
200    if matched_row_ranges.len() == 1 {
201        let span = &matched_row_ranges[0];
202        return Ok(Some(input.slice(span.start, span.end - span.start)));
203    }
204
205    let mut builder = BooleanBufferBuilder::new(input.num_rows());
206    builder.append_n(input.num_rows(), false);
207    for span in matched_row_ranges {
208        for i in span {
209            builder.set_bit(i, true);
210        }
211    }
212
213    let filtered = datatypes::arrow::compute::filter_record_batch(
214        &input,
215        &BooleanArray::new(builder.finish(), None),
216    )
217    .context(ComputeArrowSnafu)?;
218    if filtered.num_rows() == 0 {
219        Ok(None)
220    } else {
221        Ok(Some(filtered))
222    }
223}
224
225pub(crate) struct CachedPrimaryKeyFilter {
226    inner: Box<dyn PrimaryKeyFilter>,
227    last_primary_key: Vec<u8>,
228    last_match: Option<bool>,
229}
230
231impl CachedPrimaryKeyFilter {
232    pub(crate) fn new(inner: Box<dyn PrimaryKeyFilter>) -> Self {
233        Self {
234            inner,
235            last_primary_key: Vec::new(),
236            last_match: None,
237        }
238    }
239}
240
241impl PrimaryKeyFilter for CachedPrimaryKeyFilter {
242    fn matches(&mut self, pk: &[u8]) -> mito_codec::error::Result<bool> {
243        if let Some(last_match) = self.last_match
244            && self.last_primary_key == pk
245        {
246            return Ok(last_match);
247        }
248
249        let matched = self.inner.matches(pk)?;
250        self.last_primary_key.clear();
251        self.last_primary_key.extend_from_slice(pk);
252        self.last_match = Some(matched);
253        Ok(matched)
254    }
255}
256
257/// How the bulk-memtable read should apply each predicate.
258///
259/// Unlike the parquet reader, the bulk path has no prefilter pass; predicates
260/// either run row-wise inside the iterator or are pushed down to encoded-PK
261/// matching when the batch still carries the primary-key column.
262pub(crate) struct BulkFilterPlan {
263    /// Simple filters the iterator still has to evaluate row-wise on each batch.
264    pub(crate) remaining_simple_filters: Vec<SimpleFilterContext>,
265    /// Tag predicates lowered to encoded-PK filters. `None` when the batch
266    /// already exposes raw tag columns or there are no tag predicates.
267    pub(crate) pk_filters: Option<Arc<Vec<SimpleFilterEvaluator>>>,
268}
269
270/// Builds an encoded-primary-key filter from the supported tag predicates.
271///
272/// Predicates on fields, timestamps, or unsupported expression shapes are intentionally
273/// omitted. Callers use this as a pruning filter and must preserve the full predicate for
274/// authoritative filtering later in the scan.
275pub(crate) fn build_primary_key_filter(
276    sst_metadata: &RegionMetadataRef,
277    expected_metadata: Option<&RegionMetadata>,
278    predicate: Option<&Predicate>,
279) -> Option<CachedPrimaryKeyFilter> {
280    let filters = simple_tag_filters(sst_metadata, expected_metadata, predicate)
281        .into_iter()
282        .map(|(_, filter)| filter)
283        .collect::<Vec<_>>();
284    if filters.is_empty() {
285        return None;
286    }
287
288    let codec = build_primary_key_codec(sst_metadata.as_ref());
289    let filter = codec.primary_key_filter(sst_metadata, Arc::new(filters));
290    Some(CachedPrimaryKeyFilter::new(filter))
291}
292
293/// Extracts simple tag filters that can be applied to encoded primary keys or series indexes.
294pub(crate) fn simple_tag_filters(
295    sst_metadata: &RegionMetadataRef,
296    expected_metadata: Option<&RegionMetadata>,
297    predicate: Option<&Predicate>,
298) -> Vec<(Expr, SimpleFilterEvaluator)> {
299    predicate
300        .into_iter()
301        .flat_map(|predicate| predicate.exprs())
302        .filter_map(|expr| {
303            SimpleFilterContext::new_opt(sst_metadata, expected_metadata, expr)
304                .map(|filter_ctx| (expr, filter_ctx))
305        })
306        .filter_map(|(expr, filter_ctx)| {
307            (filter_ctx.semantic_type() == SemanticType::Tag)
308                .then(|| {
309                    filter_ctx
310                        .filter()
311                        .as_filter()
312                        .cloned()
313                        .map(|filter| (expr.clone(), filter))
314                })
315                .flatten()
316        })
317        .collect()
318}
319
320/// How the parquet reader should apply each predicate.
321///
322/// The reader runs in two phases. Predicates routed into `prefilter_builder`
323/// execute on a reduced column set first to compute a refined row selection;
324/// `remaining_simple_filters` execute alongside the full projection on the
325/// normal read path. The contract for what is precise vs best-effort is
326/// documented on [`build_reader_filter_plan`].
327pub(crate) struct ReaderFilterPlan {
328    /// Simple filters that must run on the normal read path: predicates with
329    /// `Matched` / `Pruned` outcomes (which carry expected-metadata
330    /// compatibility decisions later phases rely on), and predicates whose
331    /// column cannot be read directly during the prefilter pass.
332    pub(crate) remaining_simple_filters: Vec<SimpleFilterContext>,
333    /// Pre-built state for the prefilter pass, or `None` when prefiltering is
334    /// not worthwhile (no prefilter columns selected, or the prefilter
335    /// projection would cover nearly the full read).
336    pub(crate) prefilter_builder: Option<PrefilterContextBuilder>,
337}
338
339pub(crate) fn build_bulk_filter_plan(
340    read_format: &FlatReadFormat,
341    predicate: Option<&Predicate>,
342) -> BulkFilterPlan {
343    let metadata = read_format.metadata();
344    // Filters not recognized by SimpleFilterContext stay out of this fast path.
345    let simple_filters: Vec<SimpleFilterContext> = predicate
346        .into_iter()
347        .flat_map(|predicate| {
348            predicate
349                .exprs()
350                .iter()
351                .filter_map(|expr| SimpleFilterContext::new_opt(metadata, None, expr))
352        })
353        .collect();
354
355    // PK prefilter only works when flat batches still carry the encoded PK
356    // column. If tags have already been expanded to raw columns, the iterator
357    // can apply those filters directly and there is nothing to extract here.
358    if read_format.batch_has_raw_pk_columns() || metadata.primary_key.is_empty() {
359        return BulkFilterPlan {
360            remaining_simple_filters: simple_filters,
361            pk_filters: None,
362        };
363    }
364
365    let mut remaining_simple_filters = Vec::new();
366    let mut pk_filters = Vec::new();
367
368    for filter_ctx in simple_filters {
369        // Split tag predicates that can be evaluated against the encoded PK
370        // from filters that still need normal row-wise evaluation later.
371        let pk_filter = filter_ctx.filter().as_filter().and_then(|filter| {
372            (filter_ctx.semantic_type() == SemanticType::Tag).then(|| filter.clone())
373        });
374
375        if let Some(pk_filter) = pk_filter {
376            pk_filters.push(pk_filter);
377        } else {
378            remaining_simple_filters.push(filter_ctx);
379        }
380    }
381
382    BulkFilterPlan {
383        remaining_simple_filters,
384        pk_filters: (!pk_filters.is_empty()).then_some(Arc::new(pk_filters)),
385    }
386}
387
388/// Splits a query [`Predicate`] into a [`ReaderFilterPlan`]: predicates that can run
389/// during the prefilter pass (on a reduced projection, to compute a refined row
390/// selection) versus predicates that must run on the normal read path (alongside the
391/// full projection).
392///
393/// The prefilter pass is *best-effort pruning*: a physical-filter predicate is silently
394/// dropped when [`PhysicalFilterContext::new_opt`] returns `None` (column not in the
395/// projected arrow schema). This is safe because the DataFusion `FilterExec` above the
396/// reader always re-applies the original predicate, so the prefilter pass is purely a
397/// pruning hint.
398///
399/// With predicate prefiltering enabled, tag and timestamp predicates that lower to
400/// [`SimpleFilterEvaluator`] are an exception — the engine enforces them precisely in
401/// the prefilter pass. A caller can postpone simple timestamp filters to the normal
402/// precise-filter path when the scan time range covers the SST. When predicate
403/// prefiltering is disabled, all simple filters remain on the normal path instead.
404#[allow(clippy::too_many_arguments)]
405pub(crate) fn build_reader_filter_plan(
406    predicate: Option<&Predicate>,
407    expected_metadata: Option<&RegionMetadata>,
408    pre_filter_mode: PreFilterMode,
409    enable_predicate_prefilter: bool,
410    postpone_time_index_filter: bool,
411    read_format: &FlatReadFormat,
412    codec: &Arc<dyn PrimaryKeyCodec>,
413    parquet_metadata: &ParquetMetaData,
414) -> ReaderFilterPlan {
415    let Some(predicate) = predicate else {
416        return ReaderFilterPlan {
417            remaining_simple_filters: Vec::new(),
418            prefilter_builder: None,
419        };
420    };
421
422    let metadata = read_format.metadata();
423    let mut prefilter_simple_filters = Vec::new();
424    let mut remaining_simple_filters = Vec::new();
425    let mut prefilter_physical_filters = Vec::new();
426    let mut primary_key_filters = Vec::new();
427    let mut pk_filter_contexts = Vec::new();
428
429    // `SkipFields` keeps field predicates in the normal read path to avoid a
430    // second read of projected field columns, while tags/timestamp can still
431    // participate in prefiltering.
432    let field_prefilter_enabled = pre_filter_mode == PreFilterMode::All;
433    // When true, tag columns are encoded in the primary key column and are NOT
434    // stored as separate parquet columns. Tag predicates must go through PK
435    // decoding rather than direct column reads.
436    let need_pk_prefilter = !read_format.batch_has_raw_pk_columns();
437
438    // Whether a column can be read directly from parquet for prefiltering,
439    // based on its semantic type and the current mode/format.
440    let can_direct_prefilter = |semantic_type: SemanticType| -> bool {
441        match semantic_type {
442            SemanticType::Tag => !need_pk_prefilter,
443            SemanticType::Field => field_prefilter_enabled,
444            SemanticType::Timestamp => true,
445        }
446    };
447
448    for expr in predicate.exprs() {
449        // Prefer cheap simple filters first. They also preserve `Matched` /
450        // `Pruned` states for columns that only exist in expected metadata.
451        if let Some(filter_ctx) = SimpleFilterContext::new_opt(metadata, expected_metadata, expr) {
452            if !enable_predicate_prefilter {
453                remaining_simple_filters.push(filter_ctx);
454                continue;
455            }
456
457            // `Matched` and `Pruned` come from expected-metadata compatibility
458            // and must stay in the main filter list so later phases keep that
459            // outcome.
460            let Some(filter) = filter_ctx.filter().as_filter() else {
461                remaining_simple_filters.push(filter_ctx);
462                continue;
463            };
464
465            if postpone_time_index_filter && filter_ctx.semantic_type() == SemanticType::Timestamp {
466                remaining_simple_filters.push(filter_ctx);
467                continue;
468            }
469
470            // If the column is stored as a separate parquet column and is already projected in the main read,
471            // we can evaluate the simple filter directly during prefilter.
472            let direct_prefilter = can_direct_prefilter(filter_ctx.semantic_type());
473            if direct_prefilter {
474                assert!(
475                    read_format
476                        .arrow_schema()
477                        .column_with_name(filter.column_name())
478                        .is_some(),
479                    "Column '{}' is not present in the arrow schema {:?}",
480                    filter.column_name(),
481                    read_format.arrow_schema(),
482                );
483                prefilter_simple_filters.push(filter_ctx);
484                continue;
485            }
486
487            // Otherwise try to filter through encoded-PK matching.
488            if need_pk_prefilter && filter_ctx.semantic_type() == SemanticType::Tag {
489                primary_key_filters.push(filter.clone());
490                pk_filter_contexts.push(filter_ctx);
491            } else {
492                remaining_simple_filters.push(filter_ctx);
493            }
494            continue;
495        }
496
497        if !enable_predicate_prefilter {
498            continue;
499        }
500
501        // Best-effort physical-filter prefilter (see fn-level doc): `new_opt`
502        // returning `None` means the column is not in the projected arrow
503        // schema, and dropping the predicate is safe because the upper
504        // `FilterExec` re-applies it.
505        if let Some(filter) =
506            PhysicalFilterContext::new_opt(metadata, expected_metadata, read_format, expr)
507            && can_direct_prefilter(filter.semantic_type())
508        {
509            prefilter_physical_filters.push(filter);
510        }
511    }
512
513    if !enable_predicate_prefilter {
514        return ReaderFilterPlan {
515            remaining_simple_filters,
516            prefilter_builder: None,
517        };
518    }
519
520    let pk_filter_expr_strs = (!pk_filter_contexts.is_empty()).then(|| {
521        let mut expr_strs = pk_filter_contexts
522            .iter()
523            .map(|filter_ctx| filter_ctx.expr_str().to_string())
524            .collect::<Vec<_>>();
525        expr_strs.sort();
526        SmallVec::from_vec(expr_strs)
527    });
528    let pk_filter_exprs =
529        (!primary_key_filters.is_empty()).then_some(Arc::new(primary_key_filters));
530    let schema_version = expected_metadata
531        .map(|metadata| metadata.schema_version)
532        .unwrap_or_else(|| read_format.metadata().schema_version);
533    let prefilter_builder = PrefilterContextBuilder::new(
534        read_format,
535        codec,
536        pk_filter_exprs,
537        pk_filter_expr_strs,
538        prefilter_simple_filters.clone(),
539        prefilter_physical_filters,
540        schema_version,
541        parquet_metadata,
542    );
543
544    if prefilter_builder.is_some() {
545        ReaderFilterPlan {
546            remaining_simple_filters,
547            prefilter_builder,
548        }
549    } else {
550        // If prefilter setup is not worthwhile, keep the original simple
551        // filters on the normal path so behavior is unchanged.
552        remaining_simple_filters.extend(prefilter_simple_filters);
553        remaining_simple_filters.extend(pk_filter_contexts);
554        ReaderFilterPlan {
555            remaining_simple_filters,
556            prefilter_builder: None,
557        }
558    }
559}
560
561/// Context for prefiltering a row group.
562pub(crate) struct PrefilterContext {
563    /// Optional PK filter for legacy primary-key-format parquet.
564    pk_filter: Option<Box<dyn PrimaryKeyFilter>>,
565    /// Simple filters that can be evaluated directly from the prefilter batch.
566    filters: Vec<SimpleFilterContext>,
567    /// Physical filters that can be evaluated directly from the prefilter batch.
568    /// Physical expressions are only applied in the prefilter phase.
569    physical_filters: Vec<PhysicalFilterContext>,
570    /// Region schema version used in per-filter cache keys.
571    schema_version: u64,
572    /// Sorted expression strings for the encoded-PK filter group.
573    pk_filter_expr_strs: Option<SmallVec<[String; 1]>>,
574    /// Arrow schema used to build narrowed prefilter projections.
575    arrow_schema: SchemaRef,
576    /// Simple filters already proven SQL-true by this row group's statistics.
577    proven_simple_filters: Vec<bool>,
578}
579
580/// Pre-built state for constructing [PrefilterContext] per row group.
581///
582/// Fields invariant across row groups (projection mask, codec, metadata, filters)
583/// are computed once. A fresh [PrefilterContext] with its own mutable PK filter
584/// is created via [PrefilterContextBuilder::build()] for each row group.
585pub(crate) struct PrefilterContextBuilder {
586    pk_filters: Option<Arc<Vec<SimpleFilterEvaluator>>>,
587    pk_filter_expr_strs: Option<SmallVec<[String; 1]>>,
588    filters: Vec<SimpleFilterContext>,
589    physical_filters: Vec<PhysicalFilterContext>,
590    codec: Arc<dyn PrimaryKeyCodec>,
591    metadata: RegionMetadataRef,
592    schema_version: u64,
593    arrow_schema: SchemaRef,
594    /// Per-row-group simple filters already proven SQL-true by column statistics.
595    proven_simple_filters: Vec<Vec<bool>>,
596}
597
598impl PrefilterContextBuilder {
599    /// Creates a builder if prefiltering is applicable.
600    ///
601    /// Returns `None` if:
602    /// - The read format doesn't use flat layout
603    /// - No prefilter columns are selected
604    /// - Prefilter would read the full projection without any PK filter
605    #[allow(clippy::too_many_arguments)]
606    pub(crate) fn new(
607        read_format: &FlatReadFormat,
608        codec: &Arc<dyn PrimaryKeyCodec>,
609        primary_key_filters: Option<Arc<Vec<SimpleFilterEvaluator>>>,
610        primary_key_filter_expr_strs: Option<SmallVec<[String; 1]>>,
611        filters: Vec<SimpleFilterContext>,
612        physical_filters: Vec<PhysicalFilterContext>,
613        schema_version: u64,
614        parquet_metadata: &ParquetMetaData,
615    ) -> Option<Self> {
616        let metadata = read_format.metadata();
617        let use_raw_tag_columns = read_format.batch_has_raw_pk_columns();
618        let pk_filters = (!use_raw_tag_columns)
619            .then_some(primary_key_filters)
620            .flatten()
621            .filter(|filters| !filters.is_empty());
622        let pk_filter_expr_strs = pk_filters
623            .is_some()
624            .then_some(primary_key_filter_expr_strs)
625            .flatten();
626
627        let mut prefilter_column_names = HashSet::new();
628        for filter_ctx in &filters {
629            if let MaybeFilter::Filter(filter) = filter_ctx.filter() {
630                prefilter_column_names.insert(filter.column_name().to_string());
631            }
632        }
633
634        if pk_filters.is_some() {
635            prefilter_column_names.insert(PRIMARY_KEY_COLUMN_NAME.to_string());
636        }
637
638        for filter_ctx in &physical_filters {
639            prefilter_column_names.insert(filter_ctx.column_name().to_string());
640        }
641
642        let prefilter_count =
643            compute_projection_count(&prefilter_column_names, read_format.arrow_schema());
644
645        if prefilter_count == 0 {
646            return None;
647        }
648
649        let total_count = read_format.parquet_read_columns().root_indices().len();
650        let remaining_count = total_count.saturating_sub(prefilter_count);
651        if pk_filters.is_none() && prefilter_count >= total_count {
652            return None;
653        }
654
655        if pk_filters.is_none()
656            && !should_use_prefilter(prefilter_count, remaining_count, total_count)
657        {
658            return None;
659        }
660
661        let proven_simple_filters =
662            simple_filter_stats_proofs(read_format, parquet_metadata.row_groups(), &filters);
663
664        Some(Self {
665            pk_filters,
666            pk_filter_expr_strs,
667            filters,
668            physical_filters,
669            codec: Arc::clone(codec),
670            metadata: metadata.clone(),
671            schema_version,
672            arrow_schema: read_format.arrow_schema().clone(),
673            proven_simple_filters,
674        })
675    }
676
677    /// Builds a [PrefilterContext] for a specific row group.
678    pub(crate) fn build(&self, row_group_idx: usize) -> PrefilterContext {
679        let pk_filter = self
680            .build_primary_key_filter()
681            .map(|filter| Box::new(filter) as Box<dyn PrimaryKeyFilter>);
682        PrefilterContext {
683            pk_filter,
684            filters: self.filters.clone(),
685            physical_filters: self.physical_filters.clone(),
686            schema_version: self.schema_version,
687            pk_filter_expr_strs: self.pk_filter_expr_strs.clone(),
688            arrow_schema: self.arrow_schema.clone(),
689            proven_simple_filters: self
690                .proven_simple_filters
691                .get(row_group_idx)
692                .cloned()
693                .unwrap_or_else(|| vec![false; self.filters.len()]),
694        }
695    }
696
697    /// Builds a fresh encoded-primary-key filter selected by this plan.
698    pub(crate) fn build_primary_key_filter(&self) -> Option<CachedPrimaryKeyFilter> {
699        self.pk_filters.as_ref().map(|pk_filters| {
700            let filter = self
701                .codec
702                .primary_key_filter(&self.metadata, Arc::clone(pk_filters));
703            CachedPrimaryKeyFilter::new(filter)
704        })
705    }
706}
707
708const PREFILTER_COLUMN_RATIO_THRESHOLD: f64 = 0.5;
709const PREFILTER_MIN_REMAINING_COLUMNS: usize = 2;
710
711/// Returns row-group-major proof bits for simple filters. Statistics are
712/// extracted once per eligible filter across all row groups.
713fn simple_filter_stats_proofs(
714    read_format: &FlatReadFormat,
715    row_groups: &[parquet::file::metadata::RowGroupMetaData],
716    filters: &[SimpleFilterContext],
717) -> Vec<Vec<bool>> {
718    let mut proofs = vec![vec![false; filters.len()]; row_groups.len()];
719    for (filter_idx, filter_ctx) in filters.iter().enumerate() {
720        let Some((filter, literal)) = eligible_simple_filter(read_format, filter_ctx) else {
721            continue;
722        };
723        let (StatValues::Values(mins), StatValues::Values(maxs), StatValues::Values(null_counts)) = (
724            read_format.min_values(row_groups, filter_ctx.column_id()),
725            read_format.max_values(row_groups, filter_ctx.column_id()),
726            read_format.null_counts(row_groups, filter_ctx.column_id()),
727        ) else {
728            continue;
729        };
730        for (row_group_idx, proof) in proofs.iter_mut().enumerate() {
731            proof[filter_idx] = simple_filter_is_true_by_values(
732                filter,
733                &literal,
734                stat_value_at(&mins, row_group_idx),
735                stat_value_at(&maxs, row_group_idx),
736                stat_value_at(&null_counts, row_group_idx),
737            );
738        }
739    }
740    proofs
741}
742
743fn eligible_simple_filter<'a>(
744    read_format: &FlatReadFormat,
745    filter_ctx: &'a SimpleFilterContext,
746) -> Option<(&'a SimpleFilterEvaluator, Value)> {
747    if filter_ctx.semantic_type() != SemanticType::Field {
748        return None;
749    }
750    let filter = filter_ctx.filter().as_filter()?;
751    let literal = filter.literal_value()?;
752    let column = read_format
753        .metadata()
754        .column_by_id(filter_ctx.column_id())?;
755    column_type_matches_literal(&column.column_schema.data_type, &literal)
756        .then_some((filter, literal))
757}
758
759fn simple_filter_is_true_by_values(
760    filter: &SimpleFilterEvaluator,
761    literal: &Value,
762    min: Option<Value>,
763    max: Option<Value>,
764    null_count: Option<Value>,
765) -> bool {
766    let (Some(min), Some(max), Some(null_count)) = (min, max, null_count) else {
767        return false;
768    };
769    if null_count != Value::UInt64(0)
770        || !same_supported_value_type(&min, literal)
771        || !same_supported_value_type(&max, literal)
772        || min > max
773    {
774        return false;
775    }
776
777    if filter.is_gt() {
778        min > *literal
779    } else if filter.is_gt_eq() {
780        min >= *literal
781    } else if filter.is_lt() {
782        max < *literal
783    } else if filter.is_lt_eq() {
784        max <= *literal
785    } else if filter.is_eq() {
786        min == *literal && max == *literal
787    } else if filter.is_not_eq() {
788        max < *literal || min > *literal
789    } else {
790        false
791    }
792}
793
794fn column_type_matches_literal(data_type: &ConcreteDataType, literal: &Value) -> bool {
795    matches!(
796        (data_type, literal),
797        (ConcreteDataType::Int32(_), Value::Int32(_))
798            | (ConcreteDataType::UInt32(_), Value::UInt32(_))
799            | (ConcreteDataType::Int64(_), Value::Int64(_))
800            | (ConcreteDataType::UInt64(_), Value::UInt64(_))
801    )
802}
803
804fn same_supported_value_type(left: &Value, right: &Value) -> bool {
805    matches!(
806        (left, right),
807        (Value::Int32(_), Value::Int32(_))
808            | (Value::UInt32(_), Value::UInt32(_))
809            | (Value::Int64(_), Value::Int64(_))
810            | (Value::UInt64(_), Value::UInt64(_))
811    )
812}
813
814fn stat_value_at(values: &datatypes::arrow::array::ArrayRef, index: usize) -> Option<Value> {
815    let scalar = ScalarValue::try_from_array(values, index).ok()?;
816    Value::try_from(scalar).ok()
817}
818
819/// Result of prefiltering a row group.
820pub(crate) struct PrefilterResult {
821    /// Refined row selection after prefiltering.
822    pub(crate) refined_selection: RowSelection,
823    /// Number of rows filtered out by prefiltering.
824    pub(crate) filtered_rows: usize,
825}
826
827/// Executes prefiltering on a row group.
828///
829/// Reads only the prefilter columns (currently the PK dictionary column),
830/// applies filters, and returns a refined [RowSelection].
831fn compute_projection_mask(
832    column_names: &HashSet<String>,
833    arrow_schema: &datatypes::arrow::datatypes::SchemaRef,
834    parquet_schema: &SchemaDescriptor,
835) -> ProjectionMask {
836    ProjectionMask::roots(
837        parquet_schema,
838        projection_indices(column_names, arrow_schema),
839    )
840}
841
842fn compute_projection_count(
843    column_names: &HashSet<String>,
844    arrow_schema: &datatypes::arrow::datatypes::SchemaRef,
845) -> usize {
846    projection_indices(column_names, arrow_schema).len()
847}
848
849fn projection_indices(
850    column_names: &HashSet<String>,
851    arrow_schema: &datatypes::arrow::datatypes::SchemaRef,
852) -> Vec<usize> {
853    let mut projection_indices: Vec<usize> = column_names
854        .iter()
855        .filter_map(|name| arrow_schema.column_with_name(name).map(|(index, _)| index))
856        .collect();
857    projection_indices.sort_unstable();
858    projection_indices.dedup();
859    projection_indices
860}
861
862fn should_use_prefilter(
863    prefilter_count: usize,
864    remaining_count: usize,
865    total_count: usize,
866) -> bool {
867    if remaining_count == 0 {
868        return false;
869    }
870
871    if remaining_count < PREFILTER_MIN_REMAINING_COLUMNS {
872        return false;
873    }
874
875    let ratio = prefilter_count as f64 / total_count as f64;
876    ratio <= PREFILTER_COLUMN_RATIO_THRESHOLD
877}
878
879pub(crate) async fn execute_prefilter(
880    prefilter_ctx: &mut PrefilterContext,
881    reader_builder: &RowGroupReaderBuilder,
882    build_ctx: &RowGroupBuildContext<'_>,
883) -> Result<PrefilterResult> {
884    let entries = build_prefilter_cache_entries(prefilter_ctx, reader_builder, build_ctx);
885
886    if entries.is_empty() {
887        return execute_prefilter_by_reading_columns(prefilter_ctx, reader_builder, build_ctx)
888            .await;
889    }
890
891    execute_prefilter_with_result_cache(prefilter_ctx, reader_builder, build_ctx, entries).await
892}
893
894async fn execute_prefilter_with_result_cache(
895    prefilter_ctx: &mut PrefilterContext,
896    reader_builder: &RowGroupReaderBuilder,
897    build_ctx: &RowGroupBuildContext<'_>,
898    entries: Vec<PrefilterEntry>,
899) -> Result<PrefilterResult> {
900    let non_cacheable_physical = non_cacheable_physical_filters(prefilter_ctx);
901    let mut hit_mask: Option<BooleanBuffer> = None;
902    let mut misses = Vec::new();
903    for entry in entries {
904        let Some(key) = &entry.key else {
905            misses.push(entry);
906            continue;
907        };
908
909        if let Some(mask) = reader_builder.cache_strategy().get_prefilter_result(key) {
910            hit_mask = Some(match hit_mask {
911                Some(hit_mask) => hit_mask.bitand(mask.as_ref()),
912                None => mask.as_ref().clone(),
913            });
914        } else {
915            misses.push(entry);
916        }
917    }
918
919    if misses.is_empty() && non_cacheable_physical.is_empty() {
920        let combined_mask = hit_mask.unwrap_or_else(|| BooleanBuffer::new_set(0));
921        let refined_selection =
922            refined_selection_from_mask(&combined_mask, &build_ctx.row_selection);
923        let rows_before_filter = rows_before_filter(reader_builder, build_ctx);
924        let filtered_rows = rows_before_filter.saturating_sub(refined_selection.row_count());
925        return Ok(PrefilterResult {
926            refined_selection,
927            filtered_rows,
928        });
929    }
930
931    let mut uncached_entries = misses;
932    uncached_entries.extend(
933        non_cacheable_physical
934            .iter()
935            .copied()
936            .map(|idx| PrefilterEntry::without_cache(PrefilterEntryKind::Physical(idx))),
937    );
938    let (uncached_mask, read_rows) =
939        build_prefilter_masks(prefilter_ctx, reader_builder, build_ctx, &uncached_entries).await?;
940
941    let final_mask = match (hit_mask, uncached_mask) {
942        (Some(hit_mask), Some(uncached_mask)) => hit_mask.bitand(&uncached_mask),
943        (Some(hit_mask), None) => hit_mask,
944        (None, Some(uncached_mask)) => uncached_mask,
945        (None, None) => BooleanBuffer::new_set(read_rows),
946    };
947    debug_assert_eq!(final_mask.len(), read_rows);
948    let rows_selected = final_mask.count_set_bits();
949    let filtered_rows = read_rows.saturating_sub(rows_selected);
950    let refined_selection = refined_selection_from_mask(&final_mask, &build_ctx.row_selection);
951
952    Ok(PrefilterResult {
953        refined_selection,
954        filtered_rows,
955    })
956}
957
958fn non_cacheable_physical_filters(prefilter_ctx: &PrefilterContext) -> Vec<usize> {
959    prefilter_ctx
960        .physical_filters
961        .iter()
962        .enumerate()
963        .filter_map(|(idx, filter)| (!filter.is_immutable()).then_some(idx))
964        .collect()
965}
966
967async fn build_prefilter_masks(
968    prefilter_ctx: &mut PrefilterContext,
969    reader_builder: &RowGroupReaderBuilder,
970    build_ctx: &RowGroupBuildContext<'_>,
971    entries: &[PrefilterEntry],
972) -> Result<(Option<BooleanBuffer>, usize)> {
973    let prefilter_column_names = prefilter_column_names_for_entries(prefilter_ctx, entries);
974    let parquet_schema = reader_builder
975        .parquet_metadata()
976        .file_metadata()
977        .schema_descr();
978    let projection = compute_projection_mask(
979        &prefilter_column_names,
980        &prefilter_ctx.arrow_schema,
981        parquet_schema,
982    );
983
984    let mut stream = reader_builder
985        .build_with_projection(
986            build_ctx.row_group_idx,
987            build_ctx.row_selection.clone(),
988            projection,
989            build_ctx.fetch_metrics,
990        )
991        .await?;
992
993    let mut cache_builders = entries
994        .iter()
995        .map(|entry| entry.key.is_some().then(|| BooleanBufferBuilder::new(0)))
996        .collect::<Vec<_>>();
997    let mut combined_builder = (!entries.is_empty()).then(|| BooleanBufferBuilder::new(0));
998    let mut rows_before_filter = 0usize;
999
1000    while let Some(batch_result) = stream.next().await {
1001        let batch = batch_result?;
1002        let num_rows = batch.num_rows();
1003        if num_rows == 0 {
1004            continue;
1005        }
1006        rows_before_filter += num_rows;
1007
1008        let mut batch_mask = BooleanBuffer::new_set(num_rows);
1009        for (idx, entry) in entries.iter().enumerate() {
1010            let mask = eval_entry_mask(
1011                &batch,
1012                prefilter_ctx,
1013                entry.kind,
1014                reader_builder.file_path(),
1015            )?;
1016            batch_mask = batch_mask.bitand(&mask);
1017            if let Some(Some(builder)) = cache_builders.get_mut(idx) {
1018                builder.append_buffer(&mask);
1019            }
1020        }
1021        if let Some(builder) = &mut combined_builder {
1022            builder.append_buffer(&batch_mask);
1023        }
1024    }
1025
1026    for (entry, builder) in entries.iter().zip(cache_builders) {
1027        if let (Some(key), Some(mut builder)) = (&entry.key, builder) {
1028            reader_builder
1029                .cache_strategy()
1030                .put_prefilter_result(key.clone(), Arc::new(builder.finish()));
1031        }
1032    }
1033
1034    Ok((
1035        combined_builder.map(|mut builder| builder.finish()),
1036        rows_before_filter,
1037    ))
1038}
1039
1040fn prefilter_column_names_for_entries(
1041    prefilter_ctx: &PrefilterContext,
1042    entries: &[PrefilterEntry],
1043) -> HashSet<String> {
1044    let mut prefilter_column_names = HashSet::new();
1045    for entry in entries {
1046        match entry.kind {
1047            PrefilterEntryKind::Simple(idx) => {
1048                if let MaybeFilter::Filter(filter) = prefilter_ctx.filters[idx].filter() {
1049                    prefilter_column_names.insert(filter.column_name().to_string());
1050                }
1051            }
1052            PrefilterEntryKind::Physical(idx) => {
1053                prefilter_column_names.insert(
1054                    prefilter_ctx.physical_filters[idx]
1055                        .column_name()
1056                        .to_string(),
1057                );
1058            }
1059            PrefilterEntryKind::PkGroup => {
1060                prefilter_column_names.insert(PRIMARY_KEY_COLUMN_NAME.to_string());
1061            }
1062        }
1063    }
1064    prefilter_column_names
1065}
1066
1067async fn execute_prefilter_by_reading_columns(
1068    prefilter_ctx: &mut PrefilterContext,
1069    reader_builder: &RowGroupReaderBuilder,
1070    build_ctx: &RowGroupBuildContext<'_>,
1071) -> Result<PrefilterResult> {
1072    let entries = all_prefilter_entries(prefilter_ctx);
1073    if entries.is_empty() {
1074        return Ok(identity_prefilter_result(reader_builder, build_ctx));
1075    }
1076    let (mask, rows_before_filter) =
1077        build_prefilter_masks(prefilter_ctx, reader_builder, build_ctx, &entries).await?;
1078
1079    let final_mask = mask.unwrap_or_else(|| BooleanBuffer::new_set(rows_before_filter));
1080    let rows_selected = final_mask.count_set_bits();
1081    let filtered_rows = rows_before_filter.saturating_sub(rows_selected);
1082    let refined_selection = refined_selection_from_mask(&final_mask, &build_ctx.row_selection);
1083
1084    Ok(PrefilterResult {
1085        refined_selection,
1086        filtered_rows,
1087    })
1088}
1089
1090fn all_prefilter_entries(prefilter_ctx: &PrefilterContext) -> Vec<PrefilterEntry> {
1091    let mut entries = Vec::new();
1092    if prefilter_ctx.pk_filter.is_some() {
1093        entries.push(PrefilterEntry::without_cache(PrefilterEntryKind::PkGroup));
1094    }
1095    entries.extend(
1096        prefilter_ctx
1097            .filters
1098            .iter()
1099            .enumerate()
1100            .filter(|(idx, _)| {
1101                !prefilter_ctx
1102                    .proven_simple_filters
1103                    .get(*idx)
1104                    .copied()
1105                    .unwrap_or(false)
1106            })
1107            .map(|(idx, _)| PrefilterEntry::without_cache(PrefilterEntryKind::Simple(idx))),
1108    );
1109    entries.extend(
1110        prefilter_ctx
1111            .physical_filters
1112            .iter()
1113            .enumerate()
1114            .map(|(idx, _)| PrefilterEntry::without_cache(PrefilterEntryKind::Physical(idx))),
1115    );
1116    entries
1117}
1118
1119#[derive(Clone, Copy)]
1120enum PrefilterEntryKind {
1121    Simple(usize),
1122    Physical(usize),
1123    PkGroup,
1124}
1125
1126struct PrefilterEntry {
1127    kind: PrefilterEntryKind,
1128    key: Option<PrefilterKey>,
1129}
1130
1131impl PrefilterEntry {
1132    fn without_cache(kind: PrefilterEntryKind) -> Self {
1133        Self { kind, key: None }
1134    }
1135}
1136
1137fn build_prefilter_cache_entries(
1138    prefilter_ctx: &PrefilterContext,
1139    reader_builder: &RowGroupReaderBuilder,
1140    build_ctx: &RowGroupBuildContext<'_>,
1141) -> Vec<PrefilterEntry> {
1142    let row_selection = PrefilterKey::row_selection_snapshot(build_ctx.row_selection.as_ref());
1143    let file_id = reader_builder.file_handle().file_id().file_id();
1144    let row_group_idx = build_ctx.row_group_idx as u32;
1145    let mut entries = Vec::new();
1146
1147    for (idx, filter_ctx) in prefilter_ctx.filters.iter().enumerate() {
1148        if prefilter_ctx
1149            .proven_simple_filters
1150            .get(idx)
1151            .copied()
1152            .unwrap_or(false)
1153        {
1154            continue;
1155        }
1156        entries.push(PrefilterEntry {
1157            kind: PrefilterEntryKind::Simple(idx),
1158            key: Some(PrefilterKey::new(
1159                file_id,
1160                row_group_idx,
1161                row_selection.clone(),
1162                prefilter_ctx.schema_version,
1163                smallvec![filter_ctx.expr_str().to_string()],
1164            )),
1165        });
1166    }
1167
1168    for (idx, filter_ctx) in prefilter_ctx.physical_filters.iter().enumerate() {
1169        if !filter_ctx.is_immutable() {
1170            continue;
1171        }
1172        entries.push(PrefilterEntry {
1173            kind: PrefilterEntryKind::Physical(idx),
1174            key: Some(PrefilterKey::new(
1175                file_id,
1176                row_group_idx,
1177                row_selection.clone(),
1178                prefilter_ctx.schema_version,
1179                smallvec![filter_ctx.expr_str().to_string()],
1180            )),
1181        });
1182    }
1183
1184    if prefilter_ctx.pk_filter.is_some()
1185        && let Some(exprs) = &prefilter_ctx.pk_filter_expr_strs
1186    {
1187        entries.push(PrefilterEntry {
1188            kind: PrefilterEntryKind::PkGroup,
1189            key: Some(PrefilterKey::new(
1190                file_id,
1191                row_group_idx,
1192                row_selection,
1193                prefilter_ctx.schema_version,
1194                exprs.clone(),
1195            )),
1196        });
1197    }
1198
1199    entries
1200}
1201
1202fn identity_prefilter_result(
1203    reader_builder: &RowGroupReaderBuilder,
1204    build_ctx: &RowGroupBuildContext<'_>,
1205) -> PrefilterResult {
1206    let row_count = reader_builder
1207        .parquet_metadata()
1208        .row_group(build_ctx.row_group_idx)
1209        .num_rows() as usize;
1210    PrefilterResult {
1211        refined_selection: identity_row_selection(&build_ctx.row_selection, row_count),
1212        filtered_rows: 0,
1213    }
1214}
1215
1216fn identity_row_selection(
1217    original_selection: &Option<RowSelection>,
1218    row_count: usize,
1219) -> RowSelection {
1220    original_selection
1221        .clone()
1222        .unwrap_or_else(|| RowSelection::from(vec![RowSelector::select(row_count)]))
1223}
1224
1225fn rows_before_filter(
1226    reader_builder: &RowGroupReaderBuilder,
1227    build_ctx: &RowGroupBuildContext<'_>,
1228) -> usize {
1229    build_ctx.row_selection.as_ref().map_or_else(
1230        || {
1231            reader_builder
1232                .parquet_metadata()
1233                .row_group(build_ctx.row_group_idx)
1234                .num_rows() as usize
1235        },
1236        RowSelection::row_count,
1237    )
1238}
1239
1240fn refined_selection_from_mask(
1241    mask: &BooleanBuffer,
1242    original_selection: &Option<RowSelection>,
1243) -> RowSelection {
1244    if mask.is_empty() || mask.count_set_bits() == 0 {
1245        return RowSelection::from(vec![]);
1246    }
1247
1248    let prefilter_selection = RowSelection::from_filters(&[BooleanArray::from(mask.clone())]);
1249    match original_selection {
1250        Some(original) => original.and_then(&prefilter_selection),
1251        None => prefilter_selection,
1252    }
1253}
1254
1255fn eval_entry_mask(
1256    batch: &RecordBatch,
1257    prefilter_ctx: &mut PrefilterContext,
1258    kind: PrefilterEntryKind,
1259    file_path: &str,
1260) -> Result<BooleanBuffer> {
1261    match kind {
1262        PrefilterEntryKind::Simple(idx) => {
1263            eval_simple_filter_mask(batch, &prefilter_ctx.filters[idx], file_path)
1264        }
1265        PrefilterEntryKind::Physical(idx) => {
1266            eval_physical_filter_mask(batch, &prefilter_ctx.physical_filters[idx], file_path)
1267        }
1268        PrefilterEntryKind::PkGroup => {
1269            let pk_filter = prefilter_ctx.pk_filter.as_mut().context(UnexpectedSnafu {
1270                reason: "Missing primary key filter for prefilter cache entry",
1271            })?;
1272            primary_key_filter_mask(batch, pk_filter.as_mut())
1273        }
1274    }
1275}
1276
1277/// Evaluates an encoded-primary-key filter against a primary-key batch.
1278pub(crate) fn primary_key_filter_mask(
1279    batch: &RecordBatch,
1280    pk_filter: &mut dyn PrimaryKeyFilter,
1281) -> Result<BooleanBuffer> {
1282    let (pk_column_index, _) = batch
1283        .schema()
1284        .column_with_name(PRIMARY_KEY_COLUMN_NAME)
1285        .context(UnexpectedSnafu {
1286            reason: "Primary key column not found in prefilter batch",
1287        })?;
1288    let matched_row_ranges = matching_row_ranges_by_primary_key(batch, pk_column_index, pk_filter)?;
1289    let mut builder = BooleanBufferBuilder::new(batch.num_rows());
1290    builder.append_n(batch.num_rows(), false);
1291    for range in matched_row_ranges {
1292        for row in range {
1293            builder.set_bit(row, true);
1294        }
1295    }
1296    Ok(builder.finish())
1297}
1298
1299fn eval_simple_filter_mask(
1300    batch: &RecordBatch,
1301    filter_ctx: &SimpleFilterContext,
1302    file_path: &str,
1303) -> Result<BooleanBuffer> {
1304    let filter = match filter_ctx.filter() {
1305        MaybeFilter::Filter(filter) => filter,
1306        MaybeFilter::Matched => return Ok(BooleanBuffer::new_set(batch.num_rows())),
1307        MaybeFilter::Pruned => return Ok(BooleanBuffer::new_unset(batch.num_rows())),
1308    };
1309
1310    let (idx, _) = batch
1311        .schema()
1312        .column_with_name(filter.column_name())
1313        .with_context(|| UnexpectedSnafu {
1314            reason: format!(
1315                "Prefilter column '{}' (id {}) not found in batch for file {}",
1316                filter.column_name(),
1317                filter_ctx.column_id(),
1318                file_path
1319            ),
1320        })?;
1321    let column = batch.column(idx).clone();
1322    filter.evaluate_array(&column).context(RecordBatchSnafu)
1323}
1324
1325fn eval_physical_filter_mask(
1326    batch: &RecordBatch,
1327    filter_ctx: &PhysicalFilterContext,
1328    file_path: &str,
1329) -> Result<BooleanBuffer> {
1330    let filter = filter_ctx.filter();
1331
1332    let (idx, _) = batch
1333        .schema()
1334        .column_with_name(filter_ctx.column_name())
1335        .with_context(|| UnexpectedSnafu {
1336            reason: format!(
1337                "Prefilter physical column '{}' (id {}) not found in batch for file {}",
1338                filter_ctx.column_name(),
1339                filter_ctx.column_id(),
1340                file_path
1341            ),
1342        })?;
1343    let column = batch.column(idx).clone();
1344
1345    let record_batch = RecordBatch::try_new(filter_ctx.schema().clone(), vec![column])
1346        .context(NewRecordBatchSnafu)?;
1347    let evaluated = filter
1348        .evaluate(&record_batch)
1349        .context(EvalPartitionFilterSnafu)?;
1350    let array = evaluated
1351        .into_array(record_batch.num_rows())
1352        .context(EvalPartitionFilterSnafu)?;
1353    let boolean_array = array
1354        .as_any()
1355        .downcast_ref::<BooleanArray>()
1356        .context(UnexpectedSnafu {
1357            reason: "Failed to downcast physical filter result to BooleanArray",
1358        })?;
1359    // Treat null results as false (filtered out); value bits are not guaranteed
1360    // to be false for invalid entries.
1361    let mut result = boolean_array.values().clone();
1362    if let Some(nulls) = boolean_array.nulls() {
1363        result = result.bitand(nulls.inner());
1364    }
1365    Ok(result)
1366}
1367
1368#[cfg(test)]
1369mod tests {
1370    use std::sync::Arc;
1371    use std::sync::atomic::{AtomicUsize, Ordering};
1372
1373    use bytes::Bytes;
1374    use common_recordbatch::filter::SimpleFilterEvaluator;
1375    use datafusion_common::ScalarValue;
1376    use datafusion_expr::{col, lit};
1377    use datatypes::arrow::array::{
1378        ArrayRef, DictionaryArray, Int32Array, TimestampMillisecondArray, UInt8Array, UInt32Array,
1379        UInt64Array,
1380    };
1381    use datatypes::arrow::datatypes::{DataType, Field, Schema, UInt32Type};
1382    use datatypes::arrow::record_batch::RecordBatch;
1383    use datatypes::prelude::ConcreteDataType;
1384    use datatypes::value::Value;
1385    use mito_codec::row_converter::{PrimaryKeyFilter, build_primary_key_codec};
1386    use parquet::arrow::ArrowWriter;
1387    use parquet::arrow::arrow_reader::{ParquetRecordBatchReaderBuilder, RowSelector};
1388    use store_api::codec::PrimaryKeyEncoding;
1389    use store_api::metadata::RegionMetadataBuilder;
1390    use store_api::region_request::{AlterKind, ModifyColumnType};
1391
1392    use super::*;
1393    use crate::read::read_columns::ReadColumns;
1394    use crate::sst::internal_fields;
1395    use crate::sst::parquet::flat_format::{FlatReadFormat, primary_key_column_index};
1396    use crate::test_util::sst_util::{
1397        new_primary_key, new_record_batch_with_custom_sequence, sst_region_metadata,
1398        sst_region_metadata_with_encoding,
1399    };
1400
1401    struct CountingPrimaryKeyFilter {
1402        hits: Arc<AtomicUsize>,
1403        expected: Vec<u8>,
1404    }
1405
1406    impl PrimaryKeyFilter for CountingPrimaryKeyFilter {
1407        fn matches(&mut self, pk: &[u8]) -> mito_codec::error::Result<bool> {
1408            self.hits.fetch_add(1, Ordering::Relaxed);
1409            Ok(pk == self.expected.as_slice())
1410        }
1411    }
1412
1413    #[test]
1414    fn test_cached_primary_key_filter_reuses_previous_result() {
1415        let expected = new_primary_key(&["a", "x"]);
1416        let hits = Arc::new(AtomicUsize::new(0));
1417        let mut filter = CachedPrimaryKeyFilter::new(Box::new(CountingPrimaryKeyFilter {
1418            hits: Arc::clone(&hits),
1419            expected: expected.clone(),
1420        }));
1421
1422        assert!(filter.matches(expected.as_slice()).unwrap());
1423        assert!(filter.matches(expected.as_slice()).unwrap());
1424        assert!(
1425            !filter
1426                .matches(new_primary_key(&["b", "x"]).as_slice())
1427                .unwrap()
1428        );
1429
1430        assert_eq!(hits.load(Ordering::Relaxed), 2);
1431    }
1432
1433    fn new_test_filters(exprs: &[datafusion_expr::Expr]) -> Vec<SimpleFilterEvaluator> {
1434        exprs
1435            .iter()
1436            .filter_map(SimpleFilterEvaluator::try_new)
1437            .collect()
1438    }
1439
1440    fn new_simple_filter_contexts(
1441        metadata: &RegionMetadataRef,
1442        exprs: &[datafusion_expr::Expr],
1443    ) -> Vec<SimpleFilterContext> {
1444        exprs
1445            .iter()
1446            .filter_map(|expr| SimpleFilterContext::new_opt(metadata, None, expr))
1447            .collect()
1448    }
1449
1450    fn stats_metadata_with_options(
1451        values: &[Vec<u64>],
1452        nulls: Option<&[bool]>,
1453        writer_options: Option<parquet::file::properties::WriterProperties>,
1454    ) -> Arc<ParquetMetaData> {
1455        let first = new_record_batch_with_custom_sequence(&["a", "x"], 0, values[0].len(), 1);
1456        let mut bytes = Vec::new();
1457        let mut writer = ArrowWriter::try_new(&mut bytes, first.schema(), writer_options).unwrap();
1458        for (idx, values) in values.iter().enumerate() {
1459            let has_null = nulls.and_then(|nulls| nulls.get(idx)).copied() == Some(true);
1460            let batch = new_record_batch_with_custom_sequence(
1461                &["a", "x"],
1462                0,
1463                if has_null {
1464                    values.len().max(2)
1465                } else {
1466                    values.len()
1467                },
1468                1,
1469            );
1470            let mut columns = batch.columns().to_vec();
1471            columns[2] = Arc::new(if has_null {
1472                UInt64Array::from(vec![Some(values[0]), None])
1473            } else {
1474                UInt64Array::from_iter_values(values.iter().copied())
1475            });
1476            writer
1477                .write(&RecordBatch::try_new(batch.schema(), columns).unwrap())
1478                .unwrap();
1479            writer.flush().unwrap();
1480        }
1481        writer.close().unwrap();
1482        ParquetRecordBatchReaderBuilder::try_new(Bytes::from(bytes))
1483            .unwrap()
1484            .metadata()
1485            .clone()
1486    }
1487
1488    fn stats_metadata(values: &[Vec<u64>], nulls: Option<&[bool]>) -> Arc<ParquetMetaData> {
1489        stats_metadata_with_options(values, nulls, None)
1490    }
1491
1492    fn int32_stats_metadata(values: &[i32]) -> Arc<ParquetMetaData> {
1493        let batch = new_record_batch_with_custom_sequence(&["a", "x"], 0, values.len(), 1);
1494        let mut fields = batch
1495            .schema()
1496            .fields()
1497            .iter()
1498            .map(|field| field.as_ref().clone())
1499            .collect::<Vec<_>>();
1500        fields[2].set_data_type(DataType::Int32);
1501        let mut columns = batch.columns().to_vec();
1502        columns[2] = Arc::new(Int32Array::from(values.to_vec()));
1503        let batch = RecordBatch::try_new(Arc::new(Schema::new(fields)), columns).unwrap();
1504        let mut bytes = Vec::new();
1505        let mut writer = ArrowWriter::try_new(&mut bytes, batch.schema(), None).unwrap();
1506        writer.write(&batch).unwrap();
1507        writer.close().unwrap();
1508        ParquetRecordBatchReaderBuilder::try_new(Bytes::from(bytes))
1509            .unwrap()
1510            .metadata()
1511            .clone()
1512    }
1513
1514    fn metadata_with_field_type(data_type: ConcreteDataType) -> RegionMetadataRef {
1515        let mut builder = RegionMetadataBuilder::from_existing(sst_region_metadata());
1516        builder
1517            .alter(AlterKind::ModifyColumnTypes {
1518                columns: vec![ModifyColumnType {
1519                    column_name: "field_0".to_string(),
1520                    target_type: data_type,
1521                }],
1522            })
1523            .unwrap();
1524        Arc::new(builder.build().unwrap())
1525    }
1526
1527    fn new_physical_filter_contexts(
1528        metadata: &RegionMetadataRef,
1529        read_format: &FlatReadFormat,
1530        exprs: &[datafusion_expr::Expr],
1531    ) -> Vec<PhysicalFilterContext> {
1532        exprs
1533            .iter()
1534            .filter_map(|expr| PhysicalFilterContext::new_opt(metadata, None, read_format, expr))
1535            .collect()
1536    }
1537
1538    fn new_raw_batch(primary_keys: &[&[u8]], field_values: &[u64]) -> RecordBatch {
1539        assert_eq!(primary_keys.len(), field_values.len());
1540
1541        let metadata = Arc::new(sst_region_metadata());
1542        let arrow_schema = metadata.schema.arrow_schema();
1543        let field_column = arrow_schema
1544            .field(arrow_schema.index_of("field_0").unwrap())
1545            .clone();
1546        let time_index_column = arrow_schema
1547            .field(arrow_schema.index_of("ts").unwrap())
1548            .clone();
1549        let mut fields = vec![field_column, time_index_column];
1550        fields.extend(
1551            internal_fields()
1552                .into_iter()
1553                .map(|field| field.as_ref().clone()),
1554        );
1555        let schema = Arc::new(Schema::new(fields));
1556
1557        let mut dict_values = Vec::new();
1558        let mut keys = Vec::with_capacity(primary_keys.len());
1559        for pk in primary_keys {
1560            let key = dict_values
1561                .iter()
1562                .position(|existing: &&[u8]| existing == pk)
1563                .unwrap_or_else(|| {
1564                    dict_values.push(*pk);
1565                    dict_values.len() - 1
1566                });
1567            keys.push(key as u32);
1568        }
1569        let pk_array: ArrayRef = Arc::new(DictionaryArray::<UInt32Type>::new(
1570            UInt32Array::from(keys),
1571            Arc::new(BinaryArray::from_iter_values(dict_values.iter().copied())),
1572        ));
1573
1574        RecordBatch::try_new(
1575            schema,
1576            vec![
1577                Arc::new(UInt64Array::from(field_values.to_vec())),
1578                Arc::new(TimestampMillisecondArray::from_iter_values(
1579                    0..primary_keys.len() as i64,
1580                )),
1581                pk_array,
1582                Arc::new(UInt64Array::from(vec![1; primary_keys.len()])),
1583                Arc::new(UInt8Array::from(vec![1; primary_keys.len()])),
1584            ],
1585        )
1586        .unwrap()
1587    }
1588
1589    fn new_prefilter_batch(primary_keys: &[&[u8]], field_values: &[u64]) -> RecordBatch {
1590        assert_eq!(primary_keys.len(), field_values.len());
1591
1592        let metadata = Arc::new(sst_region_metadata());
1593        let arrow_schema = metadata.schema.arrow_schema();
1594        let field_column = arrow_schema
1595            .field(arrow_schema.index_of("field_0").unwrap())
1596            .clone();
1597        let time_index_column = arrow_schema
1598            .field(arrow_schema.index_of("ts").unwrap())
1599            .clone();
1600        let schema = Arc::new(Schema::new(vec![
1601            field_column,
1602            time_index_column,
1603            internal_fields()[0].as_ref().clone(),
1604        ]));
1605
1606        let mut dict_values = Vec::new();
1607        let mut keys = Vec::with_capacity(primary_keys.len());
1608        for pk in primary_keys {
1609            let key = dict_values
1610                .iter()
1611                .position(|existing: &&[u8]| existing == pk)
1612                .unwrap_or_else(|| {
1613                    dict_values.push(*pk);
1614                    dict_values.len() - 1
1615                });
1616            keys.push(key as u32);
1617        }
1618        let pk_array: ArrayRef = Arc::new(DictionaryArray::<UInt32Type>::new(
1619            UInt32Array::from(keys),
1620            Arc::new(BinaryArray::from_iter_values(dict_values.iter().copied())),
1621        ));
1622
1623        RecordBatch::try_new(
1624            schema,
1625            vec![
1626                Arc::new(UInt64Array::from(field_values.to_vec())),
1627                Arc::new(TimestampMillisecondArray::from_iter_values(
1628                    0..primary_keys.len() as i64,
1629                )),
1630                pk_array,
1631            ],
1632        )
1633        .unwrap()
1634    }
1635
1636    fn new_prefilter_batch_binary_pk(primary_keys: &[&[u8]], field_values: &[u64]) -> RecordBatch {
1637        assert_eq!(primary_keys.len(), field_values.len());
1638
1639        let metadata = Arc::new(sst_region_metadata());
1640        let arrow_schema = metadata.schema.arrow_schema();
1641        let field_column = arrow_schema
1642            .field(arrow_schema.index_of("field_0").unwrap())
1643            .clone();
1644        let time_index_column = arrow_schema
1645            .field(arrow_schema.index_of("ts").unwrap())
1646            .clone();
1647        let schema = Arc::new(Schema::new(vec![
1648            field_column,
1649            time_index_column,
1650            Field::new(PRIMARY_KEY_COLUMN_NAME, DataType::Binary, false),
1651        ]));
1652
1653        let pk_array: ArrayRef =
1654            Arc::new(BinaryArray::from_iter_values(primary_keys.iter().copied()));
1655
1656        RecordBatch::try_new(
1657            schema,
1658            vec![
1659                Arc::new(UInt64Array::from(field_values.to_vec())),
1660                Arc::new(TimestampMillisecondArray::from_iter_values(
1661                    0..primary_keys.len() as i64,
1662                )),
1663                pk_array,
1664            ],
1665        )
1666        .unwrap()
1667    }
1668
1669    fn field_values(batch: &RecordBatch) -> Vec<u64> {
1670        batch
1671            .column(0)
1672            .as_any()
1673            .downcast_ref::<UInt64Array>()
1674            .unwrap()
1675            .values()
1676            .to_vec()
1677    }
1678
1679    fn remaining_simple_filter_columns(filters: &[SimpleFilterContext]) -> Vec<&str> {
1680        filters
1681            .iter()
1682            .map(|filter_ctx| filter_ctx.filter().as_filter().unwrap().column_name())
1683            .collect()
1684    }
1685
1686    #[test]
1687    fn test_prefilter_primary_key_drops_single_dictionary_batch() {
1688        let metadata = Arc::new(sst_region_metadata());
1689        let filters = Arc::new(new_test_filters(&[col("tag_0").eq(lit("b"))]));
1690        let mut primary_key_filter =
1691            build_primary_key_codec(metadata.as_ref()).primary_key_filter(&metadata, filters);
1692        let pk_a = new_primary_key(&["a", "x"]);
1693        let batch = new_raw_batch(&[pk_a.as_slice(), pk_a.as_slice()], &[10, 11]);
1694        let pk_col_idx = primary_key_column_index(batch.num_columns());
1695
1696        let filtered =
1697            prefilter_flat_batch_by_primary_key(batch, pk_col_idx, primary_key_filter.as_mut())
1698                .unwrap();
1699
1700        assert!(filtered.is_none());
1701    }
1702
1703    #[test]
1704    fn test_prefilter_primary_key_builds_mask_for_fragmented_matches() {
1705        let metadata = Arc::new(sst_region_metadata());
1706        let filters = Arc::new(new_test_filters(&[col("tag_0")
1707            .eq(lit("a"))
1708            .or(col("tag_0").eq(lit("c")))]));
1709        let mut primary_key_filter =
1710            build_primary_key_codec(metadata.as_ref()).primary_key_filter(&metadata, filters);
1711        let pk_a = new_primary_key(&["a", "x"]);
1712        let pk_b = new_primary_key(&["b", "x"]);
1713        let pk_c = new_primary_key(&["c", "x"]);
1714        let pk_d = new_primary_key(&["d", "x"]);
1715        let batch = new_raw_batch(
1716            &[
1717                pk_a.as_slice(),
1718                pk_a.as_slice(),
1719                pk_b.as_slice(),
1720                pk_b.as_slice(),
1721                pk_c.as_slice(),
1722                pk_c.as_slice(),
1723                pk_d.as_slice(),
1724                pk_d.as_slice(),
1725            ],
1726            &[10, 11, 12, 13, 14, 15, 16, 17],
1727        );
1728        let pk_col_idx = primary_key_column_index(batch.num_columns());
1729
1730        let filtered =
1731            prefilter_flat_batch_by_primary_key(batch, pk_col_idx, primary_key_filter.as_mut())
1732                .unwrap()
1733                .unwrap();
1734
1735        assert_eq!(filtered.num_rows(), 4);
1736        assert_eq!(field_values(&filtered), vec![10, 11, 14, 15]);
1737    }
1738
1739    #[test]
1740    fn test_prefilter_builder_returns_none_without_selected_filters() {
1741        let metadata: RegionMetadataRef =
1742            Arc::new(sst_region_metadata_with_encoding(PrimaryKeyEncoding::Dense));
1743        let read_format = FlatReadFormat::new(
1744            metadata.clone(),
1745            ReadColumns::new(metadata.column_metadatas.iter().map(|c| c.column_id)),
1746            None,
1747            "test",
1748            false,
1749        )
1750        .unwrap();
1751        let codec = build_primary_key_codec(metadata.as_ref());
1752
1753        let builder = PrefilterContextBuilder::new(
1754            &read_format,
1755            &codec,
1756            None,
1757            None,
1758            Vec::new(),
1759            Vec::new(),
1760            metadata.schema_version,
1761            &stats_metadata(&[vec![1]], None),
1762        );
1763        assert!(builder.is_none());
1764    }
1765
1766    #[test]
1767    fn test_simple_filter_stats_uses_real_metadata() {
1768        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
1769        let read_format = FlatReadFormat::new(
1770            metadata.clone(),
1771            ReadColumns::new(
1772                metadata
1773                    .column_metadatas
1774                    .iter()
1775                    .map(|column| column.column_id),
1776            ),
1777            None,
1778            "test",
1779            true,
1780        )
1781        .unwrap();
1782        let parquet_metadata = stats_metadata(&[vec![i64::MAX as u64 + 2]], None);
1783        let filters = new_simple_filter_contexts(
1784            &metadata,
1785            &[
1786                col("field_0").gt(lit(i64::MAX as u64 + 1)),
1787                col("field_0").gt(lit(i64::MAX as u64 + 2)),
1788                lit(i64::MAX as u64 + 1).lt(col("field_0")),
1789            ],
1790        );
1791
1792        let proofs =
1793            simple_filter_stats_proofs(&read_format, parquet_metadata.row_groups(), &filters);
1794        assert_eq!(proofs, vec![vec![true, false, true]]);
1795    }
1796
1797    #[test]
1798    fn test_simple_filter_stats_retain_narrow_or_incomplete_metadata() {
1799        let parquet_metadata = stats_metadata(&[vec![2]], Some(&[true]));
1800        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
1801        let read_format = FlatReadFormat::new(
1802            metadata.clone(),
1803            ReadColumns::new(
1804                metadata
1805                    .column_metadatas
1806                    .iter()
1807                    .map(|column| column.column_id),
1808            ),
1809            None,
1810            "test",
1811            true,
1812        )
1813        .unwrap();
1814        let filter = new_simple_filter_contexts(&metadata, &[col("field_0").gt(lit(1_u64))]);
1815        assert_eq!(
1816            simple_filter_stats_proofs(&read_format, parquet_metadata.row_groups(), &filter),
1817            vec![vec![false]],
1818        );
1819        let missing_stats = stats_metadata_with_options(
1820            &[vec![2]],
1821            None,
1822            Some(
1823                parquet::file::properties::WriterProperties::builder()
1824                    .set_statistics_enabled(parquet::file::properties::EnabledStatistics::None)
1825                    .build(),
1826            ),
1827        );
1828        assert_eq!(
1829            simple_filter_stats_proofs(&read_format, missing_stats.row_groups(), &filter),
1830            vec![vec![false]],
1831        );
1832
1833        // Parquet INT32 stats decode as Value::Int32; without the actual SST
1834        // type gate this Int8 field would be incorrectly proven true.
1835        let narrow_metadata = metadata_with_field_type(ConcreteDataType::int8_datatype());
1836        let narrow_read_format = FlatReadFormat::new(
1837            narrow_metadata.clone(),
1838            ReadColumns::new(
1839                narrow_metadata
1840                    .column_metadatas
1841                    .iter()
1842                    .map(|column| column.column_id),
1843            ),
1844            None,
1845            "test",
1846            true,
1847        )
1848        .unwrap();
1849        let narrow_filter =
1850            new_simple_filter_contexts(&narrow_metadata, &[col("field_0").gt(lit(1_i32))]);
1851        let narrow_stats = int32_stats_metadata(&[2]);
1852        assert_eq!(
1853            simple_filter_stats_proofs(
1854                &narrow_read_format,
1855                narrow_stats.row_groups(),
1856                &narrow_filter,
1857            ),
1858            vec![vec![false]],
1859        );
1860    }
1861
1862    #[test]
1863    fn test_prefilter_builder_uses_row_group_proofs_by_index() {
1864        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
1865        let read_format = FlatReadFormat::new(
1866            metadata.clone(),
1867            ReadColumns::new(
1868                metadata
1869                    .column_metadatas
1870                    .iter()
1871                    .map(|column| column.column_id),
1872            ),
1873            None,
1874            "test",
1875            true,
1876        )
1877        .unwrap();
1878        let codec = build_primary_key_codec(metadata.as_ref());
1879        let builder = PrefilterContextBuilder::new(
1880            &read_format,
1881            &codec,
1882            None,
1883            None,
1884            new_simple_filter_contexts(&metadata, &[col("field_0").gt(lit(1_u64))]),
1885            Vec::new(),
1886            metadata.schema_version,
1887            &stats_metadata(&[vec![2], vec![1]], None),
1888        )
1889        .unwrap();
1890
1891        assert!(builder.build(0).proven_simple_filters[0]);
1892        assert!(!builder.build(1).proven_simple_filters[0]);
1893    }
1894
1895    #[test]
1896    fn test_simple_filter_stats_prove_integer_predicates() {
1897        macro_rules! assert_all_operators {
1898            ($literal:expr, $base:expr, $below:expr, $above:expr) => {{
1899                for (expr, min, max) in [
1900                    (col("x").gt(lit($literal)), $above.clone(), $above.clone()),
1901                    (col("x").gt_eq(lit($literal)), $base.clone(), $above.clone()),
1902                    (col("x").lt(lit($literal)), $below.clone(), $below.clone()),
1903                    (col("x").lt_eq(lit($literal)), $below.clone(), $base.clone()),
1904                    (col("x").eq(lit($literal)), $base.clone(), $base.clone()),
1905                    (
1906                        col("x").not_eq(lit($literal)),
1907                        $below.clone(),
1908                        $below.clone(),
1909                    ),
1910                ] {
1911                    let filter = SimpleFilterEvaluator::try_new(&expr).unwrap();
1912                    assert!(simple_filter_is_true_by_values(
1913                        &filter,
1914                        &$base,
1915                        Some(min),
1916                        Some(max),
1917                        Some(Value::UInt64(0)),
1918                    ));
1919                }
1920            }};
1921        }
1922
1923        assert_all_operators!(5_i32, Value::Int32(5), Value::Int32(-1), Value::Int32(6));
1924        assert_all_operators!(5_u32, Value::UInt32(5), Value::UInt32(4), Value::UInt32(6));
1925        assert_all_operators!(5_i64, Value::Int64(5), Value::Int64(-1), Value::Int64(6));
1926        assert_all_operators!(
1927            i64::MAX as u64 + 2,
1928            Value::UInt64(i64::MAX as u64 + 2),
1929            Value::UInt64(i64::MAX as u64 + 1),
1930            Value::UInt64(i64::MAX as u64 + 3)
1931        );
1932
1933        // Boundary cases must remain unproven: the min/max interval still
1934        // admits a row that violates the predicate.
1935        for (expr, min, max) in [
1936            (col("x").lt(lit(5_i64)), 1, 5),
1937            (col("x").gt(lit(5_i64)), 5, 9),
1938            (col("x").lt_eq(lit(5_i64)), 1, 6),
1939            (col("x").gt_eq(lit(5_i64)), 4, 9),
1940            (col("x").eq(lit(5_i64)), 5, 6),
1941            (col("x").not_eq(lit(5_i64)), 5, 9),
1942            (col("x").not_eq(lit(5_i64)), 1, 5),
1943            (col("x").not_eq(lit(5_i64)), 1, 9),
1944        ] {
1945            let filter = SimpleFilterEvaluator::try_new(&expr).unwrap();
1946            assert!(
1947                !simple_filter_is_true_by_values(
1948                    &filter,
1949                    &Value::Int64(5),
1950                    Some(Value::Int64(min)),
1951                    Some(Value::Int64(max)),
1952                    Some(Value::UInt64(0)),
1953                ),
1954                "{expr:?} must not be proven by stats {min}..={max}",
1955            );
1956        }
1957    }
1958
1959    #[test]
1960    fn test_simple_filter_stats_retain_unknown_or_unsupported_values() {
1961        let filter = SimpleFilterEvaluator::try_new(&col("x").gt(lit(1_i32))).unwrap();
1962        let literal = Value::Int32(1);
1963        for (min, max, null_count) in [
1964            (
1965                Some(Value::Int32(2)),
1966                Some(Value::Int32(3)),
1967                Some(Value::UInt64(1)),
1968            ),
1969            (Some(Value::Int32(2)), Some(Value::Int32(3)), None),
1970            (
1971                Some(Value::Null),
1972                Some(Value::Int32(3)),
1973                Some(Value::UInt64(0)),
1974            ),
1975            (None, Some(Value::Int32(3)), Some(Value::UInt64(0))),
1976            (Some(Value::Int32(2)), None, Some(Value::UInt64(0))),
1977            (
1978                Some(Value::Int64(2)),
1979                Some(Value::Int64(3)),
1980                Some(Value::UInt64(0)),
1981            ),
1982            (
1983                Some(Value::Int32(3)),
1984                Some(Value::Int32(2)),
1985                Some(Value::UInt64(0)),
1986            ),
1987        ] {
1988            assert!(!simple_filter_is_true_by_values(
1989                &filter, &literal, min, max, null_count
1990            ));
1991        }
1992
1993        let float_filter = SimpleFilterEvaluator::try_new(&col("x").gt(lit(1.0_f64))).unwrap();
1994        assert!(!simple_filter_is_true_by_values(
1995            &float_filter,
1996            &Value::Float64(1.0.into()),
1997            Some(Value::Float64(2.0.into())),
1998            Some(Value::Float64(3.0.into())),
1999            Some(Value::UInt64(0)),
2000        ));
2001    }
2002
2003    #[test]
2004    fn test_prefilter_entries_keep_only_unproven_simple_filter_indices() {
2005        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
2006        let filters = new_simple_filter_contexts(
2007            &metadata,
2008            &[col("field_0").gt(lit(1_u64)), col("field_0").lt(lit(9_u64))],
2009        );
2010        let context = PrefilterContext {
2011            pk_filter: None,
2012            filters,
2013            physical_filters: Vec::new(),
2014            schema_version: metadata.schema_version,
2015            pk_filter_expr_strs: None,
2016            arrow_schema: metadata.schema.arrow_schema().clone(),
2017            proven_simple_filters: vec![true, false],
2018        };
2019
2020        let entries = all_prefilter_entries(&context);
2021        assert!(matches!(
2022            entries.as_slice(),
2023            [PrefilterEntry {
2024                kind: PrefilterEntryKind::Simple(1),
2025                ..
2026            }]
2027        ));
2028    }
2029
2030    #[test]
2031    fn test_identity_row_selection_preserves_input() {
2032        let sparse = RowSelection::from(vec![
2033            RowSelector::skip(2),
2034            RowSelector::select(3),
2035            RowSelector::skip(1),
2036        ]);
2037        assert_eq!(identity_row_selection(&Some(sparse.clone()), 6), sparse);
2038
2039        let empty = RowSelection::from(vec![]);
2040        assert_eq!(identity_row_selection(&Some(empty.clone()), 6), empty);
2041        assert_eq!(
2042            identity_row_selection(&None, 6),
2043            RowSelection::from(vec![RowSelector::select(6)])
2044        );
2045    }
2046
2047    #[test]
2048    fn test_should_use_prefilter() {
2049        assert!(should_use_prefilter(1, 5, 6));
2050        assert!(!should_use_prefilter(1, 0, 1));
2051        assert!(!should_use_prefilter(1, 1, 2));
2052        assert!(!should_use_prefilter(4, 3, 7));
2053        assert!(should_use_prefilter(3, 3, 6));
2054    }
2055
2056    #[test]
2057    fn test_build_bulk_filter_plan_classifies_filters_across_read_paths() {
2058        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata_with_encoding(
2059            PrimaryKeyEncoding::Sparse,
2060        ));
2061        let legacy_read_format = FlatReadFormat::new(
2062            metadata.clone(),
2063            ReadColumns::new(metadata.column_metadatas.iter().map(|c| c.column_id)),
2064            None,
2065            "memtable",
2066            false,
2067        )
2068        .unwrap();
2069        assert!(!legacy_read_format.batch_has_raw_pk_columns());
2070
2071        let plan = build_bulk_filter_plan(
2072            &legacy_read_format,
2073            Some(&Predicate::new(vec![
2074                col("tag_0").eq(lit("a")),
2075                col("field_0").gt(lit(1_u64)),
2076            ])),
2077        );
2078        assert_eq!(
2079            plan.pk_filters.as_ref().map(|filters| filters.len()),
2080            Some(1)
2081        );
2082        assert_eq!(
2083            remaining_simple_filter_columns(&plan.remaining_simple_filters),
2084            vec!["field_0"]
2085        );
2086
2087        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
2088        let raw_pk_read_format = FlatReadFormat::new(
2089            metadata.clone(),
2090            ReadColumns::new(metadata.column_metadatas.iter().map(|c| c.column_id)),
2091            None,
2092            "memtable",
2093            true,
2094        )
2095        .unwrap();
2096        assert!(raw_pk_read_format.batch_has_raw_pk_columns());
2097
2098        let tag_only_plan = build_bulk_filter_plan(
2099            &raw_pk_read_format,
2100            Some(&Predicate::new(vec![col("tag_0").eq(lit("a"))])),
2101        );
2102        assert!(tag_only_plan.pk_filters.is_none());
2103        assert_eq!(
2104            remaining_simple_filter_columns(&tag_only_plan.remaining_simple_filters),
2105            vec!["tag_0"]
2106        );
2107
2108        let field_only_plan = build_bulk_filter_plan(
2109            &raw_pk_read_format,
2110            Some(&Predicate::new(vec![col("field_0").gt(lit(1_u64))])),
2111        );
2112        assert!(field_only_plan.pk_filters.is_none());
2113        assert_eq!(
2114            remaining_simple_filter_columns(&field_only_plan.remaining_simple_filters),
2115            vec!["field_0"]
2116        );
2117    }
2118
2119    #[test]
2120    fn test_build_reader_filter_plan_classifies_filters_for_prefilter_modes() {
2121        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
2122        let full_read_format = FlatReadFormat::new(
2123            metadata.clone(),
2124            ReadColumns::new(metadata.column_metadatas.iter().map(|c| c.column_id)),
2125            None,
2126            "test",
2127            true,
2128        )
2129        .unwrap();
2130        let codec = build_primary_key_codec(metadata.as_ref());
2131
2132        let skip_fields_plan = build_reader_filter_plan(
2133            Some(&Predicate::new(vec![
2134                col("tag_0").eq(lit("a")),
2135                col("field_0").gt(lit(1_u64)),
2136            ])),
2137            None,
2138            PreFilterMode::SkipFields,
2139            true,
2140            false,
2141            &full_read_format,
2142            &codec,
2143            &stats_metadata(&[vec![1]], None),
2144        );
2145        assert!(skip_fields_plan.prefilter_builder.is_some());
2146        assert_eq!(
2147            remaining_simple_filter_columns(&skip_fields_plan.remaining_simple_filters),
2148            vec!["field_0"]
2149        );
2150
2151        let postponed_time_plan = build_reader_filter_plan(
2152            Some(&Predicate::new(vec![
2153                col("tag_0").eq(lit("a")),
2154                col("field_0").gt(lit(1_u64)),
2155                col("ts").gt_eq(lit(ScalarValue::TimestampMillisecond(Some(1), None))),
2156            ])),
2157            None,
2158            PreFilterMode::SkipFields,
2159            true,
2160            true,
2161            &full_read_format,
2162            &codec,
2163            &stats_metadata(&[vec![1]], None),
2164        );
2165        assert!(postponed_time_plan.prefilter_builder.is_some());
2166        assert_eq!(
2167            remaining_simple_filter_columns(&postponed_time_plan.remaining_simple_filters),
2168            vec!["field_0", "ts"]
2169        );
2170
2171        let postponed_time_only_plan = build_reader_filter_plan(
2172            Some(&Predicate::new(vec![col("ts").gt_eq(lit(
2173                ScalarValue::TimestampMillisecond(Some(1), None),
2174            ))])),
2175            None,
2176            PreFilterMode::All,
2177            true,
2178            true,
2179            &full_read_format,
2180            &codec,
2181            &stats_metadata(&[vec![1]], None),
2182        );
2183        assert!(postponed_time_only_plan.prefilter_builder.is_none());
2184        assert_eq!(
2185            remaining_simple_filter_columns(&postponed_time_only_plan.remaining_simple_filters),
2186            vec!["ts"]
2187        );
2188
2189        let metric_metadata: RegionMetadataRef = Arc::new(sst_region_metadata_with_encoding(
2190            PrimaryKeyEncoding::Sparse,
2191        ));
2192        let field_0 = metric_metadata.column_by_name("field_0").unwrap().column_id;
2193        let ts = metric_metadata.time_index_column().column_id;
2194        let projected_read_format = FlatReadFormat::new(
2195            metric_metadata.clone(),
2196            ReadColumns::new([field_0, ts]),
2197            None,
2198            "test",
2199            true,
2200        )
2201        .unwrap();
2202        let metric_codec = build_primary_key_codec(metric_metadata.as_ref());
2203        let pk_prefilter_plan = build_reader_filter_plan(
2204            Some(&Predicate::new(vec![col("tag_0").eq(lit("a"))])),
2205            None,
2206            PreFilterMode::All,
2207            true,
2208            false,
2209            &projected_read_format,
2210            &metric_codec,
2211            &stats_metadata(&[vec![1]], None),
2212        );
2213        assert!(pk_prefilter_plan.prefilter_builder.is_some());
2214        assert!(
2215            pk_prefilter_plan
2216                .prefilter_builder
2217                .as_ref()
2218                .unwrap()
2219                .build_primary_key_filter()
2220                .is_some()
2221        );
2222        assert!(pk_prefilter_plan.remaining_simple_filters.is_empty());
2223
2224        let disabled_plan = build_reader_filter_plan(
2225            Some(&Predicate::new(vec![
2226                col("tag_0").eq(lit("a")),
2227                col("field_0").gt(lit(1_u64)),
2228                col("ts").gt_eq(lit(ScalarValue::TimestampMillisecond(Some(1), None))),
2229            ])),
2230            None,
2231            PreFilterMode::All,
2232            false,
2233            true,
2234            &projected_read_format,
2235            &metric_codec,
2236            &stats_metadata(&[vec![1]], None),
2237        );
2238        assert!(disabled_plan.prefilter_builder.is_none());
2239        assert_eq!(
2240            remaining_simple_filter_columns(&disabled_plan.remaining_simple_filters),
2241            vec!["tag_0", "field_0", "ts"]
2242        );
2243    }
2244
2245    #[test]
2246    fn test_pk_filter_expr_strings_are_stable_under_expr_order() {
2247        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata_with_encoding(
2248            PrimaryKeyEncoding::Sparse,
2249        ));
2250        let read_format = FlatReadFormat::new(
2251            metadata.clone(),
2252            ReadColumns::new(metadata.column_metadatas.iter().map(|c| c.column_id)),
2253            None,
2254            "test",
2255            false,
2256        )
2257        .unwrap();
2258        let codec = build_primary_key_codec(metadata.as_ref());
2259
2260        let expr_a = col("tag_0").eq(lit("a"));
2261        let expr_b = col("tag_1").eq(lit("x"));
2262        let plan_ab = build_reader_filter_plan(
2263            Some(&Predicate::new(vec![expr_a.clone(), expr_b.clone()])),
2264            None,
2265            PreFilterMode::All,
2266            true,
2267            false,
2268            &read_format,
2269            &codec,
2270            &stats_metadata(&[vec![1]], None),
2271        );
2272        let plan_b_a = build_reader_filter_plan(
2273            Some(&Predicate::new(vec![expr_b, expr_a])),
2274            None,
2275            PreFilterMode::All,
2276            true,
2277            false,
2278            &read_format,
2279            &codec,
2280            &stats_metadata(&[vec![1]], None),
2281        );
2282
2283        let exprs_ab = plan_ab.prefilter_builder.unwrap().pk_filter_expr_strs;
2284        let exprs_b_a = plan_b_a.prefilter_builder.unwrap().pk_filter_expr_strs;
2285        assert!(exprs_ab.is_some());
2286        assert_eq!(exprs_ab, exprs_b_a);
2287    }
2288
2289    #[test]
2290    fn test_simple_and_physical_contexts_preserve_expr_strings() {
2291        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
2292        let read_format = FlatReadFormat::new(
2293            metadata.clone(),
2294            ReadColumns::new(metadata.column_metadatas.iter().map(|c| c.column_id)),
2295            None,
2296            "test",
2297            true,
2298        )
2299        .unwrap();
2300
2301        let simple_expr = col("tag_0").eq(lit("a"));
2302        let simple = SimpleFilterContext::new_opt(&metadata, None, &simple_expr).unwrap();
2303        assert_eq!(simple.expr_str(), format!("{simple_expr:?}"));
2304
2305        let physical_expr = col("field_0").in_list(vec![lit(1_u64), lit(2_u64)], false);
2306        let physical =
2307            PhysicalFilterContext::new_opt(&metadata, None, &read_format, &physical_expr).unwrap();
2308        assert_eq!(physical.expr_str(), format!("{physical_expr:?}"));
2309    }
2310
2311    #[test]
2312    fn test_eval_simple_filter_mask_uses_flat_tag_columns_directly() {
2313        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
2314        let filters = new_simple_filter_contexts(&metadata, &[col("tag_0").eq(lit("a"))]);
2315        let batch = new_record_batch_with_custom_sequence(&["a", "x"], 0, 4, 1);
2316
2317        let mask = eval_simple_filter_mask(&batch, &filters[0], "test").unwrap();
2318        assert_eq!(mask.count_set_bits(), 4);
2319    }
2320
2321    #[test]
2322    fn test_eval_simple_filter_mask_errors_on_missing_selected_column() {
2323        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
2324        let filters = new_simple_filter_contexts(&metadata, &[col("tag_0").eq(lit("a"))]);
2325        let pk = new_primary_key(&["a", "x"]);
2326        let batch = new_raw_batch(&[pk.as_slice()], &[10]);
2327
2328        let err = eval_simple_filter_mask(&batch, &filters[0], "test").unwrap_err();
2329        let err = err.to_string();
2330        assert!(err.contains("Prefilter column"));
2331        assert!(err.contains("tag_0"));
2332    }
2333
2334    #[test]
2335    fn test_eval_physical_filter_mask_evaluates_physical_filters() {
2336        let metadata: RegionMetadataRef =
2337            Arc::new(sst_region_metadata_with_encoding(PrimaryKeyEncoding::Dense));
2338        let read_format = FlatReadFormat::new(
2339            metadata.clone(),
2340            ReadColumns::new(metadata.column_metadatas.iter().map(|c| c.column_id)),
2341            None,
2342            "test",
2343            false,
2344        )
2345        .unwrap();
2346        let expr = col("field_0").in_list(vec![lit(11_u64)], false);
2347        let physical_filters = new_physical_filter_contexts(&metadata, &read_format, &[expr]);
2348        let pk = new_primary_key(&["a", "x"]);
2349        let batch = new_raw_batch(&[pk.as_slice(), pk.as_slice(), pk.as_slice()], &[9, 10, 11]);
2350
2351        let mask = eval_physical_filter_mask(&batch, &physical_filters[0], "test").unwrap();
2352        assert_eq!(mask.count_set_bits(), 1);
2353    }
2354
2355    #[test]
2356    fn test_eval_pk_group_mask_finds_pk_column_by_name() {
2357        let metadata = Arc::new(sst_region_metadata());
2358        let filters = Arc::new(new_test_filters(&[col("tag_0").eq(lit("a"))]));
2359        let mut pk_filter = Some(Box::new(CachedPrimaryKeyFilter::new(
2360            build_primary_key_codec(metadata.as_ref()).primary_key_filter(&metadata, filters),
2361        )) as Box<dyn PrimaryKeyFilter>);
2362        let pk_a = new_primary_key(&["a", "x"]);
2363        let pk_b = new_primary_key(&["b", "x"]);
2364        let batch = new_prefilter_batch(
2365            &[
2366                pk_a.as_slice(),
2367                pk_a.as_slice(),
2368                pk_b.as_slice(),
2369                pk_b.as_slice(),
2370            ],
2371            &[10, 11, 12, 13],
2372        );
2373
2374        let mask = primary_key_filter_mask(&batch, pk_filter.as_mut().unwrap().as_mut()).unwrap();
2375
2376        assert_eq!(mask.count_set_bits(), 2);
2377    }
2378
2379    #[test]
2380    fn test_eval_pk_group_mask_handles_binary_pk_column() {
2381        let metadata = Arc::new(sst_region_metadata());
2382        let filters = Arc::new(new_test_filters(&[col("tag_0").eq(lit("a"))]));
2383        let mut pk_filter = Some(Box::new(CachedPrimaryKeyFilter::new(
2384            build_primary_key_codec(metadata.as_ref()).primary_key_filter(&metadata, filters),
2385        )) as Box<dyn PrimaryKeyFilter>);
2386        let pk_a = new_primary_key(&["a", "x"]);
2387        let pk_b = new_primary_key(&["b", "x"]);
2388        let batch = new_prefilter_batch_binary_pk(
2389            &[
2390                pk_a.as_slice(),
2391                pk_a.as_slice(),
2392                pk_b.as_slice(),
2393                pk_b.as_slice(),
2394            ],
2395            &[10, 11, 12, 13],
2396        );
2397
2398        let mask = primary_key_filter_mask(&batch, pk_filter.as_mut().unwrap().as_mut()).unwrap();
2399
2400        assert_eq!(mask.count_set_bits(), 2);
2401    }
2402}