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::any::Any;
16use std::pin::Pin;
17use std::sync::Arc;
18use std::task::{Context, Poll};
19
20use datafusion::arrow::array::{Array, ArrayRef, UInt64Array};
21use datafusion::arrow::datatypes::{DataType, SchemaRef};
22use datafusion::arrow::record_batch::RecordBatch;
23use datafusion::common::{DFSchema, DFSchemaRef};
24use datafusion::error::Result as DataFusionResult;
25use datafusion::execution::context::TaskContext;
26use datafusion::logical_expr::{EmptyRelation, Expr, LogicalPlan, UserDefinedLogicalNodeCore};
27use datafusion::physical_expr::{LexRequirement, OrderingRequirements, PhysicalSortRequirement};
28use datafusion::physical_plan::expressions::Column as ColumnExpr;
29use datafusion::physical_plan::metrics::{
30    BaselineMetrics, Count, ExecutionPlanMetricsSet, MetricBuilder, MetricValue, MetricsSet,
31};
32use datafusion::physical_plan::{
33    DisplayAs, DisplayFormatType, Distribution, ExecutionPlan, PlanProperties, RecordBatchStream,
34    SendableRecordBatchStream,
35};
36use datafusion_expr::col;
37use datatypes::arrow::compute;
38use datatypes::arrow_array::string_array_value_at_index;
39use datatypes::compute::SortOptions;
40use futures::{Stream, StreamExt, ready};
41use greptime_proto::substrait_extension as pb;
42use prost::Message;
43use snafu::ResultExt;
44
45use crate::error::{DeserializeSnafu, Result};
46use crate::extension_plan::{METRIC_NUM_SERIES, resolve_column_name, serialize_column_index};
47use crate::metrics::PROMQL_SERIES_COUNT;
48
49enum TagIdentifier<'a> {
50    /// A group of raw string tag columns.
51    Raw(Vec<RawTagColumn<'a>>),
52    /// A single UInt64 identifier (tsid).
53    Id(&'a UInt64Array),
54}
55
56impl<'a> TagIdentifier<'a> {
57    fn try_new(batch: &'a RecordBatch, tag_indices: &[usize]) -> DataFusionResult<Self> {
58        match tag_indices {
59            [] => Ok(Self::Raw(Vec::new())),
60            [index] => {
61                let array = batch.column(*index);
62                if array.data_type() == &DataType::UInt64 {
63                    let array = array
64                        .as_any()
65                        .downcast_ref::<UInt64Array>()
66                        .ok_or_else(|| {
67                            datafusion::error::DataFusionError::Internal(
68                                "Failed to downcast tag column to UInt64Array".to_string(),
69                            )
70                        })?;
71                    Ok(Self::Id(array))
72                } else {
73                    Ok(Self::Raw(vec![RawTagColumn::try_new(array)?]))
74                }
75            }
76            indices => Ok(Self::Raw(
77                indices
78                    .iter()
79                    .map(|index| RawTagColumn::try_new(batch.column(*index)))
80                    .collect::<DataFusionResult<Vec<_>>>()?,
81            )),
82        }
83    }
84
85    fn equal_at(&self, left_row: usize, other: &Self, right_row: usize) -> DataFusionResult<bool> {
86        match (self, other) {
87            (Self::Id(left), Self::Id(right)) => {
88                if left.is_null(left_row) || right.is_null(right_row) {
89                    return Ok(left.is_null(left_row) && right.is_null(right_row));
90                }
91                Ok(left.value(left_row) == right.value(right_row))
92            }
93            (Self::Raw(left), Self::Raw(right)) => {
94                if left.len() != right.len() {
95                    return Err(datafusion::error::DataFusionError::Internal(format!(
96                        "Mismatched tag column count: left={}, right={}",
97                        left.len(),
98                        right.len()
99                    )));
100                }
101
102                for (left_column, right_column) in left.iter().zip(right.iter()) {
103                    if !left_column.equal_at(left_row, right_column, right_row) {
104                        return Ok(false);
105                    }
106                }
107                Ok(true)
108            }
109            _ => Err(datafusion::error::DataFusionError::Internal(format!(
110                "Mismatched tag identifier types: left={:?}, right={:?}",
111                self.data_type(),
112                other.data_type()
113            ))),
114        }
115    }
116
117    fn data_type(&self) -> &'static str {
118        match self {
119            Self::Raw(_) => "Raw",
120            Self::Id(_) => "Id",
121        }
122    }
123}
124
125struct RawTagColumn<'a>(&'a ArrayRef);
126
127impl<'a> RawTagColumn<'a> {
128    fn try_new(array: &'a ArrayRef) -> DataFusionResult<Self> {
129        match array.data_type() {
130            DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View => Ok(Self(array)),
131            DataType::Dictionary(key, value) if key.is_integer() && value.is_string() => {
132                Ok(Self(array))
133            }
134            other => Err(datafusion::error::DataFusionError::Internal(format!(
135                "Unsupported tag column type: {other:?}"
136            ))),
137        }
138    }
139
140    fn equal_at(&self, left_row: usize, other: &Self, right_row: usize) -> bool {
141        string_array_value_at_index(self.0, left_row)
142            == string_array_value_at_index(other.0, right_row)
143    }
144}
145
146#[derive(Debug, PartialEq, Eq, Hash, PartialOrd)]
147pub struct SeriesDivide {
148    tag_columns: Vec<String>,
149    /// `SeriesDivide` requires `time_index` column's name to generate ordering requirement
150    /// for input data. But this plan itself doesn't depend on the ordering of time index
151    /// column. This is for follow on plans like `RangeManipulate`. Because requiring ordering
152    /// here can avoid unnecessary sort in follow on plans.
153    time_index_column: String,
154    input: LogicalPlan,
155    unfix: Option<UnfixIndices>,
156}
157
158#[derive(Debug, PartialEq, Eq, Hash, PartialOrd)]
159struct UnfixIndices {
160    pub tag_column_indices: Vec<u64>,
161    pub time_index_column_idx: u64,
162}
163
164impl UserDefinedLogicalNodeCore for SeriesDivide {
165    fn name(&self) -> &str {
166        Self::name()
167    }
168
169    fn inputs(&self) -> Vec<&LogicalPlan> {
170        vec![&self.input]
171    }
172
173    fn schema(&self) -> &DFSchemaRef {
174        self.input.schema()
175    }
176
177    fn expressions(&self) -> Vec<Expr> {
178        if self.unfix.is_some() {
179            return vec![];
180        }
181
182        self.tag_columns
183            .iter()
184            .map(col)
185            .chain(std::iter::once(col(&self.time_index_column)))
186            .collect()
187    }
188
189    fn necessary_children_exprs(&self, output_columns: &[usize]) -> Option<Vec<Vec<usize>>> {
190        if self.unfix.is_some() {
191            return None;
192        }
193
194        let input_schema = self.input.schema();
195        if output_columns.is_empty() {
196            let indices = (0..input_schema.fields().len()).collect::<Vec<_>>();
197            return Some(vec![indices]);
198        }
199
200        let mut required = Vec::with_capacity(output_columns.len() + 1 + self.tag_columns.len());
201        required.extend_from_slice(output_columns);
202        for tag in &self.tag_columns {
203            required.push(input_schema.index_of_column_by_name(None, tag)?);
204        }
205        required.push(input_schema.index_of_column_by_name(None, &self.time_index_column)?);
206
207        required.sort_unstable();
208        required.dedup();
209        Some(vec![required])
210    }
211
212    fn fmt_for_explain(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
213        write!(f, "PromSeriesDivide: tags={:?}", self.tag_columns)
214    }
215
216    fn with_exprs_and_inputs(
217        &self,
218        _exprs: Vec<Expr>,
219        inputs: Vec<LogicalPlan>,
220    ) -> DataFusionResult<Self> {
221        if inputs.is_empty() {
222            return Err(datafusion::error::DataFusionError::Internal(
223                "SeriesDivide must have at least one input".to_string(),
224            ));
225        }
226
227        let input: LogicalPlan = inputs[0].clone();
228        let input_schema = input.schema();
229
230        if let Some(unfix) = &self.unfix {
231            // transform indices to names
232            let tag_columns = unfix
233                .tag_column_indices
234                .iter()
235                .map(|idx| resolve_column_name(*idx, input_schema, "SeriesDivide", "tag"))
236                .collect::<DataFusionResult<Vec<String>>>()?;
237
238            let time_index_column = resolve_column_name(
239                unfix.time_index_column_idx,
240                input_schema,
241                "SeriesDivide",
242                "time index",
243            )?;
244
245            Ok(Self {
246                tag_columns,
247                time_index_column,
248                input,
249                unfix: None,
250            })
251        } else {
252            Ok(Self {
253                tag_columns: self.tag_columns.clone(),
254                time_index_column: self.time_index_column.clone(),
255                input,
256                unfix: None,
257            })
258        }
259    }
260}
261
262impl SeriesDivide {
263    pub fn new(tag_columns: Vec<String>, time_index_column: String, input: LogicalPlan) -> Self {
264        Self {
265            tag_columns,
266            time_index_column,
267            input,
268            unfix: None,
269        }
270    }
271
272    pub const fn name() -> &'static str {
273        "SeriesDivide"
274    }
275
276    pub fn to_execution_plan(&self, exec_input: Arc<dyn ExecutionPlan>) -> Arc<dyn ExecutionPlan> {
277        Arc::new(SeriesDivideExec {
278            tag_columns: self.tag_columns.clone(),
279            time_index_column: self.time_index_column.clone(),
280            input: exec_input,
281            metric: ExecutionPlanMetricsSet::new(),
282        })
283    }
284
285    pub fn tags(&self) -> &[String] {
286        &self.tag_columns
287    }
288
289    pub fn serialize(&self) -> Vec<u8> {
290        let tag_column_indices = self
291            .tag_columns
292            .iter()
293            .map(|name| serialize_column_index(self.input.schema(), name))
294            .collect::<Vec<u64>>();
295
296        let time_index_column_idx =
297            serialize_column_index(self.input.schema(), &self.time_index_column);
298
299        pb::SeriesDivide {
300            tag_column_indices,
301            time_index_column_idx,
302            ..Default::default()
303        }
304        .encode_to_vec()
305    }
306
307    pub fn deserialize(bytes: &[u8]) -> Result<Self> {
308        let pb_series_divide = pb::SeriesDivide::decode(bytes).context(DeserializeSnafu)?;
309        let placeholder_plan = LogicalPlan::EmptyRelation(EmptyRelation {
310            produce_one_row: false,
311            schema: Arc::new(DFSchema::empty()),
312        });
313
314        let unfix = UnfixIndices {
315            tag_column_indices: pb_series_divide.tag_column_indices.clone(),
316            time_index_column_idx: pb_series_divide.time_index_column_idx,
317        };
318
319        Ok(Self {
320            tag_columns: Vec::new(),
321            time_index_column: String::new(),
322            input: placeholder_plan,
323            unfix: Some(unfix),
324        })
325    }
326}
327
328#[derive(Debug)]
329pub struct SeriesDivideExec {
330    tag_columns: Vec<String>,
331    time_index_column: String,
332    input: Arc<dyn ExecutionPlan>,
333    metric: ExecutionPlanMetricsSet,
334}
335
336impl ExecutionPlan for SeriesDivideExec {
337    fn as_any(&self) -> &dyn Any {
338        self
339    }
340
341    fn schema(&self) -> SchemaRef {
342        self.input.schema()
343    }
344
345    fn properties(&self) -> &Arc<PlanProperties> {
346        self.input.properties()
347    }
348
349    fn required_input_distribution(&self) -> Vec<Distribution> {
350        if self.tag_columns.is_empty() {
351            return vec![Distribution::SinglePartition];
352        }
353        let schema = self.input.schema();
354        vec![Distribution::HashPartitioned(
355            self.tag_columns
356                .iter()
357                // Safety: the tag column names is verified in the planning phase
358                .map(|tag| Arc::new(ColumnExpr::new_with_schema(tag, &schema).unwrap()) as _)
359                .collect(),
360        )]
361    }
362
363    fn required_input_ordering(&self) -> Vec<Option<OrderingRequirements>> {
364        let input_schema = self.input.schema();
365        let mut exprs: Vec<PhysicalSortRequirement> = self
366            .tag_columns
367            .iter()
368            .map(|tag| PhysicalSortRequirement {
369                // Safety: the tag column names is verified in the planning phase
370                expr: Arc::new(ColumnExpr::new_with_schema(tag, &input_schema).unwrap()),
371                options: Some(SortOptions {
372                    descending: false,
373                    nulls_first: true,
374                }),
375            })
376            .collect();
377
378        exprs.push(PhysicalSortRequirement {
379            expr: Arc::new(
380                ColumnExpr::new_with_schema(&self.time_index_column, &input_schema).unwrap(),
381            ),
382            options: Some(SortOptions {
383                descending: false,
384                nulls_first: true,
385            }),
386        });
387
388        // Safety: `exprs` is not empty
389        let requirement = LexRequirement::new(exprs).unwrap();
390
391        vec![Some(OrderingRequirements::Hard(vec![requirement]))]
392    }
393
394    fn maintains_input_order(&self) -> Vec<bool> {
395        vec![true; self.children().len()]
396    }
397
398    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
399        vec![&self.input]
400    }
401
402    fn with_new_children(
403        self: Arc<Self>,
404        children: Vec<Arc<dyn ExecutionPlan>>,
405    ) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
406        assert!(!children.is_empty());
407        Ok(Arc::new(Self {
408            tag_columns: self.tag_columns.clone(),
409            time_index_column: self.time_index_column.clone(),
410            input: children[0].clone(),
411            metric: self.metric.clone(),
412        }))
413    }
414
415    fn execute(
416        &self,
417        partition: usize,
418        context: Arc<TaskContext>,
419    ) -> DataFusionResult<SendableRecordBatchStream> {
420        let baseline_metric = BaselineMetrics::new(&self.metric, partition);
421        let metrics_builder = MetricBuilder::new(&self.metric);
422        let num_series = Count::new();
423        metrics_builder
424            .with_partition(partition)
425            .build(MetricValue::Count {
426                name: METRIC_NUM_SERIES.into(),
427                count: num_series.clone(),
428            });
429
430        let input = self.input.execute(partition, context)?;
431        let schema = input.schema();
432        let tag_indices = self
433            .tag_columns
434            .iter()
435            .map(|tag| {
436                schema
437                    .column_with_name(tag)
438                    .unwrap_or_else(|| panic!("tag column not found {tag}"))
439                    .0
440            })
441            .collect();
442        Ok(Box::pin(SeriesDivideStream {
443            tag_indices,
444            buffer: vec![],
445            schema,
446            input,
447            metric: baseline_metric,
448            num_series,
449            inspect_start: 0,
450        }))
451    }
452
453    fn metrics(&self) -> Option<MetricsSet> {
454        Some(self.metric.clone_inner())
455    }
456
457    fn name(&self) -> &str {
458        "SeriesDivideExec"
459    }
460}
461
462impl DisplayAs for SeriesDivideExec {
463    fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
464        match t {
465            DisplayFormatType::Default
466            | DisplayFormatType::Verbose
467            | DisplayFormatType::TreeRender => {
468                write!(f, "PromSeriesDivideExec: tags={:?}", self.tag_columns)
469            }
470        }
471    }
472}
473
474/// Assume the input stream is ordered on the tag columns.
475pub struct SeriesDivideStream {
476    tag_indices: Vec<usize>,
477    buffer: Vec<RecordBatch>,
478    schema: SchemaRef,
479    input: SendableRecordBatchStream,
480    metric: BaselineMetrics,
481    /// Index of buffered batches to start inspect next time.
482    inspect_start: usize,
483    /// Number of series processed.
484    num_series: Count,
485}
486
487impl RecordBatchStream for SeriesDivideStream {
488    fn schema(&self) -> SchemaRef {
489        self.schema.clone()
490    }
491}
492
493impl Stream for SeriesDivideStream {
494    type Item = DataFusionResult<RecordBatch>;
495
496    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
497        loop {
498            if !self.buffer.is_empty() {
499                let timer = std::time::Instant::now();
500                let cut_at = match self.find_first_diff_row() {
501                    Ok(cut_at) => cut_at,
502                    Err(e) => return Poll::Ready(Some(Err(e))),
503                };
504                if let Some((batch_index, row_index)) = cut_at {
505                    // slice out the first time series and return it.
506                    let half_batch_of_first_series =
507                        self.buffer[batch_index].slice(0, row_index + 1);
508                    let half_batch_of_second_series = self.buffer[batch_index].slice(
509                        row_index + 1,
510                        self.buffer[batch_index].num_rows() - row_index - 1,
511                    );
512                    let result_batches = self
513                        .buffer
514                        .drain(0..batch_index)
515                        .chain([half_batch_of_first_series])
516                        .collect::<Vec<_>>();
517                    if half_batch_of_second_series.num_rows() > 0 {
518                        self.buffer[0] = half_batch_of_second_series;
519                    } else {
520                        self.buffer.remove(0);
521                    }
522                    let result_batch = compute::concat_batches(&self.schema, &result_batches)?;
523
524                    self.inspect_start = 0;
525                    self.num_series.add(1);
526                    self.metric.elapsed_compute().add_elapsed(timer);
527                    return Poll::Ready(Some(Ok(result_batch)));
528                } else {
529                    self.metric.elapsed_compute().add_elapsed(timer);
530                    // continue to fetch next batch as the current buffer only contains one time series.
531                    let next_batch = ready!(self.as_mut().fetch_next_batch(cx)).transpose()?;
532                    let timer = std::time::Instant::now();
533                    if let Some(next_batch) = next_batch {
534                        if next_batch.num_rows() != 0 {
535                            self.buffer.push(next_batch);
536                        }
537                        continue;
538                    } else {
539                        // input stream is ended
540                        let result = compute::concat_batches(&self.schema, &self.buffer)?;
541                        self.buffer.clear();
542                        self.inspect_start = 0;
543                        self.num_series.add(1);
544                        self.metric.elapsed_compute().add_elapsed(timer);
545                        return Poll::Ready(Some(Ok(result)));
546                    }
547                }
548            } else {
549                let batch = match ready!(self.as_mut().fetch_next_batch(cx)) {
550                    Some(Ok(batch)) => batch,
551                    None => {
552                        PROMQL_SERIES_COUNT.observe(self.num_series.value() as f64);
553                        return Poll::Ready(None);
554                    }
555                    error => return Poll::Ready(error),
556                };
557                self.buffer.push(batch);
558                continue;
559            }
560        }
561    }
562}
563
564impl SeriesDivideStream {
565    fn fetch_next_batch(
566        mut self: Pin<&mut Self>,
567        cx: &mut Context<'_>,
568    ) -> Poll<Option<DataFusionResult<RecordBatch>>> {
569        let poll = self.input.poll_next_unpin(cx);
570        self.metric.record_poll(poll)
571    }
572
573    /// Return the position to cut buffer.
574    /// None implies the current buffer only contains one time series.
575    fn find_first_diff_row(&mut self) -> DataFusionResult<Option<(usize, usize)>> {
576        // fast path: no tag columns means all data belongs to the same series.
577        if self.tag_indices.is_empty() {
578            return Ok(None);
579        }
580
581        let mut resumed_batch_index = self.inspect_start;
582
583        for batch in &self.buffer[resumed_batch_index..] {
584            let num_rows = batch.num_rows();
585            let tags = TagIdentifier::try_new(batch, &self.tag_indices)?;
586
587            // check if the first row is the same with last batch's last row
588            if resumed_batch_index > self.inspect_start.saturating_sub(1) {
589                let last_batch = &self.buffer[resumed_batch_index - 1];
590                let last_row = last_batch.num_rows() - 1;
591                let last_tags = TagIdentifier::try_new(last_batch, &self.tag_indices)?;
592                if !tags.equal_at(0, &last_tags, last_row)? {
593                    return Ok(Some((resumed_batch_index - 1, last_row)));
594                }
595            }
596
597            // quick check if all rows are the same by comparing the first and last row in this batch
598            if tags.equal_at(0, &tags, num_rows - 1)? {
599                resumed_batch_index += 1;
600                continue;
601            }
602
603            let mut same_until = 0;
604            while same_until < num_rows - 1 {
605                if !tags.equal_at(same_until, &tags, same_until + 1)? {
606                    break;
607                }
608                same_until += 1;
609            }
610
611            if same_until + 1 >= num_rows {
612                // all rows are the same, inspect next batch
613                resumed_batch_index += 1;
614            } else {
615                return Ok(Some((resumed_batch_index, same_until)));
616            }
617        }
618
619        self.inspect_start = resumed_batch_index;
620        Ok(None)
621    }
622}
623
624#[cfg(test)]
625mod test {
626    use datafusion::arrow::array::{
627        DictionaryArray, Int32Array, LargeStringArray, StringArray, StringViewArray, UInt32Array,
628    };
629    use datafusion::arrow::datatypes::{DataType, Field, Int32Type, Schema, UInt32Type};
630    use datafusion::common::ToDFSchema;
631    use datafusion::datasource::memory::MemorySourceConfig;
632    use datafusion::datasource::source::DataSourceExec;
633    use datafusion::logical_expr::{EmptyRelation, LogicalPlan};
634    use datafusion::prelude::SessionContext;
635
636    use super::*;
637
638    #[test]
639    fn test_dictionary_tag_child_null_comparison() {
640        let dictionary: ArrayRef = Arc::new(DictionaryArray::<UInt32Type>::new(
641            UInt32Array::from(vec![Some(0), None, Some(1)]),
642            Arc::new(StringArray::from(vec![None, Some("")])),
643        ));
644        let tags = RawTagColumn::try_new(&dictionary).unwrap();
645
646        assert!(tags.equal_at(0, &tags, 1));
647        assert!(!tags.equal_at(0, &tags, 2));
648        assert!(!tags.equal_at(1, &tags, 2));
649    }
650
651    #[test]
652    fn test_dictionary_tag_with_non_uint32_keys() {
653        let dictionary: ArrayRef = Arc::new(DictionaryArray::<Int32Type>::new(
654            Int32Array::from(vec![0, 1]),
655            Arc::new(StringArray::from(vec!["host-a", "host-b"])),
656        ));
657
658        let tags = RawTagColumn::try_new(&dictionary).unwrap();
659
660        assert!(tags.equal_at(0, &tags, 0));
661        assert!(!tags.equal_at(0, &tags, 1));
662    }
663
664    fn prepare_test_data() -> DataSourceExec {
665        let schema = Arc::new(Schema::new(vec![
666            Field::new("host", DataType::Utf8, true),
667            Field::new("path", DataType::Utf8, true),
668            Field::new(
669                "time_index",
670                DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
671                false,
672            ),
673        ]));
674
675        let path_column_1 = Arc::new(StringArray::from(vec![
676            "foo", "foo", "foo", "bar", "bar", "bar", "bar", "bar", "bar", "bla", "bla", "bla",
677        ])) as _;
678        let host_column_1 = Arc::new(StringArray::from(vec![
679            "000", "000", "001", "002", "002", "002", "002", "002", "003", "005", "005", "005",
680        ])) as _;
681        let time_index_column_1 = Arc::new(
682            datafusion::arrow::array::TimestampMillisecondArray::from(vec![
683                1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000, 11000, 12000,
684            ]),
685        ) as _;
686
687        let path_column_2 = Arc::new(StringArray::from(vec!["bla", "bla", "bla"])) as _;
688        let host_column_2 = Arc::new(StringArray::from(vec!["005", "005", "005"])) as _;
689        let time_index_column_2 = Arc::new(
690            datafusion::arrow::array::TimestampMillisecondArray::from(vec![13000, 14000, 15000]),
691        ) as _;
692
693        let path_column_3 = Arc::new(StringArray::from(vec![
694            "bla", "🥺", "🥺", "🥺", "🥺", "🥺", "🫠", "🫠",
695        ])) as _;
696        let host_column_3 = Arc::new(StringArray::from(vec![
697            "005", "001", "001", "001", "001", "001", "001", "001",
698        ])) as _;
699        let time_index_column_3 =
700            Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
701                vec![16000, 17000, 18000, 19000, 20000, 21000, 22000, 23000],
702            )) as _;
703
704        let data_1 = RecordBatch::try_new(
705            schema.clone(),
706            vec![path_column_1, host_column_1, time_index_column_1],
707        )
708        .unwrap();
709        let data_2 = RecordBatch::try_new(
710            schema.clone(),
711            vec![path_column_2, host_column_2, time_index_column_2],
712        )
713        .unwrap();
714        let data_3 = RecordBatch::try_new(
715            schema.clone(),
716            vec![path_column_3, host_column_3, time_index_column_3],
717        )
718        .unwrap();
719
720        DataSourceExec::new(Arc::new(
721            MemorySourceConfig::try_new(&[vec![data_1, data_2, data_3]], schema, None).unwrap(),
722        ))
723    }
724
725    #[test]
726    fn pruning_should_keep_tags_and_time_index_columns_for_exec() {
727        let df_schema = prepare_test_data().schema().to_dfschema_ref().unwrap();
728        let input = LogicalPlan::EmptyRelation(EmptyRelation {
729            produce_one_row: false,
730            schema: df_schema,
731        });
732        let plan = SeriesDivide::new(
733            vec!["host".to_string(), "path".to_string()],
734            "time_index".to_string(),
735            input,
736        );
737
738        // Simulate a parent projection requesting only the `host` column.
739        let output_columns = [0usize];
740        let required = plan.necessary_children_exprs(&output_columns).unwrap();
741        let required = &required[0];
742        assert_eq!(required.as_slice(), &[0, 1, 2]);
743    }
744
745    #[tokio::test]
746    async fn overall_data() {
747        let memory_exec = Arc::new(prepare_test_data());
748        let divide_exec = Arc::new(SeriesDivideExec {
749            tag_columns: vec!["host".to_string(), "path".to_string()],
750            time_index_column: "time_index".to_string(),
751            input: memory_exec,
752            metric: ExecutionPlanMetricsSet::new(),
753        });
754        let session_context = SessionContext::default();
755        let result = datafusion::physical_plan::collect(divide_exec, session_context.task_ctx())
756            .await
757            .unwrap();
758        let result_literal = datatypes::arrow::util::pretty::pretty_format_batches(&result)
759            .unwrap()
760            .to_string();
761
762        let expected = String::from(
763            "+------+------+---------------------+\
764            \n| host | path | time_index          |\
765            \n+------+------+---------------------+\
766            \n| foo  | 000  | 1970-01-01T00:00:01 |\
767            \n| foo  | 000  | 1970-01-01T00:00:02 |\
768            \n| foo  | 001  | 1970-01-01T00:00:03 |\
769            \n| bar  | 002  | 1970-01-01T00:00:04 |\
770            \n| bar  | 002  | 1970-01-01T00:00:05 |\
771            \n| bar  | 002  | 1970-01-01T00:00:06 |\
772            \n| bar  | 002  | 1970-01-01T00:00:07 |\
773            \n| bar  | 002  | 1970-01-01T00:00:08 |\
774            \n| bar  | 003  | 1970-01-01T00:00:09 |\
775            \n| bla  | 005  | 1970-01-01T00:00:10 |\
776            \n| bla  | 005  | 1970-01-01T00:00:11 |\
777            \n| bla  | 005  | 1970-01-01T00:00:12 |\
778            \n| bla  | 005  | 1970-01-01T00:00:13 |\
779            \n| bla  | 005  | 1970-01-01T00:00:14 |\
780            \n| bla  | 005  | 1970-01-01T00:00:15 |\
781            \n| bla  | 005  | 1970-01-01T00:00:16 |\
782            \n| 🥺   | 001  | 1970-01-01T00:00:17 |\
783            \n| 🥺   | 001  | 1970-01-01T00:00:18 |\
784            \n| 🥺   | 001  | 1970-01-01T00:00:19 |\
785            \n| 🥺   | 001  | 1970-01-01T00:00:20 |\
786            \n| 🥺   | 001  | 1970-01-01T00:00:21 |\
787            \n| 🫠   | 001  | 1970-01-01T00:00:22 |\
788            \n| 🫠   | 001  | 1970-01-01T00:00:23 |\
789            \n+------+------+---------------------+",
790        );
791        assert_eq!(result_literal, expected);
792    }
793
794    #[tokio::test]
795    async fn per_batch_data() {
796        let memory_exec = Arc::new(prepare_test_data());
797        let divide_exec = Arc::new(SeriesDivideExec {
798            tag_columns: vec!["host".to_string(), "path".to_string()],
799            time_index_column: "time_index".to_string(),
800            input: memory_exec,
801            metric: ExecutionPlanMetricsSet::new(),
802        });
803        let mut divide_stream = divide_exec
804            .execute(0, SessionContext::default().task_ctx())
805            .unwrap();
806
807        let mut expectations = vec![
808            String::from(
809                "+------+------+---------------------+\
810                \n| host | path | time_index          |\
811                \n+------+------+---------------------+\
812                \n| foo  | 000  | 1970-01-01T00:00:01 |\
813                \n| foo  | 000  | 1970-01-01T00:00:02 |\
814                \n+------+------+---------------------+",
815            ),
816            String::from(
817                "+------+------+---------------------+\
818                \n| host | path | time_index          |\
819                \n+------+------+---------------------+\
820                \n| foo  | 001  | 1970-01-01T00:00:03 |\
821                \n+------+------+---------------------+",
822            ),
823            String::from(
824                "+------+------+---------------------+\
825                \n| host | path | time_index          |\
826                \n+------+------+---------------------+\
827                \n| bar  | 002  | 1970-01-01T00:00:04 |\
828                \n| bar  | 002  | 1970-01-01T00:00:05 |\
829                \n| bar  | 002  | 1970-01-01T00:00:06 |\
830                \n| bar  | 002  | 1970-01-01T00:00:07 |\
831                \n| bar  | 002  | 1970-01-01T00:00:08 |\
832                \n+------+------+---------------------+",
833            ),
834            String::from(
835                "+------+------+---------------------+\
836                \n| host | path | time_index          |\
837                \n+------+------+---------------------+\
838                \n| bar  | 003  | 1970-01-01T00:00:09 |\
839                \n+------+------+---------------------+",
840            ),
841            String::from(
842                "+------+------+---------------------+\
843                \n| host | path | time_index          |\
844                \n+------+------+---------------------+\
845                \n| bla  | 005  | 1970-01-01T00:00:10 |\
846                \n| bla  | 005  | 1970-01-01T00:00:11 |\
847                \n| bla  | 005  | 1970-01-01T00:00:12 |\
848                \n| bla  | 005  | 1970-01-01T00:00:13 |\
849                \n| bla  | 005  | 1970-01-01T00:00:14 |\
850                \n| bla  | 005  | 1970-01-01T00:00:15 |\
851                \n| bla  | 005  | 1970-01-01T00:00:16 |\
852                \n+------+------+---------------------+",
853            ),
854            String::from(
855                "+------+------+---------------------+\
856                \n| host | path | time_index          |\
857                \n+------+------+---------------------+\
858                \n| 🥺   | 001  | 1970-01-01T00:00:17 |\
859                \n| 🥺   | 001  | 1970-01-01T00:00:18 |\
860                \n| 🥺   | 001  | 1970-01-01T00:00:19 |\
861                \n| 🥺   | 001  | 1970-01-01T00:00:20 |\
862                \n| 🥺   | 001  | 1970-01-01T00:00:21 |\
863                \n+------+------+---------------------+",
864            ),
865            String::from(
866                "+------+------+---------------------+\
867                \n| host | path | time_index          |\
868                \n+------+------+---------------------+\
869                \n| 🫠   | 001  | 1970-01-01T00:00:22 |\
870                \n| 🫠   | 001  | 1970-01-01T00:00:23 |\
871                \n+------+------+---------------------+",
872            ),
873        ];
874        expectations.reverse();
875
876        while let Some(batch) = divide_stream.next().await {
877            let formatted =
878                datatypes::arrow::util::pretty::pretty_format_batches(&[batch.unwrap()])
879                    .unwrap()
880                    .to_string();
881            let expected = expectations.pop().unwrap();
882            assert_eq!(formatted, expected);
883        }
884    }
885
886    #[tokio::test]
887    async fn test_all_batches_same_combination() {
888        // Create a schema with host and path columns, same as prepare_test_data
889        let schema = Arc::new(Schema::new(vec![
890            Field::new("host", DataType::Utf8, true),
891            Field::new("path", DataType::Utf8, true),
892            Field::new(
893                "time_index",
894                DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
895                false,
896            ),
897        ]));
898
899        // Create batches with three different combinations
900        // Each batch contains only one combination
901        // Batches with the same combination are adjacent
902
903        // First combination: "server1", "/var/log"
904        let batch1 = RecordBatch::try_new(
905            schema.clone(),
906            vec![
907                Arc::new(StringArray::from(vec!["server1", "server1", "server1"])) as _,
908                Arc::new(StringArray::from(vec!["/var/log", "/var/log", "/var/log"])) as _,
909                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
910                    vec![1000, 2000, 3000],
911                )) as _,
912            ],
913        )
914        .unwrap();
915
916        let batch2 = RecordBatch::try_new(
917            schema.clone(),
918            vec![
919                Arc::new(StringArray::from(vec!["server1", "server1"])) as _,
920                Arc::new(StringArray::from(vec!["/var/log", "/var/log"])) as _,
921                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
922                    vec![4000, 5000],
923                )) as _,
924            ],
925        )
926        .unwrap();
927
928        // Second combination: "server2", "/var/data"
929        let batch3 = RecordBatch::try_new(
930            schema.clone(),
931            vec![
932                Arc::new(StringArray::from(vec!["server2", "server2", "server2"])) as _,
933                Arc::new(StringArray::from(vec![
934                    "/var/data",
935                    "/var/data",
936                    "/var/data",
937                ])) as _,
938                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
939                    vec![6000, 7000, 8000],
940                )) as _,
941            ],
942        )
943        .unwrap();
944
945        let batch4 = RecordBatch::try_new(
946            schema.clone(),
947            vec![
948                Arc::new(StringArray::from(vec!["server2"])) as _,
949                Arc::new(StringArray::from(vec!["/var/data"])) as _,
950                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
951                    vec![9000],
952                )) as _,
953            ],
954        )
955        .unwrap();
956
957        // Third combination: "server3", "/opt/logs"
958        let batch5 = RecordBatch::try_new(
959            schema.clone(),
960            vec![
961                Arc::new(StringArray::from(vec!["server3", "server3"])) as _,
962                Arc::new(StringArray::from(vec!["/opt/logs", "/opt/logs"])) as _,
963                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
964                    vec![10000, 11000],
965                )) as _,
966            ],
967        )
968        .unwrap();
969
970        let batch6 = RecordBatch::try_new(
971            schema.clone(),
972            vec![
973                Arc::new(StringArray::from(vec!["server3", "server3", "server3"])) as _,
974                Arc::new(StringArray::from(vec![
975                    "/opt/logs",
976                    "/opt/logs",
977                    "/opt/logs",
978                ])) as _,
979                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
980                    vec![12000, 13000, 14000],
981                )) as _,
982            ],
983        )
984        .unwrap();
985
986        // Create MemoryExec with these batches, keeping same combinations adjacent
987        let memory_exec = DataSourceExec::from_data_source(
988            MemorySourceConfig::try_new(
989                &[vec![batch1, batch2, batch3, batch4, batch5, batch6]],
990                schema.clone(),
991                None,
992            )
993            .unwrap(),
994        );
995
996        // Create SeriesDivideExec
997        let divide_exec = Arc::new(SeriesDivideExec {
998            tag_columns: vec!["host".to_string(), "path".to_string()],
999            time_index_column: "time_index".to_string(),
1000            input: memory_exec,
1001            metric: ExecutionPlanMetricsSet::new(),
1002        });
1003
1004        // Execute the division
1005        let session_context = SessionContext::default();
1006        let result =
1007            datafusion::physical_plan::collect(divide_exec.clone(), session_context.task_ctx())
1008                .await
1009                .unwrap();
1010
1011        // Verify that we got 3 batches (one for each combination)
1012        assert_eq!(result.len(), 3);
1013
1014        // First batch should have 5 rows (3 + 2 from the "server1" combination)
1015        assert_eq!(result[0].num_rows(), 5);
1016
1017        // Second batch should have 4 rows (3 + 1 from the "server2" combination)
1018        assert_eq!(result[1].num_rows(), 4);
1019
1020        // Third batch should have 5 rows (2 + 3 from the "server3" combination)
1021        assert_eq!(result[2].num_rows(), 5);
1022
1023        // Verify values in first batch (server1, /var/log)
1024        let host_array1 = result[0]
1025            .column(0)
1026            .as_any()
1027            .downcast_ref::<StringArray>()
1028            .unwrap();
1029        let path_array1 = result[0]
1030            .column(1)
1031            .as_any()
1032            .downcast_ref::<StringArray>()
1033            .unwrap();
1034        let time_index_array1 = result[0]
1035            .column(2)
1036            .as_any()
1037            .downcast_ref::<datafusion::arrow::array::TimestampMillisecondArray>()
1038            .unwrap();
1039
1040        for i in 0..5 {
1041            assert_eq!(host_array1.value(i), "server1");
1042            assert_eq!(path_array1.value(i), "/var/log");
1043            assert_eq!(time_index_array1.value(i), 1000 + (i as i64) * 1000);
1044        }
1045
1046        // Verify values in second batch (server2, /var/data)
1047        let host_array2 = result[1]
1048            .column(0)
1049            .as_any()
1050            .downcast_ref::<StringArray>()
1051            .unwrap();
1052        let path_array2 = result[1]
1053            .column(1)
1054            .as_any()
1055            .downcast_ref::<StringArray>()
1056            .unwrap();
1057        let time_index_array2 = result[1]
1058            .column(2)
1059            .as_any()
1060            .downcast_ref::<datafusion::arrow::array::TimestampMillisecondArray>()
1061            .unwrap();
1062
1063        for i in 0..4 {
1064            assert_eq!(host_array2.value(i), "server2");
1065            assert_eq!(path_array2.value(i), "/var/data");
1066            assert_eq!(time_index_array2.value(i), 6000 + (i as i64) * 1000);
1067        }
1068
1069        // Verify values in third batch (server3, /opt/logs)
1070        let host_array3 = result[2]
1071            .column(0)
1072            .as_any()
1073            .downcast_ref::<StringArray>()
1074            .unwrap();
1075        let path_array3 = result[2]
1076            .column(1)
1077            .as_any()
1078            .downcast_ref::<StringArray>()
1079            .unwrap();
1080        let time_index_array3 = result[2]
1081            .column(2)
1082            .as_any()
1083            .downcast_ref::<datafusion::arrow::array::TimestampMillisecondArray>()
1084            .unwrap();
1085
1086        for i in 0..5 {
1087            assert_eq!(host_array3.value(i), "server3");
1088            assert_eq!(path_array3.value(i), "/opt/logs");
1089            assert_eq!(time_index_array3.value(i), 10000 + (i as i64) * 1000);
1090        }
1091
1092        // Also verify streaming behavior
1093        let mut divide_stream = divide_exec
1094            .execute(0, SessionContext::default().task_ctx())
1095            .unwrap();
1096
1097        // Should produce three batches, one for each combination
1098        let batch1 = divide_stream.next().await.unwrap().unwrap();
1099        assert_eq!(batch1.num_rows(), 5); // server1 combination
1100
1101        let batch2 = divide_stream.next().await.unwrap().unwrap();
1102        assert_eq!(batch2.num_rows(), 4); // server2 combination
1103
1104        let batch3 = divide_stream.next().await.unwrap().unwrap();
1105        assert_eq!(batch3.num_rows(), 5); // server3 combination
1106
1107        // No more batches should be produced
1108        assert!(divide_stream.next().await.is_none());
1109    }
1110
1111    #[tokio::test]
1112    async fn test_string_tag_column_types() {
1113        let schema = Arc::new(Schema::new(vec![
1114            Field::new("tag_large", DataType::LargeUtf8, false),
1115            Field::new("tag_view", DataType::Utf8View, false),
1116            Field::new(
1117                "time_index",
1118                DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1119                false,
1120            ),
1121        ]));
1122
1123        let batch1 = RecordBatch::try_new(
1124            schema.clone(),
1125            vec![
1126                Arc::new(LargeStringArray::from(vec!["a", "a", "a", "a"])),
1127                Arc::new(StringViewArray::from(vec!["x", "x", "y", "y"])),
1128                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1129                    vec![1000, 2000, 1000, 2000],
1130                )),
1131            ],
1132        )
1133        .unwrap();
1134
1135        let batch2 = RecordBatch::try_new(
1136            schema.clone(),
1137            vec![
1138                Arc::new(LargeStringArray::from(vec!["b", "b"])),
1139                Arc::new(StringViewArray::from(vec!["x", "x"])),
1140                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1141                    vec![1000, 2000],
1142                )),
1143            ],
1144        )
1145        .unwrap();
1146
1147        let memory_exec: Arc<dyn ExecutionPlan> = Arc::new(DataSourceExec::new(Arc::new(
1148            MemorySourceConfig::try_new(&[vec![batch1, batch2]], schema.clone(), None).unwrap(),
1149        )));
1150
1151        let divide_exec = Arc::new(SeriesDivideExec {
1152            tag_columns: vec!["tag_large".to_string(), "tag_view".to_string()],
1153            time_index_column: "time_index".to_string(),
1154            input: memory_exec,
1155            metric: ExecutionPlanMetricsSet::new(),
1156        });
1157
1158        let session_context = SessionContext::default();
1159        let result = datafusion::physical_plan::collect(divide_exec, session_context.task_ctx())
1160            .await
1161            .unwrap();
1162
1163        assert_eq!(result.len(), 3);
1164        for ((expected_large, expected_view), batch) in [("a", "x"), ("a", "y"), ("b", "x")]
1165            .into_iter()
1166            .zip(result.iter())
1167        {
1168            assert_eq!(batch.num_rows(), 2);
1169
1170            let tag_large_array = batch
1171                .column(0)
1172                .as_any()
1173                .downcast_ref::<LargeStringArray>()
1174                .unwrap();
1175            let tag_view_array = batch
1176                .column(1)
1177                .as_any()
1178                .downcast_ref::<StringViewArray>()
1179                .unwrap();
1180
1181            for row in 0..batch.num_rows() {
1182                assert_eq!(tag_large_array.value(row), expected_large);
1183                assert_eq!(tag_view_array.value(row), expected_view);
1184            }
1185        }
1186    }
1187
1188    #[tokio::test]
1189    async fn test_u64_tag_column() {
1190        let schema = Arc::new(Schema::new(vec![
1191            Field::new("tsid", DataType::UInt64, false),
1192            Field::new(
1193                "time_index",
1194                DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1195                false,
1196            ),
1197        ]));
1198
1199        let batch1 = RecordBatch::try_new(
1200            schema.clone(),
1201            vec![
1202                Arc::new(UInt64Array::from(vec![1, 1, 2, 2])),
1203                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1204                    vec![1000, 2000, 1000, 2000],
1205                )),
1206            ],
1207        )
1208        .unwrap();
1209
1210        let batch2 = RecordBatch::try_new(
1211            schema.clone(),
1212            vec![
1213                Arc::new(UInt64Array::from(vec![3, 3])),
1214                Arc::new(datafusion::arrow::array::TimestampMillisecondArray::from(
1215                    vec![1000, 2000],
1216                )),
1217            ],
1218        )
1219        .unwrap();
1220
1221        let memory_exec: Arc<dyn ExecutionPlan> = Arc::new(DataSourceExec::new(Arc::new(
1222            MemorySourceConfig::try_new(&[vec![batch1, batch2]], schema.clone(), None).unwrap(),
1223        )));
1224
1225        let divide_exec = Arc::new(SeriesDivideExec {
1226            tag_columns: vec!["tsid".to_string()],
1227            time_index_column: "time_index".to_string(),
1228            input: memory_exec,
1229            metric: ExecutionPlanMetricsSet::new(),
1230        });
1231
1232        let session_context = SessionContext::default();
1233        let result = datafusion::physical_plan::collect(divide_exec, session_context.task_ctx())
1234            .await
1235            .unwrap();
1236
1237        assert_eq!(result.len(), 3);
1238        for (expected_tsid, batch) in [1u64, 2u64, 3u64].into_iter().zip(result.iter()) {
1239            assert_eq!(batch.num_rows(), 2);
1240            let tsid_array = batch
1241                .column(0)
1242                .as_any()
1243                .downcast_ref::<UInt64Array>()
1244                .unwrap();
1245            assert!(tsid_array.iter().all(|v| v == Some(expected_tsid)));
1246        }
1247    }
1248}