Skip to main content

promql/extension_plan/
instant_manipulate.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// Manipulate the input record batch to make it suitable for Instant Operator.
66///
67/// This plan will try to align the input time series, for every timestamp between
68/// `start` and `end` with step `interval`. Find in the `lookback` range if data
69/// is missing at the given timestamp.
70#[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    // Planner-provided tag-column hint for execution fast paths.
78    tag_columns: Vec<String>,
79    /// Primary sample column used to derive the columns checked for staleness.
80    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            // transform indices to names
160            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    // Prevent reordering of input
365    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            // TODO(ruihang): support this column statistics
455            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    // Column index of TIME INDEX column's position in schema
490    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    /// Number of series processed.
499    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    /// Manipulates one complete series sorted by timestamp. The planner enforces
535    /// this input contract with a sort followed by [`SeriesDivide`].
536    ///
537    /// Prometheus `v3.9.1`'s `vectorSelectorSingle` uses a start-exclusive
538    /// lookback window `(eval_ts - lookback_delta, eval_ts]`; a sample at exactly
539    /// `eval_ts - lookback_delta` is too old.
540    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        // Early return for empty input
552        if ts_column.is_empty() {
553            return Ok(input);
554        }
555
556        // Field columns for staleness checks, classified once per batch.
557        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        // Optimize iteration range based on actual data bounds
568        let first_ts = ts_column.value(0);
569        let last_ts = ts_column.value(ts_column.len() - 1);
570        // A sample at `t` is eligible for eval time `eval_ts` iff:
571        //   t > eval_ts - lookback_delta  <=>  eval_ts < t + lookback_delta.
572        // Therefore the last eval timestamp for which the last sample is still eligible is:
573        //   last_ts + lookback_delta - 1 (millisecond granularity).
574        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        // calculate the offsets to take
604        'next: for expected_ts in aligned_ts_iter {
605            // first, search toward end to see if there is matched timestamp
606            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                            // Ignore the stale marker.
612                        } 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                // short cut this loop
626                if ts_column.value(cursor) + self.lookback_delta <= expected_ts {
627                    break;
628                }
629            }
630
631            // then examine the value
632            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                // exceeds current expected timestamp, examine the previous value
638                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                        // only use the point in the time range
642                        if is_stale(prev_cursor) {
643                            // Do not use a stale marker as the newest value.
644                            continue;
645                        }
646                        // use this point
647                        take_indices.push(prev_cursor as u64);
648                        aligned_ts.push(expected_ts);
649                    }
650                }
651            } else if is_stale(cursor) {
652                // Do not use a stale marker as the newest value.
653            } else {
654                // use this point
655                take_indices.push(cursor as u64);
656                aligned_ts.push(expected_ts);
657            }
658        }
659
660        // take record batch and replace the time index column
661        self.take_record_batch_optional(input, take_indices, aligned_ts)
662    }
663
664    /// Helper function to apply "take" on record batch.
665    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        // Simulate a parent projection requesting only the `path` column.
784        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}