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::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/// Manipulate the input record batch to make it suitable for Instant Operator.
69///
70/// This plan will try to align the input time series, for every timestamp between
71/// `start` and `end` with step `interval`. Find in the `lookback` range if data
72/// is missing at the given timestamp.
73#[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    // Planner-provided tag-column hint for execution fast paths.
82    tag_columns: Vec<String>,
83    /// Primary sample column used to derive the columns checked for staleness.
84    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            // transform indices to names
194            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    /// Returns whether this node evaluates a single timestamp rather than a range.
309    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    // Prevent reordering of input
475    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        // The child may expose native timestamps, but our output schema uses ms.
490        // Retain its execution properties, not its schema equivalences or ordering.
491        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            // TODO(ruihang): support this column statistics
589            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    // Column index of TIME INDEX column's position in schema
625    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    /// Number of series processed.
635    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    /// Manipulates one complete series sorted by timestamp. The planner enforces
670    /// this input contract with a sort followed by [`SeriesDivide`].
671    ///
672    /// Prometheus `v3.9.1`'s `vectorSelectorSingle` uses a start-exclusive
673    /// lookback window `(eval_ts - lookback_delta, eval_ts]`; a sample at exactly
674    /// `eval_ts - lookback_delta` is too old.
675    pub fn manipulate(&self, input: RecordBatch) -> DataFusionResult<RecordBatch> {
676        let ts_column = input.column(self.time_index);
677        if ts_column.is_empty() {
678            // Returning Pending after consuming a ready batch without a wake can stall the stream.
679            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        // Shift the native-tick timeline in i128 before comparing samples. Doing
697        // this in the Arrow storage unit can overflow even when the shifted
698        // PromQL millisecond evaluation time is representable.
699        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        // An exact sample remains useful with zero lookback. Otherwise the lower
704        // boundary is exclusive, so subtract one nanosecond from its final window.
705        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            // Keep the first row among exact timestamp ties; otherwise use the
751            // latest preceding row. Zero lookback admits only exact samples.
752            // Test staleness after choosing: a selected stale marker suppresses
753            // this evaluation rather than falling back to an older finite value.
754            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    /// Helper function to apply "take" on record batch.
771    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        // Simulate a parent projection requesting only the `path` column.
1216        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}