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    /// Handle of the background concat task. Cleared once the task owns a finish
740    /// command, so only unfinished scans cancel it.
741    concat_task: Option<common_runtime::JoinHandle<()>>,
742}
743
744impl CacheBatchBuffer {
745    fn new(cache_strategy: &CacheStrategy) -> Self {
746        let (sender, concat_task) = cache_strategy
747            .range_result_memory_limiter()
748            .map(|limiter| {
749                let skip_threshold_bytes = cache_strategy.range_result_cache_size().unwrap_or(0);
750                let (tx, rx) = mpsc::unbounded_channel();
751                let task = common_runtime::spawn_query(run_cache_concat_task(
752                    rx,
753                    limiter.clone(),
754                    skip_threshold_bytes,
755                ));
756                (tx, task)
757            })
758            .unzip();
759
760        Self {
761            buffered_batches: Vec::new(),
762            buffered_rows: 0,
763            buffered_size: 0,
764            sender,
765            concat_task,
766        }
767    }
768
769    fn push(&mut self, batch: RecordBatch) -> Result<()> {
770        if self.sender.is_none() {
771            return Ok(());
772        }
773
774        self.buffered_rows += batch.num_rows();
775        self.buffered_size += batch.get_array_memory_size();
776        self.buffered_batches.push(batch);
777
778        if self.buffered_batches.len() > 1
779            && (self.buffered_rows > DEFAULT_READ_BATCH_SIZE
780                || self.buffered_size > RANGE_CACHE_COMPACT_THRESHOLD_BYTES)
781        {
782            self.notify_compact();
783        }
784
785        Ok(())
786    }
787
788    fn notify_compact(&mut self) {
789        if self.buffered_batches.is_empty() || self.sender.is_none() {
790            return;
791        }
792
793        let batches = mem::take(&mut self.buffered_batches);
794        self.buffered_rows = 0;
795        self.buffered_size = 0;
796
797        let Some(sender) = &self.sender else {
798            return;
799        };
800        if sender.send(CacheConcatCommand::Compact(batches)).is_err() {
801            self.sender = None;
802        }
803    }
804
805    fn finish(
806        mut self,
807        key: RangeScanCacheKey,
808        cache_strategy: CacheStrategy,
809        part_metrics: PartitionMetrics,
810        result_tx: Option<oneshot::Sender<Result<Arc<RangeScanCacheValue>>>>,
811    ) {
812        let Some(sender) = self.sender.take() else {
813            return;
814        };
815
816        if sender
817            .send(CacheConcatCommand::Finish {
818                pending: mem::take(&mut self.buffered_batches),
819                key,
820                cache_strategy,
821                part_metrics,
822                result_tx,
823            })
824            .is_ok()
825        {
826            // The task now owns the finish command, so it may keep populating the
827            // cache after the scan stream is dropped.
828            self.concat_task = None;
829        }
830    }
831}
832
833impl Drop for CacheBatchBuffer {
834    fn drop(&mut self) {
835        // Still holding the handle means no finish command was sent: the scan was
836        // cancelled or failed and the queued batches can never reach the cache.
837        // Aborting releases them even while the task waits for a memory permit.
838        if let Some(task) = &self.concat_task {
839            task.abort();
840        }
841    }
842}
843
844/// Wraps a stream to cache its output for future range cache hits.
845pub(crate) fn cache_flat_range_stream(
846    mut stream: BoxedRecordBatchStream,
847    cache_strategy: CacheStrategy,
848    key: RangeScanCacheKey,
849    part_metrics: PartitionMetrics,
850) -> BoxedRecordBatchStream {
851    Box::pin(try_stream! {
852        let mut buffer = CacheBatchBuffer::new(&cache_strategy);
853        while let Some(batch) = stream.try_next().await? {
854            buffer.push(batch.clone())?;
855            yield batch;
856        }
857
858        buffer.finish(key, cache_strategy, part_metrics, None);
859    })
860}
861
862/// Creates a `cache_flat_range_stream` with dummy internals for benchmarking.
863///
864/// This avoids exposing `RangeScanCacheKey`, `ScanRequestFingerprint`, and
865/// `PartitionMetrics` publicly.
866#[cfg(feature = "test")]
867pub fn bench_cache_flat_range_stream(
868    stream: BoxedRecordBatchStream,
869    cache_size_bytes: u64,
870    region_id: RegionId,
871) -> BoxedRecordBatchStream {
872    use std::time::Instant;
873
874    use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
875
876    use crate::region::options::MergeMode;
877
878    let cache_manager = Arc::new(
879        crate::cache::CacheManager::builder()
880            .range_result_cache_size(cache_size_bytes)
881            .build(),
882    );
883    let cache_strategy = CacheStrategy::EnableAll(cache_manager);
884
885    let fingerprint = ScanRequestFingerprintBuilder {
886        read_columns: ReadColumns::new(std::iter::empty()),
887        read_column_types: vec![],
888        filters: vec![],
889        time_filters: vec![],
890        series_row_selector: None,
891        append_mode: false,
892        filter_deleted: false,
893        merge_mode: MergeMode::LastRow,
894        sequence_range: None,
895        partition_expr_version: 0,
896    }
897    .build();
898
899    let key = RangeScanCacheKey {
900        region_id,
901        row_groups: vec![],
902        scan: fingerprint,
903    };
904
905    let metrics_set = ExecutionPlanMetricsSet::new();
906    let part_metrics =
907        PartitionMetrics::new(region_id, 0, "bench", Instant::now(), false, &metrics_set);
908
909    cache_flat_range_stream(stream, cache_strategy, key, part_metrics)
910}
911
912#[cfg(test)]
913mod tests {
914    use std::sync::Arc;
915    use std::time::Instant;
916
917    use common_time::Timestamp;
918    use common_time::range::TimestampRange;
919    use common_time::timestamp::TimeUnit;
920    use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
921    use datafusion_common::ScalarValue;
922    use datafusion_expr::{Expr, col, lit};
923    use smallvec::smallvec;
924    use store_api::storage::{FileId, RegionId};
925
926    use super::*;
927    use crate::cache::CacheManager;
928    use crate::read::flat_projection::FlatProjectionMapper;
929    use crate::read::range::{RangeMeta, RowGroupIndex, SourceIndex};
930    use crate::read::scan_region::{PredicateGroup, ScanInput};
931    use crate::sst::file::FileTimeRange;
932    use crate::test_util::memtable_util::metadata_with_primary_key;
933    use crate::test_util::scheduler_util::SchedulerEnv;
934    use crate::test_util::sst_util::sst_file_handle_with_file_id;
935
936    fn test_cache_strategy() -> CacheStrategy {
937        CacheStrategy::EnableAll(Arc::new(
938            CacheManager::builder()
939                .range_result_cache_size(1024 * 1024)
940                .build(),
941        ))
942    }
943
944    fn test_scan_fingerprint(
945        filters: Vec<String>,
946        time_filters: Vec<String>,
947        series_row_selector: Option<TimeSeriesRowSelector>,
948        filter_deleted: bool,
949        partition_expr_version: u64,
950    ) -> ScanRequestFingerprint {
951        let read_columns = ReadColumns::new([1, 2]);
952        ScanRequestFingerprintBuilder {
953            read_columns,
954            read_column_types: vec![None, None],
955            filters,
956            time_filters,
957            series_row_selector,
958            append_mode: false,
959            filter_deleted,
960            merge_mode: MergeMode::LastRow,
961            sequence_range: None,
962            partition_expr_version,
963        }
964        .build()
965    }
966
967    fn test_cache_context(strategy: &CacheStrategy) -> (RangeScanCacheKey, PartitionMetrics) {
968        let region_id = RegionId::new(1, 1);
969        let key = RangeScanCacheKey {
970            region_id,
971            row_groups: vec![],
972            scan: test_scan_fingerprint(vec![], vec![], None, false, 0),
973        };
974
975        let metrics_set = ExecutionPlanMetricsSet::new();
976        let part_metrics =
977            PartitionMetrics::new(region_id, 0, "test", Instant::now(), false, &metrics_set);
978
979        assert!(strategy.get_range_result(&key).is_none());
980        (key, part_metrics)
981    }
982
983    async fn finish_cache_batch_buffer(
984        buffer: CacheBatchBuffer,
985        key: RangeScanCacheKey,
986        cache_strategy: CacheStrategy,
987        part_metrics: PartitionMetrics,
988    ) -> Result<Arc<RangeScanCacheValue>> {
989        let (tx, rx) = oneshot::channel();
990        common_telemetry::info!("finish start");
991        buffer.finish(key, cache_strategy, part_metrics, Some(tx));
992        common_telemetry::info!("finish end");
993        rx.await.context(crate::error::RecvSnafu)?
994    }
995
996    async fn new_stream_context(
997        filters: Vec<Expr>,
998        query_time_range: Option<TimestampRange>,
999        partition_time_range: FileTimeRange,
1000    ) -> (StreamContext, PartitionRange) {
1001        let env = SchedulerEnv::new().await;
1002        let metadata = Arc::new(metadata_with_primary_key(vec![0, 1], false));
1003        let mapper = FlatProjectionMapper::new(&metadata, [0, 2, 3]).unwrap();
1004        let predicate = PredicateGroup::new(metadata.as_ref(), &filters).unwrap();
1005        let file_id = FileId::random();
1006        let file = sst_file_handle_with_file_id(
1007            file_id,
1008            partition_time_range.0.value(),
1009            partition_time_range.1.value(),
1010        );
1011        let input = ScanInput::builder(env.access_layer.clone(), mapper)
1012            .with_predicate(predicate)
1013            .with_time_range(query_time_range)
1014            .with_files(vec![file])
1015            .with_cache(test_cache_strategy())
1016            .build();
1017        let range_meta = RangeMeta {
1018            time_range: partition_time_range,
1019            indices: smallvec![SourceIndex {
1020                index: 0,
1021                num_row_groups: 1,
1022            }],
1023            row_group_indices: smallvec![RowGroupIndex {
1024                index: 0,
1025                row_group_index: 0,
1026            }],
1027            num_rows: 10,
1028        };
1029        let partition_range = range_meta.new_partition_range(0);
1030        let stream_ctx = StreamContext {
1031            input,
1032            ranges: vec![range_meta],
1033            query_start: Instant::now(),
1034        };
1035
1036        (stream_ctx, partition_range)
1037    }
1038
1039    /// Helper to create a timestamp millisecond literal.
1040    fn ts_lit(val: i64) -> Expr {
1041        lit(ScalarValue::TimestampMillisecond(Some(val), None))
1042    }
1043
1044    fn normalized_exprs(exprs: impl IntoIterator<Item = Expr>) -> Vec<String> {
1045        let mut exprs = exprs
1046            .into_iter()
1047            .map(|expr| expr.to_string())
1048            .collect::<Vec<_>>();
1049        exprs.sort_unstable();
1050        exprs
1051    }
1052
1053    async fn assert_range_cache_filters(
1054        filters: Vec<Expr>,
1055        query_time_range: Option<TimestampRange>,
1056        partition_time_range: FileTimeRange,
1057        expected_filters: Vec<Expr>,
1058        expected_time_filters: Vec<Expr>,
1059    ) {
1060        let (stream_ctx, part_range) =
1061            new_stream_context(filters, query_time_range, partition_time_range).await;
1062
1063        let key = build_range_cache_key(&stream_ctx, &part_range).unwrap();
1064
1065        assert_eq!(
1066            key.scan.filters(),
1067            normalized_exprs(expected_filters).as_slice()
1068        );
1069        assert_eq!(
1070            key.scan.time_filters(),
1071            normalized_exprs(expected_time_filters).as_slice()
1072        );
1073    }
1074
1075    #[tokio::test]
1076    async fn range_cache_time_filter_key_cases() {
1077        let partition = (
1078            Timestamp::new_millisecond(1000),
1079            Timestamp::new_millisecond(2000),
1080        );
1081
1082        struct Case {
1083            filters: Vec<Expr>,
1084            query_time_range: Option<TimestampRange>,
1085            expected_filters: Vec<Expr>,
1086            expected_time_filters: Vec<Expr>,
1087        }
1088
1089        // Time filters are stripped only when their implied range fully covers
1090        // the partition's file-time range. `is_not_null(ts)` stays in regular
1091        // filters because it is not routed into `time_filters`.
1092        for case in [
1093            Case {
1094                filters: vec![
1095                    col("ts").gt_eq(ts_lit(1000)),
1096                    col("ts").lt(ts_lit(2001)),
1097                    col("ts").is_not_null(),
1098                    col("k0").eq(lit("foo")),
1099                ],
1100                query_time_range: TimestampRange::with_unit(1000, 2002, TimeUnit::Millisecond),
1101                expected_filters: vec![col("k0").eq(lit("foo")), col("ts").is_not_null()],
1102                expected_time_filters: vec![],
1103            },
1104            Case {
1105                filters: vec![
1106                    col("ts").gt_eq(ts_lit(500)),
1107                    col("ts").lt(ts_lit(3000)),
1108                    col("k0").eq(lit("foo")),
1109                ],
1110                query_time_range: TimestampRange::with_unit(500, 3000, TimeUnit::Millisecond),
1111                expected_filters: vec![col("k0").eq(lit("foo"))],
1112                expected_time_filters: vec![],
1113            },
1114            Case {
1115                filters: vec![
1116                    col("ts").gt_eq(ts_lit(1000)),
1117                    col("ts").lt_eq(ts_lit(2000)),
1118                    col("k0").eq(lit("foo")),
1119                ],
1120                query_time_range: TimestampRange::with_unit(1000, 2001, TimeUnit::Millisecond),
1121                expected_filters: vec![col("k0").eq(lit("foo"))],
1122                expected_time_filters: vec![],
1123            },
1124            Case {
1125                filters: vec![
1126                    col("ts").between(ts_lit(1000), ts_lit(2000)),
1127                    col("k0").eq(lit("foo")),
1128                ],
1129                query_time_range: TimestampRange::with_unit(1000, 2001, TimeUnit::Millisecond),
1130                expected_filters: vec![col("k0").eq(lit("foo"))],
1131                expected_time_filters: vec![],
1132            },
1133            Case {
1134                filters: vec![col("ts").gt_eq(ts_lit(1200)), col("k0").eq(lit("foo"))],
1135                query_time_range: TimestampRange::with_unit(1200, 2001, TimeUnit::Millisecond),
1136                expected_filters: vec![col("k0").eq(lit("foo"))],
1137                expected_time_filters: vec![col("ts").gt_eq(ts_lit(1200))],
1138            },
1139            Case {
1140                filters: vec![
1141                    col("ts").gt_eq(ts_lit(1500)),
1142                    col("ts").is_not_null(),
1143                    col("k0").eq(lit("foo")),
1144                ],
1145                query_time_range: None,
1146                expected_filters: vec![col("k0").eq(lit("foo")), col("ts").is_not_null()],
1147                expected_time_filters: vec![col("ts").gt_eq(ts_lit(1500))],
1148            },
1149        ] {
1150            assert_range_cache_filters(
1151                case.filters,
1152                case.query_time_range,
1153                partition,
1154                case.expected_filters,
1155                case.expected_time_filters,
1156            )
1157            .await;
1158        }
1159    }
1160
1161    #[tokio::test]
1162    async fn two_distinct_queries_share_cache_key_when_both_cover() {
1163        let partition_range = (
1164            Timestamp::new_millisecond(1000),
1165            Timestamp::new_millisecond(2000),
1166        );
1167
1168        let (ctx_a, part_a) = new_stream_context(
1169            vec![
1170                col("ts").gt_eq(ts_lit(500)),
1171                col("ts").lt(ts_lit(3000)),
1172                col("k0").eq(lit("foo")),
1173            ],
1174            TimestampRange::with_unit(500, 3000, TimeUnit::Millisecond),
1175            partition_range,
1176        )
1177        .await;
1178        let (ctx_b, part_b) = new_stream_context(
1179            vec![
1180                col("ts").gt_eq(ts_lit(100)),
1181                col("ts").lt(ts_lit(5000)),
1182                col("k0").eq(lit("foo")),
1183            ],
1184            TimestampRange::with_unit(100, 5000, TimeUnit::Millisecond),
1185            partition_range,
1186        )
1187        .await;
1188
1189        let key_a = build_range_cache_key(&ctx_a, &part_a).unwrap();
1190        let key_b = build_range_cache_key(&ctx_b, &part_b).unwrap();
1191        assert_eq!(key_a.scan, key_b.scan);
1192        assert!(key_a.scan.time_filters().is_empty());
1193    }
1194
1195    #[tokio::test]
1196    async fn scan_stages_have_separate_cache_keys() {
1197        let partition_range = (
1198            Timestamp::new_millisecond(1000),
1199            Timestamp::new_millisecond(2000),
1200        );
1201        let (ctx, part_range) =
1202            new_stream_context(vec![col("k0").eq(lit("foo"))], None, partition_range).await;
1203
1204        let data_key = build_range_cache_key(&ctx, &part_range).unwrap();
1205        let candidate_key = build_candidate_range_cache_key(&ctx, &part_range).unwrap();
1206        let range_0 = SeriesRange::new(0, 2).unwrap();
1207        let range_1 = SeriesRange::new(1, 2).unwrap();
1208        let series_key_0 = build_series_range_cache_key(&ctx, &part_range, range_0).unwrap();
1209        let series_key_1 = build_series_range_cache_key(&ctx, &part_range, range_1).unwrap();
1210
1211        assert_ne!(data_key.scan, candidate_key.scan);
1212        assert_ne!(data_key.scan, series_key_0.scan);
1213        assert_ne!(candidate_key.scan, series_key_0.scan);
1214        assert_ne!(series_key_0.scan, series_key_1.scan);
1215        assert_eq!(data_key.row_groups, candidate_key.row_groups);
1216        assert_eq!(data_key.row_groups, series_key_0.row_groups);
1217    }
1218
1219    #[tokio::test]
1220    async fn disables_optimization_on_or_clause() {
1221        let partition_range = (
1222            Timestamp::new_millisecond(1000),
1223            Timestamp::new_millisecond(2000),
1224        );
1225
1226        let or_a = col("ts").gt_eq(ts_lit(1000)).or(col("ts").lt(ts_lit(500)));
1227        let or_b = col("ts").gt_eq(ts_lit(900)).or(col("ts").lt(ts_lit(400)));
1228
1229        let (ctx_a, part_a) = new_stream_context(
1230            vec![or_a.clone(), col("k0").eq(lit("foo"))],
1231            None,
1232            partition_range,
1233        )
1234        .await;
1235        let (ctx_b, part_b) = new_stream_context(
1236            vec![or_b.clone(), col("k0").eq(lit("foo"))],
1237            None,
1238            partition_range,
1239        )
1240        .await;
1241
1242        assert!(ctx_a.input.implied_time_range().is_none());
1243        let key_a = build_range_cache_key(&ctx_a, &part_a).unwrap();
1244        let key_b = build_range_cache_key(&ctx_b, &part_b).unwrap();
1245        assert_ne!(key_a.scan, key_b.scan);
1246        assert_eq!(
1247            key_a.scan.time_filters(),
1248            normalized_exprs([or_a]).as_slice()
1249        );
1250    }
1251
1252    #[tokio::test]
1253    async fn empty_implied_range_does_not_panic_on_non_ms_file_range() {
1254        // Contradictory time predicates make the implied range empty. The
1255        // empty range's sentinel timestamps use `Timestamp::default()` (ms),
1256        // so without the `is_empty()` short-circuit the unit asserts would
1257        // panic against a non-ms `range_meta.time_range`.
1258        let partition = (
1259            Timestamp::new_millisecond(1000),
1260            Timestamp::new_millisecond(2000),
1261        );
1262
1263        let (mut ctx, part_range) = new_stream_context(
1264            vec![
1265                col("ts").gt_eq(ts_lit(1500)),
1266                col("ts").lt(ts_lit(1500)),
1267                col("k0").eq(lit("foo")),
1268            ],
1269            TimestampRange::with_unit(1500, 3000, TimeUnit::Millisecond),
1270            partition,
1271        )
1272        .await;
1273
1274        assert!(ctx.input.implied_time_range().unwrap().is_empty());
1275        ctx.ranges[0].time_range = (
1276            Timestamp::new(1_000_000_000, TimeUnit::Nanosecond),
1277            Timestamp::new(2_000_000_000, TimeUnit::Nanosecond),
1278        );
1279
1280        let key = build_range_cache_key(&ctx, &part_range).unwrap();
1281        // Empty implied range cannot cover, so time filters stay in the key.
1282        assert!(!key.scan.time_filters().is_empty());
1283    }
1284
1285    #[tokio::test]
1286    async fn mixed_unit_file_range_is_compared_by_instant() {
1287        // After the time index unit was widened, pre-alter files keep the old
1288        // unit while `scan_implied_time_range` uses the current column unit.
1289        // The coverage decision must compare instants across units instead of
1290        // asserting unit equality (which panicked here for widened regions).
1291        let partition = (
1292            Timestamp::new_millisecond(1000),
1293            Timestamp::new_millisecond(2000),
1294        );
1295
1296        // Covering: the implied range [0, 3000)ms contains the file range
1297        // [1s, 2s] expressed in microseconds; time filters are dropped from
1298        // the key.
1299        let (mut ctx, part_range) = new_stream_context(
1300            vec![
1301                col("ts").gt_eq(ts_lit(0)),
1302                col("ts").lt(ts_lit(3000)),
1303                col("k0").eq(lit("foo")),
1304            ],
1305            TimestampRange::with_unit(0, 3000, TimeUnit::Millisecond),
1306            partition,
1307        )
1308        .await;
1309        ctx.ranges[0].time_range = (
1310            Timestamp::new_microsecond(1_000_000),
1311            Timestamp::new_microsecond(2_000_000),
1312        );
1313        let key = build_range_cache_key(&ctx, &part_range).unwrap();
1314        assert!(key.scan.time_filters().is_empty());
1315
1316        // Not covering: the implied range [1500, 3000)ms excludes the file's
1317        // 1s lower bound; time filters stay in the key.
1318        let (mut ctx, part_range) = new_stream_context(
1319            vec![
1320                col("ts").gt_eq(ts_lit(1500)),
1321                col("ts").lt(ts_lit(3000)),
1322                col("k0").eq(lit("foo")),
1323            ],
1324            TimestampRange::with_unit(1500, 3000, TimeUnit::Millisecond),
1325            partition,
1326        )
1327        .await;
1328        ctx.ranges[0].time_range = (
1329            Timestamp::new_microsecond(1_000_000),
1330            Timestamp::new_microsecond(2_000_000),
1331        );
1332        let key = build_range_cache_key(&ctx, &part_range).unwrap();
1333        assert!(!key.scan.time_filters().is_empty());
1334    }
1335
1336    fn ms_ts(v: i64) -> Timestamp {
1337        Timestamp::new_millisecond(v)
1338    }
1339
1340    fn implied_ms(expr: Expr) -> Option<TimestampRange> {
1341        implied_time_range_from_exprs("ts", TimeUnit::Millisecond, &[&expr])
1342    }
1343
1344    #[test]
1345    fn implied_time_range_supported_exprs() {
1346        for (expr, expected) in [
1347            (
1348                col("ts").gt_eq(ts_lit(1000)),
1349                Some(TimestampRange::from_start(ms_ts(1000))),
1350            ),
1351            (
1352                col("ts").gt(ts_lit(1000)),
1353                Some(TimestampRange::from_start(ms_ts(1001))),
1354            ),
1355            (
1356                col("ts").lt_eq(ts_lit(2000)),
1357                Some(TimestampRange::until_end(ms_ts(2000), true)),
1358            ),
1359            (
1360                col("ts").lt(ts_lit(2000)),
1361                Some(TimestampRange::until_end(ms_ts(2000), false)),
1362            ),
1363            (
1364                col("ts").eq(ts_lit(1500)),
1365                Some(TimestampRange::single(ms_ts(1500))),
1366            ),
1367            (
1368                ts_lit(1000).lt_eq(col("ts")),
1369                Some(TimestampRange::from_start(ms_ts(1000))),
1370            ),
1371            (
1372                col("ts").between(ts_lit(1000), ts_lit(2000)),
1373                Some(TimestampRange::new_inclusive(
1374                    Some(ms_ts(1000)),
1375                    Some(ms_ts(2000)),
1376                )),
1377            ),
1378            (
1379                col("ts")
1380                    .gt_eq(ts_lit(1000))
1381                    .and(col("ts").lt(ts_lit(2000))),
1382                TimestampRange::with_unit(1000, 2000, TimeUnit::Millisecond),
1383            ),
1384            (
1385                col("ts")
1386                    .gt_eq(ts_lit(1000))
1387                    .and(col("ts").lt(ts_lit(5000)))
1388                    .and(col("ts").lt_eq(ts_lit(3000))),
1389                TimestampRange::with_unit(1000, 3001, TimeUnit::Millisecond),
1390            ),
1391        ] {
1392            assert_eq!(implied_ms(expr), expected);
1393        }
1394
1395        assert_eq!(
1396            implied_time_range_from_exprs("ts", TimeUnit::Millisecond, &[]),
1397            None
1398        );
1399    }
1400
1401    #[test]
1402    fn implied_time_range_unsupported_exprs() {
1403        let not_between = Expr::Between(Between {
1404            expr: Box::new(col("ts")),
1405            negated: true,
1406            low: Box::new(ts_lit(1000)),
1407            high: Box::new(ts_lit(2000)),
1408        });
1409
1410        for expr in [
1411            not_between,
1412            col("ts").gt_eq(ts_lit(1000)).or(col("ts").lt(ts_lit(500))),
1413            Expr::Not(Box::new(col("ts").gt_eq(ts_lit(1000)))),
1414            col("ts").in_list(vec![ts_lit(1000), ts_lit(2000)], false),
1415            col("ts").gt_eq(col("other")),
1416            col("other_ts").gt_eq(ts_lit(1000)),
1417        ] {
1418            assert!(implied_ms(expr).is_none());
1419        }
1420    }
1421
1422    #[test]
1423    fn implied_time_range_unit_conversion() {
1424        let second_1 = lit(ScalarValue::TimestampSecond(Some(1), None));
1425        let ns_1500 = lit(ScalarValue::TimestampNanosecond(Some(1_500_000_000), None));
1426        let ns_1500_5 = lit(ScalarValue::TimestampNanosecond(Some(1_500_500_000), None));
1427
1428        for (expr, expected) in [
1429            (
1430                col("ts").gt_eq(second_1.clone()),
1431                Some(TimestampRange::from_start(ms_ts(1000))),
1432            ),
1433            (
1434                col("ts").lt_eq(second_1),
1435                Some(TimestampRange::until_end(ms_ts(1000), true)),
1436            ),
1437            (
1438                col("ts").eq(ns_1500),
1439                Some(TimestampRange::single(ms_ts(1500))),
1440            ),
1441            (col("ts").eq(ns_1500_5.clone()), None),
1442            (
1443                col("ts").gt_eq(ns_1500_5.clone()),
1444                Some(TimestampRange::from_start(ms_ts(1501))),
1445            ),
1446            (
1447                col("ts").lt_eq(ns_1500_5.clone()),
1448                Some(TimestampRange::until_end(ms_ts(1500), true)),
1449            ),
1450            (
1451                col("ts").gt(ns_1500_5.clone()),
1452                Some(TimestampRange::from_start(ms_ts(1501))),
1453            ),
1454            (
1455                col("ts").lt(ns_1500_5),
1456                Some(TimestampRange::until_end(ms_ts(1501), false)),
1457            ),
1458        ] {
1459            assert_eq!(implied_ms(expr), expected);
1460        }
1461    }
1462
1463    #[test]
1464    fn selector_after_merge_changes_fingerprint() {
1465        let ordinary = test_scan_fingerprint(
1466            vec!["k0 = 'foo'".to_string()],
1467            vec![],
1468            Some(TimeSeriesRowSelector::LastRow { after_merge: false }),
1469            true,
1470            0,
1471        );
1472        let after_merge = test_scan_fingerprint(
1473            vec!["k0 = 'foo'".to_string()],
1474            vec![],
1475            Some(TimeSeriesRowSelector::LastRow { after_merge: true }),
1476            true,
1477            0,
1478        );
1479
1480        assert_ne!(ordinary, after_merge);
1481    }
1482
1483    #[test]
1484    fn true_selector_after_merge_is_preserved_by_fingerprint_transforms() {
1485        let normalized =
1486            test_scan_fingerprint(vec!["k0 = 'foo'".to_string()], vec![], None, true, 0);
1487
1488        assert!(normalized.time_filters().is_empty());
1489
1490        let fingerprint = test_scan_fingerprint(
1491            vec!["k0 = 'foo'".to_string()],
1492            vec!["ts >= 1000".to_string()],
1493            Some(TimeSeriesRowSelector::LastRow { after_merge: true }),
1494            true,
1495            7,
1496        );
1497
1498        let reset = fingerprint.without_time_filters();
1499        let candidate = fingerprint.for_candidate_series();
1500        let series_data = fingerprint.for_series_data(SeriesRange::new(0, 1).unwrap());
1501
1502        assert_eq!(reset.read_columns(), fingerprint.read_columns());
1503        assert_eq!(reset.read_column_types(), fingerprint.read_column_types());
1504        assert_eq!(reset.filters(), fingerprint.filters());
1505        assert!(reset.time_filters().is_empty());
1506        assert_eq!(reset.series_row_selector, fingerprint.series_row_selector);
1507        assert_eq!(
1508            fingerprint.series_row_selector,
1509            Some(TimeSeriesRowSelector::LastRow { after_merge: true })
1510        );
1511        assert_eq!(
1512            candidate.series_row_selector,
1513            fingerprint.series_row_selector
1514        );
1515        assert_eq!(
1516            series_data.series_row_selector,
1517            fingerprint.series_row_selector
1518        );
1519        assert_eq!(reset.append_mode, fingerprint.append_mode);
1520        assert_eq!(reset.filter_deleted, fingerprint.filter_deleted);
1521        assert_eq!(reset.merge_mode, fingerprint.merge_mode);
1522        assert_eq!(
1523            reset.partition_expr_version,
1524            fingerprint.partition_expr_version
1525        );
1526    }
1527
1528    fn test_schema() -> Arc<datatypes::arrow::datatypes::Schema> {
1529        use datatypes::arrow::datatypes::{DataType as ArrowDataType, Field, Schema};
1530
1531        Arc::new(Schema::new(vec![Field::new(
1532            "value",
1533            ArrowDataType::Int64,
1534            false,
1535        )]))
1536    }
1537
1538    fn make_batch(values: &[i64]) -> RecordBatch {
1539        use datatypes::arrow::array::Int64Array;
1540
1541        RecordBatch::try_new(
1542            test_schema(),
1543            vec![Arc::new(Int64Array::from(values.to_vec()))],
1544        )
1545        .unwrap()
1546    }
1547
1548    fn make_large_binary_batch(rows: usize, bytes_per_row: usize) -> RecordBatch {
1549        use datatypes::arrow::array::BinaryArray;
1550        use datatypes::arrow::datatypes::{DataType as ArrowDataType, Field, Schema};
1551
1552        let schema = Arc::new(Schema::new(vec![Field::new(
1553            "value",
1554            ArrowDataType::Binary,
1555            false,
1556        )]));
1557        let payload = vec![b'x'; bytes_per_row];
1558        let values = (0..rows).map(|_| payload.as_slice()).collect::<Vec<_>>();
1559
1560        RecordBatch::try_new(schema, vec![Arc::new(BinaryArray::from_vec(values))]).unwrap()
1561    }
1562
1563    #[test]
1564    fn compact_record_batches_keeps_original_boundaries() {
1565        let batches = vec![make_batch(&[1, 2]), make_batch(&[3]), make_batch(&[4, 5])];
1566
1567        let compacted = compact_record_batches(batches).unwrap();
1568
1569        assert_eq!(compacted.batch.num_rows(), 5);
1570        assert_eq!(compacted.slice_lengths, vec![2, 1, 2]);
1571    }
1572
1573    #[tokio::test]
1574    async fn cached_flat_range_stream_replays_original_batches() {
1575        let value = Arc::new(RangeScanCacheValue::new(
1576            vec![CachedBatchSlice {
1577                batch: make_batch(&[1, 2, 3]),
1578                slice_lengths: vec![2, 1],
1579            }],
1580            make_batch(&[1, 2, 3]).get_array_memory_size(),
1581        ));
1582
1583        let replayed = cached_flat_range_stream(value)
1584            .try_collect::<Vec<_>>()
1585            .await
1586            .unwrap();
1587
1588        assert_eq!(replayed.len(), 2);
1589        assert_eq!(replayed[0].num_rows(), 2);
1590        assert_eq!(replayed[1].num_rows(), 1);
1591    }
1592
1593    #[tokio::test]
1594    async fn dropped_cache_buffer_releases_batches_while_waiting_for_memory() {
1595        let strategy = test_cache_strategy();
1596        let limiter = strategy.range_result_memory_limiter().unwrap();
1597        // Take every permit so the concat task blocks before it can compact.
1598        let _permit = limiter
1599            .acquire(limiter.available_permits() * limiter.permit_bytes())
1600            .await
1601            .unwrap();
1602        let batch = make_batch(&vec![1; DEFAULT_READ_BATCH_SIZE / 2 + 1]);
1603        let weak = Arc::downgrade(batch.column(0));
1604        let mut buffer = CacheBatchBuffer::new(&strategy);
1605        buffer.push(batch.clone()).unwrap();
1606        buffer.push(batch).unwrap();
1607        // Both batches were handed to the task instead of staying in the buffer.
1608        assert!(buffer.buffered_batches.is_empty());
1609
1610        // Cancel only once the task is parked on the permit, otherwise the abort
1611        // could land on a task that was never polled.
1612        tokio::time::timeout(std::time::Duration::from_secs(5), async {
1613            while limiter.waited_acquires() == 0 {
1614                tokio::task::yield_now().await;
1615            }
1616        })
1617        .await
1618        .expect("concat task must wait for a memory permit");
1619        drop(buffer);
1620
1621        tokio::time::timeout(std::time::Duration::from_secs(5), async {
1622            while weak.upgrade().is_some() {
1623                tokio::task::yield_now().await;
1624            }
1625        })
1626        .await
1627        .expect("cancelled cache work must release input without waiting for a permit");
1628    }
1629
1630    #[tokio::test]
1631    async fn cache_batch_buffer_finishes_pending_batches() {
1632        let strategy = test_cache_strategy();
1633        let batch = make_batch(&[1, 2, 3]);
1634        let expected_size = batch.get_array_memory_size();
1635        let (key, part_metrics) = test_cache_context(&strategy);
1636
1637        let mut buffer = CacheBatchBuffer::new(&strategy);
1638        buffer.push(batch).unwrap();
1639
1640        let value = finish_cache_batch_buffer(buffer, key.clone(), strategy.clone(), part_metrics)
1641            .await
1642            .unwrap();
1643        assert_eq!(value.cached_batches.len(), 1);
1644        assert_eq!(value.cached_batches[0].slice_lengths, vec![3]);
1645        assert_eq!(value.estimated_batches_size, expected_size);
1646        assert!(Arc::ptr_eq(
1647            &value,
1648            &strategy.get_range_result(&key).unwrap()
1649        ));
1650    }
1651
1652    #[tokio::test]
1653    async fn cache_batch_buffer_compacts_when_rows_exceed_default_batch_size() {
1654        let strategy = test_cache_strategy();
1655        let batch = make_batch(&vec![1; DEFAULT_READ_BATCH_SIZE / 2 + 1]);
1656        let (key, part_metrics) = test_cache_context(&strategy);
1657
1658        let mut buffer = CacheBatchBuffer::new(&strategy);
1659        buffer.push(batch.clone()).unwrap();
1660        buffer.push(batch).unwrap();
1661
1662        assert_eq!(buffer.buffered_rows, 0);
1663        assert!(buffer.buffered_batches.is_empty());
1664
1665        let value = finish_cache_batch_buffer(buffer, key, strategy, part_metrics)
1666            .await
1667            .unwrap();
1668        assert_eq!(value.cached_batches.len(), 1);
1669        assert_eq!(
1670            value.cached_batches[0].slice_lengths,
1671            vec![
1672                DEFAULT_READ_BATCH_SIZE / 2 + 1,
1673                DEFAULT_READ_BATCH_SIZE / 2 + 1
1674            ]
1675        );
1676    }
1677
1678    #[tokio::test]
1679    async fn cache_batch_buffer_compacts_when_buffered_size_exceeds_threshold() {
1680        let large_batch = make_large_binary_batch(DEFAULT_READ_BATCH_SIZE, 4096);
1681        let strategy = CacheStrategy::EnableAll(Arc::new(
1682            CacheManager::builder()
1683                .range_result_cache_size((large_batch.get_array_memory_size() * 3) as u64)
1684                .build(),
1685        ));
1686        let (key, part_metrics) = test_cache_context(&strategy);
1687
1688        let mut buffer = CacheBatchBuffer::new(&strategy);
1689        buffer.push(large_batch.clone()).unwrap();
1690
1691        assert_eq!(buffer.buffered_rows, large_batch.num_rows());
1692        assert_eq!(buffer.buffered_batches.len(), 1);
1693
1694        buffer.push(large_batch.clone()).unwrap();
1695
1696        assert_eq!(buffer.buffered_rows, 0);
1697        assert!(buffer.buffered_batches.is_empty());
1698
1699        let value = finish_cache_batch_buffer(buffer, key, strategy, part_metrics)
1700            .await
1701            .unwrap();
1702        assert_eq!(value.cached_batches.len(), 1);
1703        assert_eq!(
1704            value.cached_batches[0].slice_lengths,
1705            vec![large_batch.num_rows(), large_batch.num_rows()]
1706        );
1707    }
1708
1709    #[tokio::test]
1710    async fn cache_batch_buffer_skips_cache_when_compacted_size_exceeds_limit() {
1711        let large_batch = make_large_binary_batch(DEFAULT_READ_BATCH_SIZE / 2 + 1, 4096);
1712        // Budget only fits two large batches.
1713        let budget = (large_batch.get_array_memory_size() as u64) * 2 + 1;
1714        let strategy = CacheStrategy::EnableAll(Arc::new(
1715            CacheManager::builder()
1716                .range_result_cache_size(budget)
1717                .build(),
1718        ));
1719        let (key, part_metrics) = test_cache_context(&strategy);
1720
1721        let mut buffer = CacheBatchBuffer::new(&strategy);
1722        for _ in 0..4 {
1723            buffer.push(large_batch.clone()).unwrap();
1724        }
1725        assert!(
1726            finish_cache_batch_buffer(buffer, key.clone(), strategy.clone(), part_metrics)
1727                .await
1728                .is_err()
1729        );
1730        assert!(strategy.get_range_result(&key).is_none());
1731    }
1732}