Skip to main content

mito2/read/
scan_util.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 scanners.
16
17use std::collections::{BinaryHeap, HashMap, VecDeque};
18use std::fmt;
19use std::pin::Pin;
20use std::sync::{Arc, Mutex};
21use std::task::{Context, Poll};
22use std::time::{Duration, Instant};
23
24use async_stream::try_stream;
25use common_telemetry::tracing;
26use datafusion::physical_plan::metrics::{ExecutionPlanMetricsSet, MetricBuilder, Time};
27use datatypes::arrow::record_batch::RecordBatch;
28use datatypes::timestamp::timestamp_array_to_primitive;
29#[cfg(feature = "enterprise")]
30use futures::TryStreamExt;
31use futures::{Stream, StreamExt};
32use prometheus::IntGauge;
33use smallvec::SmallVec;
34use snafu::ResultExt;
35use store_api::storage::{RegionId, SequenceRange, TimeSeriesRowSelector};
36
37use crate::cache::range_cache::RangeCacheMetricsReport;
38use crate::error::{ComputeArrowSnafu, Result};
39use crate::memtable::MemScanMetrics;
40use crate::metrics::{
41    IN_PROGRESS_SCAN, PRECISE_FILTER_ROWS_TOTAL, READ_BATCHES_RETURN, READ_ROW_GROUPS_TOTAL,
42    READ_ROWS_IN_ROW_GROUP_TOTAL, READ_ROWS_RETURN, READ_STAGE_ELAPSED,
43};
44use crate::read::dedup::{DedupMetrics, DedupMetricsReport};
45use crate::read::flat_merge::{MergeMetrics, MergeMetricsReport};
46use crate::read::last_row::FlatLastTimestampSelector;
47use crate::read::pruner::PartitionPruner;
48use crate::read::range::{RangeMeta, RowGroupIndex};
49use crate::read::scan_region::StreamContext;
50use crate::read::{BoxedRecordBatchStream, ScannerMetrics};
51use crate::sst::file::{FileTimeRange, RegionFileId};
52use crate::sst::index::bloom_filter::applier::BloomFilterIndexApplyMetrics;
53use crate::sst::index::fulltext_index::applier::FulltextIndexApplyMetrics;
54use crate::sst::index::inverted_index::applier::InvertedIndexApplyMetrics;
55use crate::sst::parquet::file_range::{FileRange, PreFilterMode};
56use crate::sst::parquet::flat_format::{sequence_column_index, time_index_column_index};
57use crate::sst::parquet::json_align;
58use crate::sst::parquet::reader::{MetadataCacheMetrics, ReaderFilterMetrics, ReaderMetrics};
59use crate::sst::parquet::row_group::ParquetFetchMetrics;
60use crate::sst::parquet::{DEFAULT_READ_BATCH_SIZE, DEFAULT_ROW_GROUP_SIZE};
61
62/// Per-file scan metrics.
63#[derive(Default, Clone)]
64pub struct FileScanMetrics {
65    /// Number of ranges (row groups) read from this file.
66    pub num_ranges: usize,
67    /// Number of rows read from this file.
68    pub num_rows: usize,
69    /// Time spent building file ranges/parts (file-level preparation).
70    pub build_part_cost: Duration,
71    /// Time spent building readers for this file (accumulated across all ranges).
72    pub build_reader_cost: Duration,
73    /// Time spent scanning this file (accumulated across all ranges).
74    pub scan_cost: Duration,
75}
76
77impl fmt::Debug for FileScanMetrics {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        write!(f, "{{\"build_part_cost\":\"{:?}\"", self.build_part_cost)?;
80
81        if self.num_ranges > 0 {
82            write!(f, ", \"num_ranges\":{}", self.num_ranges)?;
83        }
84        if self.num_rows > 0 {
85            write!(f, ", \"num_rows\":{}", self.num_rows)?;
86        }
87        if !self.build_reader_cost.is_zero() {
88            write!(
89                f,
90                ", \"build_reader_cost\":\"{:?}\"",
91                self.build_reader_cost
92            )?;
93        }
94        if !self.scan_cost.is_zero() {
95            write!(f, ", \"scan_cost\":\"{:?}\"", self.scan_cost)?;
96        }
97
98        write!(f, "}}")
99    }
100}
101
102impl FileScanMetrics {
103    /// Merges another FileMetrics into this one.
104    pub(crate) fn merge_from(&mut self, other: &FileScanMetrics) {
105        self.num_ranges += other.num_ranges;
106        self.num_rows += other.num_rows;
107        self.build_part_cost += other.build_part_cost;
108        self.build_reader_cost += other.build_reader_cost;
109        self.scan_cost += other.scan_cost;
110    }
111}
112
113/// Verbose scan metrics for a partition.
114#[derive(Default)]
115pub(crate) struct ScanMetricsSet {
116    /// Duration to prepare the scan task.
117    prepare_scan_cost: Duration,
118    /// Duration to build the (merge) reader.
119    build_reader_cost: Duration,
120    /// Duration to scan data.
121    scan_cost: Duration,
122    /// Duration while waiting for `yield`.
123    yield_cost: Duration,
124    /// Duration to convert [`Batch`]es.
125    convert_cost: Option<Time>,
126    /// Duration of the scan.
127    total_cost: Duration,
128    /// Number of rows returned.
129    num_rows: usize,
130    /// Number of batches returned.
131    num_batches: usize,
132    /// Number of mem ranges scanned.
133    num_mem_ranges: usize,
134    /// Number of file ranges scanned.
135    num_file_ranges: usize,
136
137    // Memtable related metrics:
138    /// Duration to scan memtables.
139    mem_scan_cost: Duration,
140    /// Number of rows read from memtables.
141    mem_rows: usize,
142    /// Number of batches read from memtables.
143    mem_batches: usize,
144    /// Number of series read from memtables.
145    mem_series: usize,
146    /// Duration of prefilter in memtable scan.
147    mem_prefilter_cost: Duration,
148    /// Number of rows filtered by prefilter in memtable scan.
149    mem_prefilter_rows_filtered: usize,
150
151    // SST related metrics:
152    /// Duration to build file ranges.
153    build_parts_cost: Duration,
154    /// Duration to scan SST files.
155    sst_scan_cost: Duration,
156    /// Number of row groups before filtering.
157    rg_total: usize,
158    /// Number of row groups filtered by fulltext index.
159    rg_fulltext_filtered: usize,
160    /// Number of row groups filtered by inverted index.
161    rg_inverted_filtered: usize,
162    /// Number of row groups filtered by min-max index.
163    rg_minmax_filtered: usize,
164    /// Number of row groups filtered by bloom filter index.
165    rg_bloom_filtered: usize,
166    /// Number of rows in row group before filtering.
167    rows_before_filter: usize,
168    /// Number of rows in row group filtered by fulltext index.
169    rows_fulltext_filtered: usize,
170    /// Number of rows in row group filtered by inverted index.
171    rows_inverted_filtered: usize,
172    /// Number of rows in row group filtered by bloom filter index.
173    rows_bloom_filtered: usize,
174    /// Number of rows filtered by precise filter.
175    rows_precise_filtered: usize,
176    /// Number of index result cache hits for fulltext index.
177    fulltext_index_cache_hit: usize,
178    /// Number of index result cache misses for fulltext index.
179    fulltext_index_cache_miss: usize,
180    /// Number of index result cache hits for inverted index.
181    inverted_index_cache_hit: usize,
182    /// Number of index result cache misses for inverted index.
183    inverted_index_cache_miss: usize,
184    /// Number of index result cache hits for bloom filter index.
185    bloom_filter_cache_hit: usize,
186    /// Number of index result cache misses for bloom filter index.
187    bloom_filter_cache_miss: usize,
188    /// Number of index result cache hits for minmax pruning.
189    minmax_cache_hit: usize,
190    /// Number of index result cache misses for minmax pruning.
191    minmax_cache_miss: usize,
192    /// Number of pruner builder cache hits.
193    pruner_cache_hit: usize,
194    /// Number of pruner builder cache misses.
195    pruner_cache_miss: usize,
196    /// Duration spent waiting for pruner to build file ranges.
197    pruner_prune_cost: Duration,
198    /// Number of files filtered by manifest time-range pruning.
199    files_time_range_pruned: usize,
200    /// Number of record batches read from SST.
201    num_sst_record_batches: usize,
202    /// Number of batches decoded from SST.
203    num_sst_batches: usize,
204    /// Number of rows read from SST.
205    num_sst_rows: usize,
206
207    /// Elapsed time before the first poll operation.
208    first_poll: Duration,
209
210    /// Number of send full in SeriesScan.
211    num_series_send_full: usize,
212    /// Number of rows the series distributor scanned.
213    num_distributor_rows: usize,
214    /// Number of batches the series distributor scanned.
215    num_distributor_batches: usize,
216    /// Duration of the series distributor to scan.
217    distributor_scan_cost: Duration,
218    /// Duration of the series distributor to yield.
219    distributor_yield_cost: Duration,
220    /// Duration spent in divider operations.
221    distributor_divider_cost: Duration,
222
223    /// Merge metrics.
224    merge_metrics: MergeMetrics,
225    /// Dedup metrics.
226    dedup_metrics: DedupMetrics,
227
228    /// The stream reached EOF
229    stream_eof: bool,
230
231    // Optional verbose metrics:
232    /// Inverted index apply metrics.
233    inverted_index_apply_metrics: Option<InvertedIndexApplyMetrics>,
234    /// Bloom filter index apply metrics.
235    bloom_filter_apply_metrics: Option<BloomFilterIndexApplyMetrics>,
236    /// Fulltext index apply metrics.
237    fulltext_index_apply_metrics: Option<FulltextIndexApplyMetrics>,
238    /// Parquet fetch metrics.
239    fetch_metrics: Option<ParquetFetchMetrics>,
240    /// Metadata cache metrics.
241    metadata_cache_metrics: Option<MetadataCacheMetrics>,
242    /// Per-file scan metrics, only populated when explain_verbose is true.
243    per_file_metrics: Option<HashMap<RegionFileId, FileScanMetrics>>,
244
245    /// Current memory usage for file range builders.
246    build_ranges_mem_size: isize,
247    /// Peak memory usage for file range builders.
248    build_ranges_peak_mem_size: isize,
249    /// Current number of file range builders.
250    num_range_builders: isize,
251    /// Peak number of file range builders.
252    num_peak_range_builders: isize,
253    /// Total bytes added to the range cache during this scan.
254    range_cache_size: usize,
255    /// Number of range cache hits during this scan.
256    range_cache_hit: usize,
257    /// Number of range cache misses during this scan.
258    range_cache_miss: usize,
259}
260
261/// Wrapper for file metrics that compares by total cost in reverse order.
262/// This allows using BinaryHeap as a min-heap for efficient top-K selection.
263struct CompareCostReverse<'a> {
264    total_cost: Duration,
265    file_id: RegionFileId,
266    metrics: &'a FileScanMetrics,
267}
268
269impl Ord for CompareCostReverse<'_> {
270    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
271        // Reverse comparison: smaller costs are "greater"
272        other.total_cost.cmp(&self.total_cost)
273    }
274}
275
276impl PartialOrd for CompareCostReverse<'_> {
277    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
278        Some(self.cmp(other))
279    }
280}
281
282impl Eq for CompareCostReverse<'_> {}
283
284impl PartialEq for CompareCostReverse<'_> {
285    fn eq(&self, other: &Self) -> bool {
286        self.total_cost == other.total_cost
287    }
288}
289
290impl fmt::Debug for ScanMetricsSet {
291    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292        let ScanMetricsSet {
293            prepare_scan_cost,
294            build_reader_cost,
295            scan_cost,
296            yield_cost,
297            convert_cost,
298            total_cost,
299            num_rows,
300            num_batches,
301            num_mem_ranges,
302            num_file_ranges,
303            build_parts_cost,
304            sst_scan_cost,
305            rg_total,
306            rg_fulltext_filtered,
307            rg_inverted_filtered,
308            rg_minmax_filtered,
309            rg_bloom_filtered,
310            rows_before_filter,
311            rows_fulltext_filtered,
312            rows_inverted_filtered,
313            rows_bloom_filtered,
314            rows_precise_filtered,
315            fulltext_index_cache_hit,
316            fulltext_index_cache_miss,
317            inverted_index_cache_hit,
318            inverted_index_cache_miss,
319            bloom_filter_cache_hit,
320            bloom_filter_cache_miss,
321            minmax_cache_hit,
322            minmax_cache_miss,
323            pruner_cache_hit,
324            pruner_cache_miss,
325            pruner_prune_cost,
326            files_time_range_pruned,
327            num_sst_record_batches,
328            num_sst_batches,
329            num_sst_rows,
330            first_poll,
331            num_series_send_full,
332            num_distributor_rows,
333            num_distributor_batches,
334            distributor_scan_cost,
335            distributor_yield_cost,
336            distributor_divider_cost,
337            merge_metrics,
338            dedup_metrics,
339            stream_eof,
340            mem_scan_cost,
341            mem_rows,
342            mem_batches,
343            mem_series,
344            mem_prefilter_cost,
345            mem_prefilter_rows_filtered,
346            inverted_index_apply_metrics,
347            bloom_filter_apply_metrics,
348            fulltext_index_apply_metrics,
349            fetch_metrics,
350            metadata_cache_metrics,
351            per_file_metrics,
352            build_ranges_mem_size: _,
353            build_ranges_peak_mem_size,
354            num_range_builders: _,
355            num_peak_range_builders,
356            range_cache_size,
357            range_cache_hit,
358            range_cache_miss,
359        } = self;
360
361        // Write core metrics
362        write!(
363            f,
364            "{{\"prepare_scan_cost\":\"{prepare_scan_cost:?}\", \
365            \"build_reader_cost\":\"{build_reader_cost:?}\", \
366            \"scan_cost\":\"{scan_cost:?}\", \
367            \"yield_cost\":\"{yield_cost:?}\", \
368            \"total_cost\":\"{total_cost:?}\", \
369            \"num_rows\":{num_rows}, \
370            \"num_batches\":{num_batches}, \
371            \"num_mem_ranges\":{num_mem_ranges}, \
372            \"num_file_ranges\":{num_file_ranges}, \
373            \"build_parts_cost\":\"{build_parts_cost:?}\", \
374            \"sst_scan_cost\":\"{sst_scan_cost:?}\", \
375            \"rg_total\":{rg_total}, \
376            \"rows_before_filter\":{rows_before_filter}, \
377            \"num_sst_record_batches\":{num_sst_record_batches}, \
378            \"num_sst_batches\":{num_sst_batches}, \
379            \"num_sst_rows\":{num_sst_rows}, \
380            \"first_poll\":\"{first_poll:?}\""
381        )?;
382
383        // Write convert_cost if present
384        if let Some(time) = convert_cost {
385            let duration = Duration::from_nanos(time.value() as u64);
386            write!(f, ", \"convert_cost\":\"{duration:?}\"")?;
387        }
388
389        // Write non-zero filter counters
390        if *files_time_range_pruned > 0 {
391            write!(f, ", \"files_time_range_pruned\":{files_time_range_pruned}")?;
392        }
393        if *rg_fulltext_filtered > 0 {
394            write!(f, ", \"rg_fulltext_filtered\":{rg_fulltext_filtered}")?;
395        }
396        if *rg_inverted_filtered > 0 {
397            write!(f, ", \"rg_inverted_filtered\":{rg_inverted_filtered}")?;
398        }
399        if *rg_minmax_filtered > 0 {
400            write!(f, ", \"rg_minmax_filtered\":{rg_minmax_filtered}")?;
401        }
402        if *rg_bloom_filtered > 0 {
403            write!(f, ", \"rg_bloom_filtered\":{rg_bloom_filtered}")?;
404        }
405        if *rows_fulltext_filtered > 0 {
406            write!(f, ", \"rows_fulltext_filtered\":{rows_fulltext_filtered}")?;
407        }
408        if *rows_inverted_filtered > 0 {
409            write!(f, ", \"rows_inverted_filtered\":{rows_inverted_filtered}")?;
410        }
411        if *rows_bloom_filtered > 0 {
412            write!(f, ", \"rows_bloom_filtered\":{rows_bloom_filtered}")?;
413        }
414        if *rows_precise_filtered > 0 {
415            write!(f, ", \"rows_precise_filtered\":{rows_precise_filtered}")?;
416        }
417        if *fulltext_index_cache_hit > 0 {
418            write!(
419                f,
420                ", \"fulltext_index_cache_hit\":{fulltext_index_cache_hit}"
421            )?;
422        }
423        if *fulltext_index_cache_miss > 0 {
424            write!(
425                f,
426                ", \"fulltext_index_cache_miss\":{fulltext_index_cache_miss}"
427            )?;
428        }
429        if *inverted_index_cache_hit > 0 {
430            write!(
431                f,
432                ", \"inverted_index_cache_hit\":{inverted_index_cache_hit}"
433            )?;
434        }
435        if *inverted_index_cache_miss > 0 {
436            write!(
437                f,
438                ", \"inverted_index_cache_miss\":{inverted_index_cache_miss}"
439            )?;
440        }
441        if *bloom_filter_cache_hit > 0 {
442            write!(f, ", \"bloom_filter_cache_hit\":{bloom_filter_cache_hit}")?;
443        }
444        if *bloom_filter_cache_miss > 0 {
445            write!(f, ", \"bloom_filter_cache_miss\":{bloom_filter_cache_miss}")?;
446        }
447        if *minmax_cache_hit > 0 {
448            write!(f, ", \"minmax_cache_hit\":{minmax_cache_hit}")?;
449        }
450        if *minmax_cache_miss > 0 {
451            write!(f, ", \"minmax_cache_miss\":{minmax_cache_miss}")?;
452        }
453        if *pruner_cache_hit > 0 {
454            write!(f, ", \"pruner_cache_hit\":{pruner_cache_hit}")?;
455        }
456        if *pruner_cache_miss > 0 {
457            write!(f, ", \"pruner_cache_miss\":{pruner_cache_miss}")?;
458        }
459        if !pruner_prune_cost.is_zero() {
460            write!(f, ", \"pruner_prune_cost\":\"{pruner_prune_cost:?}\"")?;
461        }
462
463        // Write non-zero distributor metrics
464        if *num_series_send_full > 0 {
465            write!(f, ", \"num_series_send_full\":{num_series_send_full}")?;
466        }
467        if *num_distributor_rows > 0 {
468            write!(f, ", \"num_distributor_rows\":{num_distributor_rows}")?;
469        }
470        if *num_distributor_batches > 0 {
471            write!(f, ", \"num_distributor_batches\":{num_distributor_batches}")?;
472        }
473        if !distributor_scan_cost.is_zero() {
474            write!(
475                f,
476                ", \"distributor_scan_cost\":\"{distributor_scan_cost:?}\""
477            )?;
478        }
479        if !distributor_yield_cost.is_zero() {
480            write!(
481                f,
482                ", \"distributor_yield_cost\":\"{distributor_yield_cost:?}\""
483            )?;
484        }
485        if !distributor_divider_cost.is_zero() {
486            write!(
487                f,
488                ", \"distributor_divider_cost\":\"{distributor_divider_cost:?}\""
489            )?;
490        }
491
492        // Write non-zero memtable metrics
493        if *mem_rows > 0 {
494            write!(f, ", \"mem_rows\":{mem_rows}")?;
495        }
496        if *mem_batches > 0 {
497            write!(f, ", \"mem_batches\":{mem_batches}")?;
498        }
499        if *mem_series > 0 {
500            write!(f, ", \"mem_series\":{mem_series}")?;
501        }
502        if !mem_scan_cost.is_zero() {
503            write!(f, ", \"mem_scan_cost\":\"{mem_scan_cost:?}\"")?;
504        }
505        if !mem_prefilter_cost.is_zero() {
506            write!(f, ", \"mem_prefilter_cost\":\"{mem_prefilter_cost:?}\"")?;
507        }
508        if *mem_prefilter_rows_filtered > 0 {
509            write!(
510                f,
511                ", \"mem_prefilter_rows_filtered\":{mem_prefilter_rows_filtered}"
512            )?;
513        }
514
515        // Write optional verbose metrics if they are not empty
516        if let Some(metrics) = inverted_index_apply_metrics
517            && !metrics.is_empty()
518        {
519            write!(f, ", \"inverted_index_apply_metrics\":{:?}", metrics)?;
520        }
521        if let Some(metrics) = bloom_filter_apply_metrics
522            && !metrics.is_empty()
523        {
524            write!(f, ", \"bloom_filter_apply_metrics\":{:?}", metrics)?;
525        }
526        if let Some(metrics) = fulltext_index_apply_metrics
527            && !metrics.is_empty()
528        {
529            write!(f, ", \"fulltext_index_apply_metrics\":{:?}", metrics)?;
530        }
531        if let Some(metrics) = fetch_metrics
532            && !metrics.is_empty()
533        {
534            write!(f, ", \"fetch_metrics\":{:?}", metrics)?;
535        }
536        if let Some(metrics) = metadata_cache_metrics
537            && !metrics.is_empty()
538        {
539            write!(f, ", \"metadata_cache_metrics\":{:?}", metrics)?;
540        }
541
542        // Write merge metrics if not empty
543        if !merge_metrics.scan_cost.is_zero() {
544            write!(f, ", \"merge_metrics\":{:?}", merge_metrics)?;
545        }
546
547        // Write dedup metrics if not empty
548        if !dedup_metrics.dedup_cost.is_zero() {
549            write!(f, ", \"dedup_metrics\":{:?}", dedup_metrics)?;
550        }
551
552        // Write top file metrics if present and non-empty
553        if let Some(file_metrics) = per_file_metrics
554            && !file_metrics.is_empty()
555        {
556            // Use min-heap (BinaryHeap with reverse comparison) to keep only top 10
557            let mut heap = BinaryHeap::new();
558            for (file_id, metrics) in file_metrics.iter() {
559                let total_cost =
560                    metrics.build_part_cost + metrics.build_reader_cost + metrics.scan_cost;
561
562                // If the file has been pruned by a pruner, the build part cost may be zero.
563                // If we didn't read any ranges from it, we don't output the file.
564                if total_cost.is_zero() && metrics.num_ranges == 0 {
565                    continue;
566                }
567
568                if heap.len() < 10 {
569                    // Haven't reached 10 yet, just push
570                    heap.push(CompareCostReverse {
571                        total_cost,
572                        file_id: *file_id,
573                        metrics,
574                    });
575                } else if let Some(min_entry) = heap.peek() {
576                    // If current cost is higher than the minimum in our top-10, replace it
577                    if total_cost > min_entry.total_cost {
578                        heap.pop();
579                        heap.push(CompareCostReverse {
580                            total_cost,
581                            file_id: *file_id,
582                            metrics,
583                        });
584                    }
585                }
586            }
587
588            let top_files = heap.into_sorted_vec();
589            write!(f, ", \"top_file_metrics\": {{")?;
590            for (i, item) in top_files.iter().enumerate() {
591                let CompareCostReverse {
592                    total_cost: _,
593                    file_id,
594                    metrics,
595                } = item;
596                if i > 0 {
597                    write!(f, ", ")?;
598                }
599                write!(f, "\"{}\": {:?}", file_id, metrics)?;
600            }
601            write!(f, "}}")?;
602        }
603
604        if *range_cache_size > 0 {
605            write!(f, ", \"range_cache_size\":{range_cache_size}")?;
606        }
607        if *range_cache_hit > 0 {
608            write!(f, ", \"range_cache_hit\":{range_cache_hit}")?;
609        }
610        if *range_cache_miss > 0 {
611            write!(f, ", \"range_cache_miss\":{range_cache_miss}")?;
612        }
613
614        write!(
615            f,
616            ", \"build_ranges_peak_mem_size\":{build_ranges_peak_mem_size}, \
617             \"num_peak_range_builders\":{num_peak_range_builders}, \
618             \"stream_eof\":{stream_eof}}}"
619        )
620    }
621}
622impl ScanMetricsSet {
623    /// Attaches the `prepare_scan_cost` to the metrics set.
624    fn with_prepare_scan_cost(mut self, cost: Duration) -> Self {
625        self.prepare_scan_cost += cost;
626        self
627    }
628
629    /// Attaches the `convert_cost` to the metrics set.
630    fn with_convert_cost(mut self, time: Time) -> Self {
631        self.convert_cost = Some(time);
632        self
633    }
634
635    /// Merges the local scanner metrics.
636    fn merge_scanner_metrics(&mut self, other: &ScannerMetrics) {
637        let ScannerMetrics {
638            scan_cost,
639            yield_cost,
640            num_batches,
641            num_rows,
642        } = other;
643
644        self.scan_cost += *scan_cost;
645        self.yield_cost += *yield_cost;
646        self.num_rows += *num_rows;
647        self.num_batches += *num_batches;
648    }
649
650    /// Merges the local reader metrics.
651    fn merge_reader_metrics(&mut self, other: &ReaderMetrics) {
652        let ReaderMetrics {
653            build_cost,
654            filter_metrics:
655                ReaderFilterMetrics {
656                    rg_total,
657                    rg_fulltext_filtered,
658                    rg_inverted_filtered,
659                    rg_minmax_filtered,
660                    rg_bloom_filtered,
661                    rows_total,
662                    rows_fulltext_filtered,
663                    rows_inverted_filtered,
664                    rows_bloom_filtered,
665                    rows_precise_filtered,
666                    fulltext_index_cache_hit,
667                    fulltext_index_cache_miss,
668                    inverted_index_cache_hit,
669                    inverted_index_cache_miss,
670                    bloom_filter_cache_hit,
671                    bloom_filter_cache_miss,
672                    minmax_cache_hit,
673                    minmax_cache_miss,
674                    pruner_cache_hit,
675                    pruner_cache_miss,
676                    pruner_prune_cost,
677                    files_time_range_pruned,
678                    inverted_index_apply_metrics,
679                    bloom_filter_apply_metrics,
680                    fulltext_index_apply_metrics,
681                },
682            num_record_batches,
683            num_batches,
684            num_rows,
685            scan_cost,
686            metadata_cache_metrics,
687            fetch_metrics,
688            metadata_mem_size,
689            num_range_builders,
690        } = other;
691
692        self.build_parts_cost += *build_cost;
693        self.sst_scan_cost += *scan_cost;
694
695        self.files_time_range_pruned += *files_time_range_pruned;
696
697        self.rg_total += *rg_total;
698        self.rg_fulltext_filtered += *rg_fulltext_filtered;
699        self.rg_inverted_filtered += *rg_inverted_filtered;
700        self.rg_minmax_filtered += *rg_minmax_filtered;
701        self.rg_bloom_filtered += *rg_bloom_filtered;
702
703        self.rows_before_filter += *rows_total;
704        self.rows_fulltext_filtered += *rows_fulltext_filtered;
705        self.rows_inverted_filtered += *rows_inverted_filtered;
706        self.rows_bloom_filtered += *rows_bloom_filtered;
707        self.rows_precise_filtered += *rows_precise_filtered;
708
709        self.fulltext_index_cache_hit += *fulltext_index_cache_hit;
710        self.fulltext_index_cache_miss += *fulltext_index_cache_miss;
711        self.inverted_index_cache_hit += *inverted_index_cache_hit;
712        self.inverted_index_cache_miss += *inverted_index_cache_miss;
713        self.bloom_filter_cache_hit += *bloom_filter_cache_hit;
714        self.bloom_filter_cache_miss += *bloom_filter_cache_miss;
715        self.minmax_cache_hit += *minmax_cache_hit;
716        self.minmax_cache_miss += *minmax_cache_miss;
717        self.pruner_cache_hit += *pruner_cache_hit;
718        self.pruner_cache_miss += *pruner_cache_miss;
719        self.pruner_prune_cost += *pruner_prune_cost;
720
721        self.num_sst_record_batches += *num_record_batches;
722        self.num_sst_batches += *num_batches;
723        self.num_sst_rows += *num_rows;
724
725        // Merge optional verbose metrics
726        if let Some(metrics) = inverted_index_apply_metrics {
727            self.inverted_index_apply_metrics
728                .get_or_insert_with(InvertedIndexApplyMetrics::default)
729                .merge_from(metrics);
730        }
731        if let Some(metrics) = bloom_filter_apply_metrics {
732            self.bloom_filter_apply_metrics
733                .get_or_insert_with(BloomFilterIndexApplyMetrics::default)
734                .merge_from(metrics);
735        }
736        if let Some(metrics) = fulltext_index_apply_metrics {
737            self.fulltext_index_apply_metrics
738                .get_or_insert_with(FulltextIndexApplyMetrics::default)
739                .merge_from(metrics);
740        }
741        if let Some(metrics) = fetch_metrics {
742            self.fetch_metrics
743                .get_or_insert_with(ParquetFetchMetrics::default)
744                .merge_from(metrics);
745        }
746        self.metadata_cache_metrics
747            .get_or_insert_with(MetadataCacheMetrics::default)
748            .merge_from(metadata_cache_metrics);
749
750        // Track memory usage and update peak.
751        self.build_ranges_mem_size += *metadata_mem_size;
752        if self.build_ranges_mem_size > self.build_ranges_peak_mem_size {
753            self.build_ranges_peak_mem_size = self.build_ranges_mem_size;
754        }
755
756        // Track number of builders and update peak.
757        self.num_range_builders += *num_range_builders;
758        if self.num_range_builders > self.num_peak_range_builders {
759            self.num_peak_range_builders = self.num_range_builders;
760        }
761    }
762
763    /// Merges per-file metrics.
764    fn merge_per_file_metrics(&mut self, other: &HashMap<RegionFileId, FileScanMetrics>) {
765        let self_file_metrics = self.per_file_metrics.get_or_insert_with(HashMap::new);
766        for (file_id, metrics) in other {
767            self_file_metrics
768                .entry(*file_id)
769                .or_default()
770                .merge_from(metrics);
771        }
772    }
773
774    /// Sets distributor metrics.
775    fn set_distributor_metrics(&mut self, distributor_metrics: &SeriesDistributorMetrics) {
776        let SeriesDistributorMetrics {
777            num_series_send_full,
778            num_rows,
779            num_batches,
780            scan_cost,
781            yield_cost,
782            divider_cost,
783        } = distributor_metrics;
784
785        self.num_series_send_full += *num_series_send_full;
786        self.num_distributor_rows += *num_rows;
787        self.num_distributor_batches += *num_batches;
788        self.distributor_scan_cost += *scan_cost;
789        self.distributor_yield_cost += *yield_cost;
790        self.distributor_divider_cost += *divider_cost;
791    }
792
793    /// Observes metrics.
794    fn observe_metrics(&self) {
795        READ_STAGE_ELAPSED
796            .with_label_values(&["prepare_scan"])
797            .observe(self.prepare_scan_cost.as_secs_f64());
798        READ_STAGE_ELAPSED
799            .with_label_values(&["build_reader"])
800            .observe(self.build_reader_cost.as_secs_f64());
801        READ_STAGE_ELAPSED
802            .with_label_values(&["scan"])
803            .observe(self.scan_cost.as_secs_f64());
804        READ_STAGE_ELAPSED
805            .with_label_values(&["yield"])
806            .observe(self.yield_cost.as_secs_f64());
807        if let Some(time) = &self.convert_cost {
808            READ_STAGE_ELAPSED
809                .with_label_values(&["convert"])
810                .observe(Duration::from_nanos(time.value() as u64).as_secs_f64());
811        }
812        READ_STAGE_ELAPSED
813            .with_label_values(&["total"])
814            .observe(self.total_cost.as_secs_f64());
815        READ_ROWS_RETURN.observe(self.num_rows as f64);
816        READ_BATCHES_RETURN.observe(self.num_batches as f64);
817
818        READ_STAGE_ELAPSED
819            .with_label_values(&["build_parts"])
820            .observe(self.build_parts_cost.as_secs_f64());
821
822        READ_ROW_GROUPS_TOTAL
823            .with_label_values(&["before_filtering"])
824            .inc_by(self.rg_total as u64);
825        READ_ROW_GROUPS_TOTAL
826            .with_label_values(&["fulltext_index_filtered"])
827            .inc_by(self.rg_fulltext_filtered as u64);
828        READ_ROW_GROUPS_TOTAL
829            .with_label_values(&["inverted_index_filtered"])
830            .inc_by(self.rg_inverted_filtered as u64);
831        READ_ROW_GROUPS_TOTAL
832            .with_label_values(&["minmax_index_filtered"])
833            .inc_by(self.rg_minmax_filtered as u64);
834        READ_ROW_GROUPS_TOTAL
835            .with_label_values(&["bloom_filter_index_filtered"])
836            .inc_by(self.rg_bloom_filtered as u64);
837
838        PRECISE_FILTER_ROWS_TOTAL
839            .with_label_values(&["parquet"])
840            .inc_by(self.rows_precise_filtered as u64);
841        READ_ROWS_IN_ROW_GROUP_TOTAL
842            .with_label_values(&["before_filtering"])
843            .inc_by(self.rows_before_filter as u64);
844        READ_ROWS_IN_ROW_GROUP_TOTAL
845            .with_label_values(&["fulltext_index_filtered"])
846            .inc_by(self.rows_fulltext_filtered as u64);
847        READ_ROWS_IN_ROW_GROUP_TOTAL
848            .with_label_values(&["inverted_index_filtered"])
849            .inc_by(self.rows_inverted_filtered as u64);
850        READ_ROWS_IN_ROW_GROUP_TOTAL
851            .with_label_values(&["bloom_filter_index_filtered"])
852            .inc_by(self.rows_bloom_filtered as u64);
853    }
854}
855
856struct PartitionMetricsInner {
857    region_id: RegionId,
858    /// Index of the partition to scan.
859    partition: usize,
860    /// Label to distinguish different scan operation.
861    scanner_type: &'static str,
862    /// Query start time.
863    query_start: Instant,
864    /// Whether to use verbose logging.
865    explain_verbose: bool,
866    /// Verbose scan metrics that only log to debug logs by default.
867    metrics: Mutex<ScanMetricsSet>,
868    in_progress_scan: IntGauge,
869
870    // Normal metrics that always report to the [ExecutionPlanMetricsSet]:
871    /// Duration to build file ranges.
872    build_parts_cost: Time,
873    /// Duration to build the (merge) reader.
874    build_reader_cost: Time,
875    /// Duration to scan data.
876    scan_cost: Time,
877    /// Duration while waiting for `yield`.
878    yield_cost: Time,
879    /// Duration to convert [`Batch`]es.
880    convert_cost: Time,
881    /// Aggregated compute time reported to DataFusion.
882    elapsed_compute: Time,
883}
884
885impl PartitionMetricsInner {
886    fn on_finish(&self, stream_eof: bool) {
887        let mut metrics = self.metrics.lock().unwrap();
888        if metrics.total_cost.is_zero() {
889            metrics.total_cost = self.query_start.elapsed();
890        }
891        if !metrics.stream_eof {
892            metrics.stream_eof = stream_eof;
893        }
894    }
895}
896
897impl MergeMetricsReport for PartitionMetricsInner {
898    fn report(&self, metrics: &mut MergeMetrics) {
899        let mut scan_metrics = self.metrics.lock().unwrap();
900        // Merge the metrics into scan_metrics
901        scan_metrics.merge_metrics.merge(metrics);
902
903        // Reset the input metrics
904        *metrics = MergeMetrics::default();
905    }
906}
907
908impl DedupMetricsReport for PartitionMetricsInner {
909    fn report(&self, metrics: &mut DedupMetrics) {
910        let mut scan_metrics = self.metrics.lock().unwrap();
911        // Merge the metrics into scan_metrics
912        scan_metrics.dedup_metrics.merge(metrics);
913
914        // Reset the input metrics
915        *metrics = DedupMetrics::default();
916    }
917}
918
919impl RangeCacheMetricsReport for PartitionMetricsInner {
920    /// Increments the total bytes added to the range cache.
921    fn inc_range_cache_size(&self, size: usize) {
922        let mut metrics = self.metrics.lock().unwrap();
923        metrics.range_cache_size += size;
924    }
925
926    /// Increments the range cache hit counter.
927    fn inc_range_cache_hit(&self) {
928        let mut metrics = self.metrics.lock().unwrap();
929        metrics.range_cache_hit += 1;
930    }
931
932    /// Increments the range cache miss counter.
933    fn inc_range_cache_miss(&self) {
934        let mut metrics = self.metrics.lock().unwrap();
935        metrics.range_cache_miss += 1;
936    }
937}
938
939impl Drop for PartitionMetricsInner {
940    fn drop(&mut self) {
941        self.on_finish(false);
942        let metrics = self.metrics.lock().unwrap();
943        metrics.observe_metrics();
944        self.in_progress_scan.dec();
945
946        if self.explain_verbose {
947            common_telemetry::info!(
948                "{} finished, region_id: {}, partition: {}, scan_metrics: {:?}",
949                self.scanner_type,
950                self.region_id,
951                self.partition,
952                metrics,
953            );
954        } else {
955            common_telemetry::debug!(
956                "{} finished, region_id: {}, partition: {}, scan_metrics: {:?}",
957                self.scanner_type,
958                self.region_id,
959                self.partition,
960                metrics,
961            );
962        }
963    }
964}
965
966/// List of PartitionMetrics.
967#[derive(Default)]
968pub(crate) struct PartitionMetricsList(Mutex<Vec<Option<PartitionMetrics>>>);
969
970impl PartitionMetricsList {
971    /// Sets a new [PartitionMetrics] at the specified partition.
972    pub(crate) fn set(&self, partition: usize, metrics: PartitionMetrics) {
973        let mut list = self.0.lock().unwrap();
974        if list.len() <= partition {
975            list.resize(partition + 1, None);
976        }
977        list[partition] = Some(metrics);
978    }
979
980    /// Format verbose metrics for each partition for explain.
981    pub(crate) fn format_verbose_metrics(&self, f: &mut fmt::Formatter) -> fmt::Result {
982        let list = self.0.lock().unwrap();
983        write!(f, ", \"metrics_per_partition\": ")?;
984        f.debug_list()
985            .entries(list.iter().filter_map(|p| p.as_ref()))
986            .finish()?;
987        write!(f, "}}")
988    }
989}
990
991/// Metrics while reading a partition.
992#[derive(Clone)]
993pub struct PartitionMetrics(Arc<PartitionMetricsInner>);
994
995impl PartitionMetrics {
996    pub(crate) fn new(
997        region_id: RegionId,
998        partition: usize,
999        scanner_type: &'static str,
1000        query_start: Instant,
1001        explain_verbose: bool,
1002        metrics_set: &ExecutionPlanMetricsSet,
1003    ) -> Self {
1004        let partition_str = partition.to_string();
1005        let in_progress_scan = IN_PROGRESS_SCAN.with_label_values(&[scanner_type, &partition_str]);
1006        in_progress_scan.inc();
1007        let convert_cost = MetricBuilder::new(metrics_set).subset_time("convert_cost", partition);
1008        let metrics = ScanMetricsSet::default()
1009            .with_prepare_scan_cost(query_start.elapsed())
1010            .with_convert_cost(convert_cost.clone());
1011        let inner = PartitionMetricsInner {
1012            region_id,
1013            partition,
1014            scanner_type,
1015            query_start,
1016            explain_verbose,
1017            metrics: Mutex::new(metrics),
1018            in_progress_scan,
1019            build_parts_cost: MetricBuilder::new(metrics_set)
1020                .subset_time("build_parts_cost", partition),
1021            build_reader_cost: MetricBuilder::new(metrics_set)
1022                .subset_time("build_reader_cost", partition),
1023            scan_cost: MetricBuilder::new(metrics_set).subset_time("scan_cost", partition),
1024            yield_cost: MetricBuilder::new(metrics_set).subset_time("yield_cost", partition),
1025            convert_cost,
1026            elapsed_compute: MetricBuilder::new(metrics_set).elapsed_compute(partition),
1027        };
1028        Self(Arc::new(inner))
1029    }
1030
1031    pub(crate) fn on_first_poll(&self) {
1032        let mut metrics = self.0.metrics.lock().unwrap();
1033        metrics.first_poll = self.0.query_start.elapsed();
1034    }
1035
1036    pub(crate) fn inc_num_mem_ranges(&self, num: usize) {
1037        let mut metrics = self.0.metrics.lock().unwrap();
1038        metrics.num_mem_ranges += num;
1039    }
1040
1041    pub fn inc_num_file_ranges(&self, num: usize) {
1042        let mut metrics = self.0.metrics.lock().unwrap();
1043        metrics.num_file_ranges += num;
1044    }
1045
1046    fn record_elapsed_compute(&self, duration: Duration) {
1047        if duration.is_zero() {
1048            return;
1049        }
1050        self.0.elapsed_compute.add_duration(duration);
1051    }
1052
1053    /// Merges `build_reader_cost`.
1054    pub(crate) fn inc_build_reader_cost(&self, cost: Duration) {
1055        self.0.build_reader_cost.add_duration(cost);
1056
1057        let mut metrics = self.0.metrics.lock().unwrap();
1058        metrics.build_reader_cost += cost;
1059    }
1060
1061    pub(crate) fn inc_convert_batch_cost(&self, cost: Duration) {
1062        self.0.convert_cost.add_duration(cost);
1063        self.record_elapsed_compute(cost);
1064    }
1065
1066    /// Reports memtable scan metrics.
1067    pub(crate) fn report_mem_scan_metrics(&self, data: &crate::memtable::MemScanMetricsData) {
1068        let mut metrics = self.0.metrics.lock().unwrap();
1069        metrics.mem_scan_cost += data.scan_cost;
1070        metrics.mem_rows += data.num_rows;
1071        metrics.mem_batches += data.num_batches;
1072        metrics.mem_series += data.total_series;
1073        metrics.mem_prefilter_cost += data.prefilter_cost;
1074        metrics.mem_prefilter_rows_filtered += data.prefilter_rows_filtered;
1075    }
1076
1077    /// Merges [ScannerMetrics], `build_reader_cost`, `scan_cost` and `yield_cost`.
1078    pub(crate) fn merge_metrics(&self, metrics: &ScannerMetrics) {
1079        self.0.scan_cost.add_duration(metrics.scan_cost);
1080        self.record_elapsed_compute(metrics.scan_cost);
1081        self.0.yield_cost.add_duration(metrics.yield_cost);
1082        self.record_elapsed_compute(metrics.yield_cost);
1083
1084        let mut metrics_set = self.0.metrics.lock().unwrap();
1085        metrics_set.merge_scanner_metrics(metrics);
1086    }
1087
1088    /// Merges [ReaderMetrics] and `build_reader_cost`.
1089    pub fn merge_reader_metrics(
1090        &self,
1091        metrics: &ReaderMetrics,
1092        per_file_metrics: Option<&HashMap<RegionFileId, FileScanMetrics>>,
1093    ) {
1094        self.0.build_parts_cost.add_duration(metrics.build_cost);
1095
1096        let mut metrics_set = self.0.metrics.lock().unwrap();
1097        metrics_set.merge_reader_metrics(metrics);
1098
1099        // Merge per-file metrics if provided
1100        if let Some(file_metrics) = per_file_metrics {
1101            metrics_set.merge_per_file_metrics(file_metrics);
1102        }
1103    }
1104
1105    /// Finishes the query.
1106    pub(crate) fn on_finish(&self) {
1107        self.0.on_finish(true);
1108    }
1109
1110    /// Sets the distributor metrics.
1111    pub(crate) fn set_distributor_metrics(&self, metrics: &SeriesDistributorMetrics) {
1112        let mut metrics_set = self.0.metrics.lock().unwrap();
1113        metrics_set.set_distributor_metrics(metrics);
1114    }
1115
1116    /// Returns whether verbose explain is enabled.
1117    pub(crate) fn explain_verbose(&self) -> bool {
1118        self.0.explain_verbose
1119    }
1120
1121    /// Returns a MergeMetricsReport trait object for reporting merge metrics.
1122    pub(crate) fn merge_metrics_reporter(&self) -> Arc<dyn MergeMetricsReport> {
1123        self.0.clone()
1124    }
1125
1126    /// Returns a DedupMetricsReport trait object for reporting dedup metrics.
1127    pub(crate) fn dedup_metrics_reporter(&self) -> Arc<dyn DedupMetricsReport> {
1128        self.0.clone()
1129    }
1130
1131    /// Returns the range-cache reporter while preserving query metric ownership.
1132    pub(crate) fn range_cache_metrics_reporter(&self) -> Arc<dyn RangeCacheMetricsReport> {
1133        self.0.clone()
1134    }
1135}
1136
1137impl fmt::Debug for PartitionMetrics {
1138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1139        let metrics = self.0.metrics.lock().unwrap();
1140        write!(
1141            f,
1142            r#"{{"partition":{}, "metrics":{:?}}}"#,
1143            self.0.partition, metrics
1144        )
1145    }
1146}
1147
1148/// Metrics for the series distributor.
1149#[derive(Default)]
1150pub(crate) struct SeriesDistributorMetrics {
1151    /// Number of send full in SeriesScan.
1152    pub(crate) num_series_send_full: usize,
1153    /// Number of rows the series distributor scanned.
1154    pub(crate) num_rows: usize,
1155    /// Number of batches the series distributor scanned.
1156    pub(crate) num_batches: usize,
1157    /// Duration of the series distributor to scan.
1158    pub(crate) scan_cost: Duration,
1159    /// Duration of the series distributor to yield.
1160    pub(crate) yield_cost: Duration,
1161    /// Duration spent in divider operations.
1162    pub(crate) divider_cost: Duration,
1163}
1164
1165/// Aligns memtable and extension batches; SST readers align before schema compatibility.
1166pub(crate) fn align_query_source_batch(
1167    context: &StreamContext,
1168    batch: RecordBatch,
1169) -> Result<RecordBatch> {
1170    let query_schema = context.input.mapper.input_arrow_schema(false);
1171    json_align::align_query_batch(batch, &query_schema)
1172}
1173
1174/// Scans memtable ranges at `index` using flat format that returns RecordBatch.
1175#[tracing::instrument(
1176    skip_all,
1177    fields(
1178        region_id = %stream_ctx.input.region_metadata().region_id,
1179        row_group_index = %index.index,
1180        source = "mem_flat"
1181    )
1182)]
1183pub(crate) fn scan_flat_mem_ranges(
1184    stream_ctx: Arc<StreamContext>,
1185    part_metrics: PartitionMetrics,
1186    index: RowGroupIndex,
1187    time_range: FileTimeRange,
1188) -> impl Stream<Item = Result<RecordBatch>> {
1189    try_stream! {
1190        let ranges = stream_ctx.input.build_mem_ranges(index);
1191        part_metrics.inc_num_mem_ranges(ranges.len());
1192        for range in ranges {
1193            let build_reader_start = Instant::now();
1194            let mem_scan_metrics = Some(MemScanMetrics::default());
1195            let mut iter = range.build_record_batch_iter(Some(time_range), mem_scan_metrics.clone())?;
1196            part_metrics.inc_build_reader_cost(build_reader_start.elapsed());
1197
1198            while let Some(record_batch) = iter.next().transpose()? {
1199                yield record_batch;
1200            }
1201
1202            // Report the memtable scan metrics to partition metrics
1203            if let Some(ref metrics) = mem_scan_metrics {
1204                let data = metrics.data();
1205                part_metrics.report_mem_scan_metrics(&data);
1206            }
1207        }
1208    }
1209}
1210
1211/// Aligns memtable JSON2 columns to query types before merge/dedup.
1212///
1213/// For append-only queries with `TimeSeriesRowSelector::LastRow { after_merge: false }`,
1214/// first keeps all rows tied at each series' maximum timestamp within each sorted
1215/// PUT-only batch, avoiding JSON2 conversion for discarded rows. Final cross-source
1216/// LastRow selection still runs. Batches containing deletes are not reduced.
1217/// This optimization uses the query's series row selector, not its merge mode;
1218/// non-append tables retain all input rows for deduplication and last-non-null backfill.
1219pub(crate) fn prepare_flat_mem_stream(
1220    context: Arc<StreamContext>,
1221    stream: impl Stream<Item = Result<RecordBatch>> + Send + 'static,
1222) -> BoxedRecordBatchStream {
1223    if context.input.read_cols.json_target_types().is_empty() {
1224        return Box::pin(stream);
1225    }
1226
1227    let select_last = context.input.append_mode
1228        && matches!(
1229            context.input.series_row_selector,
1230            Some(TimeSeriesRowSelector::LastRow { after_merge: false })
1231        );
1232    Box::pin(stream.map(move |batch| {
1233        let batch = batch?;
1234        // Select raw candidates before decoding JSON2; final cross-source selection remains.
1235        let batch = if select_last {
1236            FlatLastTimestampSelector::select_batch(batch)?
1237        } else {
1238            batch
1239        };
1240        align_query_source_batch(&context, batch)
1241    }))
1242}
1243
1244/// Files with row count greater than this threshold can contribute to the estimation.
1245const SPLIT_ROW_THRESHOLD: u64 = DEFAULT_ROW_GROUP_SIZE as u64;
1246/// Number of series threshold for splitting batches.
1247const NUM_SERIES_THRESHOLD: u64 = 10240;
1248/// Minimum batch size after splitting. The batch size is less than 60 because a series may only have
1249/// 60 samples per hour.
1250pub(crate) const BATCH_SIZE_THRESHOLD: u64 = 50;
1251/// Minimum estimated rows per series for compaction to split batches. Shorter series
1252/// would make compaction write one small batch per series.
1253pub(crate) const MIN_COMPACTION_SPLIT_ROWS: usize = DEFAULT_READ_BATCH_SIZE / 2;
1254
1255/// Returns the estimated rows per batch after splitting if splitting flat record batches
1256/// may improve merge performance. Returns `None` if splitting is not beneficial.
1257pub(crate) fn should_split_flat_batches_for_merge(
1258    stream_ctx: &Arc<StreamContext>,
1259    range_meta: &RangeMeta,
1260) -> Option<usize> {
1261    // Number of files to split and scan.
1262    let mut num_files_to_split = 0;
1263    let mut num_mem_rows = 0;
1264    let mut num_mem_series = 0;
1265    // Total rows and series for estimating batch size after splitting.
1266    let mut total_rows: u64 = 0;
1267    let mut total_series: u64 = 0;
1268    // Checks each file range, returns early if any range is not splittable.
1269    // For mem ranges, we collect the total number of rows and series because the number of rows in a
1270    // mem range may be too small.
1271    for index in &range_meta.row_group_indices {
1272        if stream_ctx.is_mem_range_index(*index) {
1273            let memtable = &stream_ctx.input.memtables[index.index];
1274            // Is mem range
1275            let stats = memtable.stats();
1276            num_mem_rows += stats.num_rows();
1277            num_mem_series += stats.series_count();
1278        } else if stream_ctx.is_file_range_index(*index) {
1279            // This is a file range.
1280            let file_index = index.index - stream_ctx.input.num_memtables();
1281            let file = &stream_ctx.input.files[file_index];
1282            let file_meta = file.meta_ref();
1283            if file_meta.level == 0 {
1284                // Short series can leave too many tiny batches retained by the merge.
1285                if file_meta.num_series > 0 {
1286                    if !can_split_series(file_meta.num_rows, file_meta.num_series) {
1287                        return None;
1288                    }
1289                    total_rows += file_meta.num_rows;
1290                    total_series += file_meta.num_series;
1291                }
1292                // Older L0 files may lack series statistics; keep their fallback behavior.
1293                num_files_to_split += 1;
1294                continue;
1295            } else if file_meta.num_rows < SPLIT_ROW_THRESHOLD || file_meta.num_series == 0 {
1296                // If the file doesn't have enough rows, or the number of series is unavailable, skips it.
1297                continue;
1298            }
1299            debug_assert!(file_meta.num_rows > 0);
1300            if !can_split_series(file_meta.num_rows, file_meta.num_series) {
1301                // We can't split batches in a file.
1302                common_telemetry::trace!(
1303                    "Can't split series for file {}, level: {}, num_rows: {}, num_series: {}",
1304                    file_meta.file_id,
1305                    file_meta.level,
1306                    file_meta.num_rows,
1307                    file_meta.num_series,
1308                );
1309                return None;
1310            } else {
1311                num_files_to_split += 1;
1312                total_rows += file.meta_ref().num_rows;
1313                total_series += file.meta_ref().num_series;
1314            }
1315        }
1316        // Skips non-file and non-mem ranges.
1317    }
1318
1319    let should_split = if num_files_to_split > 0 {
1320        // We mainly consider file ranges because they have enough data for sampling.
1321        true
1322    } else if num_mem_series > 0
1323        && num_mem_rows > 0
1324        && can_split_series(num_mem_rows as u64, num_mem_series as u64)
1325    {
1326        total_rows += num_mem_rows as u64;
1327        total_series += num_mem_series as u64;
1328        true
1329    } else {
1330        false
1331    };
1332
1333    if !should_split {
1334        return None;
1335    }
1336
1337    // Estimate rows per batch after splitting.
1338    let estimated_batch_size = if total_series > 0 && total_rows > 0 {
1339        ((total_rows / total_series) as usize).clamp(1, DEFAULT_READ_BATCH_SIZE)
1340    } else {
1341        // No valid estimate available, use a conservative fallback.
1342        DEFAULT_READ_BATCH_SIZE / 4
1343    };
1344    Some(estimated_batch_size)
1345}
1346
1347/// Computes the channel size for parallel scan based on the estimated rows per batch.
1348/// The channel should buffer approximately `2 * DEFAULT_READ_BATCH_SIZE` rows.
1349pub(crate) fn compute_parallel_channel_size(estimated_rows_per_batch: usize) -> usize {
1350    let size = 2 * DEFAULT_READ_BATCH_SIZE / estimated_rows_per_batch.max(1);
1351    size.clamp(2, 64)
1352}
1353
1354/// Computes the average estimated rows per batch across multiple range readers.
1355pub(crate) fn compute_average_batch_size(
1356    estimated_rows_per_batch: impl IntoIterator<Item = usize>,
1357) -> usize {
1358    let mut total = 0usize;
1359    let mut count = 0usize;
1360    for size in estimated_rows_per_batch {
1361        total += size;
1362        count += 1;
1363    }
1364
1365    if count == 0 {
1366        return DEFAULT_READ_BATCH_SIZE;
1367    }
1368
1369    (total / count).clamp(1, DEFAULT_READ_BATCH_SIZE)
1370}
1371
1372fn can_split_series(num_rows: u64, num_series: u64) -> bool {
1373    if num_rows == 0 || num_series == 0 {
1374        return false;
1375    }
1376
1377    // It doesn't have too many series or it will have enough rows for each batch.
1378    num_series < NUM_SERIES_THRESHOLD || num_rows / num_series >= BATCH_SIZE_THRESHOLD
1379}
1380
1381#[cfg(test)]
1382mod split_tests {
1383    use std::sync::Arc;
1384
1385    use common_time::Timestamp;
1386    use smallvec::smallvec;
1387    use store_api::storage::FileId;
1388
1389    use super::*;
1390    use crate::read::flat_projection::FlatProjectionMapper;
1391    use crate::read::range::{RangeMeta, RowGroupIndex, SourceIndex};
1392    use crate::read::scan_region::{ScanInput, StreamContext};
1393    use crate::sst::file::FileHandle;
1394    use crate::test_util::memtable_util::metadata_with_primary_key;
1395    use crate::test_util::scheduler_util::SchedulerEnv;
1396    use crate::test_util::sst_util::sst_file_handle_with_file_id;
1397
1398    async fn new_stream_context_with_files(files: Vec<FileHandle>) -> StreamContext {
1399        let env = SchedulerEnv::new().await;
1400        let metadata = Arc::new(metadata_with_primary_key(vec![0, 1], false));
1401        let mapper = FlatProjectionMapper::new(&metadata, [0, 2, 3]).unwrap();
1402        let input = ScanInput::builder(env.access_layer.clone(), mapper)
1403            .with_files(files)
1404            .build();
1405
1406        StreamContext {
1407            input,
1408            ranges: vec![],
1409            query_start: std::time::Instant::now(),
1410        }
1411    }
1412
1413    fn single_file_range_meta() -> RangeMeta {
1414        RangeMeta {
1415            time_range: (
1416                Timestamp::new_millisecond(0),
1417                Timestamp::new_millisecond(1000),
1418            ),
1419            indices: smallvec![SourceIndex {
1420                index: 0,
1421                num_row_groups: 1,
1422            }],
1423            row_group_indices: smallvec![RowGroupIndex {
1424                index: 0,
1425                row_group_index: 0,
1426            }],
1427            num_rows: 1024,
1428        }
1429    }
1430
1431    #[tokio::test]
1432    async fn should_split_level_zero_file_even_when_series_stats_are_missing() {
1433        let mut file = sst_file_handle_with_file_id(FileId::random(), 0, 1000)
1434            .meta_ref()
1435            .clone();
1436        file.level = 0;
1437        file.num_rows = DEFAULT_ROW_GROUP_SIZE as u64;
1438        file.num_row_groups = 1;
1439        file.num_series = 0;
1440
1441        let file = FileHandle::new(file, crate::test_util::new_noop_file_purger());
1442        let stream_ctx = Arc::new(new_stream_context_with_files(vec![file]).await);
1443
1444        assert!(
1445            should_split_flat_batches_for_merge(&stream_ctx, &single_file_range_meta()).is_some()
1446        );
1447    }
1448
1449    #[test]
1450    fn can_split_series_returns_false_for_zero_inputs() {
1451        assert!(!can_split_series(0, 1));
1452        assert!(!can_split_series(1, 0));
1453        assert!(!can_split_series(0, 0));
1454    }
1455}
1456
1457/// Creates a new [ReaderFilterMetrics] with optional apply metrics initialized
1458/// based on the `explain_verbose` flag.
1459pub(crate) fn new_filter_metrics(explain_verbose: bool) -> ReaderFilterMetrics {
1460    if explain_verbose {
1461        ReaderFilterMetrics {
1462            inverted_index_apply_metrics: Some(InvertedIndexApplyMetrics::default()),
1463            bloom_filter_apply_metrics: Some(BloomFilterIndexApplyMetrics::default()),
1464            fulltext_index_apply_metrics: Some(FulltextIndexApplyMetrics::default()),
1465            ..Default::default()
1466        }
1467    } else {
1468        ReaderFilterMetrics::default()
1469    }
1470}
1471
1472/// Scans file ranges at `index` using flat reader that returns RecordBatch.
1473#[tracing::instrument(
1474    skip_all,
1475    fields(
1476        region_id = %stream_ctx.input.region_metadata().region_id,
1477        row_group_index = %index.index,
1478        source = read_type
1479    )
1480)]
1481pub(crate) async fn scan_flat_file_ranges(
1482    stream_ctx: Arc<StreamContext>,
1483    part_metrics: PartitionMetrics,
1484    index: RowGroupIndex,
1485    read_type: &'static str,
1486    partition_pruner: Arc<PartitionPruner>,
1487) -> Result<impl Stream<Item = Result<RecordBatch>>> {
1488    let mut reader_metrics = ReaderMetrics {
1489        filter_metrics: new_filter_metrics(part_metrics.explain_verbose()),
1490        ..Default::default()
1491    };
1492    let ranges = partition_pruner
1493        .build_file_ranges(index, &part_metrics, &mut reader_metrics)
1494        .await?;
1495    part_metrics.inc_num_file_ranges(ranges.len());
1496    part_metrics.merge_reader_metrics(&reader_metrics, None);
1497
1498    // Creates initial per-file metrics with build_part_cost.
1499    let init_per_file_metrics = if part_metrics.explain_verbose() {
1500        let file = stream_ctx.input.file_from_index(index);
1501        let file_id = file.file_id();
1502
1503        let mut map = HashMap::new();
1504        map.insert(
1505            file_id,
1506            FileScanMetrics {
1507                build_part_cost: reader_metrics.build_cost,
1508                ..Default::default()
1509            },
1510        );
1511        Some(map)
1512    } else {
1513        None
1514    };
1515
1516    Ok(build_flat_file_range_scan_stream(
1517        stream_ctx,
1518        part_metrics,
1519        read_type,
1520        ranges,
1521        init_per_file_metrics,
1522    ))
1523}
1524
1525/// Filters a flat-format record batch by the exact sequence range.
1526///
1527/// Returns `None` when the entire batch is filtered out. The sequence column is
1528/// the second-to-last internal column of the flat format
1529/// (`__primary_key`, `__sequence`, `__op_type`), so it must be applied before any
1530/// projection/compat conversion that drops internal columns.
1531///
1532/// `file_sequence_trusted` is the per-file trust decision. Foreign reader
1533/// batches have already been virtualized to the target-local file barrier, so
1534/// they are filtered using the effective batch sequence. Local untrusted files
1535/// pass through because exact capability excludes them before row filtering.
1536pub(crate) fn filter_flat_batch_by_sequence(
1537    record_batch: RecordBatch,
1538    sequence_range: Option<SequenceRange>,
1539    file_sequence_trusted: bool,
1540) -> Result<Option<RecordBatch>> {
1541    let Some(sequence) = sequence_range else {
1542        return Ok(Some(record_batch));
1543    };
1544    if !file_sequence_trusted {
1545        return Ok(Some(record_batch));
1546    }
1547
1548    let num_rows = record_batch.num_rows();
1549    if num_rows == 0 {
1550        return Ok(Some(record_batch));
1551    }
1552    let sequence_column = record_batch.column(sequence_column_index(record_batch.num_columns()));
1553    let predicate = sequence
1554        .filter(sequence_column)
1555        .context(ComputeArrowSnafu)?;
1556    let select_count = predicate.true_count();
1557    if select_count == 0 {
1558        return Ok(None);
1559    }
1560    if select_count == num_rows {
1561        return Ok(Some(record_batch));
1562    }
1563    let filtered_batch = datatypes::arrow::compute::filter_record_batch(&record_batch, &predicate)
1564        .context(ComputeArrowSnafu)?;
1565    Ok(Some(filtered_batch))
1566}
1567
1568/// Build the stream of scanning the input [`FileRange`]s using flat reader that returns RecordBatch.
1569#[tracing::instrument(
1570    skip_all,
1571    fields(read_type = read_type, range_count = ranges.len())
1572)]
1573pub fn build_flat_file_range_scan_stream(
1574    stream_ctx: Arc<StreamContext>,
1575    part_metrics: PartitionMetrics,
1576    read_type: &'static str,
1577    ranges: SmallVec<[FileRange; 2]>,
1578    mut per_file_metrics: Option<HashMap<RegionFileId, FileScanMetrics>>,
1579) -> impl Stream<Item = Result<RecordBatch>> {
1580    try_stream! {
1581        let fetch_metrics = if part_metrics.explain_verbose() {
1582            Some(Arc::new(ParquetFetchMetrics::default()))
1583        } else {
1584            None
1585        };
1586        let reader_metrics = &mut ReaderMetrics {
1587            fetch_metrics: fetch_metrics.clone(),
1588            ..Default::default()
1589        };
1590        for range in ranges {
1591            let build_reader_start = Instant::now();
1592            let Some(mut reader) = range
1593                .flat_reader(
1594                    match stream_ctx.input.series_row_selector {
1595                        Some(TimeSeriesRowSelector::LastRow { after_merge: false })
1596                            if stream_ctx.input.sequence_range.is_none() =>
1597                        {
1598                            stream_ctx.input.series_row_selector
1599                        }
1600                        _ => None,
1601                    },
1602                    fetch_metrics.as_deref(),
1603                )
1604                .await?
1605            else {
1606                continue;
1607            };
1608            let build_cost = build_reader_start.elapsed();
1609            part_metrics.inc_build_reader_cost(build_cost);
1610
1611            let may_compat = range.compat_batch();
1612            let file_sequence_trusted = range
1613                .file_handle()
1614                .is_effective_target_sequence_trusted(stream_ctx.input.region_metadata().region_id);
1615
1616            let mapper = range.compaction_projection_mapper();
1617            while let Some(record_batch) = reader.next_batch().await? {
1618                let record_batch = if let Some(mapper) = mapper {
1619                    mapper.project(record_batch)?
1620                } else {
1621                    record_batch
1622                };
1623                let Some(record_batch) = filter_flat_batch_by_sequence(
1624                    record_batch,
1625                    stream_ctx.input.sequence_range,
1626                    file_sequence_trusted,
1627                )? else {
1628                    continue;
1629                };
1630
1631                if let Some(flat_compat) = may_compat {
1632                    let batch = flat_compat.compat(record_batch)?;
1633                    yield batch;
1634                } else {
1635                    yield record_batch;
1636                }
1637            }
1638
1639            let prune_metrics = reader.metrics();
1640
1641            // Update per-file metrics if tracking is enabled
1642            if let Some(file_metrics_map) = per_file_metrics.as_mut() {
1643                let file_id = range.file_handle().file_id();
1644                let file_metrics = file_metrics_map
1645                    .entry(file_id)
1646                    .or_insert_with(FileScanMetrics::default);
1647
1648                file_metrics.num_ranges += 1;
1649                file_metrics.num_rows += prune_metrics.num_rows;
1650                file_metrics.build_reader_cost += build_cost;
1651                file_metrics.scan_cost += prune_metrics.scan_cost;
1652            }
1653
1654            reader_metrics.merge_from(&prune_metrics);
1655        }
1656
1657        // Reports metrics.
1658        reader_metrics.observe_rows(read_type);
1659        part_metrics.merge_reader_metrics(reader_metrics, per_file_metrics.as_ref());
1660    }
1661}
1662
1663/// Build the stream of scanning the extension range in flat format denoted by the [`RowGroupIndex`].
1664#[cfg(feature = "enterprise")]
1665pub(crate) async fn scan_flat_extension_range(
1666    context: Arc<StreamContext>,
1667    index: RowGroupIndex,
1668    partition_metrics: PartitionMetrics,
1669    options: crate::extension::ExtensionRangeReadOptions,
1670) -> Result<BoxedRecordBatchStream> {
1671    use snafu::ResultExt;
1672
1673    let range = context.input.extension_range(index.index);
1674    let reader = range.flat_reader(context.as_ref(), options);
1675    let stream = reader
1676        .read(context.clone(), partition_metrics, index)
1677        .await
1678        .context(crate::error::ScanExternalRangeSnafu)?;
1679    if context.input.read_cols.json_target_types().is_empty() {
1680        return Ok(stream);
1681    }
1682    Ok(Box::pin(stream.and_then(move |batch| {
1683        std::future::ready(align_query_source_batch(&context, batch))
1684    })))
1685}
1686
1687pub(crate) async fn maybe_scan_flat_other_ranges(
1688    context: &Arc<StreamContext>,
1689    index: RowGroupIndex,
1690    metrics: &PartitionMetrics,
1691    pre_filter_mode: PreFilterMode,
1692) -> Result<BoxedRecordBatchStream> {
1693    #[cfg(feature = "enterprise")]
1694    {
1695        let options = crate::extension::ExtensionRangeReadOptions { pre_filter_mode };
1696        scan_flat_extension_range(context.clone(), index, metrics.clone(), options).await
1697    }
1698
1699    #[cfg(not(feature = "enterprise"))]
1700    {
1701        let _ = context;
1702        let _ = index;
1703        let _ = metrics;
1704        let _ = pre_filter_mode;
1705
1706        crate::error::UnexpectedSnafu {
1707            reason: "no other ranges scannable in flat format",
1708        }
1709        .fail()
1710    }
1711}
1712
1713/// A stream wrapper that splits record batches from an inner stream.
1714pub(crate) struct SplitRecordBatchStream<S> {
1715    /// The inner stream that yields record batches.
1716    inner: S,
1717    /// Buffer for split batches.
1718    batches: VecDeque<RecordBatch>,
1719}
1720
1721impl<S> SplitRecordBatchStream<S> {
1722    /// Creates a new splitting stream wrapper.
1723    pub(crate) fn new(inner: S) -> Self {
1724        Self {
1725            inner,
1726            batches: VecDeque::new(),
1727        }
1728    }
1729}
1730
1731impl<S> Stream for SplitRecordBatchStream<S>
1732where
1733    S: Stream<Item = Result<RecordBatch>> + Unpin,
1734{
1735    type Item = Result<RecordBatch>;
1736
1737    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1738        loop {
1739            // First, check if we have buffered split batches
1740            if let Some(batch) = self.batches.pop_front() {
1741                return Poll::Ready(Some(Ok(batch)));
1742            }
1743
1744            // Poll the inner stream for the next batch
1745            let record_batch = match futures::ready!(Pin::new(&mut self.inner).poll_next(cx)) {
1746                Some(Ok(batch)) => batch,
1747                Some(Err(e)) => return Poll::Ready(Some(Err(e))),
1748                None => return Poll::Ready(None),
1749            };
1750
1751            // Split the batch and buffer the results
1752            split_record_batch(record_batch, &mut self.batches);
1753            // Continue the loop to return the first split batch
1754        }
1755    }
1756}
1757
1758/// Splits the batch so each sub-batch has strictly increasing timestamps.
1759///
1760/// # Panics
1761/// Panics if the timestamp array is invalid.
1762pub(crate) fn split_record_batch(record_batch: RecordBatch, batches: &mut VecDeque<RecordBatch>) {
1763    let batch_rows = record_batch.num_rows();
1764    if batch_rows == 0 {
1765        return;
1766    }
1767    if batch_rows < 2 {
1768        batches.push_back(record_batch);
1769        return;
1770    }
1771
1772    let time_index_pos = time_index_column_index(record_batch.num_columns());
1773    let timestamps = record_batch.column(time_index_pos);
1774    let (ts_values, _unit) = timestamp_array_to_primitive(timestamps).unwrap();
1775    let mut offsets = Vec::with_capacity(16);
1776    offsets.push(0);
1777    let values = ts_values.values();
1778    for (i, &value) in values.iter().take(batch_rows - 1).enumerate() {
1779        if value >= values[i + 1] {
1780            offsets.push(i + 1);
1781        }
1782    }
1783    offsets.push(values.len());
1784
1785    // Splits the batch by offsets.
1786    for (i, &start) in offsets[..offsets.len() - 1].iter().enumerate() {
1787        let end = offsets[i + 1];
1788        let rows_in_batch = end - start;
1789        batches.push_back(record_batch.slice(start, rows_in_batch));
1790    }
1791}
1792
1793#[cfg(test)]
1794mod tests {
1795    use arrow_select::concat::concat_batches;
1796    use common_time::Timestamp;
1797    use datatypes::arrow::array::{BinaryArray, TimestampMillisecondArray, UInt64Array};
1798    use datatypes::types::json_type::JsonNativeType;
1799    use futures::TryStreamExt;
1800    use smallvec::{SmallVec, smallvec};
1801    use std::sync::Arc;
1802    use std::time::Instant;
1803    use store_api::storage::RegionId;
1804
1805    use super::*;
1806    use crate::cache::CacheStrategy;
1807    use crate::memtable::bulk::context::BulkIterContext;
1808    use crate::memtable::bulk::part_reader::BulkPartBatchIter;
1809    use crate::memtable::{
1810        BoxedBatchIterator, BoxedRecordBatchIterator, IterBuilder, MemtableRange,
1811        MemtableRangeContext, MemtableStats,
1812    };
1813    use crate::read::flat_projection::FlatProjectionMapper;
1814    use crate::read::range::{MemRangeBuilder, SourceIndex};
1815    use crate::read::read_columns::ReadColumns;
1816    use crate::read::scan_region::ScanInput;
1817    use crate::sst::file::{FileHandle, FileMeta};
1818    use crate::sst::file_purger::NoopFilePurger;
1819    use crate::test_util::memtable_util::metadata_for_test;
1820    use crate::test_util::scheduler_util::SchedulerEnv;
1821
1822    #[test]
1823    fn test_range_cache_reporter_preserves_metrics_and_lifetime() {
1824        let metrics = PartitionMetrics::new(
1825            RegionId::new(1, 1),
1826            0,
1827            "test_range_cache_reporter",
1828            Instant::now(),
1829            false,
1830            &ExecutionPlanMetricsSet::new(),
1831        );
1832        let weak = Arc::downgrade(&metrics.0);
1833        let reporter = metrics.range_cache_metrics_reporter();
1834        reporter.inc_range_cache_hit();
1835        reporter.inc_range_cache_miss();
1836        drop(metrics);
1837        // Background publication can still report after the query drops its handle.
1838        reporter.inc_range_cache_size(128);
1839        {
1840            let inner = weak.upgrade().unwrap();
1841            let metrics = inner.metrics.lock().unwrap();
1842            assert_eq!(metrics.range_cache_hit, 1);
1843            assert_eq!(metrics.range_cache_miss, 1);
1844            assert_eq!(metrics.range_cache_size, 128);
1845        }
1846        drop(reporter);
1847        assert!(weak.upgrade().is_none());
1848    }
1849
1850    struct EmptyIterBuilder;
1851
1852    impl IterBuilder for EmptyIterBuilder {
1853        fn build(&self, _metrics: Option<MemScanMetrics>) -> Result<BoxedBatchIterator> {
1854            Ok(Box::new(std::iter::empty()))
1855        }
1856
1857        fn is_record_batch(&self) -> bool {
1858            true
1859        }
1860
1861        fn build_record_batch(
1862            &self,
1863            _time_range: Option<(Timestamp, Timestamp)>,
1864            _metrics: Option<MemScanMetrics>,
1865        ) -> Result<BoxedRecordBatchIterator> {
1866            Ok(Box::new(std::iter::empty()))
1867        }
1868    }
1869
1870    async fn new_test_stream_ctx(
1871        files: Vec<FileHandle>,
1872        memtables: Vec<MemRangeBuilder>,
1873    ) -> Arc<StreamContext> {
1874        let env = SchedulerEnv::new().await;
1875        let metadata = metadata_for_test();
1876        let mapper = FlatProjectionMapper::new(&metadata, [0, 2, 3]).unwrap();
1877        let input = ScanInput::builder(env.access_layer.clone(), mapper)
1878            .with_cache(CacheStrategy::Disabled)
1879            .with_memtables(memtables)
1880            .with_files(files)
1881            .build();
1882
1883        Arc::new(StreamContext {
1884            input,
1885            ranges: Vec::new(),
1886            query_start: Instant::now(),
1887        })
1888    }
1889
1890    fn new_test_file(num_rows: u64, num_series: u64) -> FileHandle {
1891        let meta = FileMeta {
1892            region_id: RegionId::new(123, 456),
1893            file_id: Default::default(),
1894            level: 1,
1895            time_range: (
1896                Timestamp::new_millisecond(0),
1897                Timestamp::new_millisecond(1000),
1898            ),
1899            num_rows,
1900            num_series,
1901            ..Default::default()
1902        };
1903        FileHandle::new(meta, Arc::new(NoopFilePurger))
1904    }
1905
1906    fn new_test_memtable(num_rows: usize, series_count: usize) -> MemRangeBuilder {
1907        let context = Arc::new(MemtableRangeContext::new(
1908            0,
1909            Box::new(EmptyIterBuilder),
1910            Default::default(),
1911        ));
1912        let stats = MemtableStats {
1913            time_range: Some((
1914                Timestamp::new_millisecond(0),
1915                Timestamp::new_millisecond(1000),
1916            )),
1917            num_rows,
1918            num_ranges: 1,
1919            series_count,
1920            ..Default::default()
1921        };
1922        let range = MemtableRange::new(context, stats.clone());
1923        MemRangeBuilder::new(range, stats)
1924    }
1925
1926    fn new_test_range_meta(row_group_indices: SmallVec<[RowGroupIndex; 2]>) -> RangeMeta {
1927        let indices = row_group_indices
1928            .iter()
1929            .map(|row_group_index| SourceIndex {
1930                index: row_group_index.index,
1931                num_row_groups: 1,
1932            })
1933            .collect();
1934
1935        RangeMeta {
1936            time_range: (
1937                Timestamp::new_millisecond(0),
1938                Timestamp::new_millisecond(1000),
1939            ),
1940            indices,
1941            row_group_indices,
1942            num_rows: 0,
1943        }
1944    }
1945
1946    #[tokio::test]
1947    async fn test_memtable_last_row_selects_before_json2_alignment()
1948    -> std::result::Result<(), Box<dyn std::error::Error>> {
1949        struct BatchIter(RecordBatch, Arc<BulkIterContext>, Option<SequenceRange>);
1950        impl IterBuilder for BatchIter {
1951            fn build(&self, _metrics: Option<MemScanMetrics>) -> Result<BoxedBatchIterator> {
1952                Ok(Box::new(std::iter::empty()))
1953            }
1954            fn is_record_batch(&self) -> bool {
1955                true
1956            }
1957            fn build_record_batch(
1958                &self,
1959                _time_range: Option<FileTimeRange>,
1960                metrics: Option<MemScanMetrics>,
1961            ) -> Result<BoxedRecordBatchIterator> {
1962                Ok(Box::new(BulkPartBatchIter::from_single(
1963                    self.0.clone(),
1964                    self.1.clone(),
1965                    self.2,
1966                    1,
1967                    metrics,
1968                )))
1969            }
1970        }
1971        let (metadata, batch) = crate::test_util::sst_util::new_json2_record_batch()?;
1972        let batch = concat_batches(&batch.schema(), &[batch.clone(), batch.slice(0, 1)])?;
1973        let mut columns = batch.columns().to_vec();
1974        columns[1] = Arc::new(TimestampMillisecondArray::from(vec![1, 2, 3]));
1975        let seq_index = sequence_column_index(batch.num_columns());
1976        columns[seq_index] = Arc::new(UInt64Array::from(vec![1, 2, 3]));
1977        let batch = RecordBatch::try_new(batch.schema(), columns)?;
1978        let reader_context = Arc::new(BulkIterContext::new(
1979            metadata.clone(),
1980            None,
1981            None,
1982            false,
1983            DEFAULT_READ_BATCH_SIZE,
1984        )?);
1985        let time_range = (Timestamp::new_millisecond(1), Timestamp::new_millisecond(3));
1986        let env = SchedulerEnv::new().await;
1987        for (append, selector, sequence, count) in [
1988            (
1989                true,
1990                Some(TimeSeriesRowSelector::LastRow { after_merge: false }),
1991                None,
1992                1,
1993            ),
1994            (
1995                true,
1996                Some(TimeSeriesRowSelector::LastRow { after_merge: true }),
1997                None,
1998                3,
1999            ),
2000            (
2001                false,
2002                Some(TimeSeriesRowSelector::LastRow { after_merge: false }),
2003                None,
2004                3,
2005            ),
2006            (true, None, None, 3),
2007            (
2008                true,
2009                Some(TimeSeriesRowSelector::LastRow { after_merge: false }),
2010                Some(SequenceRange::GtLtEq { min: 0, max: 2 }),
2011                1,
2012            ),
2013        ] {
2014            let range_context = Arc::new(MemtableRangeContext::new(
2015                0,
2016                Box::new(BatchIter(batch.clone(), reader_context.clone(), sequence)),
2017                Default::default(),
2018            ));
2019            let stats = MemtableStats {
2020                time_range: Some(time_range),
2021                num_rows: 3,
2022                num_ranges: 1,
2023                series_count: 1,
2024                ..Default::default()
2025            };
2026            let range = MemtableRange::new(range_context, stats.clone());
2027            let read_cols = ReadColumns::new([0, 1])
2028                .with_json_target_types([(0, JsonNativeType::Variant)].into_iter().collect());
2029            let mapper =
2030                FlatProjectionMapper::new_with_read_columns(&metadata, vec![0, 1], read_cols)?;
2031            let input = ScanInput::builder(env.access_layer.clone(), mapper)
2032                .with_append_mode(append)
2033                .with_series_row_selector(selector)
2034                .with_sequence_range(sequence)
2035                .with_memtables(vec![MemRangeBuilder::new(range, stats)])
2036                .build();
2037            let context = Arc::new(StreamContext {
2038                input,
2039                ranges: vec![],
2040                query_start: Instant::now(),
2041            });
2042            let metrics = PartitionMetrics::new(
2043                metadata.region_id,
2044                0,
2045                "test_json2",
2046                context.query_start,
2047                false,
2048                &ExecutionPlanMetricsSet::new(),
2049            );
2050            let mut raw_stream = Box::pin(scan_flat_mem_ranges(
2051                context.clone(),
2052                metrics,
2053                RowGroupIndex {
2054                    index: 0,
2055                    row_group_index: -1,
2056                },
2057                time_range,
2058            ));
2059            let raw = raw_stream.try_next().await?.ok_or("expected a raw batch")?;
2060            let expected_raw = if sequence.is_some() {
2061                batch.slice(0, 2)
2062            } else {
2063                batch.clone()
2064            };
2065            assert_eq!(expected_raw, raw);
2066            assert!(raw_stream.try_next().await?.is_none());
2067            let mut stream = prepare_flat_mem_stream(context, futures::stream::iter([Ok(raw)]));
2068            let output = stream.try_next().await?.ok_or("expected a JSON2 batch")?;
2069            assert_eq!(count, output.num_rows());
2070            let values = output
2071                .column(0)
2072                .as_any()
2073                .downcast_ref::<BinaryArray>()
2074                .ok_or("expected aligned JSONB")?;
2075            assert_eq!(
2076                datatypes::types::parse_string_to_jsonb(if sequence.is_some() {
2077                    r#"{"a":2}"#
2078                } else {
2079                    r#"{"a":1}"#
2080                })?,
2081                values.value(count - 1)
2082            );
2083            assert!(stream.try_next().await?.is_none());
2084        }
2085        Ok(())
2086    }
2087
2088    #[tokio::test]
2089    async fn test_should_split_flat_batches_for_merge_uses_splittable_file_rows_per_series() {
2090        let num_rows = SPLIT_ROW_THRESHOLD * 2;
2091        let num_series = (num_rows / 100).max(1);
2092        let stream_ctx =
2093            new_test_stream_ctx(vec![new_test_file(num_rows, num_series)], vec![]).await;
2094        let range_meta = new_test_range_meta(smallvec![RowGroupIndex {
2095            index: 0,
2096            row_group_index: 0,
2097        }]);
2098
2099        assert_eq!(
2100            Some((num_rows / num_series) as usize),
2101            should_split_flat_batches_for_merge(&stream_ctx, &range_meta)
2102        );
2103    }
2104
2105    #[tokio::test]
2106    async fn test_should_split_flat_batches_for_merge_skips_small_or_unknown_series_files() {
2107        let stream_ctx = new_test_stream_ctx(
2108            vec![
2109                new_test_file(SPLIT_ROW_THRESHOLD.saturating_sub(1), 1),
2110                new_test_file(SPLIT_ROW_THRESHOLD * 2, 0),
2111            ],
2112            vec![],
2113        )
2114        .await;
2115        let range_meta = new_test_range_meta(smallvec![
2116            RowGroupIndex {
2117                index: 0,
2118                row_group_index: 0,
2119            },
2120            RowGroupIndex {
2121                index: 1,
2122                row_group_index: 0,
2123            }
2124        ]);
2125
2126        assert_eq!(
2127            None,
2128            should_split_flat_batches_for_merge(&stream_ctx, &range_meta)
2129        );
2130    }
2131
2132    #[tokio::test]
2133    async fn test_should_split_flat_batches_for_merge_returns_none_for_unsplittable_file() {
2134        let num_series =
2135            (SPLIT_ROW_THRESHOLD / (BATCH_SIZE_THRESHOLD - 1)).max(NUM_SERIES_THRESHOLD) + 1;
2136        let stream_ctx =
2137            new_test_stream_ctx(vec![new_test_file(SPLIT_ROW_THRESHOLD, num_series)], vec![]).await;
2138        let range_meta = new_test_range_meta(smallvec![RowGroupIndex {
2139            index: 0,
2140            row_group_index: 0,
2141        }]);
2142
2143        assert_eq!(
2144            None,
2145            should_split_flat_batches_for_merge(&stream_ctx, &range_meta)
2146        );
2147    }
2148
2149    #[tokio::test]
2150    async fn test_should_split_flat_batches_for_merge_uses_level_zero_series_stats() {
2151        for (num_rows, num_series, expected) in [
2152            (70_144, 35_083, None),
2153            (100_000, 1_000, Some(100)),
2154            (100_000, 0, Some(DEFAULT_READ_BATCH_SIZE / 4)),
2155        ] {
2156            let mut meta = new_test_file(num_rows, num_series).meta_ref().clone();
2157            meta.level = 0;
2158            let file = FileHandle::new(meta, Arc::new(NoopFilePurger));
2159            let stream_ctx = new_test_stream_ctx(vec![file], vec![]).await;
2160            let range_meta = new_test_range_meta(smallvec![RowGroupIndex {
2161                index: 0,
2162                row_group_index: 0,
2163            }]);
2164
2165            assert_eq!(
2166                expected,
2167                should_split_flat_batches_for_merge(&stream_ctx, &range_meta),
2168                "rows={num_rows}, series={num_series}",
2169            );
2170        }
2171    }
2172
2173    #[tokio::test]
2174    async fn test_should_split_flat_batches_for_merge_falls_back_to_memtables() {
2175        let stream_ctx = new_test_stream_ctx(vec![], vec![new_test_memtable(5_000, 100)]).await;
2176        let range_meta = new_test_range_meta(smallvec![RowGroupIndex {
2177            index: 0,
2178            row_group_index: 0,
2179        }]);
2180
2181        assert_eq!(
2182            Some(50),
2183            should_split_flat_batches_for_merge(&stream_ctx, &range_meta)
2184        );
2185    }
2186
2187    #[tokio::test]
2188    async fn test_should_split_flat_batches_for_merge_clamps_estimate() {
2189        let stream_ctx =
2190            new_test_stream_ctx(vec![new_test_file(SPLIT_ROW_THRESHOLD * 2, 1)], vec![]).await;
2191        let range_meta = new_test_range_meta(smallvec![RowGroupIndex {
2192            index: 0,
2193            row_group_index: 0,
2194        }]);
2195
2196        assert_eq!(
2197            Some(DEFAULT_READ_BATCH_SIZE),
2198            should_split_flat_batches_for_merge(&stream_ctx, &range_meta)
2199        );
2200    }
2201
2202    #[test]
2203    fn test_compute_parallel_channel_size_clamps_to_max_for_small_batches() {
2204        assert_eq!(64, compute_parallel_channel_size(0));
2205        assert_eq!(64, compute_parallel_channel_size(1));
2206    }
2207
2208    #[test]
2209    fn test_compute_parallel_channel_size_returns_expected_mid_range_size() {
2210        assert_eq!(
2211            4,
2212            compute_parallel_channel_size(DEFAULT_READ_BATCH_SIZE / 2)
2213        );
2214    }
2215
2216    #[test]
2217    fn test_compute_parallel_channel_size_clamps_to_min_for_large_batches() {
2218        assert_eq!(2, compute_parallel_channel_size(DEFAULT_READ_BATCH_SIZE));
2219        assert_eq!(
2220            2,
2221            compute_parallel_channel_size(DEFAULT_READ_BATCH_SIZE * 2)
2222        );
2223    }
2224
2225    #[test]
2226    fn test_compute_average_batch_size_uses_arithmetic_mean() {
2227        assert_eq!(24, compute_average_batch_size([16, 24, 32]));
2228    }
2229
2230    #[test]
2231    fn test_compute_average_batch_size_clamps_values() {
2232        assert_eq!(
2233            DEFAULT_READ_BATCH_SIZE,
2234            compute_average_batch_size([DEFAULT_READ_BATCH_SIZE, DEFAULT_READ_BATCH_SIZE * 2])
2235        );
2236        assert_eq!(1, compute_average_batch_size([0, 1]));
2237    }
2238
2239    #[test]
2240    fn test_compute_average_batch_size_falls_back_when_empty() {
2241        assert_eq!(
2242            DEFAULT_READ_BATCH_SIZE,
2243            compute_average_batch_size(std::iter::empty())
2244        );
2245    }
2246
2247    /// Builds a flat-format record batch whose time index column holds `timestamps`.
2248    fn flat_ts_batch(timestamps: &[i64]) -> RecordBatch {
2249        use datatypes::arrow::array::{TimestampMillisecondArray, UInt8Array, UInt64Array};
2250        use datatypes::arrow::datatypes::{DataType, Field, Schema, TimeUnit};
2251
2252        let num_rows = timestamps.len();
2253        let schema = Arc::new(Schema::new(vec![
2254            Field::new(
2255                "ts",
2256                DataType::Timestamp(TimeUnit::Millisecond, None),
2257                false,
2258            ),
2259            Field::new("pk", DataType::UInt64, false),
2260            Field::new("seq", DataType::UInt64, false),
2261            Field::new("op", DataType::UInt8, false),
2262        ]));
2263        RecordBatch::try_new(
2264            schema,
2265            vec![
2266                Arc::new(TimestampMillisecondArray::from(timestamps.to_vec())),
2267                Arc::new(UInt64Array::from(vec![0u64; num_rows])),
2268                Arc::new(UInt64Array::from(vec![0u64; num_rows])),
2269                Arc::new(UInt8Array::from(vec![0u8; num_rows])),
2270            ],
2271        )
2272        .unwrap()
2273    }
2274
2275    /// Splits `timestamps` and returns the time index values of each sub-batch.
2276    fn split_ts(timestamps: &[i64]) -> Vec<Vec<i64>> {
2277        let mut batches = VecDeque::new();
2278        split_record_batch(flat_ts_batch(timestamps), &mut batches);
2279        batches
2280            .iter()
2281            .map(|batch| {
2282                let pos = time_index_column_index(batch.num_columns());
2283                let (values, _) = timestamp_array_to_primitive(batch.column(pos)).unwrap();
2284                values.values().to_vec()
2285            })
2286            .collect()
2287    }
2288
2289    #[test]
2290    fn test_split_record_batch_on_equal_timestamps() {
2291        // Splits on both decreasing and equal timestamps.
2292        assert_eq!(
2293            split_ts(&[1, 2, 2, 3, 1]),
2294            vec![vec![1, 2], vec![2, 3], vec![1]]
2295        );
2296        // A run of equal timestamps yields single-row sub-batches.
2297        assert_eq!(split_ts(&[5, 5, 5]), vec![vec![5], vec![5], vec![5]]);
2298        // Equal-ts run at the leading edge of the batch.
2299        assert_eq!(split_ts(&[5, 5, 1, 2]), vec![vec![5], vec![5], vec![1, 2]]);
2300        // Equal-ts run at the trailing edge of the batch.
2301        assert_eq!(split_ts(&[1, 2, 5, 5]), vec![vec![1, 2, 5], vec![5]]);
2302    }
2303
2304    #[test]
2305    fn test_split_record_batch_on_decreasing_timestamps() {
2306        assert_eq!(split_ts(&[1, 2, 3]), vec![vec![1, 2, 3]]);
2307        assert_eq!(split_ts(&[1, 3, 2, 4]), vec![vec![1, 3], vec![2, 4]]);
2308    }
2309
2310    #[test]
2311    fn test_split_record_batch_empty_and_single_row() {
2312        let mut batches = VecDeque::new();
2313        split_record_batch(flat_ts_batch(&[]), &mut batches);
2314        assert!(batches.is_empty());
2315
2316        assert_eq!(split_ts(&[42]), vec![vec![42]]);
2317    }
2318}
2319
2320#[cfg(test)]
2321mod sequence_filter_tests {
2322    use std::sync::Arc;
2323
2324    use datatypes::arrow::array::{Int64Array, StringArray, UInt8Array, UInt64Array};
2325    use datatypes::arrow::datatypes::{DataType, Field, Schema};
2326    use datatypes::arrow::record_batch::RecordBatch;
2327    use store_api::storage::SequenceRange;
2328
2329    use super::filter_flat_batch_by_sequence;
2330
2331    /// Builds a flat-format record batch: `(tag, field, ts, __primary_key, __sequence, __op_type)`.
2332    fn batch(sequences: &[u64]) -> RecordBatch {
2333        let schema = Arc::new(Schema::new(vec![
2334            Field::new("tag_0", DataType::Utf8, false),
2335            Field::new("field_0", DataType::Int64, false),
2336            Field::new(
2337                "ts",
2338                DataType::Timestamp(datatypes::arrow::datatypes::TimeUnit::Millisecond, None),
2339                false,
2340            ),
2341            Field::new("__primary_key", DataType::UInt8, false),
2342            Field::new("__sequence", DataType::UInt64, false),
2343            Field::new("__op_type", DataType::UInt8, false),
2344        ]));
2345        let tags = StringArray::from_iter_values((0..sequences.len()).map(|i| i.to_string()));
2346        let fields = Int64Array::from_iter_values(0..sequences.len() as i64);
2347        let ts = datatypes::arrow::array::TimestampMillisecondArray::from_iter_values(
2348            (0..sequences.len()).map(|i| i as i64 * 1000),
2349        );
2350        let pk = UInt8Array::from(vec![0u8; sequences.len()]);
2351        let seq = UInt64Array::from_iter_values(sequences.iter().copied());
2352        let op = UInt8Array::from(vec![0u8; sequences.len()]);
2353        RecordBatch::try_new(
2354            schema,
2355            vec![
2356                Arc::new(tags),
2357                Arc::new(fields),
2358                Arc::new(ts),
2359                Arc::new(pk),
2360                Arc::new(seq),
2361                Arc::new(op),
2362            ],
2363        )
2364        .unwrap()
2365    }
2366
2367    fn remaining_tags(batch: &RecordBatch) -> Vec<String> {
2368        batch
2369            .column(0)
2370            .as_any()
2371            .downcast_ref::<StringArray>()
2372            .unwrap()
2373            .iter()
2374            .map(|v| v.unwrap().to_string())
2375            .collect()
2376    }
2377
2378    #[test]
2379    fn test_filter_flat_batch_by_sequence_no_range_or_legacy_file() {
2380        let b = batch(&[1, 2, 3, 4]);
2381
2382        let out = filter_flat_batch_by_sequence(b.clone(), None, true).unwrap();
2383        assert_eq!(remaining_tags(&out.unwrap()), vec!["0", "1", "2", "3"]);
2384
2385        let out = filter_flat_batch_by_sequence(
2386            b.clone(),
2387            Some(SequenceRange::GtLtEq { min: 2, max: 3 }),
2388            false,
2389        )
2390        .unwrap();
2391        assert_eq!(remaining_tags(&out.unwrap()), vec!["0", "1", "2", "3"]);
2392    }
2393
2394    #[test]
2395    fn test_filter_flat_batch_by_sequence_exact_range() {
2396        let b = batch(&[1, 2, 3, 4]);
2397        let out = filter_flat_batch_by_sequence(
2398            b.clone(),
2399            Some(SequenceRange::GtLtEq { min: 2, max: 3 }),
2400            true,
2401        )
2402        .unwrap();
2403        assert_eq!(remaining_tags(&out.unwrap()), vec!["2"]);
2404
2405        let out = filter_flat_batch_by_sequence(
2406            b.clone(),
2407            Some(SequenceRange::GtLtEq { min: 10, max: 20 }),
2408            true,
2409        )
2410        .unwrap();
2411        assert!(out.is_none());
2412
2413        let empty = batch(&[]);
2414        let out = filter_flat_batch_by_sequence(
2415            empty,
2416            Some(SequenceRange::GtLtEq { min: 0, max: 10 }),
2417            true,
2418        )
2419        .unwrap();
2420        assert_eq!(out.unwrap().num_rows(), 0);
2421
2422        // Foreign batches are already virtualized by the reader. Their source
2423        // marker is irrelevant, and filtering uses the effective batch values.
2424        let out = filter_flat_batch_by_sequence(
2425            batch(&[1, 5, 9]),
2426            Some(SequenceRange::GtLtEq { min: 2, max: 8 }),
2427            true,
2428        )
2429        .unwrap();
2430        assert_eq!(remaining_tags(&out.unwrap()), vec!["1"]);
2431    }
2432}