1use std::pin::Pin;
16use std::sync::Arc;
17use std::task::{Context, Poll};
18
19use common_query::prelude::{greptime_native_histogram, greptime_value};
20use datafusion::arrow::array::{Array, TimestampMillisecondArray, UInt64Array};
21use datafusion::arrow::datatypes::{DataType, SchemaRef};
22use datafusion::arrow::record_batch::RecordBatch;
23use datafusion::common::stats::Precision;
24use datafusion::common::tree_node::TreeNodeRecursion;
25use datafusion::common::{DFSchema, DFSchemaRef, ScalarValue};
26use datafusion::error::{DataFusionError, Result as DataFusionResult};
27use datafusion::execution::context::TaskContext;
28use datafusion::logical_expr::{
29 EmptyRelation, Expr, Extension, LogicalPlan, UserDefinedLogicalNodeCore,
30};
31use datafusion::physical_expr::EquivalenceProperties;
32use datafusion::physical_plan::metrics::{
33 BaselineMetrics, Count, ExecutionPlanMetricsSet, MetricBuilder, MetricValue, MetricsSet,
34};
35use datafusion::physical_plan::{
36 ChildStats, DisplayAs, DisplayFormatType, ExecutionPlan, InputDistributionRequirements,
37 PhysicalExpr, PlanProperties, RecordBatchStream, SendableRecordBatchStream, Statistics,
38 StatisticsArgs,
39};
40use datafusion_expr::col;
41use datatypes::arrow::compute;
42use datatypes::timestamp::timestamp_array_to_primitive;
43use futures::{Stream, StreamExt, ready};
44use greptime_proto::substrait_extension as pb;
45use prost::Message;
46use snafu::ResultExt;
47
48use crate::error::{DeserializeSnafu, Result};
49use crate::extension_plan::series_divide::SeriesDivide;
50use crate::extension_plan::{
51 METRIC_NUM_SERIES, Millisecond, is_prometheus_stale_sample, local_offset,
52 nanoseconds_per_native_tick, prometheus_stale_sample_column, resolve_column_name,
53 serialize_column_index, timestamp_unit,
54};
55use crate::metrics::PROMQL_SERIES_COUNT;
56
57const MAX_INSTANT_MANIPULATE_OUTPUT_POINTS: usize = 1_000_000;
58
59fn mixed_sample_fields(field: Option<&str>) -> [Option<&str>; 2] {
60 let companion = match field {
61 Some(field) if field == greptime_value() => Some(greptime_native_histogram()),
62 Some(field) if field == greptime_native_histogram() => Some(greptime_value()),
63 _ => None,
64 };
65 [field, companion]
66}
67
68#[derive(Debug, PartialEq, Eq, Hash)]
74pub struct InstantManipulate {
75 start: Millisecond,
76 end: Millisecond,
77 lookback_delta: Millisecond,
78 interval: Millisecond,
79 offset: Millisecond,
80 time_index_column: String,
81 tag_columns: Vec<String>,
83 field_column: Option<String>,
85 input: LogicalPlan,
86 output_schema: DFSchemaRef,
87 unfix: Option<UnfixIndices>,
88}
89
90impl PartialOrd for InstantManipulate {
91 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
92 (
93 self.start,
94 self.end,
95 self.lookback_delta,
96 self.interval,
97 self.offset,
98 &self.time_index_column,
99 &self.tag_columns,
100 &self.field_column,
101 &self.input,
102 &self.unfix,
103 )
104 .partial_cmp(&(
105 other.start,
106 other.end,
107 other.lookback_delta,
108 other.interval,
109 other.offset,
110 &other.time_index_column,
111 &other.tag_columns,
112 &other.field_column,
113 &other.input,
114 &other.unfix,
115 ))
116 }
117}
118
119#[derive(Debug, PartialEq, Eq, Hash, PartialOrd)]
120struct UnfixIndices {
121 pub time_index_idx: u64,
122 pub field_index_idx: u64,
123}
124
125impl UserDefinedLogicalNodeCore for InstantManipulate {
126 fn name(&self) -> &str {
127 Self::name()
128 }
129
130 fn inputs(&self) -> Vec<&LogicalPlan> {
131 vec![&self.input]
132 }
133
134 fn schema(&self) -> &DFSchemaRef {
135 &self.output_schema
136 }
137
138 fn expressions(&self) -> Vec<Expr> {
139 if self.unfix.is_some() {
140 return vec![];
141 }
142
143 let mut exprs = vec![col(&self.time_index_column)];
144 exprs.extend(self.staleness_field_columns().map(col));
145 exprs
146 }
147
148 fn necessary_children_exprs(&self, output_columns: &[usize]) -> Option<Vec<Vec<usize>>> {
149 if self.unfix.is_some() {
150 return None;
151 }
152
153 let input_schema = self.input.schema();
154 if output_columns.is_empty() {
155 let indices = (0..input_schema.fields().len()).collect::<Vec<_>>();
156 return Some(vec![indices]);
157 }
158
159 let mut required = output_columns.to_vec();
160 required.push(input_schema.index_of_column_by_name(None, &self.time_index_column)?);
161 for field in self.staleness_field_columns() {
162 required.push(input_schema.index_of_column_by_name(None, field)?);
163 }
164
165 required.sort_unstable();
166 required.dedup();
167 Some(vec![required])
168 }
169
170 fn fmt_for_explain(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
171 write!(
172 f,
173 "PromInstantManipulate: range=[{}..{}], lookback=[{}], interval=[{}], time index=[{}]",
174 self.start, self.end, self.lookback_delta, self.interval, self.time_index_column
175 )
176 }
177
178 fn with_exprs_and_inputs(
179 &self,
180 _exprs: Vec<Expr>,
181 inputs: Vec<LogicalPlan>,
182 ) -> DataFusionResult<Self> {
183 if inputs.len() != 1 {
184 return Err(DataFusionError::Internal(
185 "InstantManipulate should have exact one input".to_string(),
186 ));
187 }
188
189 let input: LogicalPlan = inputs.into_iter().next().unwrap();
190 let input_schema = input.schema();
191
192 if let Some(unfix) = &self.unfix {
193 let time_index_column = resolve_column_name(
195 unfix.time_index_idx,
196 input_schema,
197 "InstantManipulate",
198 "time index",
199 )?;
200
201 let field_column = if unfix.field_index_idx == u64::MAX {
202 None
203 } else {
204 Some(resolve_column_name(
205 unfix.field_index_idx,
206 input_schema,
207 "InstantManipulate",
208 "field",
209 )?)
210 };
211
212 Ok(Self {
213 start: self.start,
214 end: self.end,
215 lookback_delta: self.lookback_delta,
216 interval: self.interval,
217 offset: local_offset(&input, &time_index_column),
218 output_schema: Self::calculate_output_schema(&input, &time_index_column)?,
219 time_index_column,
220 tag_columns: Self::resolve_tag_columns(&input, &self.tag_columns),
221 field_column,
222 input,
223 unfix: None,
224 })
225 } else {
226 Ok(Self {
227 start: self.start,
228 end: self.end,
229 lookback_delta: self.lookback_delta,
230 interval: self.interval,
231 offset: self.offset,
232 time_index_column: self.time_index_column.clone(),
233 tag_columns: Self::resolve_tag_columns(&input, &self.tag_columns),
234 field_column: self.field_column.clone(),
235 output_schema: Self::calculate_output_schema(&input, &self.time_index_column)?,
236 input,
237 unfix: None,
238 })
239 }
240 }
241}
242
243impl InstantManipulate {
244 fn calculate_output_schema(
245 input: &LogicalPlan,
246 time_index_column: &str,
247 ) -> DataFusionResult<DFSchemaRef> {
248 let input_schema = input.schema();
249 let time_index = input_schema
250 .index_of_column_by_name(None, time_index_column)
251 .ok_or_else(|| {
252 DataFusionError::Internal(format!(
253 "InstantManipulate time index {time_index_column} not found"
254 ))
255 })?;
256 let mut fields = (0..input_schema.fields().len())
257 .map(|index| {
258 let (qualifier, field) = input_schema.qualified_field(index);
259 (qualifier.cloned(), field.clone())
260 })
261 .collect::<Vec<_>>();
262 let (qualifier, field) = input_schema.qualified_field(time_index);
263 fields[time_index] = (
264 qualifier.cloned(),
265 Arc::new(field.as_ref().clone().with_data_type(DataType::Timestamp(
266 datafusion::arrow::datatypes::TimeUnit::Millisecond,
267 None,
268 ))),
269 );
270 Ok(Arc::new(DFSchema::new_with_metadata(
271 fields,
272 input_schema.metadata().clone(),
273 )?))
274 }
275
276 #[allow(clippy::too_many_arguments)]
277 pub fn new(
278 start: Millisecond,
279 end: Millisecond,
280 lookback_delta: Millisecond,
281 interval: Millisecond,
282 offset: Millisecond,
283 time_index_column: String,
284 tag_columns: Vec<String>,
285 field_column: Option<String>,
286 input: LogicalPlan,
287 ) -> Self {
288 Self {
289 start,
290 end,
291 lookback_delta,
292 interval,
293 offset,
294 output_schema: Self::calculate_output_schema(&input, &time_index_column)
295 .unwrap_or_else(|_| input.schema().clone()),
296 time_index_column,
297 tag_columns,
298 field_column,
299 input,
300 unfix: None,
301 }
302 }
303
304 pub const fn name() -> &'static str {
305 "InstantManipulate"
306 }
307
308 pub fn is_single_evaluation(&self) -> bool {
310 self.start == self.end
311 }
312
313 fn staleness_field_columns(&self) -> impl Iterator<Item = &str> {
314 let [field, companion] = mixed_sample_fields(self.field_column.as_deref());
315 [
316 field,
317 companion.filter(|companion| {
318 self.input
319 .schema()
320 .index_of_column_by_name(None, companion)
321 .is_some()
322 }),
323 ]
324 .into_iter()
325 .flatten()
326 }
327
328 fn resolve_tag_columns(input: &LogicalPlan, tag_columns: &[String]) -> Vec<String> {
329 if !tag_columns.is_empty() {
330 return tag_columns.to_vec();
331 }
332
333 Self::find_series_divide_tags(input).unwrap_or_default()
334 }
335
336 fn find_series_divide_tags(plan: &LogicalPlan) -> Option<Vec<String>> {
337 if let LogicalPlan::Extension(Extension { node }) = plan
338 && let Some(series_divide) = node.as_any().downcast_ref::<SeriesDivide>()
339 {
340 return Some(series_divide.tags().to_vec());
341 }
342
343 plan.inputs()
344 .into_iter()
345 .find_map(Self::find_series_divide_tags)
346 }
347
348 pub fn to_execution_plan(&self, exec_input: Arc<dyn ExecutionPlan>) -> Arc<dyn ExecutionPlan> {
349 let reuse_tsid_column = matches!(self.tag_columns.as_slice(), [tag] if tag == "__tsid");
350
351 let mut fields = exec_input.schema().fields().to_vec();
352 let time_index = exec_input
353 .schema()
354 .index_of(&self.time_index_column)
355 .expect("time index column not found");
356 fields[time_index] = Arc::new(fields[time_index].as_ref().clone().with_data_type(
357 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
358 ));
359 let output_schema = Arc::new(datafusion::arrow::datatypes::Schema::new_with_metadata(
360 fields,
361 exec_input.schema().metadata().clone(),
362 ));
363 let input_properties = exec_input.properties();
364 let properties = Arc::new(PlanProperties::new(
365 EquivalenceProperties::new(output_schema.clone()),
366 input_properties.partitioning.clone(),
367 input_properties.emission_type,
368 input_properties.boundedness,
369 ));
370 Arc::new(InstantManipulateExec {
371 offset: self.offset,
372 start: self.start,
373 end: self.end,
374 lookback_delta: self.lookback_delta,
375 interval: self.interval,
376 time_index_column: self.time_index_column.clone(),
377 field_column: self.field_column.clone(),
378 reuse_tsid_column,
379 input: exec_input,
380 output_schema,
381 properties,
382 metric: ExecutionPlanMetricsSet::new(),
383 })
384 }
385
386 pub fn serialize(&self) -> Vec<u8> {
387 let time_index_idx = serialize_column_index(self.input.schema(), &self.time_index_column);
388
389 let field_index_idx = self
390 .field_column
391 .as_ref()
392 .map(|name| serialize_column_index(self.input.schema(), name))
393 .unwrap_or(u64::MAX);
394
395 pb::InstantManipulate {
396 start: self.start,
397 end: self.end,
398 interval: self.interval,
399 lookback_delta: self.lookback_delta,
400 time_index_idx,
401 field_index_idx,
402 ..Default::default()
403 }
404 .encode_to_vec()
405 }
406
407 pub fn deserialize(bytes: &[u8]) -> Result<Self> {
408 let pb_instant_manipulate =
409 pb::InstantManipulate::decode(bytes).context(DeserializeSnafu)?;
410 let empty_schema = Arc::new(DFSchema::empty());
411 let placeholder_plan = LogicalPlan::EmptyRelation(EmptyRelation {
412 produce_one_row: false,
413 schema: empty_schema.clone(),
414 });
415
416 let unfix = UnfixIndices {
417 time_index_idx: pb_instant_manipulate.time_index_idx,
418 field_index_idx: pb_instant_manipulate.field_index_idx,
419 };
420
421 Ok(Self {
422 start: pb_instant_manipulate.start,
423 end: pb_instant_manipulate.end,
424 lookback_delta: pb_instant_manipulate.lookback_delta,
425 interval: pb_instant_manipulate.interval,
426 offset: 0,
427 time_index_column: String::new(),
428 tag_columns: Vec::new(),
429 field_column: None,
430 output_schema: empty_schema,
431 input: placeholder_plan,
432 unfix: Some(unfix),
433 })
434 }
435}
436
437#[derive(Debug)]
438pub struct InstantManipulateExec {
439 offset: Millisecond,
440 start: Millisecond,
441 end: Millisecond,
442 lookback_delta: Millisecond,
443 interval: Millisecond,
444 time_index_column: String,
445 field_column: Option<String>,
446 reuse_tsid_column: bool,
447
448 input: Arc<dyn ExecutionPlan>,
449 output_schema: SchemaRef,
450 properties: Arc<PlanProperties>,
451 metric: ExecutionPlanMetricsSet,
452}
453
454impl ExecutionPlan for InstantManipulateExec {
455 fn apply_expressions(
456 &self,
457 _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> datafusion_common::Result<TreeNodeRecursion>,
458 ) -> DataFusionResult<TreeNodeRecursion> {
459 Ok(TreeNodeRecursion::Continue)
460 }
461
462 fn schema(&self) -> SchemaRef {
463 self.output_schema.clone()
464 }
465
466 fn properties(&self) -> &Arc<PlanProperties> {
467 &self.properties
468 }
469
470 fn input_distribution_requirements(&self) -> InputDistributionRequirements {
471 self.input.input_distribution_requirements()
472 }
473
474 fn maintains_input_order(&self) -> Vec<bool> {
476 vec![false; self.children().len()]
477 }
478
479 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
480 vec![&self.input]
481 }
482
483 fn with_new_children(
484 self: Arc<Self>,
485 children: Vec<Arc<dyn ExecutionPlan>>,
486 ) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
487 assert!(!children.is_empty());
488 let input = children[0].clone();
489 let input_properties = input.properties();
492 let properties = Arc::new(PlanProperties::new(
493 EquivalenceProperties::new(self.output_schema.clone()),
494 input_properties.partitioning.clone(),
495 input_properties.emission_type,
496 input_properties.boundedness,
497 ));
498 Ok(Arc::new(Self {
499 offset: self.offset,
500 start: self.start,
501 end: self.end,
502 lookback_delta: self.lookback_delta,
503 interval: self.interval,
504 time_index_column: self.time_index_column.clone(),
505 field_column: self.field_column.clone(),
506 reuse_tsid_column: self.reuse_tsid_column,
507 input,
508 output_schema: self.output_schema.clone(),
509 properties,
510 metric: self.metric.clone(),
511 }))
512 }
513
514 fn execute(
515 &self,
516 partition: usize,
517 context: Arc<TaskContext>,
518 ) -> DataFusionResult<SendableRecordBatchStream> {
519 let baseline_metric = BaselineMetrics::new(&self.metric, partition);
520 let num_series = Count::new();
521 MetricBuilder::new(&self.metric)
522 .with_partition(partition)
523 .build(MetricValue::Count {
524 name: METRIC_NUM_SERIES.into(),
525 count: num_series.clone(),
526 });
527
528 let input = self.input.execute(partition, context)?;
529 let schema = input.schema();
530 let time_index = schema
531 .column_with_name(&self.time_index_column)
532 .expect("time index column not found")
533 .0;
534 let time_unit = timestamp_unit(schema.field(time_index).data_type())?;
535 let field_indices = mixed_sample_fields(self.field_column.as_deref()).map(|field| {
536 field.and_then(|field| schema.column_with_name(field).map(|(index, _)| index))
537 });
538 let tsid_index = schema
539 .column_with_name("__tsid")
540 .filter(|(_, field)| field.data_type() == &DataType::UInt64)
541 .map(|(index, _)| index);
542 Ok(Box::pin(InstantManipulateStream {
543 offset: self.offset,
544 start: self.start,
545 end: self.end,
546 lookback_delta: self.lookback_delta,
547 interval: self.interval,
548 time_index,
549 time_unit,
550 field_indices,
551 tsid_index,
552 reuse_tsid_column: self.reuse_tsid_column && tsid_index.is_some(),
553 schema: self.output_schema.clone(),
554 input,
555 metric: baseline_metric,
556 num_series,
557 }))
558 }
559
560 fn metrics(&self) -> Option<MetricsSet> {
561 Some(self.metric.clone_inner())
562 }
563
564 fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
565 vec![ChildStats::At(partition)]
566 }
567
568 fn statistics_from_inputs(
569 &self,
570 input_stats: &[Arc<Statistics>],
571 _args: &StatisticsArgs,
572 ) -> DataFusionResult<Arc<Statistics>> {
573 let input_stats = &input_stats[0];
574
575 let estimated_row_num = (self.end - self.start) as f64 / self.interval as f64;
576 let estimated_total_bytes = input_stats
577 .total_byte_size
578 .get_value()
579 .zip(input_stats.num_rows.get_value())
580 .map(|(size, rows)| {
581 Precision::Inexact(((*size as f64 / *rows as f64) * estimated_row_num).floor() as _)
582 })
583 .unwrap_or(Precision::Absent);
584
585 Ok(Arc::new(Statistics {
586 num_rows: Precision::Inexact(estimated_row_num.floor() as _),
587 total_byte_size: estimated_total_bytes,
588 column_statistics: Statistics::unknown_column(&self.schema()),
590 }))
591 }
592
593 fn name(&self) -> &str {
594 "InstantManipulateExec"
595 }
596}
597
598impl DisplayAs for InstantManipulateExec {
599 fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
600 match t {
601 DisplayFormatType::Default
602 | DisplayFormatType::Verbose
603 | DisplayFormatType::TreeRender => {
604 write!(
605 f,
606 "PromInstantManipulateExec: range=[{}..{}], lookback=[{}], interval=[{}], time index=[{}]",
607 self.start,
608 self.end,
609 self.lookback_delta,
610 self.interval,
611 self.time_index_column
612 )
613 }
614 }
615 }
616}
617
618pub struct InstantManipulateStream {
619 offset: Millisecond,
620 start: Millisecond,
621 end: Millisecond,
622 lookback_delta: Millisecond,
623 interval: Millisecond,
624 time_index: usize,
626 time_unit: datafusion::arrow::datatypes::TimeUnit,
627 field_indices: [Option<usize>; 2],
628 tsid_index: Option<usize>,
629 reuse_tsid_column: bool,
630
631 schema: SchemaRef,
632 input: SendableRecordBatchStream,
633 metric: BaselineMetrics,
634 num_series: Count,
636}
637
638impl RecordBatchStream for InstantManipulateStream {
639 fn schema(&self) -> SchemaRef {
640 self.schema.clone()
641 }
642}
643
644impl Stream for InstantManipulateStream {
645 type Item = DataFusionResult<RecordBatch>;
646
647 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
648 let poll = match ready!(self.input.poll_next_unpin(cx)) {
649 Some(Ok(batch)) => {
650 let timer = std::time::Instant::now();
651 if batch.num_rows() != 0 {
652 self.num_series.add(1);
653 }
654 let result = Ok(batch).and_then(|batch| self.manipulate(batch));
655 self.metric.elapsed_compute().add_elapsed(timer);
656 Poll::Ready(Some(result))
657 }
658 None => {
659 PROMQL_SERIES_COUNT.observe(self.num_series.value() as f64);
660 Poll::Ready(None)
661 }
662 Some(Err(e)) => Poll::Ready(Some(Err(e))),
663 };
664 self.metric.record_poll(poll)
665 }
666}
667
668impl InstantManipulateStream {
669 pub fn manipulate(&self, input: RecordBatch) -> DataFusionResult<RecordBatch> {
676 let ts_column = input.column(self.time_index);
677 if ts_column.is_empty() {
678 return Ok(RecordBatch::new_empty(self.schema.clone()));
680 }
681 let scale = nanoseconds_per_native_tick(self.time_unit);
682 let stale_sample_columns = self.field_indices.map(|index| {
683 index.and_then(|index| prometheus_stale_sample_column(input.column(index).as_ref()))
684 });
685 let is_stale = |row| {
686 stale_sample_columns
687 .iter()
688 .flatten()
689 .any(|column| is_prometheus_stale_sample(*column, row))
690 };
691 let (timestamps, _) = timestamp_array_to_primitive(ts_column).ok_or_else(|| {
692 DataFusionError::Execution("Time index column is not a timestamp".into())
693 })?;
694 let timestamps = timestamps.values();
695 let len = timestamps.len();
696 let to_nanoseconds =
700 |timestamp: i64| (timestamp as i128) * scale + (self.offset as i128) * 1_000_000;
701 let first_ns = to_nanoseconds(timestamps[0]);
702 let last_ns = to_nanoseconds(timestamps[len - 1]);
703 let last_useful = if self.lookback_delta == 0 {
706 last_ns
707 } else {
708 last_ns + (self.lookback_delta as i128) * 1_000_000 - 1
709 };
710 let first_ms = (first_ns + 999_999).div_euclid(1_000_000);
711 let last_ms = last_useful.div_euclid(1_000_000);
712 let query_start = self.start as i128;
713 let query_end = self.end as i128;
714 let interval = self.interval as i128;
715 let max_start = first_ms.max(query_start);
716 let min_end = last_ms.min(query_end);
717 let (aligned_start, aligned_end) = if max_start > min_end {
718 (1, 0)
719 } else {
720 (
721 query_start + (max_start - query_start) / interval * interval,
722 query_end - (query_end - min_end) / interval * interval,
723 )
724 };
725 let estimated_points = if aligned_end >= aligned_start {
726 (aligned_end - aligned_start) / interval + 1
727 } else {
728 0
729 };
730 if estimated_points > MAX_INSTANT_MANIPULATE_OUTPUT_POINTS as i128 {
731 return Err(DataFusionError::Execution(format!(
732 "InstantManipulate output points exceed limit: {estimated_points} > {MAX_INSTANT_MANIPULATE_OUTPUT_POINTS}"
733 )));
734 }
735 let estimated_points = estimated_points as usize;
736 let aligned_start = aligned_start as i64;
737 let aligned_end = aligned_end as i64;
738 let mut take_indices = Vec::with_capacity(estimated_points);
739 let mut aligned_ts = Vec::with_capacity(estimated_points);
740 let mut cursor = 0;
741 for expected_ms in (aligned_start..=aligned_end).step_by(self.interval as usize) {
742 let expected = (expected_ms as i128) * 1_000_000;
743 let mut exact_candidate = None;
744 while cursor < len && to_nanoseconds(timestamps[cursor]) <= expected {
745 if to_nanoseconds(timestamps[cursor]) == expected && exact_candidate.is_none() {
746 exact_candidate = Some(cursor);
747 }
748 cursor += 1;
749 }
750 let Some(candidate) = exact_candidate.or_else(|| cursor.checked_sub(1)) else {
755 continue;
756 };
757 let candidate_ts = to_nanoseconds(timestamps[candidate]);
758 let lower = expected - (self.lookback_delta as i128) * 1_000_000;
759 if (candidate_ts == expected || candidate_ts > lower)
760 && candidate_ts <= expected
761 && !is_stale(candidate)
762 {
763 take_indices.push(candidate as u64);
764 aligned_ts.push(expected_ms);
765 }
766 }
767 self.take_record_batch_optional(input, take_indices, aligned_ts)
768 }
769
770 fn take_record_batch_optional(
772 &self,
773 record_batch: RecordBatch,
774 take_indices: Vec<u64>,
775 aligned_ts: Vec<Millisecond>,
776 ) -> DataFusionResult<RecordBatch> {
777 assert_eq!(take_indices.len(), aligned_ts.len());
778
779 let output_len = aligned_ts.len();
780 let mut indices_array = None;
781 let mut arrays = Vec::with_capacity(record_batch.num_columns());
782 let aligned_ts = Arc::new(TimestampMillisecondArray::from(aligned_ts)) as Arc<dyn Array>;
783
784 for (index, array) in record_batch.columns().iter().enumerate() {
785 if index == self.time_index {
786 arrays.push(aligned_ts.clone());
787 continue;
788 }
789
790 if self.reuse_tsid_column && self.tsid_index == Some(index) {
791 arrays.push(reuse_constant_column(array, output_len)?);
792 continue;
793 }
794
795 let indices_array =
796 indices_array.get_or_insert_with(|| UInt64Array::from(take_indices.clone()));
797 arrays.push(compute::take(array, indices_array, None)?);
798 }
799
800 let result = RecordBatch::try_new(self.schema.clone(), arrays)
801 .map_err(|e| DataFusionError::ArrowError(Box::new(e), None))?;
802 Ok(result)
803 }
804}
805
806fn reuse_constant_column(array: &Arc<dyn Array>, len: usize) -> DataFusionResult<Arc<dyn Array>> {
807 if len <= array.len() {
808 return Ok(array.slice(0, len));
809 }
810
811 if array.is_empty() {
812 return Ok(array.slice(0, 0));
813 }
814
815 ScalarValue::try_from_array(array.as_ref(), 0)?.to_array_of_size(len)
816}
817
818#[cfg(test)]
819mod test {
820 use common_query::native_histogram::build_histogram_array;
821 use common_query::prometheus::PROMETHEUS_STALE_NAN_BITS;
822 use datafusion::arrow::array::{
823 Float64Array, TimestampMicrosecondArray, TimestampNanosecondArray, TimestampSecondArray,
824 };
825 use datafusion::arrow::buffer::NullBuffer;
826 use datafusion::arrow::datatypes::{DataType, Field, Schema, TimeUnit};
827 use datafusion::common::ToDFSchema;
828 use datafusion::datasource::memory::MemorySourceConfig;
829 use datafusion::datasource::source::DataSourceExec;
830 use datafusion::logical_expr::{
831 EmptyRelation, Extension, LogicalPlan, Projection, UserDefinedLogicalNodeCore,
832 };
833 use datafusion::physical_plan::{ChildrenPropertiesMode, ReplaceChildrenOptions};
834 use datafusion::prelude::SessionContext;
835 use datafusion_expr::col;
836
837 use super::*;
838 use crate::extension_plan::test_util::{
839 TIME_INDEX_COLUMN, native_histogram, prepare_test_data, prepare_test_data_with_stale_marker,
840 };
841
842 async fn do_normalize_test(
843 start: Millisecond,
844 end: Millisecond,
845 lookback_delta: Millisecond,
846 interval: Millisecond,
847 expected: String,
848 contains_stale_marker: bool,
849 ) {
850 let memory_exec = if contains_stale_marker {
851 Arc::new(prepare_test_data_with_stale_marker())
852 } else {
853 Arc::new(prepare_test_data())
854 };
855 let normalize_exec = Arc::new(InstantManipulateExec {
856 offset: 0,
857 start,
858 end,
859 lookback_delta,
860 interval,
861 time_index_column: TIME_INDEX_COLUMN.to_string(),
862 field_column: Some("value".to_string()),
863 reuse_tsid_column: false,
864 output_schema: memory_exec.schema(),
865 properties: memory_exec.properties().clone(),
866 input: memory_exec,
867 metric: ExecutionPlanMetricsSet::new(),
868 });
869 let session_context = SessionContext::default();
870 let result = datafusion::physical_plan::collect(normalize_exec, session_context.task_ctx())
871 .await
872 .unwrap();
873 let result_literal = datatypes::arrow::util::pretty::pretty_format_batches(&result)
874 .unwrap()
875 .to_string();
876
877 assert_eq!(result_literal, expected);
878 }
879
880 #[tokio::test]
881 async fn native_timestamps_select_exact_samples_and_keep_ms_output() {
882 for (unit, ticks_per_ms) in [
883 (TimeUnit::Microsecond, 1_000_i64),
884 (TimeUnit::Nanosecond, 1_000_000_i64),
885 ] {
886 let lower = 1_000 * ticks_per_ms;
887 let upper = 1_001 * ticks_per_ms;
888 let stale = f64::from_bits(PROMETHEUS_STALE_NAN_BITS);
889 for (name, timestamps, values, expected_timestamps, expected_values) in [
890 (
891 "exact upper sample",
892 vec![lower + 1, upper],
893 vec![1.0, 2.0],
894 vec![1_001],
895 vec![2.0],
896 ),
897 (
898 "exclusive lower boundary and future sample",
899 vec![lower, upper + 1],
900 vec![1.0, 2.0],
901 vec![1_000],
902 vec![1.0],
903 ),
904 (
905 "one native tick above lower boundary",
906 vec![lower + 1, upper + 1],
907 vec![1.0, 2.0],
908 vec![1_001],
909 vec![1.0],
910 ),
911 (
912 "future stale marker does not suppress",
913 vec![lower + 1, upper + 1],
914 vec![1.0, stale],
915 vec![1_001],
916 vec![1.0],
917 ),
918 (
919 "latest in-window stale marker suppresses",
920 vec![lower + 1, lower + 2, upper + 1],
921 vec![1.0, stale, 3.0],
922 vec![],
923 vec![],
924 ),
925 ] {
926 let schema = Arc::new(Schema::new(vec![
927 Field::new(TIME_INDEX_COLUMN, DataType::Timestamp(unit, None), false),
928 Field::new("value", DataType::Float64, true),
929 ]));
930 let time: Arc<dyn Array> = match unit {
931 TimeUnit::Microsecond => Arc::new(TimestampMicrosecondArray::from(timestamps)),
932 TimeUnit::Nanosecond => Arc::new(TimestampNanosecondArray::from(timestamps)),
933 _ => unreachable!(),
934 };
935 let batch = RecordBatch::try_new(
936 schema.clone(),
937 vec![time, Arc::new(Float64Array::from(values))],
938 )
939 .unwrap();
940 let logical_input = LogicalPlan::EmptyRelation(EmptyRelation {
941 produce_one_row: false,
942 schema: schema.clone().to_dfschema_ref().unwrap(),
943 });
944 let plan = InstantManipulate::new(
945 1_000,
946 1_001,
947 1,
948 1,
949 0,
950 TIME_INDEX_COLUMN.to_string(),
951 Vec::new(),
952 Some("value".to_string()),
953 logical_input.clone(),
954 );
955 let output_schema = Arc::new(Schema::new(vec![
956 Field::new(
957 TIME_INDEX_COLUMN,
958 DataType::Timestamp(TimeUnit::Millisecond, None),
959 false,
960 ),
961 Field::new("value", DataType::Float64, true),
962 ]));
963 assert_eq!(plan.schema().as_arrow(), output_schema.as_ref());
964
965 let rebuilt = InstantManipulate::deserialize(&plan.serialize())
966 .unwrap()
967 .with_exprs_and_inputs(vec![], vec![logical_input])
968 .unwrap();
969 assert_eq!(rebuilt.schema(), plan.schema());
970 assert_eq!(rebuilt.input.schema().as_arrow(), schema.as_ref());
971
972 let input = Arc::new(DataSourceExec::new(Arc::new(
973 MemorySourceConfig::try_new(&[vec![batch]], schema.clone(), None).unwrap(),
974 )));
975 let exec = rebuilt.to_execution_plan(input);
976 assert_eq!(exec.schema(), output_schema);
977 assert_eq!(exec.children()[0].schema(), schema);
978
979 let batches =
980 datafusion::physical_plan::collect(exec, SessionContext::default().task_ctx())
981 .await
982 .unwrap();
983 assert_eq!(batches.len(), 1, "{unit:?}: {name}");
984 let output = &batches[0];
985 assert_eq!(output.schema(), output_schema);
986 let timestamps = output
987 .column(0)
988 .as_any()
989 .downcast_ref::<TimestampMillisecondArray>()
990 .unwrap();
991 let values = output
992 .column(1)
993 .as_any()
994 .downcast_ref::<Float64Array>()
995 .unwrap();
996 assert_eq!(
997 timestamps.values().as_ref(),
998 expected_timestamps.as_slice(),
999 "{unit:?}: {name}"
1000 );
1001 assert_eq!(
1002 values.values().as_ref(),
1003 expected_values.as_slice(),
1004 "{unit:?}: {name}"
1005 );
1006 assert_eq!(values.null_count(), 0, "{unit:?}: {name}");
1007 }
1008 }
1009 }
1010
1011 #[tokio::test]
1012 async fn logical_normalize_offset_survives_rebuild_and_executes() {
1013 for (name, time_unit, raw, offset, start, lookback_delta) in [
1014 (
1015 "millisecond offset",
1016 TimeUnit::Millisecond,
1017 0,
1018 1_000,
1019 1_000,
1020 0,
1021 ),
1022 (
1023 "second timestamp with negative fractional offset",
1024 TimeUnit::Second,
1025 1,
1026 -500,
1027 500,
1028 0,
1029 ),
1030 ] {
1031 let schema = Arc::new(Schema::new(vec![
1032 Field::new(
1033 TIME_INDEX_COLUMN,
1034 DataType::Timestamp(time_unit, None),
1035 false,
1036 ),
1037 Field::new("value", DataType::Float64, true),
1038 ]));
1039 let input = LogicalPlan::EmptyRelation(EmptyRelation {
1040 produce_one_row: false,
1041 schema: schema.clone().to_dfschema_ref().unwrap(),
1042 });
1043 let normalize = crate::extension_plan::SeriesNormalize::new(
1044 offset,
1045 TIME_INDEX_COLUMN,
1046 false,
1047 Vec::new(),
1048 input.clone(),
1049 );
1050 let normalize =
1051 crate::extension_plan::SeriesNormalize::deserialize(&normalize.serialize())
1052 .unwrap()
1053 .with_exprs_and_inputs(vec![], vec![input.clone()])
1054 .unwrap();
1055 let normalized = LogicalPlan::Projection(
1056 Projection::try_new(
1057 vec![col(TIME_INDEX_COLUMN), col("value")],
1058 Arc::new(LogicalPlan::Extension(Extension {
1059 node: Arc::new(normalize),
1060 })),
1061 )
1062 .unwrap(),
1063 );
1064 let fresh = InstantManipulate::new(
1065 start,
1066 start,
1067 lookback_delta,
1068 1,
1069 offset,
1070 TIME_INDEX_COLUMN.to_string(),
1071 Vec::new(),
1072 Some("value".to_string()),
1073 input.clone(),
1074 )
1075 .with_exprs_and_inputs(vec![], vec![input.clone()])
1076 .unwrap();
1077 let serialized = InstantManipulate::new(
1078 start,
1079 start,
1080 lookback_delta,
1081 1,
1082 offset,
1083 TIME_INDEX_COLUMN.to_string(),
1084 Vec::new(),
1085 Some("value".to_string()),
1086 normalized.clone(),
1087 );
1088 let decoded = InstantManipulate::deserialize(&serialized.serialize())
1089 .unwrap()
1090 .with_exprs_and_inputs(vec![], vec![normalized])
1091 .unwrap();
1092 let timestamp: Arc<dyn Array> = match time_unit {
1093 TimeUnit::Millisecond => Arc::new(TimestampMillisecondArray::from(vec![raw])),
1094 TimeUnit::Second => Arc::new(TimestampSecondArray::from(vec![raw])),
1095 _ => unreachable!(),
1096 };
1097 let batch = RecordBatch::try_new(
1098 schema.clone(),
1099 vec![timestamp, Arc::new(Float64Array::from(vec![7.0]))],
1100 )
1101 .unwrap();
1102 for (mode, rebuilt) in [("fresh", fresh), ("decoded", decoded)] {
1103 let rebuilt = rebuilt
1104 .with_exprs_and_inputs(vec![], vec![input.clone()])
1105 .unwrap();
1106 assert_eq!(rebuilt.offset, offset, "{name}: {mode}");
1107 assert_eq!(rebuilt.input.schema(), input.schema(), "{name}: {mode}");
1108
1109 let empty_exec_input = Arc::new(DataSourceExec::new(Arc::new(
1110 MemorySourceConfig::try_new(&[vec![]], schema.clone(), None).unwrap(),
1111 )));
1112 let exec_input = Arc::new(DataSourceExec::new(Arc::new(
1113 MemorySourceConfig::try_new(&[vec![batch.clone()]], schema.clone(), None)
1114 .unwrap(),
1115 )));
1116 let exec = rebuilt
1117 .to_execution_plan(empty_exec_input)
1118 .replace_children(
1119 vec![exec_input],
1120 ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
1121 )
1122 .unwrap();
1123 let output =
1124 datafusion::physical_plan::collect(exec, SessionContext::default().task_ctx())
1125 .await
1126 .unwrap();
1127 let output = &output[0];
1128 assert_eq!(output.num_rows(), 1, "{name}: {mode}");
1129 assert_eq!(
1130 output
1131 .column(0)
1132 .as_any()
1133 .downcast_ref::<TimestampMillisecondArray>()
1134 .unwrap()
1135 .value(0),
1136 start,
1137 "{name}: {mode}"
1138 );
1139 assert_eq!(
1140 output
1141 .column(1)
1142 .as_any()
1143 .downcast_ref::<Float64Array>()
1144 .unwrap()
1145 .value(0),
1146 7.0,
1147 "{name}: {mode}"
1148 );
1149 }
1150 }
1151 }
1152
1153 #[test]
1154 fn deserialized_ordering_preserves_column_indices() {
1155 let mut wire = pb::InstantManipulate::default();
1156 let first = InstantManipulate::deserialize(&wire.encode_to_vec()).unwrap();
1157 wire.time_index_idx = 1;
1158 let second = InstantManipulate::deserialize(&wire.encode_to_vec()).unwrap();
1159 assert_ne!(first, second);
1160 assert_eq!(first.partial_cmp(&second), Some(std::cmp::Ordering::Less));
1161 wire.field_index_idx = 2;
1162 let third = InstantManipulate::deserialize(&wire.encode_to_vec()).unwrap();
1163 assert_ne!(second, third);
1164 assert_eq!(second.partial_cmp(&third), Some(std::cmp::Ordering::Less));
1165
1166 let input = LogicalPlan::EmptyRelation(EmptyRelation {
1167 produce_one_row: false,
1168 schema: prepare_test_data().schema().to_dfschema_ref().unwrap(),
1169 });
1170 let first = InstantManipulate::new(
1171 0,
1172 0,
1173 0,
1174 0,
1175 0,
1176 TIME_INDEX_COLUMN.to_string(),
1177 Vec::new(),
1178 Some("value".to_string()),
1179 input.clone(),
1180 );
1181 let second = InstantManipulate::new(
1182 0,
1183 0,
1184 0,
1185 0,
1186 1,
1187 TIME_INDEX_COLUMN.to_string(),
1188 Vec::new(),
1189 Some("value".to_string()),
1190 input,
1191 );
1192 assert_ne!(first, second);
1193 assert_eq!(first.partial_cmp(&second), Some(std::cmp::Ordering::Less));
1194 }
1195
1196 #[test]
1197 fn pruning_should_keep_time_and_field_columns_for_exec() {
1198 let df_schema = prepare_test_data().schema().to_dfschema_ref().unwrap();
1199 let input = LogicalPlan::EmptyRelation(EmptyRelation {
1200 produce_one_row: false,
1201 schema: df_schema,
1202 });
1203 let plan = InstantManipulate::new(
1204 0,
1205 0,
1206 0,
1207 0,
1208 0,
1209 TIME_INDEX_COLUMN.to_string(),
1210 Vec::new(),
1211 Some("value".to_string()),
1212 input,
1213 );
1214
1215 let output_columns = [2usize];
1217 let required = plan.necessary_children_exprs(&output_columns).unwrap();
1218 let required = &required[0];
1219 assert_eq!(required.as_slice(), &[0, 1, 2]);
1220 }
1221
1222 #[test]
1223 fn rebuild_should_recover_tag_columns_from_series_divide_input() {
1224 let df_schema = prepare_test_data().schema().to_dfschema_ref().unwrap();
1225 let input = LogicalPlan::EmptyRelation(EmptyRelation {
1226 produce_one_row: false,
1227 schema: df_schema,
1228 });
1229 let series_divide = LogicalPlan::Extension(Extension {
1230 node: Arc::new(SeriesDivide::new(
1231 vec!["__tsid".to_string()],
1232 TIME_INDEX_COLUMN.to_string(),
1233 input,
1234 )),
1235 });
1236 let bytes = InstantManipulate::new(
1237 0,
1238 0,
1239 0,
1240 0,
1241 0,
1242 TIME_INDEX_COLUMN.to_string(),
1243 vec!["__tsid".to_string()],
1244 Some("value".to_string()),
1245 series_divide.clone(),
1246 )
1247 .serialize();
1248 let plan = InstantManipulate::deserialize(&bytes)
1249 .unwrap()
1250 .with_exprs_and_inputs(vec![], vec![series_divide])
1251 .unwrap();
1252
1253 assert_eq!(plan.tag_columns, vec!["__tsid".to_string()]);
1254 }
1255
1256 #[test]
1257 fn rebuild_should_recover_tag_columns_from_series_normalize_input() {
1258 let df_schema = prepare_test_data().schema().to_dfschema_ref().unwrap();
1259 let input = LogicalPlan::EmptyRelation(EmptyRelation {
1260 produce_one_row: false,
1261 schema: df_schema,
1262 });
1263 let series_divide = LogicalPlan::Extension(Extension {
1264 node: Arc::new(SeriesDivide::new(
1265 vec!["__tsid".to_string()],
1266 TIME_INDEX_COLUMN.to_string(),
1267 input,
1268 )),
1269 });
1270 let series_normalize = LogicalPlan::Extension(Extension {
1271 node: Arc::new(crate::extension_plan::SeriesNormalize::new(
1272 0,
1273 TIME_INDEX_COLUMN,
1274 false,
1275 vec!["__tsid".to_string()],
1276 series_divide,
1277 )),
1278 });
1279 let bytes = InstantManipulate::new(
1280 0,
1281 0,
1282 0,
1283 0,
1284 0,
1285 TIME_INDEX_COLUMN.to_string(),
1286 vec!["__tsid".to_string()],
1287 Some("value".to_string()),
1288 series_normalize.clone(),
1289 )
1290 .serialize();
1291 let plan = InstantManipulate::deserialize(&bytes)
1292 .unwrap()
1293 .with_exprs_and_inputs(vec![], vec![series_normalize])
1294 .unwrap();
1295
1296 assert_eq!(plan.tag_columns, vec!["__tsid".to_string()]);
1297 }
1298
1299 #[test]
1300 fn to_execution_plan_enables_tsid_fast_path() {
1301 let schema = Arc::new(Schema::new(vec![
1302 Field::new(
1303 TIME_INDEX_COLUMN,
1304 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1305 false,
1306 ),
1307 Field::new("value", DataType::Float64, true),
1308 ]));
1309 let exec_input: Arc<dyn ExecutionPlan> = Arc::new(DataSourceExec::new(Arc::new(
1310 MemorySourceConfig::try_new(&[], schema, None).unwrap(),
1311 )));
1312
1313 let exec = InstantManipulate::new(
1314 0,
1315 0,
1316 0,
1317 0,
1318 0,
1319 TIME_INDEX_COLUMN.to_string(),
1320 vec!["__tsid".to_string()],
1321 Some("value".to_string()),
1322 LogicalPlan::EmptyRelation(EmptyRelation {
1323 produce_one_row: false,
1324 schema: Arc::new(datafusion::common::DFSchema::empty()),
1325 }),
1326 )
1327 .to_execution_plan(exec_input);
1328
1329 assert!(format!("{exec:?}").contains("reuse_tsid_column: true"));
1330 }
1331
1332 #[tokio::test]
1333 async fn tsid_fast_path_reuses_tsid_column_when_output_grows() {
1334 let schema = Arc::new(Schema::new(vec![
1335 Field::new(
1336 TIME_INDEX_COLUMN,
1337 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1338 false,
1339 ),
1340 Field::new("value", DataType::Float64, true),
1341 Field::new("host", DataType::Utf8, true),
1342 Field::new("__tsid", DataType::UInt64, false),
1343 ]));
1344 let batch = RecordBatch::try_new(
1345 schema.clone(),
1346 vec![
1347 Arc::new(TimestampMillisecondArray::from(vec![0, 1_000])),
1348 Arc::new(Float64Array::from(vec![1.0, 2.0])),
1349 Arc::new(datafusion::arrow::array::StringArray::from(vec![
1350 "foo", "foo",
1351 ])),
1352 Arc::new(UInt64Array::from(vec![42, 42])),
1353 ],
1354 )
1355 .unwrap();
1356 let input = Arc::new(DataSourceExec::new(Arc::new(
1357 MemorySourceConfig::try_new(&[vec![batch]], schema, None).unwrap(),
1358 )));
1359 let normalize_exec = Arc::new(InstantManipulateExec {
1360 offset: 0,
1361 start: 0,
1362 end: 1_500,
1363 lookback_delta: 1_000,
1364 interval: 500,
1365 time_index_column: TIME_INDEX_COLUMN.to_string(),
1366 field_column: Some("value".to_string()),
1367 reuse_tsid_column: true,
1368 output_schema: input.schema(),
1369 properties: input.properties().clone(),
1370 input,
1371 metric: ExecutionPlanMetricsSet::new(),
1372 });
1373 let session_context = SessionContext::default();
1374 let result = datafusion::physical_plan::collect(normalize_exec, session_context.task_ctx())
1375 .await
1376 .unwrap();
1377 let result_literal = datatypes::arrow::util::pretty::pretty_format_batches(&result)
1378 .unwrap()
1379 .to_string();
1380
1381 assert_eq!(
1382 result_literal,
1383 "+-------------------------+-------+------+--------+\
1384 \n| timestamp | value | host | __tsid |\
1385 \n+-------------------------+-------+------+--------+\
1386 \n| 1970-01-01T00:00:00 | 1.0 | foo | 42 |\
1387 \n| 1970-01-01T00:00:00.500 | 1.0 | foo | 42 |\
1388 \n| 1970-01-01T00:00:01 | 2.0 | foo | 42 |\
1389 \n| 1970-01-01T00:00:01.500 | 2.0 | foo | 42 |\
1390 \n+-------------------------+-------+------+--------+"
1391 );
1392 }
1393
1394 #[tokio::test]
1395 async fn tsid_fast_path_still_takes_additional_field_columns() {
1396 let schema = Arc::new(Schema::new(vec![
1397 Field::new(
1398 TIME_INDEX_COLUMN,
1399 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1400 false,
1401 ),
1402 Field::new("value", DataType::Float64, true),
1403 Field::new("value_2", DataType::Float64, true),
1404 Field::new("host", DataType::Utf8, true),
1405 Field::new("__tsid", DataType::UInt64, false),
1406 ]));
1407 let batch = RecordBatch::try_new(
1408 schema.clone(),
1409 vec![
1410 Arc::new(TimestampMillisecondArray::from(vec![0, 1_000])),
1411 Arc::new(Float64Array::from(vec![1.0, 2.0])),
1412 Arc::new(Float64Array::from(vec![10.0, 20.0])),
1413 Arc::new(datafusion::arrow::array::StringArray::from(vec![
1414 "foo", "foo",
1415 ])),
1416 Arc::new(UInt64Array::from(vec![42, 42])),
1417 ],
1418 )
1419 .unwrap();
1420 let input = Arc::new(DataSourceExec::new(Arc::new(
1421 MemorySourceConfig::try_new(&[vec![batch]], schema, None).unwrap(),
1422 )));
1423 let normalize_exec = Arc::new(InstantManipulateExec {
1424 offset: 0,
1425 start: 0,
1426 end: 1_500,
1427 lookback_delta: 1_000,
1428 interval: 500,
1429 time_index_column: TIME_INDEX_COLUMN.to_string(),
1430 field_column: Some("value".to_string()),
1431 reuse_tsid_column: true,
1432 output_schema: input.schema(),
1433 properties: input.properties().clone(),
1434 input,
1435 metric: ExecutionPlanMetricsSet::new(),
1436 });
1437 let session_context = SessionContext::default();
1438 let result = datafusion::physical_plan::collect(normalize_exec, session_context.task_ctx())
1439 .await
1440 .unwrap();
1441 let result_literal = datatypes::arrow::util::pretty::pretty_format_batches(&result)
1442 .unwrap()
1443 .to_string();
1444
1445 assert_eq!(
1446 result_literal,
1447 "+-------------------------+-------+---------+------+--------+\
1448 \n| timestamp | value | value_2 | host | __tsid |\
1449 \n+-------------------------+-------+---------+------+--------+\
1450 \n| 1970-01-01T00:00:00 | 1.0 | 10.0 | foo | 42 |\
1451 \n| 1970-01-01T00:00:00.500 | 1.0 | 10.0 | foo | 42 |\
1452 \n| 1970-01-01T00:00:01 | 2.0 | 20.0 | foo | 42 |\
1453 \n| 1970-01-01T00:00:01.500 | 2.0 | 20.0 | foo | 42 |\
1454 \n+-------------------------+-------+---------+------+--------+"
1455 );
1456 }
1457
1458 #[tokio::test]
1459 async fn manipulate_should_reject_too_many_output_points() {
1460 let schema = Arc::new(Schema::new(vec![
1461 Field::new(
1462 TIME_INDEX_COLUMN,
1463 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
1464 false,
1465 ),
1466 Field::new("value", DataType::Float64, true),
1467 ]));
1468 let batch = RecordBatch::try_new(
1469 schema.clone(),
1470 vec![
1471 Arc::new(TimestampMillisecondArray::from(vec![0])),
1472 Arc::new(Float64Array::from(vec![1.0])),
1473 ],
1474 )
1475 .unwrap();
1476 let input = Arc::new(DataSourceExec::new(Arc::new(
1477 MemorySourceConfig::try_new(&[vec![batch]], schema, None).unwrap(),
1478 )));
1479 let too_many_points = MAX_INSTANT_MANIPULATE_OUTPUT_POINTS as Millisecond + 1;
1480 let normalize_exec = Arc::new(InstantManipulateExec {
1481 offset: 0,
1482 start: 0,
1483 end: too_many_points,
1484 lookback_delta: too_many_points + 1,
1485 interval: 1,
1486 time_index_column: TIME_INDEX_COLUMN.to_string(),
1487 field_column: Some("value".to_string()),
1488 reuse_tsid_column: false,
1489 output_schema: input.schema(),
1490 properties: input.properties().clone(),
1491 input,
1492 metric: ExecutionPlanMetricsSet::new(),
1493 });
1494 let session_context = SessionContext::default();
1495 let err = datafusion::physical_plan::collect(normalize_exec, session_context.task_ctx())
1496 .await
1497 .unwrap_err();
1498
1499 assert!(
1500 err.to_string()
1501 .contains("InstantManipulate output points exceed limit")
1502 );
1503 }
1504
1505 #[tokio::test]
1506 async fn lookback_10s_interval_30s() {
1507 let expected = String::from(
1508 "+---------------------+-------+------+\
1509 \n| timestamp | value | path |\
1510 \n+---------------------+-------+------+\
1511 \n| 1970-01-01T00:00:00 | 1.0 | foo |\
1512 \n| 1970-01-01T00:00:30 | 1.0 | foo |\
1513 \n| 1970-01-01T00:01:00 | 1.0 | foo |\
1514 \n| 1970-01-01T00:01:30 | 1.0 | foo |\
1515 \n| 1970-01-01T00:02:00 | 1.0 | foo |\
1516 \n| 1970-01-01T00:03:00 | 1.0 | foo |\
1517 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1518 \n| 1970-01-01T00:05:00 | 1.0 | foo |\
1519 \n+---------------------+-------+------+",
1520 );
1521 do_normalize_test(0, 310_000, 10_000, 30_000, expected, false).await;
1522 }
1523
1524 #[tokio::test]
1525 async fn lookback_10s_interval_10s() {
1526 let expected = String::from(
1527 "+---------------------+-------+------+\
1528 \n| timestamp | value | path |\
1529 \n+---------------------+-------+------+\
1530 \n| 1970-01-01T00:00:00 | 1.0 | foo |\
1531 \n| 1970-01-01T00:00:30 | 1.0 | foo |\
1532 \n| 1970-01-01T00:01:00 | 1.0 | foo |\
1533 \n| 1970-01-01T00:01:30 | 1.0 | foo |\
1534 \n| 1970-01-01T00:02:00 | 1.0 | foo |\
1535 \n| 1970-01-01T00:03:00 | 1.0 | foo |\
1536 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1537 \n| 1970-01-01T00:04:10 | 1.0 | foo |\
1538 \n| 1970-01-01T00:04:40 | 1.0 | foo |\
1539 \n| 1970-01-01T00:05:00 | 1.0 | foo |\
1540 \n+---------------------+-------+------+",
1541 );
1542 do_normalize_test(0, 300_000, 10_000, 10_000, expected, false).await;
1543 }
1544
1545 #[tokio::test]
1546 async fn lookback_30s_interval_30s() {
1547 let expected = String::from(
1548 "+---------------------+-------+------+\
1549 \n| timestamp | value | path |\
1550 \n+---------------------+-------+------+\
1551 \n| 1970-01-01T00:00:00 | 1.0 | foo |\
1552 \n| 1970-01-01T00:00:30 | 1.0 | foo |\
1553 \n| 1970-01-01T00:01:00 | 1.0 | foo |\
1554 \n| 1970-01-01T00:01:30 | 1.0 | foo |\
1555 \n| 1970-01-01T00:02:00 | 1.0 | foo |\
1556 \n| 1970-01-01T00:03:00 | 1.0 | foo |\
1557 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1558 \n| 1970-01-01T00:04:30 | 1.0 | foo |\
1559 \n| 1970-01-01T00:05:00 | 1.0 | foo |\
1560 \n+---------------------+-------+------+",
1561 );
1562 do_normalize_test(0, 300_000, 30_000, 30_000, expected, false).await;
1563 }
1564
1565 #[tokio::test]
1566 async fn lookback_30s_interval_10s() {
1567 let expected = String::from(
1568 "+---------------------+-------+------+\
1569 \n| timestamp | value | path |\
1570 \n+---------------------+-------+------+\
1571 \n| 1970-01-01T00:00:00 | 1.0 | foo |\
1572 \n| 1970-01-01T00:00:10 | 1.0 | foo |\
1573 \n| 1970-01-01T00:00:20 | 1.0 | foo |\
1574 \n| 1970-01-01T00:00:30 | 1.0 | foo |\
1575 \n| 1970-01-01T00:00:40 | 1.0 | foo |\
1576 \n| 1970-01-01T00:00:50 | 1.0 | foo |\
1577 \n| 1970-01-01T00:01:00 | 1.0 | foo |\
1578 \n| 1970-01-01T00:01:10 | 1.0 | foo |\
1579 \n| 1970-01-01T00:01:20 | 1.0 | foo |\
1580 \n| 1970-01-01T00:01:30 | 1.0 | foo |\
1581 \n| 1970-01-01T00:01:40 | 1.0 | foo |\
1582 \n| 1970-01-01T00:01:50 | 1.0 | foo |\
1583 \n| 1970-01-01T00:02:00 | 1.0 | foo |\
1584 \n| 1970-01-01T00:02:10 | 1.0 | foo |\
1585 \n| 1970-01-01T00:02:20 | 1.0 | foo |\
1586 \n| 1970-01-01T00:03:00 | 1.0 | foo |\
1587 \n| 1970-01-01T00:03:10 | 1.0 | foo |\
1588 \n| 1970-01-01T00:03:20 | 1.0 | foo |\
1589 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1590 \n| 1970-01-01T00:04:10 | 1.0 | foo |\
1591 \n| 1970-01-01T00:04:20 | 1.0 | foo |\
1592 \n| 1970-01-01T00:04:30 | 1.0 | foo |\
1593 \n| 1970-01-01T00:04:40 | 1.0 | foo |\
1594 \n| 1970-01-01T00:04:50 | 1.0 | foo |\
1595 \n| 1970-01-01T00:05:00 | 1.0 | foo |\
1596 \n+---------------------+-------+------+",
1597 );
1598 do_normalize_test(0, 300_000, 30_000, 10_000, expected, false).await;
1599 }
1600
1601 #[tokio::test]
1602 async fn lookback_60s_interval_10s() {
1603 let expected = String::from(
1604 "+---------------------+-------+------+\
1605 \n| timestamp | value | path |\
1606 \n+---------------------+-------+------+\
1607 \n| 1970-01-01T00:00:00 | 1.0 | foo |\
1608 \n| 1970-01-01T00:00:10 | 1.0 | foo |\
1609 \n| 1970-01-01T00:00:20 | 1.0 | foo |\
1610 \n| 1970-01-01T00:00:30 | 1.0 | foo |\
1611 \n| 1970-01-01T00:00:40 | 1.0 | foo |\
1612 \n| 1970-01-01T00:00:50 | 1.0 | foo |\
1613 \n| 1970-01-01T00:01:00 | 1.0 | foo |\
1614 \n| 1970-01-01T00:01:10 | 1.0 | foo |\
1615 \n| 1970-01-01T00:01:20 | 1.0 | foo |\
1616 \n| 1970-01-01T00:01:30 | 1.0 | foo |\
1617 \n| 1970-01-01T00:01:40 | 1.0 | foo |\
1618 \n| 1970-01-01T00:01:50 | 1.0 | foo |\
1619 \n| 1970-01-01T00:02:00 | 1.0 | foo |\
1620 \n| 1970-01-01T00:02:10 | 1.0 | foo |\
1621 \n| 1970-01-01T00:02:20 | 1.0 | foo |\
1622 \n| 1970-01-01T00:02:30 | 1.0 | foo |\
1623 \n| 1970-01-01T00:02:40 | 1.0 | foo |\
1624 \n| 1970-01-01T00:02:50 | 1.0 | foo |\
1625 \n| 1970-01-01T00:03:00 | 1.0 | foo |\
1626 \n| 1970-01-01T00:03:10 | 1.0 | foo |\
1627 \n| 1970-01-01T00:03:20 | 1.0 | foo |\
1628 \n| 1970-01-01T00:03:30 | 1.0 | foo |\
1629 \n| 1970-01-01T00:03:40 | 1.0 | foo |\
1630 \n| 1970-01-01T00:03:50 | 1.0 | foo |\
1631 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1632 \n| 1970-01-01T00:04:10 | 1.0 | foo |\
1633 \n| 1970-01-01T00:04:20 | 1.0 | foo |\
1634 \n| 1970-01-01T00:04:30 | 1.0 | foo |\
1635 \n| 1970-01-01T00:04:40 | 1.0 | foo |\
1636 \n| 1970-01-01T00:04:50 | 1.0 | foo |\
1637 \n| 1970-01-01T00:05:00 | 1.0 | foo |\
1638 \n+---------------------+-------+------+",
1639 );
1640 do_normalize_test(0, 300_000, 60_000, 10_000, expected, false).await;
1641 }
1642
1643 #[tokio::test]
1644 async fn lookback_60s_interval_30s() {
1645 let expected = String::from(
1646 "+---------------------+-------+------+\
1647 \n| timestamp | value | path |\
1648 \n+---------------------+-------+------+\
1649 \n| 1970-01-01T00:00:00 | 1.0 | foo |\
1650 \n| 1970-01-01T00:00:30 | 1.0 | foo |\
1651 \n| 1970-01-01T00:01:00 | 1.0 | foo |\
1652 \n| 1970-01-01T00:01:30 | 1.0 | foo |\
1653 \n| 1970-01-01T00:02:00 | 1.0 | foo |\
1654 \n| 1970-01-01T00:02:30 | 1.0 | foo |\
1655 \n| 1970-01-01T00:03:00 | 1.0 | foo |\
1656 \n| 1970-01-01T00:03:30 | 1.0 | foo |\
1657 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1658 \n| 1970-01-01T00:04:30 | 1.0 | foo |\
1659 \n| 1970-01-01T00:05:00 | 1.0 | foo |\
1660 \n+---------------------+-------+------+",
1661 );
1662 do_normalize_test(0, 300_000, 60_000, 30_000, expected, false).await;
1663 }
1664
1665 #[tokio::test]
1666 async fn small_range_lookback_0s_interval_1s() {
1667 let expected = String::from(
1668 "+---------------------+-------+------+\
1669 \n| timestamp | value | path |\
1670 \n+---------------------+-------+------+\
1671 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1672 \n| 1970-01-01T00:04:01 | 1.0 | foo |\
1673 \n+---------------------+-------+------+",
1674 );
1675 do_normalize_test(230_000, 245_000, 0, 1_000, expected, false).await;
1676 }
1677
1678 #[tokio::test]
1679 async fn small_range_lookback_10s_interval_10s() {
1680 let expected = String::from(
1681 "+---------------------+-------+------+\
1682 \n| timestamp | value | path |\
1683 \n+---------------------+-------+------+\
1684 \n| 1970-01-01T00:00:00 | 1.0 | foo |\
1685 \n| 1970-01-01T00:00:30 | 1.0 | foo |\
1686 \n+---------------------+-------+------+",
1687 );
1688 do_normalize_test(0, 30_000, 10_000, 10_000, expected, false).await;
1689 }
1690
1691 #[tokio::test]
1692 async fn large_range_lookback_30s_interval_60s() {
1693 let expected = String::from(
1694 "+---------------------+-------+------+\
1695 \n| timestamp | value | path |\
1696 \n+---------------------+-------+------+\
1697 \n| 1970-01-01T00:00:00 | 1.0 | foo |\
1698 \n| 1970-01-01T00:01:00 | 1.0 | foo |\
1699 \n| 1970-01-01T00:02:00 | 1.0 | foo |\
1700 \n| 1970-01-01T00:03:00 | 1.0 | foo |\
1701 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1702 \n| 1970-01-01T00:05:00 | 1.0 | foo |\
1703 \n+---------------------+-------+------+",
1704 );
1705 do_normalize_test(-900_000, 900_000, 30_000, 60_000, expected, false).await;
1706 }
1707
1708 #[tokio::test]
1709 async fn small_range_lookback_30s_interval_30s() {
1710 let expected = String::from(
1711 "+---------------------+-------+------+\
1712 \n| timestamp | value | path |\
1713 \n+---------------------+-------+------+\
1714 \n| 1970-01-01T00:03:10 | 1.0 | foo |\
1715 \n| 1970-01-01T00:03:20 | 1.0 | foo |\
1716 \n| 1970-01-01T00:04:00 | 1.0 | foo |\
1717 \n| 1970-01-01T00:04:10 | 1.0 | foo |\
1718 \n| 1970-01-01T00:04:20 | 1.0 | foo |\
1719 \n| 1970-01-01T00:04:30 | 1.0 | foo |\
1720 \n| 1970-01-01T00:04:40 | 1.0 | foo |\
1721 \n| 1970-01-01T00:04:50 | 1.0 | foo |\
1722 \n| 1970-01-01T00:05:00 | 1.0 | foo |\
1723 \n+---------------------+-------+------+",
1724 );
1725 do_normalize_test(190_000, 300_000, 30_000, 10_000, expected, false).await;
1726 }
1727
1728 #[tokio::test]
1729 async fn lookback_10s_interval_10s_with_stale_marker() {
1730 let expected = String::from(
1731 "+---------------------+-------+\
1732 \n| timestamp | value |\
1733 \n+---------------------+-------+\
1734 \n| 1970-01-01T00:00:00 | 0.0 |\
1735 \n| 1970-01-01T00:01:00 | 6.0 |\
1736 \n| 1970-01-01T00:02:00 | 12.0 |\
1737 \n+---------------------+-------+",
1738 );
1739 do_normalize_test(0, 300_000, 10_000, 10_000, expected, true).await;
1740 }
1741
1742 #[tokio::test]
1743 async fn lookback_10s_interval_10s_with_stale_marker_unaligned() {
1744 let expected = String::from(
1745 "+-------------------------+-------+\
1746 \n| timestamp | value |\
1747 \n+-------------------------+-------+\
1748 \n| 1970-01-01T00:00:00.001 | 0.0 |\
1749 \n| 1970-01-01T00:01:00.001 | 6.0 |\
1750 \n| 1970-01-01T00:02:00.001 | 12.0 |\
1751 \n+-------------------------+-------+",
1752 );
1753 do_normalize_test(1, 300_001, 10_000, 10_000, expected, true).await;
1754 }
1755
1756 #[tokio::test]
1757 async fn ultra_large_range() {
1758 let expected = String::from(
1759 "+-------------------------+-------+\
1760 \n| timestamp | value |\
1761 \n+-------------------------+-------+\
1762 \n| 1970-01-01T00:00:00.001 | 0.0 |\
1763 \n| 1970-01-01T00:01:00.001 | 6.0 |\
1764 \n| 1970-01-01T00:02:00.001 | 12.0 |\
1765 \n+-------------------------+-------+",
1766 );
1767 do_normalize_test(
1768 -900_000_000_000_000 + 1,
1769 900_000_000_000_000,
1770 10_000,
1771 10_000,
1772 expected,
1773 true,
1774 )
1775 .await;
1776 }
1777
1778 #[test]
1779 fn exact_ties_select_first_and_lookback_uses_latest() {
1780 for (values, expected_timestamp) in [
1781 (vec![42.0, f64::from_bits(PROMETHEUS_STALE_NAN_BITS)], 1_000),
1782 (vec![f64::from_bits(PROMETHEUS_STALE_NAN_BITS), 42.0], 1_050),
1783 ] {
1784 let schema = Arc::new(Schema::new(vec![
1785 Field::new(
1786 TIME_INDEX_COLUMN,
1787 DataType::Timestamp(TimeUnit::Millisecond, None),
1788 false,
1789 ),
1790 Field::new("value", DataType::Float64, true),
1791 ]));
1792 let input = RecordBatch::try_new(
1793 schema.clone(),
1794 vec![
1795 Arc::new(TimestampMillisecondArray::from(vec![1_000, 1_000])),
1796 Arc::new(Float64Array::from(values)),
1797 ],
1798 )
1799 .unwrap();
1800 let stream = InstantManipulateStream {
1801 offset: 0,
1802 start: 1_000,
1803 end: 1_050,
1804 lookback_delta: 100,
1805 interval: 50,
1806 time_index: 0,
1807 time_unit: TimeUnit::Millisecond,
1808 field_indices: [Some(1), None],
1809 tsid_index: None,
1810 reuse_tsid_column: false,
1811 schema: schema.clone(),
1812 input: Box::pin(
1813 datafusion::physical_plan::memory::MemoryStream::try_new(vec![], schema, None)
1814 .unwrap(),
1815 ),
1816 metric: BaselineMetrics::new(&ExecutionPlanMetricsSet::new(), 0),
1817 num_series: Count::new(),
1818 };
1819
1820 let output = stream.manipulate(input).unwrap();
1821 let timestamps = output
1822 .column(0)
1823 .as_any()
1824 .downcast_ref::<TimestampMillisecondArray>()
1825 .unwrap();
1826 let values = output
1827 .column(1)
1828 .as_any()
1829 .downcast_ref::<Float64Array>()
1830 .unwrap();
1831 assert_eq!(timestamps.values(), &[expected_timestamp]);
1832 assert_eq!(values.values(), &[42.0]);
1833 }
1834 }
1835
1836 #[test]
1837 fn empty_batches_preserve_stream_progression_and_output_schema() {
1838 use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
1839 use futures::stream;
1840 use futures::task::noop_waker_ref;
1841
1842 let input_schema = Arc::new(Schema::new(vec![Field::new(
1843 TIME_INDEX_COLUMN,
1844 DataType::Timestamp(TimeUnit::Second, None),
1845 false,
1846 )]));
1847 let output_schema = Arc::new(Schema::new(vec![Field::new(
1848 TIME_INDEX_COLUMN,
1849 DataType::Timestamp(TimeUnit::Millisecond, None),
1850 false,
1851 )]));
1852 let empty = RecordBatch::new_empty(input_schema.clone());
1853 let valid = RecordBatch::try_new(
1854 input_schema.clone(),
1855 vec![Arc::new(TimestampSecondArray::from(vec![1]))],
1856 )
1857 .unwrap();
1858 let input = RecordBatchStreamAdapter::new(
1859 input_schema,
1860 stream::iter(vec![
1861 Ok(empty.clone()),
1862 Ok(empty.clone()),
1863 Ok(valid),
1864 Ok(empty),
1865 Err(DataFusionError::Execution("injected input error".into())),
1866 ]),
1867 );
1868 let mut stream = InstantManipulateStream {
1869 offset: 0,
1870 start: 1_000,
1871 end: 1_000,
1872 lookback_delta: 0,
1873 interval: 1,
1874 time_index: 0,
1875 time_unit: TimeUnit::Second,
1876 field_indices: [None, None],
1877 tsid_index: None,
1878 reuse_tsid_column: false,
1879 schema: output_schema.clone(),
1880 input: Box::pin(input),
1881 metric: BaselineMetrics::new(&ExecutionPlanMetricsSet::new(), 0),
1882 num_series: Count::new(),
1883 };
1884 let waker = noop_waker_ref();
1885 let mut cx = Context::from_waker(waker);
1886
1887 for _ in 0..2 {
1888 let Poll::Ready(Some(Ok(batch))) = Pin::new(&mut stream).poll_next(&mut cx) else {
1889 panic!("empty batch must be returned immediately");
1890 };
1891 assert_eq!(batch.num_rows(), 0);
1892 assert_eq!(batch.schema(), output_schema);
1893 }
1894
1895 let Poll::Ready(Some(Ok(valid))) = Pin::new(&mut stream).poll_next(&mut cx) else {
1896 panic!("valid batch must follow empty batches");
1897 };
1898 assert_eq!(valid.schema(), output_schema);
1899 assert_eq!(
1900 valid.column(0).data_type(),
1901 &DataType::Timestamp(TimeUnit::Millisecond, None)
1902 );
1903 assert_eq!(
1904 valid
1905 .column(0)
1906 .as_any()
1907 .downcast_ref::<TimestampMillisecondArray>()
1908 .unwrap()
1909 .values(),
1910 &[1_000]
1911 );
1912
1913 let Poll::Ready(Some(Ok(empty))) = Pin::new(&mut stream).poll_next(&mut cx) else {
1914 panic!("empty batch after valid batch must be returned immediately");
1915 };
1916 assert_eq!(empty.num_rows(), 0);
1917 assert_eq!(empty.schema(), output_schema);
1918
1919 let Poll::Ready(Some(Err(error))) = Pin::new(&mut stream).poll_next(&mut cx) else {
1920 panic!("input error must propagate");
1921 };
1922 assert!(error.to_string().contains("injected input error"));
1923 assert!(matches!(
1924 Pin::new(&mut stream).poll_next(&mut cx),
1925 Poll::Ready(None)
1926 ));
1927 assert_eq!(stream.num_series.value(), 1);
1928 }
1929
1930 #[test]
1931 fn extreme_alignment_retains_exact_sample() {
1932 let schema = Arc::new(Schema::new(vec![
1933 Field::new(
1934 TIME_INDEX_COLUMN,
1935 DataType::Timestamp(TimeUnit::Millisecond, None),
1936 false,
1937 ),
1938 Field::new("value", DataType::Float64, true),
1939 ]));
1940 let input = RecordBatch::try_new(
1941 schema.clone(),
1942 vec![
1943 Arc::new(TimestampMillisecondArray::from(vec![i64::MAX])),
1944 Arc::new(Float64Array::from(vec![7.0])),
1945 ],
1946 )
1947 .unwrap();
1948 let stream = InstantManipulateStream {
1949 offset: 0,
1950 start: i64::MIN + 1,
1951 end: i64::MAX,
1952 lookback_delta: 0,
1953 interval: i64::MAX,
1954 time_index: 0,
1955 time_unit: TimeUnit::Millisecond,
1956 field_indices: [Some(1), None],
1957 tsid_index: None,
1958 reuse_tsid_column: false,
1959 schema: schema.clone(),
1960 input: Box::pin(
1961 datafusion::physical_plan::memory::MemoryStream::try_new(vec![], schema, None)
1962 .unwrap(),
1963 ),
1964 metric: BaselineMetrics::new(&ExecutionPlanMetricsSet::new(), 0),
1965 num_series: Count::new(),
1966 };
1967
1968 let output = stream.manipulate(input).unwrap();
1969 let timestamps = output
1970 .column(0)
1971 .as_any()
1972 .downcast_ref::<TimestampMillisecondArray>()
1973 .unwrap();
1974 let values = output
1975 .column(1)
1976 .as_any()
1977 .downcast_ref::<Float64Array>()
1978 .unwrap();
1979 assert_eq!(timestamps.values(), &[i64::MAX]);
1980 assert_eq!(values.values(), &[7.0]);
1981 }
1982
1983 #[test]
1984 fn native_nanosecond_offset_uses_wide_shifted_timeline() {
1985 for (raw, offset, eval) in [
1986 (
1987 9_223_112_837_000_000_000_i64,
1988 259_200_000,
1989 9_223_372_037_000,
1990 ),
1991 (
1992 -9_223_112_837_000_000_000_i64,
1993 -259_200_000,
1994 -9_223_372_037_000,
1995 ),
1996 ] {
1997 let schema = Arc::new(Schema::new(vec![
1998 Field::new(
1999 TIME_INDEX_COLUMN,
2000 DataType::Timestamp(TimeUnit::Nanosecond, None),
2001 false,
2002 ),
2003 Field::new("value", DataType::Float64, true),
2004 ]));
2005 let input = RecordBatch::try_new(
2006 schema.clone(),
2007 vec![
2008 Arc::new(TimestampNanosecondArray::from(vec![raw])),
2009 Arc::new(Float64Array::from(vec![7.0])),
2010 ],
2011 )
2012 .unwrap();
2013 let stream = InstantManipulateStream {
2014 offset,
2015 start: eval,
2016 end: eval,
2017 lookback_delta: 300_000,
2018 interval: 1,
2019 time_index: 0,
2020 time_unit: TimeUnit::Nanosecond,
2021 field_indices: [Some(1), None],
2022 tsid_index: None,
2023 reuse_tsid_column: false,
2024 schema: Arc::new(Schema::new(vec![
2025 Field::new(
2026 TIME_INDEX_COLUMN,
2027 DataType::Timestamp(TimeUnit::Millisecond, None),
2028 false,
2029 ),
2030 Field::new("value", DataType::Float64, true),
2031 ])),
2032 input: Box::pin(
2033 datafusion::physical_plan::memory::MemoryStream::try_new(vec![], schema, None)
2034 .unwrap(),
2035 ),
2036 metric: BaselineMetrics::new(&ExecutionPlanMetricsSet::new(), 0),
2037 num_series: Count::new(),
2038 };
2039 let output = stream.manipulate(input).unwrap();
2040 assert_eq!(output.num_rows(), 1);
2041 assert_eq!(
2042 output
2043 .column(0)
2044 .as_any()
2045 .downcast_ref::<TimestampMillisecondArray>()
2046 .unwrap()
2047 .value(0),
2048 eval
2049 );
2050 assert_eq!(
2051 output
2052 .column(1)
2053 .as_any()
2054 .downcast_ref::<Float64Array>()
2055 .unwrap()
2056 .value(0),
2057 7.0
2058 );
2059 }
2060 }
2061
2062 #[tokio::test]
2063 async fn ordinary_nan_is_selected_for_exact_and_lookback() {
2064 let schema = Arc::new(Schema::new(vec![
2065 Field::new(
2066 TIME_INDEX_COLUMN,
2067 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
2068 false,
2069 ),
2070 Field::new("value", DataType::Float64, true),
2071 ]));
2072 let batch = RecordBatch::try_new(
2073 schema.clone(),
2074 vec![
2075 Arc::new(TimestampMillisecondArray::from(vec![1_000])),
2076 Arc::new(Float64Array::from(vec![f64::NAN])),
2077 ],
2078 )
2079 .unwrap();
2080 let input = Arc::new(DataSourceExec::new(Arc::new(
2081 MemorySourceConfig::try_new(&[vec![batch]], schema, None).unwrap(),
2082 )));
2083 let exec = Arc::new(InstantManipulateExec {
2084 offset: 0,
2085 start: 1_000,
2086 end: 1_500,
2087 lookback_delta: 1_000,
2088 interval: 500,
2089 time_index_column: TIME_INDEX_COLUMN.to_string(),
2090 field_column: Some("value".to_string()),
2091 reuse_tsid_column: false,
2092 output_schema: input.schema(),
2093 properties: input.properties().clone(),
2094 input,
2095 metric: ExecutionPlanMetricsSet::new(),
2096 });
2097
2098 let context = SessionContext::default();
2099 let batches = datafusion::physical_plan::collect(exec, context.task_ctx())
2100 .await
2101 .unwrap();
2102 let values = batches
2103 .iter()
2104 .flat_map(|batch| {
2105 batch
2106 .column(1)
2107 .as_any()
2108 .downcast_ref::<Float64Array>()
2109 .unwrap()
2110 .values()
2111 .iter()
2112 .copied()
2113 })
2114 .collect::<Vec<_>>();
2115 let timestamps = batches
2116 .iter()
2117 .flat_map(|batch| {
2118 batch
2119 .column(0)
2120 .as_any()
2121 .downcast_ref::<TimestampMillisecondArray>()
2122 .unwrap()
2123 .values()
2124 .iter()
2125 .copied()
2126 })
2127 .collect::<Vec<_>>();
2128
2129 assert_eq!(values.len(), 2);
2130 assert_eq!(timestamps, vec![1_000, 1_500]);
2131 assert!(values.iter().all(|value| value.is_nan()));
2132 assert_eq!(
2133 values
2134 .iter()
2135 .map(|value| value.to_bits())
2136 .collect::<Vec<_>>(),
2137 vec![f64::NAN.to_bits(); 2]
2138 );
2139 }
2140
2141 #[tokio::test]
2142 async fn prometheus_stale_nan_selects_before_and_suppresses_after_marker() {
2143 let schema = Arc::new(Schema::new(vec![
2144 Field::new(
2145 TIME_INDEX_COLUMN,
2146 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
2147 false,
2148 ),
2149 Field::new("value", DataType::Float64, true),
2150 ]));
2151 let batch = RecordBatch::try_new(
2152 schema.clone(),
2153 vec![
2154 Arc::new(TimestampMillisecondArray::from(vec![500, 1_000])),
2155 Arc::new(Float64Array::from(vec![
2156 42.0,
2157 f64::from_bits(0x7ff0_0000_0000_0002),
2158 ])),
2159 ],
2160 )
2161 .unwrap();
2162 let input = Arc::new(DataSourceExec::new(Arc::new(
2163 MemorySourceConfig::try_new(&[vec![batch]], schema, None).unwrap(),
2164 )));
2165 let exec = Arc::new(InstantManipulateExec {
2166 offset: 0,
2167 start: 750,
2168 end: 1_500,
2169 lookback_delta: 1_001,
2170 interval: 250,
2171 time_index_column: TIME_INDEX_COLUMN.to_string(),
2172 field_column: Some("value".to_string()),
2173 reuse_tsid_column: false,
2174 output_schema: input.schema(),
2175 properties: input.properties().clone(),
2176 input,
2177 metric: ExecutionPlanMetricsSet::new(),
2178 });
2179
2180 let context = SessionContext::default();
2181 let batches = datafusion::physical_plan::collect(exec, context.task_ctx())
2182 .await
2183 .unwrap();
2184
2185 let row_count = batches.iter().map(RecordBatch::num_rows).sum::<usize>();
2186 let batch = batches.iter().find(|batch| batch.num_rows() > 0).unwrap();
2187 let timestamp = batch
2188 .column(0)
2189 .as_any()
2190 .downcast_ref::<TimestampMillisecondArray>()
2191 .unwrap()
2192 .value(0);
2193 let value = batch
2194 .column(1)
2195 .as_any()
2196 .downcast_ref::<Float64Array>()
2197 .unwrap()
2198 .value(0);
2199 assert_eq!(
2200 (row_count, timestamp, value),
2201 (1, 750, 42.0),
2202 "only the evaluation before the stale marker should select 42.0"
2203 );
2204 }
2205
2206 #[tokio::test]
2207 async fn native_histogram_stale_nan_suppresses_exact_and_lookback() {
2208 let histograms = build_histogram_array(&[
2209 Some(native_histogram(42.0)),
2210 Some(native_histogram(f64::from_bits(PROMETHEUS_STALE_NAN_BITS))),
2211 ]);
2212 let schema = Arc::new(Schema::new(vec![
2213 Field::new(
2214 TIME_INDEX_COLUMN,
2215 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
2216 false,
2217 ),
2218 Field::new("value", histograms.data_type().clone(), true),
2219 ]));
2220 let batch = RecordBatch::try_new(
2221 schema.clone(),
2222 vec![
2223 Arc::new(TimestampMillisecondArray::from(vec![500, 1_000])),
2224 histograms,
2225 ],
2226 )
2227 .unwrap();
2228 let input = Arc::new(DataSourceExec::new(Arc::new(
2229 MemorySourceConfig::try_new(&[vec![batch]], schema, None).unwrap(),
2230 )));
2231 let exec = Arc::new(InstantManipulateExec {
2232 offset: 0,
2233 start: 1_000,
2234 end: 1_500,
2235 lookback_delta: 1_001,
2236 interval: 500,
2237 time_index_column: TIME_INDEX_COLUMN.to_string(),
2238 field_column: Some("value".to_string()),
2239 reuse_tsid_column: false,
2240 output_schema: input.schema(),
2241 properties: input.properties().clone(),
2242 input,
2243 metric: ExecutionPlanMetricsSet::new(),
2244 });
2245
2246 let context = SessionContext::default();
2247 let batches = datafusion::physical_plan::collect(exec, context.task_ctx())
2248 .await
2249 .unwrap();
2250
2251 assert_eq!(batches.iter().map(RecordBatch::num_rows).sum::<usize>(), 0);
2252 }
2253
2254 #[tokio::test]
2255 async fn null_value_backed_by_stale_bits_is_selected_for_exact_and_lookback() {
2256 let schema = Arc::new(Schema::new(vec![
2257 Field::new(
2258 TIME_INDEX_COLUMN,
2259 DataType::Timestamp(datafusion::arrow::datatypes::TimeUnit::Millisecond, None),
2260 false,
2261 ),
2262 Field::new("value", DataType::Float64, true),
2263 ]));
2264 let field_column = Float64Array::new(
2265 vec![f64::from_bits(0x7ff0_0000_0000_0002)].into(),
2266 Some(NullBuffer::from(vec![false])),
2267 );
2268 assert!(!field_column.is_valid(0));
2269 assert_eq!(field_column.value(0).to_bits(), 0x7ff0_0000_0000_0002);
2270 let batch = RecordBatch::try_new(
2271 schema.clone(),
2272 vec![
2273 Arc::new(TimestampMillisecondArray::from(vec![1_000])),
2274 Arc::new(field_column),
2275 ],
2276 )
2277 .unwrap();
2278 let input = Arc::new(DataSourceExec::new(Arc::new(
2279 MemorySourceConfig::try_new(&[vec![batch]], schema, None).unwrap(),
2280 )));
2281 let exec = Arc::new(InstantManipulateExec {
2282 offset: 0,
2283 start: 1_000,
2284 end: 1_500,
2285 lookback_delta: 1_000,
2286 interval: 500,
2287 time_index_column: TIME_INDEX_COLUMN.to_string(),
2288 field_column: Some("value".to_string()),
2289 reuse_tsid_column: false,
2290 output_schema: input.schema(),
2291 properties: input.properties().clone(),
2292 input,
2293 metric: ExecutionPlanMetricsSet::new(),
2294 });
2295
2296 let context = SessionContext::default();
2297 let batches = datafusion::physical_plan::collect(exec, context.task_ctx())
2298 .await
2299 .unwrap();
2300 assert_eq!(batches.iter().map(RecordBatch::num_rows).sum::<usize>(), 2);
2301 let batch = batches.iter().find(|batch| batch.num_rows() == 2).unwrap();
2302 let timestamps = batch
2303 .column(0)
2304 .as_any()
2305 .downcast_ref::<TimestampMillisecondArray>()
2306 .unwrap();
2307 let values = batch
2308 .column(1)
2309 .as_any()
2310 .downcast_ref::<Float64Array>()
2311 .unwrap();
2312
2313 assert_eq!(timestamps.values(), &[1_000, 1_500]);
2314 assert!(!values.is_valid(0));
2315 assert!(!values.is_valid(1));
2316 }
2317}