Skip to main content

mito2/read/
range_cache.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//! Utilities for the partition range scan result cache.
16
17use std::mem;
18use std::sync::Arc;
19
20use async_stream::try_stream;
21use common_telemetry::warn;
22use common_time::Timestamp;
23use common_time::range::TimestampRange;
24use common_time::timestamp::TimeUnit;
25use datafusion_expr::expr::Expr;
26use datafusion_expr::{Between, BinaryExpr, Operator};
27use datatypes::arrow::compute::concat_batches;
28use datatypes::arrow::record_batch::RecordBatch;
29use datatypes::prelude::ConcreteDataType;
30use datatypes::value::scalar_value_to_timestamp;
31use futures::TryStreamExt;
32use snafu::ResultExt;
33use store_api::region_engine::PartitionRange;
34use store_api::storage::{FileId, RegionId, TimeSeriesRowSelector};
35use table::predicate::is_string_timestamp_literal;
36use tokio::sync::{mpsc, oneshot};
37
38use crate::cache::CacheStrategy;
39use crate::error::{ComputeArrowSnafu, Result};
40use crate::read::BoxedRecordBatchStream;
41use crate::read::read_columns::ReadColumns;
42use crate::read::scan_region::StreamContext;
43use crate::read::scan_util::PartitionMetrics;
44use crate::read::series_reader::SeriesRange;
45use crate::region::options::MergeMode;
46use crate::sst::parquet::DEFAULT_READ_BATCH_SIZE;
47
48const RANGE_CACHE_COMPACT_THRESHOLD_BYTES: usize = 8 * 1024 * 1024;
49
50/// Fingerprint of the scan request fields that affect partition range cache reuse.
51///
52/// It records a normalized view of the projected columns and filters, plus
53/// scan options that can change the returned rows. Schema-dependent metadata
54/// and the partition expression version are included so cached results are not
55/// reused across incompatible schema or partitioning changes.
56#[derive(Debug, Clone, PartialEq, Eq, Hash)]
57pub(crate) struct ScanRequestFingerprint {
58    /// Projection and filters without the time index and partition exprs.
59    inner: Arc<SharedScanRequestFingerprint>,
60    /// Filters with the time index column.
61    time_filters: Option<Arc<Vec<String>>>,
62    series_row_selector: Option<TimeSeriesRowSelector>,
63    append_mode: bool,
64    filter_deleted: bool,
65    merge_mode: MergeMode,
66    stage: RangeScanStage,
67    /// We keep the partition expr version to ensure we won't reuse the fingerprint after we change the partition expr.
68    /// We store the version instead of the whole partition expr or partition expr filters.
69    partition_expr_version: u64,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
73enum RangeScanStage {
74    Data,
75    CandidateSeries,
76    SeriesData(SeriesRange),
77}
78
79#[derive(Debug)]
80pub(crate) struct ScanRequestFingerprintBuilder {
81    pub(crate) read_columns: ReadColumns,
82    pub(crate) read_column_types: Vec<Option<ConcreteDataType>>,
83    pub(crate) filters: Vec<String>,
84    pub(crate) time_filters: Vec<String>,
85    pub(crate) series_row_selector: Option<TimeSeriesRowSelector>,
86    pub(crate) append_mode: bool,
87    pub(crate) filter_deleted: bool,
88    pub(crate) merge_mode: MergeMode,
89    pub(crate) partition_expr_version: u64,
90}
91
92impl ScanRequestFingerprintBuilder {
93    pub(crate) fn build(self) -> ScanRequestFingerprint {
94        let Self {
95            read_columns,
96            read_column_types,
97            filters,
98            time_filters,
99            series_row_selector,
100            append_mode,
101            filter_deleted,
102            merge_mode,
103            partition_expr_version,
104        } = self;
105
106        ScanRequestFingerprint {
107            inner: Arc::new(SharedScanRequestFingerprint {
108                read_columns,
109                read_column_types,
110                filters,
111            }),
112            time_filters: (!time_filters.is_empty()).then(|| Arc::new(time_filters)),
113            series_row_selector,
114            append_mode,
115            filter_deleted,
116            merge_mode,
117            stage: RangeScanStage::Data,
118            partition_expr_version,
119        }
120    }
121}
122
123/// Non-copiable struct of the fingerprint.
124#[derive(Debug, PartialEq, Eq, Hash)]
125struct SharedScanRequestFingerprint {
126    /// Logical columns of the projection.
127    read_columns: ReadColumns,
128    /// Column types of the projection.
129    /// We keep this to ensure we won't reuse the fingerprint after a schema change.
130    read_column_types: Vec<Option<ConcreteDataType>>,
131    /// Filters without the time index column and region partition exprs.
132    filters: Vec<String>,
133}
134
135impl ScanRequestFingerprint {
136    #[cfg(test)]
137    pub(crate) fn read_columns(&self) -> &ReadColumns {
138        &self.inner.read_columns
139    }
140
141    #[cfg(test)]
142    pub(crate) fn read_column_types(&self) -> &[Option<ConcreteDataType>] {
143        &self.inner.read_column_types
144    }
145
146    #[cfg(test)]
147    pub(crate) fn filters(&self) -> &[String] {
148        &self.inner.filters
149    }
150
151    #[cfg(test)]
152    pub(crate) fn time_filters(&self) -> &[String] {
153        self.time_filters
154            .as_deref()
155            .map(Vec::as_slice)
156            .unwrap_or(&[])
157    }
158
159    pub(crate) fn without_time_filters(&self) -> Self {
160        Self {
161            inner: Arc::clone(&self.inner),
162            time_filters: None,
163            series_row_selector: self.series_row_selector,
164            append_mode: self.append_mode,
165            filter_deleted: self.filter_deleted,
166            merge_mode: self.merge_mode,
167            stage: self.stage,
168            partition_expr_version: self.partition_expr_version,
169        }
170    }
171
172    fn for_candidate_series(&self) -> Self {
173        Self {
174            inner: Arc::clone(&self.inner),
175            time_filters: self.time_filters.clone(),
176            series_row_selector: self.series_row_selector,
177            append_mode: self.append_mode,
178            filter_deleted: self.filter_deleted,
179            merge_mode: self.merge_mode,
180            stage: RangeScanStage::CandidateSeries,
181            partition_expr_version: self.partition_expr_version,
182        }
183    }
184
185    fn for_series_data(&self, range: SeriesRange) -> Self {
186        Self {
187            inner: Arc::clone(&self.inner),
188            time_filters: self.time_filters.clone(),
189            series_row_selector: self.series_row_selector,
190            append_mode: self.append_mode,
191            filter_deleted: self.filter_deleted,
192            merge_mode: self.merge_mode,
193            stage: RangeScanStage::SeriesData(range),
194            partition_expr_version: self.partition_expr_version,
195        }
196    }
197
198    pub(crate) fn estimated_size(&self) -> usize {
199        mem::size_of::<SharedScanRequestFingerprint>()
200            + self.inner.read_columns.estimated_size()
201            + self.inner.read_column_types.capacity() * mem::size_of::<Option<ConcreteDataType>>()
202            + self.inner.filters.capacity() * mem::size_of::<String>()
203            + self
204                .inner
205                .filters
206                .iter()
207                .map(|filter| filter.capacity())
208                .sum::<usize>()
209            + self.time_filters.as_ref().map_or(0, |filters| {
210                mem::size_of::<Vec<String>>()
211                    + filters.capacity() * mem::size_of::<String>()
212                    + filters
213                        .iter()
214                        .map(|filter| filter.capacity())
215                        .sum::<usize>()
216            })
217    }
218}
219
220/// Cache key for range scan outputs.
221#[derive(Debug, Clone, PartialEq, Eq, Hash)]
222pub(crate) struct RangeScanCacheKey {
223    pub(crate) region_id: RegionId,
224    /// Sorted (file_id, row_group_index) pairs that uniquely identify the data this range covers.
225    pub(crate) row_groups: Vec<(FileId, i64)>,
226    pub(crate) scan: ScanRequestFingerprint,
227}
228
229impl RangeScanCacheKey {
230    pub(crate) fn estimated_size(&self) -> usize {
231        mem::size_of::<Self>()
232            + self.row_groups.capacity() * mem::size_of::<(FileId, i64)>()
233            + self.scan.estimated_size()
234    }
235}
236
237/// Cached result for one range scan.
238#[derive(Debug)]
239pub(crate) struct CachedBatchSlice {
240    batch: RecordBatch,
241    slice_lengths: Vec<usize>,
242}
243
244impl CachedBatchSlice {
245    fn metadata_size(&self) -> usize {
246        self.slice_lengths.capacity() * mem::size_of::<usize>()
247    }
248}
249
250pub(crate) struct RangeScanCacheValue {
251    cached_batches: Vec<CachedBatchSlice>,
252    /// Precomputed size of all compacted batches.
253    estimated_batches_size: usize,
254}
255
256impl RangeScanCacheValue {
257    pub(crate) fn new(
258        cached_batches: Vec<CachedBatchSlice>,
259        estimated_batches_size: usize,
260    ) -> Self {
261        Self {
262            cached_batches,
263            estimated_batches_size,
264        }
265    }
266
267    pub(crate) fn estimated_size(&self) -> usize {
268        mem::size_of::<Self>()
269            + self.cached_batches.capacity() * mem::size_of::<CachedBatchSlice>()
270            + self
271                .cached_batches
272                .iter()
273                .map(CachedBatchSlice::metadata_size)
274                .sum::<usize>()
275            + self.estimated_batches_size
276    }
277}
278
279/// Row groups and whether all sources are file-only for a partition range.
280pub(crate) struct PartitionRangeRowGroups {
281    /// Sorted (file_id, row_group_index) pairs.
282    pub(crate) row_groups: Vec<(FileId, i64)>,
283    pub(crate) only_file_sources: bool,
284}
285
286/// Collects (file_id, row_group_index) pairs from a partition range's row group indices.
287pub(crate) fn collect_partition_range_row_groups(
288    stream_ctx: &StreamContext,
289    part_range: &PartitionRange,
290) -> PartitionRangeRowGroups {
291    let range_meta = &stream_ctx.ranges[part_range.identifier];
292    let mut row_groups = Vec::new();
293    let mut only_file_sources = true;
294
295    for index in &range_meta.row_group_indices {
296        if stream_ctx.is_file_range_index(*index) {
297            let file_id = stream_ctx.input.file_from_index(*index).file_id().file_id();
298            row_groups.push((file_id, index.row_group_index));
299        } else {
300            only_file_sources = false;
301        }
302    }
303
304    row_groups.sort_unstable_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()).then(a.1.cmp(&b.1)));
305
306    PartitionRangeRowGroups {
307        row_groups,
308        only_file_sources,
309    }
310}
311
312/// Returns the timestamp range where all time-only predicates are guaranteed true.
313///
314/// Returns `Some(min_to_max)` for empty input (vacuously true everywhere).
315/// Returns `None` if any expression contains an unsupported shape: `OR`, `NOT`,
316/// `IN`, non-literal RHS, unsupported operator, column-name mismatch, an `=`
317/// literal that cannot be represented exactly in the column unit, or overflow
318/// during bound adjustment.
319///
320/// This is intentionally stricter than `extract_time_range_from_expr` in
321/// `table::predicate`: lower bounds round up and upper bounds round down. If a
322/// partition's file-time range is contained by the returned range, every row in
323/// that partition satisfies the original time predicates.
324///
325/// `IsNull`/`IsNotNull` on the time index are not routed into `time_filters`
326/// today. If that changes, handle them here before stripping time filters from
327/// the cache key.
328pub(crate) fn implied_time_range_from_exprs(
329    ts_col_name: &str,
330    ts_col_unit: TimeUnit,
331    exprs: &[&Expr],
332) -> Option<TimestampRange> {
333    let mut acc = TimestampRange::min_to_max();
334    for expr in exprs {
335        let r = implied_time_range_from_expr(ts_col_name, ts_col_unit, expr)?;
336        acc = acc.and(&r);
337    }
338    Some(acc)
339}
340
341fn implied_time_range_from_expr(
342    ts_col_name: &str,
343    ts_col_unit: TimeUnit,
344    expr: &Expr,
345) -> Option<TimestampRange> {
346    match expr {
347        Expr::BinaryExpr(BinaryExpr { left, op, right }) => match op {
348            Operator::And => {
349                let l = implied_time_range_from_expr(ts_col_name, ts_col_unit, left)?;
350                let r = implied_time_range_from_expr(ts_col_name, ts_col_unit, right)?;
351                Some(l.and(&r))
352            }
353            Operator::Eq | Operator::Lt | Operator::LtEq | Operator::Gt | Operator::GtEq => {
354                implied_from_cmp(ts_col_name, ts_col_unit, left, *op, right)
355            }
356            // `OR` would require a strict intersection over a union of half-planes
357            // (not the loose-span union provided by `TimestampRange::or`), so we
358            // refuse it. Any other operator is unsupported.
359            _ => None,
360        },
361        Expr::Between(Between {
362            expr,
363            negated,
364            low,
365            high,
366        }) => {
367            if *negated {
368                return None;
369            }
370            implied_from_between(ts_col_name, ts_col_unit, expr, low, high)
371        }
372        // Includes `IsNull`, `IsNotNull`, `Not`, `InList`, function calls, etc.
373        _ => None,
374    }
375}
376
377fn match_ts_column_literal<'a>(
378    ts_col_name: &str,
379    left: &'a Expr,
380    right: &'a Expr,
381) -> Option<(Timestamp, bool)> {
382    let (col, scalar, reverse) = match (left, right) {
383        (Expr::Column(c), Expr::Literal(s, _)) => (c, s, false),
384        (Expr::Literal(s, _), Expr::Column(c)) => (c, s, true),
385        _ => return None,
386    };
387    if col.name != ts_col_name {
388        return None;
389    }
390    // Reject string literals: their conversion needs a timezone we do not have,
391    // and the existing extractor in `table::predicate` rejects them too.
392    if is_string_timestamp_literal(scalar) {
393        return None;
394    }
395    scalar_value_to_timestamp(scalar, None).map(|t| (t, reverse))
396}
397
398fn implied_from_cmp(
399    ts_col_name: &str,
400    ts_col_unit: TimeUnit,
401    left: &Expr,
402    op: Operator,
403    right: &Expr,
404) -> Option<TimestampRange> {
405    let (ts, reverse) = match_ts_column_literal(ts_col_name, left, right)?;
406    // Normalize to "column OP literal".
407    let op = if reverse {
408        match op {
409            Operator::Lt => Operator::Gt,
410            Operator::LtEq => Operator::GtEq,
411            Operator::Gt => Operator::Lt,
412            Operator::GtEq => Operator::LtEq,
413            Operator::Eq => Operator::Eq,
414            _ => return None,
415        }
416    } else {
417        op
418    };
419
420    match op {
421        Operator::GtEq => {
422            // ts >= L. Round the lower bound up in the column unit.
423            let b = ts.convert_to_ceil(ts_col_unit)?;
424            Some(TimestampRange::from_start(b))
425        }
426        Operator::Gt => {
427            // ts > L. floor(L) + 1 is the tight lower bound in the column unit.
428            let v = ts.convert_to(ts_col_unit)?.value().checked_add(1)?;
429            Some(TimestampRange::from_start(Timestamp::new(v, ts_col_unit)))
430        }
431        Operator::LtEq => {
432            // ts <= L. Round the upper bound down in the column unit.
433            let b = ts.convert_to(ts_col_unit)?;
434            Some(TimestampRange::until_end(b, true))
435        }
436        Operator::Lt => {
437            // ts < L. `ts < ceil(L)` is the tight bound: equal to `ts < L` when
438            // L is exactly representable, and `ts <= floor(L)` otherwise.
439            let b = ts.convert_to_ceil(ts_col_unit)?;
440            Some(TimestampRange::until_end(b, false))
441        }
442        Operator::Eq => {
443            // ts = L. Only provable when L is exactly representable.
444            let f = ts.convert_to(ts_col_unit)?;
445            let c = ts.convert_to_ceil(ts_col_unit)?;
446            if f.value() != c.value() {
447                return None;
448            }
449            Some(TimestampRange::single(f))
450        }
451        _ => None,
452    }
453}
454
455fn implied_from_between(
456    ts_col_name: &str,
457    ts_col_unit: TimeUnit,
458    expr: &Expr,
459    low: &Expr,
460    high: &Expr,
461) -> Option<TimestampRange> {
462    let Expr::Column(c) = expr else {
463        return None;
464    };
465    if c.name != ts_col_name {
466        return None;
467    }
468    let (low_s, high_s) = match (low, high) {
469        (Expr::Literal(l, _), Expr::Literal(h, _)) => (l, h),
470        _ => return None,
471    };
472    if is_string_timestamp_literal(low_s) || is_string_timestamp_literal(high_s) {
473        return None;
474    }
475    let low_ts = scalar_value_to_timestamp(low_s, None)?;
476    let high_ts = scalar_value_to_timestamp(high_s, None)?;
477    // BETWEEN low AND high is equivalent to ts >= low AND ts <= high.
478    let lo = low_ts.convert_to_ceil(ts_col_unit)?;
479    let hi = high_ts.convert_to(ts_col_unit)?;
480    Some(TimestampRange::new_inclusive(Some(lo), Some(hi)))
481}
482
483/// Builds a cache key for the given partition range if it is eligible for caching.
484pub(crate) fn build_range_cache_key(
485    stream_ctx: &StreamContext,
486    part_range: &PartitionRange,
487) -> Option<RangeScanCacheKey> {
488    build_range_cache_key_inner(stream_ctx, part_range, None)
489}
490
491/// Builds a cache key for a candidate-series partition-range result.
492pub(crate) fn build_candidate_range_cache_key(
493    stream_ctx: &StreamContext,
494    part_range: &PartitionRange,
495) -> Option<RangeScanCacheKey> {
496    build_range_cache_key_inner(
497        stream_ctx,
498        part_range,
499        Some(RangeScanStage::CandidateSeries),
500    )
501}
502
503/// Builds a cache key for a two-phase series-data partition-range result.
504pub(crate) fn build_series_range_cache_key(
505    stream_ctx: &StreamContext,
506    part_range: &PartitionRange,
507    range: SeriesRange,
508) -> Option<RangeScanCacheKey> {
509    build_range_cache_key_inner(
510        stream_ctx,
511        part_range,
512        Some(RangeScanStage::SeriesData(range)),
513    )
514}
515
516fn build_range_cache_key_inner(
517    stream_ctx: &StreamContext,
518    part_range: &PartitionRange,
519    stage: Option<RangeScanStage>,
520) -> Option<RangeScanCacheKey> {
521    if !stream_ctx.input.cache_strategy.has_range_result_cache() {
522        return None;
523    }
524
525    let fingerprint = stream_ctx.scan_fingerprint.as_ref()?;
526
527    // Dyn filters can change at runtime, so we can't cache when they're present.
528    let has_dyn_filters = stream_ctx
529        .input
530        .predicate_group()
531        .predicate_without_region()
532        .is_some_and(|p| !p.dyn_filters().is_empty());
533    if has_dyn_filters {
534        return None;
535    }
536
537    let rg = collect_partition_range_row_groups(stream_ctx, part_range);
538    if !rg.only_file_sources || rg.row_groups.is_empty() {
539        return None;
540    }
541
542    // If the implied range covers this partition's `FileTimeRange`, drop
543    // time-only predicates from the cache key so that queries with different
544    // but equally-covering time bounds share an entry. `None` means some
545    // time-only predicate had an unsupported shape (e.g. `OR`), so we keep
546    // them in the key.
547    let range_meta = &stream_ctx.ranges[part_range.identifier];
548    let (file_min, file_max) = range_meta.time_range;
549    let covers = match &stream_ctx.scan_implied_time_range {
550        // An empty implied range can never cover a non-empty file range, so
551        // short-circuit. We also skip the unit asserts because
552        // `TimestampRange::empty()` uses `Timestamp::default()` (millisecond),
553        // which would falsely trip the asserts for non-ms time index columns.
554        Some(implied) if !implied.is_empty() => {
555            // The `contains` check is sound only when `file_min`/`file_max`
556            // share the implied range's unit (the time index column's unit).
557            // Mito stores time index values in that unit; assert to catch any
558            // future drift.
559            if let Some(ts) = implied.start().as_ref().or(implied.end().as_ref()) {
560                assert_eq!(file_min.unit(), ts.unit());
561                assert_eq!(file_max.unit(), ts.unit());
562            }
563            implied.contains(&file_min) && implied.contains(&file_max)
564        }
565        _ => false,
566    };
567    let scan = if covers {
568        fingerprint.without_time_filters()
569    } else {
570        fingerprint.clone()
571    };
572    let scan = match stage {
573        Some(RangeScanStage::CandidateSeries) => scan.for_candidate_series(),
574        Some(RangeScanStage::SeriesData(assignment)) => scan.for_series_data(assignment),
575        Some(RangeScanStage::Data) | None => scan,
576    };
577
578    Some(RangeScanCacheKey {
579        region_id: stream_ctx.input.region_metadata().region_id,
580        row_groups: rg.row_groups,
581        scan,
582    })
583}
584
585/// Returns a stream that replays cached record batches.
586pub(crate) fn cached_flat_range_stream(value: Arc<RangeScanCacheValue>) -> BoxedRecordBatchStream {
587    Box::pin(try_stream! {
588        for cached_batch in &value.cached_batches {
589            let mut offset = 0;
590            for &len in &cached_batch.slice_lengths {
591                yield cached_batch.batch.slice(offset, len);
592                offset += len;
593            }
594        }
595    })
596}
597
598enum CacheConcatCommand {
599    Compact(Vec<RecordBatch>),
600    Finish {
601        pending: Vec<RecordBatch>,
602        key: RangeScanCacheKey,
603        cache_strategy: CacheStrategy,
604        part_metrics: PartitionMetrics,
605        result_tx: Option<oneshot::Sender<Result<Arc<RangeScanCacheValue>>>>,
606    },
607}
608
609#[derive(Default)]
610struct CacheConcatState {
611    cached_batches: Vec<CachedBatchSlice>,
612    estimated_size: usize,
613}
614
615impl CacheConcatState {
616    async fn compact(
617        &mut self,
618        batches: Vec<RecordBatch>,
619        limiter: &crate::cache::RangeResultMemoryLimiter,
620    ) -> Result<()> {
621        if batches.is_empty() {
622            return Ok(());
623        }
624
625        let input_size = batches
626            .iter()
627            .map(RecordBatch::get_array_memory_size)
628            .sum::<usize>();
629        let _permit = limiter.acquire(input_size).await?;
630
631        let compacted = compact_record_batches(batches)?;
632        self.estimated_size += compacted.batch.get_array_memory_size();
633        self.cached_batches.push(compacted);
634        Ok(())
635    }
636
637    fn finish(self) -> RangeScanCacheValue {
638        RangeScanCacheValue::new(self.cached_batches, self.estimated_size)
639    }
640}
641
642fn compact_record_batches(batches: Vec<RecordBatch>) -> Result<CachedBatchSlice> {
643    debug_assert!(!batches.is_empty());
644
645    let slice_lengths = batches.iter().map(RecordBatch::num_rows).collect();
646    build_cached_batch_slice(batches, slice_lengths)
647}
648
649fn build_cached_batch_slice(
650    batches: Vec<RecordBatch>,
651    slice_lengths: Vec<usize>,
652) -> Result<CachedBatchSlice> {
653    let batch = if batches.len() == 1 {
654        batches.into_iter().next().unwrap()
655    } else {
656        let schema = batches[0].schema();
657        concat_batches(&schema, &batches).context(ComputeArrowSnafu)?
658    };
659
660    Ok(CachedBatchSlice {
661        batch,
662        slice_lengths,
663    })
664}
665
666async fn run_cache_concat_task(
667    mut rx: mpsc::UnboundedReceiver<CacheConcatCommand>,
668    limiter: Arc<crate::cache::RangeResultMemoryLimiter>,
669    skip_threshold_bytes: usize,
670) {
671    let mut state = CacheConcatState::default();
672
673    while let Some(cmd) = rx.recv().await {
674        match cmd {
675            CacheConcatCommand::Compact(batches) => {
676                if let Err(err) = state.compact(batches, &limiter).await {
677                    warn!(err; "Failed to compact range cache batches");
678                    return;
679                }
680                // Close the channel to stop further work as soon as the cached
681                // size exceeds the configured cache budget.
682                if state.estimated_size > skip_threshold_bytes {
683                    return;
684                }
685            }
686            CacheConcatCommand::Finish {
687                pending,
688                key,
689                cache_strategy,
690                part_metrics,
691                result_tx,
692            } => {
693                let compact_result = state
694                    .compact(pending, &limiter)
695                    .await
696                    .map(|()| state.finish());
697                let result = match compact_result {
698                    Ok(v) => {
699                        let value = Arc::new(v);
700                        part_metrics
701                            .inc_range_cache_size(key.estimated_size() + value.estimated_size());
702                        cache_strategy.put_range_result(key, value.clone());
703
704                        Ok(value)
705                    }
706                    Err(e) => {
707                        warn!(e; "Failed to finalize range cache batches");
708
709                        Err(e)
710                    }
711                };
712
713                if let Some(tx) = result_tx {
714                    let _ = tx.send(result);
715                }
716
717                break;
718            }
719        }
720    }
721}
722
723struct CacheBatchBuffer {
724    buffered_batches: Vec<RecordBatch>,
725    buffered_rows: usize,
726    buffered_size: usize,
727    sender: Option<mpsc::UnboundedSender<CacheConcatCommand>>,
728}
729
730impl CacheBatchBuffer {
731    fn new(cache_strategy: &CacheStrategy) -> Self {
732        let sender = cache_strategy.range_result_memory_limiter().map(|limiter| {
733            let skip_threshold_bytes = cache_strategy.range_result_cache_size().unwrap_or(0);
734            let (tx, rx) = mpsc::unbounded_channel();
735            common_runtime::spawn_query(run_cache_concat_task(
736                rx,
737                limiter.clone(),
738                skip_threshold_bytes,
739            ));
740            tx
741        });
742
743        Self {
744            buffered_batches: Vec::new(),
745            buffered_rows: 0,
746            buffered_size: 0,
747            sender,
748        }
749    }
750
751    fn push(&mut self, batch: RecordBatch) -> Result<()> {
752        if self.sender.is_none() {
753            return Ok(());
754        }
755
756        self.buffered_rows += batch.num_rows();
757        self.buffered_size += batch.get_array_memory_size();
758        self.buffered_batches.push(batch);
759
760        if self.buffered_batches.len() > 1
761            && (self.buffered_rows > DEFAULT_READ_BATCH_SIZE
762                || self.buffered_size > RANGE_CACHE_COMPACT_THRESHOLD_BYTES)
763        {
764            self.notify_compact();
765        }
766
767        Ok(())
768    }
769
770    fn notify_compact(&mut self) {
771        if self.buffered_batches.is_empty() || self.sender.is_none() {
772            return;
773        }
774
775        let batches = mem::take(&mut self.buffered_batches);
776        self.buffered_rows = 0;
777        self.buffered_size = 0;
778
779        let Some(sender) = &self.sender else {
780            return;
781        };
782        if sender.send(CacheConcatCommand::Compact(batches)).is_err() {
783            self.sender = None;
784        }
785    }
786
787    fn finish(
788        mut self,
789        key: RangeScanCacheKey,
790        cache_strategy: CacheStrategy,
791        part_metrics: PartitionMetrics,
792        result_tx: Option<oneshot::Sender<Result<Arc<RangeScanCacheValue>>>>,
793    ) {
794        let Some(sender) = self.sender.take() else {
795            return;
796        };
797
798        if sender
799            .send(CacheConcatCommand::Finish {
800                pending: mem::take(&mut self.buffered_batches),
801                key,
802                cache_strategy,
803                part_metrics,
804                result_tx,
805            })
806            .is_err()
807        {
808            self.sender = None;
809        }
810    }
811}
812
813/// Wraps a stream to cache its output for future range cache hits.
814pub(crate) fn cache_flat_range_stream(
815    mut stream: BoxedRecordBatchStream,
816    cache_strategy: CacheStrategy,
817    key: RangeScanCacheKey,
818    part_metrics: PartitionMetrics,
819) -> BoxedRecordBatchStream {
820    Box::pin(try_stream! {
821        let mut buffer = CacheBatchBuffer::new(&cache_strategy);
822        while let Some(batch) = stream.try_next().await? {
823            buffer.push(batch.clone())?;
824            yield batch;
825        }
826
827        buffer.finish(key, cache_strategy, part_metrics, None);
828    })
829}
830
831/// Creates a `cache_flat_range_stream` with dummy internals for benchmarking.
832///
833/// This avoids exposing `RangeScanCacheKey`, `ScanRequestFingerprint`, and
834/// `PartitionMetrics` publicly.
835#[cfg(feature = "test")]
836pub fn bench_cache_flat_range_stream(
837    stream: BoxedRecordBatchStream,
838    cache_size_bytes: u64,
839    region_id: RegionId,
840) -> BoxedRecordBatchStream {
841    use std::time::Instant;
842
843    use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
844
845    use crate::region::options::MergeMode;
846
847    let cache_manager = Arc::new(
848        crate::cache::CacheManager::builder()
849            .range_result_cache_size(cache_size_bytes)
850            .build(),
851    );
852    let cache_strategy = CacheStrategy::EnableAll(cache_manager);
853
854    let fingerprint = ScanRequestFingerprintBuilder {
855        read_columns: ReadColumns::from_deduped_column_ids(std::iter::empty()),
856        read_column_types: vec![],
857        filters: vec![],
858        time_filters: vec![],
859        series_row_selector: None,
860        append_mode: false,
861        filter_deleted: false,
862        merge_mode: MergeMode::LastRow,
863        partition_expr_version: 0,
864    }
865    .build();
866
867    let key = RangeScanCacheKey {
868        region_id,
869        row_groups: vec![],
870        scan: fingerprint,
871    };
872
873    let metrics_set = ExecutionPlanMetricsSet::new();
874    let part_metrics =
875        PartitionMetrics::new(region_id, 0, "bench", Instant::now(), false, &metrics_set);
876
877    cache_flat_range_stream(stream, cache_strategy, key, part_metrics)
878}
879
880#[cfg(test)]
881mod tests {
882    use std::sync::Arc;
883    use std::time::Instant;
884
885    use common_time::Timestamp;
886    use common_time::range::TimestampRange;
887    use common_time::timestamp::TimeUnit;
888    use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
889    use datafusion_common::ScalarValue;
890    use datafusion_expr::{Expr, col, lit};
891    use smallvec::smallvec;
892    use store_api::storage::{FileId, RegionId};
893
894    use super::*;
895    use crate::cache::CacheManager;
896    use crate::read::flat_projection::FlatProjectionMapper;
897    use crate::read::range::{RangeMeta, RowGroupIndex, SourceIndex};
898    use crate::read::scan_region::{PredicateGroup, ScanInput};
899    use crate::sst::file::FileTimeRange;
900    use crate::test_util::memtable_util::metadata_with_primary_key;
901    use crate::test_util::scheduler_util::SchedulerEnv;
902    use crate::test_util::sst_util::sst_file_handle_with_file_id;
903
904    fn test_cache_strategy() -> CacheStrategy {
905        CacheStrategy::EnableAll(Arc::new(
906            CacheManager::builder()
907                .range_result_cache_size(1024 * 1024)
908                .build(),
909        ))
910    }
911
912    fn test_scan_fingerprint(
913        filters: Vec<String>,
914        time_filters: Vec<String>,
915        series_row_selector: Option<TimeSeriesRowSelector>,
916        filter_deleted: bool,
917        partition_expr_version: u64,
918    ) -> ScanRequestFingerprint {
919        let read_columns = ReadColumns::from_deduped_column_ids([1, 2]);
920        ScanRequestFingerprintBuilder {
921            read_columns,
922            read_column_types: vec![None, None],
923            filters,
924            time_filters,
925            series_row_selector,
926            append_mode: false,
927            filter_deleted,
928            merge_mode: MergeMode::LastRow,
929            partition_expr_version,
930        }
931        .build()
932    }
933
934    fn test_cache_context(strategy: &CacheStrategy) -> (RangeScanCacheKey, PartitionMetrics) {
935        let region_id = RegionId::new(1, 1);
936        let key = RangeScanCacheKey {
937            region_id,
938            row_groups: vec![],
939            scan: test_scan_fingerprint(vec![], vec![], None, false, 0),
940        };
941
942        let metrics_set = ExecutionPlanMetricsSet::new();
943        let part_metrics =
944            PartitionMetrics::new(region_id, 0, "test", Instant::now(), false, &metrics_set);
945
946        assert!(strategy.get_range_result(&key).is_none());
947        (key, part_metrics)
948    }
949
950    async fn finish_cache_batch_buffer(
951        buffer: CacheBatchBuffer,
952        key: RangeScanCacheKey,
953        cache_strategy: CacheStrategy,
954        part_metrics: PartitionMetrics,
955    ) -> Result<Arc<RangeScanCacheValue>> {
956        let (tx, rx) = oneshot::channel();
957        common_telemetry::info!("finish start");
958        buffer.finish(key, cache_strategy, part_metrics, Some(tx));
959        common_telemetry::info!("finish end");
960        rx.await.context(crate::error::RecvSnafu)?
961    }
962
963    async fn new_stream_context(
964        filters: Vec<Expr>,
965        query_time_range: Option<TimestampRange>,
966        partition_time_range: FileTimeRange,
967    ) -> (StreamContext, PartitionRange) {
968        let env = SchedulerEnv::new().await;
969        let metadata = Arc::new(metadata_with_primary_key(vec![0, 1], false));
970        let mapper = FlatProjectionMapper::new(&metadata, [0, 2, 3]).unwrap();
971        let predicate = PredicateGroup::new(metadata.as_ref(), &filters).unwrap();
972        let file_id = FileId::random();
973        let file = sst_file_handle_with_file_id(
974            file_id,
975            partition_time_range.0.value(),
976            partition_time_range.1.value(),
977        );
978        let input = ScanInput::new(env.access_layer.clone(), mapper)
979            .with_predicate(predicate)
980            .with_time_range(query_time_range)
981            .with_files(vec![file])
982            .with_cache(test_cache_strategy());
983        let range_meta = RangeMeta {
984            time_range: partition_time_range,
985            indices: smallvec![SourceIndex {
986                index: 0,
987                num_row_groups: 1,
988            }],
989            row_group_indices: smallvec![RowGroupIndex {
990                index: 0,
991                row_group_index: 0,
992            }],
993            num_rows: 10,
994        };
995        let partition_range = range_meta.new_partition_range(0);
996        let (scan_fingerprint, scan_implied_time_range) =
997            match crate::read::scan_region::build_scan_fingerprint(&input) {
998                Some(b) => (Some(b.fingerprint), b.implied_time_range),
999                None => (None, None),
1000            };
1001        let stream_ctx = StreamContext {
1002            input,
1003            ranges: vec![range_meta],
1004            scan_fingerprint,
1005            scan_implied_time_range,
1006            query_start: Instant::now(),
1007        };
1008
1009        (stream_ctx, partition_range)
1010    }
1011
1012    /// Helper to create a timestamp millisecond literal.
1013    fn ts_lit(val: i64) -> Expr {
1014        lit(ScalarValue::TimestampMillisecond(Some(val), None))
1015    }
1016
1017    fn normalized_exprs(exprs: impl IntoIterator<Item = Expr>) -> Vec<String> {
1018        let mut exprs = exprs
1019            .into_iter()
1020            .map(|expr| expr.to_string())
1021            .collect::<Vec<_>>();
1022        exprs.sort_unstable();
1023        exprs
1024    }
1025
1026    async fn assert_range_cache_filters(
1027        filters: Vec<Expr>,
1028        query_time_range: Option<TimestampRange>,
1029        partition_time_range: FileTimeRange,
1030        expected_filters: Vec<Expr>,
1031        expected_time_filters: Vec<Expr>,
1032    ) {
1033        let (stream_ctx, part_range) =
1034            new_stream_context(filters, query_time_range, partition_time_range).await;
1035
1036        let key = build_range_cache_key(&stream_ctx, &part_range).unwrap();
1037
1038        assert_eq!(
1039            key.scan.filters(),
1040            normalized_exprs(expected_filters).as_slice()
1041        );
1042        assert_eq!(
1043            key.scan.time_filters(),
1044            normalized_exprs(expected_time_filters).as_slice()
1045        );
1046    }
1047
1048    #[tokio::test]
1049    async fn range_cache_time_filter_key_cases() {
1050        let partition = (
1051            Timestamp::new_millisecond(1000),
1052            Timestamp::new_millisecond(2000),
1053        );
1054
1055        struct Case {
1056            filters: Vec<Expr>,
1057            query_time_range: Option<TimestampRange>,
1058            expected_filters: Vec<Expr>,
1059            expected_time_filters: Vec<Expr>,
1060        }
1061
1062        // Time filters are stripped only when their implied range fully covers
1063        // the partition's file-time range. `is_not_null(ts)` stays in regular
1064        // filters because it is not routed into `time_filters`.
1065        for case in [
1066            Case {
1067                filters: vec![
1068                    col("ts").gt_eq(ts_lit(1000)),
1069                    col("ts").lt(ts_lit(2001)),
1070                    col("ts").is_not_null(),
1071                    col("k0").eq(lit("foo")),
1072                ],
1073                query_time_range: TimestampRange::with_unit(1000, 2002, TimeUnit::Millisecond),
1074                expected_filters: vec![col("k0").eq(lit("foo")), col("ts").is_not_null()],
1075                expected_time_filters: vec![],
1076            },
1077            Case {
1078                filters: vec![
1079                    col("ts").gt_eq(ts_lit(500)),
1080                    col("ts").lt(ts_lit(3000)),
1081                    col("k0").eq(lit("foo")),
1082                ],
1083                query_time_range: TimestampRange::with_unit(500, 3000, TimeUnit::Millisecond),
1084                expected_filters: vec![col("k0").eq(lit("foo"))],
1085                expected_time_filters: vec![],
1086            },
1087            Case {
1088                filters: vec![
1089                    col("ts").gt_eq(ts_lit(1000)),
1090                    col("ts").lt_eq(ts_lit(2000)),
1091                    col("k0").eq(lit("foo")),
1092                ],
1093                query_time_range: TimestampRange::with_unit(1000, 2001, TimeUnit::Millisecond),
1094                expected_filters: vec![col("k0").eq(lit("foo"))],
1095                expected_time_filters: vec![],
1096            },
1097            Case {
1098                filters: vec![
1099                    col("ts").between(ts_lit(1000), ts_lit(2000)),
1100                    col("k0").eq(lit("foo")),
1101                ],
1102                query_time_range: TimestampRange::with_unit(1000, 2001, TimeUnit::Millisecond),
1103                expected_filters: vec![col("k0").eq(lit("foo"))],
1104                expected_time_filters: vec![],
1105            },
1106            Case {
1107                filters: vec![col("ts").gt_eq(ts_lit(1200)), col("k0").eq(lit("foo"))],
1108                query_time_range: TimestampRange::with_unit(1200, 2001, TimeUnit::Millisecond),
1109                expected_filters: vec![col("k0").eq(lit("foo"))],
1110                expected_time_filters: vec![col("ts").gt_eq(ts_lit(1200))],
1111            },
1112            Case {
1113                filters: vec![
1114                    col("ts").gt_eq(ts_lit(1500)),
1115                    col("ts").is_not_null(),
1116                    col("k0").eq(lit("foo")),
1117                ],
1118                query_time_range: None,
1119                expected_filters: vec![col("k0").eq(lit("foo")), col("ts").is_not_null()],
1120                expected_time_filters: vec![col("ts").gt_eq(ts_lit(1500))],
1121            },
1122        ] {
1123            assert_range_cache_filters(
1124                case.filters,
1125                case.query_time_range,
1126                partition,
1127                case.expected_filters,
1128                case.expected_time_filters,
1129            )
1130            .await;
1131        }
1132    }
1133
1134    #[tokio::test]
1135    async fn two_distinct_queries_share_cache_key_when_both_cover() {
1136        let partition_range = (
1137            Timestamp::new_millisecond(1000),
1138            Timestamp::new_millisecond(2000),
1139        );
1140
1141        let (ctx_a, part_a) = new_stream_context(
1142            vec![
1143                col("ts").gt_eq(ts_lit(500)),
1144                col("ts").lt(ts_lit(3000)),
1145                col("k0").eq(lit("foo")),
1146            ],
1147            TimestampRange::with_unit(500, 3000, TimeUnit::Millisecond),
1148            partition_range,
1149        )
1150        .await;
1151        let (ctx_b, part_b) = new_stream_context(
1152            vec![
1153                col("ts").gt_eq(ts_lit(100)),
1154                col("ts").lt(ts_lit(5000)),
1155                col("k0").eq(lit("foo")),
1156            ],
1157            TimestampRange::with_unit(100, 5000, TimeUnit::Millisecond),
1158            partition_range,
1159        )
1160        .await;
1161
1162        let key_a = build_range_cache_key(&ctx_a, &part_a).unwrap();
1163        let key_b = build_range_cache_key(&ctx_b, &part_b).unwrap();
1164        assert_eq!(key_a.scan, key_b.scan);
1165        assert!(key_a.scan.time_filters().is_empty());
1166    }
1167
1168    #[tokio::test]
1169    async fn scan_stages_have_separate_cache_keys() {
1170        let partition_range = (
1171            Timestamp::new_millisecond(1000),
1172            Timestamp::new_millisecond(2000),
1173        );
1174        let (ctx, part_range) =
1175            new_stream_context(vec![col("k0").eq(lit("foo"))], None, partition_range).await;
1176
1177        let data_key = build_range_cache_key(&ctx, &part_range).unwrap();
1178        let candidate_key = build_candidate_range_cache_key(&ctx, &part_range).unwrap();
1179        let range_0 = SeriesRange::new(0, 2).unwrap();
1180        let range_1 = SeriesRange::new(1, 2).unwrap();
1181        let series_key_0 = build_series_range_cache_key(&ctx, &part_range, range_0).unwrap();
1182        let series_key_1 = build_series_range_cache_key(&ctx, &part_range, range_1).unwrap();
1183
1184        assert_ne!(data_key.scan, candidate_key.scan);
1185        assert_ne!(data_key.scan, series_key_0.scan);
1186        assert_ne!(candidate_key.scan, series_key_0.scan);
1187        assert_ne!(series_key_0.scan, series_key_1.scan);
1188        assert_eq!(data_key.row_groups, candidate_key.row_groups);
1189        assert_eq!(data_key.row_groups, series_key_0.row_groups);
1190    }
1191
1192    #[tokio::test]
1193    async fn disables_optimization_on_or_clause() {
1194        let partition_range = (
1195            Timestamp::new_millisecond(1000),
1196            Timestamp::new_millisecond(2000),
1197        );
1198
1199        let or_a = col("ts").gt_eq(ts_lit(1000)).or(col("ts").lt(ts_lit(500)));
1200        let or_b = col("ts").gt_eq(ts_lit(900)).or(col("ts").lt(ts_lit(400)));
1201
1202        let (ctx_a, part_a) = new_stream_context(
1203            vec![or_a.clone(), col("k0").eq(lit("foo"))],
1204            None,
1205            partition_range,
1206        )
1207        .await;
1208        let (ctx_b, part_b) = new_stream_context(
1209            vec![or_b.clone(), col("k0").eq(lit("foo"))],
1210            None,
1211            partition_range,
1212        )
1213        .await;
1214
1215        assert!(ctx_a.scan_implied_time_range.is_none());
1216        let key_a = build_range_cache_key(&ctx_a, &part_a).unwrap();
1217        let key_b = build_range_cache_key(&ctx_b, &part_b).unwrap();
1218        assert_ne!(key_a.scan, key_b.scan);
1219        assert_eq!(
1220            key_a.scan.time_filters(),
1221            normalized_exprs([or_a]).as_slice()
1222        );
1223    }
1224
1225    #[tokio::test]
1226    async fn empty_implied_range_does_not_panic_on_non_ms_file_range() {
1227        // Contradictory time predicates make the implied range empty. The
1228        // empty range's sentinel timestamps use `Timestamp::default()` (ms),
1229        // so without the `is_empty()` short-circuit the unit asserts would
1230        // panic against a non-ms `range_meta.time_range`.
1231        let partition = (
1232            Timestamp::new_millisecond(1000),
1233            Timestamp::new_millisecond(2000),
1234        );
1235
1236        let (mut ctx, part_range) = new_stream_context(
1237            vec![col("ts").gt_eq(ts_lit(1500)), col("k0").eq(lit("foo"))],
1238            TimestampRange::with_unit(1500, 3000, TimeUnit::Millisecond),
1239            partition,
1240        )
1241        .await;
1242
1243        ctx.scan_implied_time_range = Some(TimestampRange::empty());
1244        ctx.ranges[0].time_range = (
1245            Timestamp::new(1_000_000_000, TimeUnit::Nanosecond),
1246            Timestamp::new(2_000_000_000, TimeUnit::Nanosecond),
1247        );
1248
1249        let key = build_range_cache_key(&ctx, &part_range).unwrap();
1250        // Empty implied range cannot cover, so time filters stay in the key.
1251        assert!(!key.scan.time_filters().is_empty());
1252    }
1253
1254    fn ms_ts(v: i64) -> Timestamp {
1255        Timestamp::new_millisecond(v)
1256    }
1257
1258    fn implied_ms(expr: Expr) -> Option<TimestampRange> {
1259        implied_time_range_from_exprs("ts", TimeUnit::Millisecond, &[&expr])
1260    }
1261
1262    #[test]
1263    fn implied_time_range_supported_exprs() {
1264        for (expr, expected) in [
1265            (
1266                col("ts").gt_eq(ts_lit(1000)),
1267                Some(TimestampRange::from_start(ms_ts(1000))),
1268            ),
1269            (
1270                col("ts").gt(ts_lit(1000)),
1271                Some(TimestampRange::from_start(ms_ts(1001))),
1272            ),
1273            (
1274                col("ts").lt_eq(ts_lit(2000)),
1275                Some(TimestampRange::until_end(ms_ts(2000), true)),
1276            ),
1277            (
1278                col("ts").lt(ts_lit(2000)),
1279                Some(TimestampRange::until_end(ms_ts(2000), false)),
1280            ),
1281            (
1282                col("ts").eq(ts_lit(1500)),
1283                Some(TimestampRange::single(ms_ts(1500))),
1284            ),
1285            (
1286                ts_lit(1000).lt_eq(col("ts")),
1287                Some(TimestampRange::from_start(ms_ts(1000))),
1288            ),
1289            (
1290                col("ts").between(ts_lit(1000), ts_lit(2000)),
1291                Some(TimestampRange::new_inclusive(
1292                    Some(ms_ts(1000)),
1293                    Some(ms_ts(2000)),
1294                )),
1295            ),
1296            (
1297                col("ts")
1298                    .gt_eq(ts_lit(1000))
1299                    .and(col("ts").lt(ts_lit(2000))),
1300                TimestampRange::with_unit(1000, 2000, TimeUnit::Millisecond),
1301            ),
1302            (
1303                col("ts")
1304                    .gt_eq(ts_lit(1000))
1305                    .and(col("ts").lt(ts_lit(5000)))
1306                    .and(col("ts").lt_eq(ts_lit(3000))),
1307                TimestampRange::with_unit(1000, 3001, TimeUnit::Millisecond),
1308            ),
1309        ] {
1310            assert_eq!(implied_ms(expr), expected);
1311        }
1312
1313        assert_eq!(
1314            implied_time_range_from_exprs("ts", TimeUnit::Millisecond, &[]),
1315            Some(TimestampRange::min_to_max())
1316        );
1317    }
1318
1319    #[test]
1320    fn implied_time_range_unsupported_exprs() {
1321        let not_between = Expr::Between(Between {
1322            expr: Box::new(col("ts")),
1323            negated: true,
1324            low: Box::new(ts_lit(1000)),
1325            high: Box::new(ts_lit(2000)),
1326        });
1327
1328        for expr in [
1329            not_between,
1330            col("ts").gt_eq(ts_lit(1000)).or(col("ts").lt(ts_lit(500))),
1331            Expr::Not(Box::new(col("ts").gt_eq(ts_lit(1000)))),
1332            col("ts").in_list(vec![ts_lit(1000), ts_lit(2000)], false),
1333            col("ts").gt_eq(col("other")),
1334            col("other_ts").gt_eq(ts_lit(1000)),
1335        ] {
1336            assert!(implied_ms(expr).is_none());
1337        }
1338    }
1339
1340    #[test]
1341    fn implied_time_range_unit_conversion() {
1342        let second_1 = lit(ScalarValue::TimestampSecond(Some(1), None));
1343        let ns_1500 = lit(ScalarValue::TimestampNanosecond(Some(1_500_000_000), None));
1344        let ns_1500_5 = lit(ScalarValue::TimestampNanosecond(Some(1_500_500_000), None));
1345
1346        for (expr, expected) in [
1347            (
1348                col("ts").gt_eq(second_1.clone()),
1349                Some(TimestampRange::from_start(ms_ts(1000))),
1350            ),
1351            (
1352                col("ts").lt_eq(second_1),
1353                Some(TimestampRange::until_end(ms_ts(1000), true)),
1354            ),
1355            (
1356                col("ts").eq(ns_1500),
1357                Some(TimestampRange::single(ms_ts(1500))),
1358            ),
1359            (col("ts").eq(ns_1500_5.clone()), None),
1360            (
1361                col("ts").gt_eq(ns_1500_5.clone()),
1362                Some(TimestampRange::from_start(ms_ts(1501))),
1363            ),
1364            (
1365                col("ts").lt_eq(ns_1500_5.clone()),
1366                Some(TimestampRange::until_end(ms_ts(1500), true)),
1367            ),
1368            (
1369                col("ts").gt(ns_1500_5.clone()),
1370                Some(TimestampRange::from_start(ms_ts(1501))),
1371            ),
1372            (
1373                col("ts").lt(ns_1500_5),
1374                Some(TimestampRange::until_end(ms_ts(1501), false)),
1375            ),
1376        ] {
1377            assert_eq!(implied_ms(expr), expected);
1378        }
1379    }
1380
1381    #[test]
1382    fn normalizes_and_clears_time_filters() {
1383        let normalized =
1384            test_scan_fingerprint(vec!["k0 = 'foo'".to_string()], vec![], None, true, 0);
1385
1386        assert!(normalized.time_filters().is_empty());
1387
1388        let fingerprint = test_scan_fingerprint(
1389            vec!["k0 = 'foo'".to_string()],
1390            vec!["ts >= 1000".to_string()],
1391            Some(TimeSeriesRowSelector::LastRow),
1392            true,
1393            7,
1394        );
1395
1396        let reset = fingerprint.without_time_filters();
1397
1398        assert_eq!(reset.read_columns(), fingerprint.read_columns());
1399        assert_eq!(reset.read_column_types(), fingerprint.read_column_types());
1400        assert_eq!(reset.filters(), fingerprint.filters());
1401        assert!(reset.time_filters().is_empty());
1402        assert_eq!(reset.series_row_selector, fingerprint.series_row_selector);
1403        assert_eq!(reset.append_mode, fingerprint.append_mode);
1404        assert_eq!(reset.filter_deleted, fingerprint.filter_deleted);
1405        assert_eq!(reset.merge_mode, fingerprint.merge_mode);
1406        assert_eq!(
1407            reset.partition_expr_version,
1408            fingerprint.partition_expr_version
1409        );
1410    }
1411
1412    fn test_schema() -> Arc<datatypes::arrow::datatypes::Schema> {
1413        use datatypes::arrow::datatypes::{DataType as ArrowDataType, Field, Schema};
1414
1415        Arc::new(Schema::new(vec![Field::new(
1416            "value",
1417            ArrowDataType::Int64,
1418            false,
1419        )]))
1420    }
1421
1422    fn make_batch(values: &[i64]) -> RecordBatch {
1423        use datatypes::arrow::array::Int64Array;
1424
1425        RecordBatch::try_new(
1426            test_schema(),
1427            vec![Arc::new(Int64Array::from(values.to_vec()))],
1428        )
1429        .unwrap()
1430    }
1431
1432    fn make_large_binary_batch(rows: usize, bytes_per_row: usize) -> RecordBatch {
1433        use datatypes::arrow::array::BinaryArray;
1434        use datatypes::arrow::datatypes::{DataType as ArrowDataType, Field, Schema};
1435
1436        let schema = Arc::new(Schema::new(vec![Field::new(
1437            "value",
1438            ArrowDataType::Binary,
1439            false,
1440        )]));
1441        let payload = vec![b'x'; bytes_per_row];
1442        let values = (0..rows).map(|_| payload.as_slice()).collect::<Vec<_>>();
1443
1444        RecordBatch::try_new(schema, vec![Arc::new(BinaryArray::from_vec(values))]).unwrap()
1445    }
1446
1447    #[test]
1448    fn compact_record_batches_keeps_original_boundaries() {
1449        let batches = vec![make_batch(&[1, 2]), make_batch(&[3]), make_batch(&[4, 5])];
1450
1451        let compacted = compact_record_batches(batches).unwrap();
1452
1453        assert_eq!(compacted.batch.num_rows(), 5);
1454        assert_eq!(compacted.slice_lengths, vec![2, 1, 2]);
1455    }
1456
1457    #[tokio::test]
1458    async fn cached_flat_range_stream_replays_original_batches() {
1459        let value = Arc::new(RangeScanCacheValue::new(
1460            vec![CachedBatchSlice {
1461                batch: make_batch(&[1, 2, 3]),
1462                slice_lengths: vec![2, 1],
1463            }],
1464            make_batch(&[1, 2, 3]).get_array_memory_size(),
1465        ));
1466
1467        let replayed = cached_flat_range_stream(value)
1468            .try_collect::<Vec<_>>()
1469            .await
1470            .unwrap();
1471
1472        assert_eq!(replayed.len(), 2);
1473        assert_eq!(replayed[0].num_rows(), 2);
1474        assert_eq!(replayed[1].num_rows(), 1);
1475    }
1476
1477    #[tokio::test]
1478    async fn cache_batch_buffer_finishes_pending_batches() {
1479        let strategy = test_cache_strategy();
1480        let batch = make_batch(&[1, 2, 3]);
1481        let expected_size = batch.get_array_memory_size();
1482        let (key, part_metrics) = test_cache_context(&strategy);
1483
1484        let mut buffer = CacheBatchBuffer::new(&strategy);
1485        buffer.push(batch).unwrap();
1486
1487        let value = finish_cache_batch_buffer(buffer, key.clone(), strategy.clone(), part_metrics)
1488            .await
1489            .unwrap();
1490        assert_eq!(value.cached_batches.len(), 1);
1491        assert_eq!(value.cached_batches[0].slice_lengths, vec![3]);
1492        assert_eq!(value.estimated_batches_size, expected_size);
1493        assert!(Arc::ptr_eq(
1494            &value,
1495            &strategy.get_range_result(&key).unwrap()
1496        ));
1497    }
1498
1499    #[tokio::test]
1500    async fn cache_batch_buffer_compacts_when_rows_exceed_default_batch_size() {
1501        let strategy = test_cache_strategy();
1502        let batch = make_batch(&vec![1; DEFAULT_READ_BATCH_SIZE / 2 + 1]);
1503        let (key, part_metrics) = test_cache_context(&strategy);
1504
1505        let mut buffer = CacheBatchBuffer::new(&strategy);
1506        buffer.push(batch.clone()).unwrap();
1507        buffer.push(batch).unwrap();
1508
1509        assert_eq!(buffer.buffered_rows, 0);
1510        assert!(buffer.buffered_batches.is_empty());
1511
1512        let value = finish_cache_batch_buffer(buffer, key, strategy, part_metrics)
1513            .await
1514            .unwrap();
1515        assert_eq!(value.cached_batches.len(), 1);
1516        assert_eq!(
1517            value.cached_batches[0].slice_lengths,
1518            vec![
1519                DEFAULT_READ_BATCH_SIZE / 2 + 1,
1520                DEFAULT_READ_BATCH_SIZE / 2 + 1
1521            ]
1522        );
1523    }
1524
1525    #[tokio::test]
1526    async fn cache_batch_buffer_compacts_when_buffered_size_exceeds_threshold() {
1527        let large_batch = make_large_binary_batch(DEFAULT_READ_BATCH_SIZE, 4096);
1528        let strategy = CacheStrategy::EnableAll(Arc::new(
1529            CacheManager::builder()
1530                .range_result_cache_size((large_batch.get_array_memory_size() * 3) as u64)
1531                .build(),
1532        ));
1533        let (key, part_metrics) = test_cache_context(&strategy);
1534
1535        let mut buffer = CacheBatchBuffer::new(&strategy);
1536        buffer.push(large_batch.clone()).unwrap();
1537
1538        assert_eq!(buffer.buffered_rows, large_batch.num_rows());
1539        assert_eq!(buffer.buffered_batches.len(), 1);
1540
1541        buffer.push(large_batch.clone()).unwrap();
1542
1543        assert_eq!(buffer.buffered_rows, 0);
1544        assert!(buffer.buffered_batches.is_empty());
1545
1546        let value = finish_cache_batch_buffer(buffer, key, strategy, part_metrics)
1547            .await
1548            .unwrap();
1549        assert_eq!(value.cached_batches.len(), 1);
1550        assert_eq!(
1551            value.cached_batches[0].slice_lengths,
1552            vec![large_batch.num_rows(), large_batch.num_rows()]
1553        );
1554    }
1555
1556    #[tokio::test]
1557    async fn cache_batch_buffer_skips_cache_when_compacted_size_exceeds_limit() {
1558        let large_batch = make_large_binary_batch(DEFAULT_READ_BATCH_SIZE / 2 + 1, 4096);
1559        // Budget only fits two large batches.
1560        let budget = (large_batch.get_array_memory_size() as u64) * 2 + 1;
1561        let strategy = CacheStrategy::EnableAll(Arc::new(
1562            CacheManager::builder()
1563                .range_result_cache_size(budget)
1564                .build(),
1565        ));
1566        let (key, part_metrics) = test_cache_context(&strategy);
1567
1568        let mut buffer = CacheBatchBuffer::new(&strategy);
1569        for _ in 0..4 {
1570            buffer.push(large_batch.clone()).unwrap();
1571        }
1572        assert!(
1573            finish_cache_batch_buffer(buffer, key.clone(), strategy.clone(), part_metrics)
1574                .await
1575                .is_err()
1576        );
1577        assert!(strategy.get_range_result(&key).is_none());
1578    }
1579}