1use 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#[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 #[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 pub const fn name() -> &'static str {
120 "ScalarCalculate"
121 }
122
123 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 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 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 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 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 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 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 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 self.have_multi_series || batch.num_rows() == 0 {
537 return Ok(());
538 }
539 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 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 Some(Err(e)) => return Poll::Ready(Some(Err(e))),
606 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}