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_expr::Expr;
28use datatypes::arrow::array::{Array, BinaryArray, BooleanArray, BooleanBufferBuilder};
29use datatypes::arrow::buffer::BooleanBuffer;
30use datatypes::arrow::datatypes::SchemaRef;
31use datatypes::arrow::record_batch::RecordBatch;
32use futures::StreamExt;
33use mito_codec::row_converter::{PrimaryKeyCodec, PrimaryKeyFilter, build_primary_key_codec};
34use parquet::arrow::ProjectionMask;
35use parquet::arrow::arrow_reader::RowSelection;
36use parquet::schema::types::SchemaDescriptor;
37use smallvec::{SmallVec, smallvec};
38use snafu::{OptionExt, ResultExt};
39use store_api::metadata::{RegionMetadata, RegionMetadataRef};
40use store_api::storage::consts::PRIMARY_KEY_COLUMN_NAME;
41use table::predicate::Predicate;
42
43use crate::cache::PrefilterKey;
44use crate::error::{
45    ComputeArrowSnafu, DecodeSnafu, EvalPartitionFilterSnafu, NewRecordBatchSnafu,
46    RecordBatchSnafu, Result, UnexpectedSnafu,
47};
48use crate::sst::parquet::file_range::PreFilterMode;
49use crate::sst::parquet::flat_format::FlatReadFormat;
50use crate::sst::parquet::format::PrimaryKeyArray;
51use crate::sst::parquet::reader::{
52    MaybeFilter, PhysicalFilterContext, RowGroupBuildContext, RowGroupReaderBuilder,
53    SimpleFilterContext,
54};
55
56pub(crate) fn matching_row_ranges_by_primary_key(
57    input: &RecordBatch,
58    pk_column_index: usize,
59    pk_filter: &mut dyn PrimaryKeyFilter,
60) -> Result<Vec<Range<usize>>> {
61    let pk_column = input.column(pk_column_index);
62    if let Some(pk_dict_array) = pk_column.as_any().downcast_ref::<PrimaryKeyArray>() {
63        matching_row_ranges_from_dict(pk_dict_array, input.num_rows(), pk_filter)
64    } else if let Some(pk_binary_array) = pk_column.as_any().downcast_ref::<BinaryArray>() {
65        matching_row_ranges_from_binary(pk_binary_array, input.num_rows(), pk_filter)
66    } else {
67        UnexpectedSnafu {
68            reason: format!(
69                "Primary key column is neither a dictionary nor a binary array, got {:?}",
70                pk_column.data_type()
71            ),
72        }
73        .fail()
74    }
75}
76
77/// If `pk_filter` matches `pk`, records the row range `start..end`, coalescing
78/// it with the previous range when adjacent.
79fn push_matched_range(
80    matched_row_ranges: &mut Vec<Range<usize>>,
81    pk_filter: &mut dyn PrimaryKeyFilter,
82    pk: &[u8],
83    start: usize,
84    end: usize,
85) -> Result<()> {
86    if pk_filter.matches(pk).context(DecodeSnafu)? {
87        if let Some(last) = matched_row_ranges.last_mut()
88            && last.end == start
89        {
90            last.end = end;
91        } else {
92            matched_row_ranges.push(start..end);
93        }
94    }
95    Ok(())
96}
97
98/// Computes matched row ranges from a dictionary-encoded `__primary_key` column.
99fn matching_row_ranges_from_dict(
100    pk_dict_array: &PrimaryKeyArray,
101    num_rows: usize,
102    pk_filter: &mut dyn PrimaryKeyFilter,
103) -> Result<Vec<Range<usize>>> {
104    let pk_values = pk_dict_array
105        .values()
106        .as_any()
107        .downcast_ref::<BinaryArray>()
108        .context(UnexpectedSnafu {
109            reason: "Primary key values are not binary array",
110        })?;
111    let keys = pk_dict_array.keys();
112    let key_values = keys.values();
113
114    if key_values.is_empty() {
115        return Ok(std::iter::once(0..num_rows).collect());
116    }
117
118    let mut matched_row_ranges: Vec<Range<usize>> = Vec::new();
119    let mut start = 0;
120    while start < key_values.len() {
121        let key = key_values[start];
122        let mut end = start + 1;
123        while end < key_values.len() && key_values[end] == key {
124            end += 1;
125        }
126
127        push_matched_range(
128            &mut matched_row_ranges,
129            pk_filter,
130            pk_values.value(key as usize),
131            start,
132            end,
133        )?;
134
135        start = end;
136    }
137
138    Ok(matched_row_ranges)
139}
140
141/// Computes matched row ranges from a plain binary `__primary_key` column.
142///
143/// The writer falls back to plain binary encoding when the `__primary_key`
144/// chunk exceeds the dictionary page size limit (see `should_read_pk_as_binary`
145/// in the parquet reader), so the prefilter pass must handle this form too.
146fn matching_row_ranges_from_binary(
147    pk_array: &BinaryArray,
148    num_rows: usize,
149    pk_filter: &mut dyn PrimaryKeyFilter,
150) -> Result<Vec<Range<usize>>> {
151    if pk_array.is_empty() {
152        return Ok(std::iter::once(0..num_rows).collect());
153    }
154
155    let mut matched_row_ranges: Vec<Range<usize>> = Vec::new();
156    let mut start = 0;
157    while start < pk_array.len() {
158        let value = pk_array.value(start);
159        let mut end = start + 1;
160        while end < pk_array.len() && pk_array.value(end) == value {
161            end += 1;
162        }
163
164        push_matched_range(&mut matched_row_ranges, pk_filter, value, start, end)?;
165
166        start = end;
167    }
168
169    Ok(matched_row_ranges)
170}
171
172/// Filters a flat-format record batch by primary key, returning only rows whose
173/// primary key matches the filter. Returns `None` if all rows are filtered out.
174pub(crate) fn prefilter_flat_batch_by_primary_key(
175    input: RecordBatch,
176    pk_column_index: usize,
177    pk_filter: &mut dyn PrimaryKeyFilter,
178) -> Result<Option<RecordBatch>> {
179    if input.num_rows() == 0 {
180        return Ok(Some(input));
181    }
182
183    let matched_row_ranges =
184        matching_row_ranges_by_primary_key(&input, pk_column_index, pk_filter)?;
185    if matched_row_ranges.is_empty() {
186        return Ok(None);
187    }
188
189    if matched_row_ranges.len() == 1
190        && matched_row_ranges[0].start == 0
191        && matched_row_ranges[0].end == input.num_rows()
192    {
193        return Ok(Some(input));
194    }
195
196    if matched_row_ranges.len() == 1 {
197        let span = &matched_row_ranges[0];
198        return Ok(Some(input.slice(span.start, span.end - span.start)));
199    }
200
201    let mut builder = BooleanBufferBuilder::new(input.num_rows());
202    builder.append_n(input.num_rows(), false);
203    for span in matched_row_ranges {
204        for i in span {
205            builder.set_bit(i, true);
206        }
207    }
208
209    let filtered = datatypes::arrow::compute::filter_record_batch(
210        &input,
211        &BooleanArray::new(builder.finish(), None),
212    )
213    .context(ComputeArrowSnafu)?;
214    if filtered.num_rows() == 0 {
215        Ok(None)
216    } else {
217        Ok(Some(filtered))
218    }
219}
220
221pub(crate) struct CachedPrimaryKeyFilter {
222    inner: Box<dyn PrimaryKeyFilter>,
223    last_primary_key: Vec<u8>,
224    last_match: Option<bool>,
225}
226
227impl CachedPrimaryKeyFilter {
228    pub(crate) fn new(inner: Box<dyn PrimaryKeyFilter>) -> Self {
229        Self {
230            inner,
231            last_primary_key: Vec::new(),
232            last_match: None,
233        }
234    }
235}
236
237impl PrimaryKeyFilter for CachedPrimaryKeyFilter {
238    fn matches(&mut self, pk: &[u8]) -> mito_codec::error::Result<bool> {
239        if let Some(last_match) = self.last_match
240            && self.last_primary_key == pk
241        {
242            return Ok(last_match);
243        }
244
245        let matched = self.inner.matches(pk)?;
246        self.last_primary_key.clear();
247        self.last_primary_key.extend_from_slice(pk);
248        self.last_match = Some(matched);
249        Ok(matched)
250    }
251}
252
253/// How the bulk-memtable read should apply each predicate.
254///
255/// Unlike the parquet reader, the bulk path has no prefilter pass; predicates
256/// either run row-wise inside the iterator or are pushed down to encoded-PK
257/// matching when the batch still carries the primary-key column.
258pub(crate) struct BulkFilterPlan {
259    /// Simple filters the iterator still has to evaluate row-wise on each batch.
260    pub(crate) remaining_simple_filters: Vec<SimpleFilterContext>,
261    /// Tag predicates lowered to encoded-PK filters. `None` when the batch
262    /// already exposes raw tag columns or there are no tag predicates.
263    pub(crate) pk_filters: Option<Arc<Vec<SimpleFilterEvaluator>>>,
264}
265
266/// Builds an encoded-primary-key filter from the supported tag predicates.
267///
268/// Predicates on fields, timestamps, or unsupported expression shapes are intentionally
269/// omitted. Callers use this as a pruning filter and must preserve the full predicate for
270/// authoritative filtering later in the scan.
271pub(crate) fn build_primary_key_filter(
272    sst_metadata: &RegionMetadataRef,
273    expected_metadata: Option<&RegionMetadata>,
274    predicate: Option<&Predicate>,
275) -> Option<CachedPrimaryKeyFilter> {
276    let filters = simple_tag_filters(sst_metadata, expected_metadata, predicate)
277        .into_iter()
278        .map(|(_, filter)| filter)
279        .collect::<Vec<_>>();
280    if filters.is_empty() {
281        return None;
282    }
283
284    let codec = build_primary_key_codec(sst_metadata.as_ref());
285    let filter = codec.primary_key_filter(sst_metadata, Arc::new(filters));
286    Some(CachedPrimaryKeyFilter::new(filter))
287}
288
289/// Extracts simple tag filters that can be applied to encoded primary keys or series indexes.
290pub(crate) fn simple_tag_filters(
291    sst_metadata: &RegionMetadataRef,
292    expected_metadata: Option<&RegionMetadata>,
293    predicate: Option<&Predicate>,
294) -> Vec<(Expr, SimpleFilterEvaluator)> {
295    predicate
296        .into_iter()
297        .flat_map(|predicate| predicate.exprs())
298        .filter_map(|expr| {
299            SimpleFilterContext::new_opt(sst_metadata, expected_metadata, expr)
300                .map(|filter_ctx| (expr, filter_ctx))
301        })
302        .filter_map(|(expr, filter_ctx)| {
303            (filter_ctx.semantic_type() == SemanticType::Tag)
304                .then(|| {
305                    filter_ctx
306                        .filter()
307                        .as_filter()
308                        .cloned()
309                        .map(|filter| (expr.clone(), filter))
310                })
311                .flatten()
312        })
313        .collect()
314}
315
316/// How the parquet reader should apply each predicate.
317///
318/// The reader runs in two phases. Predicates routed into `prefilter_builder`
319/// execute on a reduced column set first to compute a refined row selection;
320/// `remaining_simple_filters` execute alongside the full projection on the
321/// normal read path. The contract for what is precise vs best-effort is
322/// documented on [`build_reader_filter_plan`].
323pub(crate) struct ReaderFilterPlan {
324    /// Simple filters that must run on the normal read path: predicates with
325    /// `Matched` / `Pruned` outcomes (which carry expected-metadata
326    /// compatibility decisions later phases rely on), and predicates whose
327    /// column cannot be read directly during the prefilter pass.
328    pub(crate) remaining_simple_filters: Vec<SimpleFilterContext>,
329    /// Pre-built state for the prefilter pass, or `None` when prefiltering is
330    /// not worthwhile (no prefilter columns selected, or the prefilter
331    /// projection would cover nearly the full read).
332    pub(crate) prefilter_builder: Option<PrefilterContextBuilder>,
333}
334
335pub(crate) fn build_bulk_filter_plan(
336    read_format: &FlatReadFormat,
337    predicate: Option<&Predicate>,
338) -> BulkFilterPlan {
339    let metadata = read_format.metadata();
340    // Bulk memtable only needs simple binary filters here. Any filter that
341    // cannot be reduced to a SimpleFilterContext stays out of this fast path.
342    let simple_filters: Vec<SimpleFilterContext> = predicate
343        .into_iter()
344        .flat_map(|predicate| {
345            predicate
346                .exprs()
347                .iter()
348                .filter_map(|expr| SimpleFilterContext::new_opt(metadata, None, expr))
349        })
350        .collect();
351
352    // PK prefilter only works when flat batches still carry the encoded PK
353    // column. If tags have already been expanded to raw columns, the iterator
354    // can apply those filters directly and there is nothing to extract here.
355    if read_format.batch_has_raw_pk_columns() || metadata.primary_key.is_empty() {
356        return BulkFilterPlan {
357            remaining_simple_filters: simple_filters,
358            pk_filters: None,
359        };
360    }
361
362    let mut remaining_simple_filters = Vec::new();
363    let mut pk_filters = Vec::new();
364
365    for filter_ctx in simple_filters {
366        // Split tag predicates that can be evaluated against the encoded PK
367        // from filters that still need normal row-wise evaluation later.
368        let pk_filter = filter_ctx.filter().as_filter().and_then(|filter| {
369            (filter_ctx.semantic_type() == SemanticType::Tag).then(|| filter.clone())
370        });
371
372        if let Some(pk_filter) = pk_filter {
373            pk_filters.push(pk_filter);
374        } else {
375            remaining_simple_filters.push(filter_ctx);
376        }
377    }
378
379    BulkFilterPlan {
380        remaining_simple_filters,
381        pk_filters: (!pk_filters.is_empty()).then_some(Arc::new(pk_filters)),
382    }
383}
384
385/// Splits a query [`Predicate`] into a [`ReaderFilterPlan`]: predicates that can run
386/// during the prefilter pass (on a reduced projection, to compute a refined row
387/// selection) versus predicates that must run on the normal read path (alongside the
388/// full projection).
389///
390/// The prefilter pass is *best-effort pruning*: a physical-filter predicate is silently
391/// dropped when [`PhysicalFilterContext::new_opt`] returns `None` (column not in the
392/// projected arrow schema). This is safe because the DataFusion `FilterExec` above the
393/// reader always re-applies the original predicate, so the prefilter pass is purely a
394/// pruning hint.
395///
396/// With predicate prefiltering enabled, tag and timestamp predicates that lower to
397/// [`SimpleFilterEvaluator`] are an exception — the engine enforces them precisely in
398/// the prefilter pass. A caller can postpone simple timestamp filters to the normal
399/// precise-filter path when the scan time range covers the SST. When predicate
400/// prefiltering is disabled, all simple filters remain on the normal path instead.
401pub(crate) fn build_reader_filter_plan(
402    predicate: Option<&Predicate>,
403    expected_metadata: Option<&RegionMetadata>,
404    pre_filter_mode: PreFilterMode,
405    enable_predicate_prefilter: bool,
406    postpone_time_index_filter: bool,
407    read_format: &FlatReadFormat,
408    codec: &Arc<dyn PrimaryKeyCodec>,
409) -> ReaderFilterPlan {
410    let Some(predicate) = predicate else {
411        return ReaderFilterPlan {
412            remaining_simple_filters: Vec::new(),
413            prefilter_builder: None,
414        };
415    };
416
417    let metadata = read_format.metadata();
418    let mut prefilter_simple_filters = Vec::new();
419    let mut remaining_simple_filters = Vec::new();
420    let mut prefilter_physical_filters = Vec::new();
421    let mut primary_key_filters = Vec::new();
422    let mut pk_filter_contexts = Vec::new();
423
424    // `SkipFields` keeps field predicates in the normal read path to avoid a
425    // second read of projected field columns, while tags/timestamp can still
426    // participate in prefiltering.
427    let field_prefilter_enabled = pre_filter_mode == PreFilterMode::All;
428    // When true, tag columns are encoded in the primary key column and are NOT
429    // stored as separate parquet columns. Tag predicates must go through PK
430    // decoding rather than direct column reads.
431    let need_pk_prefilter = !read_format.batch_has_raw_pk_columns();
432
433    // Whether a column can be read directly from parquet for prefiltering,
434    // based on its semantic type and the current mode/format.
435    let can_direct_prefilter = |semantic_type: SemanticType| -> bool {
436        match semantic_type {
437            SemanticType::Tag => !need_pk_prefilter,
438            SemanticType::Field => field_prefilter_enabled,
439            SemanticType::Timestamp => true,
440        }
441    };
442
443    for expr in predicate.exprs() {
444        // Prefer cheap simple filters first. They also preserve `Matched` /
445        // `Pruned` states for columns that only exist in expected metadata.
446        if let Some(filter_ctx) = SimpleFilterContext::new_opt(metadata, expected_metadata, expr) {
447            if !enable_predicate_prefilter {
448                remaining_simple_filters.push(filter_ctx);
449                continue;
450            }
451
452            // `Matched` and `Pruned` come from expected-metadata compatibility
453            // and must stay in the main filter list so later phases keep that
454            // outcome.
455            let Some(filter) = filter_ctx.filter().as_filter() else {
456                remaining_simple_filters.push(filter_ctx);
457                continue;
458            };
459
460            if postpone_time_index_filter && filter_ctx.semantic_type() == SemanticType::Timestamp {
461                remaining_simple_filters.push(filter_ctx);
462                continue;
463            }
464
465            // If the column is stored as a separate parquet column and is already projected in the main read,
466            // we can evaluate the simple filter directly during prefilter.
467            let direct_prefilter = can_direct_prefilter(filter_ctx.semantic_type());
468            if direct_prefilter {
469                assert!(
470                    read_format
471                        .arrow_schema()
472                        .column_with_name(filter.column_name())
473                        .is_some(),
474                    "Column '{}' is not present in the arrow schema {:?}",
475                    filter.column_name(),
476                    read_format.arrow_schema(),
477                );
478                prefilter_simple_filters.push(filter_ctx);
479                continue;
480            }
481
482            // Otherwise try to filter through encoded-PK matching.
483            if need_pk_prefilter && filter_ctx.semantic_type() == SemanticType::Tag {
484                primary_key_filters.push(filter.clone());
485                pk_filter_contexts.push(filter_ctx);
486            } else {
487                remaining_simple_filters.push(filter_ctx);
488            }
489            continue;
490        }
491
492        if !enable_predicate_prefilter {
493            continue;
494        }
495
496        // Best-effort physical-filter prefilter (see fn-level doc): `new_opt`
497        // returning `None` means the column is not in the projected arrow
498        // schema, and dropping the predicate is safe because the upper
499        // `FilterExec` re-applies it.
500        if let Some(filter) =
501            PhysicalFilterContext::new_opt(metadata, expected_metadata, read_format, expr)
502            && can_direct_prefilter(filter.semantic_type())
503        {
504            prefilter_physical_filters.push(filter);
505        }
506    }
507
508    if !enable_predicate_prefilter {
509        return ReaderFilterPlan {
510            remaining_simple_filters,
511            prefilter_builder: None,
512        };
513    }
514
515    let pk_filter_expr_strs = (!pk_filter_contexts.is_empty()).then(|| {
516        let mut expr_strs = pk_filter_contexts
517            .iter()
518            .map(|filter_ctx| filter_ctx.expr_str().to_string())
519            .collect::<Vec<_>>();
520        expr_strs.sort();
521        SmallVec::from_vec(expr_strs)
522    });
523    let pk_filter_exprs =
524        (!primary_key_filters.is_empty()).then_some(Arc::new(primary_key_filters));
525    let schema_version = expected_metadata
526        .map(|metadata| metadata.schema_version)
527        .unwrap_or_else(|| read_format.metadata().schema_version);
528    let prefilter_builder = PrefilterContextBuilder::new(
529        read_format,
530        codec,
531        pk_filter_exprs,
532        pk_filter_expr_strs,
533        prefilter_simple_filters.clone(),
534        prefilter_physical_filters,
535        schema_version,
536    );
537
538    if prefilter_builder.is_some() {
539        ReaderFilterPlan {
540            remaining_simple_filters,
541            prefilter_builder,
542        }
543    } else {
544        // If prefilter setup is not worthwhile, keep the original simple
545        // filters on the normal path so behavior is unchanged.
546        remaining_simple_filters.extend(prefilter_simple_filters);
547        remaining_simple_filters.extend(pk_filter_contexts);
548        ReaderFilterPlan {
549            remaining_simple_filters,
550            prefilter_builder: None,
551        }
552    }
553}
554
555/// Context for prefiltering a row group.
556pub(crate) struct PrefilterContext {
557    /// Optional PK filter for legacy primary-key-format parquet.
558    pk_filter: Option<Box<dyn PrimaryKeyFilter>>,
559    /// Simple filters that can be evaluated directly from the prefilter batch.
560    filters: Vec<SimpleFilterContext>,
561    /// Physical filters that can be evaluated directly from the prefilter batch.
562    /// Physical expressions are only applied in the prefilter phase.
563    physical_filters: Vec<PhysicalFilterContext>,
564    /// Region schema version used in per-filter cache keys.
565    schema_version: u64,
566    /// Sorted expression strings for the encoded-PK filter group.
567    pk_filter_expr_strs: Option<SmallVec<[String; 1]>>,
568    /// Arrow schema used to build narrowed prefilter projections.
569    arrow_schema: SchemaRef,
570}
571
572/// Pre-built state for constructing [PrefilterContext] per row group.
573///
574/// Fields invariant across row groups (projection mask, codec, metadata, filters)
575/// are computed once. A fresh [PrefilterContext] with its own mutable PK filter
576/// is created via [PrefilterContextBuilder::build()] for each row group.
577pub(crate) struct PrefilterContextBuilder {
578    pk_filters: Option<Arc<Vec<SimpleFilterEvaluator>>>,
579    pk_filter_expr_strs: Option<SmallVec<[String; 1]>>,
580    filters: Vec<SimpleFilterContext>,
581    physical_filters: Vec<PhysicalFilterContext>,
582    codec: Arc<dyn PrimaryKeyCodec>,
583    metadata: RegionMetadataRef,
584    schema_version: u64,
585    arrow_schema: SchemaRef,
586}
587
588impl PrefilterContextBuilder {
589    /// Creates a builder if prefiltering is applicable.
590    ///
591    /// Returns `None` if:
592    /// - The read format doesn't use flat layout
593    /// - No prefilter columns are selected
594    /// - Prefilter would read the full projection without any PK filter
595    pub(crate) fn new(
596        read_format: &FlatReadFormat,
597        codec: &Arc<dyn PrimaryKeyCodec>,
598        primary_key_filters: Option<Arc<Vec<SimpleFilterEvaluator>>>,
599        primary_key_filter_expr_strs: Option<SmallVec<[String; 1]>>,
600        filters: Vec<SimpleFilterContext>,
601        physical_filters: Vec<PhysicalFilterContext>,
602        schema_version: u64,
603    ) -> Option<Self> {
604        let metadata = read_format.metadata();
605        let use_raw_tag_columns = read_format.batch_has_raw_pk_columns();
606        let pk_filters = (!use_raw_tag_columns)
607            .then_some(primary_key_filters)
608            .flatten()
609            .filter(|filters| !filters.is_empty());
610        let pk_filter_expr_strs = pk_filters
611            .is_some()
612            .then_some(primary_key_filter_expr_strs)
613            .flatten();
614
615        let mut prefilter_column_names = HashSet::new();
616        for filter_ctx in &filters {
617            if let MaybeFilter::Filter(filter) = filter_ctx.filter() {
618                prefilter_column_names.insert(filter.column_name().to_string());
619            }
620        }
621
622        if pk_filters.is_some() {
623            prefilter_column_names.insert(PRIMARY_KEY_COLUMN_NAME.to_string());
624        }
625
626        for filter_ctx in &physical_filters {
627            prefilter_column_names.insert(filter_ctx.column_name().to_string());
628        }
629
630        let prefilter_count =
631            compute_projection_count(&prefilter_column_names, read_format.arrow_schema());
632
633        if prefilter_count == 0 {
634            return None;
635        }
636
637        let total_count = read_format.parquet_read_columns().root_indices().len();
638        let remaining_count = total_count.saturating_sub(prefilter_count);
639        if pk_filters.is_none() && prefilter_count >= total_count {
640            return None;
641        }
642
643        if pk_filters.is_none()
644            && !should_use_prefilter(prefilter_count, remaining_count, total_count)
645        {
646            return None;
647        }
648
649        Some(Self {
650            pk_filters,
651            pk_filter_expr_strs,
652            filters,
653            physical_filters,
654            codec: Arc::clone(codec),
655            metadata: metadata.clone(),
656            schema_version,
657            arrow_schema: read_format.arrow_schema().clone(),
658        })
659    }
660
661    /// Builds a [PrefilterContext] for a specific row group.
662    pub(crate) fn build(&self) -> PrefilterContext {
663        let pk_filter = self
664            .build_primary_key_filter()
665            .map(|filter| Box::new(filter) as Box<dyn PrimaryKeyFilter>);
666        PrefilterContext {
667            pk_filter,
668            filters: self.filters.clone(),
669            physical_filters: self.physical_filters.clone(),
670            schema_version: self.schema_version,
671            pk_filter_expr_strs: self.pk_filter_expr_strs.clone(),
672            arrow_schema: self.arrow_schema.clone(),
673        }
674    }
675
676    /// Builds a fresh encoded-primary-key filter selected by this plan.
677    pub(crate) fn build_primary_key_filter(&self) -> Option<CachedPrimaryKeyFilter> {
678        self.pk_filters.as_ref().map(|pk_filters| {
679            let filter = self
680                .codec
681                .primary_key_filter(&self.metadata, Arc::clone(pk_filters));
682            CachedPrimaryKeyFilter::new(filter)
683        })
684    }
685}
686
687const PREFILTER_COLUMN_RATIO_THRESHOLD: f64 = 0.5;
688const PREFILTER_MIN_REMAINING_COLUMNS: usize = 2;
689
690/// Result of prefiltering a row group.
691pub(crate) struct PrefilterResult {
692    /// Refined row selection after prefiltering.
693    pub(crate) refined_selection: RowSelection,
694    /// Number of rows filtered out by prefiltering.
695    pub(crate) filtered_rows: usize,
696}
697
698/// Executes prefiltering on a row group.
699///
700/// Reads only the prefilter columns (currently the PK dictionary column),
701/// applies filters, and returns a refined [RowSelection].
702fn compute_projection_mask(
703    column_names: &HashSet<String>,
704    arrow_schema: &datatypes::arrow::datatypes::SchemaRef,
705    parquet_schema: &SchemaDescriptor,
706) -> ProjectionMask {
707    ProjectionMask::roots(
708        parquet_schema,
709        projection_indices(column_names, arrow_schema),
710    )
711}
712
713fn compute_projection_count(
714    column_names: &HashSet<String>,
715    arrow_schema: &datatypes::arrow::datatypes::SchemaRef,
716) -> usize {
717    projection_indices(column_names, arrow_schema).len()
718}
719
720fn projection_indices(
721    column_names: &HashSet<String>,
722    arrow_schema: &datatypes::arrow::datatypes::SchemaRef,
723) -> Vec<usize> {
724    let mut projection_indices: Vec<usize> = column_names
725        .iter()
726        .filter_map(|name| arrow_schema.column_with_name(name).map(|(index, _)| index))
727        .collect();
728    projection_indices.sort_unstable();
729    projection_indices.dedup();
730    projection_indices
731}
732
733fn should_use_prefilter(
734    prefilter_count: usize,
735    remaining_count: usize,
736    total_count: usize,
737) -> bool {
738    if remaining_count == 0 {
739        return false;
740    }
741
742    if remaining_count < PREFILTER_MIN_REMAINING_COLUMNS {
743        return false;
744    }
745
746    let ratio = prefilter_count as f64 / total_count as f64;
747    ratio <= PREFILTER_COLUMN_RATIO_THRESHOLD
748}
749
750pub(crate) async fn execute_prefilter(
751    prefilter_ctx: &mut PrefilterContext,
752    reader_builder: &RowGroupReaderBuilder,
753    build_ctx: &RowGroupBuildContext<'_>,
754) -> Result<PrefilterResult> {
755    let entries = build_prefilter_cache_entries(prefilter_ctx, reader_builder, build_ctx);
756
757    if entries.is_empty() {
758        return execute_prefilter_by_reading_columns(prefilter_ctx, reader_builder, build_ctx)
759            .await;
760    }
761
762    execute_prefilter_with_result_cache(prefilter_ctx, reader_builder, build_ctx, entries).await
763}
764
765async fn execute_prefilter_with_result_cache(
766    prefilter_ctx: &mut PrefilterContext,
767    reader_builder: &RowGroupReaderBuilder,
768    build_ctx: &RowGroupBuildContext<'_>,
769    entries: Vec<PrefilterEntry>,
770) -> Result<PrefilterResult> {
771    let non_cacheable_physical = non_cacheable_physical_filters(prefilter_ctx);
772    let mut hit_mask: Option<BooleanBuffer> = None;
773    let mut misses = Vec::new();
774    for entry in entries {
775        let Some(key) = &entry.key else {
776            misses.push(entry);
777            continue;
778        };
779
780        if let Some(mask) = reader_builder.cache_strategy().get_prefilter_result(key) {
781            hit_mask = Some(match hit_mask {
782                Some(hit_mask) => hit_mask.bitand(mask.as_ref()),
783                None => mask.as_ref().clone(),
784            });
785        } else {
786            misses.push(entry);
787        }
788    }
789
790    if misses.is_empty() && non_cacheable_physical.is_empty() {
791        let combined_mask = hit_mask.unwrap_or_else(|| BooleanBuffer::new_set(0));
792        let refined_selection =
793            refined_selection_from_mask(&combined_mask, &build_ctx.row_selection);
794        let rows_before_filter = rows_before_filter(reader_builder, build_ctx);
795        let filtered_rows = rows_before_filter.saturating_sub(refined_selection.row_count());
796        return Ok(PrefilterResult {
797            refined_selection,
798            filtered_rows,
799        });
800    }
801
802    let mut uncached_entries = misses;
803    uncached_entries.extend(
804        non_cacheable_physical
805            .iter()
806            .copied()
807            .map(|idx| PrefilterEntry::without_cache(PrefilterEntryKind::Physical(idx))),
808    );
809    let (uncached_mask, read_rows) =
810        build_prefilter_masks(prefilter_ctx, reader_builder, build_ctx, &uncached_entries).await?;
811
812    let final_mask = match (hit_mask, uncached_mask) {
813        (Some(hit_mask), Some(uncached_mask)) => hit_mask.bitand(&uncached_mask),
814        (Some(hit_mask), None) => hit_mask,
815        (None, Some(uncached_mask)) => uncached_mask,
816        (None, None) => BooleanBuffer::new_set(read_rows),
817    };
818    debug_assert_eq!(final_mask.len(), read_rows);
819    let rows_selected = final_mask.count_set_bits();
820    let filtered_rows = read_rows.saturating_sub(rows_selected);
821    let refined_selection = refined_selection_from_mask(&final_mask, &build_ctx.row_selection);
822
823    Ok(PrefilterResult {
824        refined_selection,
825        filtered_rows,
826    })
827}
828
829fn non_cacheable_physical_filters(prefilter_ctx: &PrefilterContext) -> Vec<usize> {
830    prefilter_ctx
831        .physical_filters
832        .iter()
833        .enumerate()
834        .filter_map(|(idx, filter)| (!filter.is_immutable()).then_some(idx))
835        .collect()
836}
837
838async fn build_prefilter_masks(
839    prefilter_ctx: &mut PrefilterContext,
840    reader_builder: &RowGroupReaderBuilder,
841    build_ctx: &RowGroupBuildContext<'_>,
842    entries: &[PrefilterEntry],
843) -> Result<(Option<BooleanBuffer>, usize)> {
844    let prefilter_column_names = prefilter_column_names_for_entries(prefilter_ctx, entries);
845    let parquet_schema = reader_builder
846        .parquet_metadata()
847        .file_metadata()
848        .schema_descr();
849    let projection = compute_projection_mask(
850        &prefilter_column_names,
851        &prefilter_ctx.arrow_schema,
852        parquet_schema,
853    );
854
855    let mut stream = reader_builder
856        .build_with_projection(
857            build_ctx.row_group_idx,
858            build_ctx.row_selection.clone(),
859            projection,
860            build_ctx.fetch_metrics,
861        )
862        .await?;
863
864    let mut cache_builders = entries
865        .iter()
866        .map(|entry| entry.key.is_some().then(|| BooleanBufferBuilder::new(0)))
867        .collect::<Vec<_>>();
868    let mut combined_builder = (!entries.is_empty()).then(|| BooleanBufferBuilder::new(0));
869    let mut rows_before_filter = 0usize;
870
871    while let Some(batch_result) = stream.next().await {
872        let batch = batch_result?;
873        let num_rows = batch.num_rows();
874        if num_rows == 0 {
875            continue;
876        }
877        rows_before_filter += num_rows;
878
879        let mut batch_mask = BooleanBuffer::new_set(num_rows);
880        for (idx, entry) in entries.iter().enumerate() {
881            let mask = eval_entry_mask(
882                &batch,
883                prefilter_ctx,
884                entry.kind,
885                reader_builder.file_path(),
886            )?;
887            batch_mask = batch_mask.bitand(&mask);
888            if let Some(Some(builder)) = cache_builders.get_mut(idx) {
889                builder.append_buffer(&mask);
890            }
891        }
892        if let Some(builder) = &mut combined_builder {
893            builder.append_buffer(&batch_mask);
894        }
895    }
896
897    for (entry, builder) in entries.iter().zip(cache_builders) {
898        if let (Some(key), Some(mut builder)) = (&entry.key, builder) {
899            reader_builder
900                .cache_strategy()
901                .put_prefilter_result(key.clone(), Arc::new(builder.finish()));
902        }
903    }
904
905    Ok((
906        combined_builder.map(|mut builder| builder.finish()),
907        rows_before_filter,
908    ))
909}
910
911fn prefilter_column_names_for_entries(
912    prefilter_ctx: &PrefilterContext,
913    entries: &[PrefilterEntry],
914) -> HashSet<String> {
915    let mut prefilter_column_names = HashSet::new();
916    for entry in entries {
917        match entry.kind {
918            PrefilterEntryKind::Simple(idx) => {
919                if let MaybeFilter::Filter(filter) = prefilter_ctx.filters[idx].filter() {
920                    prefilter_column_names.insert(filter.column_name().to_string());
921                }
922            }
923            PrefilterEntryKind::Physical(idx) => {
924                prefilter_column_names.insert(
925                    prefilter_ctx.physical_filters[idx]
926                        .column_name()
927                        .to_string(),
928                );
929            }
930            PrefilterEntryKind::PkGroup => {
931                prefilter_column_names.insert(PRIMARY_KEY_COLUMN_NAME.to_string());
932            }
933        }
934    }
935    prefilter_column_names
936}
937
938async fn execute_prefilter_by_reading_columns(
939    prefilter_ctx: &mut PrefilterContext,
940    reader_builder: &RowGroupReaderBuilder,
941    build_ctx: &RowGroupBuildContext<'_>,
942) -> Result<PrefilterResult> {
943    let entries = all_prefilter_entries(prefilter_ctx);
944    let (mask, rows_before_filter) =
945        build_prefilter_masks(prefilter_ctx, reader_builder, build_ctx, &entries).await?;
946
947    let final_mask = mask.unwrap_or_else(|| BooleanBuffer::new_set(rows_before_filter));
948    let rows_selected = final_mask.count_set_bits();
949    let filtered_rows = rows_before_filter.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 all_prefilter_entries(prefilter_ctx: &PrefilterContext) -> Vec<PrefilterEntry> {
959    let mut entries = Vec::new();
960    if prefilter_ctx.pk_filter.is_some() {
961        entries.push(PrefilterEntry::without_cache(PrefilterEntryKind::PkGroup));
962    }
963    entries.extend(
964        prefilter_ctx
965            .filters
966            .iter()
967            .enumerate()
968            .map(|(idx, _)| PrefilterEntry::without_cache(PrefilterEntryKind::Simple(idx))),
969    );
970    entries.extend(
971        prefilter_ctx
972            .physical_filters
973            .iter()
974            .enumerate()
975            .map(|(idx, _)| PrefilterEntry::without_cache(PrefilterEntryKind::Physical(idx))),
976    );
977    entries
978}
979
980#[derive(Clone, Copy)]
981enum PrefilterEntryKind {
982    Simple(usize),
983    Physical(usize),
984    PkGroup,
985}
986
987struct PrefilterEntry {
988    kind: PrefilterEntryKind,
989    key: Option<PrefilterKey>,
990}
991
992impl PrefilterEntry {
993    fn without_cache(kind: PrefilterEntryKind) -> Self {
994        Self { kind, key: None }
995    }
996}
997
998fn build_prefilter_cache_entries(
999    prefilter_ctx: &PrefilterContext,
1000    reader_builder: &RowGroupReaderBuilder,
1001    build_ctx: &RowGroupBuildContext<'_>,
1002) -> Vec<PrefilterEntry> {
1003    let row_selection = PrefilterKey::row_selection_snapshot(build_ctx.row_selection.as_ref());
1004    let file_id = reader_builder.file_handle().file_id().file_id();
1005    let row_group_idx = build_ctx.row_group_idx as u32;
1006    let mut entries = Vec::new();
1007
1008    for (idx, filter_ctx) in prefilter_ctx.filters.iter().enumerate() {
1009        entries.push(PrefilterEntry {
1010            kind: PrefilterEntryKind::Simple(idx),
1011            key: Some(PrefilterKey::new(
1012                file_id,
1013                row_group_idx,
1014                row_selection.clone(),
1015                prefilter_ctx.schema_version,
1016                smallvec![filter_ctx.expr_str().to_string()],
1017            )),
1018        });
1019    }
1020
1021    for (idx, filter_ctx) in prefilter_ctx.physical_filters.iter().enumerate() {
1022        if !filter_ctx.is_immutable() {
1023            continue;
1024        }
1025        entries.push(PrefilterEntry {
1026            kind: PrefilterEntryKind::Physical(idx),
1027            key: Some(PrefilterKey::new(
1028                file_id,
1029                row_group_idx,
1030                row_selection.clone(),
1031                prefilter_ctx.schema_version,
1032                smallvec![filter_ctx.expr_str().to_string()],
1033            )),
1034        });
1035    }
1036
1037    if prefilter_ctx.pk_filter.is_some()
1038        && let Some(exprs) = &prefilter_ctx.pk_filter_expr_strs
1039    {
1040        entries.push(PrefilterEntry {
1041            kind: PrefilterEntryKind::PkGroup,
1042            key: Some(PrefilterKey::new(
1043                file_id,
1044                row_group_idx,
1045                row_selection,
1046                prefilter_ctx.schema_version,
1047                exprs.clone(),
1048            )),
1049        });
1050    }
1051
1052    entries
1053}
1054
1055fn rows_before_filter(
1056    reader_builder: &RowGroupReaderBuilder,
1057    build_ctx: &RowGroupBuildContext<'_>,
1058) -> usize {
1059    build_ctx.row_selection.as_ref().map_or_else(
1060        || {
1061            reader_builder
1062                .parquet_metadata()
1063                .row_group(build_ctx.row_group_idx)
1064                .num_rows() as usize
1065        },
1066        RowSelection::row_count,
1067    )
1068}
1069
1070fn refined_selection_from_mask(
1071    mask: &BooleanBuffer,
1072    original_selection: &Option<RowSelection>,
1073) -> RowSelection {
1074    if mask.is_empty() || mask.count_set_bits() == 0 {
1075        return RowSelection::from(vec![]);
1076    }
1077
1078    let prefilter_selection = RowSelection::from_filters(&[BooleanArray::from(mask.clone())]);
1079    match original_selection {
1080        Some(original) => original.and_then(&prefilter_selection),
1081        None => prefilter_selection,
1082    }
1083}
1084
1085fn eval_entry_mask(
1086    batch: &RecordBatch,
1087    prefilter_ctx: &mut PrefilterContext,
1088    kind: PrefilterEntryKind,
1089    file_path: &str,
1090) -> Result<BooleanBuffer> {
1091    match kind {
1092        PrefilterEntryKind::Simple(idx) => {
1093            eval_simple_filter_mask(batch, &prefilter_ctx.filters[idx], file_path)
1094        }
1095        PrefilterEntryKind::Physical(idx) => {
1096            eval_physical_filter_mask(batch, &prefilter_ctx.physical_filters[idx], file_path)
1097        }
1098        PrefilterEntryKind::PkGroup => {
1099            let pk_filter = prefilter_ctx.pk_filter.as_mut().context(UnexpectedSnafu {
1100                reason: "Missing primary key filter for prefilter cache entry",
1101            })?;
1102            primary_key_filter_mask(batch, pk_filter.as_mut())
1103        }
1104    }
1105}
1106
1107/// Evaluates an encoded-primary-key filter against a primary-key batch.
1108pub(crate) fn primary_key_filter_mask(
1109    batch: &RecordBatch,
1110    pk_filter: &mut dyn PrimaryKeyFilter,
1111) -> Result<BooleanBuffer> {
1112    let (pk_column_index, _) = batch
1113        .schema()
1114        .column_with_name(PRIMARY_KEY_COLUMN_NAME)
1115        .context(UnexpectedSnafu {
1116            reason: "Primary key column not found in prefilter batch",
1117        })?;
1118    let matched_row_ranges = matching_row_ranges_by_primary_key(batch, pk_column_index, pk_filter)?;
1119    let mut builder = BooleanBufferBuilder::new(batch.num_rows());
1120    builder.append_n(batch.num_rows(), false);
1121    for range in matched_row_ranges {
1122        for row in range {
1123            builder.set_bit(row, true);
1124        }
1125    }
1126    Ok(builder.finish())
1127}
1128
1129fn eval_simple_filter_mask(
1130    batch: &RecordBatch,
1131    filter_ctx: &SimpleFilterContext,
1132    file_path: &str,
1133) -> Result<BooleanBuffer> {
1134    let filter = match filter_ctx.filter() {
1135        MaybeFilter::Filter(filter) => filter,
1136        MaybeFilter::Matched => return Ok(BooleanBuffer::new_set(batch.num_rows())),
1137        MaybeFilter::Pruned => return Ok(BooleanBuffer::new_unset(batch.num_rows())),
1138    };
1139
1140    let (idx, _) = batch
1141        .schema()
1142        .column_with_name(filter.column_name())
1143        .with_context(|| UnexpectedSnafu {
1144            reason: format!(
1145                "Prefilter column '{}' (id {}) not found in batch for file {}",
1146                filter.column_name(),
1147                filter_ctx.column_id(),
1148                file_path
1149            ),
1150        })?;
1151    let column = batch.column(idx).clone();
1152    filter.evaluate_array(&column).context(RecordBatchSnafu)
1153}
1154
1155fn eval_physical_filter_mask(
1156    batch: &RecordBatch,
1157    filter_ctx: &PhysicalFilterContext,
1158    file_path: &str,
1159) -> Result<BooleanBuffer> {
1160    let filter = filter_ctx.filter();
1161
1162    let (idx, _) = batch
1163        .schema()
1164        .column_with_name(filter_ctx.column_name())
1165        .with_context(|| UnexpectedSnafu {
1166            reason: format!(
1167                "Prefilter physical column '{}' (id {}) not found in batch for file {}",
1168                filter_ctx.column_name(),
1169                filter_ctx.column_id(),
1170                file_path
1171            ),
1172        })?;
1173    let column = batch.column(idx).clone();
1174
1175    let record_batch = RecordBatch::try_new(filter_ctx.schema().clone(), vec![column])
1176        .context(NewRecordBatchSnafu)?;
1177    let evaluated = filter
1178        .evaluate(&record_batch)
1179        .context(EvalPartitionFilterSnafu)?;
1180    let array = evaluated
1181        .into_array(record_batch.num_rows())
1182        .context(EvalPartitionFilterSnafu)?;
1183    let boolean_array = array
1184        .as_any()
1185        .downcast_ref::<BooleanArray>()
1186        .context(UnexpectedSnafu {
1187            reason: "Failed to downcast physical filter result to BooleanArray",
1188        })?;
1189    // Treat null results as false (filtered out); value bits are not guaranteed
1190    // to be false for invalid entries.
1191    let mut result = boolean_array.values().clone();
1192    if let Some(nulls) = boolean_array.nulls() {
1193        result = result.bitand(nulls.inner());
1194    }
1195    Ok(result)
1196}
1197
1198#[cfg(test)]
1199mod tests {
1200    use std::sync::Arc;
1201    use std::sync::atomic::{AtomicUsize, Ordering};
1202
1203    use common_recordbatch::filter::SimpleFilterEvaluator;
1204    use datafusion_common::ScalarValue;
1205    use datafusion_expr::{col, lit};
1206    use datatypes::arrow::array::{
1207        ArrayRef, DictionaryArray, TimestampMillisecondArray, UInt8Array, UInt32Array, UInt64Array,
1208    };
1209    use datatypes::arrow::datatypes::{DataType, Field, Schema, UInt32Type};
1210    use mito_codec::row_converter::{PrimaryKeyFilter, build_primary_key_codec};
1211    use store_api::codec::PrimaryKeyEncoding;
1212
1213    use super::*;
1214    use crate::read::read_columns::ReadColumns;
1215    use crate::sst::internal_fields;
1216    use crate::sst::parquet::flat_format::{FlatReadFormat, primary_key_column_index};
1217    use crate::test_util::sst_util::{
1218        new_primary_key, new_record_batch_with_custom_sequence, sst_region_metadata,
1219        sst_region_metadata_with_encoding,
1220    };
1221
1222    struct CountingPrimaryKeyFilter {
1223        hits: Arc<AtomicUsize>,
1224        expected: Vec<u8>,
1225    }
1226
1227    impl PrimaryKeyFilter for CountingPrimaryKeyFilter {
1228        fn matches(&mut self, pk: &[u8]) -> mito_codec::error::Result<bool> {
1229            self.hits.fetch_add(1, Ordering::Relaxed);
1230            Ok(pk == self.expected.as_slice())
1231        }
1232    }
1233
1234    #[test]
1235    fn test_cached_primary_key_filter_reuses_previous_result() {
1236        let expected = new_primary_key(&["a", "x"]);
1237        let hits = Arc::new(AtomicUsize::new(0));
1238        let mut filter = CachedPrimaryKeyFilter::new(Box::new(CountingPrimaryKeyFilter {
1239            hits: Arc::clone(&hits),
1240            expected: expected.clone(),
1241        }));
1242
1243        assert!(filter.matches(expected.as_slice()).unwrap());
1244        assert!(filter.matches(expected.as_slice()).unwrap());
1245        assert!(
1246            !filter
1247                .matches(new_primary_key(&["b", "x"]).as_slice())
1248                .unwrap()
1249        );
1250
1251        assert_eq!(hits.load(Ordering::Relaxed), 2);
1252    }
1253
1254    fn new_test_filters(exprs: &[datafusion_expr::Expr]) -> Vec<SimpleFilterEvaluator> {
1255        exprs
1256            .iter()
1257            .filter_map(SimpleFilterEvaluator::try_new)
1258            .collect()
1259    }
1260
1261    fn new_simple_filter_contexts(
1262        metadata: &RegionMetadataRef,
1263        exprs: &[datafusion_expr::Expr],
1264    ) -> Vec<SimpleFilterContext> {
1265        exprs
1266            .iter()
1267            .filter_map(|expr| SimpleFilterContext::new_opt(metadata, None, expr))
1268            .collect()
1269    }
1270
1271    fn new_physical_filter_contexts(
1272        metadata: &RegionMetadataRef,
1273        read_format: &FlatReadFormat,
1274        exprs: &[datafusion_expr::Expr],
1275    ) -> Vec<PhysicalFilterContext> {
1276        exprs
1277            .iter()
1278            .filter_map(|expr| PhysicalFilterContext::new_opt(metadata, None, read_format, expr))
1279            .collect()
1280    }
1281
1282    fn new_raw_batch(primary_keys: &[&[u8]], field_values: &[u64]) -> RecordBatch {
1283        assert_eq!(primary_keys.len(), field_values.len());
1284
1285        let metadata = Arc::new(sst_region_metadata());
1286        let arrow_schema = metadata.schema.arrow_schema();
1287        let field_column = arrow_schema
1288            .field(arrow_schema.index_of("field_0").unwrap())
1289            .clone();
1290        let time_index_column = arrow_schema
1291            .field(arrow_schema.index_of("ts").unwrap())
1292            .clone();
1293        let mut fields = vec![field_column, time_index_column];
1294        fields.extend(
1295            internal_fields()
1296                .into_iter()
1297                .map(|field| field.as_ref().clone()),
1298        );
1299        let schema = Arc::new(Schema::new(fields));
1300
1301        let mut dict_values = Vec::new();
1302        let mut keys = Vec::with_capacity(primary_keys.len());
1303        for pk in primary_keys {
1304            let key = dict_values
1305                .iter()
1306                .position(|existing: &&[u8]| existing == pk)
1307                .unwrap_or_else(|| {
1308                    dict_values.push(*pk);
1309                    dict_values.len() - 1
1310                });
1311            keys.push(key as u32);
1312        }
1313        let pk_array: ArrayRef = Arc::new(DictionaryArray::<UInt32Type>::new(
1314            UInt32Array::from(keys),
1315            Arc::new(BinaryArray::from_iter_values(dict_values.iter().copied())),
1316        ));
1317
1318        RecordBatch::try_new(
1319            schema,
1320            vec![
1321                Arc::new(UInt64Array::from(field_values.to_vec())),
1322                Arc::new(TimestampMillisecondArray::from_iter_values(
1323                    0..primary_keys.len() as i64,
1324                )),
1325                pk_array,
1326                Arc::new(UInt64Array::from(vec![1; primary_keys.len()])),
1327                Arc::new(UInt8Array::from(vec![1; primary_keys.len()])),
1328            ],
1329        )
1330        .unwrap()
1331    }
1332
1333    fn new_prefilter_batch(primary_keys: &[&[u8]], field_values: &[u64]) -> RecordBatch {
1334        assert_eq!(primary_keys.len(), field_values.len());
1335
1336        let metadata = Arc::new(sst_region_metadata());
1337        let arrow_schema = metadata.schema.arrow_schema();
1338        let field_column = arrow_schema
1339            .field(arrow_schema.index_of("field_0").unwrap())
1340            .clone();
1341        let time_index_column = arrow_schema
1342            .field(arrow_schema.index_of("ts").unwrap())
1343            .clone();
1344        let schema = Arc::new(Schema::new(vec![
1345            field_column,
1346            time_index_column,
1347            internal_fields()[0].as_ref().clone(),
1348        ]));
1349
1350        let mut dict_values = Vec::new();
1351        let mut keys = Vec::with_capacity(primary_keys.len());
1352        for pk in primary_keys {
1353            let key = dict_values
1354                .iter()
1355                .position(|existing: &&[u8]| existing == pk)
1356                .unwrap_or_else(|| {
1357                    dict_values.push(*pk);
1358                    dict_values.len() - 1
1359                });
1360            keys.push(key as u32);
1361        }
1362        let pk_array: ArrayRef = Arc::new(DictionaryArray::<UInt32Type>::new(
1363            UInt32Array::from(keys),
1364            Arc::new(BinaryArray::from_iter_values(dict_values.iter().copied())),
1365        ));
1366
1367        RecordBatch::try_new(
1368            schema,
1369            vec![
1370                Arc::new(UInt64Array::from(field_values.to_vec())),
1371                Arc::new(TimestampMillisecondArray::from_iter_values(
1372                    0..primary_keys.len() as i64,
1373                )),
1374                pk_array,
1375            ],
1376        )
1377        .unwrap()
1378    }
1379
1380    fn new_prefilter_batch_binary_pk(primary_keys: &[&[u8]], field_values: &[u64]) -> RecordBatch {
1381        assert_eq!(primary_keys.len(), field_values.len());
1382
1383        let metadata = Arc::new(sst_region_metadata());
1384        let arrow_schema = metadata.schema.arrow_schema();
1385        let field_column = arrow_schema
1386            .field(arrow_schema.index_of("field_0").unwrap())
1387            .clone();
1388        let time_index_column = arrow_schema
1389            .field(arrow_schema.index_of("ts").unwrap())
1390            .clone();
1391        let schema = Arc::new(Schema::new(vec![
1392            field_column,
1393            time_index_column,
1394            Field::new(PRIMARY_KEY_COLUMN_NAME, DataType::Binary, false),
1395        ]));
1396
1397        let pk_array: ArrayRef =
1398            Arc::new(BinaryArray::from_iter_values(primary_keys.iter().copied()));
1399
1400        RecordBatch::try_new(
1401            schema,
1402            vec![
1403                Arc::new(UInt64Array::from(field_values.to_vec())),
1404                Arc::new(TimestampMillisecondArray::from_iter_values(
1405                    0..primary_keys.len() as i64,
1406                )),
1407                pk_array,
1408            ],
1409        )
1410        .unwrap()
1411    }
1412
1413    fn field_values(batch: &RecordBatch) -> Vec<u64> {
1414        batch
1415            .column(0)
1416            .as_any()
1417            .downcast_ref::<UInt64Array>()
1418            .unwrap()
1419            .values()
1420            .to_vec()
1421    }
1422
1423    fn remaining_simple_filter_columns(filters: &[SimpleFilterContext]) -> Vec<&str> {
1424        filters
1425            .iter()
1426            .map(|filter_ctx| filter_ctx.filter().as_filter().unwrap().column_name())
1427            .collect()
1428    }
1429
1430    #[test]
1431    fn test_prefilter_primary_key_drops_single_dictionary_batch() {
1432        let metadata = Arc::new(sst_region_metadata());
1433        let filters = Arc::new(new_test_filters(&[col("tag_0").eq(lit("b"))]));
1434        let mut primary_key_filter =
1435            build_primary_key_codec(metadata.as_ref()).primary_key_filter(&metadata, filters);
1436        let pk_a = new_primary_key(&["a", "x"]);
1437        let batch = new_raw_batch(&[pk_a.as_slice(), pk_a.as_slice()], &[10, 11]);
1438        let pk_col_idx = primary_key_column_index(batch.num_columns());
1439
1440        let filtered =
1441            prefilter_flat_batch_by_primary_key(batch, pk_col_idx, primary_key_filter.as_mut())
1442                .unwrap();
1443
1444        assert!(filtered.is_none());
1445    }
1446
1447    #[test]
1448    fn test_prefilter_primary_key_builds_mask_for_fragmented_matches() {
1449        let metadata = Arc::new(sst_region_metadata());
1450        let filters = Arc::new(new_test_filters(&[col("tag_0")
1451            .eq(lit("a"))
1452            .or(col("tag_0").eq(lit("c")))]));
1453        let mut primary_key_filter =
1454            build_primary_key_codec(metadata.as_ref()).primary_key_filter(&metadata, filters);
1455        let pk_a = new_primary_key(&["a", "x"]);
1456        let pk_b = new_primary_key(&["b", "x"]);
1457        let pk_c = new_primary_key(&["c", "x"]);
1458        let pk_d = new_primary_key(&["d", "x"]);
1459        let batch = new_raw_batch(
1460            &[
1461                pk_a.as_slice(),
1462                pk_a.as_slice(),
1463                pk_b.as_slice(),
1464                pk_b.as_slice(),
1465                pk_c.as_slice(),
1466                pk_c.as_slice(),
1467                pk_d.as_slice(),
1468                pk_d.as_slice(),
1469            ],
1470            &[10, 11, 12, 13, 14, 15, 16, 17],
1471        );
1472        let pk_col_idx = primary_key_column_index(batch.num_columns());
1473
1474        let filtered =
1475            prefilter_flat_batch_by_primary_key(batch, pk_col_idx, primary_key_filter.as_mut())
1476                .unwrap()
1477                .unwrap();
1478
1479        assert_eq!(filtered.num_rows(), 4);
1480        assert_eq!(field_values(&filtered), vec![10, 11, 14, 15]);
1481    }
1482
1483    #[test]
1484    fn test_prefilter_builder_returns_none_without_selected_filters() {
1485        let metadata: RegionMetadataRef =
1486            Arc::new(sst_region_metadata_with_encoding(PrimaryKeyEncoding::Dense));
1487        let read_format = FlatReadFormat::new(
1488            metadata.clone(),
1489            ReadColumns::new(metadata.column_metadatas.iter().map(|c| c.column_id)),
1490            None,
1491            "test",
1492            false,
1493        )
1494        .unwrap();
1495        let codec = build_primary_key_codec(metadata.as_ref());
1496
1497        let builder = PrefilterContextBuilder::new(
1498            &read_format,
1499            &codec,
1500            None,
1501            None,
1502            Vec::new(),
1503            Vec::new(),
1504            metadata.schema_version,
1505        );
1506        assert!(builder.is_none());
1507    }
1508
1509    #[test]
1510    fn test_should_use_prefilter() {
1511        assert!(should_use_prefilter(1, 5, 6));
1512        assert!(!should_use_prefilter(1, 0, 1));
1513        assert!(!should_use_prefilter(1, 1, 2));
1514        assert!(!should_use_prefilter(4, 3, 7));
1515        assert!(should_use_prefilter(3, 3, 6));
1516    }
1517
1518    #[test]
1519    fn test_build_bulk_filter_plan_classifies_filters_across_read_paths() {
1520        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata_with_encoding(
1521            PrimaryKeyEncoding::Sparse,
1522        ));
1523        let legacy_read_format = FlatReadFormat::new(
1524            metadata.clone(),
1525            ReadColumns::new(metadata.column_metadatas.iter().map(|c| c.column_id)),
1526            None,
1527            "memtable",
1528            false,
1529        )
1530        .unwrap();
1531        assert!(!legacy_read_format.batch_has_raw_pk_columns());
1532
1533        let plan = build_bulk_filter_plan(
1534            &legacy_read_format,
1535            Some(&Predicate::new(vec![
1536                col("tag_0").eq(lit("a")),
1537                col("field_0").gt(lit(1_u64)),
1538            ])),
1539        );
1540        assert_eq!(
1541            plan.pk_filters.as_ref().map(|filters| filters.len()),
1542            Some(1)
1543        );
1544        assert_eq!(
1545            remaining_simple_filter_columns(&plan.remaining_simple_filters),
1546            vec!["field_0"]
1547        );
1548
1549        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
1550        let raw_pk_read_format = FlatReadFormat::new(
1551            metadata.clone(),
1552            ReadColumns::new(metadata.column_metadatas.iter().map(|c| c.column_id)),
1553            None,
1554            "memtable",
1555            true,
1556        )
1557        .unwrap();
1558        assert!(raw_pk_read_format.batch_has_raw_pk_columns());
1559
1560        let tag_only_plan = build_bulk_filter_plan(
1561            &raw_pk_read_format,
1562            Some(&Predicate::new(vec![col("tag_0").eq(lit("a"))])),
1563        );
1564        assert!(tag_only_plan.pk_filters.is_none());
1565        assert_eq!(
1566            remaining_simple_filter_columns(&tag_only_plan.remaining_simple_filters),
1567            vec!["tag_0"]
1568        );
1569
1570        let field_only_plan = build_bulk_filter_plan(
1571            &raw_pk_read_format,
1572            Some(&Predicate::new(vec![col("field_0").gt(lit(1_u64))])),
1573        );
1574        assert!(field_only_plan.pk_filters.is_none());
1575        assert_eq!(
1576            remaining_simple_filter_columns(&field_only_plan.remaining_simple_filters),
1577            vec!["field_0"]
1578        );
1579    }
1580
1581    #[test]
1582    fn test_build_reader_filter_plan_classifies_filters_for_prefilter_modes() {
1583        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
1584        let full_read_format = FlatReadFormat::new(
1585            metadata.clone(),
1586            ReadColumns::new(metadata.column_metadatas.iter().map(|c| c.column_id)),
1587            None,
1588            "test",
1589            true,
1590        )
1591        .unwrap();
1592        let codec = build_primary_key_codec(metadata.as_ref());
1593
1594        let skip_fields_plan = build_reader_filter_plan(
1595            Some(&Predicate::new(vec![
1596                col("tag_0").eq(lit("a")),
1597                col("field_0").gt(lit(1_u64)),
1598            ])),
1599            None,
1600            PreFilterMode::SkipFields,
1601            true,
1602            false,
1603            &full_read_format,
1604            &codec,
1605        );
1606        assert!(skip_fields_plan.prefilter_builder.is_some());
1607        assert_eq!(
1608            remaining_simple_filter_columns(&skip_fields_plan.remaining_simple_filters),
1609            vec!["field_0"]
1610        );
1611
1612        let postponed_time_plan = build_reader_filter_plan(
1613            Some(&Predicate::new(vec![
1614                col("tag_0").eq(lit("a")),
1615                col("field_0").gt(lit(1_u64)),
1616                col("ts").gt_eq(lit(ScalarValue::TimestampMillisecond(Some(1), None))),
1617            ])),
1618            None,
1619            PreFilterMode::SkipFields,
1620            true,
1621            true,
1622            &full_read_format,
1623            &codec,
1624        );
1625        assert!(postponed_time_plan.prefilter_builder.is_some());
1626        assert_eq!(
1627            remaining_simple_filter_columns(&postponed_time_plan.remaining_simple_filters),
1628            vec!["field_0", "ts"]
1629        );
1630
1631        let postponed_time_only_plan = build_reader_filter_plan(
1632            Some(&Predicate::new(vec![col("ts").gt_eq(lit(
1633                ScalarValue::TimestampMillisecond(Some(1), None),
1634            ))])),
1635            None,
1636            PreFilterMode::All,
1637            true,
1638            true,
1639            &full_read_format,
1640            &codec,
1641        );
1642        assert!(postponed_time_only_plan.prefilter_builder.is_none());
1643        assert_eq!(
1644            remaining_simple_filter_columns(&postponed_time_only_plan.remaining_simple_filters),
1645            vec!["ts"]
1646        );
1647
1648        let metric_metadata: RegionMetadataRef = Arc::new(sst_region_metadata_with_encoding(
1649            PrimaryKeyEncoding::Sparse,
1650        ));
1651        let field_0 = metric_metadata.column_by_name("field_0").unwrap().column_id;
1652        let ts = metric_metadata.time_index_column().column_id;
1653        let projected_read_format = FlatReadFormat::new(
1654            metric_metadata.clone(),
1655            ReadColumns::new([field_0, ts]),
1656            None,
1657            "test",
1658            true,
1659        )
1660        .unwrap();
1661        let metric_codec = build_primary_key_codec(metric_metadata.as_ref());
1662        let pk_prefilter_plan = build_reader_filter_plan(
1663            Some(&Predicate::new(vec![col("tag_0").eq(lit("a"))])),
1664            None,
1665            PreFilterMode::All,
1666            true,
1667            false,
1668            &projected_read_format,
1669            &metric_codec,
1670        );
1671        assert!(pk_prefilter_plan.prefilter_builder.is_some());
1672        assert!(
1673            pk_prefilter_plan
1674                .prefilter_builder
1675                .as_ref()
1676                .unwrap()
1677                .build_primary_key_filter()
1678                .is_some()
1679        );
1680        assert!(pk_prefilter_plan.remaining_simple_filters.is_empty());
1681
1682        let disabled_plan = build_reader_filter_plan(
1683            Some(&Predicate::new(vec![
1684                col("tag_0").eq(lit("a")),
1685                col("field_0").gt(lit(1_u64)),
1686                col("ts").gt_eq(lit(ScalarValue::TimestampMillisecond(Some(1), None))),
1687            ])),
1688            None,
1689            PreFilterMode::All,
1690            false,
1691            true,
1692            &projected_read_format,
1693            &metric_codec,
1694        );
1695        assert!(disabled_plan.prefilter_builder.is_none());
1696        assert_eq!(
1697            remaining_simple_filter_columns(&disabled_plan.remaining_simple_filters),
1698            vec!["tag_0", "field_0", "ts"]
1699        );
1700    }
1701
1702    #[test]
1703    fn test_pk_filter_expr_strings_are_stable_under_expr_order() {
1704        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata_with_encoding(
1705            PrimaryKeyEncoding::Sparse,
1706        ));
1707        let read_format = FlatReadFormat::new(
1708            metadata.clone(),
1709            ReadColumns::new(metadata.column_metadatas.iter().map(|c| c.column_id)),
1710            None,
1711            "test",
1712            false,
1713        )
1714        .unwrap();
1715        let codec = build_primary_key_codec(metadata.as_ref());
1716
1717        let expr_a = col("tag_0").eq(lit("a"));
1718        let expr_b = col("tag_1").eq(lit("x"));
1719        let plan_ab = build_reader_filter_plan(
1720            Some(&Predicate::new(vec![expr_a.clone(), expr_b.clone()])),
1721            None,
1722            PreFilterMode::All,
1723            true,
1724            false,
1725            &read_format,
1726            &codec,
1727        );
1728        let plan_b_a = build_reader_filter_plan(
1729            Some(&Predicate::new(vec![expr_b, expr_a])),
1730            None,
1731            PreFilterMode::All,
1732            true,
1733            false,
1734            &read_format,
1735            &codec,
1736        );
1737
1738        let exprs_ab = plan_ab.prefilter_builder.unwrap().pk_filter_expr_strs;
1739        let exprs_b_a = plan_b_a.prefilter_builder.unwrap().pk_filter_expr_strs;
1740        assert!(exprs_ab.is_some());
1741        assert_eq!(exprs_ab, exprs_b_a);
1742    }
1743
1744    #[test]
1745    fn test_simple_and_physical_contexts_preserve_expr_strings() {
1746        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
1747        let read_format = FlatReadFormat::new(
1748            metadata.clone(),
1749            ReadColumns::new(metadata.column_metadatas.iter().map(|c| c.column_id)),
1750            None,
1751            "test",
1752            true,
1753        )
1754        .unwrap();
1755
1756        let simple_expr = col("tag_0").eq(lit("a"));
1757        let simple = SimpleFilterContext::new_opt(&metadata, None, &simple_expr).unwrap();
1758        assert_eq!(simple.expr_str(), format!("{simple_expr:?}"));
1759
1760        let physical_expr = col("field_0").in_list(vec![lit(1_u64), lit(2_u64)], false);
1761        let physical =
1762            PhysicalFilterContext::new_opt(&metadata, None, &read_format, &physical_expr).unwrap();
1763        assert_eq!(physical.expr_str(), format!("{physical_expr:?}"));
1764    }
1765
1766    #[test]
1767    fn test_eval_simple_filter_mask_uses_flat_tag_columns_directly() {
1768        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
1769        let filters = new_simple_filter_contexts(&metadata, &[col("tag_0").eq(lit("a"))]);
1770        let batch = new_record_batch_with_custom_sequence(&["a", "x"], 0, 4, 1);
1771
1772        let mask = eval_simple_filter_mask(&batch, &filters[0], "test").unwrap();
1773        assert_eq!(mask.count_set_bits(), 4);
1774    }
1775
1776    #[test]
1777    fn test_eval_simple_filter_mask_errors_on_missing_selected_column() {
1778        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
1779        let filters = new_simple_filter_contexts(&metadata, &[col("tag_0").eq(lit("a"))]);
1780        let pk = new_primary_key(&["a", "x"]);
1781        let batch = new_raw_batch(&[pk.as_slice()], &[10]);
1782
1783        let err = eval_simple_filter_mask(&batch, &filters[0], "test").unwrap_err();
1784        let err = err.to_string();
1785        assert!(err.contains("Prefilter column"));
1786        assert!(err.contains("tag_0"));
1787    }
1788
1789    #[test]
1790    fn test_eval_physical_filter_mask_evaluates_physical_filters() {
1791        let metadata: RegionMetadataRef =
1792            Arc::new(sst_region_metadata_with_encoding(PrimaryKeyEncoding::Dense));
1793        let read_format = FlatReadFormat::new(
1794            metadata.clone(),
1795            ReadColumns::new(metadata.column_metadatas.iter().map(|c| c.column_id)),
1796            None,
1797            "test",
1798            false,
1799        )
1800        .unwrap();
1801        let expr = col("field_0").in_list(vec![lit(11_u64)], false);
1802        let physical_filters = new_physical_filter_contexts(&metadata, &read_format, &[expr]);
1803        let pk = new_primary_key(&["a", "x"]);
1804        let batch = new_raw_batch(&[pk.as_slice(), pk.as_slice(), pk.as_slice()], &[9, 10, 11]);
1805
1806        let mask = eval_physical_filter_mask(&batch, &physical_filters[0], "test").unwrap();
1807        assert_eq!(mask.count_set_bits(), 1);
1808    }
1809
1810    #[test]
1811    fn test_eval_pk_group_mask_finds_pk_column_by_name() {
1812        let metadata = Arc::new(sst_region_metadata());
1813        let filters = Arc::new(new_test_filters(&[col("tag_0").eq(lit("a"))]));
1814        let mut pk_filter = Some(Box::new(CachedPrimaryKeyFilter::new(
1815            build_primary_key_codec(metadata.as_ref()).primary_key_filter(&metadata, filters),
1816        )) as Box<dyn PrimaryKeyFilter>);
1817        let pk_a = new_primary_key(&["a", "x"]);
1818        let pk_b = new_primary_key(&["b", "x"]);
1819        let batch = new_prefilter_batch(
1820            &[
1821                pk_a.as_slice(),
1822                pk_a.as_slice(),
1823                pk_b.as_slice(),
1824                pk_b.as_slice(),
1825            ],
1826            &[10, 11, 12, 13],
1827        );
1828
1829        let mask = primary_key_filter_mask(&batch, pk_filter.as_mut().unwrap().as_mut()).unwrap();
1830
1831        assert_eq!(mask.count_set_bits(), 2);
1832    }
1833
1834    #[test]
1835    fn test_eval_pk_group_mask_handles_binary_pk_column() {
1836        let metadata = Arc::new(sst_region_metadata());
1837        let filters = Arc::new(new_test_filters(&[col("tag_0").eq(lit("a"))]));
1838        let mut pk_filter = Some(Box::new(CachedPrimaryKeyFilter::new(
1839            build_primary_key_codec(metadata.as_ref()).primary_key_filter(&metadata, filters),
1840        )) as Box<dyn PrimaryKeyFilter>);
1841        let pk_a = new_primary_key(&["a", "x"]);
1842        let pk_b = new_primary_key(&["b", "x"]);
1843        let batch = new_prefilter_batch_binary_pk(
1844            &[
1845                pk_a.as_slice(),
1846                pk_a.as_slice(),
1847                pk_b.as_slice(),
1848                pk_b.as_slice(),
1849            ],
1850            &[10, 11, 12, 13],
1851        );
1852
1853        let mask = primary_key_filter_mask(&batch, pk_filter.as_mut().unwrap().as_mut()).unwrap();
1854
1855        assert_eq!(mask.count_set_bits(), 2);
1856    }
1857}