Skip to main content

mito2/read/
series_scan.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//! Per-series scan implementation.
16
17use std::fmt;
18use std::sync::atomic::{AtomicUsize, Ordering};
19use std::sync::{Arc, Mutex};
20use std::time::{Duration, Instant};
21
22use async_stream::try_stream;
23use common_error::ext::BoxedError;
24use common_recordbatch::util::ChainedRecordBatchStream;
25use common_recordbatch::{RecordBatchStreamWrapper, SendableRecordBatchStream};
26use common_telemetry::tracing::{self, Instrument};
27use common_telemetry::warn;
28use datafusion::execution::memory_pool::MemoryPool;
29use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet;
30use datafusion::physical_plan::{DisplayAs, DisplayFormatType};
31use datatypes::arrow::array::BinaryArray;
32use datatypes::arrow::record_batch::RecordBatch;
33use datatypes::schema::SchemaRef;
34use futures::{StreamExt, TryStreamExt};
35use smallvec::SmallVec;
36use snafu::{OptionExt, ResultExt, ensure};
37use store_api::metadata::RegionMetadataRef;
38use store_api::region_engine::{
39    PartitionRange, PrepareRequest, QueryScanContext, RegionScanner, ScannerProperties,
40};
41use tokio::sync::Semaphore;
42use tokio::sync::mpsc::error::{SendTimeoutError, TrySendError};
43use tokio::sync::mpsc::{self, Receiver, Sender};
44
45use crate::error::{
46    Error, InvalidSenderSnafu, JoinSnafu, PartitionOutOfRangeSnafu, Result, ScanMultiTimesSnafu,
47    ScanSeriesSnafu, TooManyFilesToReadSnafu,
48};
49use crate::read::ScannerMetrics;
50use crate::read::pruner::{PartitionPruner, Pruner, PrunerOptions};
51use crate::read::scan_region::{ScanInput, StreamContext};
52use crate::read::scan_util::{
53    PartitionMetrics, PartitionMetricsList, SeriesDistributorMetrics, compute_average_batch_size,
54    compute_parallel_channel_size,
55};
56use crate::read::seq_scan::{FlatReadMode, SeqScan};
57use crate::read::series_candidate::{SeriesCandidateScanner, is_sparse_metric_metadata};
58use crate::read::series_reader::{AssignedSeriesBatch, SeriesBatchCollector, SeriesReader};
59use crate::read::stream::{ConvertBatchStream, ScanBatch, ScanBatchStream};
60use crate::sst::parquet::flat_format::primary_key_column_index;
61use crate::sst::parquet::format::PrimaryKeyArray;
62
63/// Timeout to send a batch to a sender.
64const SEND_TIMEOUT: Duration = Duration::from_micros(100);
65
66/// Maximum number of candidate series retained before distributing assignments.
67const CANDIDATE_SERIES_ASSIGNMENT_THRESHOLD: usize = 1_000_000;
68
69/// Legacy data receivers for output partitions.
70type LegacyReceiverList = Vec<Option<Receiver<Result<SeriesBatch>>>>;
71
72/// Candidate assignment receivers for output partitions.
73type CandidateReceiverList = Vec<Option<CandidateReceiver>>;
74
75/// Candidate assignment senders for output partitions.
76type CandidateSenderList = Vec<Option<mpsc::UnboundedSender<Result<SeriesReaderInput>>>>;
77
78struct CandidateReceiver {
79    receiver: mpsc::UnboundedReceiver<Result<SeriesReaderInput>>,
80    active_receivers: Arc<AtomicUsize>,
81}
82
83impl CandidateReceiver {
84    async fn recv(&mut self) -> Option<Result<SeriesReaderInput>> {
85        self.receiver.recv().await
86    }
87}
88
89impl Drop for CandidateReceiver {
90    fn drop(&mut self) {
91        let previous = self.active_receivers.fetch_sub(1, Ordering::Relaxed);
92        debug_assert!(previous > 0);
93    }
94}
95
96/// Input required by a partition-local series reader.
97struct SeriesReaderInput {
98    assigned_series: AssignedSeriesBatch,
99    partition_pruner: Arc<PartitionPruner>,
100    range_semaphore: Arc<Semaphore>,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104enum SeriesScanMode {
105    Legacy,
106    TwoPhase,
107}
108
109impl SeriesScanMode {
110    fn as_str(self) -> &'static str {
111        match self {
112            Self::Legacy => "legacy",
113            Self::TwoPhase => "two_phase",
114        }
115    }
116}
117
118/// Scans a region and returns sorted rows of a series in the same partition.
119///
120/// The output order is always order by `(primary key, time index)` inside every
121/// partition.
122/// Always returns the same series (primary key) to the same partition.
123pub struct SeriesScan {
124    /// Implementation used by this scan.
125    mode: SeriesScanMode,
126    /// Properties of the scanner.
127    properties: ScannerProperties,
128    /// Context of streams.
129    stream_ctx: Arc<StreamContext>,
130    /// Shared pruner for file range building.
131    pruner: Arc<Pruner>,
132    /// Legacy data receivers for each partition.
133    legacy_receivers: Mutex<LegacyReceiverList>,
134    /// Candidate assignment receivers for each partition.
135    candidate_receivers: Mutex<CandidateReceiverList>,
136    /// Metrics for each partition.
137    /// The scanner only sets in query and keeps it empty during compaction.
138    metrics_list: Arc<PartitionMetricsList>,
139}
140
141impl SeriesScan {
142    /// Creates a new [SeriesScan].
143    pub(crate) fn new(input: ScanInput, experimental_series_scan_v2: bool) -> Self {
144        let mode = if experimental_series_scan_v2 && Self::supports_two_phase(&input) {
145            SeriesScanMode::TwoPhase
146        } else {
147            SeriesScanMode::Legacy
148        };
149        let mut properties = ScannerProperties::default()
150            .with_append_mode(input.append_mode)
151            .with_total_rows(input.total_rows())
152            .with_total_rows_is_exact(input.append_mode && input.total_rows_is_exact());
153        if let Some(counters) = input.query_stat_counters.clone() {
154            properties.set_query_stat_counters(counters);
155        }
156        let stream_ctx = Arc::new(StreamContext::seq_scan_ctx(input));
157        properties.partitions = vec![stream_ctx.partition_ranges()];
158
159        // Create the shared pruner with number of workers equal to CPU cores.
160        let num_workers = common_stat::get_total_cpu_cores().max(1);
161        let pruner = match mode {
162            SeriesScanMode::Legacy => Arc::new(Pruner::new(stream_ctx.clone(), num_workers)),
163            SeriesScanMode::TwoPhase => Arc::new(Pruner::new_with_options(
164                stream_ctx.clone(),
165                num_workers,
166                PrunerOptions {
167                    retain_builders: true,
168                    enable_predicate_prefilter: false,
169                },
170            )),
171        };
172
173        Self {
174            mode,
175            properties,
176            stream_ctx,
177            pruner,
178            legacy_receivers: Mutex::new(Vec::new()),
179            candidate_receivers: Mutex::new(Vec::new()),
180            metrics_list: Arc::new(PartitionMetricsList::default()),
181        }
182    }
183
184    fn supports_two_phase(input: &ScanInput) -> bool {
185        if !is_sparse_metric_metadata(input.region_metadata()) {
186            return false;
187        }
188        #[cfg(feature = "enterprise")]
189        if !input.extension_ranges().is_empty() {
190            return false;
191        }
192        true
193    }
194
195    #[tracing::instrument(
196        skip_all,
197        fields(
198            region_id = %self.stream_ctx.input.mapper.metadata().region_id,
199            partition = partition
200        )
201    )]
202    fn scan_partition_impl(
203        &self,
204        ctx: &QueryScanContext,
205        metrics_set: &ExecutionPlanMetricsSet,
206        partition: usize,
207    ) -> Result<SendableRecordBatchStream> {
208        let metrics = new_partition_metrics(
209            &self.stream_ctx,
210            ctx.explain_verbose,
211            metrics_set,
212            partition,
213            &self.metrics_list,
214        );
215
216        let batch_stream =
217            self.scan_batch_in_partition(ctx, partition, metrics.clone(), metrics_set)?;
218
219        let input = &self.stream_ctx.input;
220        let record_batch_stream = ConvertBatchStream::new(
221            batch_stream,
222            input.mapper.clone(),
223            input.cache_strategy.clone(),
224            metrics,
225        );
226
227        Ok(Box::pin(RecordBatchStreamWrapper::new(
228            input.mapper.output_schema(),
229            Box::pin(record_batch_stream),
230        )))
231    }
232
233    #[tracing::instrument(
234        skip_all,
235        fields(
236            region_id = %self.stream_ctx.input.mapper.metadata().region_id,
237            partition = partition
238        )
239    )]
240    fn scan_batch_in_partition(
241        &self,
242        ctx: &QueryScanContext,
243        partition: usize,
244        part_metrics: PartitionMetrics,
245        metrics_set: &ExecutionPlanMetricsSet,
246    ) -> Result<ScanBatchStream> {
247        if ctx.explain_verbose {
248            common_telemetry::info!(
249                "SeriesScan partition {}, region_id: {}",
250                partition,
251                self.stream_ctx.input.region_metadata().region_id
252            );
253        }
254
255        ensure!(
256            partition < self.properties.num_partitions(),
257            PartitionOutOfRangeSnafu {
258                given: partition,
259                all: self.properties.num_partitions(),
260            }
261        );
262
263        match self.mode {
264            SeriesScanMode::Legacy => self.scan_legacy_batch_in_partition(
265                partition,
266                part_metrics,
267                metrics_set,
268                ctx.explain_verbose,
269            ),
270            SeriesScanMode::TwoPhase => {
271                self.scan_two_phase_batch_in_partition(ctx, partition, part_metrics, metrics_set)
272            }
273        }
274    }
275
276    fn scan_legacy_batch_in_partition(
277        &self,
278        partition: usize,
279        part_metrics: PartitionMetrics,
280        metrics_set: &ExecutionPlanMetricsSet,
281        explain_verbose: bool,
282    ) -> Result<ScanBatchStream> {
283        self.maybe_start_legacy_distributor(metrics_set, explain_verbose);
284        let mut receiver = self.legacy_receivers.lock().unwrap()[partition]
285            .take()
286            .context(ScanMultiTimesSnafu { partition })?;
287        let stream = try_stream! {
288            part_metrics.on_first_poll();
289
290            let mut fetch_start = Instant::now();
291            let mut metrics = ScannerMetrics::default();
292            while let Some(series) = receiver.recv().await {
293                let series = series?;
294                metrics.scan_cost += fetch_start.elapsed();
295                metrics.num_batches += series.num_batches();
296                metrics.num_rows += series.num_rows();
297
298                let yield_start = Instant::now();
299                yield ScanBatch::Series(series);
300                metrics.yield_cost += yield_start.elapsed();
301                fetch_start = Instant::now();
302            }
303
304            part_metrics.merge_metrics(&metrics);
305            part_metrics.on_finish();
306        };
307        Ok(Box::pin(stream))
308    }
309
310    fn scan_two_phase_batch_in_partition(
311        &self,
312        ctx: &QueryScanContext,
313        partition: usize,
314        part_metrics: PartitionMetrics,
315        metrics_set: &ExecutionPlanMetricsSet,
316    ) -> Result<ScanBatchStream> {
317        self.maybe_start_candidate_distributor(ctx, metrics_set);
318        // Safety: `scan_batch_in_partition` validates the index, and the receiver
319        // list always contains one entry for each output partition.
320        let mut receiver = self.candidate_receivers.lock().unwrap()[partition]
321            .take()
322            .context(ScanMultiTimesSnafu { partition })?;
323        let stream_ctx = self.stream_ctx.clone();
324        let partition_ranges = self
325            .properties
326            .partitions
327            .iter()
328            .flatten()
329            .copied()
330            .collect::<Vec<_>>();
331        let stream = try_stream! {
332            part_metrics.on_first_poll();
333
334            let mut fetch_start = Instant::now();
335            let mut metrics = ScannerMetrics::default();
336            while let Some(input) = receiver.recv().await {
337                let input = input?;
338                metrics.scan_cost += fetch_start.elapsed();
339
340                let build_start = Instant::now();
341                let reader = SeriesReader::try_new(
342                    stream_ctx.clone(),
343                    partition_ranges.clone(),
344                    input.assigned_series,
345                    input.partition_pruner,
346                    input.range_semaphore,
347                    part_metrics.clone(),
348                )?;
349                let mut reader_stream = reader.build_stream().await?;
350                metrics.scan_cost += build_start.elapsed();
351                fetch_start = Instant::now();
352                while let Some(record_batch) = reader_stream.try_next().await? {
353                    metrics.scan_cost += fetch_start.elapsed();
354                    metrics.num_batches += 1;
355                    metrics.num_rows += record_batch.num_rows();
356
357                    let yield_start = Instant::now();
358                    yield ScanBatch::RecordBatch(record_batch);
359                    metrics.yield_cost += yield_start.elapsed();
360                    fetch_start = Instant::now();
361                }
362            }
363            metrics.scan_cost += fetch_start.elapsed();
364
365            part_metrics.merge_metrics(&metrics);
366            part_metrics.on_finish();
367        };
368        Ok(Box::pin(stream))
369    }
370
371    fn maybe_start_legacy_distributor(
372        &self,
373        metrics_set: &ExecutionPlanMetricsSet,
374        explain_verbose: bool,
375    ) {
376        let mut rx_list = self.legacy_receivers.lock().unwrap();
377        if !rx_list.is_empty() {
378            return;
379        }
380
381        let (senders, receivers) = new_legacy_channel_list(self.properties.num_partitions());
382        let mut distributor = SeriesDistributor {
383            stream_ctx: self.stream_ctx.clone(),
384            range_semaphore: Some(Arc::new(Semaphore::new(self.properties.num_partitions()))),
385            final_merge_semaphore: Some(Arc::new(Semaphore::new(self.properties.num_partitions()))),
386            partitions: self.properties.partitions.clone(),
387            pruner: self.pruner.clone(),
388            senders,
389            metrics_set: metrics_set.clone(),
390            metrics_list: self.metrics_list.clone(),
391            explain_verbose,
392        };
393        let region_id = distributor.stream_ctx.input.mapper.metadata().region_id;
394        let span = tracing::info_span!("SeriesScan::distributor", region_id = %region_id);
395        common_runtime::spawn_query(
396            async move {
397                distributor.execute().await;
398            }
399            .instrument(span),
400        );
401
402        *rx_list = receivers;
403    }
404
405    fn maybe_start_candidate_distributor(
406        &self,
407        ctx: &QueryScanContext,
408        metrics_set: &ExecutionPlanMetricsSet,
409    ) {
410        let mut rx_list = self.candidate_receivers.lock().unwrap();
411        if !rx_list.is_empty() {
412            return;
413        }
414
415        let (senders, receivers, active_receivers) =
416            new_candidate_channel_list(self.properties.num_partitions());
417        // Capture the first partition's runtime pool once for the shared distributor.
418        // reset_state clears the receivers so the next execution captures a fresh pool.
419        let mut distributor = SeriesCandidateDistributor {
420            memory_pool: ctx.memory_pool.clone(),
421            stream_ctx: self.stream_ctx.clone(),
422            range_semaphore: Arc::new(Semaphore::new(self.properties.num_partitions())),
423            partitions: self.properties.partitions.clone(),
424            pruner: self.pruner.clone(),
425            senders,
426            active_receivers,
427            metrics_set: metrics_set.clone(),
428            metrics_list: self.metrics_list.clone(),
429            explain_verbose: ctx.explain_verbose,
430        };
431        let region_id = distributor.stream_ctx.input.mapper.metadata().region_id;
432        let span = tracing::info_span!("SeriesScan::candidate_distributor", region_id = %region_id);
433        common_runtime::spawn_query(
434            async move {
435                distributor.execute().await;
436            }
437            .instrument(span),
438        );
439
440        *rx_list = receivers;
441    }
442
443    /// Scans the region and returns a stream.
444    #[tracing::instrument(
445        skip_all,
446        fields(region_id = %self.stream_ctx.input.mapper.metadata().region_id)
447    )]
448    pub(crate) async fn build_stream(&self) -> Result<SendableRecordBatchStream, BoxedError> {
449        let part_num = self.properties.num_partitions();
450        let metrics_set = ExecutionPlanMetricsSet::default();
451        let ctx = QueryScanContext::default();
452        let streams = (0..part_num)
453            .map(|i| self.scan_partition(&ctx, &metrics_set, i))
454            .collect::<Result<Vec<_>, BoxedError>>()?;
455        let chained_stream = ChainedRecordBatchStream::new(streams).map_err(BoxedError::new)?;
456        Ok(Box::pin(chained_stream))
457    }
458
459    /// Scan [`Batch`] in all partitions one by one.
460    pub(crate) fn scan_all_partitions(&self) -> Result<ScanBatchStream> {
461        let metrics_set = ExecutionPlanMetricsSet::new();
462
463        let ctx = QueryScanContext::default();
464        let streams = (0..self.properties.partitions.len())
465            .map(|partition| {
466                let metrics = new_partition_metrics(
467                    &self.stream_ctx,
468                    false,
469                    &metrics_set,
470                    partition,
471                    &self.metrics_list,
472                );
473
474                self.scan_batch_in_partition(&ctx, partition, metrics, &metrics_set)
475            })
476            .collect::<Result<Vec<_>>>()?;
477
478        Ok(Box::pin(futures::stream::iter(streams).flatten()))
479    }
480
481    /// Checks resource limit for the scanner.
482    pub(crate) fn check_scan_limit(&self) -> Result<()> {
483        if self.stream_ctx.input.uses_scan_memory_limit() {
484            return Ok(());
485        }
486        // Sum the total number of files across all partitions
487        let total_files: usize = self
488            .properties
489            .partitions
490            .iter()
491            .flat_map(|partition| partition.iter())
492            .map(|part_range| {
493                let range_meta = &self.stream_ctx.ranges[part_range.identifier];
494                range_meta.indices.len()
495            })
496            .sum();
497
498        let max_concurrent_files = self.stream_ctx.input.max_concurrent_scan_files;
499        if total_files > max_concurrent_files {
500            return TooManyFilesToReadSnafu {
501                actual: total_files,
502                max: max_concurrent_files,
503            }
504            .fail();
505        }
506
507        Ok(())
508    }
509}
510
511fn new_legacy_channel_list(num_partitions: usize) -> (SenderList, LegacyReceiverList) {
512    let (senders, receivers): (Vec<_>, Vec<_>) = (0..num_partitions)
513        .map(|_| {
514            let (sender, receiver) = mpsc::channel(1);
515            (Some(sender), Some(receiver))
516        })
517        .unzip();
518    (SenderList::new(senders), receivers)
519}
520
521fn new_candidate_channel_list(
522    num_partitions: usize,
523) -> (CandidateSenderList, CandidateReceiverList, Arc<AtomicUsize>) {
524    let active_receivers = Arc::new(AtomicUsize::new(num_partitions));
525    let (senders, receivers) = (0..num_partitions)
526        .map(|_| {
527            // Partition streams can be consumed sequentially. A bounded channel for an unpolled
528            // partition could block the distributor and prevent it from sending more assignments
529            // to the partition currently being consumed, causing a deadlock.
530            let (sender, receiver) = mpsc::unbounded_channel();
531            let receiver = CandidateReceiver {
532                receiver,
533                active_receivers: active_receivers.clone(),
534            };
535            (Some(sender), Some(receiver))
536        })
537        .unzip();
538    (senders, receivers, active_receivers)
539}
540
541impl RegionScanner for SeriesScan {
542    fn name(&self) -> &str {
543        "SeriesScan"
544    }
545
546    fn properties(&self) -> &ScannerProperties {
547        &self.properties
548    }
549
550    fn schema(&self) -> SchemaRef {
551        self.stream_ctx.input.mapper.output_schema()
552    }
553
554    fn metadata(&self) -> RegionMetadataRef {
555        self.stream_ctx.input.mapper.metadata().clone()
556    }
557
558    fn scan_partition(
559        &self,
560        ctx: &QueryScanContext,
561        metrics_set: &ExecutionPlanMetricsSet,
562        partition: usize,
563    ) -> Result<SendableRecordBatchStream, BoxedError> {
564        self.scan_partition_impl(ctx, metrics_set, partition)
565            .map_err(BoxedError::new)
566    }
567
568    fn prepare(&mut self, request: PrepareRequest) -> Result<(), BoxedError> {
569        self.properties.prepare(request);
570
571        self.check_scan_limit().map_err(BoxedError::new)?;
572
573        Ok(())
574    }
575
576    fn has_predicate_without_region(&self) -> bool {
577        let predicate = self
578            .stream_ctx
579            .input
580            .predicate_group()
581            .predicate_without_region();
582        predicate.is_some()
583    }
584
585    fn add_dyn_filter_to_predicate(
586        &mut self,
587        filter_exprs: Vec<Arc<dyn datafusion::physical_plan::PhysicalExpr>>,
588    ) -> Vec<bool> {
589        self.stream_ctx.add_dyn_filter_to_predicate(filter_exprs)
590    }
591
592    fn reset_state(&mut self) {
593        self.stream_ctx.input.predicate.clear_dyn_filters();
594        let num_workers = common_stat::get_total_cpu_cores().max(1);
595        self.pruner = match self.mode {
596            SeriesScanMode::Legacy => Arc::new(Pruner::new(self.stream_ctx.clone(), num_workers)),
597            SeriesScanMode::TwoPhase => Arc::new(Pruner::new_with_options(
598                self.stream_ctx.clone(),
599                num_workers,
600                PrunerOptions {
601                    retain_builders: true,
602                    enable_predicate_prefilter: false,
603                },
604            )),
605        };
606        self.legacy_receivers.lock().unwrap().clear();
607        self.candidate_receivers.lock().unwrap().clear();
608        self.metrics_list = Arc::new(PartitionMetricsList::default());
609    }
610
611    fn set_logical_region(&mut self, logical_region: bool) {
612        self.properties.set_logical_region(logical_region);
613    }
614
615    fn set_query_load_region_id(&mut self, region_id: store_api::storage::RegionId) {
616        self.properties.set_query_load_region_id(region_id);
617    }
618
619    fn snapshot_sequence(&self) -> Option<u64> {
620        self.stream_ctx.input.snapshot_sequence
621    }
622}
623
624impl DisplayAs for SeriesScan {
625    fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
626        write!(
627            f,
628            "SeriesScan: region={}, ",
629            self.stream_ctx.input.mapper.metadata().region_id
630        )?;
631        match t {
632            DisplayFormatType::Default | DisplayFormatType::TreeRender => {
633                self.stream_ctx.format_for_explain(false, f)?;
634            }
635            DisplayFormatType::Verbose => {
636                self.stream_ctx.format_for_explain(true, f)?;
637            }
638        }
639        write!(f, ", \"mode\":\"{}\"", self.mode.as_str())?;
640        if matches!(t, DisplayFormatType::Verbose) {
641            self.metrics_list.format_verbose_metrics(f)?;
642        }
643        Ok(())
644    }
645}
646
647impl fmt::Debug for SeriesScan {
648    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
649        f.debug_struct("SeriesScan")
650            .field("mode", &self.mode)
651            .field("num_ranges", &self.stream_ctx.ranges.len())
652            .finish()
653    }
654}
655
656#[cfg(test)]
657impl SeriesScan {
658    /// Returns the input.
659    pub(crate) fn input(&self) -> &ScanInput {
660        &self.stream_ctx.input
661    }
662
663    /// Returns the scan mode for tests.
664    pub(crate) fn mode(&self) -> &'static str {
665        self.mode.as_str()
666    }
667}
668
669/// The distributor scans series and distributes them to different partitions.
670struct SeriesDistributor {
671    /// Context for the scan stream.
672    stream_ctx: Arc<StreamContext>,
673    /// Semaphore for file scanning and range-level merging.
674    range_semaphore: Option<Arc<Semaphore>>,
675    /// Semaphore for the final merge across all range streams.
676    /// Must be separate from `range_semaphore` to avoid deadlock: final merge tasks
677    /// hold a permit while waiting for data from range-level merge tasks, which also
678    /// need permits to produce data.
679    final_merge_semaphore: Option<Arc<Semaphore>>,
680    /// Partition ranges to scan.
681    partitions: Vec<Vec<PartitionRange>>,
682    /// Shared pruner for file range building.
683    pruner: Arc<Pruner>,
684    /// Senders of all partitions.
685    senders: SenderList,
686    /// Metrics set to report.
687    /// The distributor report the metrics as an additional partition.
688    /// This may double the scan cost of the [SeriesScan] metrics. We can
689    /// get per-partition metrics in verbose mode to see the metrics of the
690    /// distributor.
691    metrics_set: ExecutionPlanMetricsSet,
692    metrics_list: Arc<PartitionMetricsList>,
693    /// Whether to use verbose logging and collect detailed metrics.
694    explain_verbose: bool,
695}
696
697impl SeriesDistributor {
698    /// Executes the distributor.
699    #[tracing::instrument(
700        skip_all,
701        fields(region_id = %self.stream_ctx.input.mapper.metadata().region_id)
702    )]
703    async fn execute(&mut self) {
704        if let Err(e) = self.scan_partitions_flat().await {
705            self.senders.send_error(e).await;
706        }
707    }
708
709    /// Scans all parts in flat format using FlatSeriesBatchDivider.
710    #[tracing::instrument(
711        skip_all,
712        fields(region_id = %self.stream_ctx.input.mapper.metadata().region_id)
713    )]
714    async fn scan_partitions_flat(&mut self) -> Result<()> {
715        // Initialize reference counts for all partition ranges.
716        for partition_ranges in &self.partitions {
717            self.pruner.add_partition_ranges(partition_ranges);
718        }
719
720        // Create PartitionPruner covering all partitions
721        let all_partition_ranges: Vec<_> = self.partitions.iter().flatten().cloned().collect();
722        let needs_merge = all_partition_ranges.len() > 1;
723        let partition_pruner = Arc::new(PartitionPruner::new(
724            self.pruner.clone(),
725            &all_partition_ranges,
726        ));
727
728        let part_metrics = new_partition_metrics(
729            &self.stream_ctx,
730            self.explain_verbose,
731            &self.metrics_set,
732            self.partitions.len(),
733            &self.metrics_list,
734        );
735        part_metrics.on_first_poll();
736        // Start fetch time before building sources so scan cost contains
737        // build part cost.
738        let mut fetch_start = Instant::now();
739
740        // Builds one deduped stream per partition range, then merges across ranges.
741        let build_start = Instant::now();
742        let mut tasks = Vec::new();
743        for partition in &self.partitions {
744            for part_range in partition {
745                let stream_ctx = self.stream_ctx.clone();
746                let part_range = *part_range;
747                let part_metrics = part_metrics.clone();
748                let partition_pruner = partition_pruner.clone();
749                let file_scan_semaphore = self.range_semaphore.clone();
750                let merge_semaphore = self.range_semaphore.clone();
751                tasks.push(common_runtime::spawn_query(async move {
752                    SeqScan::build_flat_partition_range_read(
753                        &stream_ctx,
754                        &part_range,
755                        if needs_merge {
756                            FlatReadMode::CrossRangeMerge
757                        } else {
758                            FlatReadMode::Sequential
759                        },
760                        &part_metrics,
761                        partition_pruner,
762                        file_scan_semaphore,
763                        merge_semaphore,
764                    )
765                    .await
766                }));
767            }
768        }
769        let mut range_streams = Vec::with_capacity(tasks.len());
770        let mut estimated_batch_sizes = Vec::with_capacity(tasks.len());
771        for task in tasks {
772            let (stream, estimated_batch_size) = task.await.context(JoinSnafu)??;
773            range_streams.push(stream);
774            estimated_batch_sizes.push(estimated_batch_size);
775        }
776        let channel_size =
777            compute_parallel_channel_size(compute_average_batch_size(estimated_batch_sizes));
778        common_telemetry::debug!(
779            "SeriesDistributor built {} range_streams, region: {}, build cost: {:?}, channel_size: {}",
780            range_streams.len(),
781            self.stream_ctx.input.region_metadata().region_id,
782            build_start.elapsed(),
783            channel_size,
784        );
785
786        // Each partition range stream is already deduped, so skip dedup here.
787        // Use a separate semaphore for the final merge to avoid deadlock with
788        // range-level merge tasks that share the range_semaphore.
789        let mut reader = SeqScan::build_flat_reader_from_sources(
790            &self.stream_ctx,
791            range_streams,
792            self.final_merge_semaphore.clone(),
793            Some(&part_metrics),
794            true,
795            channel_size,
796        )
797        .await?;
798        let mut metrics = SeriesDistributorMetrics::default();
799
800        let mut divider = FlatSeriesBatchDivider::default();
801        while let Some(record_batch) = reader.try_next().await? {
802            metrics.scan_cost += fetch_start.elapsed();
803            metrics.num_batches += 1;
804            metrics.num_rows += record_batch.num_rows();
805
806            debug_assert!(record_batch.num_rows() > 0);
807            if record_batch.num_rows() == 0 {
808                fetch_start = Instant::now();
809                continue;
810            }
811
812            // Use divider to split series
813            let divider_start = Instant::now();
814            let series_batch = divider.push(record_batch);
815            metrics.divider_cost += divider_start.elapsed();
816            if let Some(series_batch) = series_batch {
817                let yield_start = Instant::now();
818                self.senders
819                    .send_batch(SeriesBatch::Flat(series_batch))
820                    .await?;
821                metrics.yield_cost += yield_start.elapsed();
822            }
823            fetch_start = Instant::now();
824        }
825
826        // Send any remaining batch in the divider
827        let divider_start = Instant::now();
828        let series_batch = divider.finish();
829        metrics.divider_cost += divider_start.elapsed();
830        if let Some(series_batch) = series_batch {
831            let yield_start = Instant::now();
832            self.senders
833                .send_batch(SeriesBatch::Flat(series_batch))
834                .await?;
835            metrics.yield_cost += yield_start.elapsed();
836        }
837
838        metrics.scan_cost += fetch_start.elapsed();
839        metrics.num_series_send_timeout = self.senders.num_timeout;
840        metrics.num_series_send_full = self.senders.num_full;
841        part_metrics.set_distributor_metrics(&metrics);
842
843        part_metrics.on_finish();
844
845        Ok(())
846    }
847}
848
849/// Discovers metric series and sends one reader assignment to each partition.
850struct SeriesCandidateDistributor {
851    memory_pool: Arc<dyn MemoryPool>,
852    stream_ctx: Arc<StreamContext>,
853    range_semaphore: Arc<Semaphore>,
854    partitions: Vec<Vec<PartitionRange>>,
855    pruner: Arc<Pruner>,
856    senders: CandidateSenderList,
857    active_receivers: Arc<AtomicUsize>,
858    metrics_set: ExecutionPlanMetricsSet,
859    metrics_list: Arc<PartitionMetricsList>,
860    explain_verbose: bool,
861}
862
863impl SeriesCandidateDistributor {
864    async fn execute(&mut self) {
865        if let Err(e) = self.distribute().await {
866            self.send_error(e);
867        }
868    }
869
870    async fn distribute(&mut self) -> Result<()> {
871        let part_metrics = new_partition_metrics(
872            &self.stream_ctx,
873            self.explain_verbose,
874            &self.metrics_set,
875            self.partitions.len(),
876            &self.metrics_list,
877        );
878        part_metrics.on_first_poll();
879
880        let candidate_scanner = SeriesCandidateScanner::try_new(
881            self.stream_ctx.clone(),
882            self.partitions.clone(),
883            self.pruner.clone(),
884            self.range_semaphore.clone(),
885            self.memory_pool.clone(),
886            self.metrics_set.clone(),
887            part_metrics.clone(),
888        )?;
889        let partition_pruner = candidate_scanner.partition_pruner();
890        let mut candidates = candidate_scanner.build_stream().await?;
891        let mut collector =
892            SeriesBatchCollector::new(self.partitions.len()).context(InvalidSenderSnafu)?;
893        let mut chunked = false;
894        while let Some(batch) = candidates.try_next().await? {
895            if !self.should_fetch_candidates() {
896                part_metrics.on_finish();
897                return Ok(());
898            }
899            collector.push(batch);
900            if collector.len() >= CANDIDATE_SERIES_ASSIGNMENT_THRESHOLD {
901                chunked = true;
902                self.send_assignments(collector.finish(false), &partition_pruner);
903                if !self.should_fetch_candidates() {
904                    part_metrics.on_finish();
905                    return Ok(());
906                }
907                collector =
908                    SeriesBatchCollector::new(self.partitions.len()).context(InvalidSenderSnafu)?;
909            }
910        }
911
912        if collector.len() > 0 {
913            self.send_assignments(collector.finish(!chunked), &partition_pruner);
914        }
915        part_metrics.on_finish();
916        Ok(())
917    }
918
919    fn send_assignments(
920        &mut self,
921        assignments: Vec<AssignedSeriesBatch>,
922        partition_pruner: &Arc<PartitionPruner>,
923    ) {
924        for (partition, assigned_series) in assignments.into_iter().enumerate() {
925            if assigned_series.series().is_empty() {
926                continue;
927            }
928            let Some(sender) = self.senders[partition].as_ref() else {
929                continue;
930            };
931            let sent = sender
932                .send(Ok(SeriesReaderInput {
933                    assigned_series,
934                    partition_pruner: partition_pruner.clone(),
935                    range_semaphore: self.range_semaphore.clone(),
936                }))
937                .is_ok();
938            if !sent {
939                self.senders[partition] = None;
940            }
941        }
942    }
943
944    fn should_fetch_candidates(&self) -> bool {
945        self.active_receivers.load(Ordering::Relaxed) > 0
946    }
947
948    fn send_error(&mut self, error: Error) {
949        let error = Arc::new(error);
950        for sender in self.senders.iter_mut().filter_map(Option::take) {
951            let result = Err(error.clone()).context(ScanSeriesSnafu);
952            let _ = sender.send(result);
953        }
954    }
955}
956
957/// Batches of the same series.
958#[derive(Debug)]
959pub enum SeriesBatch {
960    Flat(FlatSeriesBatch),
961}
962
963impl SeriesBatch {
964    /// Returns the number of batches.
965    pub fn num_batches(&self) -> usize {
966        match self {
967            SeriesBatch::Flat(flat_batch) => flat_batch.batches.len(),
968        }
969    }
970
971    /// Returns the total number of rows across all batches.
972    pub fn num_rows(&self) -> usize {
973        match self {
974            SeriesBatch::Flat(flat_batch) => flat_batch.batches.iter().map(|x| x.num_rows()).sum(),
975        }
976    }
977}
978
979/// Batches of the same series in flat format.
980#[derive(Default, Debug)]
981pub struct FlatSeriesBatch {
982    pub batches: SmallVec<[RecordBatch; 4]>,
983}
984
985/// List of senders.
986struct SenderList {
987    senders: Vec<Option<Sender<Result<SeriesBatch>>>>,
988    /// Number of None senders.
989    num_nones: usize,
990    /// Index of the current partition to send.
991    sender_idx: usize,
992    /// Number of timeout.
993    num_timeout: usize,
994    /// Number of full senders.
995    num_full: usize,
996}
997
998impl SenderList {
999    fn new(senders: Vec<Option<Sender<Result<SeriesBatch>>>>) -> Self {
1000        let num_nones = senders.iter().filter(|sender| sender.is_none()).count();
1001        Self {
1002            senders,
1003            num_nones,
1004            sender_idx: 0,
1005            num_timeout: 0,
1006            num_full: 0,
1007        }
1008    }
1009
1010    /// Finds a partition and tries to send the batch to the partition.
1011    /// Returns None if it sends successfully.
1012    fn try_send_batch(&mut self, mut batch: SeriesBatch) -> Result<Option<SeriesBatch>> {
1013        for _ in 0..self.senders.len() {
1014            ensure!(self.num_nones < self.senders.len(), InvalidSenderSnafu);
1015
1016            let sender_idx = self.fetch_add_sender_idx();
1017            let Some(sender) = &self.senders[sender_idx] else {
1018                continue;
1019            };
1020
1021            match sender.try_send(Ok(batch)) {
1022                Ok(()) => return Ok(None),
1023                Err(TrySendError::Full(res)) => {
1024                    self.num_full += 1;
1025                    // Safety: we send Ok.
1026                    batch = res.unwrap();
1027                }
1028                Err(TrySendError::Closed(res)) => {
1029                    self.senders[sender_idx] = None;
1030                    self.num_nones += 1;
1031                    // Safety: we send Ok.
1032                    batch = res.unwrap();
1033                }
1034            }
1035        }
1036
1037        Ok(Some(batch))
1038    }
1039
1040    /// Finds a partition and sends the batch to the partition.
1041    async fn send_batch(&mut self, mut batch: SeriesBatch) -> Result<()> {
1042        // Sends the batch without blocking first.
1043        match self.try_send_batch(batch)? {
1044            Some(b) => {
1045                // Unable to send batch to partition.
1046                batch = b;
1047            }
1048            None => {
1049                return Ok(());
1050            }
1051        }
1052
1053        loop {
1054            ensure!(self.num_nones < self.senders.len(), InvalidSenderSnafu);
1055
1056            let sender_idx = self.fetch_add_sender_idx();
1057            let Some(sender) = &self.senders[sender_idx] else {
1058                continue;
1059            };
1060            // Adds a timeout to avoid blocking indefinitely and sending
1061            // the batch in a round-robin fashion when some partitions
1062            // don't poll their inputs. This may happen if we have a
1063            // node like sort merging. But it is rare when we are using SeriesScan.
1064            match sender.send_timeout(Ok(batch), SEND_TIMEOUT).await {
1065                Ok(()) => break,
1066                Err(SendTimeoutError::Timeout(res)) => {
1067                    self.num_timeout += 1;
1068                    // Safety: we send Ok.
1069                    batch = res.unwrap();
1070                }
1071                Err(SendTimeoutError::Closed(res)) => {
1072                    self.senders[sender_idx] = None;
1073                    self.num_nones += 1;
1074                    // Safety: we send Ok.
1075                    batch = res.unwrap();
1076                }
1077            }
1078        }
1079
1080        Ok(())
1081    }
1082
1083    async fn send_error(&self, error: Error) {
1084        let error = Arc::new(error);
1085        for sender in self.senders.iter().flatten() {
1086            let result = Err(error.clone()).context(ScanSeriesSnafu);
1087            let _ = sender.send(result).await;
1088        }
1089    }
1090
1091    fn fetch_add_sender_idx(&mut self) -> usize {
1092        let sender_idx = self.sender_idx;
1093        self.sender_idx = (self.sender_idx + 1) % self.senders.len();
1094        sender_idx
1095    }
1096}
1097
1098fn new_partition_metrics(
1099    stream_ctx: &StreamContext,
1100    explain_verbose: bool,
1101    metrics_set: &ExecutionPlanMetricsSet,
1102    partition: usize,
1103    metrics_list: &PartitionMetricsList,
1104) -> PartitionMetrics {
1105    let metrics = PartitionMetrics::new(
1106        stream_ctx.input.mapper.metadata().region_id,
1107        partition,
1108        "SeriesScan",
1109        stream_ctx.query_start,
1110        explain_verbose,
1111        metrics_set,
1112    );
1113
1114    metrics_list.set(partition, metrics.clone());
1115    metrics
1116}
1117
1118/// A divider to split flat record batches by time series.
1119///
1120/// It only ensures rows of the same series are returned in the same [FlatSeriesBatch].
1121/// However, a [FlatSeriesBatch] may contain rows from multiple series.
1122#[derive(Default)]
1123struct FlatSeriesBatchDivider {
1124    buffer: FlatSeriesBatch,
1125}
1126
1127impl FlatSeriesBatchDivider {
1128    /// Pushes a record batch into the divider.
1129    ///
1130    /// Returns a [FlatSeriesBatch] if we ensure the batch contains all rows of the series in it.
1131    fn push(&mut self, batch: RecordBatch) -> Option<FlatSeriesBatch> {
1132        // If buffer is empty
1133        if self.buffer.batches.is_empty() {
1134            self.buffer.batches.push(batch);
1135            return None;
1136        }
1137
1138        // Gets the primary key column from the incoming batch.
1139        let pk_column_idx = primary_key_column_index(batch.num_columns());
1140        let batch_pk_column = batch.column(pk_column_idx);
1141        let batch_pk_array = batch_pk_column
1142            .as_any()
1143            .downcast_ref::<PrimaryKeyArray>()
1144            .unwrap();
1145        let batch_pk_values = batch_pk_array
1146            .values()
1147            .as_any()
1148            .downcast_ref::<BinaryArray>()
1149            .unwrap();
1150        // Gets the last primary key of the incoming batch.
1151        let batch_last_pk =
1152            primary_key_at(batch_pk_array, batch_pk_values, batch_pk_array.len() - 1);
1153        // Gets the last primary key of the buffer.
1154        // Safety: the buffer is not empty.
1155        let buffer_last_batch = self.buffer.batches.last().unwrap();
1156        let buffer_pk_column = buffer_last_batch.column(pk_column_idx);
1157        let buffer_pk_array = buffer_pk_column
1158            .as_any()
1159            .downcast_ref::<PrimaryKeyArray>()
1160            .unwrap();
1161        let buffer_pk_values = buffer_pk_array
1162            .values()
1163            .as_any()
1164            .downcast_ref::<BinaryArray>()
1165            .unwrap();
1166        let buffer_last_pk =
1167            primary_key_at(buffer_pk_array, buffer_pk_values, buffer_pk_array.len() - 1);
1168
1169        // If last primary key in the batch is the same as last primary key in the buffer.
1170        if batch_last_pk == buffer_last_pk {
1171            self.buffer.batches.push(batch);
1172            return None;
1173        }
1174        // Otherwise, the batch must have a different primary key, we find the first offset of the
1175        // changed primary key.
1176        let batch_pk_keys = batch_pk_array.keys();
1177        let pk_indices = batch_pk_keys.values();
1178        let mut change_offset = 0;
1179        for (i, &key) in pk_indices.iter().enumerate() {
1180            let batch_pk = batch_pk_values.value(key as usize);
1181
1182            if buffer_last_pk != batch_pk {
1183                change_offset = i;
1184                break;
1185            }
1186        }
1187
1188        // Splits the batch at the change offset
1189        let (first_part, remaining_part) = if change_offset > 0 {
1190            let first_part = batch.slice(0, change_offset);
1191            let remaining_part = batch.slice(change_offset, batch.num_rows() - change_offset);
1192            (Some(first_part), Some(remaining_part))
1193        } else {
1194            (None, Some(batch))
1195        };
1196
1197        // Creates the result from current buffer + first part of new batch
1198        let mut result = std::mem::take(&mut self.buffer);
1199        if let Some(first_part) = first_part {
1200            result.batches.push(first_part);
1201        }
1202
1203        // Pushes remaining part to the buffer if it exists
1204        if let Some(remaining_part) = remaining_part {
1205            self.buffer.batches.push(remaining_part);
1206        }
1207
1208        Some(result)
1209    }
1210
1211    /// Returns the final [FlatSeriesBatch].
1212    fn finish(&mut self) -> Option<FlatSeriesBatch> {
1213        if self.buffer.batches.is_empty() {
1214            None
1215        } else {
1216            Some(std::mem::take(&mut self.buffer))
1217        }
1218    }
1219}
1220
1221/// Helper function to extract primary key bytes at a specific index from [PrimaryKeyArray].
1222fn primary_key_at<'a>(
1223    primary_key: &PrimaryKeyArray,
1224    primary_key_values: &'a BinaryArray,
1225    index: usize,
1226) -> &'a [u8] {
1227    let key = primary_key.keys().value(index);
1228    primary_key_values.value(key as usize)
1229}
1230
1231#[cfg(test)]
1232mod tests {
1233    use std::sync::Arc;
1234
1235    use api::v1::OpType;
1236    use datatypes::arrow::array::{
1237        ArrayRef, BinaryDictionaryBuilder, Int64Array, StringDictionaryBuilder,
1238        TimestampMillisecondArray, UInt8Array, UInt64Array,
1239    };
1240    use datatypes::arrow::datatypes::{DataType, Field, Schema, SchemaRef, TimeUnit, UInt32Type};
1241    use datatypes::arrow::record_batch::RecordBatch;
1242
1243    use super::*;
1244    use crate::read::flat_projection::FlatProjectionMapper;
1245    use crate::read::scan_region::PredicateGroup;
1246    use crate::test_util::scheduler_util::SchedulerEnv;
1247    use crate::test_util::sst_util::sst_region_metadata_with_encoding;
1248
1249    #[tokio::test]
1250    async fn two_phase_eligibility_allows_exact_sequence_range() {
1251        let env = SchedulerEnv::new().await;
1252        let metadata = Arc::new(sst_region_metadata_with_encoding(
1253            store_api::codec::PrimaryKeyEncoding::Sparse,
1254        ));
1255        let predicate = PredicateGroup::default();
1256
1257        let eligible = ScanInput::builder(
1258            env.access_layer.clone(),
1259            FlatProjectionMapper::new(&metadata, [0]).unwrap(),
1260        )
1261        .with_predicate(predicate.clone())
1262        .build();
1263        assert!(SeriesScan::supports_two_phase(&eligible));
1264
1265        let exact_sequence = ScanInput::builder(
1266            env.access_layer.clone(),
1267            FlatProjectionMapper::new(&metadata, [0]).unwrap(),
1268        )
1269        .with_predicate(predicate)
1270        .with_sequence_range(Some(store_api::storage::SequenceRange::GtLtEq {
1271            min: 1,
1272            max: 2,
1273        }))
1274        .build();
1275        assert!(SeriesScan::supports_two_phase(&exact_sequence));
1276    }
1277
1278    #[test]
1279    fn candidate_distributor_stops_after_all_receivers_close() {
1280        let (_senders, mut receivers, active_receivers) = new_candidate_channel_list(2);
1281        assert_eq!(2, active_receivers.load(Ordering::Relaxed));
1282
1283        drop(receivers[0].take());
1284        assert_eq!(1, active_receivers.load(Ordering::Relaxed));
1285
1286        drop(receivers[1].take());
1287        assert_eq!(0, active_receivers.load(Ordering::Relaxed));
1288    }
1289
1290    fn new_test_record_batch(
1291        primary_keys: &[&[u8]],
1292        timestamps: &[i64],
1293        sequences: &[u64],
1294        op_types: &[OpType],
1295        fields: &[u64],
1296    ) -> RecordBatch {
1297        let num_rows = timestamps.len();
1298        debug_assert_eq!(sequences.len(), num_rows);
1299        debug_assert_eq!(op_types.len(), num_rows);
1300        debug_assert_eq!(fields.len(), num_rows);
1301        debug_assert_eq!(primary_keys.len(), num_rows);
1302
1303        let columns: Vec<ArrayRef> = vec![
1304            build_test_pk_string_dict_array(primary_keys),
1305            Arc::new(Int64Array::from_iter(
1306                fields.iter().map(|v| Some(*v as i64)),
1307            )),
1308            Arc::new(TimestampMillisecondArray::from_iter_values(
1309                timestamps.iter().copied(),
1310            )),
1311            build_test_pk_array(primary_keys),
1312            Arc::new(UInt64Array::from_iter_values(sequences.iter().copied())),
1313            Arc::new(UInt8Array::from_iter_values(
1314                op_types.iter().map(|v| *v as u8),
1315            )),
1316        ];
1317
1318        RecordBatch::try_new(build_test_flat_schema(), columns).unwrap()
1319    }
1320
1321    fn build_test_pk_string_dict_array(primary_keys: &[&[u8]]) -> ArrayRef {
1322        let mut builder = StringDictionaryBuilder::<UInt32Type>::new();
1323        for &pk in primary_keys {
1324            let pk_str = std::str::from_utf8(pk).unwrap();
1325            builder.append(pk_str).unwrap();
1326        }
1327        Arc::new(builder.finish())
1328    }
1329
1330    fn build_test_pk_array(primary_keys: &[&[u8]]) -> ArrayRef {
1331        let mut builder = BinaryDictionaryBuilder::<UInt32Type>::new();
1332        for &pk in primary_keys {
1333            builder.append(pk).unwrap();
1334        }
1335        Arc::new(builder.finish())
1336    }
1337
1338    fn build_test_flat_schema() -> SchemaRef {
1339        let fields = vec![
1340            Field::new(
1341                "k0",
1342                DataType::Dictionary(Box::new(DataType::UInt32), Box::new(DataType::Utf8)),
1343                false,
1344            ),
1345            Field::new("field0", DataType::Int64, true),
1346            Field::new(
1347                "ts",
1348                DataType::Timestamp(TimeUnit::Millisecond, None),
1349                false,
1350            ),
1351            Field::new(
1352                "__primary_key",
1353                DataType::Dictionary(Box::new(DataType::UInt32), Box::new(DataType::Binary)),
1354                false,
1355            ),
1356            Field::new("__sequence", DataType::UInt64, false),
1357            Field::new("__op_type", DataType::UInt8, false),
1358        ];
1359        Arc::new(Schema::new(fields))
1360    }
1361
1362    #[test]
1363    fn test_empty_buffer_first_push() {
1364        let mut divider = FlatSeriesBatchDivider::default();
1365        let result = divider.finish();
1366        assert!(result.is_none());
1367
1368        let mut divider = FlatSeriesBatchDivider::default();
1369        let batch = new_test_record_batch(
1370            &[b"series1", b"series1"],
1371            &[1000, 2000],
1372            &[1, 2],
1373            &[OpType::Put, OpType::Put],
1374            &[10, 20],
1375        );
1376        let result = divider.push(batch);
1377        assert!(result.is_none());
1378        assert_eq!(divider.buffer.batches.len(), 1);
1379    }
1380
1381    #[test]
1382    fn test_same_series_accumulation() {
1383        let mut divider = FlatSeriesBatchDivider::default();
1384
1385        let batch1 = new_test_record_batch(
1386            &[b"series1", b"series1"],
1387            &[1000, 2000],
1388            &[1, 2],
1389            &[OpType::Put, OpType::Put],
1390            &[10, 20],
1391        );
1392
1393        let batch2 = new_test_record_batch(
1394            &[b"series1", b"series1"],
1395            &[3000, 4000],
1396            &[3, 4],
1397            &[OpType::Put, OpType::Put],
1398            &[30, 40],
1399        );
1400
1401        divider.push(batch1);
1402        let result = divider.push(batch2);
1403        assert!(result.is_none());
1404        let series_batch = divider.finish().unwrap();
1405        assert_eq!(series_batch.batches.len(), 2);
1406    }
1407
1408    #[test]
1409    fn test_series_boundary_detection() {
1410        let mut divider = FlatSeriesBatchDivider::default();
1411
1412        let batch1 = new_test_record_batch(
1413            &[b"series1", b"series1"],
1414            &[1000, 2000],
1415            &[1, 2],
1416            &[OpType::Put, OpType::Put],
1417            &[10, 20],
1418        );
1419
1420        let batch2 = new_test_record_batch(
1421            &[b"series2", b"series2"],
1422            &[3000, 4000],
1423            &[3, 4],
1424            &[OpType::Put, OpType::Put],
1425            &[30, 40],
1426        );
1427
1428        divider.push(batch1);
1429        let series_batch = divider.push(batch2).unwrap();
1430        assert_eq!(series_batch.batches.len(), 1);
1431
1432        assert_eq!(divider.buffer.batches.len(), 1);
1433    }
1434
1435    #[test]
1436    fn test_series_boundary_within_batch() {
1437        let mut divider = FlatSeriesBatchDivider::default();
1438
1439        let batch1 = new_test_record_batch(
1440            &[b"series1", b"series1"],
1441            &[1000, 2000],
1442            &[1, 2],
1443            &[OpType::Put, OpType::Put],
1444            &[10, 20],
1445        );
1446
1447        let batch2 = new_test_record_batch(
1448            &[b"series1", b"series2"],
1449            &[3000, 4000],
1450            &[3, 4],
1451            &[OpType::Put, OpType::Put],
1452            &[30, 40],
1453        );
1454
1455        divider.push(batch1);
1456        let series_batch = divider.push(batch2).unwrap();
1457        assert_eq!(series_batch.batches.len(), 2);
1458        assert_eq!(series_batch.batches[0].num_rows(), 2);
1459        assert_eq!(series_batch.batches[1].num_rows(), 1);
1460
1461        assert_eq!(divider.buffer.batches.len(), 1);
1462        assert_eq!(divider.buffer.batches[0].num_rows(), 1);
1463    }
1464
1465    #[test]
1466    fn test_series_splitting() {
1467        let mut divider = FlatSeriesBatchDivider::default();
1468
1469        let batch1 = new_test_record_batch(&[b"series1"], &[1000], &[1], &[OpType::Put], &[10]);
1470
1471        let batch2 = new_test_record_batch(
1472            &[b"series1", b"series2", b"series2", b"series3"],
1473            &[2000, 3000, 4000, 5000],
1474            &[2, 3, 4, 5],
1475            &[OpType::Put, OpType::Put, OpType::Put, OpType::Put],
1476            &[20, 30, 40, 50],
1477        );
1478
1479        divider.push(batch1);
1480        let series_batch = divider.push(batch2).unwrap();
1481        assert_eq!(series_batch.batches.len(), 2);
1482
1483        let total_rows: usize = series_batch.batches.iter().map(|b| b.num_rows()).sum();
1484        assert_eq!(total_rows, 2);
1485
1486        let final_batch = divider.finish().unwrap();
1487        assert_eq!(final_batch.batches.len(), 1);
1488        assert_eq!(final_batch.batches[0].num_rows(), 3);
1489    }
1490}