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