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