Skip to main content

promql/extension_plan/
scalar_calculate.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::collections::HashMap;
17use std::pin::Pin;
18use std::sync::Arc;
19use std::task::{Context, Poll};
20
21use datafusion::common::stats::Precision;
22use datafusion::common::{DFSchema, DFSchemaRef, Result as DataFusionResult, Statistics};
23use datafusion::error::DataFusionError;
24use datafusion::execution::context::TaskContext;
25use datafusion::logical_expr::{EmptyRelation, LogicalPlan, UserDefinedLogicalNodeCore};
26use datafusion::physical_expr::EquivalenceProperties;
27use datafusion::physical_plan::metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet};
28use datafusion::physical_plan::{
29    DisplayAs, DisplayFormatType, Distribution, ExecutionPlan, Partitioning, PlanProperties,
30    RecordBatchStream, SendableRecordBatchStream,
31};
32use datafusion::prelude::Expr;
33use datafusion::sql::TableReference;
34use datafusion_expr::col;
35use datatypes::arrow::array::{Array, ArrayRef, Float64Array, TimestampMillisecondArray};
36use datatypes::arrow::compute::{CastOptions, cast_with_options, concat_batches};
37use datatypes::arrow::datatypes::{DataType, Field, Schema, SchemaRef, TimeUnit};
38use datatypes::arrow::record_batch::RecordBatch;
39use datatypes::arrow_array::string_array_value_at_index;
40use futures::{Stream, StreamExt, ready};
41use greptime_proto::substrait_extension as pb;
42use prost::Message;
43use snafu::ResultExt;
44
45use crate::error::{ColumnNotFoundSnafu, DataFusionPlanningSnafu, DeserializeSnafu, Result};
46use crate::extension_plan::{Millisecond, resolve_column_name, serialize_column_index};
47
48/// `ScalarCalculate` is the custom logical plan to calculate
49/// [`scalar`](https://prometheus.io/docs/prometheus/latest/querying/functions/#scalar)
50/// in PromQL, return NaN when have multiple time series.
51///
52/// Return the time series as scalar value when only have one time series.
53#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54pub struct ScalarCalculate {
55    start: Millisecond,
56    end: Millisecond,
57    interval: Millisecond,
58
59    time_index: String,
60    tag_columns: Vec<String>,
61    field_column: String,
62    input: LogicalPlan,
63    output_schema: DFSchemaRef,
64    unfix: Option<UnfixIndices>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd)]
68struct UnfixIndices {
69    pub time_index_idx: u64,
70    pub tag_column_indices: Vec<u64>,
71    pub field_column_idx: u64,
72}
73
74impl ScalarCalculate {
75    /// create a new `ScalarCalculate` plan
76    #[allow(clippy::too_many_arguments)]
77    pub fn new(
78        start: Millisecond,
79        end: Millisecond,
80        interval: Millisecond,
81        input: LogicalPlan,
82        time_index: &str,
83        tag_columns: &[String],
84        field_column: &str,
85        table_name: Option<&str>,
86    ) -> Result<Self> {
87        let input_schema = input.schema();
88        let Ok(ts_field) = input_schema
89            .field_with_unqualified_name(time_index)
90            .cloned()
91        else {
92            return ColumnNotFoundSnafu { col: time_index }.fail();
93        };
94        let val_field = Field::new(format!("scalar({})", field_column), DataType::Float64, true);
95        let qualifier = table_name.map(TableReference::bare);
96        let schema = DFSchema::new_with_metadata(
97            vec![
98                (qualifier.clone(), ts_field),
99                (qualifier, Arc::new(val_field)),
100            ],
101            input_schema.metadata().clone(),
102        )
103        .context(DataFusionPlanningSnafu)?;
104
105        Ok(Self {
106            start,
107            end,
108            interval,
109            time_index: time_index.to_string(),
110            tag_columns: tag_columns.to_vec(),
111            field_column: field_column.to_string(),
112            input,
113            output_schema: Arc::new(schema),
114            unfix: None,
115        })
116    }
117
118    /// The name of this custom plan
119    pub const fn name() -> &'static str {
120        "ScalarCalculate"
121    }
122
123    /// Create a new execution plan from ScalarCalculate
124    pub fn to_execution_plan(
125        &self,
126        exec_input: Arc<dyn ExecutionPlan>,
127    ) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
128        let fields: Vec<_> = self
129            .output_schema
130            .fields()
131            .iter()
132            .map(|field| Field::new(field.name(), field.data_type().clone(), field.is_nullable()))
133            .collect();
134        let input_schema = exec_input.schema();
135        let ts_index = input_schema
136            .index_of(&self.time_index)
137            .map_err(|e| DataFusionError::ArrowError(Box::new(e), None))?;
138        let val_index = input_schema
139            .index_of(&self.field_column)
140            .map_err(|e| DataFusionError::ArrowError(Box::new(e), None))?;
141        let schema = Arc::new(Schema::new(fields));
142        let properties = exec_input.properties();
143        let properties = Arc::new(PlanProperties::new(
144            EquivalenceProperties::new(schema.clone()),
145            Partitioning::UnknownPartitioning(1),
146            properties.emission_type,
147            properties.boundedness,
148        ));
149        Ok(Arc::new(ScalarCalculateExec {
150            start: self.start,
151            end: self.end,
152            interval: self.interval,
153            schema,
154            input: exec_input,
155            project_index: (ts_index, val_index),
156            tag_columns: self.tag_columns.clone(),
157            metric: ExecutionPlanMetricsSet::new(),
158            properties,
159        }))
160    }
161
162    pub fn serialize(&self) -> Vec<u8> {
163        let time_index_idx = serialize_column_index(self.input.schema(), &self.time_index);
164
165        let tag_column_indices = self
166            .tag_columns
167            .iter()
168            .map(|name| serialize_column_index(self.input.schema(), name))
169            .collect::<Vec<u64>>();
170
171        let field_column_idx = serialize_column_index(self.input.schema(), &self.field_column);
172
173        pb::ScalarCalculate {
174            start: self.start,
175            end: self.end,
176            interval: self.interval,
177            time_index_idx,
178            tag_column_indices,
179            field_column_idx,
180            ..Default::default()
181        }
182        .encode_to_vec()
183    }
184
185    pub fn deserialize(bytes: &[u8]) -> Result<Self> {
186        let pb_scalar_calculate = pb::ScalarCalculate::decode(bytes).context(DeserializeSnafu)?;
187        let placeholder_plan = LogicalPlan::EmptyRelation(EmptyRelation {
188            produce_one_row: false,
189            schema: Arc::new(DFSchema::empty()),
190        });
191
192        let unfix = UnfixIndices {
193            time_index_idx: pb_scalar_calculate.time_index_idx,
194            tag_column_indices: pb_scalar_calculate.tag_column_indices.clone(),
195            field_column_idx: pb_scalar_calculate.field_column_idx,
196        };
197
198        // TODO(Taylor-lagrange): Supports timestamps of different precisions
199        let ts_field = Field::new(
200            "placeholder_time_index",
201            DataType::Timestamp(TimeUnit::Millisecond, None),
202            true,
203        );
204        let val_field = Field::new("placeholder_field", DataType::Float64, true);
205        // TODO(Taylor-lagrange): missing tablename in pb
206        let schema = DFSchema::new_with_metadata(
207            vec![(None, Arc::new(ts_field)), (None, Arc::new(val_field))],
208            HashMap::new(),
209        )
210        .context(DataFusionPlanningSnafu)?;
211
212        Ok(Self {
213            start: pb_scalar_calculate.start,
214            end: pb_scalar_calculate.end,
215            interval: pb_scalar_calculate.interval,
216            time_index: String::new(),
217            tag_columns: Vec::new(),
218            field_column: String::new(),
219            output_schema: Arc::new(schema),
220            input: placeholder_plan,
221            unfix: Some(unfix),
222        })
223    }
224}
225
226impl PartialOrd for ScalarCalculate {
227    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
228        // Compare fields in order excluding output_schema
229        match self.start.partial_cmp(&other.start) {
230            Some(core::cmp::Ordering::Equal) => {}
231            ord => return ord,
232        }
233        match self.end.partial_cmp(&other.end) {
234            Some(core::cmp::Ordering::Equal) => {}
235            ord => return ord,
236        }
237        match self.interval.partial_cmp(&other.interval) {
238            Some(core::cmp::Ordering::Equal) => {}
239            ord => return ord,
240        }
241        match self.time_index.partial_cmp(&other.time_index) {
242            Some(core::cmp::Ordering::Equal) => {}
243            ord => return ord,
244        }
245        match self.tag_columns.partial_cmp(&other.tag_columns) {
246            Some(core::cmp::Ordering::Equal) => {}
247            ord => return ord,
248        }
249        match self.field_column.partial_cmp(&other.field_column) {
250            Some(core::cmp::Ordering::Equal) => {}
251            ord => return ord,
252        }
253        self.input.partial_cmp(&other.input)
254    }
255}
256
257impl UserDefinedLogicalNodeCore for ScalarCalculate {
258    fn name(&self) -> &str {
259        Self::name()
260    }
261
262    fn inputs(&self) -> Vec<&LogicalPlan> {
263        vec![&self.input]
264    }
265
266    fn schema(&self) -> &DFSchemaRef {
267        &self.output_schema
268    }
269
270    fn expressions(&self) -> Vec<Expr> {
271        if self.unfix.is_some() {
272            return vec![];
273        }
274
275        self.tag_columns
276            .iter()
277            .map(col)
278            .chain(std::iter::once(col(&self.time_index)))
279            .chain(std::iter::once(col(&self.field_column)))
280            .collect()
281    }
282
283    fn necessary_children_exprs(&self, _output_columns: &[usize]) -> Option<Vec<Vec<usize>>> {
284        if self.unfix.is_some() {
285            return None;
286        }
287
288        let input_schema = self.input.schema();
289        let time_index_idx = input_schema.index_of_column_by_name(None, &self.time_index)?;
290        let field_column_idx = input_schema.index_of_column_by_name(None, &self.field_column)?;
291
292        let mut required = Vec::with_capacity(2 + self.tag_columns.len());
293        required.extend([time_index_idx, field_column_idx]);
294        for tag in &self.tag_columns {
295            required.push(input_schema.index_of_column_by_name(None, tag)?);
296        }
297
298        required.sort_unstable();
299        required.dedup();
300        Some(vec![required])
301    }
302
303    fn fmt_for_explain(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
304        write!(f, "ScalarCalculate: tags={:?}", self.tag_columns)
305    }
306
307    fn with_exprs_and_inputs(
308        &self,
309        _exprs: Vec<Expr>,
310        inputs: Vec<LogicalPlan>,
311    ) -> DataFusionResult<Self> {
312        let input: LogicalPlan = inputs.into_iter().next().unwrap();
313        let input_schema = input.schema();
314
315        if let Some(unfix) = &self.unfix {
316            // transform indices to names
317            let time_index = resolve_column_name(
318                unfix.time_index_idx,
319                input_schema,
320                "ScalarCalculate",
321                "time index",
322            )?;
323
324            let tag_columns = unfix
325                .tag_column_indices
326                .iter()
327                .map(|idx| resolve_column_name(*idx, input_schema, "ScalarCalculate", "tag"))
328                .collect::<DataFusionResult<Vec<String>>>()?;
329
330            let field_column = resolve_column_name(
331                unfix.field_column_idx,
332                input_schema,
333                "ScalarCalculate",
334                "field",
335            )?;
336
337            // Recreate output schema with actual field names
338            let ts_field = Field::new(
339                &time_index,
340                DataType::Timestamp(TimeUnit::Millisecond, None),
341                true,
342            );
343            let val_field =
344                Field::new(format!("scalar({})", field_column), DataType::Float64, true);
345            let schema = DFSchema::new_with_metadata(
346                vec![(None, Arc::new(ts_field)), (None, Arc::new(val_field))],
347                HashMap::new(),
348            )
349            .context(DataFusionPlanningSnafu)?;
350
351            Ok(ScalarCalculate {
352                start: self.start,
353                end: self.end,
354                interval: self.interval,
355                time_index,
356                tag_columns,
357                field_column,
358                input,
359                output_schema: Arc::new(schema),
360                unfix: None,
361            })
362        } else {
363            Ok(ScalarCalculate {
364                start: self.start,
365                end: self.end,
366                interval: self.interval,
367                time_index: self.time_index.clone(),
368                tag_columns: self.tag_columns.clone(),
369                field_column: self.field_column.clone(),
370                input,
371                output_schema: self.output_schema.clone(),
372                unfix: None,
373            })
374        }
375    }
376}
377
378#[derive(Debug, Clone)]
379struct ScalarCalculateExec {
380    start: Millisecond,
381    end: Millisecond,
382    interval: Millisecond,
383    schema: SchemaRef,
384    project_index: (usize, usize),
385    input: Arc<dyn ExecutionPlan>,
386    tag_columns: Vec<String>,
387    metric: ExecutionPlanMetricsSet,
388    properties: Arc<PlanProperties>,
389}
390
391impl ExecutionPlan for ScalarCalculateExec {
392    fn as_any(&self) -> &dyn Any {
393        self
394    }
395
396    fn schema(&self) -> SchemaRef {
397        self.schema.clone()
398    }
399
400    fn properties(&self) -> &Arc<PlanProperties> {
401        &self.properties
402    }
403
404    fn maintains_input_order(&self) -> Vec<bool> {
405        vec![true; self.children().len()]
406    }
407
408    fn required_input_distribution(&self) -> Vec<Distribution> {
409        vec![Distribution::SinglePartition]
410    }
411
412    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
413        vec![&self.input]
414    }
415
416    fn with_new_children(
417        self: Arc<Self>,
418        children: Vec<Arc<dyn ExecutionPlan>>,
419    ) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
420        Ok(Arc::new(ScalarCalculateExec {
421            start: self.start,
422            end: self.end,
423            interval: self.interval,
424            schema: self.schema.clone(),
425            project_index: self.project_index,
426            tag_columns: self.tag_columns.clone(),
427            input: children[0].clone(),
428            metric: self.metric.clone(),
429            properties: self.properties.clone(),
430        }))
431    }
432
433    fn execute(
434        &self,
435        partition: usize,
436        context: Arc<TaskContext>,
437    ) -> DataFusionResult<SendableRecordBatchStream> {
438        let baseline_metric = BaselineMetrics::new(&self.metric, partition);
439        let input = self.input.execute(partition, context)?;
440        let schema = input.schema();
441        let tag_indices = self
442            .tag_columns
443            .iter()
444            .map(|tag| {
445                schema
446                    .column_with_name(tag)
447                    .unwrap_or_else(|| panic!("tag column not found {tag}"))
448                    .0
449            })
450            .collect();
451
452        Ok(Box::pin(ScalarCalculateStream {
453            start: self.start,
454            end: self.end,
455            interval: self.interval,
456            schema: self.schema.clone(),
457            project_index: self.project_index,
458            metric: baseline_metric,
459            tag_indices,
460            input,
461            have_multi_series: false,
462            done: false,
463            batch: None,
464            tag_value: None,
465        }))
466    }
467
468    fn metrics(&self) -> Option<MetricsSet> {
469        Some(self.metric.clone_inner())
470    }
471
472    fn partition_statistics(&self, partition: Option<usize>) -> DataFusionResult<Statistics> {
473        let input_stats = self.input.partition_statistics(partition)?;
474
475        let estimated_row_num = (self.end - self.start) as f64 / self.interval as f64;
476        let estimated_total_bytes = input_stats
477            .total_byte_size
478            .get_value()
479            .zip(input_stats.num_rows.get_value())
480            .map(|(size, rows)| {
481                Precision::Inexact(((*size as f64 / *rows as f64) * estimated_row_num).floor() as _)
482            })
483            .unwrap_or_default();
484
485        Ok(Statistics {
486            num_rows: Precision::Inexact(estimated_row_num as _),
487            total_byte_size: estimated_total_bytes,
488            // TODO(ruihang): support this column statistics
489            column_statistics: Statistics::unknown_column(&self.schema()),
490        })
491    }
492
493    fn name(&self) -> &str {
494        "ScalarCalculateExec"
495    }
496}
497
498impl DisplayAs for ScalarCalculateExec {
499    fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
500        match t {
501            DisplayFormatType::Default
502            | DisplayFormatType::Verbose
503            | DisplayFormatType::TreeRender => {
504                write!(f, "ScalarCalculateExec: tags={:?}", self.tag_columns)
505            }
506        }
507    }
508}
509
510struct ScalarCalculateStream {
511    start: Millisecond,
512    end: Millisecond,
513    interval: Millisecond,
514    schema: SchemaRef,
515    input: SendableRecordBatchStream,
516    metric: BaselineMetrics,
517    tag_indices: Vec<usize>,
518    /// with format `(ts_index, field_index)`
519    project_index: (usize, usize),
520    have_multi_series: bool,
521    done: bool,
522    batch: Option<RecordBatch>,
523    tag_value: Option<Vec<Option<String>>>,
524}
525
526impl RecordBatchStream for ScalarCalculateStream {
527    fn schema(&self) -> SchemaRef {
528        self.schema.clone()
529    }
530}
531
532impl ScalarCalculateStream {
533    fn update_batch(&mut self, batch: RecordBatch) -> DataFusionResult<()> {
534        let _timer = self.metric.elapsed_compute();
535        // if have multi time series or empty batch, scalar will return NaN
536        if self.have_multi_series || batch.num_rows() == 0 {
537            return Ok(());
538        }
539        // fast path: no tag columns means all data belongs to the same series.
540        if self.tag_indices.is_empty() {
541            self.append_batch(batch)?;
542            return Ok(());
543        }
544        let all_same = |val: Option<&str>, array: &ArrayRef| -> bool {
545            (0..array.len()).all(|i| string_array_value_at_index(array, i) == val)
546        };
547        // assert the entire batch belong to the same series
548        let all_tag_columns_same = if let Some(tags) = &self.tag_value {
549            tags.iter()
550                .zip(self.tag_indices.iter())
551                .all(|(value, index)| {
552                    let array = batch.column(*index);
553                    all_same(value.as_deref(), array)
554                })
555        } else {
556            let mut tag_values = Vec::with_capacity(self.tag_indices.len());
557            let is_same = self.tag_indices.iter().all(|index| {
558                let array = batch.column(*index);
559                let value = string_array_value_at_index(array, 0).map(str::to_string);
560                let is_same = all_same(value.as_deref(), array);
561                tag_values.push(value);
562                is_same
563            });
564            self.tag_value = Some(tag_values);
565            is_same
566        };
567        if all_tag_columns_same {
568            self.append_batch(batch)?;
569        } else {
570            self.have_multi_series = true;
571        }
572        Ok(())
573    }
574
575    fn append_batch(&mut self, input_batch: RecordBatch) -> DataFusionResult<()> {
576        let ts_column = input_batch.column(self.project_index.0).clone();
577        let val_column = cast_with_options(
578            input_batch.column(self.project_index.1),
579            &DataType::Float64,
580            &CastOptions::default(),
581        )?;
582        let input_batch = RecordBatch::try_new(self.schema.clone(), vec![ts_column, val_column])?;
583        if let Some(batch) = &self.batch {
584            self.batch = Some(concat_batches(&self.schema, vec![batch, &input_batch])?);
585        } else {
586            self.batch = Some(input_batch);
587        }
588        Ok(())
589    }
590}
591
592impl Stream for ScalarCalculateStream {
593    type Item = DataFusionResult<RecordBatch>;
594
595    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
596        loop {
597            if self.done {
598                return Poll::Ready(None);
599            }
600            match ready!(self.input.poll_next_unpin(cx)) {
601                Some(Ok(batch)) => {
602                    self.update_batch(batch)?;
603                }
604                // inner had error, return to caller
605                Some(Err(e)) => return Poll::Ready(Some(Err(e))),
606                // inner is done, producing output
607                None => {
608                    self.done = true;
609                    return match self.batch.take() {
610                        Some(batch) if !self.have_multi_series => {
611                            self.metric.record_output(batch.num_rows());
612                            Poll::Ready(Some(Ok(batch)))
613                        }
614                        _ => {
615                            let time_array = (self.start..=self.end)
616                                .step_by(self.interval as _)
617                                .collect::<Vec<_>>();
618                            let nums = time_array.len();
619                            let nan_batch = RecordBatch::try_new(
620                                self.schema.clone(),
621                                vec![
622                                    Arc::new(TimestampMillisecondArray::from(time_array)),
623                                    Arc::new(Float64Array::from(vec![f64::NAN; nums])),
624                                ],
625                            )?;
626                            self.metric.record_output(nan_batch.num_rows());
627                            Poll::Ready(Some(Ok(nan_batch)))
628                        }
629                    };
630                }
631            };
632        }
633    }
634}
635
636#[cfg(test)]
637mod test {
638    use datafusion::arrow::datatypes::{DataType, Field, Schema};
639    use datafusion::datasource::memory::MemorySourceConfig;
640    use datafusion::datasource::source::DataSourceExec;
641    use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType};
642    use datafusion::prelude::SessionContext;
643    use datatypes::arrow::array::{
644        ArrayRef, DictionaryArray, Float64Array, StringArray, TimestampMillisecondArray,
645        UInt32Array,
646    };
647    use datatypes::arrow::datatypes::{TimeUnit, UInt32Type};
648
649    use super::*;
650
651    fn project_batch(batch: &RecordBatch, indices: &[usize]) -> RecordBatch {
652        let fields = indices
653            .iter()
654            .map(|&idx| batch.schema().field(idx).clone())
655            .collect::<Vec<_>>();
656        let columns = indices
657            .iter()
658            .map(|&idx| batch.column(idx).clone())
659            .collect::<Vec<_>>();
660        let schema = Arc::new(Schema::new(fields));
661        RecordBatch::try_new(schema, columns).unwrap()
662    }
663
664    #[test]
665    fn necessary_children_exprs_preserve_tag_columns() {
666        let schema = Arc::new(Schema::new(vec![
667            Field::new("ts", DataType::Timestamp(TimeUnit::Millisecond, None), true),
668            Field::new("tag1", DataType::Utf8, true),
669            Field::new("tag2", DataType::Utf8, true),
670            Field::new("val", DataType::Float64, true),
671            Field::new("extra", DataType::Utf8, true),
672        ]));
673        let schema = Arc::new(DFSchema::try_from(schema).unwrap());
674        let input = LogicalPlan::EmptyRelation(EmptyRelation {
675            produce_one_row: false,
676            schema,
677        });
678        let tag_columns = vec!["tag1".to_string(), "tag2".to_string()];
679        let plan = ScalarCalculate::new(0, 1, 1, input, "ts", &tag_columns, "val", None).unwrap();
680
681        let required = plan.necessary_children_exprs(&[0, 1]).unwrap();
682        assert_eq!(required, vec![vec![0, 1, 2, 3]]);
683    }
684
685    #[tokio::test]
686    async fn pruning_should_keep_tag_columns_for_exec() {
687        let schema = Arc::new(Schema::new(vec![
688            Field::new("ts", DataType::Timestamp(TimeUnit::Millisecond, None), true),
689            Field::new("tag1", DataType::Utf8, true),
690            Field::new("tag2", DataType::Utf8, true),
691            Field::new("val", DataType::Float64, true),
692            Field::new("extra", DataType::Utf8, true),
693        ]));
694        let df_schema = Arc::new(DFSchema::try_from(schema.clone()).unwrap());
695        let input = LogicalPlan::EmptyRelation(EmptyRelation {
696            produce_one_row: false,
697            schema: df_schema,
698        });
699        let tag_columns = vec!["tag1".to_string(), "tag2".to_string()];
700        let plan =
701            ScalarCalculate::new(0, 15_000, 5000, input, "ts", &tag_columns, "val", None).unwrap();
702
703        let required = plan.necessary_children_exprs(&[0, 1]).unwrap();
704        let required = &required[0];
705
706        let batch = RecordBatch::try_new(
707            schema,
708            vec![
709                Arc::new(TimestampMillisecondArray::from(vec![
710                    0, 5_000, 10_000, 15_000,
711                ])),
712                Arc::new(StringArray::from(vec!["foo", "foo", "foo", "foo"])),
713                Arc::new(StringArray::from(vec!["bar", "bar", "bar", "bar"])),
714                Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0, 4.0])),
715                Arc::new(StringArray::from(vec!["x", "x", "x", "x"])),
716            ],
717        )
718        .unwrap();
719
720        let projected_batch = project_batch(&batch, required);
721        let projected_schema = projected_batch.schema();
722        let memory_exec = Arc::new(DataSourceExec::new(Arc::new(
723            MemorySourceConfig::try_new(&[vec![projected_batch]], projected_schema, None).unwrap(),
724        )));
725        let scalar_exec = plan.to_execution_plan(memory_exec).unwrap();
726
727        let session_context = SessionContext::default();
728        let result = datafusion::physical_plan::collect(scalar_exec, session_context.task_ctx())
729            .await
730            .unwrap();
731
732        assert_eq!(result.len(), 1);
733        let batch = &result[0];
734        assert_eq!(batch.num_columns(), 2);
735        assert_eq!(batch.num_rows(), 4);
736        assert_eq!(batch.schema().field(0).name(), "ts");
737        assert_eq!(batch.schema().field(1).name(), "scalar(val)");
738
739        let ts = batch
740            .column(0)
741            .as_any()
742            .downcast_ref::<TimestampMillisecondArray>()
743            .unwrap();
744        assert_eq!(ts.values(), &[0i64, 5_000, 10_000, 15_000]);
745
746        let values = batch
747            .column(1)
748            .as_any()
749            .downcast_ref::<Float64Array>()
750            .unwrap();
751        assert_eq!(values.values(), &[1.0f64, 2.0, 3.0, 4.0]);
752    }
753
754    fn prepare_test_data(series: Vec<RecordBatch>) -> DataSourceExec {
755        let schema = series.first().unwrap().schema();
756        DataSourceExec::new(Arc::new(
757            MemorySourceConfig::try_new(&[series], schema, None).unwrap(),
758        ))
759    }
760
761    fn dictionary(values: &[&str], keys: Vec<u32>) -> ArrayRef {
762        Arc::new(DictionaryArray::<UInt32Type>::new(
763            UInt32Array::from(keys),
764            Arc::new(StringArray::from(values.to_vec())),
765        ))
766    }
767
768    async fn run_test(series: Vec<RecordBatch>, expected: &str) {
769        let memory_exec = Arc::new(prepare_test_data(series));
770        let schema = Arc::new(Schema::new(vec![
771            Field::new("ts", DataType::Timestamp(TimeUnit::Millisecond, None), true),
772            Field::new("val", DataType::Float64, true),
773        ]));
774        let properties = Arc::new(PlanProperties::new(
775            EquivalenceProperties::new(schema.clone()),
776            Partitioning::UnknownPartitioning(1),
777            EmissionType::Incremental,
778            Boundedness::Bounded,
779        ));
780        let scalar_exec = Arc::new(ScalarCalculateExec {
781            start: 0,
782            end: 15_000,
783            interval: 5000,
784            tag_columns: vec!["tag1".to_string(), "tag2".to_string()],
785            input: memory_exec,
786            schema,
787            project_index: (0, 3),
788            metric: ExecutionPlanMetricsSet::new(),
789            properties,
790        });
791        let session_context = SessionContext::default();
792        let result = datafusion::physical_plan::collect(scalar_exec, session_context.task_ctx())
793            .await
794            .unwrap();
795        let result_literal = datatypes::arrow::util::pretty::pretty_format_batches(&result)
796            .unwrap()
797            .to_string();
798        assert_eq!(result_literal, expected);
799    }
800
801    #[tokio::test]
802    async fn same_series() {
803        let schema = Arc::new(Schema::new(vec![
804            Field::new("ts", DataType::Timestamp(TimeUnit::Millisecond, None), true),
805            Field::new("tag1", DataType::Utf8, true),
806            Field::new("tag2", DataType::Utf8, true),
807            Field::new("val", DataType::Float64, true),
808        ]));
809        run_test(
810            vec![
811                RecordBatch::try_new(
812                    schema.clone(),
813                    vec![
814                        Arc::new(TimestampMillisecondArray::from(vec![0, 5_000])),
815                        Arc::new(StringArray::from(vec!["foo", "foo"])),
816                        Arc::new(StringArray::from(vec!["🥺", "🥺"])),
817                        Arc::new(Float64Array::from(vec![1.0, 2.0])),
818                    ],
819                )
820                .unwrap(),
821                RecordBatch::try_new(
822                    schema,
823                    vec![
824                        Arc::new(TimestampMillisecondArray::from(vec![10_000, 15_000])),
825                        Arc::new(StringArray::from(vec!["foo", "foo"])),
826                        Arc::new(StringArray::from(vec!["🥺", "🥺"])),
827                        Arc::new(Float64Array::from(vec![3.0, 4.0])),
828                    ],
829                )
830                .unwrap(),
831            ],
832            "+---------------------+-----+\
833            \n| ts                  | val |\
834            \n+---------------------+-----+\
835            \n| 1970-01-01T00:00:00 | 1.0 |\
836            \n| 1970-01-01T00:00:05 | 2.0 |\
837            \n| 1970-01-01T00:00:10 | 3.0 |\
838            \n| 1970-01-01T00:00:15 | 4.0 |\
839            \n+---------------------+-----+",
840        )
841        .await
842    }
843
844    #[tokio::test]
845    async fn same_series_with_dictionary_tags() {
846        let dictionary_type =
847            DataType::Dictionary(Box::new(DataType::UInt32), Box::new(DataType::Utf8));
848        let schema = Arc::new(Schema::new(vec![
849            Field::new("ts", DataType::Timestamp(TimeUnit::Millisecond, None), true),
850            Field::new("tag1", dictionary_type.clone(), true),
851            Field::new("tag2", dictionary_type, true),
852            Field::new("val", DataType::Float64, true),
853        ]));
854        run_test(
855            vec![
856                RecordBatch::try_new(
857                    schema.clone(),
858                    vec![
859                        Arc::new(TimestampMillisecondArray::from(vec![0, 5_000])),
860                        dictionary(&["foo"], vec![0, 0]),
861                        dictionary(&["unused", "bar"], vec![1, 1]),
862                        Arc::new(Float64Array::from(vec![1.0, 2.0])),
863                    ],
864                )
865                .unwrap(),
866                RecordBatch::try_new(
867                    schema,
868                    vec![
869                        Arc::new(TimestampMillisecondArray::from(vec![10_000, 15_000])),
870                        dictionary(&["other", "foo"], vec![1, 1]),
871                        dictionary(&["bar"], vec![0, 0]),
872                        Arc::new(Float64Array::from(vec![3.0, 4.0])),
873                    ],
874                )
875                .unwrap(),
876            ],
877            "+---------------------+-----+\
878            \n| ts                  | val |\
879            \n+---------------------+-----+\
880            \n| 1970-01-01T00:00:00 | 1.0 |\
881            \n| 1970-01-01T00:00:05 | 2.0 |\
882            \n| 1970-01-01T00:00:10 | 3.0 |\
883            \n| 1970-01-01T00:00:15 | 4.0 |\
884            \n+---------------------+-----+",
885        )
886        .await
887    }
888
889    #[tokio::test]
890    async fn diff_series() {
891        let schema = Arc::new(Schema::new(vec![
892            Field::new("ts", DataType::Timestamp(TimeUnit::Millisecond, None), true),
893            Field::new("tag1", DataType::Utf8, true),
894            Field::new("tag2", DataType::Utf8, true),
895            Field::new("val", DataType::Float64, true),
896        ]));
897        run_test(
898            vec![
899                RecordBatch::try_new(
900                    schema.clone(),
901                    vec![
902                        Arc::new(TimestampMillisecondArray::from(vec![0, 5_000])),
903                        Arc::new(StringArray::from(vec!["foo", "foo"])),
904                        Arc::new(StringArray::from(vec!["🥺", "🥺"])),
905                        Arc::new(Float64Array::from(vec![1.0, 2.0])),
906                    ],
907                )
908                .unwrap(),
909                RecordBatch::try_new(
910                    schema,
911                    vec![
912                        Arc::new(TimestampMillisecondArray::from(vec![10_000, 15_000])),
913                        Arc::new(StringArray::from(vec!["foo", "foo"])),
914                        Arc::new(StringArray::from(vec!["🥺", "😝"])),
915                        Arc::new(Float64Array::from(vec![3.0, 4.0])),
916                    ],
917                )
918                .unwrap(),
919            ],
920            "+---------------------+-----+\
921            \n| ts                  | val |\
922            \n+---------------------+-----+\
923            \n| 1970-01-01T00:00:00 | NaN |\
924            \n| 1970-01-01T00:00:05 | NaN |\
925            \n| 1970-01-01T00:00:10 | NaN |\
926            \n| 1970-01-01T00:00:15 | NaN |\
927            \n+---------------------+-----+",
928        )
929        .await
930    }
931
932    #[tokio::test]
933    async fn empty_series() {
934        let schema = Arc::new(Schema::new(vec![
935            Field::new("ts", DataType::Timestamp(TimeUnit::Millisecond, None), true),
936            Field::new("tag1", DataType::Utf8, true),
937            Field::new("tag2", DataType::Utf8, true),
938            Field::new("val", DataType::Float64, true),
939        ]));
940        run_test(
941            vec![
942                RecordBatch::try_new(
943                    schema,
944                    vec![
945                        Arc::new(TimestampMillisecondArray::new_null(0)),
946                        Arc::new(StringArray::new_null(0)),
947                        Arc::new(StringArray::new_null(0)),
948                        Arc::new(Float64Array::new_null(0)),
949                    ],
950                )
951                .unwrap(),
952            ],
953            "+---------------------+-----+\
954            \n| ts                  | val |\
955            \n+---------------------+-----+\
956            \n| 1970-01-01T00:00:00 | NaN |\
957            \n| 1970-01-01T00:00:05 | NaN |\
958            \n| 1970-01-01T00:00:10 | NaN |\
959            \n| 1970-01-01T00:00:15 | NaN |\
960            \n+---------------------+-----+",
961        )
962        .await
963    }
964}