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