Skip to main content

mito2/read/
scan_region.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//! Scans a region according to the scan request.
16
17use std::collections::{BTreeMap, HashSet};
18use std::fmt;
19use std::num::NonZeroU64;
20use std::sync::Arc;
21use std::time::Instant;
22
23use api::v1::SemanticType;
24use common_error::ext::BoxedError;
25use common_recordbatch::SendableRecordBatchStream;
26use common_recordbatch::adapter::RegionQueryStatCounters;
27use common_recordbatch::filter::SimpleFilterEvaluator;
28use common_telemetry::tracing::Instrument;
29use common_telemetry::{debug, error, tracing, warn};
30use common_time::range::TimestampRange;
31use datafusion::execution::memory_pool::{MemoryPool, UnboundedMemoryPool};
32use datafusion::physical_plan::expressions::DynamicFilterPhysicalExpr;
33use datafusion_common::pruning::PruningStatistics;
34use datafusion_common::{Column, ScalarValue};
35use datafusion_expr::Expr;
36use datafusion_expr::utils::expr_to_columns;
37use datatypes::arrow::array::{ArrayRef, BooleanArray, UInt64Array};
38use datatypes::extension::json::is_json2_extension_type;
39use datatypes::prelude::ConcreteDataType;
40use datatypes::types::json_type::JsonNativeType;
41use datatypes::value::timestamp_to_scalar_value;
42use futures::StreamExt;
43use partition::expr::PartitionExpr;
44use smallvec::SmallVec;
45use snafu::{OptionExt, ResultExt, ensure};
46use store_api::metadata::{RegionMetadata, RegionMetadataRef};
47use store_api::region_engine::{PartitionRange, RegionScannerRef};
48use store_api::storage::{
49    ColumnId, RegionId, ScanRequest, SequenceNumber, SequenceRange, TimeSeriesDistribution,
50    TimeSeriesRowSelector,
51};
52use table::predicate::{Predicate, build_time_range_predicate, extract_time_range_from_expr};
53use tokio::sync::{Semaphore, mpsc};
54use tokio_stream::wrappers::ReceiverStream;
55
56use crate::access_layer::AccessLayerRef;
57use crate::cache::CacheStrategy;
58use crate::config::DEFAULT_MAX_CONCURRENT_SCAN_FILES;
59use crate::error::{InvalidPartitionExprSnafu, InvalidRequestSnafu, Result};
60#[cfg(feature = "enterprise")]
61use crate::extension::{BoxedExtensionRange, BoxedExtensionRangeProvider};
62use crate::memtable::{MemtableRange, RangesOptions};
63use crate::metrics::READ_SST_COUNT;
64use crate::read::compat::{self, FlatCompatBatch};
65use crate::read::flat_projection::FlatProjectionMapper;
66use crate::read::range::{FileRangeBuilder, MemRangeBuilder, RangeMeta, RowGroupIndex};
67use crate::read::range_cache::{ScanRequestFingerprint, implied_time_range_from_exprs};
68use crate::read::read_columns::ReadColumns;
69use crate::read::seq_scan::SeqScan;
70use crate::read::series_scan::SeriesScan;
71use crate::read::stream::ScanBatchStream;
72use crate::read::unordered_scan::UnorderedScan;
73use crate::read::{BoxedRecordBatchStream, RecordBatch};
74use crate::region::options::MergeMode;
75use crate::region::version::VersionRef;
76use crate::sst::file::FileHandle;
77use crate::sst::index::bloom_filter::applier::{
78    BloomFilterIndexApplierBuilder, BloomFilterIndexApplierRef,
79};
80use crate::sst::index::fulltext_index::applier::FulltextIndexApplierRef;
81use crate::sst::index::fulltext_index::applier::builder::FulltextIndexApplierBuilder;
82use crate::sst::index::inverted_index::applier::InvertedIndexApplierRef;
83use crate::sst::index::inverted_index::applier::builder::InvertedIndexApplierBuilder;
84#[cfg(feature = "vector_index")]
85use crate::sst::index::vector_index::applier::{VectorIndexApplier, VectorIndexApplierRef};
86use crate::sst::parquet::file_range::PreFilterMode;
87use crate::sst::parquet::reader::ReaderMetrics;
88
89#[cfg(feature = "vector_index")]
90const VECTOR_INDEX_OVERFETCH_MULTIPLIER: usize = 2;
91
92/// A scanner scans a region and returns a [SendableRecordBatchStream].
93pub(crate) enum Scanner {
94    /// Sequential scan.
95    Seq(SeqScan),
96    /// Unordered scan.
97    Unordered(UnorderedScan),
98    /// Per-series scan.
99    Series(SeriesScan),
100}
101
102impl Scanner {
103    /// Returns a [SendableRecordBatchStream] to retrieve scan results from all partitions.
104    #[tracing::instrument(level = tracing::Level::DEBUG, skip_all)]
105    pub(crate) async fn scan(&self) -> Result<SendableRecordBatchStream, BoxedError> {
106        match self {
107            Scanner::Seq(seq_scan) => seq_scan.build_stream(),
108            Scanner::Unordered(unordered_scan) => unordered_scan.build_stream().await,
109            Scanner::Series(series_scan) => series_scan.build_stream().await,
110        }
111    }
112
113    /// Create a stream of [`Batch`] by this scanner.
114    pub(crate) fn scan_batch(&self) -> Result<ScanBatchStream> {
115        match self {
116            Scanner::Seq(x) => x.scan_all_partitions(),
117            Scanner::Unordered(x) => x.scan_all_partitions(),
118            Scanner::Series(x) => x.scan_all_partitions(),
119        }
120    }
121}
122
123#[cfg(test)]
124impl Scanner {
125    /// Returns number of files to scan.
126    pub(crate) fn num_files(&self) -> usize {
127        match self {
128            Scanner::Seq(seq_scan) => seq_scan.input().num_files(),
129            Scanner::Unordered(unordered_scan) => unordered_scan.input().num_files(),
130            Scanner::Series(series_scan) => series_scan.input().num_files(),
131        }
132    }
133
134    /// Returns number of memtables to scan.
135    pub(crate) fn num_memtables(&self) -> usize {
136        match self {
137            Scanner::Seq(seq_scan) => seq_scan.input().num_memtables(),
138            Scanner::Unordered(unordered_scan) => unordered_scan.input().num_memtables(),
139            Scanner::Series(series_scan) => series_scan.input().num_memtables(),
140        }
141    }
142
143    /// Returns SST file ids to scan.
144    pub(crate) fn file_ids(&self) -> Vec<crate::sst::file::RegionFileId> {
145        match self {
146            Scanner::Seq(seq_scan) => seq_scan.input().file_ids(),
147            Scanner::Unordered(unordered_scan) => unordered_scan.input().file_ids(),
148            Scanner::Series(series_scan) => series_scan.input().file_ids(),
149        }
150    }
151
152    pub(crate) fn index_ids(&self) -> Vec<crate::sst::file::RegionIndexId> {
153        match self {
154            Scanner::Seq(seq_scan) => seq_scan.input().index_ids(),
155            Scanner::Unordered(unordered_scan) => unordered_scan.input().index_ids(),
156            Scanner::Series(series_scan) => series_scan.input().index_ids(),
157        }
158    }
159
160    pub(crate) fn snapshot_sequence(&self) -> Option<SequenceNumber> {
161        match self {
162            Scanner::Seq(seq_scan) => seq_scan.input().snapshot_sequence,
163            Scanner::Unordered(unordered_scan) => unordered_scan.input().snapshot_sequence,
164            Scanner::Series(series_scan) => series_scan.input().snapshot_sequence,
165        }
166    }
167
168    /// Sets the target partitions for the scanner. It can controls the parallelism of the scanner.
169    pub(crate) fn set_target_partitions(&mut self, target_partitions: usize) {
170        use store_api::region_engine::{PrepareRequest, RegionScanner};
171
172        let request = PrepareRequest::default().with_target_partitions(target_partitions);
173        match self {
174            Scanner::Seq(seq_scan) => seq_scan.prepare(request).unwrap(),
175            Scanner::Unordered(unordered_scan) => unordered_scan.prepare(request).unwrap(),
176            Scanner::Series(series_scan) => series_scan.prepare(request).unwrap(),
177        }
178    }
179}
180
181#[cfg_attr(doc, aquamarine::aquamarine)]
182/// Helper to scans a region by [ScanRequest].
183///
184/// [ScanRegion] collects SSTs and memtables to scan without actually reading them. It
185/// creates a [Scanner] to actually scan these targets in [Scanner::scan()].
186///
187/// ```mermaid
188/// classDiagram
189/// class ScanRegion {
190///     -VersionRef version
191///     -ScanRequest request
192///     ~scanner() Scanner
193///     ~seq_scan() SeqScan
194/// }
195/// class Scanner {
196///     <<enumeration>>
197///     SeqScan
198///     UnorderedScan
199///     +scan() SendableRecordBatchStream
200/// }
201/// class SeqScan {
202///     -ScanInput input
203///     +build() SendableRecordBatchStream
204/// }
205/// class UnorderedScan {
206///     -ScanInput input
207///     +build() SendableRecordBatchStream
208/// }
209/// class ScanInput {
210///     -ProjectionMapper mapper
211///     -Option~TimeRange~ time_range
212///     -Option~Predicate~ predicate
213///     -Vec~MemtableRef~ memtables
214///     -Vec~FileHandle~ files
215/// }
216/// class ProjectionMapper {
217///     ~output_schema() SchemaRef
218///     ~convert(Batch) RecordBatch
219/// }
220/// ScanRegion -- Scanner
221/// ScanRegion o-- ScanRequest
222/// Scanner o-- SeqScan
223/// Scanner o-- UnorderedScan
224/// SeqScan o-- ScanInput
225/// UnorderedScan o-- ScanInput
226/// Scanner -- SendableRecordBatchStream
227/// ScanInput o-- ProjectionMapper
228/// SeqScan -- SendableRecordBatchStream
229/// UnorderedScan -- SendableRecordBatchStream
230/// ```
231pub(crate) struct ScanRegion {
232    /// Version of the region at scan.
233    version: VersionRef,
234    /// Access layer of the region.
235    access_layer: AccessLayerRef,
236    /// Scan request.
237    request: ScanRequest,
238    /// Cache.
239    cache_strategy: CacheStrategy,
240    /// Maximum number of SST files to scan concurrently.
241    max_concurrent_scan_files: usize,
242    /// Memory pool shared by internal scan operators across all queries.
243    scan_memory_pool: Arc<dyn MemoryPool>,
244    /// Whether to enable the experimental two-phase metric series scan.
245    experimental_series_scan_v2: bool,
246    /// Whether to ignore inverted index.
247    ignore_inverted_index: bool,
248    /// Whether to ignore fulltext index.
249    ignore_fulltext_index: bool,
250    /// Whether to ignore bloom filter.
251    ignore_bloom_filter: bool,
252    /// Start time of the scan task.
253    start_time: Option<Instant>,
254    /// Whether to filter out the deleted rows.
255    /// Usually true for normal read, and false for scan for compaction.
256    filter_deleted: bool,
257    /// Counters that should receive query-load metrics.
258    query_stat_counters: Option<RegionQueryStatCounters>,
259    #[cfg(feature = "enterprise")]
260    extension_range_provider: Option<BoxedExtensionRangeProvider>,
261}
262
263impl ScanRegion {
264    /// Creates a [ScanRegion].
265    pub(crate) fn new(
266        version: VersionRef,
267        access_layer: AccessLayerRef,
268        request: ScanRequest,
269        cache_strategy: CacheStrategy,
270    ) -> ScanRegion {
271        ScanRegion {
272            version,
273            access_layer,
274            request,
275            cache_strategy,
276            max_concurrent_scan_files: DEFAULT_MAX_CONCURRENT_SCAN_FILES,
277            scan_memory_pool: Arc::new(UnboundedMemoryPool::default()),
278            experimental_series_scan_v2: false,
279            ignore_inverted_index: false,
280            ignore_fulltext_index: false,
281            ignore_bloom_filter: false,
282            start_time: None,
283            filter_deleted: true,
284            query_stat_counters: None,
285            #[cfg(feature = "enterprise")]
286            extension_range_provider: None,
287        }
288    }
289
290    /// Sets counters that should receive query-load metrics.
291    #[must_use]
292    pub(crate) fn with_query_stat_counters(mut self, counters: RegionQueryStatCounters) -> Self {
293        self.query_stat_counters = Some(counters);
294        self
295    }
296
297    /// Sets maximum number of SST files to scan concurrently.
298    #[must_use]
299    pub(crate) fn with_max_concurrent_scan_files(
300        mut self,
301        max_concurrent_scan_files: usize,
302    ) -> Self {
303        self.max_concurrent_scan_files = max_concurrent_scan_files;
304        self
305    }
306
307    /// Sets the memory pool shared by internal scan operators.
308    #[must_use]
309    pub(crate) fn with_scan_memory_pool(mut self, scan_memory_pool: Arc<dyn MemoryPool>) -> Self {
310        self.scan_memory_pool = scan_memory_pool;
311        self
312    }
313
314    /// Sets whether eligible metric series scans use the two-phase implementation.
315    #[must_use]
316    pub(crate) fn with_experimental_series_scan_v2(mut self, enabled: bool) -> Self {
317        self.experimental_series_scan_v2 = enabled;
318        self
319    }
320
321    /// Sets whether to ignore inverted index.
322    #[must_use]
323    pub(crate) fn with_ignore_inverted_index(mut self, ignore: bool) -> Self {
324        self.ignore_inverted_index = ignore;
325        self
326    }
327
328    /// Sets whether to ignore fulltext index.
329    #[must_use]
330    pub(crate) fn with_ignore_fulltext_index(mut self, ignore: bool) -> Self {
331        self.ignore_fulltext_index = ignore;
332        self
333    }
334
335    /// Sets whether to ignore bloom filter.
336    #[must_use]
337    pub(crate) fn with_ignore_bloom_filter(mut self, ignore: bool) -> Self {
338        self.ignore_bloom_filter = ignore;
339        self
340    }
341
342    #[must_use]
343    pub(crate) fn with_start_time(mut self, now: Instant) -> Self {
344        self.start_time = Some(now);
345        self
346    }
347
348    pub(crate) fn set_filter_deleted(&mut self, filter_deleted: bool) {
349        self.filter_deleted = filter_deleted;
350    }
351
352    #[cfg(feature = "enterprise")]
353    pub(crate) fn set_extension_range_provider(
354        &mut self,
355        extension_range_provider: BoxedExtensionRangeProvider,
356    ) {
357        self.extension_range_provider = Some(extension_range_provider);
358    }
359
360    /// Returns a [Scanner] to scan the region.
361    #[tracing::instrument(skip_all, fields(region_id = %self.region_id()))]
362    pub(crate) async fn scanner(self) -> Result<Scanner> {
363        if self.use_series_scan() {
364            self.series_scan().await.map(Scanner::Series)
365        } else if self.use_unordered_scan() {
366            // If table is append only and there is no series row selector, we use unordered scan in query.
367            // We still use seq scan in compaction.
368            self.unordered_scan().await.map(Scanner::Unordered)
369        } else {
370            self.seq_scan().await.map(Scanner::Seq)
371        }
372    }
373
374    /// Returns a [RegionScanner] to scan the region.
375    #[tracing::instrument(
376        level = tracing::Level::DEBUG,
377        skip_all,
378        fields(region_id = %self.region_id())
379    )]
380    pub(crate) async fn region_scanner(self) -> Result<RegionScannerRef> {
381        if self.use_series_scan() {
382            self.series_scan()
383                .await
384                .map(|scanner| Box::new(scanner) as _)
385        } else if self.use_unordered_scan() {
386            self.unordered_scan()
387                .await
388                .map(|scanner| Box::new(scanner) as _)
389        } else {
390            self.seq_scan().await.map(|scanner| Box::new(scanner) as _)
391        }
392    }
393
394    /// Scan sequentially.
395    #[tracing::instrument(skip_all, fields(region_id = %self.region_id()))]
396    pub(crate) async fn seq_scan(self) -> Result<SeqScan> {
397        let input = self.scan_input().await?.with_compaction(false);
398        Ok(SeqScan::new(input))
399    }
400
401    /// Unordered scan.
402    #[tracing::instrument(skip_all, fields(region_id = %self.region_id()))]
403    pub(crate) async fn unordered_scan(self) -> Result<UnorderedScan> {
404        let input = self.scan_input().await?;
405        Ok(UnorderedScan::new(input))
406    }
407
408    /// Scans by series.
409    #[tracing::instrument(skip_all, fields(region_id = %self.region_id()))]
410    pub(crate) async fn series_scan(self) -> Result<SeriesScan> {
411        let experimental_series_scan_v2 = self.experimental_series_scan_v2;
412        let input = self.scan_input().await?;
413        Ok(SeriesScan::new(input, experimental_series_scan_v2))
414    }
415
416    /// Returns true if the region can use unordered scan for current request.
417    fn use_unordered_scan(&self) -> bool {
418        // We use unordered scan when:
419        // 1. The region is in append mode.
420        // 2. There is no series row selector.
421        // 3. The required distribution is None or TimeSeriesDistribution::TimeWindowed.
422        //
423        // We still use seq scan in compaction.
424        self.version.options.append_mode
425            && self.request.series_row_selector.is_none()
426            && (self.request.distribution.is_none()
427                || self.request.distribution == Some(TimeSeriesDistribution::TimeWindowed))
428    }
429
430    /// Returns true if the region can use series scan for current request.
431    fn use_series_scan(&self) -> bool {
432        self.request.distribution == Some(TimeSeriesDistribution::PerSeries)
433    }
434
435    /// Creates a scan input.
436    #[tracing::instrument(skip_all, fields(region_id = %self.region_id()))]
437    async fn scan_input(self) -> Result<ScanInput> {
438        let metadata = &self.version.metadata;
439        let sst_min_sequence = self.request.sst_min_sequence.and_then(NonZeroU64::new);
440        let time_range = self.build_time_range_predicate();
441        let predicate = PredicateGroup::new(metadata, &self.request.filters)?;
442
443        let read_col_ids =
444            self.build_read_col_ids(self.request.projection.as_deref(), &predicate)?;
445        let read_cols = self.build_read_columns(&read_col_ids)?;
446
447        // The mapper always computes projected column ids as the schema of SSTs may change.
448        let projection = self
449            .request
450            .projection
451            .clone()
452            .unwrap_or_else(|| (0..metadata.column_metadatas.len()).collect());
453        let mapper = FlatProjectionMapper::new_with_read_columns(metadata, projection, read_cols)?;
454        let mapper = if self.request.preserve_pk_dictionary_encoding {
455            mapper.with_pk_dictionary_encoding()
456        } else {
457            mapper
458        };
459
460        let ssts = &self.version.ssts;
461        let mut files = Vec::new();
462        if !self.request.skip_sst_files {
463            for level in ssts.levels() {
464                for file in level.files.values() {
465                    let exceed_min_sequence = match (sst_min_sequence, file.meta_ref().sequence) {
466                        (Some(min_sequence), Some(file_sequence)) => file_sequence > min_sequence,
467                        // If the file's sequence is None (or actually is zero), it could mean the file
468                        // is generated and added to the region "directly". In this case, its data should
469                        // be considered as fresh as the memtable. So its sequence is treated greater than
470                        // the min_sequence, whatever the value of min_sequence is. Hence the default
471                        // "true" in this arm.
472                        (Some(_), None) => true,
473                        (None, _) => true,
474                    };
475
476                    // Finds SST files in range.
477                    if exceed_min_sequence && file_in_range(file, &time_range) {
478                        files.push(file.clone());
479                    }
480                    // There is no need to check and prune for file's sequence here as the sequence number is usually very new,
481                    // unless the timing is too good, or the sequence number wouldn't be in file.
482                    // and the batch will be filtered out by tree reader anyway.
483                }
484            }
485        }
486
487        let memtables = self.version.memtables.list_memtables();
488        // Skip empty memtables and memtables out of time range.
489        let mut mem_range_builders = Vec::new();
490        let filter_mode = pre_filter_mode(
491            self.version.options.append_mode,
492            self.version.options.merge_mode(),
493        );
494
495        for m in memtables {
496            // check if memtable is empty by reading stats.
497            let Some((start, end)) = m.stats().time_range() else {
498                continue;
499            };
500            // The time range of the memtable is inclusive.
501            let memtable_range = TimestampRange::new_inclusive(Some(start), Some(end));
502            if !memtable_range.intersects(&time_range) {
503                continue;
504            }
505            let ranges_in_memtable = m.ranges(
506                Some(&read_col_ids),
507                RangesOptions::default()
508                    .with_predicate(predicate.clone())
509                    .with_sequence(SequenceRange::new(
510                        self.request.memtable_min_sequence,
511                        self.request.memtable_max_sequence,
512                    ))
513                    .with_pre_filter_mode(filter_mode),
514            )?;
515            mem_range_builders.extend(ranges_in_memtable.ranges.into_values().map(|v| {
516                let stats = v.stats().clone();
517                MemRangeBuilder::new(v, stats)
518            }));
519        }
520
521        let region_id = self.region_id();
522        debug!(
523            "Scan region {}, request: {:?}, time range: {:?}, memtables: {}, ssts_to_read: {}, append_mode: {}",
524            region_id,
525            self.request,
526            time_range,
527            mem_range_builders.len(),
528            files.len(),
529            self.version.options.append_mode,
530        );
531
532        let (non_field_filters, field_filters) = self.partition_by_field_filters();
533        let inverted_index_appliers = [
534            self.build_invereted_index_applier(&non_field_filters),
535            self.build_invereted_index_applier(&field_filters),
536        ];
537        let bloom_filter_appliers = [
538            self.build_bloom_filter_applier(&non_field_filters),
539            self.build_bloom_filter_applier(&field_filters),
540        ];
541        let fulltext_index_appliers = [
542            self.build_fulltext_index_applier(&non_field_filters),
543            self.build_fulltext_index_applier(&field_filters),
544        ];
545        #[cfg(feature = "vector_index")]
546        let vector_index_applier = self.build_vector_index_applier();
547        #[cfg(feature = "vector_index")]
548        let vector_index_k = self.request.vector_search.as_ref().map(|search| {
549            if self.request.filters.is_empty() {
550                search.k
551            } else {
552                search.k.saturating_mul(VECTOR_INDEX_OVERFETCH_MULTIPLIER)
553            }
554        });
555
556        let input = ScanInput::new(self.access_layer, mapper)
557            .with_time_range(Some(time_range))
558            .with_predicate(predicate)
559            .with_memtables(mem_range_builders)
560            .with_files(files)
561            .with_cache(self.cache_strategy)
562            .with_inverted_index_appliers(inverted_index_appliers)
563            .with_bloom_filter_index_appliers(bloom_filter_appliers)
564            .with_fulltext_index_appliers(fulltext_index_appliers)
565            .with_max_concurrent_scan_files(self.max_concurrent_scan_files)
566            .with_scan_memory_pool(self.scan_memory_pool)
567            .with_start_time(self.start_time)
568            .with_append_mode(self.version.options.append_mode)
569            .with_filter_deleted(self.filter_deleted)
570            .with_merge_mode(self.version.options.merge_mode())
571            .with_series_row_selector(self.request.series_row_selector)
572            .with_distribution(self.request.distribution)
573            .with_explain_flat_format(
574                self.version.options.sst_format == Some(crate::sst::FormatType::Flat),
575            )
576            .with_snapshot_sequence(
577                self.request
578                    .snapshot_on_scan
579                    .then_some(self.request.memtable_max_sequence)
580                    .flatten(),
581            )
582            .with_query_stat_counters(self.query_stat_counters);
583        #[cfg(feature = "vector_index")]
584        let input = input
585            .with_vector_index_applier(vector_index_applier)
586            .with_vector_index_k(vector_index_k);
587
588        #[cfg(feature = "enterprise")]
589        let input = if !self.request.skip_sst_files
590            && let Some(provider) = self.extension_range_provider
591        {
592            let ranges = provider
593                .find_extension_ranges(self.version.flushed_sequence, time_range, &self.request)
594                .await?;
595            debug!("Find extension ranges: {ranges:?}");
596            input.with_extension_ranges(ranges)
597        } else {
598            input
599        };
600        Ok(input)
601    }
602
603    /// Builds the deduplicated root column ids required by the projection and
604    /// predicate.
605    fn build_read_col_ids(
606        &self,
607        projection: Option<&[usize]>,
608        predicate: &PredicateGroup,
609    ) -> Result<Vec<ColumnId>> {
610        let metadata = &self.version.metadata;
611        let Some(projection) = projection else {
612            return Ok(metadata
613                .column_metadatas
614                .iter()
615                .map(|col| col.column_id)
616                .collect());
617        };
618
619        let mut read_col_ids = Vec::new();
620        let mut seen = HashSet::new();
621        for idx in projection {
622            let col_id = metadata
623                .column_metadatas
624                .get(*idx)
625                .with_context(|| InvalidRequestSnafu {
626                    region_id: metadata.region_id,
627                    reason: format!("projection index {} is out of bounds", idx),
628                })?
629                .column_id;
630            let inserted = seen.insert(col_id);
631            // DataFusion's logical `OptimizeProjections` rule deduplicates pushed-down
632            // projection indices via `RequiredIndices::compact()`.
633            debug_assert!(
634                inserted,
635                "projection contains duplicate column id: {}",
636                col_id
637            );
638            // Keep the projection order.
639            read_col_ids.push(col_id);
640        }
641
642        if projection.is_empty() {
643            let time_index = metadata.time_index_column().column_id;
644            if seen.insert(time_index) {
645                read_col_ids.push(time_index);
646            }
647        }
648
649        let mut extra_col_names = HashSet::new();
650        let mut cols = HashSet::new();
651
652        if let Some(p) = predicate.predicate_without_region() {
653            for expr in p.exprs() {
654                cols.clear();
655                if expr_to_columns(expr, &mut cols).is_err() {
656                    continue;
657                }
658                extra_col_names.extend(cols.iter().map(|col| col.name.clone()));
659            }
660        }
661
662        if let Some(expr) = predicate.region_partition_expr() {
663            expr.collect_column_names(&mut extra_col_names);
664        }
665
666        if !extra_col_names.is_empty() {
667            for col in &metadata.column_metadatas {
668                if extra_col_names.remove(&col.column_schema.name) && !seen.contains(&col.column_id)
669                {
670                    read_col_ids.push(col.column_id);
671                }
672            }
673            if !extra_col_names.is_empty() {
674                warn!(
675                    "Some columns in filters are not found in region {}: {:?}",
676                    metadata.region_id, extra_col_names
677                );
678            }
679        }
680        Ok(read_col_ids)
681    }
682
683    /// Builds logical read columns and attaches JSON2 target types when needed.
684    ///
685    /// The behavior about JSON2 is as follows:
686    /// - JSON2 columns without hints use Variant to read the whole column.
687    /// - Hints targeting non-JSON2 read columns are rejected.
688    fn build_read_columns(&self, col_ids: &[ColumnId]) -> Result<ReadColumns> {
689        let metadata = &self.version.metadata;
690        let json_type_hint = &self.request.json_type_hint;
691
692        let has_json2 = metadata
693            .schema
694            .arrow_schema()
695            .fields()
696            .iter()
697            .any(is_json2_extension_type);
698
699        if !has_json2 && json_type_hint.is_empty() {
700            return Ok(ReadColumns::new(col_ids.iter().copied()));
701        }
702
703        let mut json_target_types = BTreeMap::new();
704        for &col_id in col_ids {
705            let Some(col) = metadata.column_by_id(col_id) else {
706                continue;
707            };
708            let col_name = &col.column_schema.name;
709            let hint = json_type_hint.get(col_name);
710            if !col.column_schema.data_type.is_json2() {
711                ensure!(
712                    hint.is_none(),
713                    InvalidRequestSnafu {
714                        region_id: metadata.region_id,
715                        reason: format!(
716                            "JSON type hint targets non-JSON2 column {} (id: {}, type: {})",
717                            col_name, col_id, col.column_schema.data_type
718                        ),
719                    }
720                );
721                continue;
722            }
723            let target_type = hint.cloned().unwrap_or(JsonNativeType::Variant);
724            json_target_types.insert(col_id, target_type);
725        }
726        Ok(ReadColumns::new(col_ids.iter().copied()).with_json_target_types(json_target_types))
727    }
728
729    fn region_id(&self) -> RegionId {
730        self.version.metadata.region_id
731    }
732
733    /// Build time range predicate from filters.
734    fn build_time_range_predicate(&self) -> TimestampRange {
735        let time_index = self.version.metadata.time_index_column();
736        let unit = time_index
737            .column_schema
738            .data_type
739            .as_timestamp()
740            .expect("Time index must have timestamp-compatible type")
741            .unit();
742        build_time_range_predicate(&time_index.column_schema.name, unit, &self.request.filters)
743    }
744
745    /// Partitions filters into two groups: non-field filters and field filters.
746    /// Returns `(non_field_filters, field_filters)`.
747    fn partition_by_field_filters(&self) -> (Vec<Expr>, Vec<Expr>) {
748        let field_columns = self
749            .version
750            .metadata
751            .field_columns()
752            .map(|col| &col.column_schema.name)
753            .collect::<HashSet<_>>();
754
755        let mut columns = HashSet::new();
756
757        self.request.filters.iter().cloned().partition(|expr| {
758            columns.clear();
759            // `expr_to_columns` won't return error.
760            if expr_to_columns(expr, &mut columns).is_err() {
761                // If we can't extract columns, treat it as non-field filter
762                return true;
763            }
764            // Return true for non-field filters (partition puts true cases in first vec)
765            !columns
766                .iter()
767                .any(|column| field_columns.contains(&column.name))
768        })
769    }
770
771    /// Use the latest schema to build the inverted index applier.
772    fn build_invereted_index_applier(&self, filters: &[Expr]) -> Option<InvertedIndexApplierRef> {
773        if self.ignore_inverted_index {
774            return None;
775        }
776
777        let file_cache = self.cache_strategy.write_cache().map(|w| w.file_cache());
778        let inverted_index_cache = self.cache_strategy.inverted_index_cache().cloned();
779
780        let puffin_metadata_cache = self.cache_strategy.puffin_metadata_cache().cloned();
781
782        InvertedIndexApplierBuilder::new(
783            self.access_layer.table_dir().to_string(),
784            self.access_layer.path_type(),
785            self.access_layer.object_store().clone(),
786            self.version.metadata.as_ref(),
787            self.version.metadata.inverted_indexed_column_ids(
788                self.version
789                    .options
790                    .index_options
791                    .inverted_index
792                    .ignore_column_ids
793                    .iter(),
794            ),
795            self.access_layer.puffin_manager_factory().clone(),
796        )
797        .with_file_cache(file_cache)
798        .with_inverted_index_cache(inverted_index_cache)
799        .with_puffin_metadata_cache(puffin_metadata_cache)
800        .build(filters)
801        .inspect_err(|err| warn!(err; "Failed to build invereted index applier"))
802        .ok()
803        .flatten()
804        .map(Arc::new)
805    }
806
807    /// Use the latest schema to build the bloom filter index applier.
808    fn build_bloom_filter_applier(&self, filters: &[Expr]) -> Option<BloomFilterIndexApplierRef> {
809        if self.ignore_bloom_filter {
810            return None;
811        }
812
813        let file_cache = self.cache_strategy.write_cache().map(|w| w.file_cache());
814        let bloom_filter_index_cache = self.cache_strategy.bloom_filter_index_cache().cloned();
815        let puffin_metadata_cache = self.cache_strategy.puffin_metadata_cache().cloned();
816
817        BloomFilterIndexApplierBuilder::new(
818            self.access_layer.table_dir().to_string(),
819            self.access_layer.path_type(),
820            self.access_layer.object_store().clone(),
821            self.version.metadata.as_ref(),
822            self.access_layer.puffin_manager_factory().clone(),
823        )
824        .with_file_cache(file_cache)
825        .with_bloom_filter_index_cache(bloom_filter_index_cache)
826        .with_puffin_metadata_cache(puffin_metadata_cache)
827        .build(filters)
828        .inspect_err(|err| warn!(err; "Failed to build bloom filter index applier"))
829        .ok()
830        .flatten()
831        .map(Arc::new)
832    }
833
834    /// Use the latest schema to build the fulltext index applier.
835    fn build_fulltext_index_applier(&self, filters: &[Expr]) -> Option<FulltextIndexApplierRef> {
836        if self.ignore_fulltext_index {
837            return None;
838        }
839
840        let file_cache = self.cache_strategy.write_cache().map(|w| w.file_cache());
841        let puffin_metadata_cache = self.cache_strategy.puffin_metadata_cache().cloned();
842        let bloom_filter_index_cache = self.cache_strategy.bloom_filter_index_cache().cloned();
843        FulltextIndexApplierBuilder::new(
844            self.access_layer.table_dir().to_string(),
845            self.access_layer.path_type(),
846            self.access_layer.object_store().clone(),
847            self.access_layer.puffin_manager_factory().clone(),
848            self.version.metadata.as_ref(),
849        )
850        .with_file_cache(file_cache)
851        .with_puffin_metadata_cache(puffin_metadata_cache)
852        .with_bloom_filter_cache(bloom_filter_index_cache)
853        .build(filters)
854        .inspect_err(|err| warn!(err; "Failed to build fulltext index applier"))
855        .ok()
856        .flatten()
857        .map(Arc::new)
858    }
859
860    /// Build the vector index applier from vector search request.
861    #[cfg(feature = "vector_index")]
862    fn build_vector_index_applier(&self) -> Option<VectorIndexApplierRef> {
863        let vector_search = self.request.vector_search.as_ref()?;
864
865        let file_cache = self.cache_strategy.write_cache().map(|w| w.file_cache());
866        let puffin_metadata_cache = self.cache_strategy.puffin_metadata_cache().cloned();
867        let vector_index_cache = self.cache_strategy.vector_index_cache().cloned();
868
869        let applier = VectorIndexApplier::new(
870            self.access_layer.table_dir().to_string(),
871            self.access_layer.path_type(),
872            self.access_layer.object_store().clone(),
873            self.access_layer.puffin_manager_factory().clone(),
874            vector_search.column_id,
875            vector_search.query_vector.clone(),
876            vector_search.metric,
877        )
878        .with_file_cache(file_cache)
879        .with_puffin_metadata_cache(puffin_metadata_cache)
880        .with_vector_index_cache(vector_index_cache);
881
882        Some(Arc::new(applier))
883    }
884}
885
886/// Returns true if the time range of a SST `file` matches the `predicate`.
887fn file_in_range(file: &FileHandle, predicate: &TimestampRange) -> bool {
888    if predicate == &TimestampRange::min_to_max() {
889        return true;
890    }
891    // end timestamp of a SST is inclusive.
892    let (start, end) = file.time_range();
893    let file_ts_range = TimestampRange::new_inclusive(Some(start), Some(end));
894    file_ts_range.intersects(predicate)
895}
896
897/// Common input for different scanners.
898pub struct ScanInput {
899    /// Region SST access layer.
900    access_layer: AccessLayerRef,
901    /// Maps projected Batches to RecordBatches.
902    pub(crate) mapper: Arc<FlatProjectionMapper>,
903    /// The columns to read from memtables and SSTs.
904    /// Notice this is different from the columns in `mapper` which are projected columns.
905    /// But this read columns might also include non-projected columns needed for filtering.
906    pub(crate) read_cols: ReadColumns,
907    /// Time range filter for time index.
908    pub(crate) time_range: Option<TimestampRange>,
909    /// Predicate to push down.
910    pub(crate) predicate: PredicateGroup,
911    /// Region partition expr applied at read time.
912    region_partition_expr: Option<PartitionExpr>,
913    /// Memtable range builders for memtables in the time range..
914    pub(crate) memtables: Vec<MemRangeBuilder>,
915    /// Handles to SST files to scan.
916    pub(crate) files: Vec<FileHandle>,
917    /// Scan-wide hint for rows in an execution batch.
918    batch_size: usize,
919    /// Cache.
920    pub(crate) cache_strategy: CacheStrategy,
921    /// Ignores file not found error.
922    ignore_file_not_found: bool,
923    /// Maximum number of SST files to scan concurrently.
924    pub(crate) max_concurrent_scan_files: usize,
925    /// Memory pool shared by internal scan operators across all queries.
926    pub(crate) scan_memory_pool: Arc<dyn MemoryPool>,
927    /// Index appliers.
928    inverted_index_appliers: [Option<InvertedIndexApplierRef>; 2],
929    bloom_filter_index_appliers: [Option<BloomFilterIndexApplierRef>; 2],
930    fulltext_index_appliers: [Option<FulltextIndexApplierRef>; 2],
931    /// Vector index applier for KNN search.
932    #[cfg(feature = "vector_index")]
933    pub(crate) vector_index_applier: Option<VectorIndexApplierRef>,
934    /// Over-fetched k for vector index scan.
935    #[cfg(feature = "vector_index")]
936    pub(crate) vector_index_k: Option<usize>,
937    /// Start time of the query.
938    pub(crate) query_start: Option<Instant>,
939    /// The region is using append mode.
940    pub(crate) append_mode: bool,
941    /// Whether to remove deletion markers.
942    pub(crate) filter_deleted: bool,
943    /// Mode to merge duplicate rows.
944    pub(crate) merge_mode: MergeMode,
945    /// Hint to select rows from time series.
946    pub(crate) series_row_selector: Option<TimeSeriesRowSelector>,
947    /// Hint for the required distribution of the scanner.
948    pub(crate) distribution: Option<TimeSeriesDistribution>,
949    /// Whether the region's configured SST format is flat.
950    explain_flat_format: bool,
951    /// Snapshot upper bound bound at scan open and propagated back to the caller.
952    pub(crate) snapshot_sequence: Option<SequenceNumber>,
953    /// Whether this scan is for compaction.
954    pub(crate) compaction: bool,
955    /// Counters that should receive query-load metrics.
956    pub(crate) query_stat_counters: Option<RegionQueryStatCounters>,
957    #[cfg(feature = "enterprise")]
958    extension_ranges: Vec<BoxedExtensionRange>,
959}
960
961impl ScanInput {
962    /// Creates a new [ScanInput].
963    #[must_use]
964    pub(crate) fn new(access_layer: AccessLayerRef, mapper: FlatProjectionMapper) -> ScanInput {
965        ScanInput {
966            access_layer,
967            read_cols: mapper.read_columns().clone(),
968            mapper: Arc::new(mapper),
969            time_range: None,
970            predicate: PredicateGroup::default(),
971            region_partition_expr: None,
972            memtables: Vec::new(),
973            files: Vec::new(),
974            batch_size: crate::sst::parquet::DEFAULT_READ_BATCH_SIZE,
975            cache_strategy: CacheStrategy::Disabled,
976            ignore_file_not_found: false,
977            max_concurrent_scan_files: DEFAULT_MAX_CONCURRENT_SCAN_FILES,
978            scan_memory_pool: Arc::new(UnboundedMemoryPool::default()),
979            inverted_index_appliers: [None, None],
980            bloom_filter_index_appliers: [None, None],
981            fulltext_index_appliers: [None, None],
982            #[cfg(feature = "vector_index")]
983            vector_index_applier: None,
984            #[cfg(feature = "vector_index")]
985            vector_index_k: None,
986            query_start: None,
987            append_mode: false,
988            filter_deleted: true,
989            merge_mode: MergeMode::default(),
990            series_row_selector: None,
991            distribution: None,
992            explain_flat_format: false,
993            snapshot_sequence: None,
994            compaction: false,
995            query_stat_counters: None,
996            #[cfg(feature = "enterprise")]
997            extension_ranges: Vec::new(),
998        }
999    }
1000
1001    /// Sets time range filter for time index.
1002    #[must_use]
1003    pub(crate) fn with_time_range(mut self, time_range: Option<TimestampRange>) -> Self {
1004        self.time_range = time_range;
1005        self
1006    }
1007
1008    /// Sets predicate to push down.
1009    #[must_use]
1010    pub(crate) fn with_predicate(mut self, predicate: PredicateGroup) -> Self {
1011        self.region_partition_expr = predicate.region_partition_expr().cloned();
1012        self.predicate = predicate;
1013        self
1014    }
1015
1016    /// Sets memtable range builders.
1017    #[must_use]
1018    pub(crate) fn with_memtables(mut self, memtables: Vec<MemRangeBuilder>) -> Self {
1019        self.memtables = memtables;
1020        self
1021    }
1022
1023    /// Sets files to read.
1024    #[must_use]
1025    pub(crate) fn with_files(mut self, files: Vec<FileHandle>) -> Self {
1026        self.files = files;
1027        self
1028    }
1029
1030    /// Returns the scan-wide hint for rows in an execution batch.
1031    pub(crate) fn batch_size(&self) -> usize {
1032        self.batch_size
1033    }
1034
1035    /// Sets the scan-wide hint for rows in an execution batch.
1036    #[must_use]
1037    pub(crate) fn with_batch_size(mut self, batch_size: usize) -> Self {
1038        self.batch_size = batch_size;
1039        self
1040    }
1041
1042    /// Sets cache for this query.
1043    #[must_use]
1044    pub(crate) fn with_cache(mut self, cache: CacheStrategy) -> Self {
1045        self.cache_strategy = cache;
1046        self
1047    }
1048
1049    /// Ignores file not found error.
1050    #[must_use]
1051    pub(crate) fn with_ignore_file_not_found(mut self, ignore: bool) -> Self {
1052        self.ignore_file_not_found = ignore;
1053        self
1054    }
1055
1056    /// Sets maximum number of SST files to scan concurrently.
1057    #[must_use]
1058    pub(crate) fn with_max_concurrent_scan_files(
1059        mut self,
1060        max_concurrent_scan_files: usize,
1061    ) -> Self {
1062        self.max_concurrent_scan_files = max_concurrent_scan_files;
1063        self
1064    }
1065
1066    /// Sets the memory pool shared by internal scan operators.
1067    #[must_use]
1068    pub(crate) fn with_scan_memory_pool(mut self, scan_memory_pool: Arc<dyn MemoryPool>) -> Self {
1069        self.scan_memory_pool = scan_memory_pool;
1070        self
1071    }
1072
1073    /// Sets inverted index appliers.
1074    #[must_use]
1075    pub(crate) fn with_inverted_index_appliers(
1076        mut self,
1077        appliers: [Option<InvertedIndexApplierRef>; 2],
1078    ) -> Self {
1079        self.inverted_index_appliers = appliers;
1080        self
1081    }
1082
1083    /// Sets bloom filter appliers.
1084    #[must_use]
1085    pub(crate) fn with_bloom_filter_index_appliers(
1086        mut self,
1087        appliers: [Option<BloomFilterIndexApplierRef>; 2],
1088    ) -> Self {
1089        self.bloom_filter_index_appliers = appliers;
1090        self
1091    }
1092
1093    /// Sets fulltext index appliers.
1094    #[must_use]
1095    pub(crate) fn with_fulltext_index_appliers(
1096        mut self,
1097        appliers: [Option<FulltextIndexApplierRef>; 2],
1098    ) -> Self {
1099        self.fulltext_index_appliers = appliers;
1100        self
1101    }
1102
1103    /// Sets vector index applier for KNN search.
1104    #[cfg(feature = "vector_index")]
1105    #[must_use]
1106    pub(crate) fn with_vector_index_applier(
1107        mut self,
1108        applier: Option<VectorIndexApplierRef>,
1109    ) -> Self {
1110        self.vector_index_applier = applier;
1111        self
1112    }
1113
1114    /// Sets over-fetched k for vector index scan.
1115    #[cfg(feature = "vector_index")]
1116    #[must_use]
1117    pub(crate) fn with_vector_index_k(mut self, k: Option<usize>) -> Self {
1118        self.vector_index_k = k;
1119        self
1120    }
1121
1122    /// Sets start time of the query.
1123    #[must_use]
1124    pub(crate) fn with_start_time(mut self, now: Option<Instant>) -> Self {
1125        self.query_start = now;
1126        self
1127    }
1128
1129    #[must_use]
1130    pub(crate) fn with_append_mode(mut self, is_append_mode: bool) -> Self {
1131        self.append_mode = is_append_mode;
1132        self
1133    }
1134
1135    pub(crate) fn with_query_stat_counters(
1136        mut self,
1137        counters: Option<RegionQueryStatCounters>,
1138    ) -> Self {
1139        self.query_stat_counters = counters;
1140        self
1141    }
1142
1143    /// Sets whether to remove deletion markers during scan.
1144    #[must_use]
1145    pub(crate) fn with_filter_deleted(mut self, filter_deleted: bool) -> Self {
1146        self.filter_deleted = filter_deleted;
1147        self
1148    }
1149
1150    /// Sets the merge mode.
1151    #[must_use]
1152    pub(crate) fn with_merge_mode(mut self, merge_mode: MergeMode) -> Self {
1153        self.merge_mode = merge_mode;
1154        self
1155    }
1156
1157    /// Sets the distribution hint.
1158    #[must_use]
1159    pub(crate) fn with_distribution(
1160        mut self,
1161        distribution: Option<TimeSeriesDistribution>,
1162    ) -> Self {
1163        self.distribution = distribution;
1164        self
1165    }
1166
1167    /// Sets whether the region's configured SST format is flat for explain output.
1168    #[must_use]
1169    pub(crate) fn with_explain_flat_format(mut self, explain_flat_format: bool) -> Self {
1170        self.explain_flat_format = explain_flat_format;
1171        self
1172    }
1173
1174    /// Sets the time series row selector.
1175    #[must_use]
1176    pub(crate) fn with_series_row_selector(
1177        mut self,
1178        series_row_selector: Option<TimeSeriesRowSelector>,
1179    ) -> Self {
1180        self.series_row_selector = series_row_selector;
1181        self
1182    }
1183
1184    #[must_use]
1185    pub(crate) fn with_snapshot_sequence(
1186        mut self,
1187        snapshot_sequence: Option<SequenceNumber>,
1188    ) -> Self {
1189        self.snapshot_sequence = snapshot_sequence;
1190        self
1191    }
1192
1193    /// Sets whether this scan is for compaction.
1194    #[must_use]
1195    pub(crate) fn with_compaction(mut self, compaction: bool) -> Self {
1196        self.compaction = compaction;
1197        self
1198    }
1199
1200    /// Builds memtable ranges to scan by `index`.
1201    pub(crate) fn build_mem_ranges(&self, index: RowGroupIndex) -> SmallVec<[MemtableRange; 2]> {
1202        let memtable = &self.memtables[index.index];
1203        let mut ranges = SmallVec::new();
1204        memtable.build_ranges(index.row_group_index, &mut ranges);
1205        ranges
1206    }
1207
1208    pub(crate) fn predicate_for_file(&self, file: &FileHandle) -> Option<Predicate> {
1209        if self.should_skip_region_partition(file) {
1210            self.predicate.predicate_without_region().cloned()
1211        } else {
1212            self.predicate.predicate().cloned()
1213        }
1214    }
1215
1216    fn should_skip_region_partition(&self, file: &FileHandle) -> bool {
1217        match (
1218            self.region_partition_expr.as_ref(),
1219            file.meta_ref().partition_expr.as_ref(),
1220        ) {
1221            (Some(region_expr), Some(file_expr)) => region_expr == file_expr,
1222            _ => false,
1223        }
1224    }
1225
1226    /// Tries to build file-level pruning statistics using only the [FileHandle]'s manifest-level
1227    /// time range, without reading any parquet metadata.
1228    ///
1229    /// Returns `None` if timestamp unit conversion overflows (conservative: keep the file).
1230    fn try_file_level_pruning_stats(&self, file: &FileHandle) -> Option<FileLevelPruningStats> {
1231        let (ts_min, ts_max) = file.time_range();
1232        let time_index = self.mapper.metadata().time_index_column();
1233        let time_index_unit = time_index.column_schema.data_type.as_timestamp()?.unit();
1234
1235        // Convert file timestamps to the time index column's unit. Use `convert_to_ceil` for
1236        // the upper bound to avoid accidentally shrinking the manifest range.
1237        let min_ts = ts_min.convert_to(time_index_unit)?;
1238        let max_ts = ts_max.convert_to_ceil(time_index_unit)?;
1239
1240        Some(FileLevelPruningStats {
1241            min_scalar: timestamp_to_scalar_value(time_index_unit, Some(min_ts.value())),
1242            max_scalar: timestamp_to_scalar_value(time_index_unit, Some(max_ts.value())),
1243            time_index_col_name: time_index.column_schema.name.clone(),
1244        })
1245    }
1246
1247    /// Checks whether a file can be definitively pruned using only its manifest-level
1248    /// time range and the current predicate, without reading any parquet metadata.
1249    ///
1250    /// Returns `true` if [PruningStatistics] proves the file cannot contain matching rows.
1251    #[inline]
1252    pub(crate) fn can_manifest_prune_file(&self, file: &FileHandle) -> bool {
1253        let predicate = self.predicate_for_file(file);
1254        self.manifest_prunes_file(file, predicate.as_ref())
1255    }
1256
1257    fn manifest_prunes_file(&self, file: &FileHandle, predicate: Option<&Predicate>) -> bool {
1258        if let Some(pred) = predicate
1259            && !pred.is_empty()
1260            && let Some(file_level_stats) = self.try_file_level_pruning_stats(file)
1261        {
1262            let pruning_results = pred.prune_with_stats(
1263                &file_level_stats,
1264                self.mapper.metadata().schema.arrow_schema(),
1265            );
1266            pruning_results.first() == Some(&false)
1267        } else {
1268            false
1269        }
1270    }
1271
1272    /// Prunes a file to scan and returns the builder to build readers.
1273    ///
1274    /// This is the public entry point used by direct tests and non-pruner callers.
1275    /// It performs its own manifest-level pruning check internally.
1276    #[tracing::instrument(
1277        skip_all,
1278        fields(
1279            region_id = %self.region_metadata().region_id,
1280            file_id = %file.file_id()
1281        )
1282    )]
1283    pub async fn prune_file(
1284        &self,
1285        file: &FileHandle,
1286        pre_filter_mode: PreFilterMode,
1287        reader_metrics: &mut ReaderMetrics,
1288    ) -> Result<FileRangeBuilder> {
1289        let predicate = self.predicate_for_file(file);
1290
1291        // Early file-level pruning using manifest time range before any parquet metadata access.
1292        if self.manifest_prunes_file(file, predicate.as_ref()) {
1293            reader_metrics.filter_metrics.files_time_range_pruned += 1;
1294            return Ok(FileRangeBuilder::default());
1295        }
1296
1297        self.prune_file_after_manifest_check(file, pre_filter_mode, true, predicate, reader_metrics)
1298            .await
1299    }
1300
1301    /// Second half of `prune_file` — performs the actual parquet metadata /
1302    /// reader setup. Callers that already performed manifest-level pruning
1303    /// (e.g. the `Pruner` via its shared `manifest_pruned_files` cache) should
1304    /// call this directly to avoid a redundant manifest check.
1305    ///
1306    /// `predicate` is the result of `self.predicate_for_file(file)` computed
1307    /// externally so the caller can reuse it if needed.
1308    pub(crate) async fn prune_file_after_manifest_check(
1309        &self,
1310        file: &FileHandle,
1311        pre_filter_mode: PreFilterMode,
1312        enable_predicate_prefilter: bool,
1313        predicate: Option<Predicate>,
1314        reader_metrics: &mut ReaderMetrics,
1315    ) -> Result<FileRangeBuilder> {
1316        let may_build_selective_row_selection = predicate.is_some();
1317        let decode_pk_values = !self.compaction
1318            && self
1319                .mapper
1320                .read_columns()
1321                .column_ids_iter()
1322                .any(|column_id| self.mapper.metadata().primary_key.contains(&column_id));
1323        let reader = self
1324            .access_layer
1325            .read_sst(file.clone())
1326            .predicate(predicate)
1327            .projection(Some(self.read_cols.clone()))
1328            .cache(self.cache_strategy.clone())
1329            .inverted_index_appliers(self.inverted_index_appliers.clone())
1330            .bloom_filter_index_appliers(self.bloom_filter_index_appliers.clone())
1331            .fulltext_index_appliers(self.fulltext_index_appliers.clone());
1332        let reader = reader.batch_size(self.batch_size);
1333        let reader = if !self.compaction && may_build_selective_row_selection {
1334            reader.deferred_optional_page_index()
1335        } else {
1336            reader
1337        };
1338        #[cfg(feature = "vector_index")]
1339        let reader = {
1340            let mut reader = reader;
1341            reader =
1342                reader.vector_index_applier(self.vector_index_applier.clone(), self.vector_index_k);
1343            reader
1344        };
1345        let res = reader
1346            .expected_metadata(Some(self.mapper.metadata().clone()))
1347            .compaction(self.compaction)
1348            .pre_filter_mode(pre_filter_mode)
1349            .enable_predicate_prefilter(enable_predicate_prefilter)
1350            .decode_primary_key_values(decode_pk_values)
1351            .build_reader_input(reader_metrics)
1352            .await;
1353        let read_input = match res {
1354            Ok(x) => x,
1355            Err(e) => {
1356                if e.is_object_not_found() && self.ignore_file_not_found {
1357                    error!(e; "File to scan does not exist, region_id: {}, file: {}", file.region_id(), file.file_id());
1358                    return Ok(FileRangeBuilder::default());
1359                } else {
1360                    return Err(e);
1361                }
1362            }
1363        };
1364
1365        let Some((mut file_range_ctx, selection)) = read_input else {
1366            return Ok(FileRangeBuilder::default());
1367        };
1368
1369        let need_compat = !compat::has_same_columns_and_pk_encoding(
1370            &self.mapper,
1371            file_range_ctx.read_format(),
1372            self.compaction,
1373        );
1374        if need_compat {
1375            // They have different schema. We need to adapt the batch first so the
1376            // mapper can convert it.
1377            let compat = FlatCompatBatch::try_new(
1378                &self.mapper,
1379                file_range_ctx.read_format(),
1380                self.compaction,
1381            )?;
1382            file_range_ctx.set_compat_batch(compat);
1383        }
1384        Ok(FileRangeBuilder::new(Arc::new(file_range_ctx), selection))
1385    }
1386
1387    /// Scans flat sources (RecordBatch streams) in parallel.
1388    ///
1389    /// # Panics if the input doesn't allow parallel scan.
1390    #[tracing::instrument(
1391        skip(self, sources, semaphore),
1392        fields(
1393            region_id = %self.region_metadata().region_id,
1394            source_count = sources.len()
1395        )
1396    )]
1397    pub(crate) fn create_parallel_flat_sources(
1398        &self,
1399        sources: Vec<BoxedRecordBatchStream>,
1400        semaphore: Arc<Semaphore>,
1401        channel_size: usize,
1402    ) -> Result<Vec<BoxedRecordBatchStream>> {
1403        if sources.len() <= 1 {
1404            return Ok(sources);
1405        }
1406
1407        // Spawn a task for each source.
1408        let sources = sources
1409            .into_iter()
1410            .map(|source| {
1411                let (sender, receiver) = mpsc::channel(channel_size);
1412                self.spawn_flat_scan_task(source, semaphore.clone(), sender);
1413                let stream = Box::pin(ReceiverStream::new(receiver));
1414                Box::pin(stream) as _
1415            })
1416            .collect();
1417        Ok(sources)
1418    }
1419
1420    /// Spawns a task to scan a flat source (RecordBatch stream) asynchronously.
1421    #[tracing::instrument(
1422        skip(self, input, semaphore, sender),
1423        fields(region_id = %self.region_metadata().region_id)
1424    )]
1425    pub(crate) fn spawn_flat_scan_task(
1426        &self,
1427        mut input: BoxedRecordBatchStream,
1428        semaphore: Arc<Semaphore>,
1429        sender: mpsc::Sender<Result<RecordBatch>>,
1430    ) {
1431        let region_id = self.region_metadata().region_id;
1432        let span = tracing::info_span!(
1433            "ScanInput::parallel_scan_task",
1434            region_id = %region_id,
1435            stream_kind = "flat"
1436        );
1437        common_runtime::spawn_query(
1438            async move {
1439                loop {
1440                    // We release the permit before sending result to avoid the task waiting on
1441                    // the channel with the permit held.
1442                    let maybe_batch = {
1443                        // Safety: We never close the semaphore.
1444                        let _permit = semaphore.acquire().await.unwrap();
1445                        input.next().await
1446                    };
1447                    match maybe_batch {
1448                        Some(Ok(batch)) => {
1449                            let _ = sender.send(Ok(batch)).await;
1450                        }
1451                        Some(Err(e)) => {
1452                            let _ = sender.send(Err(e)).await;
1453                            break;
1454                        }
1455                        None => break,
1456                    }
1457                }
1458            }
1459            .instrument(span),
1460        );
1461    }
1462
1463    pub(crate) fn total_rows(&self) -> usize {
1464        let rows_in_files: usize = self.files.iter().map(|f| f.num_rows()).sum();
1465        let rows_in_memtables: usize = self.memtables.iter().map(|m| m.stats().num_rows()).sum();
1466
1467        let rows = rows_in_files + rows_in_memtables;
1468        #[cfg(feature = "enterprise")]
1469        let rows = rows
1470            + self
1471                .extension_ranges
1472                .iter()
1473                .map(|x| x.num_rows())
1474                .sum::<u64>() as usize;
1475        rows
1476    }
1477
1478    pub(crate) fn predicate_group(&self) -> &PredicateGroup {
1479        &self.predicate
1480    }
1481
1482    /// Returns number of memtables to scan.
1483    pub(crate) fn num_memtables(&self) -> usize {
1484        self.memtables.len()
1485    }
1486
1487    /// Returns number of SST files to scan.
1488    pub(crate) fn num_files(&self) -> usize {
1489        self.files.len()
1490    }
1491
1492    /// Gets the file handle from a row group index.
1493    pub(crate) fn file_from_index(&self, index: RowGroupIndex) -> &FileHandle {
1494        let file_index = index.index - self.num_memtables();
1495        &self.files[file_index]
1496    }
1497
1498    pub fn region_metadata(&self) -> &RegionMetadataRef {
1499        self.mapper.metadata()
1500    }
1501
1502    fn range_pre_filter_mode(&self, source_count: usize) -> PreFilterMode {
1503        if source_count <= 1 {
1504            // Duplicated rows in the same source is not a normal case and we don't provide
1505            // strict dedup semantic (last_row/last_non_null) for it. We expect the duplicated rows
1506            // are exactly identical in the same source so we use PreFilterMode::All for
1507            // performance reason.
1508            return PreFilterMode::All;
1509        }
1510
1511        pre_filter_mode(self.append_mode, self.merge_mode)
1512    }
1513}
1514
1515#[cfg(feature = "enterprise")]
1516impl ScanInput {
1517    #[must_use]
1518    pub(crate) fn with_extension_ranges(self, extension_ranges: Vec<BoxedExtensionRange>) -> Self {
1519        Self {
1520            extension_ranges,
1521            ..self
1522        }
1523    }
1524
1525    #[cfg(feature = "enterprise")]
1526    pub(crate) fn extension_ranges(&self) -> &[BoxedExtensionRange] {
1527        &self.extension_ranges
1528    }
1529
1530    /// Get a boxed [ExtensionRange] by the index in all ranges.
1531    #[cfg(feature = "enterprise")]
1532    pub(crate) fn extension_range(&self, i: usize) -> &BoxedExtensionRange {
1533        &self.extension_ranges[i - self.num_memtables() - self.num_files()]
1534    }
1535}
1536
1537/// Lightweight [PruningStatistics] that only uses the file-level time range from manifest
1538/// metadata, avoiding any parquet metadata reads. Used for early file-level pruning before
1539/// accessing row-group-level statistics.
1540pub(crate) struct FileLevelPruningStats {
1541    /// Scalar value for the file's minimum timestamp in the time index column's unit.
1542    pub(crate) min_scalar: ScalarValue,
1543    /// Scalar value for the file's maximum timestamp in the time index column's unit.
1544    pub(crate) max_scalar: ScalarValue,
1545    /// Name of the time index column.
1546    pub(crate) time_index_col_name: String,
1547}
1548
1549impl PruningStatistics for FileLevelPruningStats {
1550    fn min_values(&self, column: &Column) -> Option<ArrayRef> {
1551        if column.name == self.time_index_col_name {
1552            ScalarValue::iter_to_array(std::iter::once(self.min_scalar.clone())).ok()
1553        } else {
1554            None
1555        }
1556    }
1557
1558    fn max_values(&self, column: &Column) -> Option<ArrayRef> {
1559        if column.name == self.time_index_col_name {
1560            ScalarValue::iter_to_array(std::iter::once(self.max_scalar.clone())).ok()
1561        } else {
1562            None
1563        }
1564    }
1565
1566    fn num_containers(&self) -> usize {
1567        1
1568    }
1569
1570    fn null_counts(&self, column: &Column) -> Option<ArrayRef> {
1571        if column.name == self.time_index_col_name {
1572            // The time index column is NOT NULL.
1573            Some(Arc::new(UInt64Array::from(vec![0u64])))
1574        } else {
1575            None
1576        }
1577    }
1578
1579    fn row_counts(&self, _column: &Column) -> Option<ArrayRef> {
1580        None
1581    }
1582
1583    fn contained(&self, _column: &Column, _values: &HashSet<ScalarValue>) -> Option<BooleanArray> {
1584        None
1585    }
1586}
1587
1588#[cfg(test)]
1589impl ScanInput {
1590    /// Returns SST file ids to scan.
1591    pub(crate) fn file_ids(&self) -> Vec<crate::sst::file::RegionFileId> {
1592        self.files.iter().map(|file| file.file_id()).collect()
1593    }
1594
1595    pub(crate) fn index_ids(&self) -> Vec<crate::sst::file::RegionIndexId> {
1596        self.files.iter().map(|file| file.index_id()).collect()
1597    }
1598}
1599
1600fn pre_filter_mode(append_mode: bool, merge_mode: MergeMode) -> PreFilterMode {
1601    if append_mode {
1602        return PreFilterMode::All;
1603    }
1604
1605    match merge_mode {
1606        MergeMode::LastRow => PreFilterMode::SkipFields,
1607        MergeMode::LastNonNull => PreFilterMode::SkipFields,
1608    }
1609}
1610
1611/// Output of [build_scan_fingerprint]: the cache fingerprint plus the derived
1612/// implied time range used to decide whether the cache key can drop the time
1613/// predicates for a given partition (see `build_range_cache_key`).
1614pub(crate) struct ScanFingerprintBundle {
1615    pub(crate) fingerprint: ScanRequestFingerprint,
1616    /// `Some(r)` = all time-only predicates are guaranteed true on `r` (in the
1617    /// column's `TimeUnit`).
1618    /// `None`    = at least one time-only predicate could not be proven (e.g.
1619    /// `OR`), so the cache-key optimization is disabled for this scan.
1620    pub(crate) implied_time_range: Option<TimestampRange>,
1621}
1622
1623/// Builds a [ScanFingerprintBundle] from a [ScanInput] if the scan is eligible
1624/// for partition range caching.
1625pub(crate) fn build_scan_fingerprint(input: &ScanInput) -> Option<ScanFingerprintBundle> {
1626    let eligible = !input.compaction
1627        && !input.files.is_empty()
1628        && matches!(input.cache_strategy, CacheStrategy::EnableAll(_));
1629
1630    if !eligible {
1631        return None;
1632    }
1633
1634    let metadata = input.region_metadata();
1635    let tag_names: HashSet<&str> = metadata
1636        .column_metadatas
1637        .iter()
1638        .filter(|col| col.semantic_type == SemanticType::Tag)
1639        .map(|col| col.column_schema.name.as_str())
1640        .collect();
1641
1642    let time_index = metadata.time_index_column();
1643    let time_index_name = time_index.column_schema.name.clone();
1644    let ts_col_unit = time_index
1645        .column_schema
1646        .data_type
1647        .as_timestamp()
1648        .expect("Time index must have timestamp-compatible type")
1649        .unit();
1650
1651    let exprs = input
1652        .predicate_group()
1653        .predicate_without_region()
1654        .map(|predicate| predicate.exprs())
1655        .unwrap_or_default();
1656
1657    let mut filters = Vec::new();
1658    let mut time_only_exprs: Vec<&Expr> = Vec::new();
1659    let mut has_tag_filter = false;
1660    let mut columns = HashSet::new();
1661
1662    for expr in exprs {
1663        columns.clear();
1664        let is_time_only = match expr_to_columns(expr, &mut columns) {
1665            Ok(()) if !columns.is_empty() => {
1666                has_tag_filter |= columns
1667                    .iter()
1668                    .any(|col| tag_names.contains(col.name.as_str()));
1669                columns.iter().all(|col| col.name == time_index_name)
1670            }
1671            _ => false,
1672        };
1673
1674        // Route time-only exprs that the legacy extractor recognizes into
1675        // `time_only_exprs` so the implication walker
1676        // (`implied_time_range_from_exprs`, called below) can attempt to drop
1677        // them from the cache key when the partition's `FileTimeRange` is fully
1678        // covered, then stringify them into the fingerprint's `time_filters`
1679        // bucket. Time-only exprs that the extractor doesn't recognize stay in
1680        // `filters` and never get stripped — conservatively correct.
1681        if is_time_only
1682            && extract_time_range_from_expr(&time_index_name, ts_col_unit, expr).is_some()
1683        {
1684            time_only_exprs.push(expr);
1685        } else {
1686            filters.push(expr.to_string());
1687        }
1688    }
1689
1690    if !has_tag_filter {
1691        // We only cache requests that have tag filters to avoid caching all series.
1692        return None;
1693    }
1694
1695    let implied_time_range =
1696        implied_time_range_from_exprs(&time_index_name, ts_col_unit, &time_only_exprs);
1697    let mut time_filters: Vec<String> = time_only_exprs.iter().map(|e| e.to_string()).collect();
1698
1699    // Ensure the filters are sorted for consistent fingerprinting.
1700    filters.sort_unstable();
1701    time_filters.sort_unstable();
1702    let read_columns = input.read_cols.clone();
1703    let fingerprint = crate::read::range_cache::ScanRequestFingerprintBuilder {
1704        read_column_types: read_columns
1705            .column_ids_iter()
1706            .map(|id| {
1707                read_columns
1708                    .json_target_type(id)
1709                    .cloned()
1710                    .map(ConcreteDataType::json2)
1711                    .or_else(|| {
1712                        metadata
1713                            .column_by_id(id)
1714                            .map(|col| col.column_schema.data_type.clone())
1715                    })
1716            })
1717            .collect(),
1718        read_columns,
1719        filters,
1720        time_filters,
1721        series_row_selector: input.series_row_selector,
1722        append_mode: input.append_mode,
1723        filter_deleted: input.filter_deleted,
1724        merge_mode: input.merge_mode,
1725        partition_expr_version: metadata.partition_expr_version,
1726    }
1727    .build();
1728
1729    Some(ScanFingerprintBundle {
1730        fingerprint,
1731        implied_time_range,
1732    })
1733}
1734
1735/// Context shared by different streams from a scanner.
1736/// It contains the input and ranges to scan.
1737pub struct StreamContext {
1738    /// Input memtables and files.
1739    pub input: ScanInput,
1740    /// Metadata for partition ranges.
1741    pub(crate) ranges: Vec<RangeMeta>,
1742    /// Precomputed scan fingerprint for partition range caching.
1743    /// `None` when the scan is not eligible for caching.
1744    #[allow(dead_code)]
1745    pub(crate) scan_fingerprint: Option<ScanRequestFingerprint>,
1746    /// Implied range of every time-only predicate, in the time index column's
1747    /// `TimeUnit`. Used by `build_range_cache_key` to decide whether the
1748    /// partition's `FileTimeRange` is fully covered (allowing `time_filters`
1749    /// to be stripped from the cache key). `None` when caching is ineligible
1750    /// or when the implication walker bailed on an unsupported shape (e.g.
1751    /// `OR`).
1752    pub(crate) scan_implied_time_range: Option<TimestampRange>,
1753
1754    // Metrics:
1755    /// The start time of the query.
1756    pub(crate) query_start: Instant,
1757}
1758
1759impl StreamContext {
1760    /// Creates a new [StreamContext] for [SeqScan].
1761    pub(crate) fn seq_scan_ctx(input: ScanInput) -> Self {
1762        let query_start = input.query_start.unwrap_or_else(Instant::now);
1763        let ranges = RangeMeta::seq_scan_ranges(&input);
1764        READ_SST_COUNT.observe(input.num_files() as f64);
1765        let (scan_fingerprint, scan_implied_time_range) = match build_scan_fingerprint(&input) {
1766            Some(b) => (Some(b.fingerprint), b.implied_time_range),
1767            None => (None, None),
1768        };
1769
1770        Self {
1771            input,
1772            ranges,
1773            scan_fingerprint,
1774            scan_implied_time_range,
1775            query_start,
1776        }
1777    }
1778
1779    /// Creates a new [StreamContext] for [UnorderedScan].
1780    pub(crate) fn unordered_scan_ctx(input: ScanInput) -> Self {
1781        let query_start = input.query_start.unwrap_or_else(Instant::now);
1782        let ranges = RangeMeta::unordered_scan_ranges(&input);
1783        READ_SST_COUNT.observe(input.num_files() as f64);
1784        let (scan_fingerprint, scan_implied_time_range) = match build_scan_fingerprint(&input) {
1785            Some(b) => (Some(b.fingerprint), b.implied_time_range),
1786            None => (None, None),
1787        };
1788
1789        Self {
1790            input,
1791            ranges,
1792            scan_fingerprint,
1793            scan_implied_time_range,
1794            query_start,
1795        }
1796    }
1797
1798    /// Returns true if the index refers to a memtable.
1799    pub(crate) fn is_mem_range_index(&self, index: RowGroupIndex) -> bool {
1800        self.input.num_memtables() > index.index
1801    }
1802
1803    pub(crate) fn is_file_range_index(&self, index: RowGroupIndex) -> bool {
1804        !self.is_mem_range_index(index)
1805            && index.index < self.input.num_files() + self.input.num_memtables()
1806    }
1807
1808    pub(crate) fn range_pre_filter_mode(&self, part_range: &PartitionRange) -> PreFilterMode {
1809        let range_meta = &self.ranges[part_range.identifier];
1810        let source_count = range_meta.indices.len();
1811
1812        self.input.range_pre_filter_mode(source_count)
1813    }
1814
1815    /// Retrieves the partition ranges.
1816    pub(crate) fn partition_ranges(&self) -> Vec<PartitionRange> {
1817        self.ranges
1818            .iter()
1819            .enumerate()
1820            .map(|(idx, range_meta)| range_meta.new_partition_range(idx))
1821            .collect()
1822    }
1823
1824    /// Format the context for explain.
1825    pub(crate) fn format_for_explain(&self, verbose: bool, f: &mut fmt::Formatter) -> fmt::Result {
1826        let (mut num_mem_ranges, mut num_file_ranges, mut num_other_ranges) = (0, 0, 0);
1827        for range_meta in &self.ranges {
1828            for idx in &range_meta.row_group_indices {
1829                if self.is_mem_range_index(*idx) {
1830                    num_mem_ranges += 1;
1831                } else if self.is_file_range_index(*idx) {
1832                    num_file_ranges += 1;
1833                } else {
1834                    num_other_ranges += 1;
1835                }
1836            }
1837        }
1838        if verbose {
1839            write!(f, "{{")?;
1840        }
1841        write!(
1842            f,
1843            r#""partition_count":{{"count":{}, "mem_ranges":{}, "files":{}, "file_ranges":{}"#,
1844            self.ranges.len(),
1845            num_mem_ranges,
1846            self.input.num_files(),
1847            num_file_ranges,
1848        )?;
1849        if num_other_ranges > 0 {
1850            write!(f, r#", "other_ranges":{}"#, num_other_ranges)?;
1851        }
1852        write!(f, "}}")?;
1853
1854        if let Some(selector) = &self.input.series_row_selector {
1855            write!(f, ", \"selector\":\"{}\"", selector)?;
1856        }
1857        if let Some(distribution) = &self.input.distribution {
1858            write!(f, ", \"distribution\":\"{}\"", distribution)?;
1859        }
1860
1861        if verbose {
1862            self.format_verbose_content(f)?;
1863        }
1864
1865        Ok(())
1866    }
1867
1868    fn format_verbose_content(&self, f: &mut fmt::Formatter) -> fmt::Result {
1869        struct FileWrapper<'a> {
1870            file: &'a FileHandle,
1871        }
1872
1873        impl fmt::Debug for FileWrapper<'_> {
1874            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1875                let (start, end) = self.file.time_range();
1876                write!(
1877                    f,
1878                    r#"{{"file_id":"{}","time_range_start":"{}::{}","time_range_end":"{}::{}","rows":{},"size":{},"index_size":{}}}"#,
1879                    self.file.file_id(),
1880                    start.value(),
1881                    start.unit(),
1882                    end.value(),
1883                    end.unit(),
1884                    self.file.num_rows(),
1885                    self.file.size(),
1886                    self.file.index_size()
1887                )
1888            }
1889        }
1890
1891        struct InputWrapper<'a> {
1892            input: &'a ScanInput,
1893        }
1894
1895        #[cfg(feature = "enterprise")]
1896        impl InputWrapper<'_> {
1897            fn format_extension_ranges(&self, f: &mut fmt::Formatter) -> fmt::Result {
1898                if self.input.extension_ranges.is_empty() {
1899                    return Ok(());
1900                }
1901
1902                let mut delimiter = "";
1903                write!(f, ", extension_ranges: [")?;
1904                for range in self.input.extension_ranges() {
1905                    write!(f, "{}{:?}", delimiter, range)?;
1906                    delimiter = ", ";
1907                }
1908                write!(f, "]")?;
1909                Ok(())
1910            }
1911        }
1912
1913        impl fmt::Debug for InputWrapper<'_> {
1914            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1915                let output_schema = self.input.mapper.output_schema();
1916                if !output_schema.is_empty() {
1917                    let names: Vec<_> = output_schema
1918                        .column_schemas()
1919                        .iter()
1920                        .map(|col| &col.name)
1921                        .collect();
1922                    write!(f, ", \"projection\": {:?}", names)?;
1923                }
1924                if let Some(predicate) = &self.input.predicate.predicate() {
1925                    if !predicate.exprs().is_empty() {
1926                        let exprs: Vec<_> =
1927                            predicate.exprs().iter().map(|e| e.to_string()).collect();
1928                        write!(f, ", \"filters\": {:?}", exprs)?;
1929                    }
1930                    if !predicate.dyn_filters().is_empty() {
1931                        let dyn_filters: Vec<_> = predicate
1932                            .dyn_filters()
1933                            .iter()
1934                            .map(|f| format!("{}", f))
1935                            .collect();
1936                        write!(f, ", \"dyn_filters\": {:?}", dyn_filters)?;
1937                    }
1938                }
1939                #[cfg(feature = "vector_index")]
1940                if let Some(vector_index_k) = self.input.vector_index_k {
1941                    write!(f, ", \"vector_index_k\": {}", vector_index_k)?;
1942                }
1943                if !self.input.files.is_empty() {
1944                    write!(f, ", \"files\": ")?;
1945                    f.debug_list()
1946                        .entries(self.input.files.iter().map(|file| FileWrapper { file }))
1947                        .finish()?;
1948                }
1949                write!(f, ", \"flat_format\": {}", self.input.explain_flat_format)?;
1950                #[cfg(feature = "enterprise")]
1951                self.format_extension_ranges(f)?;
1952
1953                Ok(())
1954            }
1955        }
1956
1957        write!(f, "{:?}", InputWrapper { input: &self.input })
1958    }
1959
1960    /// Add new dynamic filters to the predicates.
1961    /// Safe after stream creation; in-flight reads may still observe an older snapshot.
1962    pub(crate) fn add_dyn_filter_to_predicate(
1963        self: &Arc<Self>,
1964        filter_exprs: Vec<Arc<dyn datafusion::physical_plan::PhysicalExpr>>,
1965    ) -> Vec<bool> {
1966        let mut supported = Vec::with_capacity(filter_exprs.len());
1967        let filter_expr = filter_exprs
1968            .into_iter()
1969            .filter_map(|expr| {
1970                if let Ok(dyn_filter) = (expr as Arc<dyn std::any::Any + Send + Sync + 'static>)
1971                .downcast::<datafusion::physical_plan::expressions::DynamicFilterPhysicalExpr>()
1972            {
1973                supported.push(true);
1974                Some(dyn_filter)
1975            } else {
1976                supported.push(false);
1977                None
1978            }
1979            })
1980            .collect();
1981        self.input.predicate.add_dyn_filters(filter_expr);
1982        supported
1983    }
1984}
1985
1986/// Predicates to evaluate.
1987/// It only keeps filters that [SimpleFilterEvaluator] supports.
1988#[derive(Clone, Default)]
1989pub struct PredicateGroup {
1990    time_filters: Option<Arc<Vec<SimpleFilterEvaluator>>>,
1991    /// Predicate that includes request filters and region partition expr (if any).
1992    predicate_all: Predicate,
1993    /// Predicate that only includes request filters.
1994    predicate_without_region: Predicate,
1995    /// Region partition expression restored from metadata.
1996    region_partition_expr: Option<PartitionExpr>,
1997}
1998
1999impl PredicateGroup {
2000    /// Creates a new `PredicateGroup` from exprs according to the metadata.
2001    pub fn new(metadata: &RegionMetadata, exprs: &[Expr]) -> Result<Self> {
2002        let mut combined_exprs = exprs.to_vec();
2003        let mut region_partition_expr = None;
2004
2005        if let Some(expr_json) = metadata.partition_expr.as_ref()
2006            && !expr_json.is_empty()
2007            && let Some(expr) = PartitionExpr::from_json_str(expr_json)
2008                .context(InvalidPartitionExprSnafu { expr: expr_json })?
2009        {
2010            let logical_expr = expr
2011                .try_as_logical_expr()
2012                .context(InvalidPartitionExprSnafu {
2013                    expr: expr_json.clone(),
2014                })?;
2015
2016            combined_exprs.push(logical_expr);
2017            region_partition_expr = Some(expr);
2018        }
2019
2020        let mut time_filters = Vec::with_capacity(combined_exprs.len());
2021        // Columns in the expr.
2022        let mut columns = HashSet::new();
2023        for expr in &combined_exprs {
2024            columns.clear();
2025            let Some(filter) = Self::expr_to_filter(expr, metadata, &mut columns) else {
2026                continue;
2027            };
2028            time_filters.push(filter);
2029        }
2030        let time_filters = if time_filters.is_empty() {
2031            None
2032        } else {
2033            Some(Arc::new(time_filters))
2034        };
2035
2036        let predicate_all = Predicate::new(combined_exprs);
2037        let predicate_without_region = Predicate::new(exprs.to_vec());
2038
2039        Ok(Self {
2040            time_filters,
2041            predicate_all,
2042            predicate_without_region,
2043            region_partition_expr,
2044        })
2045    }
2046
2047    /// Returns time filters.
2048    pub(crate) fn time_filters(&self) -> Option<Arc<Vec<SimpleFilterEvaluator>>> {
2049        self.time_filters.clone()
2050    }
2051
2052    /// Returns predicate of all exprs (including region partition expr if present).
2053    pub(crate) fn predicate(&self) -> Option<&Predicate> {
2054        if self.predicate_all.is_empty() {
2055            None
2056        } else {
2057            Some(&self.predicate_all)
2058        }
2059    }
2060
2061    /// Returns predicate that excludes region partition expr.
2062    pub(crate) fn predicate_without_region(&self) -> Option<&Predicate> {
2063        if self.predicate_without_region.is_empty() {
2064            None
2065        } else {
2066            Some(&self.predicate_without_region)
2067        }
2068    }
2069
2070    /// Add dynamic filters in the predicates.
2071    pub(crate) fn add_dyn_filters(&self, dyn_filters: Vec<Arc<DynamicFilterPhysicalExpr>>) {
2072        self.predicate_all.add_dyn_filters(dyn_filters.clone());
2073        self.predicate_without_region.add_dyn_filters(dyn_filters);
2074    }
2075
2076    /// Returns the region partition expr from metadata, if any.
2077    pub(crate) fn region_partition_expr(&self) -> Option<&PartitionExpr> {
2078        self.region_partition_expr.as_ref()
2079    }
2080
2081    fn expr_to_filter(
2082        expr: &Expr,
2083        metadata: &RegionMetadata,
2084        columns: &mut HashSet<Column>,
2085    ) -> Option<SimpleFilterEvaluator> {
2086        columns.clear();
2087        // `expr_to_columns` won't return error.
2088        // We still ignore these expressions for safety.
2089        expr_to_columns(expr, columns).ok()?;
2090        if columns.len() > 1 {
2091            // Simple filter doesn't support multiple columns.
2092            return None;
2093        }
2094        let column = columns.iter().next()?;
2095        let column_meta = metadata.column_by_name(&column.name)?;
2096        if column_meta.semantic_type == SemanticType::Timestamp {
2097            SimpleFilterEvaluator::try_new(expr)
2098        } else {
2099            None
2100        }
2101    }
2102}
2103
2104#[cfg(test)]
2105mod tests {
2106    use std::collections::BTreeMap;
2107    use std::sync::Arc;
2108
2109    use common_time::timestamp::{TimeUnit, Timestamp};
2110    use datafusion::physical_plan::expressions::{
2111        binary as physical_binary, col as physical_col, lit as physical_lit,
2112    };
2113    use datafusion_common::ScalarValue;
2114    use datafusion_expr::{Operator, col, lit};
2115    use datatypes::arrow::datatypes::{
2116        DataType as ArrowDataType, Field, Schema as ArrowSchema, TimeUnit as ArrowTimeUnit,
2117    };
2118    use datatypes::prelude::ConcreteDataType;
2119    use datatypes::schema::ColumnSchema;
2120    use datatypes::types::json_type::JsonObjectType;
2121    use datatypes::value::Value;
2122    use partition::expr::col as partition_col;
2123    use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder};
2124    use store_api::storage::{RegionId, TimeSeriesDistribution, TimeSeriesRowSelector};
2125
2126    use super::*;
2127    use crate::cache::CacheManager;
2128    use crate::read::range_cache::ScanRequestFingerprintBuilder;
2129    use crate::sst::file::FileMeta;
2130    use crate::test_util::memtable_util::metadata_with_primary_key;
2131    use crate::test_util::scheduler_util::SchedulerEnv;
2132
2133    async fn new_scan_input(metadata: RegionMetadataRef, filters: Vec<Expr>) -> ScanInput {
2134        let env = SchedulerEnv::new().await;
2135        let mapper = FlatProjectionMapper::new(&metadata, [0, 2, 3]).unwrap();
2136        let predicate = PredicateGroup::new(metadata.as_ref(), &filters).unwrap();
2137        let file = FileHandle::new(
2138            crate::sst::file::FileMeta::default(),
2139            Arc::new(crate::sst::file_purger::NoopFilePurger),
2140        );
2141
2142        ScanInput::new(env.access_layer.clone(), mapper)
2143            .with_predicate(predicate)
2144            .with_cache(CacheStrategy::EnableAll(Arc::new(
2145                CacheManager::builder()
2146                    .range_result_cache_size(1024)
2147                    .build(),
2148            )))
2149            .with_files(vec![file])
2150    }
2151
2152    /// Helper to create a timestamp millisecond literal.
2153    fn ts_lit(val: i64) -> datafusion_expr::Expr {
2154        lit(ScalarValue::TimestampMillisecond(Some(val), None))
2155    }
2156
2157    fn metadata_with_time_index_unit(unit: TimeUnit) -> RegionMetadataRef {
2158        let mut builder = RegionMetadataBuilder::new(RegionId::new(123, 456));
2159        builder
2160            .push_column_metadata(ColumnMetadata {
2161                column_schema: ColumnSchema::new(
2162                    "k0".to_string(),
2163                    ConcreteDataType::string_datatype(),
2164                    false,
2165                ),
2166                semantic_type: SemanticType::Tag,
2167                column_id: 0,
2168            })
2169            .push_column_metadata(ColumnMetadata {
2170                column_schema: ColumnSchema::new(
2171                    "k1".to_string(),
2172                    ConcreteDataType::uint32_datatype(),
2173                    false,
2174                ),
2175                semantic_type: SemanticType::Tag,
2176                column_id: 1,
2177            })
2178            .push_column_metadata(ColumnMetadata {
2179                column_schema: ColumnSchema::new(
2180                    "ts".to_string(),
2181                    ConcreteDataType::timestamp_datatype(unit),
2182                    false,
2183                ),
2184                semantic_type: SemanticType::Timestamp,
2185                column_id: 2,
2186            })
2187            .push_column_metadata(ColumnMetadata {
2188                column_schema: ColumnSchema::new(
2189                    "v0".to_string(),
2190                    ConcreteDataType::int64_datatype(),
2191                    true,
2192                ),
2193                semantic_type: SemanticType::Field,
2194                column_id: 3,
2195            })
2196            .primary_key(vec![0, 1]);
2197
2198        Arc::new(builder.build().unwrap())
2199    }
2200
2201    fn file_handle_with_time_range(start: Timestamp, end: Timestamp) -> FileHandle {
2202        FileHandle::new(
2203            FileMeta {
2204                time_range: (start, end),
2205                ..Default::default()
2206            },
2207            Arc::new(crate::sst::file_purger::NoopFilePurger),
2208        )
2209    }
2210
2211    #[tokio::test]
2212    async fn test_scan_input_uses_explicit_batch_size() {
2213        let metadata = Arc::new(metadata_with_primary_key(vec![0, 1], false));
2214        let mapper = FlatProjectionMapper::new(&metadata, [0, 2, 3]).unwrap();
2215        let env = SchedulerEnv::new().await;
2216        let input = ScanInput::new(env.access_layer.clone(), mapper);
2217        assert_eq!(
2218            crate::sst::parquet::DEFAULT_READ_BATCH_SIZE,
2219            input.batch_size()
2220        );
2221
2222        let mapper = FlatProjectionMapper::new(&metadata, [0, 2, 3]).unwrap();
2223        let input = ScanInput::new(env.access_layer.clone(), mapper)
2224            .with_compaction(true)
2225            .with_batch_size(256);
2226        assert_eq!(256, input.batch_size());
2227
2228        let mapper = FlatProjectionMapper::new(&metadata, [0, 2, 3]).unwrap();
2229        let input = ScanInput::new(env.access_layer.clone(), mapper)
2230            .with_batch_size(256)
2231            .with_compaction(true)
2232            .with_compaction(false);
2233        assert_eq!(256, input.batch_size());
2234    }
2235
2236    #[tokio::test]
2237    async fn test_build_scan_fingerprint_for_eligible_scan() {
2238        let metadata = Arc::new(metadata_with_primary_key(vec![0, 1], false));
2239        let input = new_scan_input(
2240            metadata.clone(),
2241            vec![
2242                col("ts").gt_eq(ts_lit(1000)),
2243                col("k0").eq(lit("foo")),
2244                col("v0").gt(lit(1)),
2245            ],
2246        )
2247        .await
2248        .with_distribution(Some(TimeSeriesDistribution::PerSeries))
2249        .with_series_row_selector(Some(TimeSeriesRowSelector::LastRow))
2250        .with_merge_mode(MergeMode::LastNonNull)
2251        .with_filter_deleted(false);
2252
2253        let fingerprint = build_scan_fingerprint(&input).unwrap();
2254
2255        let expected = ScanRequestFingerprintBuilder {
2256            read_columns: input.read_cols,
2257            read_column_types: vec![
2258                metadata
2259                    .column_by_id(0)
2260                    .map(|col| col.column_schema.data_type.clone()),
2261                metadata
2262                    .column_by_id(2)
2263                    .map(|col| col.column_schema.data_type.clone()),
2264                metadata
2265                    .column_by_id(3)
2266                    .map(|col| col.column_schema.data_type.clone()),
2267            ],
2268            filters: vec![
2269                col("k0").eq(lit("foo")).to_string(),
2270                col("v0").gt(lit(1)).to_string(),
2271            ],
2272            time_filters: vec![col("ts").gt_eq(ts_lit(1000)).to_string()],
2273            series_row_selector: Some(TimeSeriesRowSelector::LastRow),
2274            append_mode: false,
2275            filter_deleted: false,
2276            merge_mode: MergeMode::LastNonNull,
2277            partition_expr_version: 0,
2278        }
2279        .build();
2280        assert_eq!(expected, fingerprint.fingerprint);
2281    }
2282
2283    #[tokio::test]
2284    async fn test_build_scan_fingerprint_requires_tag_filter() {
2285        let metadata = Arc::new(metadata_with_primary_key(vec![0, 1], false));
2286        let input = new_scan_input(
2287            metadata,
2288            vec![col("ts").gt_eq(lit(1000)), col("v0").gt(lit(1))],
2289        )
2290        .await;
2291
2292        assert!(build_scan_fingerprint(&input).is_none());
2293    }
2294
2295    #[tokio::test]
2296    async fn test_build_scan_fingerprint_respects_scan_eligibility() {
2297        let metadata = Arc::new(metadata_with_primary_key(vec![0, 1], false));
2298        let filters = vec![col("k0").eq(lit("foo"))];
2299
2300        let disabled = ScanInput::new(
2301            SchedulerEnv::new().await.access_layer.clone(),
2302            FlatProjectionMapper::new(&metadata, [0, 2, 3].into_iter()).unwrap(),
2303        )
2304        .with_predicate(PredicateGroup::new(metadata.as_ref(), &filters).unwrap());
2305        assert!(build_scan_fingerprint(&disabled).is_none());
2306
2307        let compaction = new_scan_input(metadata.clone(), filters.clone())
2308            .await
2309            .with_compaction(true);
2310        assert!(build_scan_fingerprint(&compaction).is_none());
2311
2312        // No files to read.
2313        let no_files = new_scan_input(metadata, filters).await.with_files(vec![]);
2314        assert!(build_scan_fingerprint(&no_files).is_none());
2315    }
2316
2317    #[tokio::test]
2318    async fn test_build_scan_fingerprint_tracks_schema_and_partition_expr_changes() {
2319        let base = metadata_with_primary_key(vec![0, 1], false);
2320        let mut builder = RegionMetadataBuilder::from_existing(base);
2321        let partition_expr = partition_col("k0")
2322            .gt_eq(Value::String("foo".into()))
2323            .as_json_str()
2324            .unwrap();
2325        builder.partition_expr_json(Some(partition_expr));
2326        let metadata = Arc::new(builder.build_without_validation().unwrap());
2327
2328        let input = new_scan_input(metadata.clone(), vec![col("k0").eq(lit("foo"))]).await;
2329        let fingerprint = build_scan_fingerprint(&input).unwrap();
2330
2331        let expected = ScanRequestFingerprintBuilder {
2332            read_columns: input.read_cols,
2333            read_column_types: vec![
2334                metadata
2335                    .column_by_id(0)
2336                    .map(|col| col.column_schema.data_type.clone()),
2337                metadata
2338                    .column_by_id(2)
2339                    .map(|col| col.column_schema.data_type.clone()),
2340                metadata
2341                    .column_by_id(3)
2342                    .map(|col| col.column_schema.data_type.clone()),
2343            ],
2344            filters: vec![col("k0").eq(lit("foo")).to_string()],
2345            time_filters: vec![],
2346            series_row_selector: None,
2347            append_mode: false,
2348            filter_deleted: true,
2349            merge_mode: MergeMode::LastRow,
2350            partition_expr_version: metadata.partition_expr_version,
2351        }
2352        .build();
2353        assert_eq!(expected, fingerprint.fingerprint);
2354        assert_ne!(0, metadata.partition_expr_version);
2355    }
2356
2357    #[tokio::test]
2358    async fn test_build_scan_fingerprint_uses_json_target_types() {
2359        let mut builder = RegionMetadataBuilder::new(RegionId::new(123, 456));
2360        builder
2361            .push_column_metadata(ColumnMetadata {
2362                column_schema: ColumnSchema::new(
2363                    "k0".to_string(),
2364                    ConcreteDataType::string_datatype(),
2365                    false,
2366                ),
2367                semantic_type: SemanticType::Tag,
2368                column_id: 0,
2369            })
2370            .push_column_metadata(ColumnMetadata {
2371                column_schema: ColumnSchema::new(
2372                    "ts".to_string(),
2373                    ConcreteDataType::timestamp_millisecond_datatype(),
2374                    false,
2375                ),
2376                semantic_type: SemanticType::Timestamp,
2377                column_id: 1,
2378            })
2379            .push_column_metadata(ColumnMetadata {
2380                column_schema: ColumnSchema::new(
2381                    "j".to_string(),
2382                    ConcreteDataType::json2(JsonNativeType::Variant),
2383                    true,
2384                ),
2385                semantic_type: SemanticType::Field,
2386                column_id: 2,
2387            })
2388            .primary_key(vec![0]);
2389        let metadata = Arc::new(builder.build().unwrap());
2390
2391        let make_input = |target_type| async {
2392            let env = SchedulerEnv::new().await;
2393            let read_cols = ReadColumns::new([0, 1, 2])
2394                .with_json_target_types(BTreeMap::from([(2, target_type)]));
2395            let mapper =
2396                FlatProjectionMapper::new_with_read_columns(&metadata, vec![0, 1, 2], read_cols)
2397                    .unwrap();
2398            let predicate =
2399                PredicateGroup::new(metadata.as_ref(), &[col("k0").eq(lit("foo"))]).unwrap();
2400            let file = FileHandle::new(
2401                FileMeta::default(),
2402                Arc::new(crate::sst::file_purger::NoopFilePurger),
2403            );
2404            ScanInput::new(env.access_layer.clone(), mapper)
2405                .with_predicate(predicate)
2406                .with_cache(CacheStrategy::EnableAll(Arc::new(
2407                    CacheManager::builder()
2408                        .range_result_cache_size(1024)
2409                        .build(),
2410                )))
2411                .with_files(vec![file])
2412        };
2413
2414        let int_target = JsonNativeType::i64();
2415        let string_target = JsonNativeType::String;
2416        let int_fingerprint = build_scan_fingerprint(&make_input(int_target.clone()).await)
2417            .unwrap()
2418            .fingerprint;
2419        let string_fingerprint = build_scan_fingerprint(&make_input(string_target).await)
2420            .unwrap()
2421            .fingerprint;
2422
2423        assert_ne!(int_fingerprint, string_fingerprint);
2424        assert_eq!(
2425            Some(&Some(ConcreteDataType::json2(int_target))),
2426            int_fingerprint.read_column_types().get(2)
2427        );
2428    }
2429
2430    #[tokio::test]
2431    async fn test_scan_input_rejects_json_type_hint_for_non_json2_column() {
2432        let mut builder = RegionMetadataBuilder::new(RegionId::new(123, 456));
2433        builder
2434            .push_column_metadata(ColumnMetadata {
2435                column_schema: ColumnSchema::new(
2436                    "k0".to_string(),
2437                    ConcreteDataType::string_datatype(),
2438                    false,
2439                ),
2440                semantic_type: SemanticType::Tag,
2441                column_id: 0,
2442            })
2443            .push_column_metadata(ColumnMetadata {
2444                column_schema: ColumnSchema::new(
2445                    "ts".to_string(),
2446                    ConcreteDataType::timestamp_millisecond_datatype(),
2447                    false,
2448                ),
2449                semantic_type: SemanticType::Timestamp,
2450                column_id: 1,
2451            })
2452            .push_column_metadata(ColumnMetadata {
2453                column_schema: ColumnSchema::new(
2454                    "j".to_string(),
2455                    ConcreteDataType::json2(JsonNativeType::Object(JsonObjectType::from([(
2456                        "a".to_string(),
2457                        JsonNativeType::i64(),
2458                    )]))),
2459                    true,
2460                ),
2461                semantic_type: SemanticType::Field,
2462                column_id: 2,
2463            })
2464            .push_column_metadata(ColumnMetadata {
2465                column_schema: ColumnSchema::new(
2466                    "v0".to_string(),
2467                    ConcreteDataType::int64_datatype(),
2468                    true,
2469                ),
2470                semantic_type: SemanticType::Field,
2471                column_id: 3,
2472            })
2473            .primary_key(vec![0]);
2474        let metadata = Arc::new(builder.build().unwrap());
2475        let mutable = Arc::new(crate::memtable::time_partition::TimePartitions::new(
2476            metadata.clone(),
2477            Arc::new(crate::test_util::memtable_util::EmptyMemtableBuilder::default()),
2478            0,
2479            None,
2480        ));
2481        let version = Arc::new(
2482            crate::region::version::VersionBuilder::new(metadata.clone(), mutable).build(),
2483        );
2484        let env = SchedulerEnv::new().await;
2485        let request = ScanRequest {
2486            projection: Some(vec![0, 1, 2, 3]),
2487            json_type_hint: std::collections::HashMap::from([(
2488                "v0".to_string(),
2489                JsonNativeType::i64(),
2490            )]),
2491            ..Default::default()
2492        };
2493
2494        let err = ScanRegion::new(
2495            version,
2496            env.access_layer.clone(),
2497            request,
2498            CacheStrategy::Disabled,
2499        )
2500        .scan_input()
2501        .await;
2502        let Err(err) = err else {
2503            panic!("scan input should reject JSON type hint for non-JSON2 column");
2504        };
2505
2506        assert!(err.to_string().contains("non-JSON2 column v0"));
2507    }
2508
2509    #[test]
2510    fn test_update_dyn_filters_with_empty_base_predicates() {
2511        let metadata = Arc::new(metadata_with_primary_key(vec![0, 1], false));
2512        let predicate_group = PredicateGroup::new(metadata.as_ref(), &[]).unwrap();
2513        assert!(predicate_group.predicate().is_none());
2514        assert!(predicate_group.predicate_without_region().is_none());
2515
2516        let dyn_filter = Arc::new(DynamicFilterPhysicalExpr::new(vec![], physical_lit(false)));
2517        predicate_group.add_dyn_filters(vec![dyn_filter]);
2518
2519        let predicate_all = predicate_group.predicate().unwrap();
2520        assert!(predicate_all.exprs().is_empty());
2521        assert_eq!(1, predicate_all.dyn_filters().len());
2522
2523        let predicate_without_region = predicate_group.predicate_without_region().unwrap();
2524        assert!(predicate_without_region.exprs().is_empty());
2525        assert_eq!(1, predicate_without_region.dyn_filters().len());
2526    }
2527
2528    #[test]
2529    fn test_file_level_pruning_stats_prunes_old_file() {
2530        let ts_col_name = "ts";
2531        let predicate = Predicate::new(vec![col(ts_col_name).gt(ts_lit(1000))]);
2532        let arrow_schema = Arc::new(ArrowSchema::new(vec![Field::new(
2533            ts_col_name,
2534            ArrowDataType::Timestamp(ArrowTimeUnit::Millisecond, None),
2535            false,
2536        )]));
2537
2538        // File with time range [0ms, 500ms] is completely before `ts > 1000ms`.
2539        let stats = FileLevelPruningStats {
2540            min_scalar: ScalarValue::TimestampMillisecond(Some(0), None),
2541            max_scalar: ScalarValue::TimestampMillisecond(Some(500), None),
2542            time_index_col_name: ts_col_name.to_string(),
2543        };
2544        assert_eq!(
2545            vec![false],
2546            predicate.prune_with_stats(&stats, &arrow_schema)
2547        );
2548
2549        // File with time range [0ms, 2000ms] overlaps `ts > 1000ms`, so keep it.
2550        let stats = FileLevelPruningStats {
2551            min_scalar: ScalarValue::TimestampMillisecond(Some(0), None),
2552            max_scalar: ScalarValue::TimestampMillisecond(Some(2000), None),
2553            time_index_col_name: ts_col_name.to_string(),
2554        };
2555        assert_eq!(
2556            vec![true],
2557            predicate.prune_with_stats(&stats, &arrow_schema)
2558        );
2559    }
2560
2561    #[test]
2562    fn test_file_level_pruning_stats_no_predicate_keeps_all() {
2563        let predicate = Predicate::new(vec![]);
2564        assert!(predicate.is_empty());
2565
2566        let stats = FileLevelPruningStats {
2567            min_scalar: ScalarValue::TimestampMillisecond(Some(0), None),
2568            max_scalar: ScalarValue::TimestampMillisecond(Some(500), None),
2569            time_index_col_name: "ts".to_string(),
2570        };
2571        let arrow_schema = Arc::new(ArrowSchema::new(Vec::<Field>::new()));
2572        assert_eq!(
2573            vec![true],
2574            predicate.prune_with_stats(&stats, &arrow_schema)
2575        );
2576    }
2577
2578    #[tokio::test]
2579    async fn test_file_level_pruning_stats_ceil_max_unit_conversion() {
2580        let metadata = metadata_with_time_index_unit(TimeUnit::Millisecond);
2581        let input = new_scan_input(metadata, vec![]).await;
2582        let file = file_handle_with_time_range(
2583            Timestamp::new(1_000_001, TimeUnit::Nanosecond),
2584            Timestamp::new(1_000_001, TimeUnit::Nanosecond),
2585        );
2586
2587        let stats = input.try_file_level_pruning_stats(&file).unwrap();
2588        assert_eq!(
2589            ScalarValue::TimestampMillisecond(Some(1), None),
2590            stats.min_scalar
2591        );
2592        assert_eq!(
2593            ScalarValue::TimestampMillisecond(Some(2), None),
2594            stats.max_scalar
2595        );
2596
2597        // The actual max timestamp is slightly greater than 1ms. It must be kept for `ts > 1ms`.
2598        let predicate = Predicate::new(vec![col("ts").gt(ts_lit(1))]);
2599        assert_eq!(
2600            vec![true],
2601            predicate.prune_with_stats(&stats, input.mapper.metadata().schema.arrow_schema())
2602        );
2603    }
2604
2605    #[tokio::test]
2606    async fn test_file_level_pruning_stats_overflow_keeps_file() {
2607        let metadata = metadata_with_time_index_unit(TimeUnit::Nanosecond);
2608        let input = new_scan_input(metadata, vec![]).await;
2609        let file = file_handle_with_time_range(
2610            Timestamp::new(0, TimeUnit::Second),
2611            Timestamp::new(i64::MAX, TimeUnit::Second),
2612        );
2613
2614        assert!(input.try_file_level_pruning_stats(&file).is_none());
2615    }
2616
2617    #[test]
2618    fn test_file_level_pruning_stats_keeps_inclusive_boundary() {
2619        let ts_col_name = "ts";
2620        let predicate = Predicate::new(vec![col(ts_col_name).gt_eq(ts_lit(1000))]);
2621        let arrow_schema = Arc::new(ArrowSchema::new(vec![Field::new(
2622            ts_col_name,
2623            ArrowDataType::Timestamp(ArrowTimeUnit::Millisecond, None),
2624            false,
2625        )]));
2626        let stats = FileLevelPruningStats {
2627            min_scalar: ScalarValue::TimestampMillisecond(Some(0), None),
2628            max_scalar: ScalarValue::TimestampMillisecond(Some(1000), None),
2629            time_index_col_name: ts_col_name.to_string(),
2630        };
2631
2632        assert_eq!(
2633            vec![true],
2634            predicate.prune_with_stats(&stats, &arrow_schema)
2635        );
2636    }
2637
2638    #[tokio::test]
2639    async fn test_file_level_pruning_with_dyn_filter_only_predicate() {
2640        let metadata = Arc::new(metadata_with_primary_key(vec![0, 1], false));
2641        let mapper = FlatProjectionMapper::new(&metadata, [0, 2, 3]).unwrap();
2642        let predicate_group = PredicateGroup::new(metadata.as_ref(), &[]).unwrap();
2643        predicate_group.add_dyn_filters(vec![Arc::new(DynamicFilterPhysicalExpr::new(
2644            vec![],
2645            physical_lit(false),
2646        ))]);
2647        let input = ScanInput::new(SchedulerEnv::new().await.access_layer.clone(), mapper)
2648            .with_predicate(predicate_group);
2649        let file = file_handle_with_time_range(
2650            Timestamp::new_millisecond(0),
2651            Timestamp::new_millisecond(1000),
2652        );
2653        let mut reader_metrics = ReaderMetrics::default();
2654
2655        let builder = input
2656            .prune_file(&file, PreFilterMode::SkipFields, &mut reader_metrics)
2657            .await
2658            .unwrap();
2659
2660        assert_eq!(1, reader_metrics.filter_metrics.files_time_range_pruned);
2661        let mut ranges = SmallVec::new();
2662        builder.build_ranges(-1, &mut ranges);
2663        assert!(ranges.is_empty());
2664    }
2665
2666    #[tokio::test]
2667    async fn test_manifest_pruning_observes_dynamic_filter_update() {
2668        let metadata = Arc::new(metadata_with_primary_key(vec![0, 1], false));
2669        let mapper = FlatProjectionMapper::new(&metadata, [0, 2, 3]).unwrap();
2670        let predicate_group = PredicateGroup::new(metadata.as_ref(), &[]).unwrap();
2671        let arrow_schema = metadata.schema.arrow_schema();
2672        let ts_expr = physical_col("ts", arrow_schema.as_ref()).unwrap();
2673        let dyn_filter = Arc::new(DynamicFilterPhysicalExpr::new(
2674            vec![ts_expr.clone()],
2675            physical_lit(true),
2676        ));
2677        predicate_group.add_dyn_filters(vec![dyn_filter.clone()]);
2678        let input = ScanInput::new(SchedulerEnv::new().await.access_layer.clone(), mapper)
2679            .with_predicate(predicate_group);
2680        let file = file_handle_with_time_range(
2681            Timestamp::new_millisecond(0),
2682            Timestamp::new_millisecond(1000),
2683        );
2684
2685        assert!(!input.can_manifest_prune_file(&file));
2686
2687        let updated = physical_binary(
2688            ts_expr,
2689            Operator::Gt,
2690            physical_lit(ScalarValue::TimestampMillisecond(Some(1000), None)),
2691            arrow_schema.as_ref(),
2692        )
2693        .unwrap();
2694        dyn_filter.update(updated).unwrap();
2695
2696        assert!(input.can_manifest_prune_file(&file));
2697    }
2698
2699    #[tokio::test]
2700    async fn test_range_pre_filter_mode() {
2701        let metadata = Arc::new(metadata_with_primary_key(vec![0, 1], false));
2702        let cases = [
2703            (true, MergeMode::LastRow, 1, PreFilterMode::All),
2704            (false, MergeMode::LastNonNull, 1, PreFilterMode::All),
2705            (false, MergeMode::LastRow, 2, PreFilterMode::SkipFields),
2706            (true, MergeMode::LastRow, 2, PreFilterMode::All),
2707        ];
2708
2709        for (append_mode, merge_mode, source_count, expected_mode) in cases {
2710            let input = new_scan_input(metadata.clone(), vec![])
2711                .await
2712                .with_append_mode(append_mode)
2713                .with_merge_mode(merge_mode);
2714
2715            assert_eq!(expected_mode, input.range_pre_filter_mode(source_count));
2716        }
2717    }
2718}