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