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