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, HashSet};
19use std::ops::{BitAnd, Range};
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 object_store::ObjectStore;
33use parquet::arrow::arrow_reader::RowSelection;
34use parquet::file::metadata::ParquetMetaData;
35use parquet::file::statistics::Statistics;
36use snafu::{OptionExt, ResultExt, ensure};
37use store_api::codec::PrimaryKeyEncoding;
38use store_api::metadata::RegionMetadataRef;
39use store_api::storage::{ColumnId, TimeSeriesRowSelector};
40use table::predicate::Predicate;
41use tokio::sync::OnceCell;
42
43use crate::cache::CacheStrategy;
44use crate::error::{
45    ComputeArrowSnafu, DecodeStatsSnafu, EvalPartitionFilterSnafu, InvalidRecordBatchSnafu,
46    NewRecordBatchSnafu, RecordBatchSnafu, Result, StatsNotPresentSnafu, UnexpectedSnafu,
47};
48use crate::read::compat::FlatCompatBatch;
49use crate::read::flat_projection::CompactionProjectionMapper;
50use crate::read::last_row::FlatRowGroupLastRowCachedReader;
51use crate::read::prune::FlatPruneReader;
52use crate::sst::file::FileHandle;
53use crate::sst::parquet::flat_format::{
54    DecodedPrimaryKeys, FlatReadFormat, decode_primary_keys, primary_key_column_index,
55    time_index_column_index,
56};
57use crate::sst::parquet::json_align::ProjectedRecordBatchStream;
58use crate::sst::parquet::prefilter::primary_key_filter_mask;
59use crate::sst::parquet::reader::{
60    FlatRowGroupReader, MaybeFilter, RowGroupBuildContext, RowGroupReaderBuilder,
61    SimpleFilterContext,
62};
63use crate::sst::parquet::row_group::ParquetFetchMetrics;
64use crate::sst::parquet::row_selection::{intersect_row_selections, row_selection_from_row_ranges};
65use crate::sst::parquet::stats::RowGroupPruningStats;
66use crate::sst::range_index::{SstRangeIndexSearcher, range_index_path};
67
68/// Checks if a row group contains delete operations by examining the min value of op_type column.
69///
70/// Returns `Ok(true)` if the row group contains delete operations, `Ok(false)` if it doesn't,
71/// or an error if the statistics are not present or cannot be decoded.
72pub(crate) fn row_group_contains_delete(
73    parquet_meta: &ParquetMetaData,
74    row_group_index: usize,
75    file_path: &str,
76) -> Result<bool> {
77    let row_group_metadata = &parquet_meta.row_groups()[row_group_index];
78
79    // safety: The last column of SST must be op_type
80    let column_metadata = &row_group_metadata.columns().last().unwrap();
81    let stats = column_metadata
82        .statistics()
83        .context(StatsNotPresentSnafu { file_path })?;
84    stats
85        .min_bytes_opt()
86        .context(StatsNotPresentSnafu { file_path })?
87        .try_into()
88        .map(i32::from_le_bytes)
89        .map(|min_op_type| min_op_type == OpType::Delete as i32)
90        .ok()
91        .context(DecodeStatsSnafu { file_path })
92}
93
94/// A range of a parquet SST. Now it is a row group.
95/// We can read different file ranges in parallel.
96#[derive(Clone)]
97pub struct FileRange {
98    /// Shared context.
99    context: FileRangeContextRef,
100    /// Index of the row group in the SST.
101    row_group_idx: usize,
102    /// Row selection for the row group. `None` means all rows.
103    row_selection: Option<RowSelection>,
104}
105
106impl FileRange {
107    /// Returns the shared range-index searcher, opening it on first use.
108    pub(crate) async fn range_index_searcher(&self) -> Result<Option<&SstRangeIndexSearcher>> {
109        self.context.range_index_searcher().await
110    }
111
112    /// Returns the region metadata stored in this SST.
113    pub(crate) fn region_metadata(&self) -> &RegionMetadataRef {
114        self.context.read_format().metadata()
115    }
116
117    /// Returns encoded primary-key min/max statistics for this row group.
118    pub(crate) fn primary_key_range(&self) -> Option<(&[u8], &[u8])> {
119        let metadata = self.context.reader_builder.parquet_metadata();
120        let num_columns = metadata.file_metadata().schema_descr().num_columns();
121        let primary_key_index = primary_key_column_index(num_columns);
122        match metadata
123            .row_group(self.row_group_idx)
124            .column(primary_key_index)
125            .statistics()?
126        {
127            Statistics::ByteArray(statistics) => {
128                Some((statistics.min_bytes_opt()?, statistics.max_bytes_opt()?))
129            }
130            _ => None,
131        }
132    }
133
134    /// Creates a new [FileRange].
135    pub(crate) fn new(
136        context: FileRangeContextRef,
137        row_group_idx: usize,
138        row_selection: Option<RowSelection>,
139    ) -> Self {
140        Self {
141            context,
142            row_group_idx,
143            row_selection,
144        }
145    }
146
147    /// Returns true if [FileRange] selects all rows in row group.
148    fn select_all(&self) -> bool {
149        let rows_in_group = self
150            .context
151            .reader_builder
152            .parquet_metadata()
153            .row_group(self.row_group_idx)
154            .num_rows();
155
156        let Some(row_selection) = &self.row_selection else {
157            return true;
158        };
159        row_selection.row_count() == rows_in_group as usize
160    }
161
162    /// Performs pruning before reading the [FileRange].
163    /// It use latest dynamic filters with row group statistics to prune the range.
164    ///
165    /// Returns false if the entire range is pruned and can be skipped.
166    fn in_dynamic_filter_range(&self) -> bool {
167        if self.context.base.dyn_filters.is_empty() {
168            return true;
169        }
170        let curr_row_group = self
171            .context
172            .reader_builder
173            .parquet_metadata()
174            .row_group(self.row_group_idx);
175        let read_format = self.context.read_format();
176        let prune_schema = &self.context.base.prune_schema;
177        let stats = RowGroupPruningStats::new(
178            std::slice::from_ref(curr_row_group),
179            read_format,
180            self.context.base.expected_metadata.clone(),
181            self.context.base.pre_filter_mode.skip_fields(),
182        );
183
184        // not costly to create a predicate here since dynamic filters are wrapped in Arc
185        let pred = Predicate::with_dyn_filters(vec![], self.context.base.dyn_filters.clone());
186
187        pred.prune_with_stats(&stats, prune_schema.arrow_schema())
188            .first()
189            .cloned()
190            .unwrap_or(true) // unexpected, not skip just in case
191    }
192
193    /// Creates a flat reader that returns RecordBatch.
194    pub async fn flat_reader(
195        &self,
196        selector: Option<TimeSeriesRowSelector>,
197        fetch_metrics: Option<&ParquetFetchMetrics>,
198    ) -> Result<Option<FlatPruneReader>> {
199        if !self.in_dynamic_filter_range() {
200            return Ok(None);
201        }
202        // Compute skip_fields once for this row group
203        let skip_fields = self.context.base.pre_filter_mode.skip_fields();
204        let parquet_reader = self
205            .context
206            .reader_builder
207            .build(self.context.build_context(
208                self.row_group_idx,
209                self.row_selection.clone(),
210                fetch_metrics,
211            ))
212            .await?;
213
214        let use_last_row_reader = if matches!(
215            selector,
216            Some(TimeSeriesRowSelector::LastRow { after_merge: false })
217        ) {
218            // Only use the last row reader if row group does not contain DELETE, all
219            // rows are selected, and filters that still run after this reader
220            // cannot change which row is last. Tag filters are safe because a
221            // tag is constant within a series. Timestamp and field filters are
222            // not safe for this shortcut.
223            let put_only = !self
224                .context
225                .contains_delete(self.row_group_idx)
226                .inspect_err(|e| {
227                    error!(e; "Failed to decode min value of op_type, fallback to FlatRowGroupReader");
228                })
229                .unwrap_or(true);
230            put_only && self.select_all() && self.context.remaining_filters_preserve_last_row()
231        } else {
232            false
233        };
234
235        let flat_prune_reader = if use_last_row_reader {
236            let flat_row_group_reader =
237                FlatRowGroupReader::new(self.context.clone(), parquet_reader);
238            // Predicate prefiltering makes the input stream predicate-dependent, so cached
239            // selector results are not reusable across queries with different filters.
240            let cache_strategy = if self.context.reader_builder.has_predicate_prefilter() {
241                CacheStrategy::Disabled
242            } else {
243                self.context.reader_builder.cache_strategy().clone()
244            };
245            let reader = FlatRowGroupLastRowCachedReader::new(
246                self.file_handle().file_id().file_id(),
247                self.row_group_idx,
248                cache_strategy,
249                self.context.read_format().parquet_read_columns(),
250                self.context.read_format().json_target_types().clone(),
251                flat_row_group_reader,
252            );
253            FlatPruneReader::new_with_last_row_reader(self.context.clone(), reader, skip_fields)
254        } else {
255            let flat_row_group_reader =
256                FlatRowGroupReader::new(self.context.clone(), parquet_reader);
257            FlatPruneReader::new_with_row_group_reader(
258                self.context.clone(),
259                flat_row_group_reader,
260                skip_fields,
261            )
262        };
263
264        Ok(Some(flat_prune_reader))
265    }
266
267    /// Creates a reader that returns only the encoded primary-key column.
268    ///
269    /// The returned primary keys are compatible with the expected region metadata.
270    pub(crate) async fn primary_key_reader(
271        &self,
272        fetch_metrics: Option<&ParquetFetchMetrics>,
273    ) -> Result<Option<ProjectedRecordBatchStream>> {
274        self.primary_key_reader_inner(fetch_metrics, true).await
275    }
276
277    async fn primary_key_reader_inner(
278        &self,
279        fetch_metrics: Option<&ParquetFetchMetrics>,
280        check_dynamic_filter: bool,
281    ) -> Result<Option<ProjectedRecordBatchStream>> {
282        if check_dynamic_filter && !self.in_dynamic_filter_range() {
283            return Ok(None);
284        }
285        let stream = self
286            .context
287            .reader_builder
288            .build_primary_key(self.context.build_context(
289                self.row_group_idx,
290                self.row_selection.clone(),
291                fetch_metrics,
292            ))
293            .await?;
294        if self.context.compat_batch().is_none() {
295            return Ok(Some(stream));
296        }
297
298        let context = self.context.clone();
299        let stream = stream
300            .map(move |batch| {
301                let batch = batch?;
302                let compat = context.compat_batch().context(UnexpectedSnafu {
303                    reason: "Primary-key compatibility helper is missing",
304                })?;
305                let primary_key = compat.compat_primary_key(batch.column(0))?;
306                RecordBatch::try_new(batch.schema(), vec![primary_key]).context(NewRecordBatchSnafu)
307            })
308            .boxed();
309        Ok(Some(stream))
310    }
311
312    /// Builds a full-projection reader selected only by the provided encoded-PK
313    /// filter and this range's existing row selection.
314    ///
315    /// This deliberately bypasses generic predicate prefiltering. The series
316    /// pruner selected the row group independently, and simple predicates retained
317    /// by the disabled prefilter plan are applied precisely before merge.
318    pub(crate) async fn reader_by_primary_key(
319        &self,
320        primary_key_filter: &mut dyn mito_codec::row_converter::PrimaryKeyFilter,
321        fetch_metrics: Option<&ParquetFetchMetrics>,
322    ) -> Result<Option<FlatRowGroupReader>> {
323        let Some(mut primary_keys) = self.primary_key_reader_inner(fetch_metrics, false).await?
324        else {
325            return Ok(None);
326        };
327
328        let mut masks = Vec::new();
329        while let Some(batch) = primary_keys.next().await {
330            let batch = batch?;
331            masks.push(BooleanArray::from(primary_key_filter_mask(
332                &batch,
333                primary_key_filter,
334            )?));
335        }
336        let Some(selected) = refine_primary_key_selection(&masks, &self.row_selection) else {
337            return Ok(None);
338        };
339
340        let stream = self
341            .context
342            .reader_builder
343            .build_without_prefilter(self.context.build_context(
344                self.row_group_idx,
345                Some(selected),
346                fetch_metrics,
347            ))
348            .await?;
349        Ok(Some(FlatRowGroupReader::new(self.context.clone(), stream)))
350    }
351
352    /// Returns the source SST row-group index.
353    pub(crate) fn row_group_index(&self) -> usize {
354        self.row_group_idx
355    }
356
357    /// Builds a reader from absolute row-group offsets supplied by a range index.
358    /// Existing pruning is intersected before reading, without predicate prefiltering.
359    pub(crate) async fn reader_by_row_ranges(
360        &self,
361        ranges: Vec<Range<usize>>,
362        fetch_metrics: Option<&ParquetFetchMetrics>,
363    ) -> Result<Option<FlatRowGroupReader>> {
364        let num_rows = self
365            .context
366            .reader_builder
367            .parquet_metadata()
368            .row_group(self.row_group_idx)
369            .num_rows() as usize;
370        let Some(selected) = refine_row_range_selection(ranges, num_rows, &self.row_selection)?
371        else {
372            return Ok(None);
373        };
374        let stream = self
375            .context
376            .reader_builder
377            .build_without_prefilter(self.context.build_context(
378                self.row_group_idx,
379                Some(selected),
380                fetch_metrics,
381            ))
382            .await?;
383        Ok(Some(FlatRowGroupReader::new(self.context.clone(), stream)))
384    }
385
386    /// Returns the helper to compat batches.
387    pub(crate) fn compat_batch(&self) -> Option<&FlatCompatBatch> {
388        self.context.compat_batch()
389    }
390
391    /// Returns the helper to project batches.
392    pub(crate) fn compaction_projection_mapper(&self) -> Option<&CompactionProjectionMapper> {
393        self.context.compaction_projection_mapper()
394    }
395
396    /// Filters a full-projection batch using this range's precise filters.
397    pub(crate) fn precise_filter_flat(
398        &self,
399        input: RecordBatch,
400        skip_fields: bool,
401        skip_tags: bool,
402    ) -> Result<Option<RecordBatch>> {
403        self.context
404            .precise_filter_flat(input, skip_fields, skip_tags)
405    }
406
407    /// Returns the precise-filter mode configured for this range.
408    pub(crate) fn pre_filter_mode(&self) -> PreFilterMode {
409        self.context.pre_filter_mode()
410    }
411
412    /// Returns the file handle of the file range.
413    pub(crate) fn file_handle(&self) -> &FileHandle {
414        self.context.reader_builder.file_handle()
415    }
416}
417
418/// Intersects source-row offsets, rather than offsets within an already selected stream.
419fn refine_row_range_selection(
420    ranges: Vec<Range<usize>>,
421    num_rows: usize,
422    original: &Option<RowSelection>,
423) -> Result<Option<RowSelection>> {
424    let mut previous_end = 0;
425    for range in &ranges {
426        ensure!(
427            range.start >= previous_end && range.start < range.end && range.end <= num_rows,
428            InvalidRecordBatchSnafu {
429                reason: format!(
430                    "invalid range-index row range {range:?} after {previous_end}, row group has {num_rows} rows"
431                ),
432            }
433        );
434        previous_end = range.end;
435    }
436    let selected = row_selection_from_row_ranges(ranges.into_iter(), num_rows);
437    let selected = match original {
438        Some(original) => intersect_row_selections(original, &selected),
439        None => selected,
440    };
441    Ok((selected.row_count() > 0).then_some(selected))
442}
443
444fn refine_primary_key_selection(
445    masks: &[BooleanArray],
446    original: &Option<RowSelection>,
447) -> Option<RowSelection> {
448    if masks.is_empty() {
449        return None;
450    }
451    let selected = RowSelection::from_filters(masks);
452    let selected = match original {
453        Some(original) => original.and_then(&selected),
454        None => selected,
455    };
456    (selected.row_count() > 0).then_some(selected)
457}
458
459/// Context shared by ranges of the same parquet SST.
460pub struct FileRangeContext {
461    /// Store for a range index registered in the scan's index snapshot.
462    range_index_store: Option<ObjectStore>,
463    /// Lazily opened range index shared by all ranges of this file.
464    range_index_searcher: OnceCell<SstRangeIndexSearcher>,
465    /// Row group reader builder for the file.
466    reader_builder: RowGroupReaderBuilder,
467    /// Base of the context.
468    base: RangeBase,
469}
470
471pub type FileRangeContextRef = Arc<FileRangeContext>;
472
473impl FileRangeContext {
474    /// Creates a new [FileRangeContext].
475    pub(crate) fn new(
476        reader_builder: RowGroupReaderBuilder,
477        base: RangeBase,
478        range_index_store: Option<ObjectStore>,
479    ) -> Self {
480        Self {
481            reader_builder,
482            base,
483            range_index_store,
484            range_index_searcher: OnceCell::new(),
485        }
486    }
487
488    /// Opens the range index once, retaining the SST handle throughout its use.
489    async fn range_index_searcher(&self) -> Result<Option<&SstRangeIndexSearcher>> {
490        let Some(store) = &self.range_index_store else {
491            return Ok(None);
492        };
493        self.range_index_searcher
494            .get_or_try_init(|| async {
495                let file = self.reader_builder.file_handle();
496                let path = range_index_path(file.region_id(), file.file_id().file_id());
497                SstRangeIndexSearcher::open(store.clone(), &path).await
498            })
499            .await
500            .map(Some)
501    }
502
503    /// Returns filters pushed down.
504    pub(crate) fn filters(&self) -> &[SimpleFilterContext] {
505        &self.base.filters
506    }
507
508    /// Returns true if a partition filter is configured.
509    pub(crate) fn has_partition_filter(&self) -> bool {
510        self.base.partition_filter.is_some()
511    }
512
513    /// Returns true if applying the remaining precise filters after selecting
514    /// the last row cannot change which row is selected for a series.
515    fn remaining_filters_preserve_last_row(&self) -> bool {
516        !self.has_partition_filter()
517            && self
518                .filters()
519                .iter()
520                .all(|filter| filter.semantic_type() == SemanticType::Tag)
521    }
522
523    /// Returns the format helper.
524    pub(crate) fn read_format(&self) -> &FlatReadFormat {
525        &self.base.read_format
526    }
527
528    /// Returns the reader builder.
529    pub(crate) fn reader_builder(&self) -> &RowGroupReaderBuilder {
530        &self.reader_builder
531    }
532
533    /// Returns the helper to compat batches.
534    pub(crate) fn compat_batch(&self) -> Option<&FlatCompatBatch> {
535        self.base.compat_batch.as_ref()
536    }
537
538    /// Returns the helper to project batches.
539    pub(crate) fn compaction_projection_mapper(&self) -> Option<&CompactionProjectionMapper> {
540        self.base.compaction_projection_mapper.as_ref()
541    }
542
543    /// Sets the compat helper to the context.
544    pub(crate) fn set_compat_batch(&mut self, compat: Option<FlatCompatBatch>) {
545        self.base.compat_batch = compat;
546    }
547
548    /// Filters the input RecordBatch by the pushed down predicate and returns RecordBatch.
549    /// If a partition expr filter is configured, it is also applied.
550    /// Physical filter exprs are not evaluated here; they are only applied during prefiltering.
551    pub(crate) fn precise_filter_flat(
552        &self,
553        input: RecordBatch,
554        skip_fields: bool,
555        skip_tags: bool,
556    ) -> Result<Option<RecordBatch>> {
557        self.base.precise_filter_flat(input, skip_fields, skip_tags)
558    }
559
560    pub(crate) fn pre_filter_mode(&self) -> PreFilterMode {
561        self.base.pre_filter_mode
562    }
563
564    //// Decodes parquet metadata and finds if row group contains delete op.
565    pub(crate) fn contains_delete(&self, row_group_index: usize) -> Result<bool> {
566        let metadata = self.reader_builder.parquet_metadata();
567        row_group_contains_delete(metadata, row_group_index, self.reader_builder.file_path())
568    }
569
570    /// Creates a [RowGroupBuildContext] for building row group readers with prefiltering.
571    pub(crate) fn build_context<'a>(
572        &'a self,
573        row_group_idx: usize,
574        row_selection: Option<RowSelection>,
575        fetch_metrics: Option<&'a ParquetFetchMetrics>,
576    ) -> RowGroupBuildContext<'a> {
577        RowGroupBuildContext {
578            row_group_idx,
579            row_selection,
580            fetch_metrics,
581        }
582    }
583
584    /// Returns the estimated memory size of this context.
585    /// Mainly accounts for the parquet metadata size.
586    pub(crate) fn memory_size(&self) -> usize {
587        self.reader_builder.parquet_metadata_size()
588    }
589}
590
591/// Mode to pre-filter columns in a range.
592#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
593pub enum PreFilterMode {
594    /// Filters all columns.
595    All,
596    /// Always skip fields.
597    SkipFields,
598}
599
600impl PreFilterMode {
601    pub(crate) fn skip_fields(self) -> bool {
602        matches!(self, Self::SkipFields)
603    }
604}
605
606/// Context for partition expression filtering.
607pub(crate) struct PartitionFilterContext {
608    pub(crate) region_partition_physical_expr: Arc<dyn PhysicalExpr>,
609    /// Schema containing only columns referenced by the partition expression.
610    /// This is used to build a minimal RecordBatch for partition filter evaluation.
611    pub(crate) partition_schema: Arc<Schema>,
612}
613
614/// Common fields for a range to read and filter batches.
615pub(crate) struct RangeBase {
616    /// Filters pushed down.
617    pub(crate) filters: Vec<SimpleFilterContext>,
618    /// Dynamic filter physical exprs.
619    pub(crate) dyn_filters: Vec<Arc<DynamicFilterPhysicalExpr>>,
620    /// Helper to read the SST.
621    pub(crate) read_format: FlatReadFormat,
622    pub(crate) expected_metadata: Option<RegionMetadataRef>,
623    /// Schema used for pruning with dynamic filters.
624    pub(crate) prune_schema: Arc<Schema>,
625    /// Decoder for primary keys
626    pub(crate) codec: Arc<dyn PrimaryKeyCodec>,
627    /// Optional helper to compat batches.
628    pub(crate) compat_batch: Option<FlatCompatBatch>,
629    /// Optional helper to project batches.
630    pub(crate) compaction_projection_mapper: Option<CompactionProjectionMapper>,
631    /// Mode to pre-filter columns.
632    pub(crate) pre_filter_mode: PreFilterMode,
633    /// Partition filter.
634    pub(crate) partition_filter: Option<PartitionFilterContext>,
635}
636
637pub(crate) struct TagDecodeState {
638    decoded_pks: Option<DecodedPrimaryKeys>,
639    decoded_tag_cache: HashMap<ColumnId, ArrayRef>,
640}
641
642impl TagDecodeState {
643    pub(crate) fn new() -> Self {
644        Self {
645            decoded_pks: None,
646            decoded_tag_cache: HashMap::new(),
647        }
648    }
649}
650
651impl RangeBase {
652    /// Filters the input RecordBatch by the pushed down predicate and returns RecordBatch.
653    ///
654    /// It assumes all necessary tags are already decoded from the primary key.
655    ///
656    /// # Arguments
657    /// * `input` - The RecordBatch to filter
658    /// * `skip_fields` - Whether to skip field filters based on PreFilterMode
659    /// * `skip_tags` - Whether to skip tag filters that were applied in an earlier phase
660    pub(crate) fn precise_filter_flat(
661        &self,
662        input: RecordBatch,
663        skip_fields: bool,
664        skip_tags: bool,
665    ) -> Result<Option<RecordBatch>> {
666        let mut tag_decode_state = TagDecodeState::new();
667        let mask =
668            self.compute_filter_mask_flat(&input, skip_fields, skip_tags, &mut tag_decode_state)?;
669
670        // If mask is None, the entire batch is filtered out
671        let Some(mut mask) = mask else {
672            return Ok(None);
673        };
674
675        // Apply partition filter
676        if let Some(partition_filter) = &self.partition_filter {
677            let record_batch = self.project_record_batch_for_pruning_flat(
678                &input,
679                &partition_filter.partition_schema,
680                &mut tag_decode_state,
681            )?;
682            let partition_mask = self.evaluate_partition_filter(&record_batch, partition_filter)?;
683            mask = mask.bitand(&partition_mask);
684        }
685
686        let num_selected = mask.count_set_bits();
687        if num_selected == 0 {
688            return Ok(None);
689        }
690        if num_selected == input.num_rows() {
691            // Nothing was filtered out, e.g. all filters were skipped by
692            // `skip_fields`/`skip_tags`. Avoid copying the whole batch.
693            return Ok(Some(input));
694        }
695
696        let filtered_batch =
697            datatypes::arrow::compute::filter_record_batch(&input, &BooleanArray::from(mask))
698                .context(ComputeArrowSnafu)?;
699
700        if filtered_batch.num_rows() > 0 {
701            Ok(Some(filtered_batch))
702        } else {
703            Ok(None)
704        }
705    }
706
707    /// Computes the filter mask for the input RecordBatch based on pushed down predicates.
708    /// If a partition expr filter is configured, it is applied later in `precise_filter_flat` but **NOT** in this function.
709    /// Physical filter exprs are excluded here and only apply during prefiltering.
710    ///
711    /// Returns `None` if the entire batch is filtered out, otherwise returns the boolean mask.
712    ///
713    /// # Arguments
714    /// * `input` - The RecordBatch to compute mask for
715    /// * `skip_fields` - Whether to skip field filters based on PreFilterMode
716    /// * `skip_tags` - Whether to skip tag filters that were applied in an earlier phase
717    pub(crate) fn compute_filter_mask_flat(
718        &self,
719        input: &RecordBatch,
720        skip_fields: bool,
721        skip_tags: bool,
722        tag_decode_state: &mut TagDecodeState,
723    ) -> Result<Option<BooleanBuffer>> {
724        let metadata = self.read_format.metadata();
725        // Honor skip_fields/skip_tags before inspecting MaybeFilter::Pruned: a predicate
726        // marked Pruned is still skipped if it must not run before merge/dedup.
727        // For example, after adding nullable JSON2 column j to a last_non_null table,
728        // an old SST may contain (key=a, ts=1, f=10) without j, while a newer row has
729        // (key=a, ts=1, f=NULL, j={"a":1}). WHERE j IS NOT NULL is Pruned for the old
730        // SST, but discarding that row here would lose f=10 needed for backfill.
731        // Keep it for merge/dedup and evaluate the field predicate on the merged row.
732        // This also applies to added non-JSON2 fields, not only JSON2 columns.
733        let filters = self.filters.iter().filter(|ctx| {
734            !(skip_fields && ctx.semantic_type() == SemanticType::Field
735                || skip_tags && ctx.semantic_type() == SemanticType::Tag)
736        });
737        if filters
738            .clone()
739            .any(|ctx| matches!(ctx.filter(), MaybeFilter::Pruned))
740        {
741            return Ok(None);
742        }
743        let filter_tags = self
744            .filters
745            .iter()
746            .filter(|ctx| {
747                !skip_tags
748                    && ctx.semantic_type() == SemanticType::Tag
749                    && matches!(ctx.filter(), MaybeFilter::Filter(_))
750            })
751            .map(|ctx| ctx.column_id());
752        let partition_tags = self.partition_filter.iter().flat_map(|filter| {
753            filter
754                .partition_schema
755                .column_schemas()
756                .iter()
757                .filter_map(|column| {
758                    metadata
759                        .column_by_name(&column.name)
760                        .map(|column| column.column_id)
761                })
762        });
763        // Partition predicates are evaluated later, but extracting their tags
764        // together with the simple filters avoids a second scan of each key.
765        self.cache_sparse_tag_columns(input, filter_tags.chain(partition_tags), tag_decode_state)?;
766
767        let mut mask = BooleanBuffer::new_set(input.num_rows());
768
769        // Run filter one by one and combine them result
770        for filter_ctx in filters {
771            let filter = match filter_ctx.filter() {
772                MaybeFilter::Filter(f) => f,
773                // Column matches.
774                MaybeFilter::Matched => continue,
775                // Column doesn't match, filter the entire batch.
776                MaybeFilter::Pruned => return Ok(None),
777            };
778
779            // Get the column directly by its projected index.
780            // If the column is missing and it's not a tag/time column, this filter is skipped.
781            // Assumes the projection indices align with the input batch schema.
782            let column_idx = self
783                .read_format
784                .projected_index_by_id(filter_ctx.column_id());
785            if let Some(idx) = column_idx {
786                let column = &input.columns().get(idx).unwrap();
787                let result = filter.evaluate_array(column).context(RecordBatchSnafu)?;
788                mask = mask.bitand(&result);
789            } else if filter_ctx.semantic_type() == SemanticType::Tag {
790                // Column not found in projection, it may be a tag column.
791                let column_id = filter_ctx.column_id();
792
793                if let Some(tag_column) =
794                    self.maybe_decode_tag_column(metadata, column_id, input, tag_decode_state)?
795                {
796                    let result = filter
797                        .evaluate_array(&tag_column)
798                        .context(RecordBatchSnafu)?;
799                    mask = mask.bitand(&result);
800                }
801            } else if filter_ctx.semantic_type() == SemanticType::Timestamp {
802                let time_index_pos = time_index_column_index(input.num_columns());
803                let column = &input.columns()[time_index_pos];
804                let result = filter.evaluate_array(column).context(RecordBatchSnafu)?;
805                mask = mask.bitand(&result);
806            }
807            // Non-tag column not found in projection.
808        }
809
810        Ok(Some(mask))
811    }
812
813    /// Materializes missing sparse tags together, preserving the single-column fast path.
814    fn cache_sparse_tag_columns(
815        &self,
816        input: &RecordBatch,
817        column_ids: impl Iterator<Item = ColumnId>,
818        state: &mut TagDecodeState,
819    ) -> Result<()> {
820        if self.codec.encoding() != PrimaryKeyEncoding::Sparse {
821            return Ok(());
822        }
823        let metadata = self.read_format.metadata();
824        let mut seen = HashSet::new();
825        let columns: Vec<_> = column_ids
826            .filter(|id| {
827                metadata.primary_key_index(*id).is_some()
828                    && self.read_format.projected_index_by_id(*id).is_none()
829                    && !state.decoded_tag_cache.contains_key(id)
830                    && seen.insert(*id)
831            })
832            .filter_map(|id| {
833                metadata
834                    .column_by_id(id)
835                    .map(|column| (id, column.column_schema.data_type.clone()))
836            })
837            .collect();
838        if columns.is_empty() {
839            return Ok(());
840        }
841        let decoded = match &mut state.decoded_pks {
842            Some(decoded) => decoded,
843            None => state
844                .decoded_pks
845                .insert(decode_primary_keys(self.codec.as_ref(), input)?),
846        };
847        if let [(column_id, column_type)] = columns.as_slice() {
848            let array = decoded.get_tag_column(*column_id, None, column_type)?;
849            state.decoded_tag_cache.insert(*column_id, array);
850        } else {
851            let arrays = decoded.get_sparse_tag_columns(&columns)?;
852            state.decoded_tag_cache.extend(
853                columns
854                    .into_iter()
855                    .map(|(column_id, _)| column_id)
856                    .zip(arrays),
857            );
858        }
859        Ok(())
860    }
861
862    /// Returns the decoded tag column for `column_id`, or `None` if it's not a tag.
863    fn maybe_decode_tag_column(
864        &self,
865        metadata: &RegionMetadataRef,
866        column_id: ColumnId,
867        input: &RecordBatch,
868        tag_decode_state: &mut TagDecodeState,
869    ) -> Result<Option<ArrayRef>> {
870        let Some(pk_index) = metadata.primary_key_index(column_id) else {
871            return Ok(None);
872        };
873
874        if let Some(cached_column) = tag_decode_state.decoded_tag_cache.get(&column_id) {
875            return Ok(Some(cached_column.clone()));
876        }
877
878        if tag_decode_state.decoded_pks.is_none() {
879            tag_decode_state.decoded_pks = Some(decode_primary_keys(self.codec.as_ref(), input)?);
880        }
881
882        let pk_index = if self.codec.encoding() == PrimaryKeyEncoding::Sparse {
883            None
884        } else {
885            Some(pk_index)
886        };
887        let Some(column_index) = metadata.column_index_by_id(column_id) else {
888            return Ok(None);
889        };
890        let Some(decoded) = tag_decode_state.decoded_pks.as_mut() else {
891            return Ok(None);
892        };
893
894        let column_metadata = &metadata.column_metadatas[column_index];
895        let tag_column = decoded.get_tag_column(
896            column_id,
897            pk_index,
898            &column_metadata.column_schema.data_type,
899        )?;
900        tag_decode_state
901            .decoded_tag_cache
902            .insert(column_id, tag_column.clone());
903
904        Ok(Some(tag_column))
905    }
906
907    /// Evaluates the partition filter against the input `RecordBatch`.
908    fn evaluate_partition_filter(
909        &self,
910        record_batch: &RecordBatch,
911        partition_filter: &PartitionFilterContext,
912    ) -> Result<BooleanBuffer> {
913        let columnar_value = partition_filter
914            .region_partition_physical_expr
915            .evaluate(record_batch)
916            .context(EvalPartitionFilterSnafu)?;
917        let array = columnar_value
918            .into_array(record_batch.num_rows())
919            .context(EvalPartitionFilterSnafu)?;
920        let boolean_array =
921            array
922                .as_any()
923                .downcast_ref::<BooleanArray>()
924                .context(UnexpectedSnafu {
925                    reason: "Failed to downcast to BooleanArray".to_string(),
926                })?;
927
928        // also need to consider nulls in the partition filter result. If a value is null, it should be treated as false (filtered out).
929        let mut mask = boolean_array.values().clone();
930        if let Some(nulls) = boolean_array.nulls() {
931            mask = mask.bitand(nulls.inner());
932        }
933
934        Ok(mask)
935    }
936
937    /// Projects the input `RecordBatch` to match the given schema.
938    ///
939    /// This is used for partition expression evaluation. The schema should only contain
940    /// the columns referenced by the partition expression to minimize overhead.
941    fn project_record_batch_for_pruning_flat(
942        &self,
943        input: &RecordBatch,
944        schema: &Arc<Schema>,
945        tag_decode_state: &mut TagDecodeState,
946    ) -> Result<RecordBatch> {
947        let arrow_schema = schema.arrow_schema();
948        let mut columns = Vec::with_capacity(arrow_schema.fields().len());
949
950        let metadata = self.read_format.metadata();
951
952        for field in arrow_schema.fields() {
953            let column_id = metadata.column_by_name(field.name()).map(|c| c.column_id);
954
955            let Some(column_id) = column_id else {
956                return UnexpectedSnafu {
957                    reason: format!(
958                        "Partition pruning schema expects column '{}' but it is missing in \
959                         region metadata",
960                        field.name()
961                    ),
962                }
963                .fail();
964            };
965
966            if let Some(idx) = self.read_format.projected_index_by_id(column_id) {
967                columns.push(input.column(idx).clone());
968                continue;
969            }
970
971            if metadata.time_index_column().column_id == column_id {
972                let time_index_pos = time_index_column_index(input.num_columns());
973                columns.push(input.column(time_index_pos).clone());
974                continue;
975            }
976
977            if let Some(tag_column) =
978                self.maybe_decode_tag_column(metadata, column_id, input, tag_decode_state)?
979            {
980                columns.push(tag_column);
981                continue;
982            }
983
984            return UnexpectedSnafu {
985                reason: format!(
986                    "Partition pruning schema expects column '{}' (id {}) but it is not \
987                     present in projected record batch",
988                    field.name(),
989                    column_id
990                ),
991            }
992            .fail();
993        }
994
995        RecordBatch::try_new(arrow_schema.clone(), columns).context(NewRecordBatchSnafu)
996    }
997}
998
999#[cfg(test)]
1000mod tests {
1001    use std::sync::Arc;
1002
1003    use super::*;
1004    use crate::read::read_columns::ReadColumns;
1005    use crate::sst::parquet::flat_format::FlatReadFormat;
1006    use crate::sst::{FlatSchemaOptions, to_flat_sst_arrow_schema};
1007    use crate::test_util::sst_util::{
1008        new_record_batch_with_custom_sequence, sst_region_metadata,
1009        sst_region_metadata_with_encoding,
1010    };
1011    use datafusion_expr::{col, lit};
1012    use datatypes::arrow::array::{
1013        BinaryDictionaryBuilder, TimestampMillisecondArray, UInt8Array, UInt64Array,
1014    };
1015    use datatypes::arrow::datatypes::UInt32Type;
1016    use datatypes::prelude::ConcreteDataType;
1017    use datatypes::schema::ColumnSchema;
1018    use datatypes::value::Value;
1019    use mito_codec::row_converter::SparsePrimaryKeyCodec;
1020    use parquet::arrow::arrow_reader::RowSelector;
1021    use partition::expr::col as partition_col;
1022    use store_api::metadata::RegionMetadataBuilder;
1023
1024    fn new_test_range_base(filters: Vec<SimpleFilterContext>) -> RangeBase {
1025        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
1026
1027        let read_format = FlatReadFormat::new(
1028            metadata.clone(),
1029            ReadColumns::new(metadata.column_metadatas.iter().map(|c| c.column_id)),
1030            None,
1031            "test",
1032            true,
1033        )
1034        .unwrap();
1035
1036        RangeBase {
1037            filters,
1038            dyn_filters: vec![],
1039            read_format,
1040            expected_metadata: None,
1041            prune_schema: metadata.schema.clone(),
1042            codec: mito_codec::row_converter::build_primary_key_codec(metadata.as_ref()),
1043            compat_batch: None,
1044            compaction_projection_mapper: None,
1045            pre_filter_mode: PreFilterMode::All,
1046            partition_filter: None,
1047        }
1048    }
1049
1050    #[test]
1051    fn test_compute_filter_mask_flat_applies_remaining_simple_filters() {
1052        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
1053        let filters = vec![
1054            SimpleFilterContext::new_opt(&metadata, None, &col("tag_0").eq(lit("a"))).unwrap(),
1055            SimpleFilterContext::new_opt(&metadata, None, &col("field_0").gt(lit(1_u64))).unwrap(),
1056        ];
1057        let base = new_test_range_base(filters);
1058        let batch = new_record_batch_with_custom_sequence(&["b", "x"], 0, 4, 1);
1059
1060        let mask = base
1061            .compute_filter_mask_flat(&batch, false, false, &mut TagDecodeState::new())
1062            .unwrap()
1063            .unwrap();
1064        assert_eq!(mask.count_set_bits(), 0);
1065
1066        let mask = base
1067            .compute_filter_mask_flat(&batch, false, true, &mut TagDecodeState::new())
1068            .unwrap()
1069            .unwrap();
1070        assert_eq!(mask.count_set_bits(), 2);
1071    }
1072
1073    #[test]
1074    fn test_precise_filter_flat_returns_input_when_nothing_is_filtered() {
1075        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
1076        let tag_filter =
1077            SimpleFilterContext::new_opt(&metadata, None, &col("tag_0").eq(lit("z"))).unwrap();
1078        let base = new_test_range_base(vec![tag_filter]);
1079        let batch = new_record_batch_with_custom_sequence(&["b", "x"], 0, 4, 1);
1080
1081        // The only filter is a tag filter, and it is skipped, so the batch must
1082        // come back untouched rather than being copied through `filter_record_batch`.
1083        let filtered = base
1084            .precise_filter_flat(batch.clone(), false, true)
1085            .unwrap()
1086            .unwrap();
1087        assert_eq!(batch, filtered);
1088    }
1089
1090    #[test]
1091    fn test_sparse_filter_and_partition_tags_preserve_skip_modes() {
1092        let metadata = Arc::new(sst_region_metadata_with_encoding(
1093            PrimaryKeyEncoding::Sparse,
1094        ));
1095        let codec = SparsePrimaryKeyCodec::schemaless();
1096        let long_tag = "x".repeat(80);
1097        let mut pk_builder = BinaryDictionaryBuilder::<UInt32Type>::new();
1098        for (series, tag0, tag1) in [
1099            (0, Some(long_tag.as_str()), "east"),
1100            (1, Some(""), "east"),
1101            (2, Some("b"), "west"),
1102            (3, None, "east"),
1103            (4, Some("c"), "east"),
1104            (0, Some(long_tag.as_str()), "east"),
1105        ] {
1106            let mut pk = Vec::new();
1107            codec.encode_internal(1, series, &mut pk).unwrap();
1108            let tags = tag0
1109                .map(|tag| (0, tag.as_bytes()))
1110                .into_iter()
1111                .chain([(1, tag1.as_bytes())]);
1112            codec.encode_raw_tag_value(tags, &mut pk).unwrap();
1113            pk_builder.append(pk).unwrap();
1114        }
1115        let raw = RecordBatch::try_new(
1116            to_flat_sst_arrow_schema(
1117                &metadata,
1118                &FlatSchemaOptions::from_encoding(PrimaryKeyEncoding::Sparse),
1119            ),
1120            vec![
1121                Arc::new(UInt64Array::from_iter_values(0..6)),
1122                Arc::new(TimestampMillisecondArray::from_iter_values(0..6)),
1123                Arc::new(pk_builder.finish()),
1124                Arc::new(UInt64Array::from(vec![1; 6])),
1125                Arc::new(UInt8Array::from(vec![0; 6])),
1126            ],
1127        )
1128        .unwrap();
1129        let filters: Vec<_> = [
1130            col("tag_0").gt(lit("")),
1131            col("tag_0").lt(lit("z")),
1132            col("field_0").gt(lit(1_u64)),
1133        ]
1134        .iter()
1135        .map(|expr| SimpleFilterContext::new_opt(&metadata, None, expr).unwrap())
1136        .collect();
1137        // Match build_partition_filter's dictionary schema for string tags.
1138        let partition_schema = Arc::new(Schema::new(vec![ColumnSchema::new(
1139            "tag_1",
1140            ConcreteDataType::dictionary_datatype(
1141                ConcreteDataType::uint32_datatype(),
1142                ConcreteDataType::string_datatype(),
1143            ),
1144            true,
1145        )]));
1146        let partition_expr = partition_col("tag_1")
1147            .eq(Value::String("east".into()))
1148            .try_as_physical_expr(partition_schema.arrow_schema())
1149            .unwrap();
1150
1151        // The partition tag is always encoded; tag_0 may already be materialized.
1152        for materialized in [false, true] {
1153            let read_format = FlatReadFormat::new(
1154                metadata.clone(),
1155                ReadColumns::new([0, 2, 3]),
1156                None,
1157                "test",
1158                !materialized,
1159            )
1160            .unwrap();
1161            let batch = read_format.convert_batch(raw.clone(), None).unwrap();
1162            let base = RangeBase {
1163                filters: filters.clone(),
1164                dyn_filters: vec![],
1165                read_format,
1166                expected_metadata: None,
1167                prune_schema: metadata.schema.clone(),
1168                codec: Arc::new(codec.clone()),
1169                compat_batch: None,
1170                compaction_projection_mapper: None,
1171                pre_filter_mode: PreFilterMode::All,
1172                partition_filter: Some(PartitionFilterContext {
1173                    partition_schema: partition_schema.clone(),
1174                    region_partition_physical_expr: partition_expr.clone(),
1175                }),
1176            };
1177            for (skip_fields, skip_tags, expected) in [
1178                (false, false, vec![4, 5]),
1179                (true, false, vec![0, 4, 5]),
1180                (false, true, vec![3, 4, 5]),
1181                (true, true, vec![0, 1, 3, 4, 5]),
1182            ] {
1183                let output = base
1184                    .precise_filter_flat(batch.clone(), skip_fields, skip_tags)
1185                    .unwrap()
1186                    .unwrap();
1187                let timestamps = output
1188                    .column(time_index_column_index(output.num_columns()))
1189                    .as_any()
1190                    .downcast_ref::<TimestampMillisecondArray>()
1191                    .unwrap();
1192                assert_eq!(
1193                    timestamps.values().as_ref(),
1194                    expected.as_slice(),
1195                    "materialized={materialized}, skip_fields={skip_fields}, skip_tags={skip_tags}"
1196                );
1197            }
1198        }
1199    }
1200
1201    #[test]
1202    fn test_compute_filter_mask_flat_does_not_postfilter_physical_filters() {
1203        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
1204        let read_format = FlatReadFormat::new(
1205            metadata.clone(),
1206            ReadColumns::new(metadata.column_metadatas.iter().map(|c| c.column_id)),
1207            None,
1208            "test",
1209            true,
1210        )
1211        .unwrap();
1212        let physical_filter = crate::sst::parquet::reader::PhysicalFilterContext::new_opt(
1213            &metadata,
1214            None,
1215            &read_format,
1216            &col("field_0").in_list(vec![lit(1_u64), lit(2_u64)], false),
1217        );
1218        assert!(physical_filter.is_some());
1219        let base = new_test_range_base(vec![]);
1220        let batch = new_record_batch_with_custom_sequence(&["b", "x"], 0, 4, 1);
1221
1222        let mask = base
1223            .compute_filter_mask_flat(&batch, false, false, &mut TagDecodeState::new())
1224            .unwrap()
1225            .unwrap();
1226        assert_eq!(mask.count_set_bits(), 4);
1227    }
1228
1229    #[test]
1230    fn test_precise_filter_flat_applies_partition_filter_when_skipping_tags() {
1231        let metadata: RegionMetadataRef = Arc::new(sst_region_metadata());
1232        let tag_filter =
1233            SimpleFilterContext::new_opt(&metadata, None, &col("tag_0").eq(lit("z"))).unwrap();
1234        let mut base = new_test_range_base(vec![tag_filter]);
1235        let batch = new_record_batch_with_custom_sequence(&["b", "x"], 0, 4, 1);
1236
1237        let batch_schema = batch.schema();
1238        let tag_field = batch_schema.field(0);
1239        let partition_schema = Arc::new(Schema::new(vec![ColumnSchema::new(
1240            "tag_0".to_string(),
1241            ConcreteDataType::from_arrow_type(tag_field.data_type()),
1242            tag_field.is_nullable(),
1243        )]));
1244        let partition_expr = partition_col("tag_0")
1245            .gt_eq(Value::String("a".into()))
1246            .and(partition_col("tag_0").lt(Value::String("c".into())));
1247        base.partition_filter = Some(PartitionFilterContext {
1248            region_partition_physical_expr: partition_expr
1249                .try_as_physical_expr(partition_schema.arrow_schema())
1250                .unwrap(),
1251            partition_schema,
1252        });
1253
1254        let filtered = base
1255            .precise_filter_flat(batch, false, true)
1256            .unwrap()
1257            .unwrap();
1258        assert_eq!(filtered.num_rows(), 4);
1259
1260        let out_of_partition = new_record_batch_with_custom_sequence(&["z", "x"], 0, 4, 1);
1261        assert!(
1262            base.precise_filter_flat(out_of_partition, false, true)
1263                .unwrap()
1264                .is_none()
1265        );
1266    }
1267
1268    #[test]
1269    fn test_refine_row_range_selection() {
1270        let original = Some(RowSelection::from(vec![
1271            RowSelector::skip(2),
1272            RowSelector::select(3),
1273            RowSelector::skip(1),
1274            RowSelector::select(2),
1275        ]));
1276        let selected = refine_row_range_selection(vec![0..3, 4..7, 9..10], 10, &original)
1277            .unwrap()
1278            .unwrap();
1279        assert_eq!(
1280            selected,
1281            RowSelection::from(vec![
1282                RowSelector::skip(2),
1283                RowSelector::select(1),
1284                RowSelector::skip(1),
1285                RowSelector::select(1),
1286                RowSelector::skip(1),
1287                RowSelector::select(1),
1288                RowSelector::skip(1),
1289            ])
1290        );
1291        assert!(
1292            refine_row_range_selection(vec![0..2, 8..10], 10, &original)
1293                .unwrap()
1294                .is_none()
1295        );
1296        assert!(
1297            refine_row_range_selection(vec![], 10, &None)
1298                .unwrap()
1299                .is_none()
1300        );
1301        assert_eq!(
1302            refine_row_range_selection(vec![2..4, 4..6], 10, &None)
1303                .unwrap()
1304                .unwrap(),
1305            RowSelection::from(vec![RowSelector::skip(2), RowSelector::select(4)])
1306        );
1307        for ranges in [
1308            std::iter::once(0..11).collect(),
1309            std::iter::once(11..12).collect(),
1310            std::iter::once(3..3).collect(),
1311            vec![4..6, 5..7],
1312            vec![4..6, 0..2],
1313        ] {
1314            assert!(refine_row_range_selection(ranges, 10, &None).is_err());
1315        }
1316    }
1317
1318    #[test]
1319    fn test_refine_primary_key_selection_intersects_original_selection() {
1320        let original = Some(RowSelection::from(vec![
1321            RowSelector::skip(2),
1322            RowSelector::select(3),
1323            RowSelector::skip(1),
1324            RowSelector::select(2),
1325        ]));
1326        let mask = BooleanArray::from(vec![true, false, true, false, true]);
1327        let actual = refine_primary_key_selection(&[mask], &original).unwrap();
1328        let expected = RowSelection::from(vec![
1329            RowSelector::skip(2),
1330            RowSelector::select(1),
1331            RowSelector::skip(1),
1332            RowSelector::select(1),
1333            RowSelector::skip(2),
1334            RowSelector::select(1),
1335        ]);
1336        assert_eq!(actual, expected);
1337    }
1338    #[test]
1339    fn test_pruned_missing_field_respects_skip_fields()
1340    -> std::result::Result<(), Box<dyn std::error::Error>> {
1341        let metadata = Arc::new(sst_region_metadata());
1342        let mut builder = RegionMetadataBuilder::from_existing(metadata.as_ref().clone());
1343        builder.push_column_metadata(store_api::metadata::ColumnMetadata {
1344            column_schema: ColumnSchema::new("added", ConcreteDataType::float64_datatype(), true),
1345            semantic_type: SemanticType::Field,
1346            column_id: 100,
1347        });
1348        let expected = builder.build()?;
1349        let filter =
1350            SimpleFilterContext::new_opt(&metadata, Some(&expected), &col("added").is_not_null())
1351                .ok_or("expected a missing-column filter")?;
1352        assert!(matches!(filter.filter(), MaybeFilter::Pruned));
1353        let base = new_test_range_base(vec![filter]);
1354        let batch = new_record_batch_with_custom_sequence(&["b", "x"], 0, 4, 1);
1355        assert!(
1356            base.compute_filter_mask_flat(&batch, false, false, &mut TagDecodeState::new())?
1357                .is_none()
1358        );
1359        let mask = base
1360            .compute_filter_mask_flat(&batch, true, false, &mut TagDecodeState::new())?
1361            .ok_or("skipped field filter must preserve backfill rows")?;
1362        assert_eq!(batch.num_rows(), mask.count_set_bits());
1363        Ok(())
1364    }
1365}