Skip to main content

mito2/read/
series_candidate.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//! Candidate metric-series discovery for the two-stage series scan.
16
17use std::cmp::Reverse;
18use std::collections::HashSet;
19use std::sync::Arc;
20use std::time::Instant;
21
22use async_stream::try_stream;
23use datafusion::execution::memory_pool::{MemoryConsumer, MemoryPool};
24use datafusion::physical_expr::{LexOrdering, PhysicalSortExpr};
25use datafusion::physical_plan::expressions::Column;
26use datafusion::physical_plan::metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricBuilder};
27use datafusion::physical_plan::sorts::streaming_merge::StreamingMergeBuilder;
28use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
29use datafusion_common::DataFusionError;
30use datatypes::arrow::array::{Array, BinaryArray, BinaryBuilder};
31use datatypes::arrow::compute::SortOptions;
32use datatypes::arrow::datatypes::{DataType, Field, Schema, SchemaRef};
33use datatypes::arrow::record_batch::RecordBatch;
34use datatypes::prelude::ConcreteDataType;
35use futures::{StreamExt, TryStreamExt};
36use mito_codec::row_converter::{PrimaryKeyFilter, SparsePrimaryKeyCodec};
37use snafu::{OptionExt, ResultExt, ensure};
38use store_api::codec::PrimaryKeyEncoding;
39use store_api::region_engine::PartitionRange;
40use store_api::storage::consts::{PRIMARY_KEY_COLUMN_NAME, ReservedColumnId};
41use tokio::sync::Semaphore;
42
43use crate::error::{
44    InvalidRequestSnafu, JoinSnafu, MergeCandidateSeriesSnafu, NewRecordBatchSnafu, Result,
45    UnexpectedSnafu,
46};
47use crate::read::BoxedRecordBatchStream;
48use crate::read::pruner::{PartitionPruner, Pruner};
49use crate::read::range::RowGroupIndex;
50use crate::read::range_cache::{CachedRangeReader, build_candidate_range_cache_key};
51use crate::read::scan_region::StreamContext;
52use crate::read::scan_util::{PartitionMetrics, new_filter_metrics, scan_flat_mem_ranges};
53use crate::series_index::{
54    METRIC_SERIES_ID_BATCH_SIZE, MetricSeriesId, MetricSeriesIdStream, SeriesIndexFileHandle,
55    SeriesIndexReadContext, SeriesIndexSearcher,
56};
57use crate::sst::parquet::DEFAULT_READ_BATCH_SIZE;
58use crate::sst::parquet::format::PrimaryKeyArray;
59use crate::sst::parquet::prefilter::{
60    CachedPrimaryKeyFilter, build_primary_key_filter, prefilter_flat_batch_by_primary_key,
61};
62use crate::sst::parquet::reader::ReaderMetrics;
63use crate::sst::parquet::row_group::ParquetFetchMetrics;
64
65/// Builds candidate metric series from the ranges assigned to a [`SeriesScan`](super::series_scan::SeriesScan).
66pub(crate) struct SeriesCandidateScanner {
67    stream_ctx: Arc<StreamContext>,
68    partitions: Vec<Vec<PartitionRange>>,
69    partition_pruner: Arc<PartitionPruner>,
70    candidate_pruner: Arc<PartitionPruner>,
71    coverage: Arc<SeriesIndexCoverage>,
72    range_semaphore: Arc<Semaphore>,
73    memory_pool: Arc<dyn MemoryPool>,
74    metrics_set: ExecutionPlanMetricsSet,
75    part_metrics: PartitionMetrics,
76}
77
78impl SeriesCandidateScanner {
79    /// Creates a candidate-series scanner for native memtable and SST ranges.
80    ///
81    /// Callers must fall back to the legacy series-scan path when the scan contains
82    /// extension ranges. Candidate-series discovery does not support other range types.
83    ///
84    /// `pruner` must be built with `PrunerOptions::enable_predicate_prefilter` set to
85    /// `false`: both scan phases share its file range builders, and the two-stage path
86    /// applies simple filters on the precise-filter path instead.
87    pub(crate) fn try_new(
88        stream_ctx: Arc<StreamContext>,
89        partitions: Vec<Vec<PartitionRange>>,
90        pruner: Arc<Pruner>,
91        range_semaphore: Arc<Semaphore>,
92        memory_pool: Arc<dyn MemoryPool>,
93        metrics_set: ExecutionPlanMetricsSet,
94        part_metrics: PartitionMetrics,
95    ) -> Result<Self> {
96        validate_metric_metadata(&stream_ctx)?;
97        #[cfg(feature = "enterprise")]
98        ensure!(
99            stream_ctx.input.extension_ranges().is_empty(),
100            InvalidRequestSnafu {
101                region_id: stream_ctx.input.region_metadata().region_id,
102                reason: "candidate-series scan does not support extension ranges; use the legacy series-scan path",
103            }
104        );
105        ensure!(
106            !pruner.predicate_prefilter_enabled(),
107            UnexpectedSnafu {
108                reason: format!(
109                    "candidate-series scan for region {} requires a pruner without predicate prefiltering",
110                    stream_ctx.input.region_metadata().region_id
111                ),
112            }
113        );
114        let all_ranges = partitions.iter().flatten().copied().collect::<Vec<_>>();
115        pruner.add_partition_ranges(&all_ranges);
116        let coverage = Arc::new(SeriesIndexCoverage::new(&stream_ctx, &all_ranges));
117        let partition_pruner = Arc::new(PartitionPruner::new(pruner.clone(), &all_ranges));
118        let candidate_pruner = if coverage.covered_files.is_empty() {
119            partition_pruner.clone()
120        } else {
121            Arc::new(
122                PartitionPruner::new(pruner, &all_ranges).excluding_files(&coverage.covered_files),
123            )
124        };
125        Ok(Self {
126            stream_ctx,
127            partitions,
128            partition_pruner,
129            candidate_pruner,
130            coverage,
131            range_semaphore,
132            memory_pool,
133            metrics_set,
134            part_metrics,
135        })
136    }
137
138    /// Builds a globally sorted stream of candidate metric-series IDs.
139    pub(crate) async fn build_stream(&self) -> Result<MetricSeriesIdStream> {
140        let all_ranges = self
141            .partitions
142            .iter()
143            .flatten()
144            .copied()
145            .collect::<Vec<_>>();
146        let range_builder = SeriesCandidateRangeBuilder {
147            stream_ctx: self.stream_ctx.clone(),
148            partition_pruner: self.candidate_pruner.clone(),
149            coverage: self.coverage.clone(),
150            range_semaphore: self.range_semaphore.clone(),
151            memory_pool: self.memory_pool.clone(),
152            metrics_set: self.metrics_set.clone(),
153            part_metrics: self.part_metrics.clone(),
154        };
155        let mut tasks = Vec::with_capacity(all_ranges.len());
156        for (range_idx, part_range) in all_ranges.into_iter().enumerate() {
157            let range_builder = range_builder.clone();
158            tasks.push(common_runtime::spawn_query(async move {
159                range_builder
160                    .build_range_stream(part_range, range_idx)
161                    .await
162            }));
163        }
164
165        let mut range_streams = Vec::with_capacity(tasks.len());
166        for task in tasks {
167            range_streams.push(task.await.context(JoinSnafu)??);
168        }
169
170        if let Some(context) = &self.stream_ctx.input.series_index {
171            MetricBuilder::new(&self.metrics_set)
172                .counter("candidate_index_files", self.partitions.len())
173                .add(self.coverage.indexes.len());
174            MetricBuilder::new(&self.metrics_set)
175                .counter("candidate_index_covered_ssts", self.partitions.len())
176                .add(self.coverage.covered_files.len());
177            for index in &self.coverage.indexes {
178                range_streams.push(index_primary_key_stream(
179                    self.stream_ctx.clone(),
180                    context.clone(),
181                    index.clone(),
182                    self.range_semaphore.clone(),
183                ));
184            }
185        }
186
187        // Keep scanner-level merge metrics in the same synthetic partition as
188        // SeriesDistributor. Output partitions occupy 0..self.partitions.len().
189        let merged = merge_primary_key_streams(
190            range_streams,
191            self.memory_pool.clone(),
192            &self.metrics_set,
193            self.partitions.len(),
194            "SeriesCandidateScanner::final_merge",
195        )?;
196        decode_metric_series(merged, self.stream_ctx.input.region_metadata().clone())
197    }
198
199    /// Returns the partition pruner shared with the data phase.
200    pub(crate) fn partition_pruner(&self) -> Arc<PartitionPruner> {
201        self.partition_pruner.clone()
202    }
203}
204
205/// A scanner-wide replacement plan, independent of partition-range boundaries.
206#[derive(Default)]
207struct SeriesIndexCoverage {
208    indexes: Vec<SeriesIndexFileHandle>,
209    /// Indices into `ScanInput.files`, shared by every occurrence of an SST.
210    covered_files: HashSet<usize>,
211}
212
213impl SeriesIndexCoverage {
214    fn new(stream_ctx: &StreamContext, ranges: &[PartitionRange]) -> Self {
215        let Some(context) = &stream_ctx.input.series_index else {
216            return Self::default();
217        };
218        let mut uncovered: HashSet<_> = ranges
219            .iter()
220            .flat_map(|range| {
221                stream_ctx.ranges[range.identifier]
222                    .row_group_indices
223                    .iter()
224                    .filter(|index| stream_ctx.is_file_range_index(**index))
225                    .map(|index| index.index - stream_ctx.input.num_memtables())
226            })
227            .collect();
228        let region_id = stream_ctx.input.region_metadata().region_id;
229        let mut candidates: Vec<_> = context
230            .version
231            .series_indexes
232            .values()
233            .map(|index| {
234                let files: HashSet<_> = uncovered
235                    .iter()
236                    .copied()
237                    .filter(|file_index| {
238                        index
239                            .entry()
240                            .covers_file(stream_ctx.input.files[*file_index].meta_ref(), region_id)
241                    })
242                    .collect();
243                (index, files)
244            })
245            .collect();
246        let mut coverage = Self::default();
247        while let Some((position, count)) = candidates
248            .iter()
249            .enumerate()
250            .map(|(position, (index, files))| {
251                (
252                    position,
253                    files.intersection(&uncovered).count(),
254                    index.entry(),
255                )
256            })
257            .max_by_key(|(_, count, entry)| {
258                (
259                    *count,
260                    entry.max_file_sequence,
261                    Reverse(entry.index_uuid.as_bytes()),
262                )
263            })
264            .map(|(position, count, _)| (position, count))
265        {
266            if count == 0 {
267                break;
268            }
269            let (index, files) = candidates.swap_remove(position);
270            for file in files {
271                if uncovered.remove(&file) {
272                    coverage.covered_files.insert(file);
273                }
274            }
275            coverage.indexes.push(index.clone());
276        }
277        coverage
278    }
279
280    fn covers_source(&self, stream_ctx: &StreamContext, index: RowGroupIndex) -> bool {
281        stream_ctx.is_file_range_index(index)
282            && self
283                .covered_files
284                .contains(&(index.index - stream_ctx.input.num_memtables()))
285    }
286}
287
288/// Reads an index once for the entire scan, rather than once per partition range.
289fn index_primary_key_stream(
290    stream_ctx: Arc<StreamContext>,
291    context: SeriesIndexReadContext,
292    index: SeriesIndexFileHandle,
293    semaphore: Arc<Semaphore>,
294) -> BoxedRecordBatchStream {
295    Box::pin(try_stream! {
296        let metadata = stream_ctx.input.region_metadata();
297        let codec = SparsePrimaryKeyCodec::new(metadata);
298        let mut series = {
299            let _permit = semaphore.acquire().await.map_err(|error| UnexpectedSnafu {
300                reason: format!("failed to acquire candidate index permit: {error}"),
301            }.build())?;
302            SeriesIndexSearcher::try_new(
303                metadata.clone(),
304                context.store.clone(),
305                index,
306                stream_ctx.input.predicate_group().predicate(),
307                stream_ctx.input.time_range,
308            ).await?.search()?
309        };
310        loop {
311            let batch = {
312                let _permit = semaphore.acquire().await.map_err(|error| UnexpectedSnafu {
313                    reason: format!("failed to acquire candidate index permit: {error}"),
314                }.build())?;
315                series.try_next().await?
316            };
317            let Some(batch) = batch else { break };
318            let mut builder = BinaryBuilder::new();
319            let mut key = Vec::new();
320            for series in batch {
321                key.clear();
322                codec.encode_internal(series.table_id, series.tsid, &mut key)
323                    .context(crate::error::EncodeSnafu)?;
324                builder.append_value(&key);
325            }
326            yield RecordBatch::try_new(primary_key_schema(), vec![Arc::new(builder.finish())])
327                .context(NewRecordBatchSnafu)?;
328        }
329    })
330}
331
332#[derive(Clone)]
333struct SeriesCandidateRangeBuilder {
334    stream_ctx: Arc<StreamContext>,
335    coverage: Arc<SeriesIndexCoverage>,
336    partition_pruner: Arc<PartitionPruner>,
337    range_semaphore: Arc<Semaphore>,
338    memory_pool: Arc<dyn MemoryPool>,
339    metrics_set: ExecutionPlanMetricsSet,
340    part_metrics: PartitionMetrics,
341}
342
343impl SeriesCandidateRangeBuilder {
344    async fn build_range_stream(
345        &self,
346        part_range: PartitionRange,
347        merge_partition: usize,
348    ) -> Result<BoxedRecordBatchStream> {
349        let range_meta = &self.stream_ctx.ranges[part_range.identifier];
350        // A cache entry describes the complete original range. Never cache a
351        // partial range whose missing candidates are supplied by a global index.
352        let replaced_sources = range_meta
353            .row_group_indices
354            .iter()
355            .any(|index| self.coverage.covers_source(&self.stream_ctx, *index));
356        let cache_key = if replaced_sources {
357            None
358        } else {
359            build_candidate_range_cache_key(&self.stream_ctx, &part_range, &self.partition_pruner)
360        };
361        let builder = self.clone();
362        let (stream, _) = CachedRangeReader {
363            stream_ctx: &self.stream_ctx,
364            part_range: &part_range,
365            key: cache_key,
366            metrics: &self.part_metrics,
367        }
368        .build_with_memory_pool(&self.memory_pool, move || async move {
369            let stream = builder
370                .build_uncached_range_stream(part_range, merge_partition)
371                .await?;
372            Ok((stream, DEFAULT_READ_BATCH_SIZE))
373        })
374        .await?;
375        Ok(stream)
376    }
377
378    async fn build_uncached_range_stream(
379        &self,
380        part_range: PartitionRange,
381        merge_partition: usize,
382    ) -> Result<BoxedRecordBatchStream> {
383        // Release the build permit before the cache loader drives parallel sources,
384        // which acquire permits from this same semaphore.
385        let _permit = self.range_semaphore.acquire().await.map_err(|error| {
386            UnexpectedSnafu {
387                reason: format!("failed to acquire candidate range permit: {error}"),
388            }
389            .build()
390        })?;
391        let range_meta = &self.stream_ctx.ranges[part_range.identifier];
392        let mut sources = Vec::with_capacity(range_meta.row_group_indices.len());
393        for index in &range_meta.row_group_indices {
394            let source = self.build_source(*index, range_meta.time_range).await?;
395            if let Some(source) = source {
396                sources.push(source);
397            }
398        }
399
400        let sources = self.stream_ctx.input.create_parallel_flat_sources(
401            sources,
402            self.range_semaphore.clone(),
403            2,
404        )?;
405        let stream = merge_primary_key_streams(
406            sources,
407            self.memory_pool.clone(),
408            &self.metrics_set,
409            merge_partition,
410            "SeriesCandidateScanner::range_merge",
411        )?;
412
413        Ok(stream)
414    }
415
416    async fn build_source(
417        &self,
418        index: RowGroupIndex,
419        time_range: crate::sst::file::FileTimeRange,
420    ) -> Result<Option<BoxedRecordBatchStream>> {
421        let metadata = self.stream_ctx.input.region_metadata().clone();
422        if self.stream_ctx.is_mem_range_index(index) {
423            let raw = scan_flat_mem_ranges(
424                self.stream_ctx.clone(),
425                self.part_metrics.clone(),
426                index,
427                time_range,
428            );
429            let filter = build_primary_key_filter(
430                &metadata,
431                None,
432                self.stream_ctx.input.predicate_group().predicate(),
433            );
434            return Ok(Some(candidate_primary_key_stream(Box::pin(raw), filter)));
435        }
436
437        if self.stream_ctx.is_file_range_index(index) {
438            if self.coverage.covers_source(&self.stream_ctx, index) {
439                // Leave the range reference for the data phase so its first
440                // read can cache the builder. Retaining builders only prevents
441                // eviction; it does not allow caching at zero references.
442                return Ok(None);
443            }
444            let file = self.stream_ctx.input.file_from_index(index);
445            let predicate = self.stream_ctx.input.predicate_for_file(file);
446            if self
447                .partition_pruner
448                .try_skip_manifest_pruned_file_range(index, &self.part_metrics)
449            {
450                return Ok(None);
451            }
452            let mut reader_metrics = ReaderMetrics {
453                filter_metrics: new_filter_metrics(self.part_metrics.explain_verbose()),
454                ..Default::default()
455            };
456            let ranges = self
457                .partition_pruner
458                .build_file_ranges(index, &self.part_metrics, &mut reader_metrics)
459                .await?;
460            self.part_metrics.inc_num_file_ranges(ranges.len());
461            self.part_metrics
462                .merge_reader_metrics(&reader_metrics, None);
463
464            // Build a fresh encoded-PK filter from this SST's metadata so schema
465            // compatibility is evaluated independently for each file.
466            let filter = ranges.first().and_then(|range| {
467                build_primary_key_filter(
468                    range.region_metadata(),
469                    Some(metadata.as_ref()),
470                    predicate.as_ref(),
471                )
472            });
473            let part_metrics = self.part_metrics.clone();
474            let raw = Box::pin(try_stream! {
475                let fetch_metrics = part_metrics
476                    .explain_verbose()
477                    .then(|| Arc::new(ParquetFetchMetrics::default()));
478                let mut reader_metrics = ReaderMetrics {
479                    fetch_metrics: fetch_metrics.clone(),
480                    ..Default::default()
481                };
482                for range in ranges {
483                    let build_start = Instant::now();
484                    let Some(mut reader) = range
485                        .primary_key_reader(fetch_metrics.as_deref())
486                        .await?
487                    else {
488                        continue;
489                    };
490                    reader_metrics.build_cost += build_start.elapsed();
491
492                    let scan_start = Instant::now();
493                    while let Some(batch) = reader.try_next().await? {
494                        reader_metrics.num_record_batches += 1;
495                        reader_metrics.num_batches += 1;
496                        reader_metrics.num_rows += batch.num_rows();
497                        yield batch;
498                    }
499                    reader_metrics.scan_cost += scan_start.elapsed();
500                }
501                reader_metrics.observe_rows("candidate_series");
502                part_metrics.merge_reader_metrics(&reader_metrics, None);
503            });
504            return Ok(Some(candidate_primary_key_stream(raw, filter)));
505        }
506
507        UnexpectedSnafu {
508            reason: format!(
509                "candidate-series scan received unsupported range index {}",
510                index.index
511            ),
512        }
513        .fail()
514    }
515}
516
517pub(crate) fn is_sparse_metric_metadata(metadata: &store_api::metadata::RegionMetadataRef) -> bool {
518    let valid_prefix = metadata
519        .primary_key
520        .starts_with(&[ReservedColumnId::table_id(), ReservedColumnId::tsid()]);
521    let valid_types = metadata
522        .column_by_id(ReservedColumnId::table_id())
523        .zip(metadata.column_by_id(ReservedColumnId::tsid()))
524        .is_some_and(|(table_id, tsid)| {
525            table_id.column_schema.data_type == ConcreteDataType::uint32_datatype()
526                && tsid.column_schema.data_type == ConcreteDataType::uint64_datatype()
527        });
528
529    metadata.primary_key_encoding == PrimaryKeyEncoding::Sparse && valid_prefix && valid_types
530}
531
532pub(crate) fn validate_metric_metadata(stream_ctx: &StreamContext) -> Result<()> {
533    let metadata = stream_ctx.input.region_metadata();
534    ensure!(
535        is_sparse_metric_metadata(metadata),
536        InvalidRequestSnafu {
537            region_id: metadata.region_id,
538            reason: "candidate-series scan requires sparse (__table_id, __tsid) primary keys",
539        }
540    );
541    Ok(())
542}
543
544fn primary_key_schema() -> SchemaRef {
545    Arc::new(Schema::new(vec![Field::new(
546        PRIMARY_KEY_COLUMN_NAME,
547        DataType::Binary,
548        false,
549    )]))
550}
551
552/// Filters a source by encoded-primary-key predicates and emits one binary row per local key.
553fn candidate_primary_key_stream(
554    mut input: BoxedRecordBatchStream,
555    mut filter: Option<CachedPrimaryKeyFilter>,
556) -> BoxedRecordBatchStream {
557    Box::pin(try_stream! {
558        let mut last_primary_key = Vec::new();
559        let mut has_last = false;
560        while let Some(batch) = input.try_next().await? {
561            if let Some(batch) = normalize_candidate_batch(
562                batch,
563                filter.as_mut(),
564                &mut last_primary_key,
565                &mut has_last,
566            )? {
567                yield batch;
568            }
569        }
570    })
571}
572
573fn normalize_candidate_batch(
574    mut batch: RecordBatch,
575    filter: Option<&mut CachedPrimaryKeyFilter>,
576    last_primary_key: &mut Vec<u8>,
577    has_last: &mut bool,
578) -> Result<Option<RecordBatch>> {
579    let pk_idx = batch
580        .schema()
581        .column_with_name(PRIMARY_KEY_COLUMN_NAME)
582        .map(|(idx, _)| idx)
583        .context(UnexpectedSnafu {
584            reason: "candidate source does not contain __primary_key",
585        })?;
586    if let Some(filter) = filter {
587        let Some(filtered) = prefilter_flat_batch_by_primary_key(
588            batch,
589            pk_idx,
590            filter as &mut dyn PrimaryKeyFilter,
591        )?
592        else {
593            return Ok(None);
594        };
595        batch = filtered;
596    }
597
598    let pk_column = batch.column(pk_idx);
599    let mut builder = BinaryBuilder::new();
600    if let Some(array) = pk_column.as_any().downcast_ref::<PrimaryKeyArray>() {
601        let values = array
602            .values()
603            .as_any()
604            .downcast_ref::<BinaryArray>()
605            .context(UnexpectedSnafu {
606                reason: "dictionary primary-key values are not binary",
607            })?;
608        for key in array.keys().values() {
609            append_unique_primary_key(
610                values.value(*key as usize),
611                &mut builder,
612                last_primary_key,
613                has_last,
614            );
615        }
616    } else if let Some(array) = pk_column.as_any().downcast_ref::<BinaryArray>() {
617        for value in array.iter().flatten() {
618            append_unique_primary_key(value, &mut builder, last_primary_key, has_last);
619        }
620    } else {
621        return UnexpectedSnafu {
622            reason: format!(
623                "primary-key column is neither binary nor dictionary, got {:?}",
624                pk_column.data_type()
625            ),
626        }
627        .fail();
628    }
629
630    let array = builder.finish();
631    if array.is_empty() {
632        return Ok(None);
633    }
634    let batch = RecordBatch::try_new(primary_key_schema(), vec![Arc::new(array)])
635        .context(NewRecordBatchSnafu)?;
636    Ok(Some(batch))
637}
638
639fn append_unique_primary_key(
640    value: &[u8],
641    builder: &mut BinaryBuilder,
642    last_primary_key: &mut Vec<u8>,
643    has_last: &mut bool,
644) {
645    if !*has_last || last_primary_key != value {
646        builder.append_value(value);
647        last_primary_key.clear();
648        last_primary_key.extend_from_slice(value);
649        *has_last = true;
650    }
651}
652
653fn merge_primary_key_streams(
654    sources: Vec<BoxedRecordBatchStream>,
655    memory_pool: Arc<dyn MemoryPool>,
656    metrics_set: &ExecutionPlanMetricsSet,
657    partition: usize,
658    consumer_name: &'static str,
659) -> Result<BoxedRecordBatchStream> {
660    if sources.is_empty() {
661        return Ok(Box::pin(futures::stream::empty()));
662    }
663    if sources.len() == 1 {
664        return Ok(sources.into_iter().next().unwrap());
665    }
666
667    let schema = primary_key_schema();
668    let df_sources = sources
669        .into_iter()
670        .map(|source| {
671            let stream = source.map_err(|error| DataFusionError::External(Box::new(error)));
672            Box::pin(RecordBatchStreamAdapter::new(schema.clone(), stream)) as _
673        })
674        .collect();
675    let ordering = LexOrdering::new([PhysicalSortExpr {
676        expr: Arc::new(Column::new(PRIMARY_KEY_COLUMN_NAME, 0)),
677        options: SortOptions {
678            descending: false,
679            nulls_first: false,
680        },
681    }])
682    // Safe to unwrap because `LexOrdering::new` returns `None` only for empty
683    // input, and this array always contains one sort expression.
684    .unwrap();
685    let reservation = MemoryConsumer::new(consumer_name).register(&memory_pool);
686    let mut merged = StreamingMergeBuilder::new()
687        .with_streams(df_sources)
688        .with_schema(schema)
689        .with_expressions(&ordering)
690        .with_metrics(BaselineMetrics::new(metrics_set, partition))
691        .with_batch_size(DEFAULT_READ_BATCH_SIZE)
692        .with_reservation(reservation)
693        .build()
694        .context(MergeCandidateSeriesSnafu)?;
695
696    Ok(Box::pin(try_stream! {
697        while let Some(batch) = merged.next().await {
698            yield batch.context(MergeCandidateSeriesSnafu)?;
699        }
700    }))
701}
702
703fn decode_metric_series(
704    mut input: BoxedRecordBatchStream,
705    metadata: store_api::metadata::RegionMetadataRef,
706) -> Result<MetricSeriesIdStream> {
707    let codec = SparsePrimaryKeyCodec::new(&metadata);
708    Ok(Box::pin(try_stream! {
709        let mut last_series = None;
710        let mut output = Vec::with_capacity(METRIC_SERIES_ID_BATCH_SIZE);
711        while let Some(batch) = input.try_next().await? {
712            let array = batch
713                .column(0)
714                .as_any()
715                .downcast_ref::<BinaryArray>()
716                .context(UnexpectedSnafu {
717                    reason: "merged candidate primary key is not binary",
718                })?;
719            for primary_key in array.iter().flatten() {
720                let (table_id, tsid) = codec
721                    .decode_ids(primary_key)
722                    .context(crate::error::DecodeSnafu)?;
723                let series = MetricSeriesId { table_id, tsid };
724                if last_series == Some(series) {
725                    continue;
726                }
727                last_series = Some(series);
728                output.push(series);
729                if output.len() == METRIC_SERIES_ID_BATCH_SIZE {
730                    yield std::mem::replace(
731                        &mut output,
732                        Vec::with_capacity(METRIC_SERIES_ID_BATCH_SIZE),
733                    );
734                }
735            }
736        }
737        if !output.is_empty() {
738            yield output;
739        }
740    }))
741}
742
743#[cfg(test)]
744mod tests {
745    use std::num::NonZeroU64;
746    use std::time::Instant;
747
748    use common_time::Timestamp;
749    use datafusion::execution::memory_pool::UnboundedMemoryPool;
750    use datafusion_expr::{col, lit};
751    use datatypes::arrow::array::{
752        ArrayRef, DictionaryArray, TimestampMillisecondArray, UInt8Array, UInt32Array, UInt64Array,
753    };
754    use datatypes::arrow::datatypes::UInt32Type;
755    use futures::TryStreamExt;
756    use store_api::codec::PrimaryKeyEncoding;
757    use store_api::storage::FileId;
758    use table::predicate::Predicate;
759
760    use super::*;
761    use crate::cache::{CacheManager, CacheStrategy};
762    use crate::read::flat_projection::FlatProjectionMapper;
763    use crate::read::pruner::PrunerOptions;
764    use crate::read::scan_region::ScanInput;
765    use crate::read::scan_util::PartitionMetrics;
766    use crate::series_index::{
767        SeriesIndexEntry, SeriesIndexVersion, SeriesIndexWriter, SeriesIndexWriterOptions,
768        series_index_channel, series_index_path,
769    };
770    use crate::sst::file::{FileHandle, FileMeta};
771    use crate::test_util::new_noop_file_purger;
772    use crate::test_util::scheduler_util::SchedulerEnv;
773    use crate::test_util::sst_util::sst_region_metadata_with_encoding;
774
775    #[tokio::test]
776    async fn shared_candidate_ranges_isolate_query_memory_pools() {
777        use datafusion::execution::memory_pool::GreedyMemoryPool;
778        use datatypes::arrow::array::BinaryDictionaryBuilder;
779        use parquet::arrow::ArrowWriter;
780        use parquet::file::metadata::KeyValue;
781        use parquet::file::properties::WriterProperties;
782        use store_api::region_request::PathType;
783
784        use crate::read::scan_region::PredicateGroup;
785        use crate::sst::parquet::PARQUET_METADATA_KEY;
786        use crate::sst::parquet::flat_format::primary_key_column_index;
787        use crate::sst::{FlatSchemaOptions, to_flat_sst_arrow_schema};
788        use crate::test_util::sst_util::{new_record_batch_by_range, new_sparse_primary_key};
789
790        let env = SchedulerEnv::new().await;
791        let metadata = Arc::new(sst_region_metadata_with_encoding(
792            PrimaryKeyEncoding::Sparse,
793        ));
794        let dense = new_record_batch_by_range(&["a", "x"], 0, 2);
795        let mut columns: Vec<ArrayRef> = vec![
796            Arc::new(UInt32Array::from(vec![1, 1])),
797            Arc::new(UInt64Array::from(vec![10, 20])),
798        ];
799        columns.extend_from_slice(dense.columns());
800        let mut keys = BinaryDictionaryBuilder::<UInt32Type>::new();
801        for tsid in [10, 20] {
802            keys.append(new_sparse_primary_key(&["a", "x"], &metadata, 1, tsid))
803                .unwrap();
804        }
805        let pk_index = primary_key_column_index(columns.len());
806        columns[pk_index] = Arc::new(keys.finish());
807        let batch = RecordBatch::try_new(
808            to_flat_sst_arrow_schema(&metadata, &FlatSchemaOptions::default()),
809            columns,
810        )
811        .unwrap();
812        let properties = WriterProperties::builder()
813            .set_key_value_metadata(Some(vec![KeyValue::new(
814                PARQUET_METADATA_KEY.to_string(),
815                metadata.to_json().unwrap(),
816            )]))
817            .build();
818        let mut bytes = Vec::new();
819        let mut writer =
820            ArrowWriter::try_new(&mut bytes, batch.schema(), Some(properties)).unwrap();
821        writer.write(&batch).unwrap();
822        writer.close().unwrap();
823        let mut files = Vec::new();
824        for _ in 0..2 {
825            let file = FileHandle::new(
826                FileMeta {
827                    region_id: metadata.region_id,
828                    file_id: FileId::random(),
829                    time_range: (Timestamp::new_millisecond(0), Timestamp::new_millisecond(1)),
830                    file_size: bytes.len() as u64,
831                    num_rows: 2,
832                    num_row_groups: 1,
833                    preserve_row_sequence: true,
834                    ..Default::default()
835                },
836                new_noop_file_purger(),
837            );
838            env.access_layer
839                .object_store()
840                .write(&file.file_path("", PathType::Bare), bytes.clone())
841                .await
842                .unwrap();
843            files.push(file);
844        }
845        let strategy = CacheStrategy::EnableAll(Arc::new(
846            CacheManager::builder()
847                .range_result_cache_size(1024 * 1024)
848                .build(),
849        ));
850        let mapper =
851            FlatProjectionMapper::new(&metadata, 0..metadata.column_metadatas.len()).unwrap();
852        let input = ScanInput::builder(env.access_layer.clone(), mapper)
853            .with_files(files)
854            .with_cache(strategy)
855            .with_predicate(
856                PredicateGroup::new(&metadata, &[col("__table_id").eq(lit(1_u32))]).unwrap(),
857            )
858            .build();
859        let context = Arc::new(StreamContext::seq_scan_ctx(input));
860        let ranges = context.partition_ranges();
861        assert_eq!(ranges.len(), 1);
862        assert_eq!(context.ranges[0].row_group_indices.len(), 2);
863        let make_scanner = |pool: Arc<dyn MemoryPool>| {
864            let pruner = Arc::new(Pruner::new_with_options(
865                context.clone(),
866                1,
867                PrunerOptions {
868                    retain_builders: true,
869                    enable_predicate_prefilter: false,
870                },
871            ));
872            let metrics = ExecutionPlanMetricsSet::new();
873            let partition_metrics = PartitionMetrics::new(
874                metadata.region_id,
875                1,
876                "pool-test",
877                Instant::now(),
878                false,
879                &metrics,
880            );
881            SeriesCandidateScanner::try_new(
882                context.clone(),
883                vec![ranges.clone()],
884                pruner,
885                Arc::new(Semaphore::new(1)),
886                pool,
887                metrics,
888                partition_metrics,
889            )
890            .unwrap()
891        };
892        let low_pool = Arc::new(GreedyMemoryPool::new(0));
893        let high_pool = Arc::new(UnboundedMemoryPool::default());
894        let low = make_scanner(low_pool.clone());
895        let high = make_scanner(high_pool.clone());
896        tokio::time::timeout(std::time::Duration::from_secs(5), async {
897            // Register both scans before driving either source to its first batch.
898            let low_stream = low.build_stream().await.unwrap();
899            let high_stream = high.build_stream().await.unwrap();
900            let error = low_stream.try_collect::<Vec<_>>().await.unwrap_err();
901            assert!(
902                format!("{error:?}").contains("ResourcesExhausted"),
903                "{error:?}"
904            );
905            let groups = high_stream.try_collect::<Vec<_>>().await.unwrap();
906            assert_eq!(
907                groups.into_iter().flatten().collect::<Vec<_>>(),
908                [10, 20]
909                    .map(|tsid| MetricSeriesId { table_id: 1, tsid })
910                    .to_vec(),
911            );
912        })
913        .await
914        .expect("candidate scans stalled with different query pools");
915        drop(low);
916        drop(high);
917        assert_eq!(low_pool.reserved(), 0);
918        assert_eq!(high_pool.reserved(), 0);
919    }
920
921    /// Uses absent SST objects so any covered-SST read fails the test.
922    async fn indexed_scanner() -> (SchedulerEnv, SeriesCandidateScanner, Arc<Pruner>) {
923        let env = SchedulerEnv::new().await;
924        let metadata = Arc::new(sst_region_metadata_with_encoding(
925            PrimaryKeyEncoding::Sparse,
926        ));
927        let store = env.access_layer.object_store().clone();
928        let entry = SeriesIndexEntry {
929            file_size: 0,
930            index_uuid: FileId::random(),
931            bucket_start: Timestamp::new_millisecond(0),
932            bucket_end: Timestamp::new_millisecond(20),
933            source_file_ids: Vec::new(),
934            min_file_sequence: 1,
935            max_file_sequence: 2,
936            compaction_window_secs: 1,
937            window_sequences: Default::default(),
938        };
939        let path = series_index_path(metadata.region_id, entry.index_uuid);
940        let codec = SparsePrimaryKeyCodec::new(&metadata);
941        let keys: Vec<_> = (0..1001)
942            .map(|tsid| {
943                let mut key = Vec::new();
944                codec.encode_internal(1, tsid, &mut key).unwrap();
945                key
946            })
947            .collect();
948        let batch = RecordBatch::try_from_iter(vec![
949            (
950                "ts",
951                Arc::new(TimestampMillisecondArray::from(vec![10; keys.len()])) as ArrayRef,
952            ),
953            (
954                "__primary_key",
955                Arc::new(BinaryArray::from_iter_values(&keys)),
956            ),
957            (
958                "__sequence",
959                Arc::new(UInt64Array::from_value(1, keys.len())),
960            ),
961            ("__op_type", Arc::new(UInt8Array::from_value(0, keys.len()))),
962        ])
963        .unwrap();
964        let mut writer = SeriesIndexWriter::try_new(
965            metadata.clone(),
966            store.clone(),
967            &path,
968            SeriesIndexWriterOptions {
969                row_group_size: 500,
970            },
971            None,
972        )
973        .await
974        .unwrap();
975        writer.write(&batch).await.unwrap();
976        writer.finish().await.unwrap();
977        let (purger, _receiver) = series_index_channel(store.clone());
978        let handle = SeriesIndexFileHandle::new(metadata.region_id, entry.clone(), purger);
979        let context = SeriesIndexReadContext {
980            store,
981            version: Arc::new(SeriesIndexVersion {
982                series_indexes: [(entry.index_uuid, handle)].into(),
983                ..Default::default()
984            }),
985        };
986        let files = (0..2)
987            .map(|i| {
988                FileHandle::new(
989                    FileMeta {
990                        region_id: metadata.region_id,
991                        file_id: FileId::random(),
992                        time_range: (
993                            Timestamp::new_millisecond(i * 10),
994                            Timestamp::new_millisecond(i * 10 + 9),
995                        ),
996                        sequence: NonZeroU64::new(i as u64 + 1),
997                        num_row_groups: 1,
998                        ..Default::default()
999                    },
1000                    new_noop_file_purger(),
1001                )
1002            })
1003            .collect();
1004        let mapper =
1005            FlatProjectionMapper::new(&metadata, 0..metadata.column_metadatas.len()).unwrap();
1006        let input = ScanInput::builder(env.access_layer.clone(), mapper)
1007            .with_predicate(
1008                crate::read::scan_region::PredicateGroup::new(
1009                    &metadata,
1010                    &[col("__table_id").eq(lit(1_u32))],
1011                )
1012                .unwrap(),
1013            )
1014            .with_files(files)
1015            .with_series_index(Some(context))
1016            .with_cache(CacheStrategy::EnableAll(Arc::new(
1017                CacheManager::builder()
1018                    .range_result_cache_size(1024 * 1024)
1019                    .build(),
1020            )))
1021            .build();
1022        let stream_ctx = Arc::new(StreamContext::seq_scan_ctx(input));
1023        let ranges = stream_ctx.partition_ranges();
1024        assert_eq!(2, ranges.len());
1025        let metrics_set = ExecutionPlanMetricsSet::new();
1026        let part_metrics = PartitionMetrics::new(
1027            metadata.region_id,
1028            2,
1029            "candidate-test",
1030            Instant::now(),
1031            false,
1032            &metrics_set,
1033        );
1034        let pruner = Arc::new(Pruner::new_with_options(
1035            stream_ctx.clone(),
1036            1,
1037            PrunerOptions {
1038                retain_builders: true,
1039                enable_predicate_prefilter: false,
1040            },
1041        ));
1042        let scanner = SeriesCandidateScanner::try_new(
1043            stream_ctx,
1044            ranges.into_iter().map(|range| vec![range]).collect(),
1045            pruner.clone(),
1046            Arc::new(Semaphore::new(1)),
1047            Arc::new(UnboundedMemoryPool::default()),
1048            metrics_set,
1049            part_metrics,
1050        )
1051        .unwrap();
1052        (env, scanner, pruner)
1053    }
1054
1055    #[tokio::test]
1056    async fn index_is_shared_across_ranges_without_caching_partial_candidates() {
1057        let (_env, scanner, pruner) = indexed_scanner().await;
1058        let keys: Vec<_> = scanner
1059            .partitions
1060            .iter()
1061            .flatten()
1062            .map(|range| {
1063                build_candidate_range_cache_key(
1064                    &scanner.stream_ctx,
1065                    range,
1066                    &scanner.candidate_pruner,
1067                )
1068                .unwrap()
1069            })
1070            .collect();
1071        let groups = scanner
1072            .build_stream()
1073            .await
1074            .unwrap()
1075            .try_collect::<Vec<_>>()
1076            .await
1077            .unwrap();
1078        assert_eq!(
1079            (0..1001)
1080                .map(|tsid| MetricSeriesId { table_id: 1, tsid })
1081                .collect::<Vec<_>>(),
1082            groups.into_iter().flatten().collect::<Vec<_>>()
1083        );
1084        // Covered SSTs have no builder yet. Keep their references so the data
1085        // phase can cache each builder on its first read and reuse it later.
1086        for file_index in 0..2 {
1087            assert_eq!(1, pruner.test_remaining_ranges(file_index));
1088        }
1089        assert_eq!(
1090            1,
1091            scanner
1092                .metrics_set
1093                .clone_inner()
1094                .sum_by_name("candidate_index_files")
1095                .unwrap()
1096                .as_usize()
1097        );
1098        assert_eq!(
1099            2,
1100            scanner
1101                .metrics_set
1102                .clone_inner()
1103                .sum_by_name("candidate_index_covered_ssts")
1104                .unwrap()
1105                .as_usize()
1106        );
1107        // Index-backed ranges must not publish their empty residual streams as
1108        // complete candidate results for a future scan without the index.
1109        assert!(!format!("{:?}", scanner.part_metrics).contains("range_cache_miss"));
1110        assert!(!format!("{:?}", scanner.part_metrics).contains("range_cache_hit"));
1111        for key in keys {
1112            assert!(
1113                scanner
1114                    .stream_ctx
1115                    .input
1116                    .cache_strategy
1117                    .get_range_result(&key)
1118                    .is_none()
1119            );
1120        }
1121    }
1122
1123    #[tokio::test]
1124    async fn index_read_failure_after_output_is_propagated() {
1125        let (_env, scanner, _pruner) = indexed_scanner().await;
1126        let context = scanner.stream_ctx.input.series_index.clone().unwrap();
1127        let index = scanner.coverage.indexes[0].clone();
1128        let path = series_index_path(
1129            scanner.stream_ctx.input.region_metadata().region_id,
1130            index.entry().index_uuid,
1131        );
1132        let mut stream = index_primary_key_stream(
1133            scanner.stream_ctx.clone(),
1134            context.clone(),
1135            index,
1136            scanner.range_semaphore.clone(),
1137        );
1138        assert_eq!(500, stream.try_next().await.unwrap().unwrap().num_rows());
1139        context.store.delete(&path).await.unwrap();
1140        assert!(stream.try_collect::<Vec<_>>().await.is_err());
1141        // Opening the missing selected index must also fail, rather than emit
1142        // an incomplete candidate set or fall back to the absent SSTs.
1143        assert!(
1144            scanner
1145                .build_stream()
1146                .await
1147                .unwrap()
1148                .try_collect::<Vec<_>>()
1149                .await
1150                .is_err()
1151        );
1152    }
1153
1154    #[tokio::test]
1155    async fn candidate_scanner_rejects_predicate_prefilter_pruner() {
1156        let env = SchedulerEnv::new().await;
1157        let metadata = Arc::new(sst_region_metadata_with_encoding(
1158            PrimaryKeyEncoding::Sparse,
1159        ));
1160        let mapper =
1161            FlatProjectionMapper::new(&metadata, 0..metadata.column_metadatas.len()).unwrap();
1162        let stream_ctx = Arc::new(StreamContext::seq_scan_ctx(
1163            ScanInput::builder(env.access_layer.clone(), mapper).build(),
1164        ));
1165        let pruner = Arc::new(Pruner::new(stream_ctx.clone(), 1));
1166        let metrics_set = ExecutionPlanMetricsSet::new();
1167        let part_metrics = PartitionMetrics::new(
1168            metadata.region_id,
1169            0,
1170            "candidate-test",
1171            Instant::now(),
1172            false,
1173            &metrics_set,
1174        );
1175
1176        let error = SeriesCandidateScanner::try_new(
1177            stream_ctx,
1178            Vec::new(),
1179            pruner,
1180            Arc::new(Semaphore::new(1)),
1181            Arc::new(UnboundedMemoryPool::default()),
1182            metrics_set,
1183            part_metrics,
1184        )
1185        .err()
1186        .unwrap();
1187
1188        assert!(matches!(error, crate::error::Error::Unexpected { .. }));
1189        assert!(
1190            error
1191                .to_string()
1192                .contains("requires a pruner without predicate prefiltering")
1193        );
1194    }
1195
1196    fn binary_batch(values: &[&[u8]]) -> RecordBatch {
1197        RecordBatch::try_new(
1198            primary_key_schema(),
1199            vec![Arc::new(BinaryArray::from_iter_values(
1200                values.iter().copied(),
1201            ))],
1202        )
1203        .unwrap()
1204    }
1205
1206    fn dictionary_batch(values: &[&[u8]], keys: &[u32]) -> RecordBatch {
1207        let dict_values: ArrayRef = Arc::new(BinaryArray::from_iter_values(values.iter().copied()));
1208        let dict =
1209            DictionaryArray::<UInt32Type>::try_new(UInt32Array::from(keys.to_vec()), dict_values)
1210                .unwrap();
1211        let schema = Arc::new(Schema::new(vec![Field::new_dictionary(
1212            PRIMARY_KEY_COLUMN_NAME,
1213            DataType::UInt32,
1214            DataType::Binary,
1215            false,
1216        )]));
1217        RecordBatch::try_new(schema, vec![Arc::new(dict)]).unwrap()
1218    }
1219
1220    #[tokio::test]
1221    async fn candidate_stream_normalizes_and_deduplicates_primary_keys() {
1222        let input = Box::pin(futures::stream::iter(vec![
1223            Ok(dictionary_batch(&[b"a", b"b"], &[0, 0, 1])),
1224            Ok(binary_batch(&[b"b", b"c", b"c"])),
1225        ]));
1226        let batches = candidate_primary_key_stream(input, None)
1227            .try_collect::<Vec<_>>()
1228            .await
1229            .unwrap();
1230        let actual = batches
1231            .iter()
1232            .flat_map(|batch| {
1233                batch
1234                    .column(0)
1235                    .as_any()
1236                    .downcast_ref::<BinaryArray>()
1237                    .unwrap()
1238                    .iter()
1239                    .flatten()
1240            })
1241            .collect::<Vec<_>>();
1242        assert_eq!(actual, vec![b"a".as_slice(), b"b", b"c"]);
1243    }
1244
1245    #[tokio::test]
1246    async fn candidate_stream_filters_primary_keys_before_merge() {
1247        let metadata = Arc::new(sst_region_metadata_with_encoding(
1248            PrimaryKeyEncoding::Sparse,
1249        ));
1250        let codec = SparsePrimaryKeyCodec::new(&metadata);
1251        let mut table_1 = Vec::new();
1252        let mut table_2 = Vec::new();
1253        codec.encode_internal(1, 10, &mut table_1).unwrap();
1254        codec.encode_internal(2, 20, &mut table_2).unwrap();
1255
1256        let predicate = Predicate::new(vec![
1257            col(store_api::metric_engine_consts::DATA_SCHEMA_TABLE_ID_COLUMN_NAME).eq(lit(1_u32)),
1258        ]);
1259        let filter = build_primary_key_filter(&metadata, None, Some(&predicate));
1260        let input = Box::pin(futures::stream::iter(vec![Ok(dictionary_batch(
1261            &[table_1.as_slice(), table_2.as_slice()],
1262            &[0, 1],
1263        ))]));
1264        let batches = candidate_primary_key_stream(input, filter)
1265            .try_collect::<Vec<_>>()
1266            .await
1267            .unwrap();
1268
1269        assert_eq!(batches.len(), 1);
1270        let array = batches[0]
1271            .column(0)
1272            .as_any()
1273            .downcast_ref::<BinaryArray>()
1274            .unwrap();
1275        assert_eq!(array.len(), 1);
1276        assert_eq!(array.value(0), table_1);
1277    }
1278
1279    #[tokio::test]
1280    async fn decode_metric_series_yields_groups_of_500() {
1281        let metadata = Arc::new(sst_region_metadata_with_encoding(
1282            PrimaryKeyEncoding::Sparse,
1283        ));
1284        let codec = SparsePrimaryKeyCodec::new(&metadata);
1285        let primary_keys = (0..501_u64)
1286            .map(|tsid| {
1287                let mut primary_key = Vec::new();
1288                codec.encode_internal(1, tsid, &mut primary_key).unwrap();
1289                primary_key
1290            })
1291            .collect::<Vec<_>>();
1292        let batch = binary_batch(&primary_keys.iter().map(Vec::as_slice).collect::<Vec<_>>());
1293        let source = Box::pin(futures::stream::iter(vec![Ok(batch)]));
1294        let metrics = ExecutionPlanMetricsSet::new();
1295        let pool = Arc::new(UnboundedMemoryPool::default());
1296        let merged =
1297            merge_primary_key_streams(vec![source], pool, &metrics, 0, "candidate-test").unwrap();
1298        let groups = decode_metric_series(merged, metadata)
1299            .unwrap()
1300            .try_collect::<Vec<_>>()
1301            .await
1302            .unwrap();
1303
1304        assert_eq!(groups.iter().map(Vec::len).collect::<Vec<_>>(), [500, 1]);
1305        assert_eq!(
1306            groups[0][0],
1307            MetricSeriesId {
1308                table_id: 1,
1309                tsid: 0
1310            }
1311        );
1312        assert_eq!(
1313            groups[1][0],
1314            MetricSeriesId {
1315                table_id: 1,
1316                tsid: 500
1317            }
1318        );
1319    }
1320
1321    #[tokio::test]
1322    async fn merge_primary_keys_globally_sorts_and_deduplicates_series() {
1323        let metadata = Arc::new(sst_region_metadata_with_encoding(
1324            PrimaryKeyEncoding::Sparse,
1325        ));
1326        let codec = SparsePrimaryKeyCodec::new(&metadata);
1327        let encode = |tsid| {
1328            let mut primary_key = Vec::new();
1329            codec.encode_internal(1, tsid, &mut primary_key).unwrap();
1330            primary_key
1331        };
1332        let keys_1 = [encode(1), encode(3)];
1333        let mut alternate_key_for_series_1 = encode(1);
1334        alternate_key_for_series_1.push(0);
1335        let keys_2 = [alternate_key_for_series_1, encode(2)];
1336        let sources = vec![
1337            Box::pin(futures::stream::iter(vec![Ok(binary_batch(
1338                &keys_1.iter().map(Vec::as_slice).collect::<Vec<_>>(),
1339            ))])) as BoxedRecordBatchStream,
1340            Box::pin(futures::stream::iter(vec![Ok(binary_batch(
1341                &keys_2.iter().map(Vec::as_slice).collect::<Vec<_>>(),
1342            ))])),
1343        ];
1344
1345        let merged = merge_primary_key_streams(
1346            sources,
1347            Arc::new(UnboundedMemoryPool::default()),
1348            &ExecutionPlanMetricsSet::new(),
1349            0,
1350            "candidate-merge-test",
1351        )
1352        .unwrap();
1353        let groups = decode_metric_series(merged, metadata)
1354            .unwrap()
1355            .try_collect::<Vec<_>>()
1356            .await
1357            .unwrap();
1358
1359        assert_eq!(
1360            groups,
1361            vec![vec![
1362                MetricSeriesId {
1363                    table_id: 1,
1364                    tsid: 1,
1365                },
1366                MetricSeriesId {
1367                    table_id: 1,
1368                    tsid: 2,
1369                },
1370                MetricSeriesId {
1371                    table_id: 1,
1372                    tsid: 3,
1373                },
1374            ]]
1375        );
1376    }
1377}