Skip to main content

promql/extension_plan/
series_divide.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
15use std::pin::Pin;
16use std::sync::Arc;
17use std::task::{Context, Poll};
18
19use datafusion::arrow::array::{
20    Array, ArrayRef, DictionaryArray, LargeStringArray, PrimitiveArray, StringArray,
21    StringViewArray, UInt64Array,
22};
23use datafusion::arrow::buffer::NullBuffer;
24use datafusion::arrow::datatypes::{ArrowDictionaryKeyType, DataType, SchemaRef};
25use datafusion::arrow::downcast_dictionary_array;
26use datafusion::arrow::record_batch::RecordBatch;
27use datafusion::common::tree_node::TreeNodeRecursion;
28use datafusion::common::{DFSchema, DFSchemaRef};
29use datafusion::error::Result as DataFusionResult;
30use datafusion::execution::context::TaskContext;
31use datafusion::logical_expr::{EmptyRelation, Expr, LogicalPlan, UserDefinedLogicalNodeCore};
32use datafusion::physical_expr::{LexRequirement, OrderingRequirements, PhysicalSortRequirement};
33use datafusion::physical_plan::expressions::Column as ColumnExpr;
34use datafusion::physical_plan::metrics::{
35    BaselineMetrics, Count, ExecutionPlanMetricsSet, MetricBuilder, MetricValue, MetricsSet,
36};
37use datafusion::physical_plan::{
38    DisplayAs, DisplayFormatType, Distribution, ExecutionPlan, InputDistributionRequirements,
39    PhysicalExpr, PlanProperties, RecordBatchStream, SendableRecordBatchStream,
40};
41use datafusion_expr::ident;
42use datatypes::arrow::compute;
43use datatypes::arrow_array::string_array_value_at_index;
44use datatypes::compute::SortOptions;
45use futures::{Stream, StreamExt, ready};
46use greptime_proto::substrait_extension as pb;
47use prost::Message;
48use snafu::ResultExt;
49
50use crate::error::{DeserializeSnafu, Result};
51use crate::extension_plan::{METRIC_NUM_SERIES, resolve_column_name, serialize_column_index};
52use crate::metrics::PROMQL_SERIES_COUNT;
53
54enum TagIdentifier<'a> {
55    /// A group of raw string tag columns.
56    Raw(Vec<RawTagColumn<'a>>),
57    /// A single UInt64 identifier (tsid).
58    Id(&'a UInt64Array),
59}
60
61impl<'a> TagIdentifier<'a> {
62    fn try_new(batch: &'a RecordBatch, tag_indices: &[usize]) -> DataFusionResult<Self> {
63        match tag_indices {
64            [] => Ok(Self::Raw(Vec::new())),
65            [index] => {
66                let array = batch.column(*index);
67                if array.data_type() == &DataType::UInt64 {
68                    let array = array
69                        .as_any()
70                        .downcast_ref::<UInt64Array>()
71                        .ok_or_else(|| {
72                            datafusion::error::DataFusionError::Internal(
73                                "Failed to downcast tag column to UInt64Array".to_string(),
74                            )
75                        })?;
76                    Ok(Self::Id(array))
77                } else {
78                    Ok(Self::Raw(vec![RawTagColumn::try_new(array)?]))
79                }
80            }
81            indices => Ok(Self::Raw(
82                indices
83                    .iter()
84                    .map(|index| RawTagColumn::try_new(batch.column(*index)))
85                    .collect::<DataFusionResult<Vec<_>>>()?,
86            )),
87        }
88    }
89
90    fn equal_at(&self, left_row: usize, other: &Self, right_row: usize) -> DataFusionResult<bool> {
91        match (self, other) {
92            (Self::Id(left), Self::Id(right)) => {
93                if left.is_null(left_row) || right.is_null(right_row) {
94                    return Ok(left.is_null(left_row) && right.is_null(right_row));
95                }
96                Ok(left.value(left_row) == right.value(right_row))
97            }
98            (Self::Raw(left), Self::Raw(right)) => {
99                if left.len() != right.len() {
100                    return Err(datafusion::error::DataFusionError::Internal(format!(
101                        "Mismatched tag column count: left={}, right={}",
102                        left.len(),
103                        right.len()
104                    )));
105                }
106
107                for (left_column, right_column) in left.iter().zip(right.iter()) {
108                    if !left_column.equal_at(left_row, right_column, right_row) {
109                        return Ok(false);
110                    }
111                }
112                Ok(true)
113            }
114            _ => Err(datafusion::error::DataFusionError::Internal(format!(
115                "Mismatched tag identifier types: left={:?}, right={:?}",
116                self.data_type(),
117                other.data_type()
118            ))),
119        }
120    }
121
122    fn data_type(&self) -> &'static str {
123        match self {
124            Self::Raw(_) => "Raw",
125            Self::Id(_) => "Id",
126        }
127    }
128}
129
130struct RawTagColumn<'a>(&'a ArrayRef);
131
132impl<'a> RawTagColumn<'a> {
133    fn try_new(array: &'a ArrayRef) -> DataFusionResult<Self> {
134        match array.data_type() {
135            DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View => Ok(Self(array)),
136            DataType::Dictionary(key, value) if key.is_integer() && value.is_string() => {
137                Ok(Self(array))
138            }
139            other => Err(datafusion::error::DataFusionError::Internal(format!(
140                "Unsupported tag column type: {other:?}"
141            ))),
142        }
143    }
144
145    fn equal_at(&self, left_row: usize, other: &Self, right_row: usize) -> bool {
146        string_array_value_at_index(self.0, left_row)
147            == string_array_value_at_index(other.0, right_row)
148    }
149}
150
151#[derive(Debug, PartialEq, Eq, Hash, PartialOrd)]
152pub struct SeriesDivide {
153    tag_columns: Vec<String>,
154    /// `SeriesDivide` requires `time_index` column's name to generate ordering requirement
155    /// for input data. But this plan itself doesn't depend on the ordering of time index
156    /// column. This is for follow on plans like `RangeManipulate`. Because requiring ordering
157    /// here can avoid unnecessary sort in follow on plans.
158    time_index_column: String,
159    input: LogicalPlan,
160    unfix: Option<UnfixIndices>,
161}
162
163#[derive(Debug, PartialEq, Eq, Hash, PartialOrd)]
164struct UnfixIndices {
165    pub tag_column_indices: Vec<u64>,
166    pub time_index_column_idx: u64,
167}
168
169impl UserDefinedLogicalNodeCore for SeriesDivide {
170    fn name(&self) -> &str {
171        Self::name()
172    }
173
174    fn inputs(&self) -> Vec<&LogicalPlan> {
175        vec![&self.input]
176    }
177
178    fn schema(&self) -> &DFSchemaRef {
179        self.input.schema()
180    }
181
182    fn expressions(&self) -> Vec<Expr> {
183        if self.unfix.is_some() {
184            return vec![];
185        }
186
187        self.tag_columns
188            .iter()
189            .map(ident)
190            .chain(std::iter::once(ident(&self.time_index_column)))
191            .collect()
192    }
193
194    fn necessary_children_exprs(&self, output_columns: &[usize]) -> Option<Vec<Vec<usize>>> {
195        if self.unfix.is_some() {
196            return None;
197        }
198
199        let input_schema = self.input.schema();
200        if output_columns.is_empty() {
201            let indices = (0..input_schema.fields().len()).collect::<Vec<_>>();
202            return Some(vec![indices]);
203        }
204
205        let mut required = Vec::with_capacity(output_columns.len() + 1 + self.tag_columns.len());
206        required.extend_from_slice(output_columns);
207        for tag in &self.tag_columns {
208            required.push(input_schema.index_of_column_by_name(None, tag)?);
209        }
210        required.push(input_schema.index_of_column_by_name(None, &self.time_index_column)?);
211
212        required.sort_unstable();
213        required.dedup();
214        Some(vec![required])
215    }
216
217    fn fmt_for_explain(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
218        write!(f, "PromSeriesDivide: tags={:?}", self.tag_columns)
219    }
220
221    fn with_exprs_and_inputs(
222        &self,
223        _exprs: Vec<Expr>,
224        inputs: Vec<LogicalPlan>,
225    ) -> DataFusionResult<Self> {
226        if inputs.is_empty() {
227            return Err(datafusion::error::DataFusionError::Internal(
228                "SeriesDivide must have at least one input".to_string(),
229            ));
230        }
231
232        let input: LogicalPlan = inputs[0].clone();
233        let input_schema = input.schema();
234
235        if let Some(unfix) = &self.unfix {
236            // transform indices to names
237            let tag_columns = unfix
238                .tag_column_indices
239                .iter()
240                .map(|idx| resolve_column_name(*idx, input_schema, "SeriesDivide", "tag"))
241                .collect::<DataFusionResult<Vec<String>>>()?;
242
243            let time_index_column = resolve_column_name(
244                unfix.time_index_column_idx,
245                input_schema,
246                "SeriesDivide",
247                "time index",
248            )?;
249
250            Ok(Self {
251                tag_columns,
252                time_index_column,
253                input,
254                unfix: None,
255            })
256        } else {
257            Ok(Self {
258                tag_columns: self.tag_columns.clone(),
259                time_index_column: self.time_index_column.clone(),
260                input,
261                unfix: None,
262            })
263        }
264    }
265}
266
267impl SeriesDivide {
268    pub fn new(tag_columns: Vec<String>, time_index_column: String, input: LogicalPlan) -> Self {
269        Self {
270            tag_columns,
271            time_index_column,
272            input,
273            unfix: None,
274        }
275    }
276
277    pub const fn name() -> &'static str {
278        "SeriesDivide"
279    }
280
281    pub fn to_execution_plan(&self, exec_input: Arc<dyn ExecutionPlan>) -> Arc<dyn ExecutionPlan> {
282        Arc::new(SeriesDivideExec {
283            tag_columns: self.tag_columns.clone(),
284            time_index_column: self.time_index_column.clone(),
285            input: exec_input,
286            metric: ExecutionPlanMetricsSet::new(),
287        })
288    }
289
290    pub fn tags(&self) -> &[String] {
291        &self.tag_columns
292    }
293
294    pub fn serialize(&self) -> Vec<u8> {
295        let tag_column_indices = self
296            .tag_columns
297            .iter()
298            .map(|name| serialize_column_index(self.input.schema(), name))
299            .collect::<Vec<u64>>();
300
301        let time_index_column_idx =
302            serialize_column_index(self.input.schema(), &self.time_index_column);
303
304        pb::SeriesDivide {
305            tag_column_indices,
306            time_index_column_idx,
307            ..Default::default()
308        }
309        .encode_to_vec()
310    }
311
312    pub fn deserialize(bytes: &[u8]) -> Result<Self> {
313        let pb_series_divide = pb::SeriesDivide::decode(bytes).context(DeserializeSnafu)?;
314        let placeholder_plan = LogicalPlan::EmptyRelation(EmptyRelation {
315            produce_one_row: false,
316            schema: Arc::new(DFSchema::empty()),
317        });
318
319        let unfix = UnfixIndices {
320            tag_column_indices: pb_series_divide.tag_column_indices.clone(),
321            time_index_column_idx: pb_series_divide.time_index_column_idx,
322        };
323
324        Ok(Self {
325            tag_columns: Vec::new(),
326            time_index_column: String::new(),
327            input: placeholder_plan,
328            unfix: Some(unfix),
329        })
330    }
331}
332
333#[derive(Debug)]
334pub struct SeriesDivideExec {
335    tag_columns: Vec<String>,
336    time_index_column: String,
337    input: Arc<dyn ExecutionPlan>,
338    metric: ExecutionPlanMetricsSet,
339}
340
341impl ExecutionPlan for SeriesDivideExec {
342    fn apply_expressions(
343        &self,
344        _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> datafusion_common::Result<TreeNodeRecursion>,
345    ) -> DataFusionResult<TreeNodeRecursion> {
346        Ok(TreeNodeRecursion::Continue)
347    }
348
349    fn schema(&self) -> SchemaRef {
350        self.input.schema()
351    }
352
353    fn properties(&self) -> &Arc<PlanProperties> {
354        self.input.properties()
355    }
356
357    fn input_distribution_requirements(&self) -> InputDistributionRequirements {
358        if self.tag_columns.is_empty() {
359            return InputDistributionRequirements::new(vec![Distribution::SinglePartition]);
360        }
361        let schema = self.input.schema();
362        InputDistributionRequirements::new(vec![Distribution::KeyPartitioned(
363            self.tag_columns
364                .iter()
365                // Safety: the tag column names is verified in the planning phase
366                .map(|tag| Arc::new(ColumnExpr::new_with_schema(tag, &schema).unwrap()) as _)
367                .collect(),
368        )])
369    }
370
371    fn required_input_ordering(&self) -> Vec<Option<OrderingRequirements>> {
372        let input_schema = self.input.schema();
373        let mut exprs: Vec<PhysicalSortRequirement> = self
374            .tag_columns
375            .iter()
376            .map(|tag| PhysicalSortRequirement {
377                // Safety: the tag column names is verified in the planning phase
378                expr: Arc::new(ColumnExpr::new_with_schema(tag, &input_schema).unwrap()),
379                options: Some(SortOptions {
380                    descending: false,
381                    nulls_first: true,
382                }),
383            })
384            .collect();
385
386        exprs.push(PhysicalSortRequirement {
387            expr: Arc::new(
388                ColumnExpr::new_with_schema(&self.time_index_column, &input_schema).unwrap(),
389            ),
390            options: Some(SortOptions {
391                descending: false,
392                nulls_first: true,
393            }),
394        });
395
396        // Safety: `exprs` is not empty
397        let requirement = LexRequirement::new(exprs).unwrap();
398
399        vec![Some(OrderingRequirements::Hard(vec![requirement]))]
400    }
401
402    fn maintains_input_order(&self) -> Vec<bool> {
403        vec![true; self.children().len()]
404    }
405
406    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
407        vec![&self.input]
408    }
409
410    fn with_new_children(
411        self: Arc<Self>,
412        children: Vec<Arc<dyn ExecutionPlan>>,
413    ) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
414        assert!(!children.is_empty());
415        Ok(Arc::new(Self {
416            tag_columns: self.tag_columns.clone(),
417            time_index_column: self.time_index_column.clone(),
418            input: children[0].clone(),
419            metric: self.metric.clone(),
420        }))
421    }
422
423    fn execute(
424        &self,
425        partition: usize,
426        context: Arc<TaskContext>,
427    ) -> DataFusionResult<SendableRecordBatchStream> {
428        let baseline_metric = BaselineMetrics::new(&self.metric, partition);
429        let metrics_builder = MetricBuilder::new(&self.metric);
430        let num_series = Count::new();
431        metrics_builder
432            .with_partition(partition)
433            .build(MetricValue::Count {
434                name: METRIC_NUM_SERIES.into(),
435                count: num_series.clone(),
436            });
437
438        let input = self.input.execute(partition, context)?;
439        let schema = input.schema();
440        let tag_indices = self
441            .tag_columns
442            .iter()
443            .map(|tag| {
444                schema
445                    .column_with_name(tag)
446                    .unwrap_or_else(|| panic!("tag column not found {tag}"))
447                    .0
448            })
449            .collect();
450        Ok(Box::pin(SeriesDivideStream {
451            tag_indices,
452            buffer: vec![],
453            schema,
454            input,
455            metric: baseline_metric,
456            num_series,
457            inspect_start: 0,
458        }))
459    }
460
461    fn metrics(&self) -> Option<MetricsSet> {
462        Some(self.metric.clone_inner())
463    }
464
465    fn name(&self) -> &str {
466        "SeriesDivideExec"
467    }
468}
469
470impl DisplayAs for SeriesDivideExec {
471    fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
472        match t {
473            DisplayFormatType::Default
474            | DisplayFormatType::Verbose
475            | DisplayFormatType::TreeRender => {
476                write!(f, "PromSeriesDivideExec: tags={:?}", self.tag_columns)
477            }
478        }
479    }
480}
481
482/// Assume the input stream is ordered on the tag columns.
483pub struct SeriesDivideStream {
484    tag_indices: Vec<usize>,
485    buffer: Vec<RecordBatch>,
486    schema: SchemaRef,
487    input: SendableRecordBatchStream,
488    metric: BaselineMetrics,
489    /// Index of buffered batches to start inspect next time.
490    inspect_start: usize,
491    /// Number of series processed.
492    num_series: Count,
493}
494
495impl RecordBatchStream for SeriesDivideStream {
496    fn schema(&self) -> SchemaRef {
497        self.schema.clone()
498    }
499}
500
501fn constant_string_dictionary<K: ArrowDictionaryKeyType>(
502    dictionary: &DictionaryArray<K>,
503    total_rows: usize,
504) -> DataFusionResult<ArrayRef> {
505    let key = dictionary.key(0);
506    let value = key.and_then(|key| string_array_value_at_index(dictionary.values(), key));
507    let values: ArrayRef = match dictionary.values().data_type() {
508        DataType::Utf8 => Arc::new(StringArray::from(vec![value])),
509        DataType::LargeUtf8 => Arc::new(LargeStringArray::from(vec![value])),
510        DataType::Utf8View => Arc::new(StringViewArray::from(vec![value])),
511        _ => unreachable!("dictionary values must be strings"),
512    };
513    let keys = PrimitiveArray::<K>::new(
514        vec![K::Native::default(); total_rows].into(),
515        key.is_none().then(|| NullBuffer::new_null(total_rows)),
516    );
517
518    Ok(Arc::new(DictionaryArray::try_new(keys, values)?))
519}
520
521/// Concatenates batches from one isolated series; every designated tag is constant.
522fn concat_series_batches(
523    schema: &SchemaRef,
524    batches: &[RecordBatch],
525    tag_indices: &[usize],
526) -> DataFusionResult<RecordBatch> {
527    if batches.len() <= 1 || tag_indices.is_empty() {
528        return Ok(compute::concat_batches(schema, batches)?);
529    }
530
531    let Some(first_batch) = batches.iter().find(|batch| batch.num_rows() > 0) else {
532        return Ok(compute::concat_batches(schema, batches)?);
533    };
534
535    // This endpoint-only sanity check does not validate tags in interior rows.
536    #[cfg(debug_assertions)]
537    {
538        let last_batch = batches
539            .iter()
540            .rfind(|batch| batch.num_rows() > 0)
541            .expect("first non-empty batch implies a last non-empty batch");
542        if let (Ok(first_tags), Ok(last_tags)) = (
543            TagIdentifier::try_new(first_batch, tag_indices),
544            TagIdentifier::try_new(last_batch, tag_indices),
545        ) {
546            debug_assert!(
547                first_tags.equal_at(0, &last_tags, last_batch.num_rows() - 1)?,
548                "series batch tag endpoints must match"
549            );
550        }
551    }
552
553    let total_rows: usize = batches.iter().map(RecordBatch::num_rows).sum();
554    let columns = schema
555        .fields()
556        .iter()
557        .enumerate()
558        .map(|(index, field)| -> DataFusionResult<ArrayRef> {
559            if tag_indices.contains(&index)
560                && matches!(field.data_type(), DataType::Dictionary(_, value_type) if value_type.is_string())
561            {
562                let array = first_batch.column(index);
563                downcast_dictionary_array! {
564                    array => constant_string_dictionary(array, total_rows),
565                    _ => unreachable!("dictionary keys must be integers"),
566                }
567            } else {
568                compute::concat(
569                    &batches
570                        .iter()
571                        .map(|batch| batch.column(index).as_ref())
572                        .collect::<Vec<_>>(),
573                )
574                .map_err(Into::into)
575            }
576        })
577        .collect::<DataFusionResult<Vec<_>>>()?;
578
579    RecordBatch::try_new(schema.clone(), columns).map_err(Into::into)
580}
581
582impl Stream for SeriesDivideStream {
583    type Item = DataFusionResult<RecordBatch>;
584
585    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
586        loop {
587            if !self.buffer.is_empty() {
588                let timer = std::time::Instant::now();
589                let cut_at = match self.find_first_diff_row() {
590                    Ok(cut_at) => cut_at,
591                    Err(e) => return Poll::Ready(Some(Err(e))),
592                };
593                if let Some((batch_index, row_index)) = cut_at {
594                    // slice out the first time series and return it.
595                    let half_batch_of_first_series =
596                        self.buffer[batch_index].slice(0, row_index + 1);
597                    let half_batch_of_second_series = self.buffer[batch_index].slice(
598                        row_index + 1,
599                        self.buffer[batch_index].num_rows() - row_index - 1,
600                    );
601                    let result_batches = self
602                        .buffer
603                        .drain(0..batch_index)
604                        .chain([half_batch_of_first_series])
605                        .collect::<Vec<_>>();
606                    if half_batch_of_second_series.num_rows() > 0 {
607                        self.buffer[0] = half_batch_of_second_series;
608                    } else {
609                        self.buffer.remove(0);
610                    }
611                    let result_batch =
612                        concat_series_batches(&self.schema, &result_batches, &self.tag_indices)?;
613
614                    self.inspect_start = 0;
615                    self.num_series.add(1);
616                    self.metric.elapsed_compute().add_elapsed(timer);
617                    return Poll::Ready(Some(Ok(result_batch)));
618                } else {
619                    self.metric.elapsed_compute().add_elapsed(timer);
620                    // continue to fetch next batch as the current buffer only contains one time series.
621                    let next_batch = ready!(self.as_mut().fetch_next_batch(cx)).transpose()?;
622                    let timer = std::time::Instant::now();
623                    if let Some(next_batch) = next_batch {
624                        if next_batch.num_rows() != 0 {
625                            self.buffer.push(next_batch);
626                        }
627                        continue;
628                    } else {
629                        // input stream is ended
630                        let result =
631                            concat_series_batches(&self.schema, &self.buffer, &self.tag_indices)?;
632                        self.buffer.clear();
633                        self.inspect_start = 0;
634                        self.num_series.add(1);
635                        self.metric.elapsed_compute().add_elapsed(timer);
636                        return Poll::Ready(Some(Ok(result)));
637                    }
638                }
639            } else {
640                let batch = match ready!(self.as_mut().fetch_next_batch(cx)) {
641                    Some(Ok(batch)) => batch,
642                    None => {
643                        PROMQL_SERIES_COUNT.observe(self.num_series.value() as f64);
644                        return Poll::Ready(None);
645                    }
646                    error => return Poll::Ready(error),
647                };
648                if batch.num_rows() != 0 {
649                    self.buffer.push(batch);
650                }
651                continue;
652            }
653        }
654    }
655}
656
657impl SeriesDivideStream {
658    fn fetch_next_batch(
659        mut self: Pin<&mut Self>,
660        cx: &mut Context<'_>,
661    ) -> Poll<Option<DataFusionResult<RecordBatch>>> {
662        let poll = self.input.poll_next_unpin(cx);
663        self.metric.record_poll(poll)
664    }
665
666    /// Return the position to cut buffer.
667    /// None implies the current buffer only contains one time series.
668    fn find_first_diff_row(&mut self) -> DataFusionResult<Option<(usize, usize)>> {
669        // fast path: no tag columns means all data belongs to the same series.
670        if self.tag_indices.is_empty() {
671            return Ok(None);
672        }
673
674        let mut resumed_batch_index = self.inspect_start;
675
676        for batch in &self.buffer[resumed_batch_index..] {
677            let num_rows = batch.num_rows();
678            let tags = TagIdentifier::try_new(batch, &self.tag_indices)?;
679
680            // check if the first row is the same with last batch's last row
681            if resumed_batch_index > self.inspect_start.saturating_sub(1) {
682                let last_batch = &self.buffer[resumed_batch_index - 1];
683                let last_row = last_batch.num_rows() - 1;
684                let last_tags = TagIdentifier::try_new(last_batch, &self.tag_indices)?;
685                if !tags.equal_at(0, &last_tags, last_row)? {
686                    return Ok(Some((resumed_batch_index - 1, last_row)));
687                }
688            }
689
690            // quick check if all rows are the same by comparing the first and last row in this batch
691            if tags.equal_at(0, &tags, num_rows - 1)? {
692                resumed_batch_index += 1;
693                continue;
694            }
695
696            let mut same_until = 0;
697            while same_until < num_rows - 1 {
698                if !tags.equal_at(same_until, &tags, same_until + 1)? {
699                    break;
700                }
701                same_until += 1;
702            }
703
704            if same_until + 1 >= num_rows {
705                // all rows are the same, inspect next batch
706                resumed_batch_index += 1;
707            } else {
708                return Ok(Some((resumed_batch_index, same_until)));
709            }
710        }
711
712        self.inspect_start = resumed_batch_index;
713        Ok(None)
714    }
715}
716
717#[cfg(test)]
718mod test {
719    use std::collections::HashMap;
720
721    use datafusion::arrow::array::{
722        DictionaryArray, Int32Array, Int64Array, LargeStringArray, PrimitiveArray, StringArray,
723        StringViewArray, UInt32Array, UInt64Array,
724    };
725    use datafusion::arrow::datatypes::{
726        ArrowDictionaryKeyType, DataType, Field, Int8Type, Int16Type, Int32Type, Int64Type, Schema,
727        UInt8Type, UInt16Type, UInt32Type, UInt64Type,
728    };
729    use datafusion::common::ToDFSchema;
730    use datafusion::datasource::memory::MemorySourceConfig;
731    use datafusion::datasource::source::DataSourceExec;
732    use datafusion::logical_expr::{EmptyRelation, LogicalPlan};
733    use datafusion::prelude::SessionContext;
734
735    use super::*;
736
737    fn assert_concat_matches_reference(
738        schema: SchemaRef,
739        batches: Vec<RecordBatch>,
740        tags: &[usize],
741    ) {
742        let expected = compute::concat_batches(&schema, &batches).unwrap();
743        let actual = concat_series_batches(&schema, &batches, tags).unwrap();
744        assert_eq!(actual.schema(), schema);
745        assert_eq!(actual.num_rows(), expected.num_rows());
746
747        for (index, (actual, expected)) in
748            actual.columns().iter().zip(expected.columns()).enumerate()
749        {
750            assert_eq!(actual.data_type(), expected.data_type(), "column {index}");
751            let (actual, expected) = match actual.data_type() {
752                DataType::Dictionary(_, value_type) => (
753                    compute::cast(actual.as_ref(), value_type).unwrap(),
754                    compute::cast(expected.as_ref(), value_type).unwrap(),
755                ),
756                _ => (actual.clone(), expected.clone()),
757            };
758            assert_eq!(actual.to_data(), expected.to_data(), "column {index}");
759        }
760    }
761
762    #[test]
763    fn test_concat_series_batches() {
764        let schema = Arc::new(Schema::new(vec![
765            Field::new("tag", DataType::Utf8, true),
766            Field::new("value", DataType::Int64, true),
767        ]));
768        let batches = vec![
769            RecordBatch::try_new(
770                schema.clone(),
771                vec![
772                    Arc::new(StringArray::from(vec![Some("tag"), Some("tag")])),
773                    Arc::new(Int64Array::from(vec![Some(1), None])),
774                ],
775            )
776            .unwrap(),
777            RecordBatch::try_new(
778                schema.clone(),
779                vec![
780                    Arc::new(StringArray::from(vec![Some("tag"), Some("tag")])),
781                    Arc::new(Int64Array::from(vec![Some(3), Some(4)])),
782                ],
783            )
784            .unwrap(),
785        ];
786        assert_concat_matches_reference(schema, batches, &[0]);
787    }
788
789    #[cfg(debug_assertions)]
790    #[test]
791    #[should_panic(expected = "series batch tag endpoints must match")]
792    fn test_concat_series_batches_mismatched_tag_endpoints_panics() {
793        let schema = Arc::new(Schema::new(vec![
794            Field::new("tag", DataType::Utf8, true),
795            Field::new("value", DataType::Int64, true),
796        ]));
797        let batches = vec![
798            RecordBatch::try_new(
799                schema.clone(),
800                vec![
801                    Arc::new(StringArray::from(vec!["first"])),
802                    Arc::new(Int64Array::from(vec![1])),
803                ],
804            )
805            .unwrap(),
806            RecordBatch::try_new(
807                schema.clone(),
808                vec![
809                    Arc::new(StringArray::from(vec!["last"])),
810                    Arc::new(Int64Array::from(vec![2])),
811                ],
812            )
813            .unwrap(),
814        ];
815
816        concat_series_batches(&schema, &batches, &[0]).unwrap();
817    }
818
819    #[test]
820    fn test_concat_series_batches_string_tags() {
821        for (data_type, batches) in [
822            (
823                DataType::LargeUtf8,
824                vec![
825                    Arc::new(LargeStringArray::from(vec![
826                        Some("long tag"),
827                        Some("long tag"),
828                    ])) as ArrayRef,
829                    Arc::new(LargeStringArray::from(vec![Some("long tag")])) as ArrayRef,
830                ],
831            ),
832            (
833                DataType::Utf8View,
834                vec![
835                    Arc::new(StringViewArray::from(vec![
836                        Some("view tag longer than twelve bytes"),
837                        Some("view tag longer than twelve bytes"),
838                    ])) as ArrayRef,
839                    Arc::new(StringViewArray::from(vec![Some(
840                        "view tag longer than twelve bytes",
841                    )])) as ArrayRef,
842                ],
843            ),
844            (
845                DataType::Utf8,
846                vec![
847                    Arc::new(StringArray::from(vec![None::<&str>, None])) as ArrayRef,
848                    Arc::new(StringArray::from(vec![None::<&str>])) as ArrayRef,
849                ],
850            ),
851            (
852                DataType::Utf8,
853                vec![
854                    Arc::new(StringArray::from(vec![Some(""), Some("")])) as ArrayRef,
855                    Arc::new(StringArray::from(vec![Some("")])) as ArrayRef,
856                ],
857            ),
858        ] {
859            let schema = Arc::new(Schema::new(vec![
860                Field::new("tag", data_type, true),
861                Field::new("value", DataType::Int64, true),
862            ]));
863            let batches = batches
864                .into_iter()
865                .enumerate()
866                .map(|(index, tag)| {
867                    RecordBatch::try_new(
868                        schema.clone(),
869                        vec![
870                            tag.clone(),
871                            Arc::new(Int64Array::from_iter_values(
872                                (0..tag.len()).map(|row| (index * 10 + row) as i64),
873                            )),
874                        ],
875                    )
876                    .unwrap()
877                })
878                .collect();
879            assert_concat_matches_reference(schema, batches, &[0]);
880        }
881    }
882
883    fn assert_dictionary_concat<K: ArrowDictionaryKeyType>(
884        keys: Vec<Option<K::Native>>,
885        values: ArrayRef,
886        other_keys: Vec<Option<K::Native>>,
887        other_values: ArrayRef,
888    ) where
889        PrimitiveArray<K>: From<Vec<Option<K::Native>>>,
890    {
891        let data_type = DataType::Dictionary(
892            Box::new(PrimitiveArray::<K>::from(keys.clone()).data_type().clone()),
893            Box::new(values.data_type().clone()),
894        );
895        let schema = Arc::new(Schema::new(vec![
896            Field::new("tag", data_type, true),
897            Field::new("value", DataType::Int64, true),
898        ]));
899        let first = Arc::new(DictionaryArray::<K>::new(
900            PrimitiveArray::<K>::from(keys),
901            values,
902        )) as ArrayRef;
903        let second = Arc::new(DictionaryArray::<K>::new(
904            PrimitiveArray::<K>::from(other_keys),
905            other_values,
906        )) as ArrayRef;
907        let batches = [first, second]
908            .into_iter()
909            .enumerate()
910            .map(|(index, tag)| {
911                RecordBatch::try_new(
912                    schema.clone(),
913                    vec![
914                        tag.clone(),
915                        Arc::new(Int64Array::from_iter_values(
916                            (0..tag.len()).map(|row| (index * 10 + row) as i64),
917                        )),
918                    ],
919                )
920                .unwrap()
921            })
922            .collect();
923        assert_concat_matches_reference(schema, batches, &[0]);
924    }
925
926    #[test]
927    fn test_concat_series_batches_dictionary_tags() {
928        macro_rules! dictionary_cases {
929            ($($key_type:ty, $key:expr),+ $(,)?) => {
930                $(for (values, other_values) in [
931                    (
932                        Arc::new(StringArray::from(vec!["other", "tag"])) as ArrayRef,
933                        Arc::new(StringArray::from(vec!["tag", "other"])) as ArrayRef,
934                    ),
935                    (
936                        Arc::new(LargeStringArray::from(vec!["other", "tag"])) as ArrayRef,
937                        Arc::new(LargeStringArray::from(vec!["tag", "other"])) as ArrayRef,
938                    ),
939                    (
940                        Arc::new(StringViewArray::from(vec![
941                            "other",
942                            "view tag longer than twelve bytes",
943                        ])) as ArrayRef,
944                        Arc::new(StringViewArray::from(vec![
945                            "view tag longer than twelve bytes",
946                            "other",
947                        ])) as ArrayRef,
948                    ),
949                ] {
950                    assert_dictionary_concat::<$key_type>(
951                        vec![Some(($key)(1)), Some(($key)(1))],
952                        values,
953                        vec![Some(($key)(0))],
954                        other_values,
955                    );
956                })+
957            };
958        }
959        dictionary_cases!(
960            Int8Type,
961            |value| value as i8,
962            Int16Type,
963            |value| value as i16,
964            Int32Type,
965            |value: i32| value,
966            Int64Type,
967            |value| value as i64,
968            UInt8Type,
969            |value| value as u8,
970            UInt16Type,
971            |value| value as u16,
972            UInt32Type,
973            |value| value as u32,
974            UInt64Type,
975            |value| value as u64,
976        );
977        assert_dictionary_concat::<UInt16Type>(
978            vec![Some(0), Some(0)],
979            Arc::new(LargeStringArray::from(vec![None::<&str>])),
980            vec![None],
981            Arc::new(LargeStringArray::from(vec!["unused"])),
982        );
983        // Compare logical values rather than dictionary keys for the reverse null orientation.
984        assert_dictionary_concat::<UInt16Type>(
985            vec![None],
986            Arc::new(StringArray::from(vec!["unused"])),
987            vec![Some(0)],
988            Arc::new(StringArray::from(vec![None::<&str>])),
989        );
990    }
991
992    #[test]
993    fn test_concat_series_batches_dictionary_tag_drops_unused_values() {
994        let active = "active view tag longer than twelve bytes";
995        let unused = "unused view tag ".repeat(1024);
996        let values = Arc::new(StringViewArray::from(vec![unused.as_str(), active])) as ArrayRef;
997        let schema = Arc::new(Schema::new(vec![
998            Field::new(
999                "tag",
1000                DataType::Dictionary(Box::new(DataType::UInt32), Box::new(DataType::Utf8View)),
1001                false,
1002            ),
1003            Field::new("value", DataType::Int64, false),
1004        ]));
1005        let batches = [vec![1, 1], vec![1]]
1006            .into_iter()
1007            .enumerate()
1008            .map(|(batch, keys)| {
1009                let num_rows = keys.len();
1010                RecordBatch::try_new(
1011                    schema.clone(),
1012                    vec![
1013                        Arc::new(DictionaryArray::<UInt32Type>::new(
1014                            UInt32Array::from(keys),
1015                            values.clone(),
1016                        )),
1017                        Arc::new(Int64Array::from_iter_values(
1018                            (0..num_rows).map(|row| (batch * 10 + row) as i64),
1019                        )),
1020                    ],
1021                )
1022                .unwrap()
1023            })
1024            .collect::<Vec<_>>();
1025
1026        assert_concat_matches_reference(schema.clone(), batches.clone(), &[0]);
1027
1028        let actual = concat_series_batches(&schema, &batches, &[0]).unwrap();
1029        let tag = actual
1030            .column(0)
1031            .as_any()
1032            .downcast_ref::<DictionaryArray<UInt32Type>>()
1033            .unwrap();
1034        let values = tag
1035            .values()
1036            .as_any()
1037            .downcast_ref::<StringViewArray>()
1038            .unwrap();
1039        assert_eq!(values.len(), 1);
1040        assert_eq!(values.value(0), active);
1041        assert_eq!(
1042            values
1043                .data_buffers()
1044                .iter()
1045                .map(|buffer| buffer.len())
1046                .sum::<usize>(),
1047            active.len()
1048        );
1049    }
1050
1051    #[test]
1052    fn test_concat_series_batches_uint64_nullable_tag() {
1053        let schema = Arc::new(Schema::new(vec![
1054            Field::new("tsid", DataType::UInt64, true),
1055            Field::new("value", DataType::Int64, true),
1056        ]));
1057        for tags in [vec![Some(42), Some(42)], vec![None, None]] {
1058            let batches = [tags.clone(), tags]
1059                .into_iter()
1060                .enumerate()
1061                .map(|(batch, tags)| {
1062                    RecordBatch::try_new(
1063                        schema.clone(),
1064                        vec![
1065                            Arc::new(UInt64Array::from(tags)),
1066                            Arc::new(Int64Array::from_iter_values(
1067                                (0..2).map(|row| (batch * 10 + row) as i64),
1068                            )),
1069                        ],
1070                    )
1071                    .unwrap()
1072                })
1073                .collect();
1074            assert_concat_matches_reference(schema.clone(), batches, &[0]);
1075        }
1076    }
1077
1078    #[test]
1079    fn test_concat_series_batches_leading_empty_and_sliced_batches() {
1080        let schema = Arc::new(Schema::new(vec![
1081            Field::new("tag", DataType::Utf8, true),
1082            Field::new("value", DataType::Int64, true),
1083        ]));
1084        let make_sliced_batch = |values| {
1085            RecordBatch::try_new(
1086                schema.clone(),
1087                vec![
1088                    Arc::new(StringArray::from(vec!["discard", "tag", "tag"])),
1089                    Arc::new(Int64Array::from(values)),
1090                ],
1091            )
1092            .unwrap()
1093            .slice(1, 2)
1094        };
1095        let batches = vec![
1096            RecordBatch::new_empty(schema.clone()),
1097            make_sliced_batch(vec![0, 1, 2]),
1098            make_sliced_batch(vec![3, 4, 5]),
1099        ];
1100        assert_concat_matches_reference(schema, batches, &[0]);
1101    }
1102
1103    #[test]
1104    fn test_concat_series_batches_interleaved_tags_and_schema_metadata() {
1105        let schema = Arc::new(Schema::new_with_metadata(
1106            vec![
1107                Field::new("value_before", DataType::Int64, true),
1108                Field::new("host", DataType::Utf8, true),
1109                Field::new("value_between", DataType::UInt64, true),
1110                Field::new("path", DataType::Utf8, true),
1111                Field::new("value_after", DataType::Int32, true),
1112            ],
1113            HashMap::from([("source".to_string(), "concat test".to_string())]),
1114        ));
1115        let batches = [
1116            (
1117                vec![Some(1), None],
1118                vec![Some(10), Some(11)],
1119                vec![100, 101],
1120            ),
1121            (
1122                vec![Some(2), Some(3)],
1123                vec![Some(12), Some(13)],
1124                vec![102, 103],
1125            ),
1126        ]
1127        .into_iter()
1128        .map(|(before, between, after)| {
1129            RecordBatch::try_new(
1130                schema.clone(),
1131                vec![
1132                    Arc::new(Int64Array::from(before)),
1133                    Arc::new(StringArray::from(vec!["host-a", "host-a"])),
1134                    Arc::new(UInt64Array::from(between)),
1135                    Arc::new(StringArray::from(vec!["/metrics", "/metrics"])),
1136                    Arc::new(Int32Array::from(after)),
1137                ],
1138            )
1139            .unwrap()
1140        })
1141        .collect();
1142        assert_concat_matches_reference(schema, batches, &[1, 3]);
1143    }
1144
1145    #[test]
1146    fn test_concat_series_batches_fallbacks() {
1147        let schema = Arc::new(Schema::new(vec![Field::new(
1148            "value",
1149            DataType::Int64,
1150            true,
1151        )]));
1152        let batch = RecordBatch::try_new(
1153            schema.clone(),
1154            vec![Arc::new(Int64Array::from(vec![Some(1), None]))],
1155        )
1156        .unwrap();
1157        assert_concat_matches_reference(schema.clone(), vec![batch.clone()], &[0]);
1158        assert_concat_matches_reference(schema.clone(), vec![batch.clone(), batch], &[]);
1159
1160        let empty_schema = Arc::new(Schema::empty());
1161        let empty_batch = RecordBatch::try_new_with_options(
1162            empty_schema.clone(),
1163            vec![],
1164            &datafusion::arrow::record_batch::RecordBatchOptions::new().with_row_count(Some(2)),
1165        )
1166        .unwrap();
1167        assert_concat_matches_reference(
1168            empty_schema.clone(),
1169            vec![empty_batch.clone(), empty_batch],
1170            &[],
1171        );
1172
1173        let zero_rows = RecordBatch::new_empty(schema.clone());
1174        assert_concat_matches_reference(schema, vec![zero_rows.clone(), zero_rows], &[0]);
1175    }
1176
1177    #[test]
1178    fn test_dictionary_tag_child_null_comparison() {
1179        let dictionary: ArrayRef = Arc::new(DictionaryArray::<UInt32Type>::new(
1180            UInt32Array::from(vec![Some(0), None, Some(1)]),
1181            Arc::new(StringArray::from(vec![None, Some("")])),
1182        ));
1183        let tags = RawTagColumn::try_new(&dictionary).unwrap();
1184
1185        assert!(tags.equal_at(0, &tags, 1));
1186        assert!(!tags.equal_at(0, &tags, 2));
1187        assert!(!tags.equal_at(1, &tags, 2));
1188    }
1189
1190    #[test]
1191    fn test_dictionary_tag_with_non_uint32_keys() {
1192        let dictionary: ArrayRef = Arc::new(DictionaryArray::<Int32Type>::new(
1193            Int32Array::from(vec![0, 1]),
1194            Arc::new(StringArray::from(vec!["host-a", "host-b"])),
1195        ));
1196
1197        let tags = RawTagColumn::try_new(&dictionary).unwrap();
1198
1199        assert!(tags.equal_at(0, &tags, 0));
1200        assert!(!tags.equal_at(0, &tags, 1));
1201    }
1202
1203    fn prepare_test_data() -> DataSourceExec {
1204        let schema = Arc::new(Schema::new(vec![
1205            Field::new("host", DataType::Utf8, true),
1206            Field::new("path", DataType::Utf8, true),
1207            Field::new(
1208                "time_index",
1209                DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1210                false,
1211            ),
1212        ]));
1213
1214        let path_column_1 = Arc::new(StringArray::from(vec![
1215            "foo", "foo", "foo", "bar", "bar", "bar", "bar", "bar", "bar", "bla", "bla", "bla",
1216        ])) as _;
1217        let host_column_1 = Arc::new(StringArray::from(vec![
1218            "000", "000", "001", "002", "002", "002", "002", "002", "003", "005", "005", "005",
1219        ])) as _;
1220        let time_index_column_1 = Arc::new(
1221            datafusion::arrow::array::TimestampMillisecondArray::from(vec![
1222                1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000, 11000, 12000,
1223            ]),
1224        ) as _;
1225
1226        let path_column_2 = Arc::new(StringArray::from(vec!["bla", "bla", "bla"])) as _;
1227        let host_column_2 = Arc::new(StringArray::from(vec!["005", "005", "005"])) as _;
1228        let time_index_column_2 = Arc::new(
1229            datafusion::arrow::array::TimestampMillisecondArray::from(vec![13000, 14000, 15000]),
1230        ) as _;
1231
1232        let path_column_3 = Arc::new(StringArray::from(vec![
1233            "bla", "🥺", "🥺", "🥺", "🥺", "🥺", "🫠", "🫠",
1234        ])) as _;
1235        let host_column_3 = Arc::new(StringArray::from(vec![
1236            "005", "001", "001", "001", "001", "001", "001", "001",
1237        ])) as _;
1238        let time_index_column_3 =
1239            Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1240                vec![16000, 17000, 18000, 19000, 20000, 21000, 22000, 23000],
1241            )) as _;
1242
1243        let data_1 = RecordBatch::try_new(
1244            schema.clone(),
1245            vec![path_column_1, host_column_1, time_index_column_1],
1246        )
1247        .unwrap();
1248        let data_2 = RecordBatch::try_new(
1249            schema.clone(),
1250            vec![path_column_2, host_column_2, time_index_column_2],
1251        )
1252        .unwrap();
1253        let data_3 = RecordBatch::try_new(
1254            schema.clone(),
1255            vec![path_column_3, host_column_3, time_index_column_3],
1256        )
1257        .unwrap();
1258
1259        DataSourceExec::new(Arc::new(
1260            MemorySourceConfig::try_new(&[vec![data_1, data_2, data_3]], schema, None).unwrap(),
1261        ))
1262    }
1263
1264    #[test]
1265    fn pruning_should_keep_tags_and_time_index_columns_for_exec() {
1266        let df_schema = prepare_test_data().schema().to_dfschema_ref().unwrap();
1267        let input = LogicalPlan::EmptyRelation(EmptyRelation {
1268            produce_one_row: false,
1269            schema: df_schema,
1270        });
1271        let plan = SeriesDivide::new(
1272            vec!["host".to_string(), "path".to_string()],
1273            "time_index".to_string(),
1274            input,
1275        );
1276
1277        // Simulate a parent projection requesting only the `host` column.
1278        let output_columns = [0usize];
1279        let required = plan.necessary_children_exprs(&output_columns).unwrap();
1280        let required = &required[0];
1281        assert_eq!(required.as_slice(), &[0, 1, 2]);
1282    }
1283
1284    #[tokio::test]
1285    async fn overall_data() {
1286        let memory_exec = Arc::new(prepare_test_data());
1287        let divide_exec = Arc::new(SeriesDivideExec {
1288            tag_columns: vec!["host".to_string(), "path".to_string()],
1289            time_index_column: "time_index".to_string(),
1290            input: memory_exec,
1291            metric: ExecutionPlanMetricsSet::new(),
1292        });
1293        let session_context = SessionContext::default();
1294        let result = datafusion::physical_plan::collect(divide_exec, session_context.task_ctx())
1295            .await
1296            .unwrap();
1297        let result_literal = datatypes::arrow::util::pretty::pretty_format_batches(&result)
1298            .unwrap()
1299            .to_string();
1300
1301        let expected = String::from(
1302            "+------+------+---------------------+\
1303            \n| host | path | time_index          |\
1304            \n+------+------+---------------------+\
1305            \n| foo  | 000  | 1970-01-01T00:00:01 |\
1306            \n| foo  | 000  | 1970-01-01T00:00:02 |\
1307            \n| foo  | 001  | 1970-01-01T00:00:03 |\
1308            \n| bar  | 002  | 1970-01-01T00:00:04 |\
1309            \n| bar  | 002  | 1970-01-01T00:00:05 |\
1310            \n| bar  | 002  | 1970-01-01T00:00:06 |\
1311            \n| bar  | 002  | 1970-01-01T00:00:07 |\
1312            \n| bar  | 002  | 1970-01-01T00:00:08 |\
1313            \n| bar  | 003  | 1970-01-01T00:00:09 |\
1314            \n| bla  | 005  | 1970-01-01T00:00:10 |\
1315            \n| bla  | 005  | 1970-01-01T00:00:11 |\
1316            \n| bla  | 005  | 1970-01-01T00:00:12 |\
1317            \n| bla  | 005  | 1970-01-01T00:00:13 |\
1318            \n| bla  | 005  | 1970-01-01T00:00:14 |\
1319            \n| bla  | 005  | 1970-01-01T00:00:15 |\
1320            \n| bla  | 005  | 1970-01-01T00:00:16 |\
1321            \n| 🥺   | 001  | 1970-01-01T00:00:17 |\
1322            \n| 🥺   | 001  | 1970-01-01T00:00:18 |\
1323            \n| 🥺   | 001  | 1970-01-01T00:00:19 |\
1324            \n| 🥺   | 001  | 1970-01-01T00:00:20 |\
1325            \n| 🥺   | 001  | 1970-01-01T00:00:21 |\
1326            \n| 🫠   | 001  | 1970-01-01T00:00:22 |\
1327            \n| 🫠   | 001  | 1970-01-01T00:00:23 |\
1328            \n+------+------+---------------------+",
1329        );
1330        assert_eq!(result_literal, expected);
1331    }
1332
1333    #[tokio::test]
1334    async fn per_batch_data() {
1335        let memory_exec = Arc::new(prepare_test_data());
1336        let divide_exec = Arc::new(SeriesDivideExec {
1337            tag_columns: vec!["host".to_string(), "path".to_string()],
1338            time_index_column: "time_index".to_string(),
1339            input: memory_exec,
1340            metric: ExecutionPlanMetricsSet::new(),
1341        });
1342        let mut divide_stream = divide_exec
1343            .execute(0, SessionContext::default().task_ctx())
1344            .unwrap();
1345
1346        let mut expectations = vec![
1347            String::from(
1348                "+------+------+---------------------+\
1349                \n| host | path | time_index          |\
1350                \n+------+------+---------------------+\
1351                \n| foo  | 000  | 1970-01-01T00:00:01 |\
1352                \n| foo  | 000  | 1970-01-01T00:00:02 |\
1353                \n+------+------+---------------------+",
1354            ),
1355            String::from(
1356                "+------+------+---------------------+\
1357                \n| host | path | time_index          |\
1358                \n+------+------+---------------------+\
1359                \n| foo  | 001  | 1970-01-01T00:00:03 |\
1360                \n+------+------+---------------------+",
1361            ),
1362            String::from(
1363                "+------+------+---------------------+\
1364                \n| host | path | time_index          |\
1365                \n+------+------+---------------------+\
1366                \n| bar  | 002  | 1970-01-01T00:00:04 |\
1367                \n| bar  | 002  | 1970-01-01T00:00:05 |\
1368                \n| bar  | 002  | 1970-01-01T00:00:06 |\
1369                \n| bar  | 002  | 1970-01-01T00:00:07 |\
1370                \n| bar  | 002  | 1970-01-01T00:00:08 |\
1371                \n+------+------+---------------------+",
1372            ),
1373            String::from(
1374                "+------+------+---------------------+\
1375                \n| host | path | time_index          |\
1376                \n+------+------+---------------------+\
1377                \n| bar  | 003  | 1970-01-01T00:00:09 |\
1378                \n+------+------+---------------------+",
1379            ),
1380            String::from(
1381                "+------+------+---------------------+\
1382                \n| host | path | time_index          |\
1383                \n+------+------+---------------------+\
1384                \n| bla  | 005  | 1970-01-01T00:00:10 |\
1385                \n| bla  | 005  | 1970-01-01T00:00:11 |\
1386                \n| bla  | 005  | 1970-01-01T00:00:12 |\
1387                \n| bla  | 005  | 1970-01-01T00:00:13 |\
1388                \n| bla  | 005  | 1970-01-01T00:00:14 |\
1389                \n| bla  | 005  | 1970-01-01T00:00:15 |\
1390                \n| bla  | 005  | 1970-01-01T00:00:16 |\
1391                \n+------+------+---------------------+",
1392            ),
1393            String::from(
1394                "+------+------+---------------------+\
1395                \n| host | path | time_index          |\
1396                \n+------+------+---------------------+\
1397                \n| 🥺   | 001  | 1970-01-01T00:00:17 |\
1398                \n| 🥺   | 001  | 1970-01-01T00:00:18 |\
1399                \n| 🥺   | 001  | 1970-01-01T00:00:19 |\
1400                \n| 🥺   | 001  | 1970-01-01T00:00:20 |\
1401                \n| 🥺   | 001  | 1970-01-01T00:00:21 |\
1402                \n+------+------+---------------------+",
1403            ),
1404            String::from(
1405                "+------+------+---------------------+\
1406                \n| host | path | time_index          |\
1407                \n+------+------+---------------------+\
1408                \n| 🫠   | 001  | 1970-01-01T00:00:22 |\
1409                \n| 🫠   | 001  | 1970-01-01T00:00:23 |\
1410                \n+------+------+---------------------+",
1411            ),
1412        ];
1413        expectations.reverse();
1414
1415        while let Some(batch) = divide_stream.next().await {
1416            let formatted =
1417                datatypes::arrow::util::pretty::pretty_format_batches(&[batch.unwrap()])
1418                    .unwrap()
1419                    .to_string();
1420            let expected = expectations.pop().unwrap();
1421            assert_eq!(formatted, expected);
1422        }
1423    }
1424
1425    #[tokio::test]
1426    async fn test_all_batches_same_combination() {
1427        // Create a schema with host and path columns, same as prepare_test_data
1428        let schema = Arc::new(Schema::new(vec![
1429            Field::new("host", DataType::Utf8, true),
1430            Field::new("path", DataType::Utf8, true),
1431            Field::new(
1432                "time_index",
1433                DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1434                false,
1435            ),
1436        ]));
1437
1438        // Create batches with three different combinations
1439        // Each batch contains only one combination
1440        // Batches with the same combination are adjacent
1441
1442        // First combination: "server1", "/var/log"
1443        let batch1 = RecordBatch::try_new(
1444            schema.clone(),
1445            vec![
1446                Arc::new(StringArray::from(vec!["server1", "server1", "server1"])) as _,
1447                Arc::new(StringArray::from(vec!["/var/log", "/var/log", "/var/log"])) as _,
1448                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1449                    vec![1000, 2000, 3000],
1450                )) as _,
1451            ],
1452        )
1453        .unwrap();
1454
1455        let batch2 = RecordBatch::try_new(
1456            schema.clone(),
1457            vec![
1458                Arc::new(StringArray::from(vec!["server1", "server1"])) as _,
1459                Arc::new(StringArray::from(vec!["/var/log", "/var/log"])) as _,
1460                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1461                    vec![4000, 5000],
1462                )) as _,
1463            ],
1464        )
1465        .unwrap();
1466
1467        // Second combination: "server2", "/var/data"
1468        let batch3 = RecordBatch::try_new(
1469            schema.clone(),
1470            vec![
1471                Arc::new(StringArray::from(vec!["server2", "server2", "server2"])) as _,
1472                Arc::new(StringArray::from(vec![
1473                    "/var/data",
1474                    "/var/data",
1475                    "/var/data",
1476                ])) as _,
1477                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1478                    vec![6000, 7000, 8000],
1479                )) as _,
1480            ],
1481        )
1482        .unwrap();
1483
1484        let batch4 = RecordBatch::try_new(
1485            schema.clone(),
1486            vec![
1487                Arc::new(StringArray::from(vec!["server2"])) as _,
1488                Arc::new(StringArray::from(vec!["/var/data"])) as _,
1489                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1490                    vec![9000],
1491                )) as _,
1492            ],
1493        )
1494        .unwrap();
1495
1496        // Third combination: "server3", "/opt/logs"
1497        let batch5 = RecordBatch::try_new(
1498            schema.clone(),
1499            vec![
1500                Arc::new(StringArray::from(vec!["server3", "server3"])) as _,
1501                Arc::new(StringArray::from(vec!["/opt/logs", "/opt/logs"])) as _,
1502                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1503                    vec![10000, 11000],
1504                )) as _,
1505            ],
1506        )
1507        .unwrap();
1508
1509        let batch6 = RecordBatch::try_new(
1510            schema.clone(),
1511            vec![
1512                Arc::new(StringArray::from(vec!["server3", "server3", "server3"])) as _,
1513                Arc::new(StringArray::from(vec![
1514                    "/opt/logs",
1515                    "/opt/logs",
1516                    "/opt/logs",
1517                ])) as _,
1518                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1519                    vec![12000, 13000, 14000],
1520                )) as _,
1521            ],
1522        )
1523        .unwrap();
1524
1525        // Create MemoryExec with these batches, keeping same combinations adjacent
1526        let memory_exec = DataSourceExec::from_data_source(
1527            MemorySourceConfig::try_new(
1528                &[vec![batch1, batch2, batch3, batch4, batch5, batch6]],
1529                schema.clone(),
1530                None,
1531            )
1532            .unwrap(),
1533        );
1534
1535        // Create SeriesDivideExec
1536        let divide_exec = Arc::new(SeriesDivideExec {
1537            tag_columns: vec!["host".to_string(), "path".to_string()],
1538            time_index_column: "time_index".to_string(),
1539            input: memory_exec,
1540            metric: ExecutionPlanMetricsSet::new(),
1541        });
1542
1543        // Execute the division
1544        let session_context = SessionContext::default();
1545        let result =
1546            datafusion::physical_plan::collect(divide_exec.clone(), session_context.task_ctx())
1547                .await
1548                .unwrap();
1549
1550        // Verify that we got 3 batches (one for each combination)
1551        assert_eq!(result.len(), 3);
1552
1553        // First batch should have 5 rows (3 + 2 from the "server1" combination)
1554        assert_eq!(result[0].num_rows(), 5);
1555
1556        // Second batch should have 4 rows (3 + 1 from the "server2" combination)
1557        assert_eq!(result[1].num_rows(), 4);
1558
1559        // Third batch should have 5 rows (2 + 3 from the "server3" combination)
1560        assert_eq!(result[2].num_rows(), 5);
1561
1562        // Verify values in first batch (server1, /var/log)
1563        let host_array1 = result[0]
1564            .column(0)
1565            .as_any()
1566            .downcast_ref::<StringArray>()
1567            .unwrap();
1568        let path_array1 = result[0]
1569            .column(1)
1570            .as_any()
1571            .downcast_ref::<StringArray>()
1572            .unwrap();
1573        let time_index_array1 = result[0]
1574            .column(2)
1575            .as_any()
1576            .downcast_ref::<datafusion::arrow::array::TimestampMillisecondArray>()
1577            .unwrap();
1578
1579        for i in 0..5 {
1580            assert_eq!(host_array1.value(i), "server1");
1581            assert_eq!(path_array1.value(i), "/var/log");
1582            assert_eq!(time_index_array1.value(i), 1000 + (i as i64) * 1000);
1583        }
1584
1585        // Verify values in second batch (server2, /var/data)
1586        let host_array2 = result[1]
1587            .column(0)
1588            .as_any()
1589            .downcast_ref::<StringArray>()
1590            .unwrap();
1591        let path_array2 = result[1]
1592            .column(1)
1593            .as_any()
1594            .downcast_ref::<StringArray>()
1595            .unwrap();
1596        let time_index_array2 = result[1]
1597            .column(2)
1598            .as_any()
1599            .downcast_ref::<datafusion::arrow::array::TimestampMillisecondArray>()
1600            .unwrap();
1601
1602        for i in 0..4 {
1603            assert_eq!(host_array2.value(i), "server2");
1604            assert_eq!(path_array2.value(i), "/var/data");
1605            assert_eq!(time_index_array2.value(i), 6000 + (i as i64) * 1000);
1606        }
1607
1608        // Verify values in third batch (server3, /opt/logs)
1609        let host_array3 = result[2]
1610            .column(0)
1611            .as_any()
1612            .downcast_ref::<StringArray>()
1613            .unwrap();
1614        let path_array3 = result[2]
1615            .column(1)
1616            .as_any()
1617            .downcast_ref::<StringArray>()
1618            .unwrap();
1619        let time_index_array3 = result[2]
1620            .column(2)
1621            .as_any()
1622            .downcast_ref::<datafusion::arrow::array::TimestampMillisecondArray>()
1623            .unwrap();
1624
1625        for i in 0..5 {
1626            assert_eq!(host_array3.value(i), "server3");
1627            assert_eq!(path_array3.value(i), "/opt/logs");
1628            assert_eq!(time_index_array3.value(i), 10000 + (i as i64) * 1000);
1629        }
1630
1631        // Also verify streaming behavior
1632        let mut divide_stream = divide_exec
1633            .execute(0, SessionContext::default().task_ctx())
1634            .unwrap();
1635
1636        // Should produce three batches, one for each combination
1637        let batch1 = divide_stream.next().await.unwrap().unwrap();
1638        assert_eq!(batch1.num_rows(), 5); // server1 combination
1639
1640        let batch2 = divide_stream.next().await.unwrap().unwrap();
1641        assert_eq!(batch2.num_rows(), 4); // server2 combination
1642
1643        let batch3 = divide_stream.next().await.unwrap().unwrap();
1644        assert_eq!(batch3.num_rows(), 5); // server3 combination
1645
1646        // No more batches should be produced
1647        assert!(divide_stream.next().await.is_none());
1648    }
1649
1650    #[tokio::test]
1651    async fn test_dictionary_tags_across_batches_and_eof() {
1652        let schema = Arc::new(Schema::new(vec![
1653            Field::new(
1654                "tag",
1655                DataType::Dictionary(Box::new(DataType::UInt32), Box::new(DataType::Utf8)),
1656                false,
1657            ),
1658            Field::new("value", DataType::Int64, false),
1659            Field::new(
1660                "time_index",
1661                DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1662                false,
1663            ),
1664        ]));
1665        let make_batch = |values: Vec<&str>, keys: Vec<u32>, payload: Vec<i64>| {
1666            RecordBatch::try_new(
1667                schema.clone(),
1668                vec![
1669                    Arc::new(DictionaryArray::<UInt32Type>::new(
1670                        UInt32Array::from(keys),
1671                        Arc::new(StringArray::from(values)),
1672                    )),
1673                    Arc::new(Int64Array::from(payload.clone())),
1674                    Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1675                        payload,
1676                    )),
1677                ],
1678            )
1679            .unwrap()
1680        };
1681        let memory_exec: Arc<dyn ExecutionPlan> = Arc::new(DataSourceExec::new(Arc::new(
1682            MemorySourceConfig::try_new(
1683                &[vec![
1684                    make_batch(vec!["b", "a", "unused-a"], vec![1, 1, 0], vec![1, 2, 3]),
1685                    make_batch(vec!["c", "b", "unused-b"], vec![1, 1, 0], vec![4, 5, 6]),
1686                    make_batch(vec!["unused-c", "c"], vec![1, 1], vec![7, 8]),
1687                ]],
1688                schema,
1689                None,
1690            )
1691            .unwrap(),
1692        )));
1693        let divide_exec = Arc::new(SeriesDivideExec {
1694            tag_columns: vec!["tag".to_string()],
1695            time_index_column: "time_index".to_string(),
1696            input: memory_exec,
1697            metric: ExecutionPlanMetricsSet::new(),
1698        });
1699        let mut stream = divide_exec
1700            .execute(0, SessionContext::default().task_ctx())
1701            .unwrap();
1702
1703        for (expected_tag, expected_payload, concatenated) in [
1704            ("a", vec![1, 2], false),
1705            ("b", vec![3, 4, 5], true),
1706            ("c", vec![6, 7, 8], true),
1707        ] {
1708            let batch = stream.next().await.unwrap().unwrap();
1709            assert_eq!(batch.num_rows(), expected_payload.len());
1710            assert_eq!(
1711                (0..batch.num_rows())
1712                    .map(|row| string_array_value_at_index(batch.column(0), row).unwrap())
1713                    .collect::<Vec<_>>(),
1714                vec![expected_tag; expected_payload.len()]
1715            );
1716            assert_eq!(
1717                batch
1718                    .column(1)
1719                    .as_any()
1720                    .downcast_ref::<Int64Array>()
1721                    .unwrap()
1722                    .iter()
1723                    .flatten()
1724                    .collect::<Vec<_>>(),
1725                expected_payload
1726            );
1727
1728            if concatenated {
1729                let tag = batch
1730                    .column(0)
1731                    .as_any()
1732                    .downcast_ref::<DictionaryArray<UInt32Type>>()
1733                    .unwrap();
1734                let values = tag.values().as_any().downcast_ref::<StringArray>().unwrap();
1735                assert_eq!(values.len(), 1, "tag {expected_tag}");
1736                assert_eq!(values.value(0), expected_tag);
1737                assert!(tag.keys().iter().all(|key| key == Some(0)));
1738            }
1739        }
1740        assert!(stream.next().await.is_none());
1741    }
1742
1743    #[tokio::test]
1744    async fn test_string_tag_column_types() {
1745        let schema = Arc::new(Schema::new(vec![
1746            Field::new("tag_large", DataType::LargeUtf8, false),
1747            Field::new("tag_view", DataType::Utf8View, false),
1748            Field::new(
1749                "time_index",
1750                DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1751                false,
1752            ),
1753        ]));
1754
1755        let batch1 = RecordBatch::try_new(
1756            schema.clone(),
1757            vec![
1758                Arc::new(LargeStringArray::from(vec!["a", "a", "a", "a"])),
1759                Arc::new(StringViewArray::from(vec!["x", "x", "y", "y"])),
1760                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1761                    vec![1000, 2000, 1000, 2000],
1762                )),
1763            ],
1764        )
1765        .unwrap();
1766
1767        let batch2 = RecordBatch::try_new(
1768            schema.clone(),
1769            vec![
1770                Arc::new(LargeStringArray::from(vec!["b", "b"])),
1771                Arc::new(StringViewArray::from(vec!["x", "x"])),
1772                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1773                    vec![1000, 2000],
1774                )),
1775            ],
1776        )
1777        .unwrap();
1778
1779        let memory_exec: Arc<dyn ExecutionPlan> = Arc::new(DataSourceExec::new(Arc::new(
1780            MemorySourceConfig::try_new(&[vec![batch1, batch2]], schema.clone(), None).unwrap(),
1781        )));
1782
1783        let divide_exec = Arc::new(SeriesDivideExec {
1784            tag_columns: vec!["tag_large".to_string(), "tag_view".to_string()],
1785            time_index_column: "time_index".to_string(),
1786            input: memory_exec,
1787            metric: ExecutionPlanMetricsSet::new(),
1788        });
1789
1790        let session_context = SessionContext::default();
1791        let result = datafusion::physical_plan::collect(divide_exec, session_context.task_ctx())
1792            .await
1793            .unwrap();
1794
1795        assert_eq!(result.len(), 3);
1796        for ((expected_large, expected_view), batch) in [("a", "x"), ("a", "y"), ("b", "x")]
1797            .into_iter()
1798            .zip(result.iter())
1799        {
1800            assert_eq!(batch.num_rows(), 2);
1801
1802            let tag_large_array = batch
1803                .column(0)
1804                .as_any()
1805                .downcast_ref::<LargeStringArray>()
1806                .unwrap();
1807            let tag_view_array = batch
1808                .column(1)
1809                .as_any()
1810                .downcast_ref::<StringViewArray>()
1811                .unwrap();
1812
1813            for row in 0..batch.num_rows() {
1814                assert_eq!(tag_large_array.value(row), expected_large);
1815                assert_eq!(tag_view_array.value(row), expected_view);
1816            }
1817        }
1818    }
1819
1820    #[tokio::test]
1821    async fn test_u64_tag_column() {
1822        let schema = Arc::new(Schema::new(vec![
1823            Field::new("tsid", DataType::UInt64, false),
1824            Field::new(
1825                "time_index",
1826                DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1827                false,
1828            ),
1829        ]));
1830
1831        let batch1 = RecordBatch::try_new(
1832            schema.clone(),
1833            vec![
1834                Arc::new(UInt64Array::from(vec![1, 1, 2, 2])),
1835                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1836                    vec![1000, 2000, 1000, 2000],
1837                )),
1838            ],
1839        )
1840        .unwrap();
1841
1842        let batch2 = RecordBatch::try_new(
1843            schema.clone(),
1844            vec![
1845                Arc::new(UInt64Array::from(vec![3, 3])),
1846                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1847                    vec![1000, 2000],
1848                )),
1849            ],
1850        )
1851        .unwrap();
1852
1853        let memory_exec: Arc<dyn ExecutionPlan> = Arc::new(DataSourceExec::new(Arc::new(
1854            MemorySourceConfig::try_new(&[vec![batch1, batch2]], schema.clone(), None).unwrap(),
1855        )));
1856
1857        let divide_exec = Arc::new(SeriesDivideExec {
1858            tag_columns: vec!["tsid".to_string()],
1859            time_index_column: "time_index".to_string(),
1860            input: memory_exec,
1861            metric: ExecutionPlanMetricsSet::new(),
1862        });
1863
1864        let session_context = SessionContext::default();
1865        let result = datafusion::physical_plan::collect(divide_exec, session_context.task_ctx())
1866            .await
1867            .unwrap();
1868
1869        assert_eq!(result.len(), 3);
1870        for (expected_tsid, batch) in [1u64, 2u64, 3u64].into_iter().zip(result.iter()) {
1871            assert_eq!(batch.num_rows(), 2);
1872            let tsid_array = batch
1873                .column(0)
1874                .as_any()
1875                .downcast_ref::<UInt64Array>()
1876                .unwrap();
1877            assert!(tsid_array.iter().all(|v| v == Some(expected_tsid)));
1878        }
1879    }
1880
1881    /// The input stream may legally start with a batch that has zero rows.
1882    /// `SeriesDivideStream` must skip it and keep working on the real rows that
1883    /// follow (here: two batches of the very same tagged series).
1884    #[tokio::test]
1885    async fn test_stream_leading_empty_batch_then_same_tags() {
1886        let schema = Arc::new(Schema::new(vec![
1887            Field::new("host", DataType::Utf8, false),
1888            Field::new("path", DataType::Utf8, false),
1889            Field::new(
1890                "time_index",
1891                DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1892                false,
1893            ),
1894        ]));
1895
1896        let make_batch = |hosts: Vec<&str>, paths: Vec<&str>, times: Vec<i64>| {
1897            RecordBatch::try_new(
1898                schema.clone(),
1899                vec![
1900                    Arc::new(StringArray::from(hosts)),
1901                    Arc::new(StringArray::from(paths)),
1902                    Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1903                        times,
1904                    )),
1905                ],
1906            )
1907            .unwrap()
1908        };
1909
1910        let memory_exec: Arc<dyn ExecutionPlan> = Arc::new(DataSourceExec::new(Arc::new(
1911            MemorySourceConfig::try_new(
1912                &[vec![
1913                    RecordBatch::new_empty(schema.clone()),
1914                    make_batch(
1915                        vec!["server1", "server1"],
1916                        vec!["/var/log", "/var/log"],
1917                        vec![1000, 2000],
1918                    ),
1919                    make_batch(vec!["server1"], vec!["/var/log"], vec![3000]),
1920                ]],
1921                schema.clone(),
1922                None,
1923            )
1924            .unwrap(),
1925        )));
1926
1927        let divide_exec = Arc::new(SeriesDivideExec {
1928            tag_columns: vec!["host".to_string(), "path".to_string()],
1929            time_index_column: "time_index".to_string(),
1930            input: memory_exec,
1931            metric: ExecutionPlanMetricsSet::new(),
1932        });
1933
1934        let mut stream = divide_exec
1935            .execute(0, SessionContext::default().task_ctx())
1936            .unwrap();
1937
1938        let batch = stream.next().await.unwrap().unwrap();
1939        assert_eq!(batch.num_rows(), 3);
1940        let expected = [
1941            ("server1", "/var/log", 1000i64),
1942            ("server1", "/var/log", 2000),
1943            ("server1", "/var/log", 3000),
1944        ];
1945        for (row, (host, path, time)) in expected.into_iter().enumerate() {
1946            assert_eq!(
1947                string_array_value_at_index(batch.column(0), row),
1948                Some(host)
1949            );
1950            assert_eq!(
1951                string_array_value_at_index(batch.column(1), row),
1952                Some(path)
1953            );
1954            assert_eq!(
1955                batch
1956                    .column(2)
1957                    .as_any()
1958                    .downcast_ref::<datafusion::arrow::array::TimestampMillisecondArray>()
1959                    .unwrap()
1960                    .value(row),
1961                time
1962            );
1963        }
1964        assert!(stream.next().await.is_none());
1965    }
1966
1967    /// Several leading empty batches (and one empty batch in the middle of a
1968    /// series) must not disturb series division: the two series are still
1969    /// emitted separately, including the last one flushed at end of input.
1970    #[tokio::test]
1971    async fn test_stream_repeated_empty_batches_and_two_series() {
1972        let schema = Arc::new(Schema::new(vec![
1973            Field::new("host", DataType::Utf8, false),
1974            Field::new("value", DataType::Int64, false),
1975            Field::new(
1976                "time_index",
1977                DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1978                false,
1979            ),
1980        ]));
1981
1982        let make_batch = |hosts: Vec<&str>, values: Vec<i64>| {
1983            RecordBatch::try_new(
1984                schema.clone(),
1985                vec![
1986                    Arc::new(StringArray::from(hosts)),
1987                    Arc::new(Int64Array::from(values.clone())),
1988                    Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1989                        values.iter().map(|v| v * 1000).collect::<Vec<_>>(),
1990                    )),
1991                ],
1992            )
1993            .unwrap()
1994        };
1995
1996        let memory_exec: Arc<dyn ExecutionPlan> = Arc::new(DataSourceExec::new(Arc::new(
1997            MemorySourceConfig::try_new(
1998                &[vec![
1999                    RecordBatch::new_empty(schema.clone()),
2000                    RecordBatch::new_empty(schema.clone()),
2001                    make_batch(vec!["server1", "server1"], vec![1, 2]),
2002                    RecordBatch::new_empty(schema.clone()),
2003                    make_batch(vec!["server1"], vec![3]),
2004                    make_batch(vec!["server2", "server2"], vec![4, 5]),
2005                ]],
2006                schema.clone(),
2007                None,
2008            )
2009            .unwrap(),
2010        )));
2011
2012        let divide_exec = Arc::new(SeriesDivideExec {
2013            tag_columns: vec!["host".to_string()],
2014            time_index_column: "time_index".to_string(),
2015            input: memory_exec,
2016            metric: ExecutionPlanMetricsSet::new(),
2017        });
2018
2019        let mut stream = divide_exec
2020            .execute(0, SessionContext::default().task_ctx())
2021            .unwrap();
2022
2023        for (expected_host, expected_values) in
2024            [("server1", vec![1i64, 2, 3]), ("server2", vec![4, 5])]
2025        {
2026            let batch = stream.next().await.unwrap().unwrap();
2027            assert_eq!(batch.num_rows(), expected_values.len());
2028            for (row, value) in expected_values.into_iter().enumerate() {
2029                assert_eq!(
2030                    string_array_value_at_index(batch.column(0), row),
2031                    Some(expected_host)
2032                );
2033                assert_eq!(
2034                    batch
2035                        .column(1)
2036                        .as_any()
2037                        .downcast_ref::<Int64Array>()
2038                        .unwrap()
2039                        .value(row),
2040                    value
2041                );
2042            }
2043        }
2044        assert!(stream.next().await.is_none());
2045    }
2046
2047    /// A stream that only ever yields zero-row batches must terminate with no
2048    /// batches instead of failing.
2049    #[tokio::test]
2050    async fn test_stream_all_empty_batches_yields_nothing() {
2051        let schema = Arc::new(Schema::new(vec![
2052            Field::new("host", DataType::Utf8, false),
2053            Field::new("value", DataType::Int64, false),
2054            Field::new(
2055                "time_index",
2056                DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
2057                false,
2058            ),
2059        ]));
2060
2061        let memory_exec: Arc<dyn ExecutionPlan> = Arc::new(DataSourceExec::new(Arc::new(
2062            MemorySourceConfig::try_new(
2063                &[vec![
2064                    RecordBatch::new_empty(schema.clone()),
2065                    RecordBatch::new_empty(schema.clone()),
2066                    RecordBatch::new_empty(schema.clone()),
2067                ]],
2068                schema.clone(),
2069                None,
2070            )
2071            .unwrap(),
2072        )));
2073
2074        let divide_exec = Arc::new(SeriesDivideExec {
2075            tag_columns: vec!["host".to_string()],
2076            time_index_column: "time_index".to_string(),
2077            input: memory_exec,
2078            metric: ExecutionPlanMetricsSet::new(),
2079        });
2080
2081        let mut stream = divide_exec
2082            .execute(0, SessionContext::default().task_ctx())
2083            .unwrap();
2084        assert!(stream.next().await.is_none());
2085
2086        let collected =
2087            datafusion::physical_plan::collect(divide_exec, SessionContext::default().task_ctx())
2088                .await
2089                .unwrap();
2090        assert!(collected.is_empty());
2091    }
2092}