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
21#[cfg(any(test, feature = "test"))]
22use async_stream::try_stream;
23use common_time::Timestamp;
24use common_time::range::TimestampRange;
25use common_time::timestamp::TimeUnit;
26use datafusion::execution::memory_pool::MemoryPool;
27use datafusion_expr::expr::Expr;
28use datafusion_expr::{Between, BinaryExpr, Operator};
29#[cfg(test)]
30use datatypes::arrow::record_batch::RecordBatch;
31use datatypes::prelude::ConcreteDataType;
32use datatypes::value::scalar_value_to_timestamp;
33#[cfg(any(test, feature = "test"))]
34use futures::TryStreamExt;
35use store_api::region_engine::PartitionRange;
36use store_api::storage::{FileId, RegionId, SequenceRange, TimeSeriesRowSelector};
37use table::predicate::is_string_timestamp_literal;
38
39use crate::cache::CacheStrategy;
40use crate::cache::range_cache::RangeCacheScope;
41#[cfg(test)]
42use crate::cache::range_cache::{RANGE_CACHE_COMPACT_THRESHOLD_BYTES, RangeScanCacheValue};
43use crate::error::Result;
44use crate::read::BoxedRecordBatchStream;
45use crate::read::pruner::PartitionPruner;
46use crate::read::read_columns::ReadColumns;
47use crate::read::scan_region::StreamContext;
48use crate::read::scan_util::PartitionMetrics;
49use crate::read::series_reader::SeriesRange;
50use crate::region::options::MergeMode;
51#[cfg(any(test, feature = "test"))]
52use crate::sst::parquet::DEFAULT_READ_BATCH_SIZE;
53use crate::sst::parquet::file_range::PreFilterMode;
54
55/// Fingerprint of the scan request fields that affect partition range cache reuse.
56///
57/// It records a normalized view of the projected columns and filters, plus
58/// scan options that can change the returned rows. Schema-dependent metadata
59/// and the partition expression version are included so cached results are not
60/// reused across incompatible schema or partitioning changes.
61#[derive(Debug, Clone, PartialEq, Eq, Hash)]
62pub(crate) struct ScanRequestFingerprint {
63    /// Projection and filters without the time index and partition exprs.
64    inner: Arc<SharedScanRequestFingerprint>,
65    /// Filters with the time index column.
66    time_filters: Option<Arc<Vec<String>>>,
67    series_row_selector: Option<TimeSeriesRowSelector>,
68    append_mode: bool,
69    filter_deleted: bool,
70    merge_mode: MergeMode,
71    /// Exact sequence range applied row-level on SST reads, or `None` for scans
72    /// without row-level sequence filtering. Kept in the cache key so filtered
73    /// range results are never reused for scans with a different (or no) range.
74    sequence_range: Option<SequenceRange>,
75    stage: RangeScanStage,
76    /// File interpretation and snapshot guards, aligned with the key's row groups.
77    file_reads: Option<Arc<Vec<FileReadFingerprint>>>,
78    /// We keep the partition expr version to ensure we won't reuse the fingerprint after we change the partition expr.
79    /// We store the version instead of the whole partition expr or partition expr filters.
80    partition_expr_version: u64,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
84enum RangeScanStage {
85    Data,
86    /// Source batches may be split for a later cross-range merge. Keep their
87    /// cached batch boundaries separate from ordinary sequential scans.
88    DataForCrossRangeMerge,
89    CandidateSeries,
90    SeriesData(SeriesRange),
91}
92
93#[derive(Debug)]
94pub(crate) struct ScanRequestFingerprintBuilder {
95    pub(crate) read_columns: ReadColumns,
96    pub(crate) read_column_types: Vec<Option<ConcreteDataType>>,
97    pub(crate) filters: Vec<String>,
98    pub(crate) time_filters: Vec<String>,
99    pub(crate) series_row_selector: Option<TimeSeriesRowSelector>,
100    pub(crate) append_mode: bool,
101    pub(crate) filter_deleted: bool,
102    pub(crate) merge_mode: MergeMode,
103    pub(crate) sequence_range: Option<SequenceRange>,
104    pub(crate) partition_expr_version: u64,
105}
106
107impl ScanRequestFingerprintBuilder {
108    pub(crate) fn build(self) -> ScanRequestFingerprint {
109        let Self {
110            read_columns,
111            read_column_types,
112            filters,
113            time_filters,
114            series_row_selector,
115            append_mode,
116            filter_deleted,
117            merge_mode,
118            sequence_range,
119            partition_expr_version,
120        } = self;
121
122        ScanRequestFingerprint {
123            inner: Arc::new(SharedScanRequestFingerprint {
124                read_columns,
125                read_column_types,
126                filters,
127            }),
128            time_filters: (!time_filters.is_empty()).then(|| Arc::new(time_filters)),
129            series_row_selector,
130            append_mode,
131            filter_deleted,
132            merge_mode,
133            sequence_range,
134            stage: RangeScanStage::Data,
135            file_reads: None,
136            partition_expr_version,
137        }
138    }
139}
140
141/// Inputs affecting a file read's output.
142#[derive(Debug, Clone, PartialEq, Eq, Hash)]
143struct FileReadFingerprint {
144    target_schema_version: u64,
145    source_region: RegionId,
146    file_sequence: Option<u64>,
147    preserve_row_sequence: bool,
148    snapshot_sequence: Option<u64>,
149    pre_filter_mode: PreFilterMode,
150}
151
152/// Non-copiable struct of the fingerprint.
153#[derive(Debug, PartialEq, Eq, Hash)]
154struct SharedScanRequestFingerprint {
155    /// Logical columns of the projection.
156    read_columns: ReadColumns,
157    /// Column types of the projection.
158    /// We keep this to ensure we won't reuse the fingerprint after a schema change.
159    read_column_types: Vec<Option<ConcreteDataType>>,
160    /// Filters without the time index column and region partition exprs.
161    filters: Vec<String>,
162}
163
164impl ScanRequestFingerprint {
165    #[cfg(test)]
166    pub(crate) fn read_columns(&self) -> &ReadColumns {
167        &self.inner.read_columns
168    }
169
170    #[cfg(test)]
171    pub(crate) fn read_column_types(&self) -> &[Option<ConcreteDataType>] {
172        &self.inner.read_column_types
173    }
174
175    #[cfg(test)]
176    pub(crate) fn filters(&self) -> &[String] {
177        &self.inner.filters
178    }
179
180    #[cfg(test)]
181    pub(crate) fn time_filters(&self) -> &[String] {
182        self.time_filters
183            .as_deref()
184            .map(Vec::as_slice)
185            .unwrap_or(&[])
186    }
187
188    pub(crate) fn without_time_filters(&self) -> Self {
189        Self {
190            inner: Arc::clone(&self.inner),
191            time_filters: None,
192            series_row_selector: self.series_row_selector,
193            append_mode: self.append_mode,
194            filter_deleted: self.filter_deleted,
195            merge_mode: self.merge_mode,
196            sequence_range: self.sequence_range,
197            file_reads: self.file_reads.clone(),
198            stage: self.stage,
199            partition_expr_version: self.partition_expr_version,
200        }
201    }
202
203    fn for_candidate_series(&self) -> Self {
204        Self {
205            inner: Arc::clone(&self.inner),
206            time_filters: self.time_filters.clone(),
207            series_row_selector: self.series_row_selector,
208            append_mode: self.append_mode,
209            filter_deleted: self.filter_deleted,
210            merge_mode: self.merge_mode,
211            sequence_range: self.sequence_range,
212            file_reads: self.file_reads.clone(),
213            stage: RangeScanStage::CandidateSeries,
214            partition_expr_version: self.partition_expr_version,
215        }
216    }
217
218    fn for_series_data(&self, range: SeriesRange) -> Self {
219        Self {
220            inner: Arc::clone(&self.inner),
221            time_filters: self.time_filters.clone(),
222            series_row_selector: self.series_row_selector,
223            append_mode: self.append_mode,
224            filter_deleted: self.filter_deleted,
225            merge_mode: self.merge_mode,
226            sequence_range: self.sequence_range,
227            file_reads: self.file_reads.clone(),
228            stage: RangeScanStage::SeriesData(range),
229            partition_expr_version: self.partition_expr_version,
230        }
231    }
232
233    pub(crate) fn estimated_size(&self) -> usize {
234        mem::size_of::<SharedScanRequestFingerprint>()
235            + self.file_reads.as_ref().map_or(0, |files| {
236                mem::size_of::<Vec<FileReadFingerprint>>()
237                    + files.capacity() * mem::size_of::<FileReadFingerprint>()
238            })
239            + self.inner.read_columns.estimated_size()
240            + self.inner.read_column_types.capacity() * mem::size_of::<Option<ConcreteDataType>>()
241            + self.inner.filters.capacity() * mem::size_of::<String>()
242            + self
243                .inner
244                .filters
245                .iter()
246                .map(|filter| filter.capacity())
247                .sum::<usize>()
248            + self.time_filters.as_ref().map_or(0, |filters| {
249                mem::size_of::<Vec<String>>()
250                    + filters.capacity() * mem::size_of::<String>()
251                    + filters
252                        .iter()
253                        .map(|filter| filter.capacity())
254                        .sum::<usize>()
255            })
256    }
257}
258
259/// Cache key for range scan outputs.
260#[derive(Debug, Clone, PartialEq, Eq, Hash)]
261pub(crate) struct RangeScanCacheKey {
262    pub(crate) region_id: RegionId,
263    /// Sorted file/row-group identities for merged results.
264    pub(crate) row_groups: Vec<(FileId, i64)>,
265    pub(crate) scan: ScanRequestFingerprint,
266}
267
268impl RangeScanCacheKey {
269    pub(crate) fn for_cross_range_merge(mut self) -> Self {
270        debug_assert_eq!(self.scan.stage, RangeScanStage::Data);
271        self.scan.stage = RangeScanStage::DataForCrossRangeMerge;
272        self
273    }
274
275    pub(crate) fn estimated_size(&self) -> usize {
276        mem::size_of::<Self>()
277            + self.row_groups.capacity() * mem::size_of::<(FileId, i64)>()
278            + self.scan.estimated_size()
279    }
280}
281
282/// Returns the timestamp range where all time-only predicates are guaranteed true.
283///
284/// Returns `None` for empty input because there is no time-filter implication
285/// that cache-key normalization or prefilter postponement can use. It also
286/// returns `None` if any expression contains an unsupported shape: `OR`, `NOT`,
287/// `IN`, non-literal RHS, unsupported operator, column-name mismatch, an `=`
288/// literal that cannot be represented exactly in the column unit, or overflow
289/// during bound adjustment.
290///
291/// This is intentionally stricter than `extract_time_range_from_expr` in
292/// `table::predicate`: lower bounds round up and upper bounds round down. If a
293/// partition's file-time range is contained by the returned range, every row in
294/// that partition satisfies the original time predicates.
295///
296/// `IsNull`/`IsNotNull` on the time index are not routed into `time_filters`
297/// today. If that changes, handle them here before stripping time filters from
298/// the cache key.
299pub(crate) fn implied_time_range_from_exprs(
300    ts_col_name: &str,
301    ts_col_unit: TimeUnit,
302    exprs: &[&Expr],
303) -> Option<TimestampRange> {
304    if exprs.is_empty() {
305        return None;
306    }
307
308    let mut acc = TimestampRange::min_to_max();
309    for expr in exprs {
310        let r = implied_time_range_from_expr(ts_col_name, ts_col_unit, expr)?;
311        acc = acc.and(&r);
312    }
313    Some(acc)
314}
315
316fn implied_time_range_from_expr(
317    ts_col_name: &str,
318    ts_col_unit: TimeUnit,
319    expr: &Expr,
320) -> Option<TimestampRange> {
321    match expr {
322        Expr::BinaryExpr(BinaryExpr { left, op, right }) => match op {
323            Operator::And => {
324                let l = implied_time_range_from_expr(ts_col_name, ts_col_unit, left)?;
325                let r = implied_time_range_from_expr(ts_col_name, ts_col_unit, right)?;
326                Some(l.and(&r))
327            }
328            Operator::Eq | Operator::Lt | Operator::LtEq | Operator::Gt | Operator::GtEq => {
329                implied_from_cmp(ts_col_name, ts_col_unit, left, *op, right)
330            }
331            // `OR` would require a strict intersection over a union of half-planes
332            // (not the loose-span union provided by `TimestampRange::or`), so we
333            // refuse it. Any other operator is unsupported.
334            _ => None,
335        },
336        Expr::Between(Between {
337            expr,
338            negated,
339            low,
340            high,
341        }) => {
342            if *negated {
343                return None;
344            }
345            implied_from_between(ts_col_name, ts_col_unit, expr, low, high)
346        }
347        // Includes `IsNull`, `IsNotNull`, `Not`, `InList`, function calls, etc.
348        _ => None,
349    }
350}
351
352fn match_ts_column_literal<'a>(
353    ts_col_name: &str,
354    left: &'a Expr,
355    right: &'a Expr,
356) -> Option<(Timestamp, bool)> {
357    let (col, scalar, reverse) = match (left, right) {
358        (Expr::Column(c), Expr::Literal(s, _)) => (c, s, false),
359        (Expr::Literal(s, _), Expr::Column(c)) => (c, s, true),
360        _ => return None,
361    };
362    if col.name != ts_col_name {
363        return None;
364    }
365    // Reject string literals: their conversion needs a timezone we do not have,
366    // and the existing extractor in `table::predicate` rejects them too.
367    if is_string_timestamp_literal(scalar) {
368        return None;
369    }
370    scalar_value_to_timestamp(scalar, None).map(|t| (t, reverse))
371}
372
373fn implied_from_cmp(
374    ts_col_name: &str,
375    ts_col_unit: TimeUnit,
376    left: &Expr,
377    op: Operator,
378    right: &Expr,
379) -> Option<TimestampRange> {
380    let (ts, reverse) = match_ts_column_literal(ts_col_name, left, right)?;
381    // Normalize to "column OP literal".
382    let op = if reverse {
383        match op {
384            Operator::Lt => Operator::Gt,
385            Operator::LtEq => Operator::GtEq,
386            Operator::Gt => Operator::Lt,
387            Operator::GtEq => Operator::LtEq,
388            Operator::Eq => Operator::Eq,
389            _ => return None,
390        }
391    } else {
392        op
393    };
394
395    match op {
396        Operator::GtEq => {
397            // ts >= L. Round the lower bound up in the column unit.
398            let b = ts.convert_to_ceil(ts_col_unit)?;
399            Some(TimestampRange::from_start(b))
400        }
401        Operator::Gt => {
402            // ts > L. floor(L) + 1 is the tight lower bound in the column unit.
403            let v = ts.convert_to(ts_col_unit)?.value().checked_add(1)?;
404            Some(TimestampRange::from_start(Timestamp::new(v, ts_col_unit)))
405        }
406        Operator::LtEq => {
407            // ts <= L. Round the upper bound down in the column unit.
408            let b = ts.convert_to(ts_col_unit)?;
409            Some(TimestampRange::until_end(b, true))
410        }
411        Operator::Lt => {
412            // ts < L. `ts < ceil(L)` is the tight bound: equal to `ts < L` when
413            // L is exactly representable, and `ts <= floor(L)` otherwise.
414            let b = ts.convert_to_ceil(ts_col_unit)?;
415            Some(TimestampRange::until_end(b, false))
416        }
417        Operator::Eq => {
418            // ts = L. Only provable when L is exactly representable.
419            let f = ts.convert_to(ts_col_unit)?;
420            let c = ts.convert_to_ceil(ts_col_unit)?;
421            if f.value() != c.value() {
422                return None;
423            }
424            Some(TimestampRange::single(f))
425        }
426        _ => None,
427    }
428}
429
430fn implied_from_between(
431    ts_col_name: &str,
432    ts_col_unit: TimeUnit,
433    expr: &Expr,
434    low: &Expr,
435    high: &Expr,
436) -> Option<TimestampRange> {
437    let Expr::Column(c) = expr else {
438        return None;
439    };
440    if c.name != ts_col_name {
441        return None;
442    }
443    let (low_s, high_s) = match (low, high) {
444        (Expr::Literal(l, _), Expr::Literal(h, _)) => (l, h),
445        _ => return None,
446    };
447    if is_string_timestamp_literal(low_s) || is_string_timestamp_literal(high_s) {
448        return None;
449    }
450    let low_ts = scalar_value_to_timestamp(low_s, None)?;
451    let high_ts = scalar_value_to_timestamp(high_s, None)?;
452    // BETWEEN low AND high is equivalent to ts >= low AND ts <= high.
453    let lo = low_ts.convert_to_ceil(ts_col_unit)?;
454    let hi = high_ts.convert_to(ts_col_unit)?;
455    Some(TimestampRange::new_inclusive(Some(lo), Some(hi)))
456}
457
458/// Builds a cache key for the given partition range if it is eligible for caching.
459pub(crate) fn build_range_cache_key(
460    stream_ctx: &StreamContext,
461    part_range: &PartitionRange,
462    partition_pruner: Option<&PartitionPruner>,
463) -> Option<RangeScanCacheKey> {
464    build_range_cache_key_inner(stream_ctx, part_range, None, partition_pruner)
465}
466
467/// Builds a cache key for a candidate-series partition-range result.
468pub(crate) fn build_candidate_range_cache_key(
469    stream_ctx: &StreamContext,
470    part_range: &PartitionRange,
471    partition_pruner: &PartitionPruner,
472) -> Option<RangeScanCacheKey> {
473    build_range_cache_key_inner(
474        stream_ctx,
475        part_range,
476        Some(RangeScanStage::CandidateSeries),
477        Some(partition_pruner),
478    )
479}
480
481/// Builds a cache key for a two-phase series-data partition-range result.
482pub(crate) fn build_series_range_cache_key(
483    stream_ctx: &StreamContext,
484    part_range: &PartitionRange,
485    range: SeriesRange,
486    partition_pruner: &PartitionPruner,
487) -> Option<RangeScanCacheKey> {
488    build_range_cache_key_inner(
489        stream_ctx,
490        part_range,
491        Some(RangeScanStage::SeriesData(range)),
492        Some(partition_pruner),
493    )
494}
495
496fn build_range_cache_key_inner(
497    stream_ctx: &StreamContext,
498    part_range: &PartitionRange,
499    stage: Option<RangeScanStage>,
500    partition_pruner: Option<&PartitionPruner>,
501) -> Option<RangeScanCacheKey> {
502    if !stream_ctx.input.cache_strategy.has_range_result_cache() {
503        return None;
504    }
505
506    let fingerprint = stream_ctx.input.scan_fingerprint()?;
507
508    // Dyn filters can change at runtime, so we can't cache when they're present.
509    let has_dyn_filters = stream_ctx
510        .input
511        .predicate_group()
512        .predicate_without_region()
513        .is_some_and(|p| !p.dyn_filters().is_empty());
514    if has_dyn_filters {
515        return None;
516    }
517
518    let range_meta = &stream_ctx.ranges[part_range.identifier];
519    let mut files = Vec::with_capacity(range_meta.row_group_indices.len());
520    for index in &range_meta.row_group_indices {
521        if !stream_ctx.is_file_range_index(*index) {
522            return None;
523        }
524        let handle = stream_ctx.input.file_from_index(*index);
525        let file = handle.meta_ref();
526        files.push((
527            (handle.file_id().file_id(), index.row_group_index),
528            FileReadFingerprint {
529                target_schema_version: stream_ctx.input.region_metadata().schema_version,
530                source_region: handle.region_id(),
531                file_sequence: file.sequence.map(|sequence| sequence.get()),
532                preserve_row_sequence: file.preserve_row_sequence,
533                snapshot_sequence: stream_ctx.input.snapshot_sequence,
534                pre_filter_mode: partition_pruner.map_or_else(
535                    || stream_ctx.range_pre_filter_mode(part_range),
536                    |pruner| pruner.pre_filter_mode(index.index - stream_ctx.input.num_memtables()),
537                ),
538            },
539        ));
540    }
541    if files.is_empty() {
542        return None;
543    }
544    files.sort_unstable_by(|a, b| {
545        a.0.0
546            .as_bytes()
547            .cmp(b.0.0.as_bytes())
548            .then(a.0.1.cmp(&b.0.1))
549    });
550    let (row_groups, file_reads): (Vec<_>, Vec<_>) = files.into_iter().unzip();
551
552    // If the implied range covers this partition's `FileTimeRange`, drop
553    // time-only predicates from the cache key so that queries with different
554    // but equally-covering time bounds share an entry. `None` means there is no
555    // analyzable time-only predicate or some predicate had an unsupported shape
556    // (e.g. `OR`), so we keep the fingerprint unchanged. When `time_filters` is
557    // already empty, cloning the fingerprint is equivalent to stripping them.
558    let (file_min, file_max) = range_meta.time_range;
559    let covers = match stream_ctx.input.implied_time_range() {
560        // An empty implied range can never cover a non-empty file range, so
561        // short-circuit.
562        Some(implied) if !implied.is_empty() => {
563            // `file_min`/`file_max` may use a different unit than the implied
564            // range: after the time index unit was widened, pre-alter files
565            // still carry the old unit while the implied range uses the
566            // current column unit. `Timestamp` comparisons normalize units,
567            // so `contains` compares instants and stays sound across units.
568            implied.contains(&file_min) && implied.contains(&file_max)
569        }
570        _ => false,
571    };
572    let scan = if covers {
573        fingerprint.without_time_filters()
574    } else {
575        fingerprint.clone()
576    };
577    let mut scan = match stage {
578        Some(RangeScanStage::CandidateSeries) => scan.for_candidate_series(),
579        Some(RangeScanStage::SeriesData(assignment)) => scan.for_series_data(assignment),
580        Some(RangeScanStage::DataForCrossRangeMerge) => {
581            let mut scan = scan;
582            scan.stage = RangeScanStage::DataForCrossRangeMerge;
583            scan
584        }
585        Some(RangeScanStage::Data) | None => scan,
586    };
587
588    scan.file_reads = Some(Arc::new(file_reads));
589    Some(RangeScanCacheKey {
590        region_id: stream_ctx.input.region_metadata().region_id,
591        row_groups,
592        scan,
593    })
594}
595
596/// Cache access for one range, including the SST handles that guard publication.
597pub(crate) struct CachedRangeReader<'a> {
598    pub(crate) stream_ctx: &'a StreamContext,
599    pub(crate) part_range: &'a PartitionRange,
600    pub(crate) key: Option<RangeScanCacheKey>,
601    pub(crate) metrics: &'a PartitionMetrics,
602}
603
604impl CachedRangeReader<'_> {
605    pub(crate) async fn build<F, Fut>(self, build: F) -> Result<(BoxedRecordBatchStream, usize)>
606    where
607        F: FnOnce() -> Fut,
608        Fut: Future<Output = Result<(BoxedRecordBatchStream, usize)>>,
609    {
610        self.build_in_scope(RangeCacheScope::default(), build).await
611    }
612
613    /// Shares active scans only with readers using the same query memory pool.
614    pub(crate) async fn build_with_memory_pool<F, Fut>(
615        self,
616        memory_pool: &Arc<dyn MemoryPool>,
617        build: F,
618    ) -> Result<(BoxedRecordBatchStream, usize)>
619    where
620        F: FnOnce() -> Fut,
621        Fut: Future<Output = Result<(BoxedRecordBatchStream, usize)>>,
622    {
623        self.build_in_scope(RangeCacheScope::for_memory_pool(memory_pool), build)
624            .await
625    }
626
627    async fn build_in_scope<F, Fut>(
628        self,
629        scope: RangeCacheScope,
630        build: F,
631    ) -> Result<(BoxedRecordBatchStream, usize)>
632    where
633        F: FnOnce() -> Fut,
634        Fut: Future<Output = Result<(BoxedRecordBatchStream, usize)>>,
635    {
636        let Some(key) = self.key else {
637            return build().await;
638        };
639        let files = self.stream_ctx.ranges[self.part_range.identifier]
640            .row_group_indices
641            .iter()
642            .map(|index| self.stream_ctx.input.file_from_index(*index).clone())
643            .collect();
644        load_shared_range(
645            &self.stream_ctx.input.cache_strategy,
646            key,
647            self.metrics,
648            files,
649            scope,
650            build,
651        )
652        .await
653    }
654}
655
656/// Joins an active scan while retaining independently advancing stream cursors.
657#[cfg(any(test, feature = "test"))]
658pub(crate) async fn get_or_load_range_result<F, Fut>(
659    strategy: &CacheStrategy,
660    key: RangeScanCacheKey,
661    part_metrics: &PartitionMetrics,
662    build: F,
663) -> Result<(BoxedRecordBatchStream, usize)>
664where
665    F: FnOnce() -> Fut,
666    Fut: Future<Output = Result<(BoxedRecordBatchStream, usize)>>,
667{
668    get_or_load_range_result_in_scope(
669        strategy,
670        key,
671        part_metrics,
672        RangeCacheScope::default(),
673        build,
674    )
675    .await
676}
677
678/// Isolates active sources that reserve memory from a query-specific pool.
679/// Pool identity affects active sharing only, never completed-result reuse.
680#[cfg(any(test, feature = "test"))]
681pub(crate) async fn get_or_load_range_result_in_scope<F, Fut>(
682    strategy: &CacheStrategy,
683    key: RangeScanCacheKey,
684    part_metrics: &PartitionMetrics,
685    scope: RangeCacheScope,
686    build: F,
687) -> Result<(BoxedRecordBatchStream, usize)>
688where
689    F: FnOnce() -> Fut,
690    Fut: Future<Output = Result<(BoxedRecordBatchStream, usize)>>,
691{
692    load_shared_range(strategy, key, part_metrics, Vec::new(), scope, build).await
693}
694
695async fn load_shared_range<F, Fut>(
696    strategy: &CacheStrategy,
697    key: RangeScanCacheKey,
698    part_metrics: &PartitionMetrics,
699    files: Vec<crate::sst::file::FileHandle>,
700    scope: RangeCacheScope,
701    build: F,
702) -> Result<(BoxedRecordBatchStream, usize)>
703where
704    F: FnOnce() -> Fut,
705    Fut: Future<Output = Result<(BoxedRecordBatchStream, usize)>>,
706{
707    let Some(cache) = strategy.range_result_cache() else {
708        return build().await;
709    };
710    cache
711        .read(
712            key,
713            part_metrics.range_cache_metrics_reporter(),
714            files,
715            scope,
716            build,
717        )
718        .await
719}
720
721/// Benchmarks the production range-result loader with a fresh cache per call.
722///
723/// A zero capacity disables caching. A nonzero capacity measures cold construction,
724/// including staging and consumption; background compaction may outlive the stream.
725/// It does not measure warm reuse or concurrent load sharing.
726/// This avoids exposing `RangeScanCacheKey`, `ScanRequestFingerprint`, and
727/// `PartitionMetrics` publicly.
728#[cfg(feature = "test")]
729pub fn bench_cache_flat_range_stream(
730    stream: BoxedRecordBatchStream,
731    cache_size_bytes: u64,
732    region_id: RegionId,
733) -> BoxedRecordBatchStream {
734    use std::time::Instant;
735
736    use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
737
738    use crate::region::options::MergeMode;
739
740    let cache_manager = Arc::new(
741        crate::cache::CacheManager::builder()
742            .range_result_cache_size(cache_size_bytes)
743            .build(),
744    );
745    let cache_strategy = CacheStrategy::EnableAll(cache_manager);
746
747    let fingerprint = ScanRequestFingerprintBuilder {
748        read_columns: ReadColumns::new(std::iter::empty()),
749        read_column_types: vec![],
750        filters: vec![],
751        time_filters: vec![],
752        series_row_selector: None,
753        append_mode: false,
754        filter_deleted: false,
755        merge_mode: MergeMode::LastRow,
756        sequence_range: None,
757        partition_expr_version: 0,
758    }
759    .build();
760
761    let key = RangeScanCacheKey {
762        region_id,
763        row_groups: vec![],
764        scan: fingerprint,
765    };
766
767    let metrics_set = ExecutionPlanMetricsSet::new();
768    let part_metrics =
769        PartitionMetrics::new(region_id, 0, "bench", Instant::now(), false, &metrics_set);
770
771    Box::pin(try_stream! {
772        let (mut stream, _) = get_or_load_range_result(
773            &cache_strategy,
774            key,
775            &part_metrics,
776            || async { Ok((stream, DEFAULT_READ_BATCH_SIZE)) },
777        ).await?;
778        while let Some(batch) = stream.try_next().await? {
779            yield batch;
780        }
781    })
782}
783
784#[cfg(test)]
785mod tests {
786    use std::sync::Arc;
787    use std::time::Instant;
788
789    use common_time::Timestamp;
790    use common_time::range::TimestampRange;
791    use common_time::timestamp::TimeUnit;
792    use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
793    use datafusion_common::ScalarValue;
794    use datafusion_expr::{Expr, col, lit};
795    use smallvec::smallvec;
796    use store_api::storage::{FileId, RegionId};
797
798    use super::*;
799    use crate::cache::CacheManager;
800    use crate::read::flat_projection::FlatProjectionMapper;
801    use crate::read::pruner::Pruner;
802    use crate::read::range::{RangeMeta, RowGroupIndex, SourceIndex};
803    use crate::read::scan_region::{PredicateGroup, ScanInput, StreamContext};
804    use crate::sst::file::FileTimeRange;
805    use crate::test_util::memtable_util::metadata_with_primary_key;
806    use crate::test_util::scheduler_util::SchedulerEnv;
807    use crate::test_util::sst_util::sst_file_handle_with_file_id;
808
809    fn test_cache_strategy() -> CacheStrategy {
810        CacheStrategy::EnableAll(Arc::new(
811            CacheManager::builder()
812                .range_result_cache_size(1024 * 1024)
813                .build(),
814        ))
815    }
816
817    fn test_scan_fingerprint(
818        filters: Vec<String>,
819        time_filters: Vec<String>,
820        series_row_selector: Option<TimeSeriesRowSelector>,
821        filter_deleted: bool,
822        partition_expr_version: u64,
823    ) -> ScanRequestFingerprint {
824        let read_columns = ReadColumns::new([1, 2]);
825        ScanRequestFingerprintBuilder {
826            read_columns,
827            read_column_types: vec![None, None],
828            filters,
829            time_filters,
830            series_row_selector,
831            append_mode: false,
832            filter_deleted,
833            merge_mode: MergeMode::LastRow,
834            sequence_range: None,
835            partition_expr_version,
836        }
837        .build()
838    }
839
840    fn test_cache_context(strategy: &CacheStrategy) -> (RangeScanCacheKey, PartitionMetrics) {
841        let region_id = RegionId::new(1, 1);
842        let key = RangeScanCacheKey {
843            region_id,
844            row_groups: vec![],
845            scan: test_scan_fingerprint(vec![], vec![], None, false, 0),
846        };
847
848        let metrics_set = ExecutionPlanMetricsSet::new();
849        let part_metrics =
850            PartitionMetrics::new(region_id, 0, "test", Instant::now(), false, &metrics_set);
851
852        assert!(strategy.get_range_result(&key).is_none());
853        (key, part_metrics)
854    }
855
856    async fn new_stream_context(
857        filters: Vec<Expr>,
858        query_time_range: Option<TimestampRange>,
859        partition_time_range: FileTimeRange,
860    ) -> (StreamContext, PartitionRange) {
861        let env = SchedulerEnv::new().await;
862        let metadata = Arc::new(metadata_with_primary_key(vec![0, 1], false));
863        let mapper = FlatProjectionMapper::new(&metadata, [0, 2, 3]).unwrap();
864        let predicate = PredicateGroup::new(metadata.as_ref(), &filters).unwrap();
865        let file_id = FileId::random();
866        let file = sst_file_handle_with_file_id(
867            file_id,
868            partition_time_range.0.value(),
869            partition_time_range.1.value(),
870        );
871        let input = ScanInput::builder(env.access_layer.clone(), mapper)
872            .with_predicate(predicate)
873            .with_time_range(query_time_range)
874            .with_files(vec![file])
875            .with_cache(test_cache_strategy())
876            .build();
877        let range_meta = RangeMeta {
878            time_range: partition_time_range,
879            indices: smallvec![SourceIndex {
880                index: 0,
881                num_row_groups: 1,
882            }],
883            row_group_indices: smallvec![RowGroupIndex {
884                index: 0,
885                row_group_index: 0,
886            }],
887            num_rows: 10,
888        };
889        let partition_range = range_meta.new_partition_range(0);
890        let stream_ctx = StreamContext {
891            input,
892            ranges: vec![range_meta],
893            query_start: Instant::now(),
894        };
895
896        (stream_ctx, partition_range)
897    }
898
899    #[rstest::rstest]
900    #[case(1024 * 1024, true)]
901    #[case(1, false)]
902    #[tokio::test]
903    async fn test_shared_range_read_preserves_batches_when_admission_budget_is_exhausted(
904        #[case] capacity: u64,
905        #[case] cached: bool,
906    ) {
907        use std::sync::atomic::{AtomicUsize, Ordering};
908
909        let (mut ctx, range) =
910            new_stream_context(vec![col("k0").eq(lit("a"))], None, FileTimeRange::default()).await;
911        ctx.input.cache_strategy = CacheStrategy::EnableAll(Arc::new(
912            CacheManager::builder()
913                .range_result_cache_size(capacity)
914                .build(),
915        ));
916        let key = build_range_cache_key(&ctx, &range, None).unwrap();
917        let (_, metrics) = test_cache_context(&ctx.input.cache_strategy);
918        let calls = Arc::new(AtomicUsize::new(0));
919        let expected = vec![make_batch(&[1, 2]), make_batch(&[3, 4, 5])];
920        let batches = expected.clone();
921        let count = calls.clone();
922        let (stream, _) = CachedRangeReader {
923            stream_ctx: &ctx,
924            part_range: &range,
925            key: Some(key.clone()),
926            metrics: &metrics,
927        }
928        .build(move || async move {
929            count.fetch_add(1, Ordering::Relaxed);
930            Ok((
931                Box::pin(futures::stream::iter(batches.into_iter().map(Ok)))
932                    as BoxedRecordBatchStream,
933                DEFAULT_READ_BATCH_SIZE,
934            ))
935        })
936        .await
937        .unwrap();
938        let actual: Vec<_> = stream.try_collect().await.unwrap();
939        assert_eq!(actual, expected);
940        assert_eq!(calls.load(Ordering::Relaxed), 1);
941        if cached {
942            wait_cached(&ctx.input.cache_strategy, &key).await;
943        }
944        assert_eq!(
945            ctx.input.cache_strategy.get_range_result(&key).is_some(),
946            cached
947        );
948        let memory = &ctx
949            .input
950            .cache_strategy
951            .range_result_cache()
952            .unwrap()
953            .retention_memory();
954        tokio::time::timeout(std::time::Duration::from_secs(5), async {
955            while memory.available_permits() != (capacity as usize).div_ceil(1024) {
956                tokio::task::yield_now().await;
957            }
958        })
959        .await
960        .expect("finalization did not release the source reservation");
961    }
962
963    #[tokio::test]
964    async fn test_cancelling_a_partial_load_releases_its_reservation() {
965        let strategy = test_cache_strategy();
966        let worker = strategy.range_result_cache().unwrap().clone();
967        let capacity = worker.retention_memory().available_permits();
968        let (key, metrics) = test_cache_context(&strategy);
969        let (mut stream, _) =
970            get_or_load_range_result(&strategy, key.clone(), &metrics, || async {
971                Ok((
972                    Box::pin(try_stream! {
973                        yield make_batch(&[1, 2, 3]);
974                        std::future::pending::<()>().await;
975                    }) as BoxedRecordBatchStream,
976                    DEFAULT_READ_BATCH_SIZE,
977                ))
978            })
979            .await
980            .unwrap();
981        assert!(stream.try_next().await.unwrap().is_some());
982        assert!(worker.retention_memory().available_permits() < capacity);
983        drop(stream);
984        assert_eq!(worker.retention_memory().available_permits(), capacity);
985        assert!(worker.get(&key).is_none());
986    }
987
988    #[tokio::test]
989    async fn test_follower_can_finish_without_polling_the_first_range_stream() {
990        use std::sync::atomic::{AtomicUsize, Ordering};
991
992        let (ctx, range) =
993            new_stream_context(vec![col("k0").eq(lit("a"))], None, FileTimeRange::default()).await;
994        let key = build_range_cache_key(&ctx, &range, None).unwrap();
995        let (_, metrics) = test_cache_context(&ctx.input.cache_strategy);
996        let gate = Arc::new(tokio::sync::Semaphore::new(0));
997        let calls = Arc::new(AtomicUsize::new(0));
998        let count = calls.clone();
999        let loader_gate = gate.clone();
1000        let build = move || async move {
1001            count.fetch_add(1, Ordering::Relaxed);
1002            Ok((
1003                Box::pin(try_stream! {
1004                    let _permit = loader_gate.acquire().await.unwrap();
1005                    yield make_batch(&[1, 2, 3]);
1006                }) as BoxedRecordBatchStream,
1007                DEFAULT_READ_BATCH_SIZE,
1008            ))
1009        };
1010        let (first, _) = CachedRangeReader {
1011            stream_ctx: &ctx,
1012            part_range: &range,
1013            key: Some(key.clone()),
1014            metrics: &metrics,
1015        }
1016        .build(build.clone())
1017        .await
1018        .unwrap();
1019        let (second, _) = CachedRangeReader {
1020            stream_ctx: &ctx,
1021            part_range: &range,
1022            key: Some(key),
1023            metrics: &metrics,
1024        }
1025        .build(build)
1026        .await
1027        .unwrap();
1028        gate.add_permits(1);
1029        let second_batches: Vec<_> =
1030            tokio::time::timeout(std::time::Duration::from_secs(5), second.try_collect())
1031                .await
1032                .expect("a follower must not depend on polling the first query")
1033                .unwrap();
1034        let first_batches: Vec<_> = first.try_collect().await.unwrap();
1035        assert_eq!(first_batches, second_batches);
1036        assert_eq!(second_batches, vec![make_batch(&[1, 2, 3])]);
1037        assert_eq!(calls.load(Ordering::Relaxed), 1);
1038    }
1039
1040    /// Helper to create a timestamp millisecond literal.
1041    fn ts_lit(val: i64) -> Expr {
1042        lit(ScalarValue::TimestampMillisecond(Some(val), None))
1043    }
1044
1045    fn normalized_exprs(exprs: impl IntoIterator<Item = Expr>) -> Vec<String> {
1046        let mut exprs = exprs
1047            .into_iter()
1048            .map(|expr| expr.to_string())
1049            .collect::<Vec<_>>();
1050        exprs.sort_unstable();
1051        exprs
1052    }
1053
1054    async fn assert_range_cache_filters(
1055        filters: Vec<Expr>,
1056        query_time_range: Option<TimestampRange>,
1057        partition_time_range: FileTimeRange,
1058        expected_filters: Vec<Expr>,
1059        expected_time_filters: Vec<Expr>,
1060    ) {
1061        let (stream_ctx, part_range) =
1062            new_stream_context(filters, query_time_range, partition_time_range).await;
1063
1064        let key = build_range_cache_key(&stream_ctx, &part_range, None).unwrap();
1065
1066        assert_eq!(
1067            key.scan.filters(),
1068            normalized_exprs(expected_filters).as_slice()
1069        );
1070        assert_eq!(
1071            key.scan.time_filters(),
1072            normalized_exprs(expected_time_filters).as_slice()
1073        );
1074    }
1075
1076    #[tokio::test]
1077    async fn test_range_cache_time_filter_key_cases() {
1078        let partition = (
1079            Timestamp::new_millisecond(1000),
1080            Timestamp::new_millisecond(2000),
1081        );
1082
1083        struct Case {
1084            filters: Vec<Expr>,
1085            query_time_range: Option<TimestampRange>,
1086            expected_filters: Vec<Expr>,
1087            expected_time_filters: Vec<Expr>,
1088        }
1089
1090        // Time filters are stripped only when their implied range fully covers
1091        // the partition's file-time range. `is_not_null(ts)` stays in regular
1092        // filters because it is not routed into `time_filters`.
1093        for case in [
1094            Case {
1095                filters: vec![
1096                    col("ts").gt_eq(ts_lit(1000)),
1097                    col("ts").lt(ts_lit(2001)),
1098                    col("ts").is_not_null(),
1099                    col("k0").eq(lit("foo")),
1100                ],
1101                query_time_range: TimestampRange::with_unit(1000, 2002, TimeUnit::Millisecond),
1102                expected_filters: vec![col("k0").eq(lit("foo")), col("ts").is_not_null()],
1103                expected_time_filters: vec![],
1104            },
1105            Case {
1106                filters: vec![
1107                    col("ts").gt_eq(ts_lit(500)),
1108                    col("ts").lt(ts_lit(3000)),
1109                    col("k0").eq(lit("foo")),
1110                ],
1111                query_time_range: TimestampRange::with_unit(500, 3000, TimeUnit::Millisecond),
1112                expected_filters: vec![col("k0").eq(lit("foo"))],
1113                expected_time_filters: vec![],
1114            },
1115            Case {
1116                filters: vec![
1117                    col("ts").gt_eq(ts_lit(1000)),
1118                    col("ts").lt_eq(ts_lit(2000)),
1119                    col("k0").eq(lit("foo")),
1120                ],
1121                query_time_range: TimestampRange::with_unit(1000, 2001, TimeUnit::Millisecond),
1122                expected_filters: vec![col("k0").eq(lit("foo"))],
1123                expected_time_filters: vec![],
1124            },
1125            Case {
1126                filters: vec![
1127                    col("ts").between(ts_lit(1000), ts_lit(2000)),
1128                    col("k0").eq(lit("foo")),
1129                ],
1130                query_time_range: TimestampRange::with_unit(1000, 2001, TimeUnit::Millisecond),
1131                expected_filters: vec![col("k0").eq(lit("foo"))],
1132                expected_time_filters: vec![],
1133            },
1134            Case {
1135                filters: vec![col("ts").gt_eq(ts_lit(1200)), col("k0").eq(lit("foo"))],
1136                query_time_range: TimestampRange::with_unit(1200, 2001, TimeUnit::Millisecond),
1137                expected_filters: vec![col("k0").eq(lit("foo"))],
1138                expected_time_filters: vec![col("ts").gt_eq(ts_lit(1200))],
1139            },
1140            Case {
1141                filters: vec![
1142                    col("ts").gt_eq(ts_lit(1500)),
1143                    col("ts").is_not_null(),
1144                    col("k0").eq(lit("foo")),
1145                ],
1146                query_time_range: None,
1147                expected_filters: vec![col("k0").eq(lit("foo")), col("ts").is_not_null()],
1148                expected_time_filters: vec![col("ts").gt_eq(ts_lit(1500))],
1149            },
1150        ] {
1151            assert_range_cache_filters(
1152                case.filters,
1153                case.query_time_range,
1154                partition,
1155                case.expected_filters,
1156                case.expected_time_filters,
1157            )
1158            .await;
1159        }
1160    }
1161
1162    #[tokio::test]
1163    async fn test_two_distinct_queries_share_cache_key_when_both_cover() {
1164        let partition_range = (
1165            Timestamp::new_millisecond(1000),
1166            Timestamp::new_millisecond(2000),
1167        );
1168
1169        let (ctx_a, part_a) = new_stream_context(
1170            vec![
1171                col("ts").gt_eq(ts_lit(500)),
1172                col("ts").lt(ts_lit(3000)),
1173                col("k0").eq(lit("foo")),
1174            ],
1175            TimestampRange::with_unit(500, 3000, TimeUnit::Millisecond),
1176            partition_range,
1177        )
1178        .await;
1179        let (ctx_b, part_b) = new_stream_context(
1180            vec![
1181                col("ts").gt_eq(ts_lit(100)),
1182                col("ts").lt(ts_lit(5000)),
1183                col("k0").eq(lit("foo")),
1184            ],
1185            TimestampRange::with_unit(100, 5000, TimeUnit::Millisecond),
1186            partition_range,
1187        )
1188        .await;
1189
1190        let key_a = build_range_cache_key(&ctx_a, &part_a, None).unwrap();
1191        let key_b = build_range_cache_key(&ctx_b, &part_b, None).unwrap();
1192        assert_eq!(key_a.scan, key_b.scan);
1193        assert!(key_a.scan.time_filters().is_empty());
1194    }
1195
1196    #[tokio::test]
1197    async fn test_scan_stages_have_separate_cache_keys() {
1198        let partition_range = (
1199            Timestamp::new_millisecond(1000),
1200            Timestamp::new_millisecond(2000),
1201        );
1202        let (ctx, part_range) =
1203            new_stream_context(vec![col("k0").eq(lit("foo"))], None, partition_range).await;
1204
1205        let ctx = Arc::new(ctx);
1206        let pruner = Arc::new(crate::read::pruner::Pruner::new(ctx.clone(), 0));
1207        let partition = PartitionPruner::new(pruner, std::slice::from_ref(&part_range));
1208        let data_key = build_range_cache_key(&ctx, &part_range, None).unwrap();
1209        let candidate_key = build_candidate_range_cache_key(&ctx, &part_range, &partition).unwrap();
1210        let range_0 = SeriesRange::new(0, 2).unwrap();
1211        let range_1 = SeriesRange::new(1, 2).unwrap();
1212        let series_key_0 =
1213            build_series_range_cache_key(&ctx, &part_range, range_0, &partition).unwrap();
1214        let series_key_1 =
1215            build_series_range_cache_key(&ctx, &part_range, range_1, &partition).unwrap();
1216
1217        let merge_key = data_key.clone().for_cross_range_merge();
1218        assert_ne!(merge_key, data_key);
1219        assert_ne!(merge_key.scan, candidate_key.scan);
1220        assert_ne!(merge_key.scan, series_key_0.scan);
1221        assert_eq!(merge_key.row_groups, data_key.row_groups);
1222        assert_eq!(merge_key, data_key.clone().for_cross_range_merge());
1223        assert_ne!(data_key.scan, candidate_key.scan);
1224        assert_ne!(candidate_key.scan, series_key_0.scan);
1225        assert_eq!(data_key.row_groups, candidate_key.row_groups);
1226        assert_ne!(data_key.scan, series_key_0.scan);
1227        assert_ne!(series_key_0.scan, series_key_1.scan);
1228        assert_eq!(data_key.row_groups, series_key_0.row_groups);
1229    }
1230
1231    #[tokio::test]
1232    async fn test_disables_optimization_on_or_clause() {
1233        let partition_range = (
1234            Timestamp::new_millisecond(1000),
1235            Timestamp::new_millisecond(2000),
1236        );
1237
1238        let or_a = col("ts").gt_eq(ts_lit(1000)).or(col("ts").lt(ts_lit(500)));
1239        let or_b = col("ts").gt_eq(ts_lit(900)).or(col("ts").lt(ts_lit(400)));
1240
1241        let (ctx_a, part_a) = new_stream_context(
1242            vec![or_a.clone(), col("k0").eq(lit("foo"))],
1243            None,
1244            partition_range,
1245        )
1246        .await;
1247        let (ctx_b, part_b) = new_stream_context(
1248            vec![or_b.clone(), col("k0").eq(lit("foo"))],
1249            None,
1250            partition_range,
1251        )
1252        .await;
1253
1254        assert!(ctx_a.input.implied_time_range().is_none());
1255        let key_a = build_range_cache_key(&ctx_a, &part_a, None).unwrap();
1256        let key_b = build_range_cache_key(&ctx_b, &part_b, None).unwrap();
1257        assert_ne!(key_a.scan, key_b.scan);
1258        assert_eq!(
1259            key_a.scan.time_filters(),
1260            normalized_exprs([or_a]).as_slice()
1261        );
1262    }
1263
1264    #[tokio::test]
1265    async fn test_empty_implied_range_does_not_panic_on_non_ms_file_range() {
1266        // Contradictory time predicates make the implied range empty. The
1267        // empty range's sentinel timestamps use `Timestamp::default()` (ms),
1268        // so without the `is_empty()` short-circuit the unit asserts would
1269        // panic against a non-ms `range_meta.time_range`.
1270        let partition = (
1271            Timestamp::new_millisecond(1000),
1272            Timestamp::new_millisecond(2000),
1273        );
1274
1275        let (mut ctx, part_range) = new_stream_context(
1276            vec![
1277                col("ts").gt_eq(ts_lit(1500)),
1278                col("ts").lt(ts_lit(1500)),
1279                col("k0").eq(lit("foo")),
1280            ],
1281            TimestampRange::with_unit(1500, 3000, TimeUnit::Millisecond),
1282            partition,
1283        )
1284        .await;
1285
1286        assert!(ctx.input.implied_time_range().unwrap().is_empty());
1287        ctx.ranges[0].time_range = (
1288            Timestamp::new(1_000_000_000, TimeUnit::Nanosecond),
1289            Timestamp::new(2_000_000_000, TimeUnit::Nanosecond),
1290        );
1291
1292        let key = build_range_cache_key(&ctx, &part_range, None).unwrap();
1293        // Empty implied range cannot cover, so time filters stay in the key.
1294        assert!(!key.scan.time_filters().is_empty());
1295    }
1296
1297    #[tokio::test]
1298    async fn test_mixed_unit_file_range_is_compared_by_instant() {
1299        // After the time index unit was widened, pre-alter files keep the old
1300        // unit while `scan_implied_time_range` uses the current column unit.
1301        // The coverage decision must compare instants across units instead of
1302        // asserting unit equality (which panicked here for widened regions).
1303        let partition = (
1304            Timestamp::new_millisecond(1000),
1305            Timestamp::new_millisecond(2000),
1306        );
1307
1308        // Covering: the implied range [0, 3000)ms contains the file range
1309        // [1s, 2s] expressed in microseconds; time filters are dropped from
1310        // the key.
1311        let (mut ctx, part_range) = new_stream_context(
1312            vec![
1313                col("ts").gt_eq(ts_lit(0)),
1314                col("ts").lt(ts_lit(3000)),
1315                col("k0").eq(lit("foo")),
1316            ],
1317            TimestampRange::with_unit(0, 3000, TimeUnit::Millisecond),
1318            partition,
1319        )
1320        .await;
1321        ctx.ranges[0].time_range = (
1322            Timestamp::new_microsecond(1_000_000),
1323            Timestamp::new_microsecond(2_000_000),
1324        );
1325        let key = build_range_cache_key(&ctx, &part_range, None).unwrap();
1326        assert!(key.scan.time_filters().is_empty());
1327
1328        // Not covering: the implied range [1500, 3000)ms excludes the file's
1329        // 1s lower bound; time filters stay in the key.
1330        let (mut ctx, part_range) = new_stream_context(
1331            vec![
1332                col("ts").gt_eq(ts_lit(1500)),
1333                col("ts").lt(ts_lit(3000)),
1334                col("k0").eq(lit("foo")),
1335            ],
1336            TimestampRange::with_unit(1500, 3000, TimeUnit::Millisecond),
1337            partition,
1338        )
1339        .await;
1340        ctx.ranges[0].time_range = (
1341            Timestamp::new_microsecond(1_000_000),
1342            Timestamp::new_microsecond(2_000_000),
1343        );
1344        let key = build_range_cache_key(&ctx, &part_range, None).unwrap();
1345        assert!(!key.scan.time_filters().is_empty());
1346    }
1347
1348    fn ms_ts(v: i64) -> Timestamp {
1349        Timestamp::new_millisecond(v)
1350    }
1351
1352    fn implied_ms(expr: Expr) -> Option<TimestampRange> {
1353        implied_time_range_from_exprs("ts", TimeUnit::Millisecond, &[&expr])
1354    }
1355
1356    #[test]
1357    fn test_implied_time_range_supported_exprs() {
1358        for (expr, expected) in [
1359            (
1360                col("ts").gt_eq(ts_lit(1000)),
1361                Some(TimestampRange::from_start(ms_ts(1000))),
1362            ),
1363            (
1364                col("ts").gt(ts_lit(1000)),
1365                Some(TimestampRange::from_start(ms_ts(1001))),
1366            ),
1367            (
1368                col("ts").lt_eq(ts_lit(2000)),
1369                Some(TimestampRange::until_end(ms_ts(2000), true)),
1370            ),
1371            (
1372                col("ts").lt(ts_lit(2000)),
1373                Some(TimestampRange::until_end(ms_ts(2000), false)),
1374            ),
1375            (
1376                col("ts").eq(ts_lit(1500)),
1377                Some(TimestampRange::single(ms_ts(1500))),
1378            ),
1379            (
1380                ts_lit(1000).lt_eq(col("ts")),
1381                Some(TimestampRange::from_start(ms_ts(1000))),
1382            ),
1383            (
1384                col("ts").between(ts_lit(1000), ts_lit(2000)),
1385                Some(TimestampRange::new_inclusive(
1386                    Some(ms_ts(1000)),
1387                    Some(ms_ts(2000)),
1388                )),
1389            ),
1390            (
1391                col("ts")
1392                    .gt_eq(ts_lit(1000))
1393                    .and(col("ts").lt(ts_lit(2000))),
1394                TimestampRange::with_unit(1000, 2000, TimeUnit::Millisecond),
1395            ),
1396            (
1397                col("ts")
1398                    .gt_eq(ts_lit(1000))
1399                    .and(col("ts").lt(ts_lit(5000)))
1400                    .and(col("ts").lt_eq(ts_lit(3000))),
1401                TimestampRange::with_unit(1000, 3001, TimeUnit::Millisecond),
1402            ),
1403        ] {
1404            assert_eq!(implied_ms(expr), expected);
1405        }
1406
1407        assert_eq!(
1408            implied_time_range_from_exprs("ts", TimeUnit::Millisecond, &[]),
1409            None
1410        );
1411    }
1412
1413    #[test]
1414    fn test_implied_time_range_unsupported_exprs() {
1415        let not_between = Expr::Between(Between {
1416            expr: Box::new(col("ts")),
1417            negated: true,
1418            low: Box::new(ts_lit(1000)),
1419            high: Box::new(ts_lit(2000)),
1420        });
1421
1422        for expr in [
1423            not_between,
1424            col("ts").gt_eq(ts_lit(1000)).or(col("ts").lt(ts_lit(500))),
1425            Expr::Not(Box::new(col("ts").gt_eq(ts_lit(1000)))),
1426            col("ts").in_list(vec![ts_lit(1000), ts_lit(2000)], false),
1427            col("ts").gt_eq(col("other")),
1428            col("other_ts").gt_eq(ts_lit(1000)),
1429        ] {
1430            assert!(implied_ms(expr).is_none());
1431        }
1432    }
1433
1434    #[test]
1435    fn test_implied_time_range_unit_conversion() {
1436        let second_1 = lit(ScalarValue::TimestampSecond(Some(1), None));
1437        let ns_1500 = lit(ScalarValue::TimestampNanosecond(Some(1_500_000_000), None));
1438        let ns_1500_5 = lit(ScalarValue::TimestampNanosecond(Some(1_500_500_000), None));
1439
1440        for (expr, expected) in [
1441            (
1442                col("ts").gt_eq(second_1.clone()),
1443                Some(TimestampRange::from_start(ms_ts(1000))),
1444            ),
1445            (
1446                col("ts").lt_eq(second_1),
1447                Some(TimestampRange::until_end(ms_ts(1000), true)),
1448            ),
1449            (
1450                col("ts").eq(ns_1500),
1451                Some(TimestampRange::single(ms_ts(1500))),
1452            ),
1453            (col("ts").eq(ns_1500_5.clone()), None),
1454            (
1455                col("ts").gt_eq(ns_1500_5.clone()),
1456                Some(TimestampRange::from_start(ms_ts(1501))),
1457            ),
1458            (
1459                col("ts").lt_eq(ns_1500_5.clone()),
1460                Some(TimestampRange::until_end(ms_ts(1500), true)),
1461            ),
1462            (
1463                col("ts").gt(ns_1500_5.clone()),
1464                Some(TimestampRange::from_start(ms_ts(1501))),
1465            ),
1466            (
1467                col("ts").lt(ns_1500_5),
1468                Some(TimestampRange::until_end(ms_ts(1501), false)),
1469            ),
1470        ] {
1471            assert_eq!(implied_ms(expr), expected);
1472        }
1473    }
1474
1475    #[test]
1476    fn test_selector_after_merge_changes_fingerprint() {
1477        let ordinary = test_scan_fingerprint(
1478            vec!["k0 = 'foo'".to_string()],
1479            vec![],
1480            Some(TimeSeriesRowSelector::LastRow { after_merge: false }),
1481            true,
1482            0,
1483        );
1484        let after_merge = test_scan_fingerprint(
1485            vec!["k0 = 'foo'".to_string()],
1486            vec![],
1487            Some(TimeSeriesRowSelector::LastRow { after_merge: true }),
1488            true,
1489            0,
1490        );
1491
1492        assert_ne!(ordinary, after_merge);
1493    }
1494
1495    #[test]
1496    fn test_true_selector_after_merge_is_preserved_by_fingerprint_transforms() {
1497        let normalized =
1498            test_scan_fingerprint(vec!["k0 = 'foo'".to_string()], vec![], None, true, 0);
1499
1500        assert!(normalized.time_filters().is_empty());
1501
1502        let fingerprint = test_scan_fingerprint(
1503            vec!["k0 = 'foo'".to_string()],
1504            vec!["ts >= 1000".to_string()],
1505            Some(TimeSeriesRowSelector::LastRow { after_merge: true }),
1506            true,
1507            7,
1508        );
1509
1510        let reset = fingerprint.without_time_filters();
1511        let candidate = fingerprint.for_candidate_series();
1512        let series_data = fingerprint.for_series_data(SeriesRange::new(0, 1).unwrap());
1513
1514        assert_eq!(reset.read_columns(), fingerprint.read_columns());
1515        assert_eq!(reset.read_column_types(), fingerprint.read_column_types());
1516        assert_eq!(reset.filters(), fingerprint.filters());
1517        assert!(reset.time_filters().is_empty());
1518        assert_eq!(reset.series_row_selector, fingerprint.series_row_selector);
1519        assert_eq!(
1520            fingerprint.series_row_selector,
1521            Some(TimeSeriesRowSelector::LastRow { after_merge: true })
1522        );
1523        assert_eq!(
1524            candidate.series_row_selector,
1525            fingerprint.series_row_selector
1526        );
1527        assert_eq!(
1528            series_data.series_row_selector,
1529            fingerprint.series_row_selector
1530        );
1531        assert_eq!(reset.append_mode, fingerprint.append_mode);
1532        assert_eq!(reset.filter_deleted, fingerprint.filter_deleted);
1533        assert_eq!(reset.merge_mode, fingerprint.merge_mode);
1534        assert_eq!(
1535            reset.partition_expr_version,
1536            fingerprint.partition_expr_version
1537        );
1538    }
1539
1540    fn test_schema() -> Arc<datatypes::arrow::datatypes::Schema> {
1541        use datatypes::arrow::datatypes::{DataType as ArrowDataType, Field, Schema};
1542
1543        Arc::new(Schema::new(vec![Field::new(
1544            "value",
1545            ArrowDataType::Int64,
1546            false,
1547        )]))
1548    }
1549
1550    fn make_batch(values: &[i64]) -> RecordBatch {
1551        use datatypes::arrow::array::Int64Array;
1552
1553        RecordBatch::try_new(
1554            test_schema(),
1555            vec![Arc::new(Int64Array::from(values.to_vec()))],
1556        )
1557        .unwrap()
1558    }
1559
1560    fn make_large_binary_batch(rows: usize, bytes_per_row: usize) -> RecordBatch {
1561        use datatypes::arrow::array::BinaryArray;
1562        use datatypes::arrow::datatypes::{DataType as ArrowDataType, Field, Schema};
1563
1564        let schema = Arc::new(Schema::new(vec![Field::new(
1565            "value",
1566            ArrowDataType::Binary,
1567            false,
1568        )]));
1569        let payloads: Vec<_> = (0..rows)
1570            .map(|i| vec![(i % 256) as u8; bytes_per_row])
1571            .collect();
1572        let values = payloads.iter().map(Vec::as_slice).collect::<Vec<_>>();
1573
1574        RecordBatch::try_new(schema, vec![Arc::new(BinaryArray::from_vec(values))]).unwrap()
1575    }
1576
1577    fn source_stream(batches: Vec<RecordBatch>) -> BoxedRecordBatchStream {
1578        Box::pin(futures::stream::iter(batches.into_iter().map(Ok)))
1579    }
1580
1581    async fn wait_cached(
1582        strategy: &CacheStrategy,
1583        key: &RangeScanCacheKey,
1584    ) -> Arc<RangeScanCacheValue> {
1585        tokio::time::timeout(std::time::Duration::from_secs(5), async {
1586            loop {
1587                if let Some(value) = strategy.get_range_result(key) {
1588                    return value;
1589                }
1590                tokio::task::yield_now().await;
1591            }
1592        })
1593        .await
1594        .expect("range result was not published")
1595    }
1596
1597    #[tokio::test]
1598    async fn test_merged_key_covers_files_and_snapshot_independently_of_order() {
1599        let (context, range) = new_stream_context(
1600            vec![col("k0").eq(lit("a"))],
1601            None,
1602            (
1603                Timestamp::new_millisecond(0),
1604                Timestamp::new_millisecond(10),
1605            ),
1606        )
1607        .await;
1608        let mut context = Arc::new(context);
1609        let first = build_range_cache_key(&context, &range, None).unwrap();
1610        let file = sst_file_handle_with_file_id(FileId::random(), 0, 10);
1611        Arc::get_mut(&mut context).unwrap().input.files.push(file);
1612        Arc::get_mut(&mut context).unwrap().ranges[0]
1613            .indices
1614            .push(SourceIndex {
1615                index: 1,
1616                num_row_groups: 1,
1617            });
1618        Arc::get_mut(&mut context).unwrap().ranges[0]
1619            .row_group_indices
1620            .push(RowGroupIndex {
1621                index: 1,
1622                row_group_index: -1,
1623            });
1624        let whole = build_range_cache_key(&context, &range, None).unwrap();
1625        assert_eq!(whole.row_groups.len(), 2);
1626        assert!(
1627            whole
1628                .row_groups
1629                .iter()
1630                .any(|(_, row_group)| *row_group == -1)
1631        );
1632        assert_ne!(first, whole);
1633        Arc::get_mut(&mut context).unwrap().ranges[0]
1634            .row_group_indices
1635            .swap(0, 1);
1636        let reordered = build_range_cache_key(&context, &range, None).unwrap();
1637        assert_eq!(whole, reordered);
1638        Arc::get_mut(&mut context).unwrap().input.snapshot_sequence = Some(42);
1639        let snapshot = build_range_cache_key(&context, &range, None).unwrap();
1640        assert_ne!(snapshot, reordered);
1641        assert_eq!(snapshot.scan.stage, RangeScanStage::Data);
1642        let sources = snapshot.scan.file_reads.as_ref().unwrap();
1643        assert_eq!(sources.len(), 2);
1644        assert!(
1645            sources
1646                .iter()
1647                .all(|source| source.snapshot_sequence == Some(42))
1648        );
1649    }
1650
1651    fn new_partition_pruner(
1652        context: &Arc<StreamContext>,
1653        range: &PartitionRange,
1654    ) -> Arc<PartitionPruner> {
1655        let pruner = Arc::new(Pruner::new(context.clone(), 1));
1656        pruner.add_partition_ranges(std::slice::from_ref(range));
1657        Arc::new(PartitionPruner::new(pruner, std::slice::from_ref(range)))
1658    }
1659
1660    async fn sst_range_stream(
1661        context: &Arc<StreamContext>,
1662        range: &PartitionRange,
1663        metrics: &PartitionMetrics,
1664        partition: Arc<PartitionPruner>,
1665        semaphore: Option<Arc<tokio::sync::Semaphore>>,
1666    ) -> BoxedRecordBatchStream {
1667        crate::read::seq_scan::SeqScan::build_flat_partition_range_read(
1668            context,
1669            range,
1670            crate::read::seq_scan::FlatReadMode::Sequential,
1671            metrics,
1672            partition,
1673            semaphore.clone(),
1674            semaphore,
1675        )
1676        .await
1677        .unwrap()
1678        .0
1679    }
1680
1681    fn u64_values(batches: &[RecordBatch], name: &str) -> Vec<Option<u64>> {
1682        batches
1683            .iter()
1684            .flat_map(|batch| {
1685                batch
1686                    .column_by_name(name)
1687                    .unwrap()
1688                    .as_any()
1689                    .downcast_ref::<datatypes::arrow::array::UInt64Array>()
1690                    .unwrap()
1691                    .iter()
1692            })
1693            .collect()
1694    }
1695
1696    async fn write_overlapping_sst_files(
1697        env: &SchedulerEnv,
1698    ) -> (
1699        store_api::metadata::RegionMetadataRef,
1700        Vec<crate::sst::file::FileHandle>,
1701    ) {
1702        use datatypes::arrow::array::UInt64Array;
1703        use parquet::arrow::ArrowWriter;
1704        use parquet::file::metadata::KeyValue;
1705        use parquet::file::properties::WriterProperties;
1706        use store_api::region_request::PathType;
1707
1708        use crate::sst::file::{FileHandle, FileMeta};
1709        use crate::sst::parquet::PARQUET_METADATA_KEY;
1710        use crate::test_util::sst_util::{
1711            new_record_batch_with_custom_sequence, sst_region_metadata,
1712        };
1713
1714        let metadata = Arc::new(sst_region_metadata());
1715        let mut files = Vec::new();
1716        for sequence in [1, 2] {
1717            let props = WriterProperties::builder()
1718                .set_key_value_metadata(Some(vec![KeyValue::new(
1719                    PARQUET_METADATA_KEY.to_string(),
1720                    metadata.to_json().unwrap(),
1721                )]))
1722                .build();
1723            let first = new_record_batch_with_custom_sequence(&["a", "x"], 0, 2, sequence);
1724            let mut bytes = Vec::new();
1725            let mut writer = ArrowWriter::try_new(&mut bytes, first.schema(), Some(props)).unwrap();
1726            for start in [0, 2] {
1727                let batch =
1728                    new_record_batch_with_custom_sequence(&["a", "x"], start, start + 2, sequence);
1729                let mut columns = batch.columns().to_vec();
1730                columns[2] = Arc::new(UInt64Array::from(vec![
1731                    sequence * 100 + start as u64,
1732                    sequence * 100 + start as u64 + 1,
1733                ]));
1734                writer
1735                    .write(&RecordBatch::try_new(batch.schema(), columns).unwrap())
1736                    .unwrap();
1737                writer.flush().unwrap();
1738            }
1739            writer.close().unwrap();
1740            let file = FileHandle::new(
1741                FileMeta {
1742                    region_id: metadata.region_id,
1743                    file_id: FileId::random(),
1744                    time_range: (Timestamp::new_millisecond(0), Timestamp::new_millisecond(3)),
1745                    file_size: bytes.len() as u64,
1746                    num_rows: 4,
1747                    num_row_groups: 2,
1748                    preserve_row_sequence: true,
1749                    ..Default::default()
1750                },
1751                crate::test_util::new_noop_file_purger(),
1752            );
1753            env.access_layer
1754                .object_store()
1755                .write(&file.file_path("", PathType::Bare), bytes)
1756                .await
1757                .unwrap();
1758            files.push(file);
1759        }
1760        (metadata, files)
1761    }
1762
1763    #[tokio::test]
1764    async fn test_range_cache_keeps_cross_sst_dedup_and_task_membership() {
1765        use store_api::storage::consts::SEQUENCE_COLUMN_NAME;
1766
1767        for capacity in [0, 1024, 1024 * 1024] {
1768            let env = SchedulerEnv::new().await;
1769            let (metadata, files) = write_overlapping_sst_files(&env).await;
1770            let strategy = CacheStrategy::EnableAll(Arc::new(
1771                CacheManager::builder()
1772                    .range_result_cache_size(capacity)
1773                    .build(),
1774            ));
1775            for (case, (max_sequence, last_row)) in [
1776                (1, false),
1777                (2, false),
1778                (1, false),
1779                (2, true),
1780                (2, false),
1781                (1, true),
1782            ]
1783            .into_iter()
1784            .enumerate()
1785            {
1786                let mapper =
1787                    FlatProjectionMapper::new(&metadata, 0..metadata.column_metadatas.len())
1788                        .unwrap();
1789                let input = ScanInput::builder(env.access_layer.clone(), mapper)
1790                    .with_files(files.clone())
1791                    .with_cache(strategy.clone())
1792                    .with_append_mode(false)
1793                    .with_predicate(
1794                        PredicateGroup::new(&metadata, &[col("tag_0").eq(lit("a"))]).unwrap(),
1795                    )
1796                    .with_batch_size(1)
1797                    .with_snapshot_sequence(Some(max_sequence))
1798                    .with_series_row_selector(
1799                        last_row.then_some(TimeSeriesRowSelector::LastRow { after_merge: true }),
1800                    )
1801                    .with_sequence_range(Some(SequenceRange::GtLtEq {
1802                        min: 0,
1803                        max: max_sequence,
1804                    }))
1805                    .build();
1806                let context = Arc::new(StreamContext::seq_scan_ctx(input));
1807                assert_eq!(context.ranges.len(), 1);
1808                assert_eq!(context.ranges[0].row_group_indices.len(), 2);
1809                let range = context.ranges[0].new_partition_range(0);
1810                let (_, metrics) = test_cache_context(&strategy);
1811                for run in 0..2 {
1812                    let partition = new_partition_pruner(&context, &range);
1813                    let semaphore = Arc::new(tokio::sync::Semaphore::new(1));
1814                    let batches = tokio::time::timeout(std::time::Duration::from_secs(5), async {
1815                        let mut stream = sst_range_stream(
1816                            &context,
1817                            &range,
1818                            &metrics,
1819                            partition,
1820                            Some(semaphore.clone()),
1821                        )
1822                        .await;
1823                        // The first scan at snapshot 2 has no completed cache entry.
1824                        if capacity > 0 && case == 1 && run == 0 {
1825                            assert!(build_range_cache_key(&context, &range, None).is_some());
1826                            let (_, follower_metrics) = test_cache_context(&strategy);
1827                            let follower = sst_range_stream(
1828                                &context,
1829                                &range,
1830                                &follower_metrics,
1831                                new_partition_pruner(&context, &range),
1832                                Some(semaphore.clone()),
1833                            )
1834                            .await;
1835                            let first = stream.try_next().await.unwrap().unwrap();
1836                            assert!(first.num_rows() < 4);
1837                            drop(stream);
1838                            let batches = follower.try_collect::<Vec<_>>().await.unwrap();
1839                            assert!(batches.len() > 1);
1840                            assert_eq!(batches.first().unwrap(), &first);
1841                            assert_eq!(semaphore.available_permits(), 1);
1842                            let owner: serde_json::Value =
1843                                serde_json::from_str(&format!("{metrics:?}")).unwrap();
1844                            let follower: serde_json::Value =
1845                                serde_json::from_str(&format!("{follower_metrics:?}")).unwrap();
1846                            assert!(owner["metrics"]["num_file_ranges"].as_u64().unwrap() > 0);
1847                            assert_eq!(follower["metrics"]["num_file_ranges"], 0);
1848                            batches
1849                        } else {
1850                            stream.try_collect::<Vec<_>>().await.unwrap()
1851                        }
1852                    })
1853                    .await
1854                    .unwrap();
1855                    assert!(
1856                        u64_values(&batches, SEQUENCE_COLUMN_NAME)
1857                            .iter()
1858                            .all(|sequence| *sequence == Some(max_sequence))
1859                    );
1860                    let values = u64_values(&batches, "field_0")
1861                        .into_iter()
1862                        .map(Option::unwrap)
1863                        .collect::<Vec<_>>();
1864                    let expected = if last_row {
1865                        vec![max_sequence * 100 + 3]
1866                    } else {
1867                        (0..4).map(|offset| max_sequence * 100 + offset).collect()
1868                    };
1869                    assert_eq!(values, expected);
1870                }
1871                if capacity > 1024 {
1872                    let key = build_range_cache_key(&context, &range, None).unwrap();
1873                    let value = wait_cached(&strategy, &key).await;
1874                    assert!(value.cached_batches().next().is_some());
1875                }
1876            }
1877        }
1878    }
1879
1880    fn range_cache_strategy(capacity: u64) -> CacheStrategy {
1881        CacheStrategy::EnableAll(Arc::new(
1882            CacheManager::builder()
1883                .range_result_cache_size(capacity)
1884                .build(),
1885        ))
1886    }
1887
1888    #[tokio::test]
1889    async fn test_shared_slices_compact_before_budget_fallback() {
1890        for (capacity, count) in [(64 * 1024, 16), (16 * 1024 * 1024, 128)] {
1891            let strategy = range_cache_strategy(capacity);
1892            let (key, metrics) = test_cache_context(&strategy);
1893            let backing = make_large_binary_batch(1024, 256);
1894            let mut batches: Vec<_> = (0..count).map(|i| backing.slice(i, 1)).collect();
1895            batches.insert(count / 2, backing.slice(count / 2, 0));
1896            let input = batches.clone();
1897            let (mut owner, _) =
1898                get_or_load_range_result(&strategy, key.clone(), &metrics, || async {
1899                    Ok((source_stream(input), 1))
1900                })
1901                .await
1902                .unwrap();
1903            let (slow, _) = get_or_load_range_result(&strategy, key.clone(), &metrics, || async {
1904                panic!("reader must share sliced source")
1905            })
1906            .await
1907            .unwrap();
1908            for expected in &batches[..count * 3 / 4] {
1909                assert_eq!(&owner.try_next().await.unwrap().unwrap(), expected);
1910            }
1911            let (late, _) = get_or_load_range_result(&strategy, key.clone(), &metrics, || async {
1912                panic!("compacted source must still admit readers")
1913            })
1914            .await
1915            .unwrap();
1916            drop(owner);
1917            assert_eq!(slow.try_collect::<Vec<_>>().await.unwrap(), batches);
1918            assert_eq!(late.try_collect::<Vec<_>>().await.unwrap(), batches);
1919            let value = wait_cached(&strategy, &key).await;
1920            assert!(value.estimated_size() < 64 * 1024);
1921            assert_eq!(
1922                get_or_load_range_result(&strategy, key.clone(), &metrics, || async {
1923                    panic!("cached result")
1924                })
1925                .await
1926                .unwrap()
1927                .0
1928                .try_collect::<Vec<_>>()
1929                .await
1930                .unwrap(),
1931                batches
1932            );
1933        }
1934    }
1935
1936    #[tokio::test]
1937    async fn test_failed_shared_stream_does_not_poison_a_later_request() {
1938        let pool_a: Arc<dyn MemoryPool> =
1939            Arc::new(datafusion::execution::memory_pool::UnboundedMemoryPool::default());
1940        let pool_b: Arc<dyn MemoryPool> =
1941            Arc::new(datafusion::execution::memory_pool::UnboundedMemoryPool::default());
1942        let scope_a = RangeCacheScope::for_memory_pool(&pool_a);
1943        let scope_b = RangeCacheScope::for_memory_pool(&pool_b);
1944        let strategy = test_cache_strategy();
1945        let (key, metrics) = test_cache_context(&strategy);
1946        let (mut failed, _) = get_or_load_range_result_in_scope(
1947            &strategy,
1948            key.clone(),
1949            &metrics,
1950            scope_a,
1951            || async {
1952                let source = futures::stream::iter(vec![
1953                    Ok(make_batch(&[7])),
1954                    Err(crate::error::UnexpectedSnafu {
1955                        reason: "transient source error",
1956                    }
1957                    .build()),
1958                ]);
1959                Ok((Box::pin(source) as BoxedRecordBatchStream, 3))
1960            },
1961        )
1962        .await
1963        .unwrap();
1964        // Keep a second reader alive so the failed generation remains registered.
1965        let (mut existing, _) = get_or_load_range_result_in_scope(
1966            &strategy,
1967            key.clone(),
1968            &metrics,
1969            scope_a,
1970            || async { panic!("existing reader must share the source") },
1971        )
1972        .await
1973        .unwrap();
1974        let (other_pool, _) = get_or_load_range_result_in_scope(
1975            &strategy,
1976            key.clone(),
1977            &metrics,
1978            scope_b,
1979            || async { Ok((source_stream(vec![make_batch(&[8])]), 1)) },
1980        )
1981        .await
1982        .unwrap();
1983        assert_eq!(failed.try_next().await.unwrap().unwrap(), make_batch(&[7]));
1984        let error = failed.try_next().await.unwrap_err();
1985        let crate::error::Error::LoadRangeResult { source, .. } = error else {
1986            panic!("expected shared source error, got {error:?}");
1987        };
1988        assert!(
1989            matches!(source.as_ref(), crate::error::Error::Unexpected { reason, .. }
1990            if reason == "transient source error")
1991        );
1992        let expected = make_batch(&[1, 2, 3]);
1993        let (mut retry, _) = get_or_load_range_result_in_scope(
1994            &strategy,
1995            key.clone(),
1996            &metrics,
1997            scope_a,
1998            || async {
1999                Ok((
2000                    Box::pin(futures::stream::iter([Ok(expected.clone())]))
2001                        as BoxedRecordBatchStream,
2002                    3,
2003                ))
2004            },
2005        )
2006        .await
2007        .unwrap();
2008        assert_eq!(retry.try_next().await.unwrap().unwrap(), expected);
2009        assert_eq!(
2010            existing.try_next().await.unwrap().unwrap(),
2011            make_batch(&[7])
2012        );
2013        let error = existing.try_next().await.unwrap_err();
2014        let crate::error::Error::LoadRangeResult { source, .. } = error else {
2015            panic!("expected shared source error, got {error:?}");
2016        };
2017        assert!(
2018            matches!(source.as_ref(), crate::error::Error::Unexpected { reason, .. }
2019            if reason == "transient source error")
2020        );
2021        assert_eq!(
2022            other_pool.try_collect::<Vec<_>>().await.unwrap(),
2023            vec![make_batch(&[8])]
2024        );
2025        wait_cached(&strategy, &key).await;
2026        let (cached, _) =
2027            get_or_load_range_result_in_scope(&strategy, key, &metrics, scope_a, || async {
2028                panic!("completed result must be reusable across pools")
2029            })
2030            .await
2031            .unwrap();
2032        assert_eq!(
2033            cached.try_collect::<Vec<_>>().await.unwrap(),
2034            vec![make_batch(&[8])]
2035        );
2036    }
2037
2038    #[tokio::test]
2039    async fn test_shared_ranges_build_once_before_consumers_poll() {
2040        use std::sync::atomic::{AtomicUsize, Ordering};
2041        use std::time::Duration;
2042
2043        for enabled in [false, true] {
2044            let strategy = range_cache_strategy(if enabled { 1024 * 1024 } else { 0 });
2045            let (key, metrics) = test_cache_context(&strategy);
2046            let builds = AtomicUsize::new(0);
2047            let gate = Arc::new(tokio::sync::Semaphore::new(0));
2048            let produced = Arc::new(AtomicUsize::new(0));
2049            let expected: Vec<_> = (0..32).map(|n| make_batch(&[n])).collect();
2050            let mut requests = (0..8)
2051                .map(|_| {
2052                    Box::pin(get_or_load_range_result(
2053                        &strategy,
2054                        key.clone(),
2055                        &metrics,
2056                        || async {
2057                            builds.fetch_add(1, Ordering::Relaxed);
2058                            let produced = produced.clone();
2059                            let gate = gate.clone();
2060                            let stream = Box::pin(try_stream! {
2061                                gate.acquire().await.unwrap().forget();
2062                                for n in 0..32 {
2063                                    tokio::task::yield_now().await;
2064                                    produced.fetch_add(1, Ordering::Relaxed);
2065                                    yield make_batch(&[n]);
2066                                }
2067                            });
2068                            Ok((stream as BoxedRecordBatchStream, 3))
2069                        },
2070                    ))
2071                })
2072                .collect::<Vec<_>>();
2073            // Polling all builders to completion must not require consuming any
2074            // returned stream, matching the series distributor's construction order.
2075            let streams = tokio::time::timeout(
2076                Duration::from_secs(5),
2077                futures::future::join_all(&mut requests),
2078            )
2079            .await
2080            .unwrap();
2081            assert_eq!(builds.load(Ordering::Relaxed), if enabled { 1 } else { 8 });
2082            gate.add_permits(8);
2083            let results = futures::future::join_all(
2084                streams
2085                    .into_iter()
2086                    .map(|stream| stream.unwrap().0.try_collect::<Vec<_>>()),
2087            )
2088            .await;
2089            for result in results {
2090                assert_eq!(result.unwrap(), expected);
2091            }
2092            assert_eq!(
2093                produced.load(Ordering::Relaxed),
2094                if enabled { 32 } else { 256 }
2095            );
2096        }
2097    }
2098
2099    #[tokio::test]
2100    async fn test_oversized_shared_range_keeps_existing_readers_and_declines_new_readers() {
2101        use std::sync::atomic::{AtomicUsize, Ordering};
2102
2103        let reads = Arc::new(AtomicUsize::new(0));
2104        let count = reads.clone();
2105        let strategy = range_cache_strategy(1024);
2106        let (key, metrics) = test_cache_context(&strategy);
2107        let expected = vec![
2108            make_batch(&[1]),
2109            make_batch(&[2, 3]),
2110            make_batch(&vec![5; 256]),
2111            make_batch(&[4]),
2112        ];
2113        let input = expected.clone();
2114        let (mut owner, _) = get_or_load_range_result(&strategy, key.clone(), &metrics, || async {
2115            let source = futures::stream::iter(input.into_iter().map(move |batch| {
2116                count.fetch_add(1, Ordering::Relaxed);
2117                Ok(batch)
2118            }));
2119            Ok((Box::pin(source) as BoxedRecordBatchStream, 1))
2120        })
2121        .await
2122        .unwrap();
2123        assert_eq!(reads.load(Ordering::Relaxed), 0);
2124        let limiter = strategy.range_result_cache().unwrap().retention_memory();
2125        assert_eq!(limiter.available_permits(), 1);
2126        assert!(strategy.get_range_result(&key).is_none());
2127        let (mut follower, _) =
2128            get_or_load_range_result(&strategy, key.clone(), &metrics, || async {
2129                panic!("existing follower must share")
2130            })
2131            .await
2132            .unwrap();
2133        assert_eq!(owner.try_next().await.unwrap().unwrap(), expected[0]);
2134        assert_eq!(owner.try_next().await.unwrap().unwrap(), expected[1]);
2135        assert_eq!(owner.try_next().await.unwrap().unwrap(), expected[2]);
2136        let (late, _) = get_or_load_range_result(&strategy, key.clone(), &metrics, || async {
2137            Ok((source_stream(vec![make_batch(&[9])]), 1))
2138        })
2139        .await
2140        .unwrap();
2141        assert_eq!(
2142            late.try_collect::<Vec<_>>().await.unwrap(),
2143            vec![make_batch(&[9])]
2144        );
2145        // The first two batch views share one compacted allocation reservation.
2146        assert_eq!(limiter.available_permits(), 0);
2147        assert_eq!(follower.try_next().await.unwrap().unwrap(), expected[0]);
2148        assert_eq!(limiter.available_permits(), 0);
2149        assert_eq!(follower.try_next().await.unwrap().unwrap(), expected[1]);
2150        assert_eq!(limiter.available_permits(), 1);
2151        assert_eq!(
2152            follower.try_collect::<Vec<_>>().await.unwrap(),
2153            expected[2..]
2154        );
2155        assert_eq!(
2156            owner.try_collect::<Vec<_>>().await.unwrap(),
2157            vec![make_batch(&[4])]
2158        );
2159        assert!(strategy.get_range_result(&key).is_none());
2160        assert_eq!(reads.load(Ordering::Relaxed), 4);
2161        assert_eq!(limiter.available_permits(), 1);
2162        let (retry, _) = get_or_load_range_result(&strategy, key.clone(), &metrics, || async {
2163            Ok((source_stream(vec![]), 1))
2164        })
2165        .await
2166        .unwrap();
2167        assert!(retry.try_collect::<Vec<_>>().await.unwrap().is_empty());
2168        wait_cached(&strategy, &key).await;
2169    }
2170
2171    #[tokio::test]
2172    async fn test_dropping_reader_releases_unpolled_or_pending_source() {
2173        for polled in [false, true] {
2174            let strategy = test_cache_strategy();
2175            let (key, metrics) = test_cache_context(&strategy);
2176            let source_gate = Arc::new(tokio::sync::Semaphore::new(1));
2177            let permit = source_gate.clone().acquire_owned().await.unwrap();
2178            let (mut stream, _) =
2179                get_or_load_range_result(&strategy, key.clone(), &metrics, || async {
2180                    let source = Box::pin(try_stream! {
2181                        let _permit = permit;
2182                        std::future::pending::<()>().await;
2183                        yield make_batch(&[1]);
2184                    });
2185                    Ok((source as BoxedRecordBatchStream, 1))
2186                })
2187                .await
2188                .unwrap();
2189            assert_eq!(source_gate.available_permits(), 0);
2190            if polled {
2191                let next = stream.try_next();
2192                tokio::pin!(next);
2193                assert!(futures::poll!(&mut next).is_pending());
2194            }
2195            assert_eq!(source_gate.available_permits(), 0);
2196            drop(stream);
2197            assert_eq!(source_gate.available_permits(), 1);
2198            assert!(strategy.get_range_result(&key).is_none());
2199            let (retry, _) = get_or_load_range_result(&strategy, key, &metrics, || async {
2200                Ok((source_stream(vec![make_batch(&[2])]), 1))
2201            })
2202            .await
2203            .unwrap();
2204            assert_eq!(
2205                retry.try_collect::<Vec<_>>().await.unwrap(),
2206                vec![make_batch(&[2])]
2207            );
2208        }
2209    }
2210
2211    #[tokio::test]
2212    async fn test_cancelling_initial_reader_preserves_followers_and_releases_memory() {
2213        let strategy = test_cache_strategy();
2214        let (key, metrics) = test_cache_context(&strategy);
2215        let limiter = strategy.range_result_cache().unwrap().retention_memory();
2216        let initial = limiter.available_permits();
2217        let gate = Arc::new(tokio::sync::Notify::new());
2218        let producer_gate = gate.clone();
2219        let (mut owner, _) = get_or_load_range_result(&strategy, key.clone(), &metrics, || async {
2220            let source = Box::pin(try_stream! {
2221                yield make_batch(&[1]);
2222                producer_gate.notified().await;
2223                yield make_batch(&[2]);
2224            });
2225            Ok((source as BoxedRecordBatchStream, 1))
2226        })
2227        .await
2228        .unwrap();
2229        let (mut follower, _) =
2230            get_or_load_range_result(&strategy, key.clone(), &metrics, || async {
2231                panic!("follower must share")
2232            })
2233            .await
2234            .unwrap();
2235        assert_eq!(limiter.available_permits(), initial);
2236        assert_eq!(owner.try_next().await.unwrap().unwrap(), make_batch(&[1]));
2237        assert!(limiter.available_permits() < initial);
2238        drop(owner);
2239        assert_eq!(
2240            follower.try_next().await.unwrap().unwrap(),
2241            make_batch(&[1])
2242        );
2243        gate.notify_one();
2244        assert_eq!(
2245            follower.try_next().await.unwrap().unwrap(),
2246            make_batch(&[2])
2247        );
2248        // Stop before EOF: unfinished results must not be cached.
2249        drop(follower);
2250        assert_eq!(limiter.available_permits(), initial);
2251        assert!(strategy.get_range_result(&key).is_none());
2252    }
2253
2254    #[tokio::test]
2255    async fn test_completed_stream_does_not_wait_for_cache_compaction() {
2256        let strategy = test_cache_strategy();
2257        let (key, metrics) = test_cache_context(&strategy);
2258        let limiter = strategy.range_concat_memory().unwrap();
2259        let permit = limiter
2260            .acquire(limiter.available_permits() * limiter.permit_bytes())
2261            .await
2262            .unwrap();
2263        let expected = vec![make_batch(&[1]), make_batch(&[2])];
2264        let input = expected.clone();
2265        let (stream, _) = get_or_load_range_result(&strategy, key.clone(), &metrics, || async {
2266            Ok((source_stream(input), 1))
2267        })
2268        .await
2269        .unwrap();
2270        assert_eq!(stream.try_collect::<Vec<_>>().await.unwrap(), expected);
2271        tokio::time::timeout(std::time::Duration::from_secs(5), async {
2272            while limiter.waited_acquires() == 0 {
2273                tokio::task::yield_now().await;
2274            }
2275        })
2276        .await
2277        .expect("background compaction did not reach its memory wait");
2278        assert!(strategy.get_range_result(&key).is_none());
2279        let (joined, _) = get_or_load_range_result(&strategy, key.clone(), &metrics, || async {
2280            panic!("finalizing scan must remain discoverable")
2281        })
2282        .await
2283        .unwrap();
2284        assert_eq!(joined.try_collect::<Vec<_>>().await.unwrap(), expected);
2285        drop(permit);
2286        wait_cached(&strategy, &key).await;
2287    }
2288
2289    #[tokio::test]
2290    async fn test_range_load_compacts_at_thresholds_and_keeps_tail() {
2291        let row_batch = make_batch(&vec![1; DEFAULT_READ_BATCH_SIZE / 2 + 1]);
2292        // Two batches exceed the byte threshold without reaching the row threshold.
2293        let byte_batch = make_large_binary_batch(1024, 4097);
2294        assert!(byte_batch.num_rows() * 2 <= DEFAULT_READ_BATCH_SIZE);
2295        assert!(byte_batch.get_array_memory_size() * 2 > RANGE_CACHE_COMPACT_THRESHOLD_BYTES);
2296        assert!(row_batch.get_array_memory_size() * 2 < RANGE_CACHE_COMPACT_THRESHOLD_BYTES);
2297        for batches in [
2298            vec![],
2299            vec![make_batch(&[1, 2]), make_batch(&[3]), make_batch(&[4, 5])],
2300            vec![make_batch(&[1, 2, 3])],
2301            vec![row_batch.clone(), row_batch.clone(), row_batch],
2302            vec![byte_batch.clone(), byte_batch.clone(), byte_batch],
2303        ] {
2304            let input_size = batches
2305                .iter()
2306                .map(RecordBatch::get_array_memory_size)
2307                .sum::<usize>();
2308            let strategy = range_cache_strategy((input_size * 2).max(1024 * 1024) as u64);
2309            let (key, metrics) = test_cache_context(&strategy);
2310            let input = batches.clone();
2311            let expected_batch_size = 7;
2312            let (stream, batch_size) =
2313                get_or_load_range_result(&strategy, key.clone(), &metrics, || async move {
2314                    Ok((source_stream(input), expected_batch_size))
2315                })
2316                .await
2317                .unwrap();
2318            assert_eq!(batch_size, expected_batch_size);
2319            assert_eq!(stream.try_collect::<Vec<_>>().await.unwrap(), batches);
2320            let value = wait_cached(&strategy, &key).await;
2321            let expected_lengths = if batches.is_empty() {
2322                vec![]
2323            } else if batches.iter().map(RecordBatch::num_rows).sum::<usize>() <= 5 {
2324                vec![batches.iter().map(RecordBatch::num_rows).collect()]
2325            } else {
2326                vec![
2327                    vec![batches[0].num_rows(), batches[1].num_rows()],
2328                    vec![batches[2].num_rows()],
2329                ]
2330            };
2331            assert_eq!(
2332                value
2333                    .cached_batches()
2334                    .map(|(_, lengths)| lengths.to_vec())
2335                    .collect::<Vec<_>>(),
2336                expected_lengths
2337            );
2338            assert_eq!(
2339                value.estimated_batches_size(),
2340                value
2341                    .cached_batches()
2342                    .map(|(batch, _)| batch.get_array_memory_size())
2343                    .sum::<usize>()
2344            );
2345            let (hit, batch_size) = get_or_load_range_result(&strategy, key, &metrics, || async {
2346                panic!("completed range must be reused without rebuilding");
2347            })
2348            .await
2349            .unwrap();
2350            assert_eq!(batch_size, expected_batch_size);
2351            assert_eq!(hit.try_collect::<Vec<_>>().await.unwrap(), batches);
2352        }
2353    }
2354}