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::future::Future;
18use std::mem;
19use std::sync::Arc;
20
21use async_stream::try_stream;
22use common_telemetry::warn;
23use common_time::Timestamp;
24use common_time::range::TimestampRange;
25use common_time::timestamp::TimeUnit;
26use datafusion_expr::expr::Expr;
27use datafusion_expr::{Between, BinaryExpr, Operator};
28use datatypes::arrow::compute::concat_batches;
29use datatypes::arrow::record_batch::RecordBatch;
30use datatypes::prelude::ConcreteDataType;
31use datatypes::value::scalar_value_to_timestamp;
32use futures::TryStreamExt;
33use snafu::ResultExt;
34use store_api::region_engine::PartitionRange;
35use store_api::storage::{FileId, RegionId, SequenceRange, TimeSeriesRowSelector};
36use table::predicate::is_string_timestamp_literal;
37use tokio::sync::{mpsc, oneshot};
38
39use crate::cache::CacheStrategy;
40use crate::cache::range_cache::{RangeCacheDelivery, RangeCacheLookup, RangeCacheWorker};
41use crate::error::{ComputeArrowSnafu, Result};
42use crate::read::BoxedRecordBatchStream;
43use crate::read::read_columns::ReadColumns;
44use crate::read::scan_region::StreamContext;
45use crate::read::scan_util::PartitionMetrics;
46use crate::read::series_reader::SeriesRange;
47use crate::region::options::MergeMode;
48use crate::sst::parquet::DEFAULT_READ_BATCH_SIZE;
49
50const RANGE_CACHE_COMPACT_THRESHOLD_BYTES: usize = 8 * 1024 * 1024;
51
52/// Fingerprint of the scan request fields that affect partition range cache reuse.
53///
54/// It records a normalized view of the projected columns and filters, plus
55/// scan options that can change the returned rows. Schema-dependent metadata
56/// and the partition expression version are included so cached results are not
57/// reused across incompatible schema or partitioning changes.
58#[derive(Debug, Clone, PartialEq, Eq, Hash)]
59pub(crate) struct ScanRequestFingerprint {
60    /// Projection and filters without the time index and partition exprs.
61    inner: Arc<SharedScanRequestFingerprint>,
62    /// Filters with the time index column.
63    time_filters: Option<Arc<Vec<String>>>,
64    series_row_selector: Option<TimeSeriesRowSelector>,
65    append_mode: bool,
66    filter_deleted: bool,
67    merge_mode: MergeMode,
68    /// Exact sequence range applied row-level on SST reads, or `None` for scans
69    /// without row-level sequence filtering. Kept in the cache key so filtered
70    /// range results are never reused for scans with a different (or no) range.
71    sequence_range: Option<SequenceRange>,
72    stage: RangeScanStage,
73    /// We keep the partition expr version to ensure we won't reuse the fingerprint after we change the partition expr.
74    /// We store the version instead of the whole partition expr or partition expr filters.
75    partition_expr_version: u64,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
79enum RangeScanStage {
80    Data,
81    /// Source batches may be split for a later cross-range merge. Keep their
82    /// cached batch boundaries separate from ordinary sequential scans.
83    DataForCrossRangeMerge,
84    CandidateSeries,
85    SeriesData(SeriesRange),
86}
87
88#[derive(Debug)]
89pub(crate) struct ScanRequestFingerprintBuilder {
90    pub(crate) read_columns: ReadColumns,
91    pub(crate) read_column_types: Vec<Option<ConcreteDataType>>,
92    pub(crate) filters: Vec<String>,
93    pub(crate) time_filters: Vec<String>,
94    pub(crate) series_row_selector: Option<TimeSeriesRowSelector>,
95    pub(crate) append_mode: bool,
96    pub(crate) filter_deleted: bool,
97    pub(crate) merge_mode: MergeMode,
98    pub(crate) sequence_range: Option<SequenceRange>,
99    pub(crate) partition_expr_version: u64,
100}
101
102impl ScanRequestFingerprintBuilder {
103    pub(crate) fn build(self) -> ScanRequestFingerprint {
104        let Self {
105            read_columns,
106            read_column_types,
107            filters,
108            time_filters,
109            series_row_selector,
110            append_mode,
111            filter_deleted,
112            merge_mode,
113            sequence_range,
114            partition_expr_version,
115        } = self;
116
117        ScanRequestFingerprint {
118            inner: Arc::new(SharedScanRequestFingerprint {
119                read_columns,
120                read_column_types,
121                filters,
122            }),
123            time_filters: (!time_filters.is_empty()).then(|| Arc::new(time_filters)),
124            series_row_selector,
125            append_mode,
126            filter_deleted,
127            merge_mode,
128            sequence_range,
129            stage: RangeScanStage::Data,
130            partition_expr_version,
131        }
132    }
133}
134
135/// Non-copiable struct of the fingerprint.
136#[derive(Debug, PartialEq, Eq, Hash)]
137struct SharedScanRequestFingerprint {
138    /// Logical columns of the projection.
139    read_columns: ReadColumns,
140    /// Column types of the projection.
141    /// We keep this to ensure we won't reuse the fingerprint after a schema change.
142    read_column_types: Vec<Option<ConcreteDataType>>,
143    /// Filters without the time index column and region partition exprs.
144    filters: Vec<String>,
145}
146
147impl ScanRequestFingerprint {
148    #[cfg(test)]
149    pub(crate) fn read_columns(&self) -> &ReadColumns {
150        &self.inner.read_columns
151    }
152
153    #[cfg(test)]
154    pub(crate) fn read_column_types(&self) -> &[Option<ConcreteDataType>] {
155        &self.inner.read_column_types
156    }
157
158    #[cfg(test)]
159    pub(crate) fn filters(&self) -> &[String] {
160        &self.inner.filters
161    }
162
163    #[cfg(test)]
164    pub(crate) fn time_filters(&self) -> &[String] {
165        self.time_filters
166            .as_deref()
167            .map(Vec::as_slice)
168            .unwrap_or(&[])
169    }
170
171    pub(crate) fn without_time_filters(&self) -> Self {
172        Self {
173            inner: Arc::clone(&self.inner),
174            time_filters: None,
175            series_row_selector: self.series_row_selector,
176            append_mode: self.append_mode,
177            filter_deleted: self.filter_deleted,
178            merge_mode: self.merge_mode,
179            sequence_range: self.sequence_range,
180            stage: self.stage,
181            partition_expr_version: self.partition_expr_version,
182        }
183    }
184
185    fn for_candidate_series(&self) -> Self {
186        Self {
187            inner: Arc::clone(&self.inner),
188            time_filters: self.time_filters.clone(),
189            series_row_selector: self.series_row_selector,
190            append_mode: self.append_mode,
191            filter_deleted: self.filter_deleted,
192            merge_mode: self.merge_mode,
193            sequence_range: self.sequence_range,
194            stage: RangeScanStage::CandidateSeries,
195            partition_expr_version: self.partition_expr_version,
196        }
197    }
198
199    fn for_series_data(&self, range: SeriesRange) -> Self {
200        Self {
201            inner: Arc::clone(&self.inner),
202            time_filters: self.time_filters.clone(),
203            series_row_selector: self.series_row_selector,
204            append_mode: self.append_mode,
205            filter_deleted: self.filter_deleted,
206            merge_mode: self.merge_mode,
207            sequence_range: self.sequence_range,
208            stage: RangeScanStage::SeriesData(range),
209            partition_expr_version: self.partition_expr_version,
210        }
211    }
212
213    pub(crate) fn estimated_size(&self) -> usize {
214        mem::size_of::<SharedScanRequestFingerprint>()
215            + self.inner.read_columns.estimated_size()
216            + self.inner.read_column_types.capacity() * mem::size_of::<Option<ConcreteDataType>>()
217            + self.inner.filters.capacity() * mem::size_of::<String>()
218            + self
219                .inner
220                .filters
221                .iter()
222                .map(|filter| filter.capacity())
223                .sum::<usize>()
224            + self.time_filters.as_ref().map_or(0, |filters| {
225                mem::size_of::<Vec<String>>()
226                    + filters.capacity() * mem::size_of::<String>()
227                    + filters
228                        .iter()
229                        .map(|filter| filter.capacity())
230                        .sum::<usize>()
231            })
232    }
233}
234
235/// Cache key for range scan outputs.
236#[derive(Debug, Clone, PartialEq, Eq, Hash)]
237pub(crate) struct RangeScanCacheKey {
238    pub(crate) region_id: RegionId,
239    /// Sorted (file_id, row_group_index) pairs that uniquely identify the data this range covers.
240    pub(crate) row_groups: Vec<(FileId, i64)>,
241    pub(crate) scan: ScanRequestFingerprint,
242}
243
244impl RangeScanCacheKey {
245    pub(crate) fn for_cross_range_merge(mut self) -> Self {
246        debug_assert_eq!(self.scan.stage, RangeScanStage::Data);
247        self.scan.stage = RangeScanStage::DataForCrossRangeMerge;
248        self
249    }
250
251    pub(crate) fn estimated_size(&self) -> usize {
252        mem::size_of::<Self>()
253            + self.row_groups.capacity() * mem::size_of::<(FileId, i64)>()
254            + self.scan.estimated_size()
255    }
256}
257
258/// Cached result for one range scan.
259#[derive(Debug)]
260pub(crate) struct CachedBatchSlice {
261    batch: RecordBatch,
262    slice_lengths: Vec<usize>,
263}
264
265impl CachedBatchSlice {
266    fn metadata_size(&self) -> usize {
267        self.slice_lengths.capacity() * mem::size_of::<usize>()
268    }
269}
270
271pub(crate) struct RangeScanCacheValue {
272    cached_batches: Vec<CachedBatchSlice>,
273    /// Precomputed size of all compacted batches.
274    estimated_batches_size: usize,
275}
276
277impl RangeScanCacheValue {
278    pub(crate) fn new(
279        cached_batches: Vec<CachedBatchSlice>,
280        estimated_batches_size: usize,
281    ) -> Self {
282        Self {
283            cached_batches,
284            estimated_batches_size,
285        }
286    }
287
288    pub(crate) fn estimated_size(&self) -> usize {
289        mem::size_of::<Self>()
290            + self.cached_batches.capacity() * mem::size_of::<CachedBatchSlice>()
291            + self
292                .cached_batches
293                .iter()
294                .map(CachedBatchSlice::metadata_size)
295                .sum::<usize>()
296            + self.estimated_batches_size
297    }
298}
299
300/// Row groups and whether all sources are file-only for a partition range.
301pub(crate) struct PartitionRangeRowGroups {
302    /// Sorted (file_id, row_group_index) pairs.
303    pub(crate) row_groups: Vec<(FileId, i64)>,
304    pub(crate) only_file_sources: bool,
305}
306
307/// Collects (file_id, row_group_index) pairs from a partition range's row group indices.
308pub(crate) fn collect_partition_range_row_groups(
309    stream_ctx: &StreamContext,
310    part_range: &PartitionRange,
311) -> PartitionRangeRowGroups {
312    let range_meta = &stream_ctx.ranges[part_range.identifier];
313    let mut row_groups = Vec::new();
314    let mut only_file_sources = true;
315
316    for index in &range_meta.row_group_indices {
317        if stream_ctx.is_file_range_index(*index) {
318            let file_id = stream_ctx.input.file_from_index(*index).file_id().file_id();
319            row_groups.push((file_id, index.row_group_index));
320        } else {
321            only_file_sources = false;
322        }
323    }
324
325    row_groups.sort_unstable_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()).then(a.1.cmp(&b.1)));
326
327    PartitionRangeRowGroups {
328        row_groups,
329        only_file_sources,
330    }
331}
332
333/// Returns the timestamp range where all time-only predicates are guaranteed true.
334///
335/// Returns `None` for empty input because there is no time-filter implication
336/// that cache-key normalization or prefilter postponement can use. It also
337/// returns `None` if any expression contains an unsupported shape: `OR`, `NOT`,
338/// `IN`, non-literal RHS, unsupported operator, column-name mismatch, an `=`
339/// literal that cannot be represented exactly in the column unit, or overflow
340/// during bound adjustment.
341///
342/// This is intentionally stricter than `extract_time_range_from_expr` in
343/// `table::predicate`: lower bounds round up and upper bounds round down. If a
344/// partition's file-time range is contained by the returned range, every row in
345/// that partition satisfies the original time predicates.
346///
347/// `IsNull`/`IsNotNull` on the time index are not routed into `time_filters`
348/// today. If that changes, handle them here before stripping time filters from
349/// the cache key.
350pub(crate) fn implied_time_range_from_exprs(
351    ts_col_name: &str,
352    ts_col_unit: TimeUnit,
353    exprs: &[&Expr],
354) -> Option<TimestampRange> {
355    if exprs.is_empty() {
356        return None;
357    }
358
359    let mut acc = TimestampRange::min_to_max();
360    for expr in exprs {
361        let r = implied_time_range_from_expr(ts_col_name, ts_col_unit, expr)?;
362        acc = acc.and(&r);
363    }
364    Some(acc)
365}
366
367fn implied_time_range_from_expr(
368    ts_col_name: &str,
369    ts_col_unit: TimeUnit,
370    expr: &Expr,
371) -> Option<TimestampRange> {
372    match expr {
373        Expr::BinaryExpr(BinaryExpr { left, op, right }) => match op {
374            Operator::And => {
375                let l = implied_time_range_from_expr(ts_col_name, ts_col_unit, left)?;
376                let r = implied_time_range_from_expr(ts_col_name, ts_col_unit, right)?;
377                Some(l.and(&r))
378            }
379            Operator::Eq | Operator::Lt | Operator::LtEq | Operator::Gt | Operator::GtEq => {
380                implied_from_cmp(ts_col_name, ts_col_unit, left, *op, right)
381            }
382            // `OR` would require a strict intersection over a union of half-planes
383            // (not the loose-span union provided by `TimestampRange::or`), so we
384            // refuse it. Any other operator is unsupported.
385            _ => None,
386        },
387        Expr::Between(Between {
388            expr,
389            negated,
390            low,
391            high,
392        }) => {
393            if *negated {
394                return None;
395            }
396            implied_from_between(ts_col_name, ts_col_unit, expr, low, high)
397        }
398        // Includes `IsNull`, `IsNotNull`, `Not`, `InList`, function calls, etc.
399        _ => None,
400    }
401}
402
403fn match_ts_column_literal<'a>(
404    ts_col_name: &str,
405    left: &'a Expr,
406    right: &'a Expr,
407) -> Option<(Timestamp, bool)> {
408    let (col, scalar, reverse) = match (left, right) {
409        (Expr::Column(c), Expr::Literal(s, _)) => (c, s, false),
410        (Expr::Literal(s, _), Expr::Column(c)) => (c, s, true),
411        _ => return None,
412    };
413    if col.name != ts_col_name {
414        return None;
415    }
416    // Reject string literals: their conversion needs a timezone we do not have,
417    // and the existing extractor in `table::predicate` rejects them too.
418    if is_string_timestamp_literal(scalar) {
419        return None;
420    }
421    scalar_value_to_timestamp(scalar, None).map(|t| (t, reverse))
422}
423
424fn implied_from_cmp(
425    ts_col_name: &str,
426    ts_col_unit: TimeUnit,
427    left: &Expr,
428    op: Operator,
429    right: &Expr,
430) -> Option<TimestampRange> {
431    let (ts, reverse) = match_ts_column_literal(ts_col_name, left, right)?;
432    // Normalize to "column OP literal".
433    let op = if reverse {
434        match op {
435            Operator::Lt => Operator::Gt,
436            Operator::LtEq => Operator::GtEq,
437            Operator::Gt => Operator::Lt,
438            Operator::GtEq => Operator::LtEq,
439            Operator::Eq => Operator::Eq,
440            _ => return None,
441        }
442    } else {
443        op
444    };
445
446    match op {
447        Operator::GtEq => {
448            // ts >= L. Round the lower bound up in the column unit.
449            let b = ts.convert_to_ceil(ts_col_unit)?;
450            Some(TimestampRange::from_start(b))
451        }
452        Operator::Gt => {
453            // ts > L. floor(L) + 1 is the tight lower bound in the column unit.
454            let v = ts.convert_to(ts_col_unit)?.value().checked_add(1)?;
455            Some(TimestampRange::from_start(Timestamp::new(v, ts_col_unit)))
456        }
457        Operator::LtEq => {
458            // ts <= L. Round the upper bound down in the column unit.
459            let b = ts.convert_to(ts_col_unit)?;
460            Some(TimestampRange::until_end(b, true))
461        }
462        Operator::Lt => {
463            // ts < L. `ts < ceil(L)` is the tight bound: equal to `ts < L` when
464            // L is exactly representable, and `ts <= floor(L)` otherwise.
465            let b = ts.convert_to_ceil(ts_col_unit)?;
466            Some(TimestampRange::until_end(b, false))
467        }
468        Operator::Eq => {
469            // ts = L. Only provable when L is exactly representable.
470            let f = ts.convert_to(ts_col_unit)?;
471            let c = ts.convert_to_ceil(ts_col_unit)?;
472            if f.value() != c.value() {
473                return None;
474            }
475            Some(TimestampRange::single(f))
476        }
477        _ => None,
478    }
479}
480
481fn implied_from_between(
482    ts_col_name: &str,
483    ts_col_unit: TimeUnit,
484    expr: &Expr,
485    low: &Expr,
486    high: &Expr,
487) -> Option<TimestampRange> {
488    let Expr::Column(c) = expr else {
489        return None;
490    };
491    if c.name != ts_col_name {
492        return None;
493    }
494    let (low_s, high_s) = match (low, high) {
495        (Expr::Literal(l, _), Expr::Literal(h, _)) => (l, h),
496        _ => return None,
497    };
498    if is_string_timestamp_literal(low_s) || is_string_timestamp_literal(high_s) {
499        return None;
500    }
501    let low_ts = scalar_value_to_timestamp(low_s, None)?;
502    let high_ts = scalar_value_to_timestamp(high_s, None)?;
503    // BETWEEN low AND high is equivalent to ts >= low AND ts <= high.
504    let lo = low_ts.convert_to_ceil(ts_col_unit)?;
505    let hi = high_ts.convert_to(ts_col_unit)?;
506    Some(TimestampRange::new_inclusive(Some(lo), Some(hi)))
507}
508
509/// Builds a cache key for the given partition range if it is eligible for caching.
510pub(crate) fn build_range_cache_key(
511    stream_ctx: &StreamContext,
512    part_range: &PartitionRange,
513) -> Option<RangeScanCacheKey> {
514    build_range_cache_key_inner(stream_ctx, part_range, None)
515}
516
517/// Builds a cache key for a candidate-series partition-range result.
518pub(crate) fn build_candidate_range_cache_key(
519    stream_ctx: &StreamContext,
520    part_range: &PartitionRange,
521) -> Option<RangeScanCacheKey> {
522    build_range_cache_key_inner(
523        stream_ctx,
524        part_range,
525        Some(RangeScanStage::CandidateSeries),
526    )
527}
528
529/// Builds a cache key for a two-phase series-data partition-range result.
530pub(crate) fn build_series_range_cache_key(
531    stream_ctx: &StreamContext,
532    part_range: &PartitionRange,
533    range: SeriesRange,
534) -> Option<RangeScanCacheKey> {
535    build_range_cache_key_inner(
536        stream_ctx,
537        part_range,
538        Some(RangeScanStage::SeriesData(range)),
539    )
540}
541
542fn build_range_cache_key_inner(
543    stream_ctx: &StreamContext,
544    part_range: &PartitionRange,
545    stage: Option<RangeScanStage>,
546) -> Option<RangeScanCacheKey> {
547    if !stream_ctx.input.cache_strategy.has_range_result_cache() {
548        return None;
549    }
550
551    let fingerprint = stream_ctx.input.scan_fingerprint()?;
552
553    // Dyn filters can change at runtime, so we can't cache when they're present.
554    let has_dyn_filters = stream_ctx
555        .input
556        .predicate_group()
557        .predicate_without_region()
558        .is_some_and(|p| !p.dyn_filters().is_empty());
559    if has_dyn_filters {
560        return None;
561    }
562
563    let rg = collect_partition_range_row_groups(stream_ctx, part_range);
564    if !rg.only_file_sources || rg.row_groups.is_empty() {
565        return None;
566    }
567
568    // If the implied range covers this partition's `FileTimeRange`, drop
569    // time-only predicates from the cache key so that queries with different
570    // but equally-covering time bounds share an entry. `None` means there is no
571    // analyzable time-only predicate or some predicate had an unsupported shape
572    // (e.g. `OR`), so we keep the fingerprint unchanged. When `time_filters` is
573    // already empty, cloning the fingerprint is equivalent to stripping them.
574    let range_meta = &stream_ctx.ranges[part_range.identifier];
575    let (file_min, file_max) = range_meta.time_range;
576    let covers = match stream_ctx.input.implied_time_range() {
577        // An empty implied range can never cover a non-empty file range, so
578        // short-circuit.
579        Some(implied) if !implied.is_empty() => {
580            // `file_min`/`file_max` may use a different unit than the implied
581            // range: after the time index unit was widened, pre-alter files
582            // still carry the old unit while the implied range uses the
583            // current column unit. `Timestamp` comparisons normalize units,
584            // so `contains` compares instants and stays sound across units.
585            implied.contains(&file_min) && implied.contains(&file_max)
586        }
587        _ => false,
588    };
589    let scan = if covers {
590        fingerprint.without_time_filters()
591    } else {
592        fingerprint.clone()
593    };
594    let scan = match stage {
595        Some(RangeScanStage::CandidateSeries) => scan.for_candidate_series(),
596        Some(RangeScanStage::SeriesData(assignment)) => scan.for_series_data(assignment),
597        Some(RangeScanStage::DataForCrossRangeMerge) => {
598            let mut scan = scan;
599            scan.stage = RangeScanStage::DataForCrossRangeMerge;
600            scan
601        }
602        Some(RangeScanStage::Data) | None => scan,
603    };
604
605    Some(RangeScanCacheKey {
606        region_id: stream_ctx.input.region_metadata().region_id,
607        row_groups: rg.row_groups,
608        scan,
609    })
610}
611
612/// Returns a stream that replays cached record batches.
613pub(crate) fn cached_flat_range_stream(
614    delivery: Arc<RangeCacheDelivery>,
615) -> BoxedRecordBatchStream {
616    Box::pin(try_stream! {
617        // Borrow through the delivery guard so it survives every yield. Batches
618        // handed to downstream operators are then owned by the query, not by
619        // this completed-result backlog.
620        for cached_batch in &delivery.value().cached_batches {
621            let mut offset = 0;
622            for &len in &cached_batch.slice_lengths {
623                yield cached_batch.batch.slice(offset, len);
624                offset += len;
625            }
626        }
627    })
628}
629
630/// Builds only one decoded result per full cache key. A load must be driven
631/// independently of any caller: merge readers can wait for another stream with
632/// the same key before polling their first stream to completion.
633pub(crate) struct CachedRangeReader<'a> {
634    pub(crate) stream_ctx: &'a StreamContext,
635    pub(crate) part_range: &'a PartitionRange,
636    pub(crate) key: Option<RangeScanCacheKey>,
637    pub(crate) metrics: &'a PartitionMetrics,
638}
639
640impl CachedRangeReader<'_> {
641    pub(crate) async fn build<F, Fut>(self, build: F) -> Result<(BoxedRecordBatchStream, usize)>
642    where
643        F: FnOnce() -> Fut + Clone + Send + 'static,
644        Fut: Future<Output = Result<(BoxedRecordBatchStream, usize)>> + Send + 'static,
645    {
646        let Self {
647            stream_ctx,
648            part_range,
649            key,
650            metrics,
651        } = self;
652        let strategy = &stream_ctx.input.cache_strategy;
653        let (Some(key), Some(worker)) = (key, strategy.range_cache_worker()) else {
654            return build().await;
655        };
656        let files = stream_ctx.ranges[part_range.identifier]
657            .row_group_indices
658            .iter()
659            .map(|index| stream_ctx.input.file_from_index(*index).clone())
660            .collect();
661        let loader = build.clone();
662        let load_worker = worker.clone();
663        let load_strategy = strategy.clone();
664        let load_metrics = metrics.clone();
665        let key_size = key.estimated_size();
666        match worker.get_or_load(key, files, async move {
667            let result = async {
668                let (stream, _) = loader().await?;
669                collect_range_result(stream, &load_worker, &load_strategy).await
670            }
671            .await;
672            match result {
673                Ok(Some(value)) => {
674                    load_metrics.inc_range_cache_size(key_size + value.value().estimated_size());
675                    Some(value)
676                }
677                Ok(None) => None,
678                Err(error) => {
679                    warn!(error; "Failed to load range cache; falling back to an uncached scan");
680                    None
681                }
682            }
683        }) {
684            RangeCacheLookup::Hit(value) => {
685                metrics.inc_range_cache_hit();
686                Ok((cached_flat_range_stream(value), DEFAULT_READ_BATCH_SIZE))
687            }
688            RangeCacheLookup::Loading(receiver) => {
689                metrics.inc_range_cache_miss();
690                Ok((
691                    shared_range_stream(receiver, build),
692                    DEFAULT_READ_BATCH_SIZE,
693                ))
694            }
695            RangeCacheLookup::Bypass => {
696                metrics.inc_range_cache_miss();
697                build().await
698            }
699        }
700    }
701}
702
703/// Waiting is lazy, but the producer is independent of every consumer.
704fn shared_range_stream<F, Fut>(
705    mut receiver: tokio::sync::watch::Receiver<Option<Arc<RangeCacheDelivery>>>,
706    build: F,
707) -> BoxedRecordBatchStream
708where
709    F: FnOnce() -> Fut + Send + 'static,
710    Fut: Future<Output = Result<(BoxedRecordBatchStream, usize)>> + Send + 'static,
711{
712    Box::pin(try_stream! {
713        let cached = receiver.wait_for(|cached| cached.is_some()).await
714            .ok().and_then(|cached| cached.clone());
715        drop(receiver);
716        let mut stream = if let Some(cached) = cached {
717            cached_flat_range_stream(cached)
718        } else {
719            // A failed/oversized/cancelled fill is never a query result.
720            // Retry once through the ordinary streaming path, not the worker.
721            build().await?.0
722        };
723        while let Some(batch) = stream.try_next().await? {
724            yield batch;
725        }
726    })
727}
728
729/// The load budget covers retained input buffers and completed deliveries. Acquisition
730/// must be nonblocking: waiting with a partially accumulated result deadlocks
731/// when several loads consume all permits. Oversized ranges retain streaming
732/// query semantics by falling back to an uncached scan.
733async fn collect_range_result(
734    mut stream: BoxedRecordBatchStream,
735    worker: &RangeCacheWorker,
736    strategy: &CacheStrategy,
737) -> Result<Option<Arc<RangeCacheDelivery>>> {
738    let mut permit: Option<tokio::sync::OwnedSemaphorePermit> = None;
739    let mut buffer = CacheBatchBuffer::new(strategy);
740    while let Some(batch) = stream.try_next().await? {
741        let Some(next_permit) = worker
742            .load_memory
743            .try_acquire(batch.get_array_memory_size())
744        else {
745            return Ok(None);
746        };
747        if let Some(permit) = &mut permit {
748            permit.merge(next_permit);
749        } else {
750            permit = Some(next_permit);
751        }
752        buffer.push(batch)?;
753    }
754    let Some(value) = buffer.finish().await.transpose()? else {
755        return Ok(None);
756    };
757    let Some(permit) = worker
758        .load_memory
759        .try_resize(permit, value.estimated_size())
760    else {
761        return Ok(None);
762    };
763    Ok(Some(RangeCacheDelivery::new(value, permit)))
764}
765
766enum CacheConcatCommand {
767    Compact(Vec<RecordBatch>),
768    Finish {
769        pending: Vec<RecordBatch>,
770        result_tx: oneshot::Sender<Result<Arc<RangeScanCacheValue>>>,
771    },
772}
773
774#[derive(Default)]
775struct CacheConcatState {
776    cached_batches: Vec<CachedBatchSlice>,
777    estimated_size: usize,
778}
779
780impl CacheConcatState {
781    async fn compact(
782        &mut self,
783        batches: Vec<RecordBatch>,
784        limiter: &crate::cache::RangeResultMemoryLimiter,
785    ) -> Result<()> {
786        if batches.is_empty() {
787            return Ok(());
788        }
789
790        let input_size = batches
791            .iter()
792            .map(RecordBatch::get_array_memory_size)
793            .sum::<usize>();
794        let _permit = limiter.acquire(input_size).await?;
795
796        let compacted = compact_record_batches(batches)?;
797        self.estimated_size += compacted.batch.get_array_memory_size();
798        self.cached_batches.push(compacted);
799        Ok(())
800    }
801
802    fn finish(self) -> RangeScanCacheValue {
803        RangeScanCacheValue::new(self.cached_batches, self.estimated_size)
804    }
805}
806
807fn compact_record_batches(batches: Vec<RecordBatch>) -> Result<CachedBatchSlice> {
808    debug_assert!(!batches.is_empty());
809
810    let slice_lengths = batches.iter().map(RecordBatch::num_rows).collect();
811    build_cached_batch_slice(batches, slice_lengths)
812}
813
814fn build_cached_batch_slice(
815    batches: Vec<RecordBatch>,
816    slice_lengths: Vec<usize>,
817) -> Result<CachedBatchSlice> {
818    let batch = if batches.len() == 1 {
819        batches.into_iter().next().unwrap()
820    } else {
821        let schema = batches[0].schema();
822        concat_batches(&schema, &batches).context(ComputeArrowSnafu)?
823    };
824
825    Ok(CachedBatchSlice {
826        batch,
827        slice_lengths,
828    })
829}
830
831async fn run_cache_concat_task(
832    mut rx: mpsc::UnboundedReceiver<CacheConcatCommand>,
833    limiter: Arc<crate::cache::RangeResultMemoryLimiter>,
834    skip_threshold_bytes: usize,
835) {
836    let mut state = CacheConcatState::default();
837
838    while let Some(cmd) = rx.recv().await {
839        match cmd {
840            CacheConcatCommand::Compact(batches) => {
841                if let Err(err) = state.compact(batches, &limiter).await {
842                    warn!(err; "Failed to compact range cache batches");
843                    return;
844                }
845                // Close the channel to stop further work as soon as the cached
846                // size exceeds the configured cache budget.
847                if state.estimated_size > skip_threshold_bytes {
848                    return;
849                }
850            }
851            CacheConcatCommand::Finish { pending, result_tx } => {
852                let compact_result = state
853                    .compact(pending, &limiter)
854                    .await
855                    .map(|()| state.finish());
856                let result = match compact_result {
857                    Ok(v) => {
858                        let value = Arc::new(v);
859                        Ok(value)
860                    }
861                    Err(e) => {
862                        warn!(e; "Failed to finalize range cache batches");
863
864                        Err(e)
865                    }
866                };
867
868                let _ = result_tx.send(result);
869
870                break;
871            }
872        }
873    }
874}
875
876struct CacheBatchBuffer {
877    buffered_batches: Vec<RecordBatch>,
878    buffered_rows: usize,
879    buffered_size: usize,
880    sender: Option<mpsc::UnboundedSender<CacheConcatCommand>>,
881    /// Kept until concat finishes so abandoning the shared load cancels concat too.
882    concat_task: Option<common_runtime::JoinHandle<()>>,
883}
884
885impl CacheBatchBuffer {
886    fn new(cache_strategy: &CacheStrategy) -> Self {
887        let (sender, concat_task) = cache_strategy
888            .range_result_memory_limiter()
889            .map(|limiter| {
890                let skip_threshold_bytes = cache_strategy.range_result_cache_size().unwrap_or(0);
891                let (tx, rx) = mpsc::unbounded_channel();
892                let task = common_runtime::spawn_query(run_cache_concat_task(
893                    rx,
894                    limiter.clone(),
895                    skip_threshold_bytes,
896                ));
897                (tx, task)
898            })
899            .unzip();
900
901        Self {
902            buffered_batches: Vec::new(),
903            buffered_rows: 0,
904            buffered_size: 0,
905            sender,
906            concat_task,
907        }
908    }
909
910    fn push(&mut self, batch: RecordBatch) -> Result<()> {
911        if self.sender.is_none() {
912            return Ok(());
913        }
914
915        self.buffered_rows += batch.num_rows();
916        self.buffered_size += batch.get_array_memory_size();
917        self.buffered_batches.push(batch);
918
919        if self.buffered_batches.len() > 1
920            && (self.buffered_rows > DEFAULT_READ_BATCH_SIZE
921                || self.buffered_size > RANGE_CACHE_COMPACT_THRESHOLD_BYTES)
922        {
923            self.notify_compact();
924        }
925
926        Ok(())
927    }
928
929    fn notify_compact(&mut self) {
930        if self.buffered_batches.is_empty() || self.sender.is_none() {
931            return;
932        }
933
934        let batches = mem::take(&mut self.buffered_batches);
935        self.buffered_rows = 0;
936        self.buffered_size = 0;
937
938        let Some(sender) = &self.sender else {
939            return;
940        };
941        if sender.send(CacheConcatCommand::Compact(batches)).is_err() {
942            self.sender = None;
943        }
944    }
945
946    async fn finish(mut self) -> Option<Result<Arc<RangeScanCacheValue>>> {
947        let sender = self.sender.take()?;
948        let (result_tx, result_rx) = oneshot::channel();
949        sender
950            .send(CacheConcatCommand::Finish {
951                pending: mem::take(&mut self.buffered_batches),
952                result_tx,
953            })
954            .ok()?;
955        // Keep ownership until the result arrives so cancellation also aborts
956        // concat while it waits for its temporary memory permits.
957        let result = result_rx.await.ok();
958        self.concat_task = None;
959        result
960    }
961}
962
963impl Drop for CacheBatchBuffer {
964    fn drop(&mut self) {
965        // Aborting releases queued batches even while concat waits for a permit.
966        if let Some(task) = &self.concat_task {
967            task.abort();
968        }
969    }
970}
971
972/// Wraps a stream to cache its output for future range cache hits.
973#[cfg(feature = "test")]
974pub(crate) fn cache_flat_range_stream(
975    mut stream: BoxedRecordBatchStream,
976    cache_strategy: CacheStrategy,
977    key: RangeScanCacheKey,
978    part_metrics: PartitionMetrics,
979) -> BoxedRecordBatchStream {
980    Box::pin(try_stream! {
981        let mut buffer = CacheBatchBuffer::new(&cache_strategy);
982        while let Some(batch) = stream.try_next().await? {
983            buffer.push(batch.clone())?;
984            yield batch;
985        }
986
987        if let Some(Ok(value)) = buffer.finish().await {
988            part_metrics.inc_range_cache_size(key.estimated_size() + value.estimated_size());
989            cache_strategy.put_range_result(key, value);
990        }
991    })
992}
993
994/// Creates a `cache_flat_range_stream` with dummy internals for benchmarking.
995///
996/// This avoids exposing `RangeScanCacheKey`, `ScanRequestFingerprint`, and
997/// `PartitionMetrics` publicly.
998#[cfg(feature = "test")]
999pub fn bench_cache_flat_range_stream(
1000    stream: BoxedRecordBatchStream,
1001    cache_size_bytes: u64,
1002    region_id: RegionId,
1003) -> BoxedRecordBatchStream {
1004    use std::time::Instant;
1005
1006    use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
1007
1008    use crate::region::options::MergeMode;
1009
1010    let cache_manager = Arc::new(
1011        crate::cache::CacheManager::builder()
1012            .range_result_cache_size(cache_size_bytes)
1013            .build(),
1014    );
1015    let cache_strategy = CacheStrategy::EnableAll(cache_manager);
1016
1017    let fingerprint = ScanRequestFingerprintBuilder {
1018        read_columns: ReadColumns::new(std::iter::empty()),
1019        read_column_types: vec![],
1020        filters: vec![],
1021        time_filters: vec![],
1022        series_row_selector: None,
1023        append_mode: false,
1024        filter_deleted: false,
1025        merge_mode: MergeMode::LastRow,
1026        sequence_range: None,
1027        partition_expr_version: 0,
1028    }
1029    .build();
1030
1031    let key = RangeScanCacheKey {
1032        region_id,
1033        row_groups: vec![],
1034        scan: fingerprint,
1035    };
1036
1037    let metrics_set = ExecutionPlanMetricsSet::new();
1038    let part_metrics =
1039        PartitionMetrics::new(region_id, 0, "bench", Instant::now(), false, &metrics_set);
1040
1041    cache_flat_range_stream(stream, cache_strategy, key, part_metrics)
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046    use std::sync::Arc;
1047    use std::time::Instant;
1048
1049    use common_time::Timestamp;
1050    use common_time::range::TimestampRange;
1051    use common_time::timestamp::TimeUnit;
1052    use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
1053    use datafusion_common::ScalarValue;
1054    use datafusion_expr::{Expr, col, lit};
1055    use smallvec::smallvec;
1056    use store_api::storage::{FileId, RegionId};
1057    use tokio::sync::watch;
1058
1059    use super::*;
1060    use crate::cache::CacheManager;
1061    use crate::cache::test_util::range_cache_key;
1062    use crate::read::flat_projection::FlatProjectionMapper;
1063    use crate::read::range::{RangeMeta, RowGroupIndex, SourceIndex};
1064    use crate::read::scan_region::{PredicateGroup, ScanInput};
1065    use crate::sst::file::FileTimeRange;
1066    use crate::test_util::memtable_util::metadata_with_primary_key;
1067    use crate::test_util::scheduler_util::SchedulerEnv;
1068    use crate::test_util::sst_util::sst_file_handle_with_file_id;
1069
1070    fn test_cache_strategy() -> CacheStrategy {
1071        CacheStrategy::EnableAll(Arc::new(
1072            CacheManager::builder()
1073                .range_result_cache_size(1024 * 1024)
1074                .build(),
1075        ))
1076    }
1077
1078    fn test_scan_fingerprint(
1079        filters: Vec<String>,
1080        time_filters: Vec<String>,
1081        series_row_selector: Option<TimeSeriesRowSelector>,
1082        filter_deleted: bool,
1083        partition_expr_version: u64,
1084    ) -> ScanRequestFingerprint {
1085        let read_columns = ReadColumns::new([1, 2]);
1086        ScanRequestFingerprintBuilder {
1087            read_columns,
1088            read_column_types: vec![None, None],
1089            filters,
1090            time_filters,
1091            series_row_selector,
1092            append_mode: false,
1093            filter_deleted,
1094            merge_mode: MergeMode::LastRow,
1095            sequence_range: None,
1096            partition_expr_version,
1097        }
1098        .build()
1099    }
1100
1101    fn test_cache_context(strategy: &CacheStrategy) -> (RangeScanCacheKey, PartitionMetrics) {
1102        let region_id = RegionId::new(1, 1);
1103        let key = RangeScanCacheKey {
1104            region_id,
1105            row_groups: vec![],
1106            scan: test_scan_fingerprint(vec![], vec![], None, false, 0),
1107        };
1108
1109        let metrics_set = ExecutionPlanMetricsSet::new();
1110        let part_metrics =
1111            PartitionMetrics::new(region_id, 0, "test", Instant::now(), false, &metrics_set);
1112
1113        assert!(strategy.get_range_result(&key).is_none());
1114        (key, part_metrics)
1115    }
1116
1117    async fn new_stream_context(
1118        filters: Vec<Expr>,
1119        query_time_range: Option<TimestampRange>,
1120        partition_time_range: FileTimeRange,
1121    ) -> (StreamContext, PartitionRange) {
1122        let env = SchedulerEnv::new().await;
1123        let metadata = Arc::new(metadata_with_primary_key(vec![0, 1], false));
1124        let mapper = FlatProjectionMapper::new(&metadata, [0, 2, 3]).unwrap();
1125        let predicate = PredicateGroup::new(metadata.as_ref(), &filters).unwrap();
1126        let file_id = FileId::random();
1127        let file = sst_file_handle_with_file_id(
1128            file_id,
1129            partition_time_range.0.value(),
1130            partition_time_range.1.value(),
1131        );
1132        let input = ScanInput::builder(env.access_layer.clone(), mapper)
1133            .with_predicate(predicate)
1134            .with_time_range(query_time_range)
1135            .with_files(vec![file])
1136            .with_cache(test_cache_strategy())
1137            .build();
1138        let range_meta = RangeMeta {
1139            time_range: partition_time_range,
1140            indices: smallvec![SourceIndex {
1141                index: 0,
1142                num_row_groups: 1,
1143            }],
1144            row_group_indices: smallvec![RowGroupIndex {
1145                index: 0,
1146                row_group_index: 0,
1147            }],
1148            num_rows: 10,
1149        };
1150        let partition_range = range_meta.new_partition_range(0);
1151        let stream_ctx = StreamContext {
1152            input,
1153            ranges: vec![range_meta],
1154            query_start: Instant::now(),
1155        };
1156
1157        (stream_ctx, partition_range)
1158    }
1159
1160    #[rstest::rstest]
1161    #[case(1024 * 1024, true)]
1162    #[case(1, false)]
1163    #[tokio::test]
1164    async fn shared_range_read_preserves_batches_or_falls_back_on_budget_exhaustion(
1165        #[case] capacity: u64,
1166        #[case] cached: bool,
1167    ) {
1168        use std::sync::atomic::{AtomicUsize, Ordering};
1169
1170        let (mut ctx, range) =
1171            new_stream_context(vec![col("k0").eq(lit("a"))], None, FileTimeRange::default()).await;
1172        ctx.input.cache_strategy = CacheStrategy::EnableAll(Arc::new(
1173            CacheManager::builder()
1174                .range_result_cache_size(capacity)
1175                .build(),
1176        ));
1177        let key = build_range_cache_key(&ctx, &range).unwrap();
1178        let (_, metrics) = test_cache_context(&ctx.input.cache_strategy);
1179        let calls = Arc::new(AtomicUsize::new(0));
1180        let expected = vec![make_batch(&[1, 2]), make_batch(&[3, 4, 5])];
1181        let batches = expected.clone();
1182        let count = calls.clone();
1183        let (stream, _) = CachedRangeReader {
1184            stream_ctx: &ctx,
1185            part_range: &range,
1186            key: Some(key.clone()),
1187            metrics: &metrics,
1188        }
1189        .build(move || async move {
1190            count.fetch_add(1, Ordering::Relaxed);
1191            Ok((
1192                Box::pin(futures::stream::iter(batches.into_iter().map(Ok)))
1193                    as BoxedRecordBatchStream,
1194                DEFAULT_READ_BATCH_SIZE,
1195            ))
1196        })
1197        .await
1198        .unwrap();
1199        let actual: Vec<_> = stream.try_collect().await.unwrap();
1200        assert_eq!(actual, expected);
1201        assert_eq!(calls.load(Ordering::Relaxed), if cached { 1 } else { 2 });
1202        assert_eq!(
1203            ctx.input.cache_strategy.get_range_result(&key).is_some(),
1204            cached
1205        );
1206        assert_eq!(
1207            ctx.input
1208                .cache_strategy
1209                .range_cache_worker()
1210                .unwrap()
1211                .load_memory
1212                .available_permits(),
1213            (capacity as usize).div_ceil(1024)
1214        );
1215    }
1216
1217    fn start_shared_load(
1218        strategy: &CacheStrategy,
1219        key: RangeScanCacheKey,
1220        batches: Vec<RecordBatch>,
1221    ) -> watch::Receiver<Option<Arc<RangeCacheDelivery>>> {
1222        let worker = strategy.range_cache_worker().unwrap();
1223        let loader_worker = worker.clone();
1224        let loader_strategy = strategy.clone();
1225        let lookup = worker.get_or_load(key, vec![], async move {
1226            collect_range_result(
1227                Box::pin(futures::stream::iter(batches.into_iter().map(Ok))),
1228                &loader_worker,
1229                &loader_strategy,
1230            )
1231            .await
1232            .unwrap()
1233        });
1234        let RangeCacheLookup::Loading(receiver) = lookup else {
1235            panic!("expected a shared load");
1236        };
1237        receiver
1238    }
1239
1240    async fn wait_for_shared_load(
1241        receiver: &mut watch::Receiver<Option<Arc<RangeCacheDelivery>>>,
1242    ) -> Option<Arc<RangeCacheDelivery>> {
1243        tokio::time::timeout(std::time::Duration::from_secs(5), async {
1244            receiver
1245                .wait_for(|value| value.is_some())
1246                .await
1247                .ok()
1248                .and_then(|value| value.clone())
1249        })
1250        .await
1251        .expect("a shared load must finish or fall back without waiting for consumers")
1252    }
1253
1254    #[tokio::test]
1255    async fn completed_load_keeps_memory_until_all_subscribers_release() {
1256        let strategy = test_cache_strategy();
1257        let worker = strategy.range_cache_worker().unwrap();
1258        let capacity = worker.load_memory.available_permits();
1259        let (key, _) = test_cache_context(&strategy);
1260        let mut first = start_shared_load(&strategy, key.clone(), vec![make_batch(&[1, 2, 3])]);
1261        let second = first.clone();
1262        drop(wait_for_shared_load(&mut first).await.unwrap());
1263
1264        // A completed flight no longer counts toward the flight limit, but
1265        // unpolled subscribers must still reserve its decoded result memory.
1266        assert!(worker.load_memory.available_permits() < capacity);
1267        drop(first);
1268        assert!(worker.load_memory.available_permits() < capacity);
1269        drop(second);
1270        tokio::time::timeout(std::time::Duration::from_secs(5), async {
1271            while worker.load_memory.available_permits() != capacity {
1272                tokio::task::yield_now().await;
1273            }
1274        })
1275        .await
1276        .expect("the last subscriber must release the delivery reservation");
1277        assert!(
1278            worker.get(&key).is_some(),
1279            "the cache still owns the result"
1280        );
1281    }
1282
1283    #[rstest::rstest]
1284    #[tokio::test]
1285    async fn successive_loads_with_slow_consumers_exhaust_delivery_budget(
1286        #[values(false, true)] invalidate: bool,
1287    ) {
1288        let strategy = CacheStrategy::EnableAll(Arc::new(
1289            CacheManager::builder()
1290                .range_result_cache_size(8192)
1291                .build(),
1292        ));
1293        let worker = strategy.range_cache_worker().unwrap();
1294        let capacity = worker.load_memory.available_permits();
1295        let mut held_streams = Vec::new();
1296        let mut fallbacks = 0;
1297
1298        // Each flight finishes before the next starts, exceeding the 32-flight
1299        // limit cumulatively. Only the delivery budget can bound this backlog.
1300        for _ in 0..40 {
1301            let file = FileId::random();
1302            let key = range_cache_key(&[file]);
1303            let expected = vec![make_large_binary_batch(1, 2048)];
1304            let mut receiver = start_shared_load(&strategy, key, expected.clone());
1305            let shared = wait_for_shared_load(&mut receiver).await.is_some();
1306            if invalidate {
1307                worker.invalidate_file(file);
1308            }
1309            let fallback_batches = expected.clone();
1310            let stream = shared_range_stream(receiver, move || async move {
1311                Ok((
1312                    Box::pin(futures::stream::iter(fallback_batches.into_iter().map(Ok)))
1313                        as BoxedRecordBatchStream,
1314                    DEFAULT_READ_BATCH_SIZE,
1315                ))
1316            });
1317            if shared {
1318                held_streams.push(stream);
1319            } else {
1320                fallbacks += 1;
1321                let actual: Vec<_> =
1322                    tokio::time::timeout(std::time::Duration::from_secs(5), stream.try_collect())
1323                        .await
1324                        .expect("fallback must not wait for an unpolled range")
1325                        .unwrap();
1326                assert_eq!(actual, expected);
1327            }
1328        }
1329
1330        assert!(!held_streams.is_empty());
1331        assert!(held_streams.len() < 32);
1332        assert!(
1333            fallbacks > 0,
1334            "completed results must not escape the budget"
1335        );
1336        assert!(worker.load_memory.available_permits() < capacity);
1337
1338        // Transfer ownership out of watch receivers into actively polled replay
1339        // streams, then leave them suspended at their first yield.
1340        for stream in &mut held_streams {
1341            assert!(stream.try_next().await.unwrap().is_some());
1342        }
1343        assert!(worker.load_memory.available_permits() < capacity);
1344        drop(held_streams);
1345        tokio::time::timeout(std::time::Duration::from_secs(5), async {
1346            while worker.load_memory.available_permits() != capacity {
1347                tokio::task::yield_now().await;
1348            }
1349        })
1350        .await
1351        .expect("dropping slow readers must unblock new shared loads");
1352
1353        let mut retry = start_shared_load(
1354            &strategy,
1355            range_cache_key(&[FileId::random()]),
1356            vec![make_large_binary_batch(1, 2048)],
1357        );
1358        assert!(wait_for_shared_load(&mut retry).await.is_some());
1359    }
1360
1361    #[tokio::test]
1362    async fn cancelling_a_partial_load_releases_its_reservation() {
1363        let strategy = test_cache_strategy();
1364        let worker = strategy.range_cache_worker().unwrap().clone();
1365        let capacity = worker.load_memory.available_permits();
1366        let (key, _) = test_cache_context(&strategy);
1367        let loader_worker = worker.clone();
1368        let lookup = worker.get_or_load(key.clone(), vec![], async move {
1369            let stream = Box::pin(try_stream! {
1370                yield make_batch(&[1, 2, 3]);
1371                std::future::pending::<()>().await;
1372            });
1373            collect_range_result(stream, &loader_worker, &strategy)
1374                .await
1375                .unwrap()
1376        });
1377        let RangeCacheLookup::Loading(receiver) = lookup else {
1378            panic!("expected a shared load");
1379        };
1380        tokio::time::timeout(std::time::Duration::from_secs(5), async {
1381            while worker.load_memory.available_permits() == capacity {
1382                tokio::task::yield_now().await;
1383            }
1384        })
1385        .await
1386        .expect("the partial result must acquire its input reservation");
1387        drop(receiver);
1388        tokio::time::timeout(std::time::Duration::from_secs(5), async {
1389            while worker.load_memory.available_permits() != capacity {
1390                tokio::task::yield_now().await;
1391            }
1392        })
1393        .await
1394        .expect("cancellation must release the partial result reservation");
1395        assert!(worker.get(&key).is_none());
1396    }
1397
1398    #[tokio::test]
1399    async fn follower_can_finish_without_polling_the_first_range_stream() {
1400        use std::sync::atomic::{AtomicUsize, Ordering};
1401
1402        let (ctx, range) =
1403            new_stream_context(vec![col("k0").eq(lit("a"))], None, FileTimeRange::default()).await;
1404        let key = build_range_cache_key(&ctx, &range).unwrap();
1405        let (_, metrics) = test_cache_context(&ctx.input.cache_strategy);
1406        let gate = Arc::new(tokio::sync::Semaphore::new(0));
1407        let calls = Arc::new(AtomicUsize::new(0));
1408        let count = calls.clone();
1409        let loader_gate = gate.clone();
1410        let build = move || async move {
1411            count.fetch_add(1, Ordering::Relaxed);
1412            let _permit = loader_gate.acquire().await.unwrap();
1413            Ok((
1414                Box::pin(futures::stream::iter([Ok(make_batch(&[1, 2, 3]))]))
1415                    as BoxedRecordBatchStream,
1416                DEFAULT_READ_BATCH_SIZE,
1417            ))
1418        };
1419        let (first, _) = CachedRangeReader {
1420            stream_ctx: &ctx,
1421            part_range: &range,
1422            key: Some(key.clone()),
1423            metrics: &metrics,
1424        }
1425        .build(build.clone())
1426        .await
1427        .unwrap();
1428        let (second, _) = CachedRangeReader {
1429            stream_ctx: &ctx,
1430            part_range: &range,
1431            key: Some(key),
1432            metrics: &metrics,
1433        }
1434        .build(build)
1435        .await
1436        .unwrap();
1437        gate.add_permits(1);
1438        let second_batches: Vec<_> =
1439            tokio::time::timeout(std::time::Duration::from_secs(5), second.try_collect())
1440                .await
1441                .expect("a follower must not depend on polling the first query")
1442                .unwrap();
1443        let first_batches: Vec<_> = first.try_collect().await.unwrap();
1444        assert_eq!(first_batches, second_batches);
1445        assert_eq!(second_batches, vec![make_batch(&[1, 2, 3])]);
1446        assert_eq!(calls.load(Ordering::Relaxed), 1);
1447    }
1448
1449    /// Helper to create a timestamp millisecond literal.
1450    fn ts_lit(val: i64) -> Expr {
1451        lit(ScalarValue::TimestampMillisecond(Some(val), None))
1452    }
1453
1454    fn normalized_exprs(exprs: impl IntoIterator<Item = Expr>) -> Vec<String> {
1455        let mut exprs = exprs
1456            .into_iter()
1457            .map(|expr| expr.to_string())
1458            .collect::<Vec<_>>();
1459        exprs.sort_unstable();
1460        exprs
1461    }
1462
1463    async fn assert_range_cache_filters(
1464        filters: Vec<Expr>,
1465        query_time_range: Option<TimestampRange>,
1466        partition_time_range: FileTimeRange,
1467        expected_filters: Vec<Expr>,
1468        expected_time_filters: Vec<Expr>,
1469    ) {
1470        let (stream_ctx, part_range) =
1471            new_stream_context(filters, query_time_range, partition_time_range).await;
1472
1473        let key = build_range_cache_key(&stream_ctx, &part_range).unwrap();
1474
1475        assert_eq!(
1476            key.scan.filters(),
1477            normalized_exprs(expected_filters).as_slice()
1478        );
1479        assert_eq!(
1480            key.scan.time_filters(),
1481            normalized_exprs(expected_time_filters).as_slice()
1482        );
1483    }
1484
1485    #[tokio::test]
1486    async fn range_cache_time_filter_key_cases() {
1487        let partition = (
1488            Timestamp::new_millisecond(1000),
1489            Timestamp::new_millisecond(2000),
1490        );
1491
1492        struct Case {
1493            filters: Vec<Expr>,
1494            query_time_range: Option<TimestampRange>,
1495            expected_filters: Vec<Expr>,
1496            expected_time_filters: Vec<Expr>,
1497        }
1498
1499        // Time filters are stripped only when their implied range fully covers
1500        // the partition's file-time range. `is_not_null(ts)` stays in regular
1501        // filters because it is not routed into `time_filters`.
1502        for case in [
1503            Case {
1504                filters: vec![
1505                    col("ts").gt_eq(ts_lit(1000)),
1506                    col("ts").lt(ts_lit(2001)),
1507                    col("ts").is_not_null(),
1508                    col("k0").eq(lit("foo")),
1509                ],
1510                query_time_range: TimestampRange::with_unit(1000, 2002, TimeUnit::Millisecond),
1511                expected_filters: vec![col("k0").eq(lit("foo")), col("ts").is_not_null()],
1512                expected_time_filters: vec![],
1513            },
1514            Case {
1515                filters: vec![
1516                    col("ts").gt_eq(ts_lit(500)),
1517                    col("ts").lt(ts_lit(3000)),
1518                    col("k0").eq(lit("foo")),
1519                ],
1520                query_time_range: TimestampRange::with_unit(500, 3000, TimeUnit::Millisecond),
1521                expected_filters: vec![col("k0").eq(lit("foo"))],
1522                expected_time_filters: vec![],
1523            },
1524            Case {
1525                filters: vec![
1526                    col("ts").gt_eq(ts_lit(1000)),
1527                    col("ts").lt_eq(ts_lit(2000)),
1528                    col("k0").eq(lit("foo")),
1529                ],
1530                query_time_range: TimestampRange::with_unit(1000, 2001, TimeUnit::Millisecond),
1531                expected_filters: vec![col("k0").eq(lit("foo"))],
1532                expected_time_filters: vec![],
1533            },
1534            Case {
1535                filters: vec![
1536                    col("ts").between(ts_lit(1000), ts_lit(2000)),
1537                    col("k0").eq(lit("foo")),
1538                ],
1539                query_time_range: TimestampRange::with_unit(1000, 2001, TimeUnit::Millisecond),
1540                expected_filters: vec![col("k0").eq(lit("foo"))],
1541                expected_time_filters: vec![],
1542            },
1543            Case {
1544                filters: vec![col("ts").gt_eq(ts_lit(1200)), col("k0").eq(lit("foo"))],
1545                query_time_range: TimestampRange::with_unit(1200, 2001, TimeUnit::Millisecond),
1546                expected_filters: vec![col("k0").eq(lit("foo"))],
1547                expected_time_filters: vec![col("ts").gt_eq(ts_lit(1200))],
1548            },
1549            Case {
1550                filters: vec![
1551                    col("ts").gt_eq(ts_lit(1500)),
1552                    col("ts").is_not_null(),
1553                    col("k0").eq(lit("foo")),
1554                ],
1555                query_time_range: None,
1556                expected_filters: vec![col("k0").eq(lit("foo")), col("ts").is_not_null()],
1557                expected_time_filters: vec![col("ts").gt_eq(ts_lit(1500))],
1558            },
1559        ] {
1560            assert_range_cache_filters(
1561                case.filters,
1562                case.query_time_range,
1563                partition,
1564                case.expected_filters,
1565                case.expected_time_filters,
1566            )
1567            .await;
1568        }
1569    }
1570
1571    #[tokio::test]
1572    async fn two_distinct_queries_share_cache_key_when_both_cover() {
1573        let partition_range = (
1574            Timestamp::new_millisecond(1000),
1575            Timestamp::new_millisecond(2000),
1576        );
1577
1578        let (ctx_a, part_a) = new_stream_context(
1579            vec![
1580                col("ts").gt_eq(ts_lit(500)),
1581                col("ts").lt(ts_lit(3000)),
1582                col("k0").eq(lit("foo")),
1583            ],
1584            TimestampRange::with_unit(500, 3000, TimeUnit::Millisecond),
1585            partition_range,
1586        )
1587        .await;
1588        let (ctx_b, part_b) = new_stream_context(
1589            vec![
1590                col("ts").gt_eq(ts_lit(100)),
1591                col("ts").lt(ts_lit(5000)),
1592                col("k0").eq(lit("foo")),
1593            ],
1594            TimestampRange::with_unit(100, 5000, TimeUnit::Millisecond),
1595            partition_range,
1596        )
1597        .await;
1598
1599        let key_a = build_range_cache_key(&ctx_a, &part_a).unwrap();
1600        let key_b = build_range_cache_key(&ctx_b, &part_b).unwrap();
1601        assert_eq!(key_a.scan, key_b.scan);
1602        assert!(key_a.scan.time_filters().is_empty());
1603    }
1604
1605    #[tokio::test]
1606    async fn scan_stages_have_separate_cache_keys() {
1607        let partition_range = (
1608            Timestamp::new_millisecond(1000),
1609            Timestamp::new_millisecond(2000),
1610        );
1611        let (ctx, part_range) =
1612            new_stream_context(vec![col("k0").eq(lit("foo"))], None, partition_range).await;
1613
1614        let data_key = build_range_cache_key(&ctx, &part_range).unwrap();
1615        let candidate_key = build_candidate_range_cache_key(&ctx, &part_range).unwrap();
1616        let range_0 = SeriesRange::new(0, 2).unwrap();
1617        let range_1 = SeriesRange::new(1, 2).unwrap();
1618        let series_key_0 = build_series_range_cache_key(&ctx, &part_range, range_0).unwrap();
1619        let series_key_1 = build_series_range_cache_key(&ctx, &part_range, range_1).unwrap();
1620
1621        let merge_key = data_key.clone().for_cross_range_merge();
1622        assert_ne!(merge_key, data_key);
1623        assert_ne!(merge_key.scan, candidate_key.scan);
1624        assert_ne!(merge_key.scan, series_key_0.scan);
1625        assert_eq!(merge_key.row_groups, data_key.row_groups);
1626        assert_eq!(merge_key, data_key.clone().for_cross_range_merge());
1627        assert_ne!(data_key.scan, candidate_key.scan);
1628        assert_ne!(data_key.scan, series_key_0.scan);
1629        assert_ne!(candidate_key.scan, series_key_0.scan);
1630        assert_ne!(series_key_0.scan, series_key_1.scan);
1631        assert_eq!(data_key.row_groups, candidate_key.row_groups);
1632        assert_eq!(data_key.row_groups, series_key_0.row_groups);
1633    }
1634
1635    #[tokio::test]
1636    async fn disables_optimization_on_or_clause() {
1637        let partition_range = (
1638            Timestamp::new_millisecond(1000),
1639            Timestamp::new_millisecond(2000),
1640        );
1641
1642        let or_a = col("ts").gt_eq(ts_lit(1000)).or(col("ts").lt(ts_lit(500)));
1643        let or_b = col("ts").gt_eq(ts_lit(900)).or(col("ts").lt(ts_lit(400)));
1644
1645        let (ctx_a, part_a) = new_stream_context(
1646            vec![or_a.clone(), col("k0").eq(lit("foo"))],
1647            None,
1648            partition_range,
1649        )
1650        .await;
1651        let (ctx_b, part_b) = new_stream_context(
1652            vec![or_b.clone(), col("k0").eq(lit("foo"))],
1653            None,
1654            partition_range,
1655        )
1656        .await;
1657
1658        assert!(ctx_a.input.implied_time_range().is_none());
1659        let key_a = build_range_cache_key(&ctx_a, &part_a).unwrap();
1660        let key_b = build_range_cache_key(&ctx_b, &part_b).unwrap();
1661        assert_ne!(key_a.scan, key_b.scan);
1662        assert_eq!(
1663            key_a.scan.time_filters(),
1664            normalized_exprs([or_a]).as_slice()
1665        );
1666    }
1667
1668    #[tokio::test]
1669    async fn empty_implied_range_does_not_panic_on_non_ms_file_range() {
1670        // Contradictory time predicates make the implied range empty. The
1671        // empty range's sentinel timestamps use `Timestamp::default()` (ms),
1672        // so without the `is_empty()` short-circuit the unit asserts would
1673        // panic against a non-ms `range_meta.time_range`.
1674        let partition = (
1675            Timestamp::new_millisecond(1000),
1676            Timestamp::new_millisecond(2000),
1677        );
1678
1679        let (mut ctx, part_range) = new_stream_context(
1680            vec![
1681                col("ts").gt_eq(ts_lit(1500)),
1682                col("ts").lt(ts_lit(1500)),
1683                col("k0").eq(lit("foo")),
1684            ],
1685            TimestampRange::with_unit(1500, 3000, TimeUnit::Millisecond),
1686            partition,
1687        )
1688        .await;
1689
1690        assert!(ctx.input.implied_time_range().unwrap().is_empty());
1691        ctx.ranges[0].time_range = (
1692            Timestamp::new(1_000_000_000, TimeUnit::Nanosecond),
1693            Timestamp::new(2_000_000_000, TimeUnit::Nanosecond),
1694        );
1695
1696        let key = build_range_cache_key(&ctx, &part_range).unwrap();
1697        // Empty implied range cannot cover, so time filters stay in the key.
1698        assert!(!key.scan.time_filters().is_empty());
1699    }
1700
1701    #[tokio::test]
1702    async fn mixed_unit_file_range_is_compared_by_instant() {
1703        // After the time index unit was widened, pre-alter files keep the old
1704        // unit while `scan_implied_time_range` uses the current column unit.
1705        // The coverage decision must compare instants across units instead of
1706        // asserting unit equality (which panicked here for widened regions).
1707        let partition = (
1708            Timestamp::new_millisecond(1000),
1709            Timestamp::new_millisecond(2000),
1710        );
1711
1712        // Covering: the implied range [0, 3000)ms contains the file range
1713        // [1s, 2s] expressed in microseconds; time filters are dropped from
1714        // the key.
1715        let (mut ctx, part_range) = new_stream_context(
1716            vec![
1717                col("ts").gt_eq(ts_lit(0)),
1718                col("ts").lt(ts_lit(3000)),
1719                col("k0").eq(lit("foo")),
1720            ],
1721            TimestampRange::with_unit(0, 3000, TimeUnit::Millisecond),
1722            partition,
1723        )
1724        .await;
1725        ctx.ranges[0].time_range = (
1726            Timestamp::new_microsecond(1_000_000),
1727            Timestamp::new_microsecond(2_000_000),
1728        );
1729        let key = build_range_cache_key(&ctx, &part_range).unwrap();
1730        assert!(key.scan.time_filters().is_empty());
1731
1732        // Not covering: the implied range [1500, 3000)ms excludes the file's
1733        // 1s lower bound; time filters stay in the key.
1734        let (mut ctx, part_range) = new_stream_context(
1735            vec![
1736                col("ts").gt_eq(ts_lit(1500)),
1737                col("ts").lt(ts_lit(3000)),
1738                col("k0").eq(lit("foo")),
1739            ],
1740            TimestampRange::with_unit(1500, 3000, TimeUnit::Millisecond),
1741            partition,
1742        )
1743        .await;
1744        ctx.ranges[0].time_range = (
1745            Timestamp::new_microsecond(1_000_000),
1746            Timestamp::new_microsecond(2_000_000),
1747        );
1748        let key = build_range_cache_key(&ctx, &part_range).unwrap();
1749        assert!(!key.scan.time_filters().is_empty());
1750    }
1751
1752    fn ms_ts(v: i64) -> Timestamp {
1753        Timestamp::new_millisecond(v)
1754    }
1755
1756    fn implied_ms(expr: Expr) -> Option<TimestampRange> {
1757        implied_time_range_from_exprs("ts", TimeUnit::Millisecond, &[&expr])
1758    }
1759
1760    #[test]
1761    fn implied_time_range_supported_exprs() {
1762        for (expr, expected) in [
1763            (
1764                col("ts").gt_eq(ts_lit(1000)),
1765                Some(TimestampRange::from_start(ms_ts(1000))),
1766            ),
1767            (
1768                col("ts").gt(ts_lit(1000)),
1769                Some(TimestampRange::from_start(ms_ts(1001))),
1770            ),
1771            (
1772                col("ts").lt_eq(ts_lit(2000)),
1773                Some(TimestampRange::until_end(ms_ts(2000), true)),
1774            ),
1775            (
1776                col("ts").lt(ts_lit(2000)),
1777                Some(TimestampRange::until_end(ms_ts(2000), false)),
1778            ),
1779            (
1780                col("ts").eq(ts_lit(1500)),
1781                Some(TimestampRange::single(ms_ts(1500))),
1782            ),
1783            (
1784                ts_lit(1000).lt_eq(col("ts")),
1785                Some(TimestampRange::from_start(ms_ts(1000))),
1786            ),
1787            (
1788                col("ts").between(ts_lit(1000), ts_lit(2000)),
1789                Some(TimestampRange::new_inclusive(
1790                    Some(ms_ts(1000)),
1791                    Some(ms_ts(2000)),
1792                )),
1793            ),
1794            (
1795                col("ts")
1796                    .gt_eq(ts_lit(1000))
1797                    .and(col("ts").lt(ts_lit(2000))),
1798                TimestampRange::with_unit(1000, 2000, TimeUnit::Millisecond),
1799            ),
1800            (
1801                col("ts")
1802                    .gt_eq(ts_lit(1000))
1803                    .and(col("ts").lt(ts_lit(5000)))
1804                    .and(col("ts").lt_eq(ts_lit(3000))),
1805                TimestampRange::with_unit(1000, 3001, TimeUnit::Millisecond),
1806            ),
1807        ] {
1808            assert_eq!(implied_ms(expr), expected);
1809        }
1810
1811        assert_eq!(
1812            implied_time_range_from_exprs("ts", TimeUnit::Millisecond, &[]),
1813            None
1814        );
1815    }
1816
1817    #[test]
1818    fn implied_time_range_unsupported_exprs() {
1819        let not_between = Expr::Between(Between {
1820            expr: Box::new(col("ts")),
1821            negated: true,
1822            low: Box::new(ts_lit(1000)),
1823            high: Box::new(ts_lit(2000)),
1824        });
1825
1826        for expr in [
1827            not_between,
1828            col("ts").gt_eq(ts_lit(1000)).or(col("ts").lt(ts_lit(500))),
1829            Expr::Not(Box::new(col("ts").gt_eq(ts_lit(1000)))),
1830            col("ts").in_list(vec![ts_lit(1000), ts_lit(2000)], false),
1831            col("ts").gt_eq(col("other")),
1832            col("other_ts").gt_eq(ts_lit(1000)),
1833        ] {
1834            assert!(implied_ms(expr).is_none());
1835        }
1836    }
1837
1838    #[test]
1839    fn implied_time_range_unit_conversion() {
1840        let second_1 = lit(ScalarValue::TimestampSecond(Some(1), None));
1841        let ns_1500 = lit(ScalarValue::TimestampNanosecond(Some(1_500_000_000), None));
1842        let ns_1500_5 = lit(ScalarValue::TimestampNanosecond(Some(1_500_500_000), None));
1843
1844        for (expr, expected) in [
1845            (
1846                col("ts").gt_eq(second_1.clone()),
1847                Some(TimestampRange::from_start(ms_ts(1000))),
1848            ),
1849            (
1850                col("ts").lt_eq(second_1),
1851                Some(TimestampRange::until_end(ms_ts(1000), true)),
1852            ),
1853            (
1854                col("ts").eq(ns_1500),
1855                Some(TimestampRange::single(ms_ts(1500))),
1856            ),
1857            (col("ts").eq(ns_1500_5.clone()), None),
1858            (
1859                col("ts").gt_eq(ns_1500_5.clone()),
1860                Some(TimestampRange::from_start(ms_ts(1501))),
1861            ),
1862            (
1863                col("ts").lt_eq(ns_1500_5.clone()),
1864                Some(TimestampRange::until_end(ms_ts(1500), true)),
1865            ),
1866            (
1867                col("ts").gt(ns_1500_5.clone()),
1868                Some(TimestampRange::from_start(ms_ts(1501))),
1869            ),
1870            (
1871                col("ts").lt(ns_1500_5),
1872                Some(TimestampRange::until_end(ms_ts(1501), false)),
1873            ),
1874        ] {
1875            assert_eq!(implied_ms(expr), expected);
1876        }
1877    }
1878
1879    #[test]
1880    fn selector_after_merge_changes_fingerprint() {
1881        let ordinary = test_scan_fingerprint(
1882            vec!["k0 = 'foo'".to_string()],
1883            vec![],
1884            Some(TimeSeriesRowSelector::LastRow { after_merge: false }),
1885            true,
1886            0,
1887        );
1888        let after_merge = test_scan_fingerprint(
1889            vec!["k0 = 'foo'".to_string()],
1890            vec![],
1891            Some(TimeSeriesRowSelector::LastRow { after_merge: true }),
1892            true,
1893            0,
1894        );
1895
1896        assert_ne!(ordinary, after_merge);
1897    }
1898
1899    #[test]
1900    fn true_selector_after_merge_is_preserved_by_fingerprint_transforms() {
1901        let normalized =
1902            test_scan_fingerprint(vec!["k0 = 'foo'".to_string()], vec![], None, true, 0);
1903
1904        assert!(normalized.time_filters().is_empty());
1905
1906        let fingerprint = test_scan_fingerprint(
1907            vec!["k0 = 'foo'".to_string()],
1908            vec!["ts >= 1000".to_string()],
1909            Some(TimeSeriesRowSelector::LastRow { after_merge: true }),
1910            true,
1911            7,
1912        );
1913
1914        let reset = fingerprint.without_time_filters();
1915        let candidate = fingerprint.for_candidate_series();
1916        let series_data = fingerprint.for_series_data(SeriesRange::new(0, 1).unwrap());
1917
1918        assert_eq!(reset.read_columns(), fingerprint.read_columns());
1919        assert_eq!(reset.read_column_types(), fingerprint.read_column_types());
1920        assert_eq!(reset.filters(), fingerprint.filters());
1921        assert!(reset.time_filters().is_empty());
1922        assert_eq!(reset.series_row_selector, fingerprint.series_row_selector);
1923        assert_eq!(
1924            fingerprint.series_row_selector,
1925            Some(TimeSeriesRowSelector::LastRow { after_merge: true })
1926        );
1927        assert_eq!(
1928            candidate.series_row_selector,
1929            fingerprint.series_row_selector
1930        );
1931        assert_eq!(
1932            series_data.series_row_selector,
1933            fingerprint.series_row_selector
1934        );
1935        assert_eq!(reset.append_mode, fingerprint.append_mode);
1936        assert_eq!(reset.filter_deleted, fingerprint.filter_deleted);
1937        assert_eq!(reset.merge_mode, fingerprint.merge_mode);
1938        assert_eq!(
1939            reset.partition_expr_version,
1940            fingerprint.partition_expr_version
1941        );
1942    }
1943
1944    fn test_schema() -> Arc<datatypes::arrow::datatypes::Schema> {
1945        use datatypes::arrow::datatypes::{DataType as ArrowDataType, Field, Schema};
1946
1947        Arc::new(Schema::new(vec![Field::new(
1948            "value",
1949            ArrowDataType::Int64,
1950            false,
1951        )]))
1952    }
1953
1954    fn make_batch(values: &[i64]) -> RecordBatch {
1955        use datatypes::arrow::array::Int64Array;
1956
1957        RecordBatch::try_new(
1958            test_schema(),
1959            vec![Arc::new(Int64Array::from(values.to_vec()))],
1960        )
1961        .unwrap()
1962    }
1963
1964    fn make_large_binary_batch(rows: usize, bytes_per_row: usize) -> RecordBatch {
1965        use datatypes::arrow::array::BinaryArray;
1966        use datatypes::arrow::datatypes::{DataType as ArrowDataType, Field, Schema};
1967
1968        let schema = Arc::new(Schema::new(vec![Field::new(
1969            "value",
1970            ArrowDataType::Binary,
1971            false,
1972        )]));
1973        let payload = vec![b'x'; bytes_per_row];
1974        let values = (0..rows).map(|_| payload.as_slice()).collect::<Vec<_>>();
1975
1976        RecordBatch::try_new(schema, vec![Arc::new(BinaryArray::from_vec(values))]).unwrap()
1977    }
1978
1979    #[test]
1980    fn compact_record_batches_keeps_original_boundaries() {
1981        let batches = vec![make_batch(&[1, 2]), make_batch(&[3]), make_batch(&[4, 5])];
1982
1983        let compacted = compact_record_batches(batches).unwrap();
1984
1985        assert_eq!(compacted.batch.num_rows(), 5);
1986        assert_eq!(compacted.slice_lengths, vec![2, 1, 2]);
1987    }
1988
1989    #[tokio::test]
1990    async fn cached_flat_range_stream_replays_original_batches() {
1991        let value = Arc::new(RangeScanCacheValue::new(
1992            vec![CachedBatchSlice {
1993                batch: make_batch(&[1, 2, 3]),
1994                slice_lengths: vec![2, 1],
1995            }],
1996            make_batch(&[1, 2, 3]).get_array_memory_size(),
1997        ));
1998
1999        let worker = RangeCacheWorker::new(1024 * 1024);
2000        let permit = worker
2001            .load_memory
2002            .try_acquire(value.estimated_size())
2003            .unwrap();
2004        let delivery = RangeCacheDelivery::new(value, permit);
2005        let replayed = cached_flat_range_stream(delivery)
2006            .try_collect::<Vec<_>>()
2007            .await
2008            .unwrap();
2009
2010        assert_eq!(replayed.len(), 2);
2011        assert_eq!(replayed[0].num_rows(), 2);
2012        assert_eq!(replayed[1].num_rows(), 1);
2013    }
2014
2015    #[tokio::test]
2016    async fn dropped_cache_buffer_releases_batches_while_waiting_for_memory() {
2017        let strategy = test_cache_strategy();
2018        let limiter = strategy.range_result_memory_limiter().unwrap();
2019        // Take every permit so the concat task blocks before it can compact.
2020        let _permit = limiter
2021            .acquire(limiter.available_permits() * limiter.permit_bytes())
2022            .await
2023            .unwrap();
2024        let batch = make_batch(&vec![1; DEFAULT_READ_BATCH_SIZE / 2 + 1]);
2025        let weak = Arc::downgrade(batch.column(0));
2026        let mut buffer = CacheBatchBuffer::new(&strategy);
2027        buffer.push(batch.clone()).unwrap();
2028        buffer.push(batch).unwrap();
2029        // Both batches were handed to the task instead of staying in the buffer.
2030        assert!(buffer.buffered_batches.is_empty());
2031
2032        // Cancel only once the task is parked on the permit, otherwise the abort
2033        // could land on a task that was never polled.
2034        tokio::time::timeout(std::time::Duration::from_secs(5), async {
2035            while limiter.waited_acquires() == 0 {
2036                tokio::task::yield_now().await;
2037            }
2038        })
2039        .await
2040        .expect("concat task must wait for a memory permit");
2041        drop(buffer);
2042
2043        tokio::time::timeout(std::time::Duration::from_secs(5), async {
2044            while weak.upgrade().is_some() {
2045                tokio::task::yield_now().await;
2046            }
2047        })
2048        .await
2049        .expect("cancelled cache work must release input without waiting for a permit");
2050    }
2051
2052    #[tokio::test]
2053    async fn cache_batch_buffer_finishes_pending_batches() {
2054        let strategy = test_cache_strategy();
2055        let batch = make_batch(&[1, 2, 3]);
2056        let expected_size = batch.get_array_memory_size();
2057
2058        let mut buffer = CacheBatchBuffer::new(&strategy);
2059        buffer.push(batch).unwrap();
2060
2061        let value = buffer.finish().await.unwrap().unwrap();
2062        assert_eq!(value.cached_batches.len(), 1);
2063        assert_eq!(value.cached_batches[0].slice_lengths, vec![3]);
2064        assert_eq!(value.estimated_batches_size, expected_size);
2065    }
2066
2067    #[tokio::test]
2068    async fn cache_batch_buffer_compacts_when_rows_exceed_default_batch_size() {
2069        let strategy = test_cache_strategy();
2070        let batch = make_batch(&vec![1; DEFAULT_READ_BATCH_SIZE / 2 + 1]);
2071
2072        let mut buffer = CacheBatchBuffer::new(&strategy);
2073        buffer.push(batch.clone()).unwrap();
2074        buffer.push(batch).unwrap();
2075
2076        assert_eq!(buffer.buffered_rows, 0);
2077        assert!(buffer.buffered_batches.is_empty());
2078
2079        let value = buffer.finish().await.unwrap().unwrap();
2080        assert_eq!(value.cached_batches.len(), 1);
2081        assert_eq!(
2082            value.cached_batches[0].slice_lengths,
2083            vec![
2084                DEFAULT_READ_BATCH_SIZE / 2 + 1,
2085                DEFAULT_READ_BATCH_SIZE / 2 + 1
2086            ]
2087        );
2088    }
2089
2090    #[tokio::test]
2091    async fn cache_batch_buffer_compacts_when_buffered_size_exceeds_threshold() {
2092        let large_batch = make_large_binary_batch(DEFAULT_READ_BATCH_SIZE, 4096);
2093        let strategy = CacheStrategy::EnableAll(Arc::new(
2094            CacheManager::builder()
2095                .range_result_cache_size((large_batch.get_array_memory_size() * 3) as u64)
2096                .build(),
2097        ));
2098
2099        let mut buffer = CacheBatchBuffer::new(&strategy);
2100        buffer.push(large_batch.clone()).unwrap();
2101
2102        assert_eq!(buffer.buffered_rows, large_batch.num_rows());
2103        assert_eq!(buffer.buffered_batches.len(), 1);
2104
2105        buffer.push(large_batch.clone()).unwrap();
2106
2107        assert_eq!(buffer.buffered_rows, 0);
2108        assert!(buffer.buffered_batches.is_empty());
2109
2110        let value = buffer.finish().await.unwrap().unwrap();
2111        assert_eq!(value.cached_batches.len(), 1);
2112        assert_eq!(
2113            value.cached_batches[0].slice_lengths,
2114            vec![large_batch.num_rows(), large_batch.num_rows()]
2115        );
2116    }
2117
2118    #[tokio::test]
2119    async fn cache_batch_buffer_skips_cache_when_compacted_size_exceeds_limit() {
2120        let large_batch = make_large_binary_batch(DEFAULT_READ_BATCH_SIZE / 2 + 1, 4096);
2121        // Budget only fits two large batches.
2122        let budget = (large_batch.get_array_memory_size() as u64) * 2 + 1;
2123        let strategy = CacheStrategy::EnableAll(Arc::new(
2124            CacheManager::builder()
2125                .range_result_cache_size(budget)
2126                .build(),
2127        ));
2128
2129        let mut buffer = CacheBatchBuffer::new(&strategy);
2130        for _ in 0..4 {
2131            buffer.push(large_batch.clone()).unwrap();
2132        }
2133        assert!(!matches!(buffer.finish().await, Some(Ok(_))));
2134    }
2135}