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