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