1use std::any::Any;
16use std::cmp::Ordering;
17use std::pin::Pin;
18use std::sync::Arc;
19use std::task::{Context, Poll};
20
21use common_query::prelude::{greptime_native_histogram, greptime_value};
22use datafusion::arrow::array::{Array, TimestampMillisecondArray, UInt64Array};
23use datafusion::arrow::datatypes::{DataType, SchemaRef};
24use datafusion::arrow::record_batch::RecordBatch;
25use datafusion::common::stats::Precision;
26use datafusion::common::{DFSchema, DFSchemaRef, ScalarValue};
27use datafusion::error::{DataFusionError, Result as DataFusionResult};
28use datafusion::execution::context::TaskContext;
29use datafusion::logical_expr::{
30 EmptyRelation, Expr, Extension, LogicalPlan, UserDefinedLogicalNodeCore,
31};
32use datafusion::physical_plan::metrics::{
33 BaselineMetrics, Count, ExecutionPlanMetricsSet, MetricBuilder, MetricValue, MetricsSet,
34};
35use datafusion::physical_plan::{
36 DisplayAs, DisplayFormatType, Distribution, ExecutionPlan, PlanProperties, RecordBatchStream,
37 SendableRecordBatchStream, Statistics,
38};
39use datafusion_expr::col;
40use datatypes::arrow::compute;
41use futures::{Stream, StreamExt, ready};
42use greptime_proto::substrait_extension as pb;
43use prost::Message;
44use snafu::ResultExt;
45
46use crate::error::{DeserializeSnafu, Result};
47use crate::extension_plan::series_divide::SeriesDivide;
48use crate::extension_plan::{
49 METRIC_NUM_SERIES, Millisecond, is_prometheus_stale_sample, prometheus_stale_sample_column,
50 resolve_column_name, serialize_column_index,
51};
52use crate::metrics::PROMQL_SERIES_COUNT;
53
54const MAX_INSTANT_MANIPULATE_OUTPUT_POINTS: usize = 1_000_000;
55
56fn mixed_sample_fields(field: Option<&str>) -> [Option<&str>; 2] {
57 let companion = match field {
58 Some(field) if field == greptime_value() => Some(greptime_native_histogram()),
59 Some(field) if field == greptime_native_histogram() => Some(greptime_value()),
60 _ => None,
61 };
62 [field, companion]
63}
64
65#[derive(Debug, PartialEq, Eq, Hash, PartialOrd)]
71pub struct InstantManipulate {
72 start: Millisecond,
73 end: Millisecond,
74 lookback_delta: Millisecond,
75 interval: Millisecond,
76 time_index_column: String,
77 tag_columns: Vec<String>,
79 field_column: Option<String>,
81 input: LogicalPlan,
82 unfix: Option<UnfixIndices>,
83}
84
85#[derive(Debug, PartialEq, Eq, Hash, PartialOrd)]
86struct UnfixIndices {
87 pub time_index_idx: u64,
88 pub field_index_idx: u64,
89}
90
91impl UserDefinedLogicalNodeCore for InstantManipulate {
92 fn name(&self) -> &str {
93 Self::name()
94 }
95
96 fn inputs(&self) -> Vec<&LogicalPlan> {
97 vec![&self.input]
98 }
99
100 fn schema(&self) -> &DFSchemaRef {
101 self.input.schema()
102 }
103
104 fn expressions(&self) -> Vec<Expr> {
105 if self.unfix.is_some() {
106 return vec![];
107 }
108
109 let mut exprs = vec![col(&self.time_index_column)];
110 exprs.extend(self.staleness_field_columns().map(col));
111 exprs
112 }
113
114 fn necessary_children_exprs(&self, output_columns: &[usize]) -> Option<Vec<Vec<usize>>> {
115 if self.unfix.is_some() {
116 return None;
117 }
118
119 let input_schema = self.input.schema();
120 if output_columns.is_empty() {
121 let indices = (0..input_schema.fields().len()).collect::<Vec<_>>();
122 return Some(vec![indices]);
123 }
124
125 let mut required = output_columns.to_vec();
126 required.push(input_schema.index_of_column_by_name(None, &self.time_index_column)?);
127 for field in self.staleness_field_columns() {
128 required.push(input_schema.index_of_column_by_name(None, field)?);
129 }
130
131 required.sort_unstable();
132 required.dedup();
133 Some(vec![required])
134 }
135
136 fn fmt_for_explain(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
137 write!(
138 f,
139 "PromInstantManipulate: range=[{}..{}], lookback=[{}], interval=[{}], time index=[{}]",
140 self.start, self.end, self.lookback_delta, self.interval, self.time_index_column
141 )
142 }
143
144 fn with_exprs_and_inputs(
145 &self,
146 _exprs: Vec<Expr>,
147 inputs: Vec<LogicalPlan>,
148 ) -> DataFusionResult<Self> {
149 if inputs.len() != 1 {
150 return Err(DataFusionError::Internal(
151 "InstantManipulate should have exact one input".to_string(),
152 ));
153 }
154
155 let input: LogicalPlan = inputs.into_iter().next().unwrap();
156 let input_schema = input.schema();
157
158 if let Some(unfix) = &self.unfix {
159 let time_index_column = resolve_column_name(
161 unfix.time_index_idx,
162 input_schema,
163 "InstantManipulate",
164 "time index",
165 )?;
166
167 let field_column = if unfix.field_index_idx == u64::MAX {
168 None
169 } else {
170 Some(resolve_column_name(
171 unfix.field_index_idx,
172 input_schema,
173 "InstantManipulate",
174 "field",
175 )?)
176 };
177
178 Ok(Self {
179 start: self.start,
180 end: self.end,
181 lookback_delta: self.lookback_delta,
182 interval: self.interval,
183 time_index_column,
184 tag_columns: Self::resolve_tag_columns(&input, &self.tag_columns),
185 field_column,
186 input,
187 unfix: None,
188 })
189 } else {
190 Ok(Self {
191 start: self.start,
192 end: self.end,
193 lookback_delta: self.lookback_delta,
194 interval: self.interval,
195 time_index_column: self.time_index_column.clone(),
196 tag_columns: Self::resolve_tag_columns(&input, &self.tag_columns),
197 field_column: self.field_column.clone(),
198 input,
199 unfix: None,
200 })
201 }
202 }
203}
204
205impl InstantManipulate {
206 #[allow(clippy::too_many_arguments)]
207 pub fn new(
208 start: Millisecond,
209 end: Millisecond,
210 lookback_delta: Millisecond,
211 interval: Millisecond,
212 time_index_column: String,
213 tag_columns: Vec<String>,
214 field_column: Option<String>,
215 input: LogicalPlan,
216 ) -> Self {
217 Self {
218 start,
219 end,
220 lookback_delta,
221 interval,
222 time_index_column,
223 tag_columns,
224 field_column,
225 input,
226 unfix: None,
227 }
228 }
229
230 pub const fn name() -> &'static str {
231 "InstantManipulate"
232 }
233
234 fn staleness_field_columns(&self) -> impl Iterator<Item = &str> {
235 let [field, companion] = mixed_sample_fields(self.field_column.as_deref());
236 [
237 field,
238 companion.filter(|companion| {
239 self.input
240 .schema()
241 .index_of_column_by_name(None, companion)
242 .is_some()
243 }),
244 ]
245 .into_iter()
246 .flatten()
247 }
248
249 fn resolve_tag_columns(input: &LogicalPlan, tag_columns: &[String]) -> Vec<String> {
250 if !tag_columns.is_empty() {
251 return tag_columns.to_vec();
252 }
253
254 Self::find_series_divide_tags(input).unwrap_or_default()
255 }
256
257 fn find_series_divide_tags(plan: &LogicalPlan) -> Option<Vec<String>> {
258 if let LogicalPlan::Extension(Extension { node }) = plan
259 && let Some(series_divide) = node.as_any().downcast_ref::<SeriesDivide>()
260 {
261 return Some(series_divide.tags().to_vec());
262 }
263
264 plan.inputs()
265 .into_iter()
266 .find_map(Self::find_series_divide_tags)
267 }
268
269 pub fn to_execution_plan(&self, exec_input: Arc<dyn ExecutionPlan>) -> Arc<dyn ExecutionPlan> {
270 let reuse_tsid_column = matches!(self.tag_columns.as_slice(), [tag] if tag == "__tsid");
271
272 Arc::new(InstantManipulateExec {
273 start: self.start,
274 end: self.end,
275 lookback_delta: self.lookback_delta,
276 interval: self.interval,
277 time_index_column: self.time_index_column.clone(),
278 field_column: self.field_column.clone(),
279 reuse_tsid_column,
280 input: exec_input,
281 metric: ExecutionPlanMetricsSet::new(),
282 })
283 }
284
285 pub fn serialize(&self) -> Vec<u8> {
286 let time_index_idx = serialize_column_index(self.input.schema(), &self.time_index_column);
287
288 let field_index_idx = self
289 .field_column
290 .as_ref()
291 .map(|name| serialize_column_index(self.input.schema(), name))
292 .unwrap_or(u64::MAX);
293
294 pb::InstantManipulate {
295 start: self.start,
296 end: self.end,
297 interval: self.interval,
298 lookback_delta: self.lookback_delta,
299 time_index_idx,
300 field_index_idx,
301 ..Default::default()
302 }
303 .encode_to_vec()
304 }
305
306 pub fn deserialize(bytes: &[u8]) -> Result<Self> {
307 let pb_instant_manipulate =
308 pb::InstantManipulate::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 time_index_idx: pb_instant_manipulate.time_index_idx,
316 field_index_idx: pb_instant_manipulate.field_index_idx,
317 };
318
319 Ok(Self {
320 start: pb_instant_manipulate.start,
321 end: pb_instant_manipulate.end,
322 lookback_delta: pb_instant_manipulate.lookback_delta,
323 interval: pb_instant_manipulate.interval,
324 time_index_column: String::new(),
325 tag_columns: Vec::new(),
326 field_column: None,
327 input: placeholder_plan,
328 unfix: Some(unfix),
329 })
330 }
331}
332
333#[derive(Debug)]
334pub struct InstantManipulateExec {
335 start: Millisecond,
336 end: Millisecond,
337 lookback_delta: Millisecond,
338 interval: Millisecond,
339 time_index_column: String,
340 field_column: Option<String>,
341 reuse_tsid_column: bool,
342
343 input: Arc<dyn ExecutionPlan>,
344 metric: ExecutionPlanMetricsSet,
345}
346
347impl ExecutionPlan for InstantManipulateExec {
348 fn as_any(&self) -> &dyn Any {
349 self
350 }
351
352 fn schema(&self) -> SchemaRef {
353 self.input.schema()
354 }
355
356 fn properties(&self) -> &Arc<PlanProperties> {
357 self.input.properties()
358 }
359
360 fn required_input_distribution(&self) -> Vec<Distribution> {
361 self.input.required_input_distribution()
362 }
363
364 fn maintains_input_order(&self) -> Vec<bool> {
366 vec![false; self.children().len()]
367 }
368
369 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
370 vec![&self.input]
371 }
372
373 fn with_new_children(
374 self: Arc<Self>,
375 children: Vec<Arc<dyn ExecutionPlan>>,
376 ) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
377 assert!(!children.is_empty());
378 Ok(Arc::new(Self {
379 start: self.start,
380 end: self.end,
381 lookback_delta: self.lookback_delta,
382 interval: self.interval,
383 time_index_column: self.time_index_column.clone(),
384 field_column: self.field_column.clone(),
385 reuse_tsid_column: self.reuse_tsid_column,
386 input: children[0].clone(),
387 metric: self.metric.clone(),
388 }))
389 }
390
391 fn execute(
392 &self,
393 partition: usize,
394 context: Arc<TaskContext>,
395 ) -> DataFusionResult<SendableRecordBatchStream> {
396 let baseline_metric = BaselineMetrics::new(&self.metric, partition);
397 let num_series = Count::new();
398 MetricBuilder::new(&self.metric)
399 .with_partition(partition)
400 .build(MetricValue::Count {
401 name: METRIC_NUM_SERIES.into(),
402 count: num_series.clone(),
403 });
404
405 let input = self.input.execute(partition, context)?;
406 let schema = input.schema();
407 let time_index = schema
408 .column_with_name(&self.time_index_column)
409 .expect("time index column not found")
410 .0;
411 let field_indices = mixed_sample_fields(self.field_column.as_deref()).map(|field| {
412 field.and_then(|field| schema.column_with_name(field).map(|(index, _)| index))
413 });
414 let tsid_index = schema
415 .column_with_name("__tsid")
416 .filter(|(_, field)| field.data_type() == &DataType::UInt64)
417 .map(|(index, _)| index);
418 Ok(Box::pin(InstantManipulateStream {
419 start: self.start,
420 end: self.end,
421 lookback_delta: self.lookback_delta,
422 interval: self.interval,
423 time_index,
424 field_indices,
425 tsid_index,
426 reuse_tsid_column: self.reuse_tsid_column && tsid_index.is_some(),
427 schema,
428 input,
429 metric: baseline_metric,
430 num_series,
431 }))
432 }
433
434 fn metrics(&self) -> Option<MetricsSet> {
435 Some(self.metric.clone_inner())
436 }
437
438 fn partition_statistics(&self, partition: Option<usize>) -> DataFusionResult<Statistics> {
439 let input_stats = self.input.partition_statistics(partition)?;
440
441 let estimated_row_num = (self.end - self.start) as f64 / self.interval as f64;
442 let estimated_total_bytes = input_stats
443 .total_byte_size
444 .get_value()
445 .zip(input_stats.num_rows.get_value())
446 .map(|(size, rows)| {
447 Precision::Inexact(((*size as f64 / *rows as f64) * estimated_row_num).floor() as _)
448 })
449 .unwrap_or(Precision::Absent);
450
451 Ok(Statistics {
452 num_rows: Precision::Inexact(estimated_row_num.floor() as _),
453 total_byte_size: estimated_total_bytes,
454 column_statistics: Statistics::unknown_column(&self.schema()),
456 })
457 }
458
459 fn name(&self) -> &str {
460 "InstantManipulateExec"
461 }
462}
463
464impl DisplayAs for InstantManipulateExec {
465 fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
466 match t {
467 DisplayFormatType::Default
468 | DisplayFormatType::Verbose
469 | DisplayFormatType::TreeRender => {
470 write!(
471 f,
472 "PromInstantManipulateExec: range=[{}..{}], lookback=[{}], interval=[{}], time index=[{}]",
473 self.start,
474 self.end,
475 self.lookback_delta,
476 self.interval,
477 self.time_index_column
478 )
479 }
480 }
481 }
482}
483
484pub struct InstantManipulateStream {
485 start: Millisecond,
486 end: Millisecond,
487 lookback_delta: Millisecond,
488 interval: Millisecond,
489 time_index: usize,
491 field_indices: [Option<usize>; 2],
492 tsid_index: Option<usize>,
493 reuse_tsid_column: bool,
494
495 schema: SchemaRef,
496 input: SendableRecordBatchStream,
497 metric: BaselineMetrics,
498 num_series: Count,
500}
501
502impl RecordBatchStream for InstantManipulateStream {
503 fn schema(&self) -> SchemaRef {
504 self.schema.clone()
505 }
506}
507
508impl Stream for InstantManipulateStream {
509 type Item = DataFusionResult<RecordBatch>;
510
511 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
512 let poll = match ready!(self.input.poll_next_unpin(cx)) {
513 Some(Ok(batch)) => {
514 if batch.num_rows() == 0 {
515 return Poll::Pending;
516 }
517 let timer = std::time::Instant::now();
518 self.num_series.add(1);
519 let result = Ok(batch).and_then(|batch| self.manipulate(batch));
520 self.metric.elapsed_compute().add_elapsed(timer);
521 Poll::Ready(Some(result))
522 }
523 None => {
524 PROMQL_SERIES_COUNT.observe(self.num_series.value() as f64);
525 Poll::Ready(None)
526 }
527 Some(Err(e)) => Poll::Ready(Some(Err(e))),
528 };
529 self.metric.record_poll(poll)
530 }
531}
532
533impl InstantManipulateStream {
534 pub fn manipulate(&self, input: RecordBatch) -> DataFusionResult<RecordBatch> {
541 let ts_column = input
542 .column(self.time_index)
543 .as_any()
544 .downcast_ref::<TimestampMillisecondArray>()
545 .ok_or_else(|| {
546 DataFusionError::Execution(
547 "Time index Column downcast to TimestampMillisecondArray failed".into(),
548 )
549 })?;
550
551 if ts_column.is_empty() {
553 return Ok(input);
554 }
555
556 let stale_sample_columns = self.field_indices.map(|index| {
558 index.and_then(|index| prometheus_stale_sample_column(input.column(index).as_ref()))
559 });
560 let is_stale = |row| {
561 stale_sample_columns
562 .iter()
563 .flatten()
564 .any(|column| is_prometheus_stale_sample(*column, row))
565 };
566
567 let first_ts = ts_column.value(0);
569 let last_ts = ts_column.value(ts_column.len() - 1);
570 let last_useful = if self.lookback_delta > 0 {
575 last_ts + self.lookback_delta - 1
576 } else {
577 last_ts
578 };
579
580 let max_start = first_ts.max(self.start);
581 let min_end = last_useful.min(self.end);
582
583 let aligned_start = self.start + (max_start - self.start) / self.interval * self.interval;
584 let aligned_end = self.end - (self.end - min_end) / self.interval * self.interval;
585
586 let estimated_points = if aligned_end >= aligned_start {
587 ((aligned_end - aligned_start) / self.interval).saturating_add(1) as usize
588 } else {
589 0
590 };
591 if estimated_points > MAX_INSTANT_MANIPULATE_OUTPUT_POINTS {
592 return Err(DataFusionError::Execution(format!(
593 "InstantManipulate output points exceed limit: {estimated_points} > {MAX_INSTANT_MANIPULATE_OUTPUT_POINTS}"
594 )));
595 }
596 let mut take_indices = Vec::with_capacity(estimated_points);
597
598 let mut cursor = 0;
599
600 let aligned_ts_iter = (aligned_start..=aligned_end).step_by(self.interval as usize);
601 let mut aligned_ts = Vec::with_capacity(estimated_points);
602
603 'next: for expected_ts in aligned_ts_iter {
605 while cursor < ts_column.len() {
607 let curr = ts_column.value(cursor);
608 match curr.cmp(&expected_ts) {
609 Ordering::Equal => {
610 if is_stale(cursor) {
611 } else {
613 take_indices.push(cursor as u64);
614 aligned_ts.push(expected_ts);
615 }
616 continue 'next;
617 }
618 Ordering::Greater => break,
619 Ordering::Less => {}
620 }
621 cursor += 1;
622 }
623 if cursor == ts_column.len() {
624 cursor -= 1;
625 if ts_column.value(cursor) + self.lookback_delta <= expected_ts {
627 break;
628 }
629 }
630
631 let curr_ts = ts_column.value(cursor);
633 if curr_ts + self.lookback_delta <= expected_ts {
634 continue;
635 }
636 if curr_ts > expected_ts {
637 if let Some(prev_cursor) = cursor.checked_sub(1) {
639 let prev_ts = ts_column.value(prev_cursor);
640 if prev_ts + self.lookback_delta > expected_ts {
641 if is_stale(prev_cursor) {
643 continue;
645 }
646 take_indices.push(prev_cursor as u64);
648 aligned_ts.push(expected_ts);
649 }
650 }
651 } else if is_stale(cursor) {
652 } else {
654 take_indices.push(cursor as u64);
656 aligned_ts.push(expected_ts);
657 }
658 }
659
660 self.take_record_batch_optional(input, take_indices, aligned_ts)
662 }
663
664 fn take_record_batch_optional(
666 &self,
667 record_batch: RecordBatch,
668 take_indices: Vec<u64>,
669 aligned_ts: Vec<Millisecond>,
670 ) -> DataFusionResult<RecordBatch> {
671 assert_eq!(take_indices.len(), aligned_ts.len());
672
673 let output_len = aligned_ts.len();
674 let mut indices_array = None;
675 let mut arrays = Vec::with_capacity(record_batch.num_columns());
676 let aligned_ts = Arc::new(TimestampMillisecondArray::from(aligned_ts)) as Arc<dyn Array>;
677
678 for (index, array) in record_batch.columns().iter().enumerate() {
679 if index == self.time_index {
680 arrays.push(aligned_ts.clone());
681 continue;
682 }
683
684 if self.reuse_tsid_column && self.tsid_index == Some(index) {
685 arrays.push(reuse_constant_column(array, output_len)?);
686 continue;
687 }
688
689 let indices_array =
690 indices_array.get_or_insert_with(|| UInt64Array::from(take_indices.clone()));
691 arrays.push(compute::take(array, indices_array, None)?);
692 }
693
694 let result = RecordBatch::try_new(record_batch.schema(), arrays)
695 .map_err(|e| DataFusionError::ArrowError(Box::new(e), None))?;
696 Ok(result)
697 }
698}
699
700fn reuse_constant_column(array: &Arc<dyn Array>, len: usize) -> DataFusionResult<Arc<dyn Array>> {
701 if len <= array.len() {
702 return Ok(array.slice(0, len));
703 }
704
705 if array.is_empty() {
706 return Ok(array.slice(0, 0));
707 }
708
709 ScalarValue::try_from_array(array.as_ref(), 0)?.to_array_of_size(len)
710}
711
712#[cfg(test)]
713mod test {
714 use common_query::native_histogram::build_histogram_array;
715 use common_query::prometheus::PROMETHEUS_STALE_NAN_BITS;
716 use datafusion::arrow::array::Float64Array;
717 use datafusion::arrow::buffer::NullBuffer;
718 use datafusion::arrow::datatypes::{DataType, Field, Schema};
719 use datafusion::common::ToDFSchema;
720 use datafusion::datasource::memory::MemorySourceConfig;
721 use datafusion::datasource::source::DataSourceExec;
722 use datafusion::logical_expr::{EmptyRelation, LogicalPlan};
723 use datafusion::prelude::SessionContext;
724
725 use super::*;
726 use crate::extension_plan::test_util::{
727 TIME_INDEX_COLUMN, native_histogram, prepare_test_data, prepare_test_data_with_stale_marker,
728 };
729
730 async fn do_normalize_test(
731 start: Millisecond,
732 end: Millisecond,
733 lookback_delta: Millisecond,
734 interval: Millisecond,
735 expected: String,
736 contains_stale_marker: bool,
737 ) {
738 let memory_exec = if contains_stale_marker {
739 Arc::new(prepare_test_data_with_stale_marker())
740 } else {
741 Arc::new(prepare_test_data())
742 };
743 let normalize_exec = Arc::new(InstantManipulateExec {
744 start,
745 end,
746 lookback_delta,
747 interval,
748 time_index_column: TIME_INDEX_COLUMN.to_string(),
749 field_column: Some("value".to_string()),
750 reuse_tsid_column: false,
751 input: memory_exec,
752 metric: ExecutionPlanMetricsSet::new(),
753 });
754 let session_context = SessionContext::default();
755 let result = datafusion::physical_plan::collect(normalize_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 assert_eq!(result_literal, expected);
763 }
764
765 #[test]
766 fn pruning_should_keep_time_and_field_columns_for_exec() {
767 let df_schema = prepare_test_data().schema().to_dfschema_ref().unwrap();
768 let input = LogicalPlan::EmptyRelation(EmptyRelation {
769 produce_one_row: false,
770 schema: df_schema,
771 });
772 let plan = InstantManipulate::new(
773 0,
774 0,
775 0,
776 0,
777 TIME_INDEX_COLUMN.to_string(),
778 Vec::new(),
779 Some("value".to_string()),
780 input,
781 );
782
783 let output_columns = [2usize];
785 let required = plan.necessary_children_exprs(&output_columns).unwrap();
786 let required = &required[0];
787 assert_eq!(required.as_slice(), &[0, 1, 2]);
788 }
789
790 #[test]
791 fn rebuild_should_recover_tag_columns_from_series_divide_input() {
792 let df_schema = prepare_test_data().schema().to_dfschema_ref().unwrap();
793 let input = LogicalPlan::EmptyRelation(EmptyRelation {
794 produce_one_row: false,
795 schema: df_schema,
796 });
797 let series_divide = LogicalPlan::Extension(Extension {
798 node: Arc::new(SeriesDivide::new(
799 vec!["__tsid".to_string()],
800 TIME_INDEX_COLUMN.to_string(),
801 input,
802 )),
803 });
804 let bytes = InstantManipulate::new(
805 0,
806 0,
807 0,
808 0,
809 TIME_INDEX_COLUMN.to_string(),
810 vec!["__tsid".to_string()],
811 Some("value".to_string()),
812 series_divide.clone(),
813 )
814 .serialize();
815 let plan = InstantManipulate::deserialize(&bytes)
816 .unwrap()
817 .with_exprs_and_inputs(vec![], vec![series_divide])
818 .unwrap();
819
820 assert_eq!(plan.tag_columns, vec!["__tsid".to_string()]);
821 }
822
823 #[test]
824 fn rebuild_should_recover_tag_columns_from_series_normalize_input() {
825 let df_schema = prepare_test_data().schema().to_dfschema_ref().unwrap();
826 let input = LogicalPlan::EmptyRelation(EmptyRelation {
827 produce_one_row: false,
828 schema: df_schema,
829 });
830 let series_divide = LogicalPlan::Extension(Extension {
831 node: Arc::new(SeriesDivide::new(
832 vec!["__tsid".to_string()],
833 TIME_INDEX_COLUMN.to_string(),
834 input,
835 )),
836 });
837 let series_normalize = LogicalPlan::Extension(Extension {
838 node: Arc::new(crate::extension_plan::SeriesNormalize::new(
839 0,
840 TIME_INDEX_COLUMN,
841 false,
842 vec!["__tsid".to_string()],
843 series_divide,
844 )),
845 });
846 let bytes = InstantManipulate::new(
847 0,
848 0,
849 0,
850 0,
851 TIME_INDEX_COLUMN.to_string(),
852 vec!["__tsid".to_string()],
853 Some("value".to_string()),
854 series_normalize.clone(),
855 )
856 .serialize();
857 let plan = InstantManipulate::deserialize(&bytes)
858 .unwrap()
859 .with_exprs_and_inputs(vec![], vec![series_normalize])
860 .unwrap();
861
862 assert_eq!(plan.tag_columns, vec!["__tsid".to_string()]);
863 }
864
865 #[test]
866 fn to_execution_plan_enables_tsid_fast_path() {
867 let schema = Arc::new(Schema::new(vec![
868 Field::new(
869 TIME_INDEX_COLUMN,
870 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
871 false,
872 ),
873 Field::new("value", DataType::Float64, true),
874 ]));
875 let exec_input: Arc<dyn ExecutionPlan> = Arc::new(DataSourceExec::new(Arc::new(
876 MemorySourceConfig::try_new(&[], schema, None).unwrap(),
877 )));
878
879 let exec = InstantManipulate::new(
880 0,
881 0,
882 0,
883 0,
884 TIME_INDEX_COLUMN.to_string(),
885 vec!["__tsid".to_string()],
886 Some("value".to_string()),
887 LogicalPlan::EmptyRelation(EmptyRelation {
888 produce_one_row: false,
889 schema: Arc::new(datafusion::common::DFSchema::empty()),
890 }),
891 )
892 .to_execution_plan(exec_input);
893
894 assert!(format!("{exec:?}").contains("reuse_tsid_column: true"));
895 }
896
897 #[tokio::test]
898 async fn tsid_fast_path_reuses_tsid_column_when_output_grows() {
899 let schema = Arc::new(Schema::new(vec![
900 Field::new(
901 TIME_INDEX_COLUMN,
902 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
903 false,
904 ),
905 Field::new("value", DataType::Float64, true),
906 Field::new("host", DataType::Utf8, true),
907 Field::new("__tsid", DataType::UInt64, false),
908 ]));
909 let batch = RecordBatch::try_new(
910 schema.clone(),
911 vec![
912 Arc::new(TimestampMillisecondArray::from(vec![0, 1_000])),
913 Arc::new(Float64Array::from(vec![1.0, 2.0])),
914 Arc::new(datafusion::arrow::array::StringArray::from(vec![
915 "foo", "foo",
916 ])),
917 Arc::new(UInt64Array::from(vec![42, 42])),
918 ],
919 )
920 .unwrap();
921 let input = Arc::new(DataSourceExec::new(Arc::new(
922 MemorySourceConfig::try_new(&[vec![batch]], schema, None).unwrap(),
923 )));
924 let normalize_exec = Arc::new(InstantManipulateExec {
925 start: 0,
926 end: 1_500,
927 lookback_delta: 1_000,
928 interval: 500,
929 time_index_column: TIME_INDEX_COLUMN.to_string(),
930 field_column: Some("value".to_string()),
931 reuse_tsid_column: true,
932 input,
933 metric: ExecutionPlanMetricsSet::new(),
934 });
935 let session_context = SessionContext::default();
936 let result = datafusion::physical_plan::collect(normalize_exec, session_context.task_ctx())
937 .await
938 .unwrap();
939 let result_literal = datatypes::arrow::util::pretty::pretty_format_batches(&result)
940 .unwrap()
941 .to_string();
942
943 assert_eq!(
944 result_literal,
945 "+-------------------------+-------+------+--------+\
946 \n| timestamp | value | host | __tsid |\
947 \n+-------------------------+-------+------+--------+\
948 \n| 1970-01-01T00:00:00 | 1.0 | foo | 42 |\
949 \n| 1970-01-01T00:00:00.500 | 1.0 | foo | 42 |\
950 \n| 1970-01-01T00:00:01 | 2.0 | foo | 42 |\
951 \n| 1970-01-01T00:00:01.500 | 2.0 | foo | 42 |\
952 \n+-------------------------+-------+------+--------+"
953 );
954 }
955
956 #[tokio::test]
957 async fn tsid_fast_path_still_takes_additional_field_columns() {
958 let schema = Arc::new(Schema::new(vec![
959 Field::new(
960 TIME_INDEX_COLUMN,
961 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
962 false,
963 ),
964 Field::new("value", DataType::Float64, true),
965 Field::new("value_2", DataType::Float64, true),
966 Field::new("host", DataType::Utf8, true),
967 Field::new("__tsid", DataType::UInt64, false),
968 ]));
969 let batch = RecordBatch::try_new(
970 schema.clone(),
971 vec![
972 Arc::new(TimestampMillisecondArray::from(vec![0, 1_000])),
973 Arc::new(Float64Array::from(vec![1.0, 2.0])),
974 Arc::new(Float64Array::from(vec![10.0, 20.0])),
975 Arc::new(datafusion::arrow::array::StringArray::from(vec![
976 "foo", "foo",
977 ])),
978 Arc::new(UInt64Array::from(vec![42, 42])),
979 ],
980 )
981 .unwrap();
982 let input = Arc::new(DataSourceExec::new(Arc::new(
983 MemorySourceConfig::try_new(&[vec![batch]], schema, None).unwrap(),
984 )));
985 let normalize_exec = Arc::new(InstantManipulateExec {
986 start: 0,
987 end: 1_500,
988 lookback_delta: 1_000,
989 interval: 500,
990 time_index_column: TIME_INDEX_COLUMN.to_string(),
991 field_column: Some("value".to_string()),
992 reuse_tsid_column: true,
993 input,
994 metric: ExecutionPlanMetricsSet::new(),
995 });
996 let session_context = SessionContext::default();
997 let result = datafusion::physical_plan::collect(normalize_exec, session_context.task_ctx())
998 .await
999 .unwrap();
1000 let result_literal = datatypes::arrow::util::pretty::pretty_format_batches(&result)
1001 .unwrap()
1002 .to_string();
1003
1004 assert_eq!(
1005 result_literal,
1006 "+-------------------------+-------+---------+------+--------+\
1007 \n| timestamp | value | value_2 | host | __tsid |\
1008 \n+-------------------------+-------+---------+------+--------+\
1009 \n| 1970-01-01T00:00:00 | 1.0 | 10.0 | foo | 42 |\
1010 \n| 1970-01-01T00:00:00.500 | 1.0 | 10.0 | foo | 42 |\
1011 \n| 1970-01-01T00:00:01 | 2.0 | 20.0 | foo | 42 |\
1012 \n| 1970-01-01T00:00:01.500 | 2.0 | 20.0 | foo | 42 |\
1013 \n+-------------------------+-------+---------+------+--------+"
1014 );
1015 }
1016
1017 #[tokio::test]
1018 async fn manipulate_should_reject_too_many_output_points() {
1019 let schema = Arc::new(Schema::new(vec![
1020 Field::new(
1021 TIME_INDEX_COLUMN,
1022 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1023 false,
1024 ),
1025 Field::new("value", DataType::Float64, true),
1026 ]));
1027 let batch = RecordBatch::try_new(
1028 schema.clone(),
1029 vec![
1030 Arc::new(TimestampMillisecondArray::from(vec![0])),
1031 Arc::new(Float64Array::from(vec![1.0])),
1032 ],
1033 )
1034 .unwrap();
1035 let input = Arc::new(DataSourceExec::new(Arc::new(
1036 MemorySourceConfig::try_new(&[vec![batch]], schema, None).unwrap(),
1037 )));
1038 let too_many_points = MAX_INSTANT_MANIPULATE_OUTPUT_POINTS as Millisecond + 1;
1039 let normalize_exec = Arc::new(InstantManipulateExec {
1040 start: 0,
1041 end: too_many_points,
1042 lookback_delta: too_many_points + 1,
1043 interval: 1,
1044 time_index_column: TIME_INDEX_COLUMN.to_string(),
1045 field_column: Some("value".to_string()),
1046 reuse_tsid_column: false,
1047 input,
1048 metric: ExecutionPlanMetricsSet::new(),
1049 });
1050 let session_context = SessionContext::default();
1051 let err = datafusion::physical_plan::collect(normalize_exec, session_context.task_ctx())
1052 .await
1053 .unwrap_err();
1054
1055 assert!(
1056 err.to_string()
1057 .contains("InstantManipulate output points exceed limit")
1058 );
1059 }
1060
1061 #[tokio::test]
1062 async fn lookback_10s_interval_30s() {
1063 let expected = String::from(
1064 "+---------------------+-------+------+\
1065 \n| timestamp | value | path |\
1066 \n+---------------------+-------+------+\
1067 \n| 1970-01-01T00:00:00 | 1.0 | foo |\
1068 \n| 1970-01-01T00:00:30 | 1.0 | foo |\
1069 \n| 1970-01-01T00:01:00 | 1.0 | foo |\
1070 \n| 1970-01-01T00:01:30 | 1.0 | foo |\
1071 \n| 1970-01-01T00:02:00 | 1.0 | foo |\
1072 \n| 1970-01-01T00:03:00 | 1.0 | foo |\
1073 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1074 \n| 1970-01-01T00:05:00 | 1.0 | foo |\
1075 \n+---------------------+-------+------+",
1076 );
1077 do_normalize_test(0, 310_000, 10_000, 30_000, expected, false).await;
1078 }
1079
1080 #[tokio::test]
1081 async fn lookback_10s_interval_10s() {
1082 let expected = String::from(
1083 "+---------------------+-------+------+\
1084 \n| timestamp | value | path |\
1085 \n+---------------------+-------+------+\
1086 \n| 1970-01-01T00:00:00 | 1.0 | foo |\
1087 \n| 1970-01-01T00:00:30 | 1.0 | foo |\
1088 \n| 1970-01-01T00:01:00 | 1.0 | foo |\
1089 \n| 1970-01-01T00:01:30 | 1.0 | foo |\
1090 \n| 1970-01-01T00:02:00 | 1.0 | foo |\
1091 \n| 1970-01-01T00:03:00 | 1.0 | foo |\
1092 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1093 \n| 1970-01-01T00:04:10 | 1.0 | foo |\
1094 \n| 1970-01-01T00:04:40 | 1.0 | foo |\
1095 \n| 1970-01-01T00:05:00 | 1.0 | foo |\
1096 \n+---------------------+-------+------+",
1097 );
1098 do_normalize_test(0, 300_000, 10_000, 10_000, expected, false).await;
1099 }
1100
1101 #[tokio::test]
1102 async fn lookback_30s_interval_30s() {
1103 let expected = String::from(
1104 "+---------------------+-------+------+\
1105 \n| timestamp | value | path |\
1106 \n+---------------------+-------+------+\
1107 \n| 1970-01-01T00:00:00 | 1.0 | foo |\
1108 \n| 1970-01-01T00:00:30 | 1.0 | foo |\
1109 \n| 1970-01-01T00:01:00 | 1.0 | foo |\
1110 \n| 1970-01-01T00:01:30 | 1.0 | foo |\
1111 \n| 1970-01-01T00:02:00 | 1.0 | foo |\
1112 \n| 1970-01-01T00:03:00 | 1.0 | foo |\
1113 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1114 \n| 1970-01-01T00:04:30 | 1.0 | foo |\
1115 \n| 1970-01-01T00:05:00 | 1.0 | foo |\
1116 \n+---------------------+-------+------+",
1117 );
1118 do_normalize_test(0, 300_000, 30_000, 30_000, expected, false).await;
1119 }
1120
1121 #[tokio::test]
1122 async fn lookback_30s_interval_10s() {
1123 let expected = String::from(
1124 "+---------------------+-------+------+\
1125 \n| timestamp | value | path |\
1126 \n+---------------------+-------+------+\
1127 \n| 1970-01-01T00:00:00 | 1.0 | foo |\
1128 \n| 1970-01-01T00:00:10 | 1.0 | foo |\
1129 \n| 1970-01-01T00:00:20 | 1.0 | foo |\
1130 \n| 1970-01-01T00:00:30 | 1.0 | foo |\
1131 \n| 1970-01-01T00:00:40 | 1.0 | foo |\
1132 \n| 1970-01-01T00:00:50 | 1.0 | foo |\
1133 \n| 1970-01-01T00:01:00 | 1.0 | foo |\
1134 \n| 1970-01-01T00:01:10 | 1.0 | foo |\
1135 \n| 1970-01-01T00:01:20 | 1.0 | foo |\
1136 \n| 1970-01-01T00:01:30 | 1.0 | foo |\
1137 \n| 1970-01-01T00:01:40 | 1.0 | foo |\
1138 \n| 1970-01-01T00:01:50 | 1.0 | foo |\
1139 \n| 1970-01-01T00:02:00 | 1.0 | foo |\
1140 \n| 1970-01-01T00:02:10 | 1.0 | foo |\
1141 \n| 1970-01-01T00:02:20 | 1.0 | foo |\
1142 \n| 1970-01-01T00:03:00 | 1.0 | foo |\
1143 \n| 1970-01-01T00:03:10 | 1.0 | foo |\
1144 \n| 1970-01-01T00:03:20 | 1.0 | foo |\
1145 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1146 \n| 1970-01-01T00:04:10 | 1.0 | foo |\
1147 \n| 1970-01-01T00:04:20 | 1.0 | foo |\
1148 \n| 1970-01-01T00:04:30 | 1.0 | foo |\
1149 \n| 1970-01-01T00:04:40 | 1.0 | foo |\
1150 \n| 1970-01-01T00:04:50 | 1.0 | foo |\
1151 \n| 1970-01-01T00:05:00 | 1.0 | foo |\
1152 \n+---------------------+-------+------+",
1153 );
1154 do_normalize_test(0, 300_000, 30_000, 10_000, expected, false).await;
1155 }
1156
1157 #[tokio::test]
1158 async fn lookback_60s_interval_10s() {
1159 let expected = String::from(
1160 "+---------------------+-------+------+\
1161 \n| timestamp | value | path |\
1162 \n+---------------------+-------+------+\
1163 \n| 1970-01-01T00:00:00 | 1.0 | foo |\
1164 \n| 1970-01-01T00:00:10 | 1.0 | foo |\
1165 \n| 1970-01-01T00:00:20 | 1.0 | foo |\
1166 \n| 1970-01-01T00:00:30 | 1.0 | foo |\
1167 \n| 1970-01-01T00:00:40 | 1.0 | foo |\
1168 \n| 1970-01-01T00:00:50 | 1.0 | foo |\
1169 \n| 1970-01-01T00:01:00 | 1.0 | foo |\
1170 \n| 1970-01-01T00:01:10 | 1.0 | foo |\
1171 \n| 1970-01-01T00:01:20 | 1.0 | foo |\
1172 \n| 1970-01-01T00:01:30 | 1.0 | foo |\
1173 \n| 1970-01-01T00:01:40 | 1.0 | foo |\
1174 \n| 1970-01-01T00:01:50 | 1.0 | foo |\
1175 \n| 1970-01-01T00:02:00 | 1.0 | foo |\
1176 \n| 1970-01-01T00:02:10 | 1.0 | foo |\
1177 \n| 1970-01-01T00:02:20 | 1.0 | foo |\
1178 \n| 1970-01-01T00:02:30 | 1.0 | foo |\
1179 \n| 1970-01-01T00:02:40 | 1.0 | foo |\
1180 \n| 1970-01-01T00:02:50 | 1.0 | foo |\
1181 \n| 1970-01-01T00:03:00 | 1.0 | foo |\
1182 \n| 1970-01-01T00:03:10 | 1.0 | foo |\
1183 \n| 1970-01-01T00:03:20 | 1.0 | foo |\
1184 \n| 1970-01-01T00:03:30 | 1.0 | foo |\
1185 \n| 1970-01-01T00:03:40 | 1.0 | foo |\
1186 \n| 1970-01-01T00:03:50 | 1.0 | foo |\
1187 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1188 \n| 1970-01-01T00:04:10 | 1.0 | foo |\
1189 \n| 1970-01-01T00:04:20 | 1.0 | foo |\
1190 \n| 1970-01-01T00:04:30 | 1.0 | foo |\
1191 \n| 1970-01-01T00:04:40 | 1.0 | foo |\
1192 \n| 1970-01-01T00:04:50 | 1.0 | foo |\
1193 \n| 1970-01-01T00:05:00 | 1.0 | foo |\
1194 \n+---------------------+-------+------+",
1195 );
1196 do_normalize_test(0, 300_000, 60_000, 10_000, expected, false).await;
1197 }
1198
1199 #[tokio::test]
1200 async fn lookback_60s_interval_30s() {
1201 let expected = String::from(
1202 "+---------------------+-------+------+\
1203 \n| timestamp | value | path |\
1204 \n+---------------------+-------+------+\
1205 \n| 1970-01-01T00:00:00 | 1.0 | foo |\
1206 \n| 1970-01-01T00:00:30 | 1.0 | foo |\
1207 \n| 1970-01-01T00:01:00 | 1.0 | foo |\
1208 \n| 1970-01-01T00:01:30 | 1.0 | foo |\
1209 \n| 1970-01-01T00:02:00 | 1.0 | foo |\
1210 \n| 1970-01-01T00:02:30 | 1.0 | foo |\
1211 \n| 1970-01-01T00:03:00 | 1.0 | foo |\
1212 \n| 1970-01-01T00:03:30 | 1.0 | foo |\
1213 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1214 \n| 1970-01-01T00:04:30 | 1.0 | foo |\
1215 \n| 1970-01-01T00:05:00 | 1.0 | foo |\
1216 \n+---------------------+-------+------+",
1217 );
1218 do_normalize_test(0, 300_000, 60_000, 30_000, expected, false).await;
1219 }
1220
1221 #[tokio::test]
1222 async fn small_range_lookback_0s_interval_1s() {
1223 let expected = String::from(
1224 "+---------------------+-------+------+\
1225 \n| timestamp | value | path |\
1226 \n+---------------------+-------+------+\
1227 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1228 \n| 1970-01-01T00:04:01 | 1.0 | foo |\
1229 \n+---------------------+-------+------+",
1230 );
1231 do_normalize_test(230_000, 245_000, 0, 1_000, expected, false).await;
1232 }
1233
1234 #[tokio::test]
1235 async fn small_range_lookback_10s_interval_10s() {
1236 let expected = String::from(
1237 "+---------------------+-------+------+\
1238 \n| timestamp | value | path |\
1239 \n+---------------------+-------+------+\
1240 \n| 1970-01-01T00:00:00 | 1.0 | foo |\
1241 \n| 1970-01-01T00:00:30 | 1.0 | foo |\
1242 \n+---------------------+-------+------+",
1243 );
1244 do_normalize_test(0, 30_000, 10_000, 10_000, expected, false).await;
1245 }
1246
1247 #[tokio::test]
1248 async fn large_range_lookback_30s_interval_60s() {
1249 let expected = String::from(
1250 "+---------------------+-------+------+\
1251 \n| timestamp | value | path |\
1252 \n+---------------------+-------+------+\
1253 \n| 1970-01-01T00:00:00 | 1.0 | foo |\
1254 \n| 1970-01-01T00:01:00 | 1.0 | foo |\
1255 \n| 1970-01-01T00:02:00 | 1.0 | foo |\
1256 \n| 1970-01-01T00:03:00 | 1.0 | foo |\
1257 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1258 \n| 1970-01-01T00:05:00 | 1.0 | foo |\
1259 \n+---------------------+-------+------+",
1260 );
1261 do_normalize_test(-900_000, 900_000, 30_000, 60_000, expected, false).await;
1262 }
1263
1264 #[tokio::test]
1265 async fn small_range_lookback_30s_interval_30s() {
1266 let expected = String::from(
1267 "+---------------------+-------+------+\
1268 \n| timestamp | value | path |\
1269 \n+---------------------+-------+------+\
1270 \n| 1970-01-01T00:03:10 | 1.0 | foo |\
1271 \n| 1970-01-01T00:03:20 | 1.0 | foo |\
1272 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1273 \n| 1970-01-01T00:04:10 | 1.0 | foo |\
1274 \n| 1970-01-01T00:04:20 | 1.0 | foo |\
1275 \n| 1970-01-01T00:04:30 | 1.0 | foo |\
1276 \n| 1970-01-01T00:04:40 | 1.0 | foo |\
1277 \n| 1970-01-01T00:04:50 | 1.0 | foo |\
1278 \n| 1970-01-01T00:05:00 | 1.0 | foo |\
1279 \n+---------------------+-------+------+",
1280 );
1281 do_normalize_test(190_000, 300_000, 30_000, 10_000, expected, false).await;
1282 }
1283
1284 #[tokio::test]
1285 async fn lookback_10s_interval_10s_with_stale_marker() {
1286 let expected = String::from(
1287 "+---------------------+-------+\
1288 \n| timestamp | value |\
1289 \n+---------------------+-------+\
1290 \n| 1970-01-01T00:00:00 | 0.0 |\
1291 \n| 1970-01-01T00:01:00 | 6.0 |\
1292 \n| 1970-01-01T00:02:00 | 12.0 |\
1293 \n+---------------------+-------+",
1294 );
1295 do_normalize_test(0, 300_000, 10_000, 10_000, expected, true).await;
1296 }
1297
1298 #[tokio::test]
1299 async fn lookback_10s_interval_10s_with_stale_marker_unaligned() {
1300 let expected = String::from(
1301 "+-------------------------+-------+\
1302 \n| timestamp | value |\
1303 \n+-------------------------+-------+\
1304 \n| 1970-01-01T00:00:00.001 | 0.0 |\
1305 \n| 1970-01-01T00:01:00.001 | 6.0 |\
1306 \n| 1970-01-01T00:02:00.001 | 12.0 |\
1307 \n+-------------------------+-------+",
1308 );
1309 do_normalize_test(1, 300_001, 10_000, 10_000, expected, true).await;
1310 }
1311
1312 #[tokio::test]
1313 async fn ultra_large_range() {
1314 let expected = String::from(
1315 "+-------------------------+-------+\
1316 \n| timestamp | value |\
1317 \n+-------------------------+-------+\
1318 \n| 1970-01-01T00:00:00.001 | 0.0 |\
1319 \n| 1970-01-01T00:01:00.001 | 6.0 |\
1320 \n| 1970-01-01T00:02:00.001 | 12.0 |\
1321 \n+-------------------------+-------+",
1322 );
1323 do_normalize_test(
1324 -900_000_000_000_000 + 1,
1325 900_000_000_000_000,
1326 10_000,
1327 10_000,
1328 expected,
1329 true,
1330 )
1331 .await;
1332 }
1333
1334 #[tokio::test]
1335 async fn ordinary_nan_is_selected_for_exact_and_lookback() {
1336 let schema = Arc::new(Schema::new(vec![
1337 Field::new(
1338 TIME_INDEX_COLUMN,
1339 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1340 false,
1341 ),
1342 Field::new("value", DataType::Float64, true),
1343 ]));
1344 let batch = RecordBatch::try_new(
1345 schema.clone(),
1346 vec![
1347 Arc::new(TimestampMillisecondArray::from(vec![1_000])),
1348 Arc::new(Float64Array::from(vec![f64::NAN])),
1349 ],
1350 )
1351 .unwrap();
1352 let input = Arc::new(DataSourceExec::new(Arc::new(
1353 MemorySourceConfig::try_new(&[vec![batch]], schema, None).unwrap(),
1354 )));
1355 let exec = Arc::new(InstantManipulateExec {
1356 start: 1_000,
1357 end: 1_500,
1358 lookback_delta: 1_000,
1359 interval: 500,
1360 time_index_column: TIME_INDEX_COLUMN.to_string(),
1361 field_column: Some("value".to_string()),
1362 reuse_tsid_column: false,
1363 input,
1364 metric: ExecutionPlanMetricsSet::new(),
1365 });
1366
1367 let context = SessionContext::default();
1368 let batches = datafusion::physical_plan::collect(exec, context.task_ctx())
1369 .await
1370 .unwrap();
1371 let values = batches
1372 .iter()
1373 .flat_map(|batch| {
1374 batch
1375 .column(1)
1376 .as_any()
1377 .downcast_ref::<Float64Array>()
1378 .unwrap()
1379 .values()
1380 .iter()
1381 .copied()
1382 })
1383 .collect::<Vec<_>>();
1384 let timestamps = batches
1385 .iter()
1386 .flat_map(|batch| {
1387 batch
1388 .column(0)
1389 .as_any()
1390 .downcast_ref::<TimestampMillisecondArray>()
1391 .unwrap()
1392 .values()
1393 .iter()
1394 .copied()
1395 })
1396 .collect::<Vec<_>>();
1397
1398 assert_eq!(values.len(), 2);
1399 assert_eq!(timestamps, vec![1_000, 1_500]);
1400 assert!(values.iter().all(|value| value.is_nan()));
1401 assert_eq!(
1402 values
1403 .iter()
1404 .map(|value| value.to_bits())
1405 .collect::<Vec<_>>(),
1406 vec![f64::NAN.to_bits(); 2]
1407 );
1408 }
1409
1410 #[tokio::test]
1411 async fn prometheus_stale_nan_selects_before_and_suppresses_after_marker() {
1412 let schema = Arc::new(Schema::new(vec![
1413 Field::new(
1414 TIME_INDEX_COLUMN,
1415 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1416 false,
1417 ),
1418 Field::new("value", DataType::Float64, true),
1419 ]));
1420 let batch = RecordBatch::try_new(
1421 schema.clone(),
1422 vec![
1423 Arc::new(TimestampMillisecondArray::from(vec![500, 1_000])),
1424 Arc::new(Float64Array::from(vec![
1425 42.0,
1426 f64::from_bits(0x7ff0_0000_0000_0002),
1427 ])),
1428 ],
1429 )
1430 .unwrap();
1431 let input = Arc::new(DataSourceExec::new(Arc::new(
1432 MemorySourceConfig::try_new(&[vec![batch]], schema, None).unwrap(),
1433 )));
1434 let exec = Arc::new(InstantManipulateExec {
1435 start: 750,
1436 end: 1_500,
1437 lookback_delta: 1_001,
1438 interval: 250,
1439 time_index_column: TIME_INDEX_COLUMN.to_string(),
1440 field_column: Some("value".to_string()),
1441 reuse_tsid_column: false,
1442 input,
1443 metric: ExecutionPlanMetricsSet::new(),
1444 });
1445
1446 let context = SessionContext::default();
1447 let batches = datafusion::physical_plan::collect(exec, context.task_ctx())
1448 .await
1449 .unwrap();
1450
1451 let row_count = batches.iter().map(RecordBatch::num_rows).sum::<usize>();
1452 let batch = batches.iter().find(|batch| batch.num_rows() > 0).unwrap();
1453 let timestamp = batch
1454 .column(0)
1455 .as_any()
1456 .downcast_ref::<TimestampMillisecondArray>()
1457 .unwrap()
1458 .value(0);
1459 let value = batch
1460 .column(1)
1461 .as_any()
1462 .downcast_ref::<Float64Array>()
1463 .unwrap()
1464 .value(0);
1465 assert_eq!(
1466 (row_count, timestamp, value),
1467 (1, 750, 42.0),
1468 "only the evaluation before the stale marker should select 42.0"
1469 );
1470 }
1471
1472 #[tokio::test]
1473 async fn native_histogram_stale_nan_suppresses_exact_and_lookback() {
1474 let histograms = build_histogram_array(&[
1475 Some(native_histogram(42.0)),
1476 Some(native_histogram(f64::from_bits(PROMETHEUS_STALE_NAN_BITS))),
1477 ]);
1478 let schema = Arc::new(Schema::new(vec![
1479 Field::new(
1480 TIME_INDEX_COLUMN,
1481 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1482 false,
1483 ),
1484 Field::new("value", histograms.data_type().clone(), true),
1485 ]));
1486 let batch = RecordBatch::try_new(
1487 schema.clone(),
1488 vec![
1489 Arc::new(TimestampMillisecondArray::from(vec![500, 1_000])),
1490 histograms,
1491 ],
1492 )
1493 .unwrap();
1494 let input = Arc::new(DataSourceExec::new(Arc::new(
1495 MemorySourceConfig::try_new(&[vec![batch]], schema, None).unwrap(),
1496 )));
1497 let exec = Arc::new(InstantManipulateExec {
1498 start: 1_000,
1499 end: 1_500,
1500 lookback_delta: 1_001,
1501 interval: 500,
1502 time_index_column: TIME_INDEX_COLUMN.to_string(),
1503 field_column: Some("value".to_string()),
1504 reuse_tsid_column: false,
1505 input,
1506 metric: ExecutionPlanMetricsSet::new(),
1507 });
1508
1509 let context = SessionContext::default();
1510 let batches = datafusion::physical_plan::collect(exec, context.task_ctx())
1511 .await
1512 .unwrap();
1513
1514 assert_eq!(batches.iter().map(RecordBatch::num_rows).sum::<usize>(), 0);
1515 }
1516
1517 #[tokio::test]
1518 async fn null_value_backed_by_stale_bits_is_selected_for_exact_and_lookback() {
1519 let schema = Arc::new(Schema::new(vec![
1520 Field::new(
1521 TIME_INDEX_COLUMN,
1522 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1523 false,
1524 ),
1525 Field::new("value", DataType::Float64, true),
1526 ]));
1527 let field_column = Float64Array::new(
1528 vec![f64::from_bits(0x7ff0_0000_0000_0002)].into(),
1529 Some(NullBuffer::from(vec![false])),
1530 );
1531 assert!(!field_column.is_valid(0));
1532 assert_eq!(field_column.value(0).to_bits(), 0x7ff0_0000_0000_0002);
1533 let batch = RecordBatch::try_new(
1534 schema.clone(),
1535 vec![
1536 Arc::new(TimestampMillisecondArray::from(vec![1_000])),
1537 Arc::new(field_column),
1538 ],
1539 )
1540 .unwrap();
1541 let input = Arc::new(DataSourceExec::new(Arc::new(
1542 MemorySourceConfig::try_new(&[vec![batch]], schema, None).unwrap(),
1543 )));
1544 let exec = Arc::new(InstantManipulateExec {
1545 start: 1_000,
1546 end: 1_500,
1547 lookback_delta: 1_000,
1548 interval: 500,
1549 time_index_column: TIME_INDEX_COLUMN.to_string(),
1550 field_column: Some("value".to_string()),
1551 reuse_tsid_column: false,
1552 input,
1553 metric: ExecutionPlanMetricsSet::new(),
1554 });
1555
1556 let context = SessionContext::default();
1557 let batches = datafusion::physical_plan::collect(exec, context.task_ctx())
1558 .await
1559 .unwrap();
1560 assert_eq!(batches.iter().map(RecordBatch::num_rows).sum::<usize>(), 2);
1561 let batch = batches.iter().find(|batch| batch.num_rows() == 2).unwrap();
1562 let timestamps = batch
1563 .column(0)
1564 .as_any()
1565 .downcast_ref::<TimestampMillisecondArray>()
1566 .unwrap();
1567 let values = batch
1568 .column(1)
1569 .as_any()
1570 .downcast_ref::<Float64Array>()
1571 .unwrap();
1572
1573 assert_eq!(timestamps.values(), &[1_000, 1_500]);
1574 assert!(!values.is_valid(0));
1575 assert!(!values.is_valid(1));
1576 }
1577}