Skip to main content

mito2/sst/parquet/
file_range.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//! Structs and functions for reading ranges from a parquet file. A file range
16//! is usually a row group in a parquet file.
17
18use std::collections::HashMap;
19use std::ops::BitAnd;
20use std::sync::Arc;
21
22use api::v1::{OpType, SemanticType};
23use common_telemetry::error;
24use datafusion::physical_plan::PhysicalExpr;
25use datafusion::physical_plan::expressions::DynamicFilterPhysicalExpr;
26use datatypes::arrow::array::{Array as _, ArrayRef, BooleanArray};
27use datatypes::arrow::buffer::BooleanBuffer;
28use datatypes::arrow::record_batch::RecordBatch;
29use datatypes::schema::Schema;
30use futures::StreamExt;
31use mito_codec::row_converter::PrimaryKeyCodec;
32use parquet::arrow::arrow_reader::RowSelection;
33use parquet::file::metadata::ParquetMetaData;
34use parquet::file::statistics::Statistics;
35use snafu::{OptionExt, ResultExt};
36use store_api::codec::PrimaryKeyEncoding;
37use store_api::metadata::RegionMetadataRef;
38use store_api::storage::{ColumnId, TimeSeriesRowSelector};
39use table::predicate::Predicate;
40
41use crate::cache::CacheStrategy;
42use crate::error::{
43    ComputeArrowSnafu, DecodeStatsSnafu, EvalPartitionFilterSnafu, NewRecordBatchSnafu,
44    RecordBatchSnafu, Result, StatsNotPresentSnafu, UnexpectedSnafu,
45};
46use crate::read::compat::FlatCompatBatch;
47use crate::read::flat_projection::CompactionProjectionMapper;
48use crate::read::last_row::FlatRowGroupLastRowCachedReader;
49use crate::read::prune::FlatPruneReader;
50use crate::sst::file::FileHandle;
51use crate::sst::parquet::flat_format::{
52    DecodedPrimaryKeys, FlatReadFormat, decode_primary_keys, primary_key_column_index,
53    time_index_column_index,
54};
55use crate::sst::parquet::json_align::ProjectedRecordBatchStream;
56use crate::sst::parquet::prefilter::primary_key_filter_mask;
57use crate::sst::parquet::reader::{
58    FlatRowGroupReader, MaybeFilter, RowGroupBuildContext, RowGroupReaderBuilder,
59    SimpleFilterContext,
60};
61use crate::sst::parquet::row_group::ParquetFetchMetrics;
62use crate::sst::parquet::stats::RowGroupPruningStats;
63
64/// Checks if a row group contains delete operations by examining the min value of op_type column.
65///
66/// Returns `Ok(true)` if the row group contains delete operations, `Ok(false)` if it doesn't,
67/// or an error if the statistics are not present or cannot be decoded.
68pub(crate) fn row_group_contains_delete(
69    parquet_meta: &ParquetMetaData,
70    row_group_index: usize,
71    file_path: &str,
72) -> Result<bool> {
73    let row_group_metadata = &parquet_meta.row_groups()[row_group_index];
74
75    // safety: The last column of SST must be op_type
76    let column_metadata = &row_group_metadata.columns().last().unwrap();
77    let stats = column_metadata
78        .statistics()
79        .context(StatsNotPresentSnafu { file_path })?;
80    stats
81        .min_bytes_opt()
82        .context(StatsNotPresentSnafu { file_path })?
83        .try_into()
84        .map(i32::from_le_bytes)
85        .map(|min_op_type| min_op_type == OpType::Delete as i32)
86        .ok()
87        .context(DecodeStatsSnafu { file_path })
88}
89
90/// A range of a parquet SST. Now it is a row group.
91/// We can read different file ranges in parallel.
92#[derive(Clone)]
93pub struct FileRange {
94    /// Shared context.
95    context: FileRangeContextRef,
96    /// Index of the row group in the SST.
97    row_group_idx: usize,
98    /// Row selection for the row group. `None` means all rows.
99    row_selection: Option<RowSelection>,
100}
101
102impl FileRange {
103    /// Returns the region metadata stored in this SST.
104    pub(crate) fn region_metadata(&self) -> &RegionMetadataRef {
105        self.context.read_format().metadata()
106    }
107
108    /// Returns encoded primary-key min/max statistics for this row group.
109    pub(crate) fn primary_key_range(&self) -> Option<(&[u8], &[u8])> {
110        let metadata = self.context.reader_builder.parquet_metadata();
111        let num_columns = metadata.file_metadata().schema_descr().num_columns();
112        let primary_key_index = primary_key_column_index(num_columns);
113        match metadata
114            .row_group(self.row_group_idx)
115            .column(primary_key_index)
116            .statistics()?
117        {
118            Statistics::ByteArray(statistics) => {
119                Some((statistics.min_bytes_opt()?, statistics.max_bytes_opt()?))
120            }
121            _ => None,
122        }
123    }
124
125    /// Creates a new [FileRange].
126    pub(crate) fn new(
127        context: FileRangeContextRef,
128        row_group_idx: usize,
129        row_selection: Option<RowSelection>,
130    ) -> Self {
131        Self {
132            context,
133            row_group_idx,
134            row_selection,
135        }
136    }
137
138    /// Returns true if [FileRange] selects all rows in row group.
139    fn select_all(&self) -> bool {
140        let rows_in_group = self
141            .context
142            .reader_builder
143            .parquet_metadata()
144            .row_group(self.row_group_idx)
145            .num_rows();
146
147        let Some(row_selection) = &self.row_selection else {
148            return true;
149        };
150        row_selection.row_count() == rows_in_group as usize
151    }
152
153    /// Performs pruning before reading the [FileRange].
154    /// It use latest dynamic filters with row group statistics to prune the range.
155    ///
156    /// Returns false if the entire range is pruned and can be skipped.
157    fn in_dynamic_filter_range(&self) -> bool {
158        if self.context.base.dyn_filters.is_empty() {
159            return true;
160        }
161        let curr_row_group = self
162            .context
163            .reader_builder
164            .parquet_metadata()
165            .row_group(self.row_group_idx);
166        let read_format = self.context.read_format();
167        let prune_schema = &self.context.base.prune_schema;
168        let stats = RowGroupPruningStats::new(
169            std::slice::from_ref(curr_row_group),
170            read_format,
171            self.context.base.expected_metadata.clone(),
172            self.context.base.pre_filter_mode.skip_fields(),
173        );
174
175        // not costly to create a predicate here since dynamic filters are wrapped in Arc
176        let pred = Predicate::with_dyn_filters(vec![], self.context.base.dyn_filters.clone());
177
178        pred.prune_with_stats(&stats, prune_schema.arrow_schema())
179            .first()
180            .cloned()
181            .unwrap_or(true) // unexpected, not skip just in case
182    }
183
184    /// Creates a flat reader that returns RecordBatch.
185    pub async fn flat_reader(
186        &self,
187        selector: Option<TimeSeriesRowSelector>,
188        fetch_metrics: Option<&ParquetFetchMetrics>,
189    ) -> Result<Option<FlatPruneReader>> {
190        if !self.in_dynamic_filter_range() {
191            return Ok(None);
192        }
193        // Compute skip_fields once for this row group
194        let skip_fields = self.context.base.pre_filter_mode.skip_fields();
195        let parquet_reader = self
196            .context
197            .reader_builder
198            .build(self.context.build_context(
199                self.row_group_idx,
200                self.row_selection.clone(),
201                fetch_metrics,
202            ))
203            .await?;
204
205        let use_last_row_reader = if selector
206            .map(|s| s == TimeSeriesRowSelector::LastRow)
207            .unwrap_or(false)
208        {
209            // Only use LastRowReader if row group does not contain DELETE
210            // and all rows are selected.
211            let put_only = !self
212                .context
213                .contains_delete(self.row_group_idx)
214                .inspect_err(|e| {
215                    error!(e; "Failed to decode min value of op_type, fallback to FlatRowGroupReader");
216                })
217                .unwrap_or(true);
218            put_only && self.select_all()
219        } else {
220            false
221        };
222
223        let flat_prune_reader = if use_last_row_reader {
224            let flat_row_group_reader =
225                FlatRowGroupReader::new(self.context.clone(), parquet_reader);
226            // Flat PK prefilter makes the input stream predicate-dependent, so cached
227            // selector results are not reusable across queries with different filters.
228            let cache_strategy = if self.context.reader_builder.has_predicate_prefilter() {
229                CacheStrategy::Disabled
230            } else {
231                self.context.reader_builder.cache_strategy().clone()
232            };
233            let reader = FlatRowGroupLastRowCachedReader::new(
234                self.file_handle().file_id().file_id(),
235                self.row_group_idx,
236                cache_strategy,
237                self.context.read_format().parquet_read_columns(),
238                flat_row_group_reader,
239            );
240            FlatPruneReader::new_with_last_row_reader(self.context.clone(), reader, skip_fields)
241        } else {
242            let flat_row_group_reader =
243                FlatRowGroupReader::new(self.context.clone(), parquet_reader);
244            FlatPruneReader::new_with_row_group_reader(
245                self.context.clone(),
246                flat_row_group_reader,
247                skip_fields,
248            )
249        };
250
251        Ok(Some(flat_prune_reader))
252    }
253
254    /// Creates a reader that returns only the encoded primary-key column.
255    ///
256    /// The returned primary keys are compatible with the expected region metadata.
257    pub(crate) async fn primary_key_reader(
258        &self,
259        fetch_metrics: Option<&ParquetFetchMetrics>,
260    ) -> Result<Option<ProjectedRecordBatchStream>> {
261        self.primary_key_reader_inner(fetch_metrics, true).await
262    }
263
264    async fn primary_key_reader_inner(
265        &self,
266        fetch_metrics: Option<&ParquetFetchMetrics>,
267        check_dynamic_filter: bool,
268    ) -> Result<Option<ProjectedRecordBatchStream>> {
269        if check_dynamic_filter && !self.in_dynamic_filter_range() {
270            return Ok(None);
271        }
272        let stream = self
273            .context
274            .reader_builder
275            .build_primary_key(self.context.build_context(
276                self.row_group_idx,
277                self.row_selection.clone(),
278                fetch_metrics,
279            ))
280            .await?;
281        if self.context.compat_batch().is_none() {
282            return Ok(Some(stream));
283        }
284
285        let context = self.context.clone();
286        let stream = stream
287            .map(move |batch| {
288                let batch = batch?;
289                let compat = context.compat_batch().context(UnexpectedSnafu {
290                    reason: "Primary-key compatibility helper is missing",
291                })?;
292                let primary_key = compat.compat_primary_key(batch.column(0))?;
293                RecordBatch::try_new(batch.schema(), vec![primary_key]).context(NewRecordBatchSnafu)
294            })
295            .boxed();
296        Ok(Some(stream))
297    }
298
299    /// Builds a full-projection reader selected only by the provided encoded-PK
300    /// filter and this range's existing row selection.
301    ///
302    /// This deliberately bypasses generic predicate prefiltering. The series
303    /// pruner selected the row group independently, and simple predicates retained
304    /// by the disabled prefilter plan are applied precisely before merge.
305    pub(crate) async fn reader_by_primary_key(
306        &self,
307        primary_key_filter: &mut dyn mito_codec::row_converter::PrimaryKeyFilter,
308        fetch_metrics: Option<&ParquetFetchMetrics>,
309    ) -> Result<Option<FlatRowGroupReader>> {
310        let Some(mut primary_keys) = self.primary_key_reader_inner(fetch_metrics, false).await?
311        else {
312            return Ok(None);
313        };
314
315        let mut masks = Vec::new();
316        while let Some(batch) = primary_keys.next().await {
317            let batch = batch?;
318            masks.push(BooleanArray::from(primary_key_filter_mask(
319                &batch,
320                primary_key_filter,
321            )?));
322        }
323        let Some(selected) = refine_primary_key_selection(&masks, &self.row_selection) else {
324            return Ok(None);
325        };
326
327        let stream = self
328            .context
329            .reader_builder
330            .build_without_prefilter(self.context.build_context(
331                self.row_group_idx,
332                Some(selected),
333                fetch_metrics,
334            ))
335            .await?;
336        Ok(Some(FlatRowGroupReader::new(self.context.clone(), stream)))
337    }
338
339    /// Returns the helper to compat batches.
340    pub(crate) fn compat_batch(&self) -> Option<&FlatCompatBatch> {
341        self.context.compat_batch()
342    }
343
344    /// Returns the helper to project batches.
345    pub(crate) fn compaction_projection_mapper(&self) -> Option<&CompactionProjectionMapper> {
346        self.context.compaction_projection_mapper()
347    }
348
349    /// Filters a full-projection batch using this range's precise filters.
350    pub(crate) fn precise_filter_flat(
351        &self,
352        input: RecordBatch,
353        skip_fields: bool,
354        skip_tags: bool,
355    ) -> Result<Option<RecordBatch>> {
356        self.context
357            .precise_filter_flat(input, skip_fields, skip_tags)
358    }
359
360    /// Returns the precise-filter mode configured for this range.
361    pub(crate) fn pre_filter_mode(&self) -> PreFilterMode {
362        self.context.pre_filter_mode()
363    }
364
365    /// Returns the file handle of the file range.
366    pub(crate) fn file_handle(&self) -> &FileHandle {
367        self.context.reader_builder.file_handle()
368    }
369}
370
371fn refine_primary_key_selection(
372    masks: &[BooleanArray],
373    original: &Option<RowSelection>,
374) -> Option<RowSelection> {
375    if masks.is_empty() {
376        return None;
377    }
378    let selected = RowSelection::from_filters(masks);
379    let selected = match original {
380        Some(original) => original.and_then(&selected),
381        None => selected,
382    };
383    (selected.row_count() > 0).then_some(selected)
384}
385
386/// Context shared by ranges of the same parquet SST.
387pub struct FileRangeContext {
388    /// Row group reader builder for the file.
389    reader_builder: RowGroupReaderBuilder,
390    /// Base of the context.
391    base: RangeBase,
392}
393
394pub type FileRangeContextRef = Arc<FileRangeContext>;
395
396impl FileRangeContext {
397    /// Creates a new [FileRangeContext].
398    pub(crate) fn new(reader_builder: RowGroupReaderBuilder, base: RangeBase) -> Self {
399        Self {
400            reader_builder,
401            base,
402        }
403    }
404
405    /// Returns filters pushed down.
406    pub(crate) fn filters(&self) -> &[SimpleFilterContext] {
407        &self.base.filters
408    }
409
410    /// Returns true if a partition filter is configured.
411    pub(crate) fn has_partition_filter(&self) -> bool {
412        self.base.partition_filter.is_some()
413    }
414
415    /// Returns the format helper.
416    pub(crate) fn read_format(&self) -> &FlatReadFormat {
417        &self.base.read_format
418    }
419
420    /// Returns the reader builder.
421    pub(crate) fn reader_builder(&self) -> &RowGroupReaderBuilder {
422        &self.reader_builder
423    }
424
425    /// Returns the helper to compat batches.
426    pub(crate) fn compat_batch(&self) -> Option<&FlatCompatBatch> {
427        self.base.compat_batch.as_ref()
428    }
429
430    /// Returns the helper to project batches.
431    pub(crate) fn compaction_projection_mapper(&self) -> Option<&CompactionProjectionMapper> {
432        self.base.compaction_projection_mapper.as_ref()
433    }
434
435    /// Sets the compat helper to the context.
436    pub(crate) fn set_compat_batch(&mut self, compat: Option<FlatCompatBatch>) {
437        self.base.compat_batch = compat;
438    }
439
440    /// Filters the input RecordBatch by the pushed down predicate and returns RecordBatch.
441    /// If a partition expr filter is configured, it is also applied.
442    /// Physical filter exprs are not evaluated here; they are only applied during prefiltering.
443    pub(crate) fn precise_filter_flat(
444        &self,
445        input: RecordBatch,
446        skip_fields: bool,
447        skip_tags: bool,
448    ) -> Result<Option<RecordBatch>> {
449        self.base.precise_filter_flat(input, skip_fields, skip_tags)
450    }
451
452    pub(crate) fn pre_filter_mode(&self) -> PreFilterMode {
453        self.base.pre_filter_mode
454    }
455
456    //// Decodes parquet metadata and finds if row group contains delete op.
457    pub(crate) fn contains_delete(&self, row_group_index: usize) -> Result<bool> {
458        let metadata = self.reader_builder.parquet_metadata();
459        row_group_contains_delete(metadata, row_group_index, self.reader_builder.file_path())
460    }
461
462    /// Creates a [RowGroupBuildContext] for building row group readers with prefiltering.
463    pub(crate) fn build_context<'a>(
464        &'a self,
465        row_group_idx: usize,
466        row_selection: Option<RowSelection>,
467        fetch_metrics: Option<&'a ParquetFetchMetrics>,
468    ) -> RowGroupBuildContext<'a> {
469        RowGroupBuildContext {
470            row_group_idx,
471            row_selection,
472            fetch_metrics,
473        }
474    }
475
476    /// Returns the estimated memory size of this context.
477    /// Mainly accounts for the parquet metadata size.
478    pub(crate) fn memory_size(&self) -> usize {
479        self.reader_builder.parquet_metadata_size()
480    }
481}
482
483/// Mode to pre-filter columns in a range.
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
485pub enum PreFilterMode {
486    /// Filters all columns.
487    All,
488    /// Always skip fields.
489    SkipFields,
490}
491
492impl PreFilterMode {
493    pub(crate) fn skip_fields(self) -> bool {
494        matches!(self, Self::SkipFields)
495    }
496}
497
498/// Context for partition expression filtering.
499pub(crate) struct PartitionFilterContext {
500    pub(crate) region_partition_physical_expr: Arc<dyn PhysicalExpr>,
501    /// Schema containing only columns referenced by the partition expression.
502    /// This is used to build a minimal RecordBatch for partition filter evaluation.
503    pub(crate) partition_schema: Arc<Schema>,
504}
505
506/// Common fields for a range to read and filter batches.
507pub(crate) struct RangeBase {
508    /// Filters pushed down.
509    pub(crate) filters: Vec<SimpleFilterContext>,
510    /// Dynamic filter physical exprs.
511    pub(crate) dyn_filters: Vec<Arc<DynamicFilterPhysicalExpr>>,
512    /// Helper to read the SST.
513    pub(crate) read_format: FlatReadFormat,
514    pub(crate) expected_metadata: Option<RegionMetadataRef>,
515    /// Schema used for pruning with dynamic filters.
516    pub(crate) prune_schema: Arc<Schema>,
517    /// Decoder for primary keys
518    pub(crate) codec: Arc<dyn PrimaryKeyCodec>,
519    /// Optional helper to compat batches.
520    pub(crate) compat_batch: Option<FlatCompatBatch>,
521    /// Optional helper to project batches.
522    pub(crate) compaction_projection_mapper: Option<CompactionProjectionMapper>,
523    /// Mode to pre-filter columns.
524    pub(crate) pre_filter_mode: PreFilterMode,
525    /// Partition filter.
526    pub(crate) partition_filter: Option<PartitionFilterContext>,
527}
528
529pub(crate) struct TagDecodeState {
530    decoded_pks: Option<DecodedPrimaryKeys>,
531    decoded_tag_cache: HashMap<ColumnId, ArrayRef>,
532}
533
534impl TagDecodeState {
535    pub(crate) fn new() -> Self {
536        Self {
537            decoded_pks: None,
538            decoded_tag_cache: HashMap::new(),
539        }
540    }
541}
542
543impl RangeBase {
544    /// Filters the input RecordBatch by the pushed down predicate and returns RecordBatch.
545    ///
546    /// It assumes all necessary tags are already decoded from the primary key.
547    ///
548    /// # Arguments
549    /// * `input` - The RecordBatch to filter
550    /// * `skip_fields` - Whether to skip field filters based on PreFilterMode
551    /// * `skip_tags` - Whether to skip tag filters that were applied in an earlier phase
552    pub(crate) fn precise_filter_flat(
553        &self,
554        input: RecordBatch,
555        skip_fields: bool,
556        skip_tags: bool,
557    ) -> Result<Option<RecordBatch>> {
558        let mut tag_decode_state = TagDecodeState::new();
559        let mask =
560            self.compute_filter_mask_flat(&input, skip_fields, skip_tags, &mut tag_decode_state)?;
561
562        // If mask is None, the entire batch is filtered out
563        let Some(mut mask) = mask else {
564            return Ok(None);
565        };
566
567        // Apply partition filter
568        if let Some(partition_filter) = &self.partition_filter {
569            let record_batch = self.project_record_batch_for_pruning_flat(
570                &input,
571                &partition_filter.partition_schema,
572                &mut tag_decode_state,
573            )?;
574            let partition_mask = self.evaluate_partition_filter(&record_batch, partition_filter)?;
575            mask = mask.bitand(&partition_mask);
576        }
577
578        let num_selected = mask.count_set_bits();
579        if num_selected == 0 {
580            return Ok(None);
581        }
582        if num_selected == input.num_rows() {
583            // Nothing was filtered out, e.g. all filters were skipped by
584            // `skip_fields`/`skip_tags`. Avoid copying the whole batch.
585            return Ok(Some(input));
586        }
587
588        let filtered_batch =
589            datatypes::arrow::compute::filter_record_batch(&input, &BooleanArray::from(mask))
590                .context(ComputeArrowSnafu)?;
591
592        if filtered_batch.num_rows() > 0 {
593            Ok(Some(filtered_batch))
594        } else {
595            Ok(None)
596        }
597    }
598
599    /// Computes the filter mask for the input RecordBatch based on pushed down predicates.
600    /// If a partition expr filter is configured, it is applied later in `precise_filter_flat` but **NOT** in this function.
601    /// Physical filter exprs are excluded here and only apply during prefiltering.
602    ///
603    /// Returns `None` if the entire batch is filtered out, otherwise returns the boolean mask.
604    ///
605    /// # Arguments
606    /// * `input` - The RecordBatch to compute mask for
607    /// * `skip_fields` - Whether to skip field filters based on PreFilterMode
608    /// * `skip_tags` - Whether to skip tag filters that were applied in an earlier phase
609    pub(crate) fn compute_filter_mask_flat(
610        &self,
611        input: &RecordBatch,
612        skip_fields: bool,
613        skip_tags: bool,
614        tag_decode_state: &mut TagDecodeState,
615    ) -> Result<Option<BooleanBuffer>> {
616        let mut mask = BooleanBuffer::new_set(input.num_rows());
617
618        let metadata = self.read_format.metadata();
619
620        // Run filter one by one and combine them result
621        for filter_ctx in &self.filters {
622            let filter = match filter_ctx.filter() {
623                MaybeFilter::Filter(f) => f,
624                // Column matches.
625                MaybeFilter::Matched => continue,
626                // Column doesn't match, filter the entire batch.
627                MaybeFilter::Pruned => return Ok(None),
628            };
629
630            // Skip field filters if skip_fields is true
631            if skip_fields && filter_ctx.semantic_type() == SemanticType::Field {
632                continue;
633            }
634            if skip_tags && filter_ctx.semantic_type() == SemanticType::Tag {
635                continue;
636            }
637
638            // Get the column directly by its projected index.
639            // If the column is missing and it's not a tag/time column, this filter is skipped.
640            // Assumes the projection indices align with the input batch schema.
641            let column_idx = self
642                .read_format
643                .projected_index_by_id(filter_ctx.column_id());
644            if let Some(idx) = column_idx {
645                let column = &input.columns().get(idx).unwrap();
646                let result = filter.evaluate_array(column).context(RecordBatchSnafu)?;
647                mask = mask.bitand(&result);
648            } else if filter_ctx.semantic_type() == SemanticType::Tag {
649                // Column not found in projection, it may be a tag column.
650                let column_id = filter_ctx.column_id();
651
652                if let Some(tag_column) =
653                    self.maybe_decode_tag_column(metadata, column_id, input, tag_decode_state)?
654                {
655                    let result = filter
656                        .evaluate_array(&tag_column)
657                        .context(RecordBatchSnafu)?;
658                    mask = mask.bitand(&result);
659                }
660            } else if filter_ctx.semantic_type() == SemanticType::Timestamp {
661                let time_index_pos = time_index_column_index(input.num_columns());
662                let column = &input.columns()[time_index_pos];
663                let result = filter.evaluate_array(column).context(RecordBatchSnafu)?;
664                mask = mask.bitand(&result);
665            }
666            // Non-tag column not found in projection.
667        }
668
669        Ok(Some(mask))
670    }
671
672    /// Returns the decoded tag column for `column_id`, or `None` if it's not a tag.
673    fn maybe_decode_tag_column(
674        &self,
675        metadata: &RegionMetadataRef,
676        column_id: ColumnId,
677        input: &RecordBatch,
678        tag_decode_state: &mut TagDecodeState,
679    ) -> Result<Option<ArrayRef>> {
680        let Some(pk_index) = metadata.primary_key_index(column_id) else {
681            return Ok(None);
682        };
683
684        if let Some(cached_column) = tag_decode_state.decoded_tag_cache.get(&column_id) {
685            return Ok(Some(cached_column.clone()));
686        }
687
688        if tag_decode_state.decoded_pks.is_none() {
689            tag_decode_state.decoded_pks = Some(decode_primary_keys(self.codec.as_ref(), input)?);
690        }
691
692        let pk_index = if self.codec.encoding() == PrimaryKeyEncoding::Sparse {
693            None
694        } else {
695            Some(pk_index)
696        };
697        let Some(column_index) = metadata.column_index_by_id(column_id) else {
698            return Ok(None);
699        };
700        let Some(decoded) = tag_decode_state.decoded_pks.as_ref() else {
701            return Ok(None);
702        };
703
704        let column_metadata = &metadata.column_metadatas[column_index];
705        let tag_column = decoded.get_tag_column(
706            column_id,
707            pk_index,
708            &column_metadata.column_schema.data_type,
709        )?;
710        tag_decode_state
711            .decoded_tag_cache
712            .insert(column_id, tag_column.clone());
713
714        Ok(Some(tag_column))
715    }
716
717    /// Evaluates the partition filter against the input `RecordBatch`.
718    fn evaluate_partition_filter(
719        &self,
720        record_batch: &RecordBatch,
721        partition_filter: &PartitionFilterContext,
722    ) -> Result<BooleanBuffer> {
723        let columnar_value = partition_filter
724            .region_partition_physical_expr
725            .evaluate(record_batch)
726            .context(EvalPartitionFilterSnafu)?;
727        let array = columnar_value
728            .into_array(record_batch.num_rows())
729            .context(EvalPartitionFilterSnafu)?;
730        let boolean_array =
731            array
732                .as_any()
733                .downcast_ref::<BooleanArray>()
734                .context(UnexpectedSnafu {
735                    reason: "Failed to downcast to BooleanArray".to_string(),
736                })?;
737
738        // also need to consider nulls in the partition filter result. If a value is null, it should be treated as false (filtered out).
739        let mut mask = boolean_array.values().clone();
740        if let Some(nulls) = boolean_array.nulls() {
741            mask = mask.bitand(nulls.inner());
742        }
743
744        Ok(mask)
745    }
746
747    /// Projects the input `RecordBatch` to match the given schema.
748    ///
749    /// This is used for partition expression evaluation. The schema should only contain
750    /// the columns referenced by the partition expression to minimize overhead.
751    fn project_record_batch_for_pruning_flat(
752        &self,
753        input: &RecordBatch,
754        schema: &Arc<Schema>,
755        tag_decode_state: &mut TagDecodeState,
756    ) -> Result<RecordBatch> {
757        let arrow_schema = schema.arrow_schema();
758        let mut columns = Vec::with_capacity(arrow_schema.fields().len());
759
760        let metadata = self.read_format.metadata();
761
762        for field in arrow_schema.fields() {
763            let column_id = metadata.column_by_name(field.name()).map(|c| c.column_id);
764
765            let Some(column_id) = column_id else {
766                return UnexpectedSnafu {
767                    reason: format!(
768                        "Partition pruning schema expects column '{}' but it is missing in \
769                         region metadata",
770                        field.name()
771                    ),
772                }
773                .fail();
774            };
775
776            if let Some(idx) = self.read_format.projected_index_by_id(column_id) {
777                columns.push(input.column(idx).clone());
778                continue;
779            }
780
781            if metadata.time_index_column().column_id == column_id {
782                let time_index_pos = time_index_column_index(input.num_columns());
783                columns.push(input.column(time_index_pos).clone());
784                continue;
785            }
786
787            if let Some(tag_column) =
788                self.maybe_decode_tag_column(metadata, column_id, input, tag_decode_state)?
789            {
790                columns.push(tag_column);
791                continue;
792            }
793
794            return UnexpectedSnafu {
795                reason: format!(
796                    "Partition pruning schema expects column '{}' (id {}) but it is not \
797                     present in projected record batch",
798                    field.name(),
799                    column_id
800                ),
801            }
802            .fail();
803        }
804
805        RecordBatch::try_new(arrow_schema.clone(), columns).context(NewRecordBatchSnafu)
806    }
807}
808
809#[cfg(test)]
810mod tests {
811    use std::sync::Arc;
812
813    use datafusion_expr::{col, lit};
814    use datatypes::prelude::ConcreteDataType;
815    use datatypes::schema::ColumnSchema;
816    use datatypes::value::Value;
817    use parquet::arrow::arrow_reader::RowSelector;
818    use partition::expr::col as partition_col;
819
820    use super::*;
821    use crate::read::read_columns::ReadColumns;
822    use crate::sst::parquet::flat_format::FlatReadFormat;
823    use crate::test_util::sst_util::{new_record_batch_with_custom_sequence, sst_region_metadata};
824
825    fn new_test_range_base(filters: Vec<SimpleFilterContext>) -> RangeBase {
826        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
827
828        let read_format = FlatReadFormat::new(
829            metadata.clone(),
830            ReadColumns::from_deduped_column_ids(
831                metadata.column_metadatas.iter().map(|c| c.column_id),
832            ),
833            None,
834            "test",
835            true,
836        )
837        .unwrap();
838
839        RangeBase {
840            filters,
841            dyn_filters: vec![],
842            read_format,
843            expected_metadata: None,
844            prune_schema: metadata.schema.clone(),
845            codec: mito_codec::row_converter::build_primary_key_codec(metadata.as_ref()),
846            compat_batch: None,
847            compaction_projection_mapper: None,
848            pre_filter_mode: PreFilterMode::All,
849            partition_filter: None,
850        }
851    }
852
853    #[test]
854    fn test_compute_filter_mask_flat_applies_remaining_simple_filters() {
855        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
856        let filters = vec![
857            SimpleFilterContext::new_opt(&metadata, None, &col("tag_0").eq(lit("a"))).unwrap(),
858            SimpleFilterContext::new_opt(&metadata, None, &col("field_0").gt(lit(1_u64))).unwrap(),
859        ];
860        let base = new_test_range_base(filters);
861        let batch = new_record_batch_with_custom_sequence(&["b", "x"], 0, 4, 1);
862
863        let mask = base
864            .compute_filter_mask_flat(&batch, false, false, &mut TagDecodeState::new())
865            .unwrap()
866            .unwrap();
867        assert_eq!(mask.count_set_bits(), 0);
868
869        let mask = base
870            .compute_filter_mask_flat(&batch, false, true, &mut TagDecodeState::new())
871            .unwrap()
872            .unwrap();
873        assert_eq!(mask.count_set_bits(), 2);
874    }
875
876    #[test]
877    fn test_precise_filter_flat_returns_input_when_nothing_is_filtered() {
878        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
879        let tag_filter =
880            SimpleFilterContext::new_opt(&metadata, None, &col("tag_0").eq(lit("z"))).unwrap();
881        let base = new_test_range_base(vec![tag_filter]);
882        let batch = new_record_batch_with_custom_sequence(&["b", "x"], 0, 4, 1);
883
884        // The only filter is a tag filter, and it is skipped, so the batch must
885        // come back untouched rather than being copied through `filter_record_batch`.
886        let filtered = base
887            .precise_filter_flat(batch.clone(), false, true)
888            .unwrap()
889            .unwrap();
890        assert_eq!(batch, filtered);
891    }
892
893    #[test]
894    fn test_compute_filter_mask_flat_does_not_postfilter_physical_filters() {
895        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
896        let read_format = FlatReadFormat::new(
897            metadata.clone(),
898            ReadColumns::from_deduped_column_ids(
899                metadata.column_metadatas.iter().map(|c| c.column_id),
900            ),
901            None,
902            "test",
903            true,
904        )
905        .unwrap();
906        let physical_filter = crate::sst::parquet::reader::PhysicalFilterContext::new_opt(
907            &metadata,
908            None,
909            &read_format,
910            &col("field_0").in_list(vec![lit(1_u64), lit(2_u64)], false),
911        );
912        assert!(physical_filter.is_some());
913        let base = new_test_range_base(vec![]);
914        let batch = new_record_batch_with_custom_sequence(&["b", "x"], 0, 4, 1);
915
916        let mask = base
917            .compute_filter_mask_flat(&batch, false, false, &mut TagDecodeState::new())
918            .unwrap()
919            .unwrap();
920        assert_eq!(mask.count_set_bits(), 4);
921    }
922
923    #[test]
924    fn test_precise_filter_flat_applies_partition_filter_when_skipping_tags() {
925        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
926        let tag_filter =
927            SimpleFilterContext::new_opt(&metadata, None, &col("tag_0").eq(lit("z"))).unwrap();
928        let mut base = new_test_range_base(vec![tag_filter]);
929        let batch = new_record_batch_with_custom_sequence(&["b", "x"], 0, 4, 1);
930
931        let batch_schema = batch.schema();
932        let tag_field = batch_schema.field(0);
933        let partition_schema = Arc::new(Schema::new(vec![ColumnSchema::new(
934            "tag_0".to_string(),
935            ConcreteDataType::from_arrow_type(tag_field.data_type()),
936            tag_field.is_nullable(),
937        )]));
938        let partition_expr = partition_col("tag_0")
939            .gt_eq(Value::String("a".into()))
940            .and(partition_col("tag_0").lt(Value::String("c".into())));
941        base.partition_filter = Some(PartitionFilterContext {
942            region_partition_physical_expr: partition_expr
943                .try_as_physical_expr(partition_schema.arrow_schema())
944                .unwrap(),
945            partition_schema,
946        });
947
948        let filtered = base
949            .precise_filter_flat(batch, false, true)
950            .unwrap()
951            .unwrap();
952        assert_eq!(filtered.num_rows(), 4);
953
954        let out_of_partition = new_record_batch_with_custom_sequence(&["z", "x"], 0, 4, 1);
955        assert!(
956            base.precise_filter_flat(out_of_partition, false, true)
957                .unwrap()
958                .is_none()
959        );
960    }
961
962    #[test]
963    fn test_refine_primary_key_selection_intersects_original_selection() {
964        let original = Some(RowSelection::from(vec![
965            RowSelector::skip(2),
966            RowSelector::select(3),
967            RowSelector::skip(1),
968            RowSelector::select(2),
969        ]));
970        let mask = BooleanArray::from(vec![true, false, true, false, true]);
971        let actual = refine_primary_key_selection(&[mask], &original).unwrap();
972        let expected = RowSelection::from(vec![
973            RowSelector::skip(2),
974            RowSelector::select(1),
975            RowSelector::skip(1),
976            RowSelector::select(1),
977            RowSelector::skip(2),
978            RowSelector::select(1),
979        ]);
980        assert_eq!(actual, expected);
981    }
982}