Skip to main content

promql/extension_plan/
range_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::collections::HashSet;
16use std::ops::{Add, Mul, Sub};
17use std::pin::Pin;
18use std::sync::Arc;
19use std::task::{Context, Poll};
20
21use common_telemetry::{debug, warn};
22use datafusion::arrow::array::{Array, ArrayRef, Int64Array, TimestampMillisecondArray};
23use datafusion::arrow::compute;
24use datafusion::arrow::datatypes::{DataType, Field, Int64Type, SchemaRef, TimeUnit};
25use datafusion::arrow::error::ArrowError;
26use datafusion::arrow::record_batch::RecordBatch;
27use datafusion::common::stats::Precision;
28use datafusion::common::tree_node::TreeNodeRecursion;
29use datafusion::common::{DFSchema, DFSchemaRef, TableReference};
30use datafusion::error::{DataFusionError, Result as DataFusionResult};
31use datafusion::execution::context::TaskContext;
32use datafusion::logical_expr::{EmptyRelation, Expr, LogicalPlan, UserDefinedLogicalNodeCore};
33use datafusion::physical_expr::EquivalenceProperties;
34use datafusion::physical_plan::metrics::{
35    BaselineMetrics, Count, ExecutionPlanMetricsSet, MetricBuilder, MetricValue, MetricsSet,
36};
37use datafusion::physical_plan::{
38    ChildStats, DisplayAs, DisplayFormatType, Distribution, ExecutionPlan,
39    InputDistributionRequirements, PhysicalExpr, PlanProperties, RecordBatchStream,
40    SendableRecordBatchStream, Statistics, StatisticsArgs,
41};
42use datafusion_expr::ident;
43use datatypes::timestamp::timestamp_array_to_primitive;
44use futures::{Stream, StreamExt, ready};
45use greptime_proto::substrait_extension as pb;
46use prost::Message;
47use snafu::ResultExt;
48
49use crate::error::{DeserializeSnafu, Result};
50use crate::extension_plan::{
51    METRIC_NUM_SERIES, Millisecond, local_offset, nanoseconds_per_native_tick, resolve_column_name,
52    serialize_column_index, timestamp_unit,
53};
54use crate::metrics::PROMQL_SERIES_COUNT;
55use crate::range_array::RangeArray;
56
57/// Time series manipulator for range function.
58///
59/// This plan will "fold" time index and value columns into [RangeArray]s, and truncate
60/// other columns to the same length with the "folded" [RangeArray] column.
61///
62/// To pass runtime information to the execution plan (or the range function), This plan
63/// will add those extra columns:
64/// - timestamp range with type [RangeArray], which is the folded timestamp column.
65/// - end of current range with the same type as the timestamp column. (todo)
66#[derive(Debug, PartialEq, Eq, Hash)]
67pub struct RangeManipulate {
68    start: Millisecond,
69    end: Millisecond,
70    interval: Millisecond,
71    range: Millisecond,
72    offset: Millisecond,
73    time_index: String,
74    field_columns: Vec<String>,
75    input: LogicalPlan,
76    output_schema: DFSchemaRef,
77    unfix: Option<UnfixIndices>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Hash)]
81struct UnfixIndices {
82    pub time_index_idx: u64,
83    pub tag_column_indices: Vec<u64>,
84}
85
86impl RangeManipulate {
87    #[allow(clippy::too_many_arguments)]
88    pub fn new(
89        start: Millisecond,
90        end: Millisecond,
91        interval: Millisecond,
92        offset: Millisecond,
93        range: Millisecond,
94        time_index: String,
95        field_columns: Vec<String>,
96        input: LogicalPlan,
97    ) -> DataFusionResult<Self> {
98        let output_schema =
99            Self::calculate_output_schema(input.schema(), &time_index, &field_columns)?;
100        Ok(Self {
101            start,
102            end,
103            interval,
104            range,
105            offset,
106            time_index,
107            field_columns,
108            input,
109            output_schema,
110            unfix: None,
111        })
112    }
113
114    pub const fn name() -> &'static str {
115        "RangeManipulate"
116    }
117
118    pub fn build_timestamp_range_name(time_index: &str) -> String {
119        format!("{time_index}_range")
120    }
121
122    pub fn internal_range_end_col_name() -> String {
123        "__internal_range_end".to_string()
124    }
125
126    fn range_timestamp_name(&self) -> String {
127        Self::build_timestamp_range_name(&self.time_index)
128    }
129
130    fn calculate_output_schema(
131        input_schema: &DFSchemaRef,
132        time_index: &str,
133        field_columns: &[String],
134    ) -> DataFusionResult<DFSchemaRef> {
135        let columns = input_schema.fields();
136        let mut new_columns = Vec::with_capacity(columns.len() + 1);
137        for i in 0..columns.len() {
138            let x = input_schema.qualified_field(i);
139            new_columns.push((x.0.cloned(), x.1.clone()));
140        }
141
142        // process time index column
143        // the raw timestamp field is preserved. And a new timestamp_range field is appended to the last.
144        let Some(ts_col_index) = input_schema.index_of_column_by_name(None, time_index) else {
145            return Err(datafusion::common::field_not_found(
146                None::<TableReference>,
147                time_index,
148                input_schema.as_ref(),
149            ));
150        };
151        let ts_col_field = &columns[ts_col_index];
152        let output_time_field = Arc::new(
153            ts_col_field
154                .as_ref()
155                .clone()
156                .with_data_type(DataType::Timestamp(TimeUnit::Millisecond, None)),
157        );
158        new_columns[ts_col_index] = (
159            input_schema.qualified_field(ts_col_index).0.cloned(),
160            output_time_field.clone(),
161        );
162        let timestamp_range_field = Field::new(
163            Self::build_timestamp_range_name(time_index),
164            RangeArray::convert_field(output_time_field.as_ref())
165                .data_type()
166                .clone(),
167            ts_col_field.is_nullable(),
168        );
169        new_columns.push((None, Arc::new(timestamp_range_field)));
170
171        // process value columns
172        for name in field_columns {
173            let Some(index) = input_schema.index_of_column_by_name(None, name) else {
174                return Err(datafusion::common::field_not_found(
175                    None::<TableReference>,
176                    name,
177                    input_schema.as_ref(),
178                ));
179            };
180            new_columns[index] = (None, Arc::new(RangeArray::convert_field(&columns[index])));
181        }
182
183        Ok(Arc::new(DFSchema::new_with_metadata(
184            new_columns,
185            input_schema.metadata().clone(),
186        )?))
187    }
188
189    pub fn to_execution_plan(&self, exec_input: Arc<dyn ExecutionPlan>) -> Arc<dyn ExecutionPlan> {
190        let output_schema: SchemaRef = self.output_schema.inner().clone();
191        let properties = exec_input.properties();
192        let properties = Arc::new(PlanProperties::new(
193            EquivalenceProperties::new(output_schema.clone()),
194            properties.partitioning.clone(),
195            properties.emission_type,
196            properties.boundedness,
197        ));
198        Arc::new(RangeManipulateExec {
199            offset: self.offset,
200            start: self.start,
201            end: self.end,
202            interval: self.interval,
203            range: self.range,
204            time_index_column: self.time_index.clone(),
205            time_range_column: self.range_timestamp_name(),
206            field_columns: self.field_columns.clone(),
207            input: exec_input,
208            output_schema,
209            metric: ExecutionPlanMetricsSet::new(),
210            properties,
211        })
212    }
213
214    pub fn serialize(&self) -> Vec<u8> {
215        let time_index_idx = serialize_column_index(self.input.schema(), &self.time_index);
216
217        let tag_column_indices = self
218            .field_columns
219            .iter()
220            .map(|name| serialize_column_index(self.input.schema(), name))
221            .collect::<Vec<u64>>();
222
223        pb::RangeManipulate {
224            start: self.start,
225            end: self.end,
226            interval: self.interval,
227            range: self.range,
228            time_index_idx,
229            tag_column_indices,
230            ..Default::default()
231        }
232        .encode_to_vec()
233    }
234
235    pub fn deserialize(bytes: &[u8]) -> Result<Self> {
236        let pb_range_manipulate = pb::RangeManipulate::decode(bytes).context(DeserializeSnafu)?;
237        let empty_schema = Arc::new(DFSchema::empty());
238        let placeholder_plan = LogicalPlan::EmptyRelation(EmptyRelation {
239            produce_one_row: false,
240            schema: empty_schema.clone(),
241        });
242
243        let unfix = UnfixIndices {
244            time_index_idx: pb_range_manipulate.time_index_idx,
245            tag_column_indices: pb_range_manipulate.tag_column_indices.clone(),
246        };
247        debug!("RangeManipulate deserialize unfix: {:?}", unfix);
248
249        // Unlike `Self::new()`, this method doesn't check the input schema as it will fail
250        // because the input schema is empty.
251        // But this is Ok since datafusion guarantees to call `with_exprs_and_inputs` for the
252        // deserialized plan.
253        Ok(Self {
254            start: pb_range_manipulate.start,
255            end: pb_range_manipulate.end,
256            interval: pb_range_manipulate.interval,
257            range: pb_range_manipulate.range,
258            offset: 0,
259            time_index: String::new(),
260            field_columns: Vec::new(),
261            input: placeholder_plan,
262            output_schema: empty_schema,
263            unfix: Some(unfix),
264        })
265    }
266}
267
268impl PartialOrd for RangeManipulate {
269    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
270        // Compare fields in order excluding output_schema
271        match self.start.partial_cmp(&other.start) {
272            Some(core::cmp::Ordering::Equal) => {}
273            ord => return ord,
274        }
275        match self.end.partial_cmp(&other.end) {
276            Some(core::cmp::Ordering::Equal) => {}
277            ord => return ord,
278        }
279        match self.interval.partial_cmp(&other.interval) {
280            Some(core::cmp::Ordering::Equal) => {}
281            ord => return ord,
282        }
283        match self.range.partial_cmp(&other.range) {
284            Some(core::cmp::Ordering::Equal) => {}
285            ord => return ord,
286        }
287        match self.offset.partial_cmp(&other.offset) {
288            Some(core::cmp::Ordering::Equal) => {}
289            ord => return ord,
290        }
291        match self.time_index.partial_cmp(&other.time_index) {
292            Some(core::cmp::Ordering::Equal) => {}
293            ord => return ord,
294        }
295        match self.field_columns.partial_cmp(&other.field_columns) {
296            Some(core::cmp::Ordering::Equal) => {}
297            ord => return ord,
298        }
299        self.input.partial_cmp(&other.input)
300    }
301}
302
303impl UserDefinedLogicalNodeCore for RangeManipulate {
304    fn name(&self) -> &str {
305        Self::name()
306    }
307
308    fn inputs(&self) -> Vec<&LogicalPlan> {
309        vec![&self.input]
310    }
311
312    fn schema(&self) -> &DFSchemaRef {
313        &self.output_schema
314    }
315
316    fn expressions(&self) -> Vec<Expr> {
317        if self.unfix.is_some() {
318            return vec![];
319        }
320
321        let mut exprs = Vec::with_capacity(1 + self.field_columns.len());
322        exprs.push(ident(&self.time_index));
323        exprs.extend(self.field_columns.iter().map(ident));
324        exprs
325    }
326
327    fn necessary_children_exprs(&self, output_columns: &[usize]) -> Option<Vec<Vec<usize>>> {
328        if self.unfix.is_some() {
329            return None;
330        }
331
332        let input_schema = self.input.schema();
333        let input_len = input_schema.fields().len();
334        let time_index_idx = input_schema.index_of_column_by_name(None, &self.time_index)?;
335
336        if output_columns.is_empty() {
337            let indices = (0..input_len).collect::<Vec<_>>();
338            return Some(vec![indices]);
339        }
340
341        let mut required = Vec::with_capacity(output_columns.len() + 1 + self.field_columns.len());
342        required.push(time_index_idx);
343        for value_column in &self.field_columns {
344            required.push(input_schema.index_of_column_by_name(None, value_column)?);
345        }
346        for &idx in output_columns {
347            if idx < input_len {
348                required.push(idx);
349            } else if idx == input_len {
350                // Derived timestamp range column.
351                required.push(time_index_idx);
352            } else {
353                warn!(
354                    "Output column index {} is out of bounds for input schema with length {}",
355                    idx, input_len
356                );
357                return None;
358            }
359        }
360
361        required.sort_unstable();
362        required.dedup();
363        Some(vec![required])
364    }
365
366    fn fmt_for_explain(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
367        write!(
368            f,
369            "PromRangeManipulate: req range=[{}..{}], interval=[{}], eval range=[{}], time index=[{}], values={:?}",
370            self.start, self.end, self.interval, self.range, self.time_index, self.field_columns
371        )
372    }
373
374    fn with_exprs_and_inputs(
375        &self,
376        _exprs: Vec<Expr>,
377        mut inputs: Vec<LogicalPlan>,
378    ) -> DataFusionResult<Self> {
379        if inputs.len() != 1 {
380            return Err(DataFusionError::Internal(
381                "RangeManipulate should have at exact one input".to_string(),
382            ));
383        }
384
385        let input: LogicalPlan = inputs.pop().unwrap();
386        let input_schema = input.schema();
387
388        if let Some(unfix) = &self.unfix {
389            // transform indices to names
390            let time_index = resolve_column_name(
391                unfix.time_index_idx,
392                input_schema,
393                "RangeManipulate",
394                "time index",
395            )?;
396
397            let field_columns = unfix
398                .tag_column_indices
399                .iter()
400                .map(|idx| resolve_column_name(*idx, input_schema, "RangeManipulate", "tag"))
401                .collect::<DataFusionResult<Vec<String>>>()?;
402
403            let output_schema =
404                Self::calculate_output_schema(input_schema, &time_index, &field_columns)?;
405
406            Ok(Self {
407                start: self.start,
408                end: self.end,
409                interval: self.interval,
410                range: self.range,
411                offset: local_offset(&input, &time_index),
412                time_index,
413                field_columns,
414                input,
415                output_schema,
416                unfix: None,
417            })
418        } else {
419            let output_schema =
420                Self::calculate_output_schema(input_schema, &self.time_index, &self.field_columns)?;
421
422            Ok(Self {
423                start: self.start,
424                end: self.end,
425                interval: self.interval,
426                range: self.range,
427                offset: self.offset,
428                time_index: self.time_index.clone(),
429                field_columns: self.field_columns.clone(),
430                input,
431                output_schema,
432                unfix: None,
433            })
434        }
435    }
436}
437
438#[derive(Debug)]
439pub struct RangeManipulateExec {
440    offset: Millisecond,
441    start: Millisecond,
442    end: Millisecond,
443    interval: Millisecond,
444    range: Millisecond,
445    time_index_column: String,
446    time_range_column: String,
447    field_columns: Vec<String>,
448
449    input: Arc<dyn ExecutionPlan>,
450    output_schema: SchemaRef,
451    metric: ExecutionPlanMetricsSet,
452    properties: Arc<PlanProperties>,
453}
454
455impl ExecutionPlan for RangeManipulateExec {
456    fn apply_expressions(
457        &self,
458        _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> datafusion_common::Result<TreeNodeRecursion>,
459    ) -> DataFusionResult<TreeNodeRecursion> {
460        Ok(TreeNodeRecursion::Continue)
461    }
462
463    fn schema(&self) -> SchemaRef {
464        self.output_schema.clone()
465    }
466
467    fn properties(&self) -> &Arc<PlanProperties> {
468        &self.properties
469    }
470
471    fn maintains_input_order(&self) -> Vec<bool> {
472        vec![true; self.children().len()]
473    }
474
475    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
476        vec![&self.input]
477    }
478
479    fn input_distribution_requirements(&self) -> InputDistributionRequirements {
480        let input_requirement = self
481            .input
482            .input_distribution_requirements()
483            .into_per_child();
484        if input_requirement.is_empty() {
485            // if the input is EmptyMetric, its required_input_distribution() is empty so we can't
486            // use its input distribution.
487            InputDistributionRequirements::new(vec![Distribution::UnspecifiedDistribution])
488        } else {
489            InputDistributionRequirements::new(input_requirement)
490        }
491    }
492
493    fn with_new_children(
494        self: Arc<Self>,
495        children: Vec<Arc<dyn ExecutionPlan>>,
496    ) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
497        assert!(!children.is_empty());
498        let exec_input = children[0].clone();
499        let properties = exec_input.properties();
500        let properties = Arc::new(PlanProperties::new(
501            EquivalenceProperties::new(self.output_schema.clone()),
502            properties.partitioning.clone(),
503            properties.emission_type,
504            properties.boundedness,
505        ));
506        Ok(Arc::new(Self {
507            offset: self.offset,
508            start: self.start,
509            end: self.end,
510            interval: self.interval,
511            range: self.range,
512            time_index_column: self.time_index_column.clone(),
513            time_range_column: self.time_range_column.clone(),
514            field_columns: self.field_columns.clone(),
515            output_schema: self.output_schema.clone(),
516            input: children[0].clone(),
517            metric: self.metric.clone(),
518            properties,
519        }))
520    }
521
522    fn execute(
523        &self,
524        partition: usize,
525        context: Arc<TaskContext>,
526    ) -> DataFusionResult<SendableRecordBatchStream> {
527        let baseline_metric = BaselineMetrics::new(&self.metric, partition);
528        let metrics_builder = MetricBuilder::new(&self.metric);
529        let num_series = Count::new();
530        metrics_builder
531            .with_partition(partition)
532            .build(MetricValue::Count {
533                name: METRIC_NUM_SERIES.into(),
534                count: num_series.clone(),
535            });
536
537        let input = self.input.execute(partition, context)?;
538        let schema = input.schema();
539        let time_index = schema
540            .column_with_name(&self.time_index_column)
541            .unwrap_or_else(|| panic!("time index column {} not found", self.time_index_column))
542            .0;
543        let field_columns = self
544            .field_columns
545            .iter()
546            .map(|value_col| {
547                schema
548                    .column_with_name(value_col)
549                    .unwrap_or_else(|| panic!("value column {value_col} not found",))
550                    .0
551            })
552            .collect();
553        let time_unit = timestamp_unit(schema.field(time_index).data_type())?;
554        let aligned_ts_array =
555            RangeManipulateStream::build_aligned_ts_array(self.start, self.end, self.interval);
556        Ok(Box::pin(RangeManipulateStream {
557            offset: self.offset,
558            start: self.start,
559            end: self.end,
560            interval: self.interval,
561            range: self.range,
562            time_index,
563            time_unit,
564            field_columns,
565            aligned_ts_array,
566            output_schema: self.output_schema.clone(),
567            input,
568            metric: baseline_metric,
569            num_series,
570        }))
571    }
572
573    fn metrics(&self) -> Option<MetricsSet> {
574        Some(self.metric.clone_inner())
575    }
576
577    fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
578        vec![ChildStats::At(partition)]
579    }
580
581    fn statistics_from_inputs(
582        &self,
583        input_stats: &[Arc<Statistics>],
584        _args: &StatisticsArgs,
585    ) -> DataFusionResult<Arc<Statistics>> {
586        let input_stats = &input_stats[0];
587
588        let estimated_row_num = (self.end - self.start) as f64 / self.interval as f64;
589        let estimated_total_bytes = input_stats
590            .total_byte_size
591            .get_value()
592            .zip(input_stats.num_rows.get_value())
593            .map(|(size, rows)| {
594                Precision::Inexact(((*size as f64 / *rows as f64) * estimated_row_num).floor() as _)
595            })
596            .unwrap_or_default();
597
598        Ok(Arc::new(Statistics {
599            num_rows: Precision::Inexact(estimated_row_num as _),
600            total_byte_size: estimated_total_bytes,
601            // TODO(ruihang): support this column statistics
602            column_statistics: Statistics::unknown_column(&self.schema()),
603        }))
604    }
605
606    fn name(&self) -> &str {
607        "RangeManipulateExec"
608    }
609}
610
611impl DisplayAs for RangeManipulateExec {
612    fn fmt_as(&self, t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
613        match t {
614            DisplayFormatType::Default
615            | DisplayFormatType::Verbose
616            | DisplayFormatType::TreeRender => {
617                write!(
618                    f,
619                    "PromRangeManipulateExec: req range=[{}..{}], interval=[{}], eval range=[{}], time index=[{}]",
620                    self.start, self.end, self.interval, self.range, self.time_index_column
621                )
622            }
623        }
624    }
625}
626
627pub struct RangeManipulateStream {
628    offset: Millisecond,
629    start: Millisecond,
630    end: Millisecond,
631    interval: Millisecond,
632    range: Millisecond,
633    time_index: usize,
634    time_unit: TimeUnit,
635    field_columns: Vec<usize>,
636    aligned_ts_array: ArrayRef,
637
638    output_schema: SchemaRef,
639    input: SendableRecordBatchStream,
640    metric: BaselineMetrics,
641    /// Number of series processed.
642    num_series: Count,
643}
644
645impl RecordBatchStream for RangeManipulateStream {
646    fn schema(&self) -> SchemaRef {
647        self.output_schema.clone()
648    }
649}
650
651impl Stream for RangeManipulateStream {
652    type Item = DataFusionResult<RecordBatch>;
653
654    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
655        let poll = loop {
656            match ready!(self.input.poll_next_unpin(cx)) {
657                Some(Ok(batch)) => {
658                    let timer = std::time::Instant::now();
659                    let result = self.manipulate(batch);
660                    if let Ok(None) = result {
661                        self.metric.elapsed_compute().add_elapsed(timer);
662                        continue;
663                    } else {
664                        self.num_series.add(1);
665                        self.metric.elapsed_compute().add_elapsed(timer);
666                        break Poll::Ready(result.transpose());
667                    }
668                }
669                None => {
670                    PROMQL_SERIES_COUNT.observe(self.num_series.value() as f64);
671                    break Poll::Ready(None);
672                }
673                Some(Err(e)) => break Poll::Ready(Some(Err(e))),
674            }
675        };
676        self.metric.record_poll(poll)
677    }
678}
679
680impl RangeManipulateStream {
681    // Prometheus: https://github.com/prometheus/prometheus/blob/e934d0f01158a1d55fa0ebb035346b195fcc1260/promql/engine.go#L1113-L1198
682    // But they are not exactly the same, because we don't eager-evaluate on the data in this plan.
683    // And the generated timestamp is not aligned to the step. It's expected to do later.
684    pub fn manipulate(&self, input: RecordBatch) -> DataFusionResult<Option<RecordBatch>> {
685        // Prometheus sample identity is one sample per series per timestamp, and this operator's
686        // input is one series per batch (the planner inserts a `SeriesDivide` for that). A read
687        // path that does not deduplicate can still deliver two rows for one timestamp, and every
688        // range function reads its samples from here, so collapse them once instead of
689        // special-casing each function. Comparing native ticks rather than the millisecond
690        // payload keeps distinct sub-millisecond samples apart.
691        //
692        // The first row of a tied group wins. A deduplicating scan also keeps the first row
693        // after ordering equal primary keys and timestamps by descending sequence, so the values
694        // agree when the scan presents those rows newest-first. This operator cannot establish
695        // that order itself: unordered scans and scans whose sequence values were overwritten
696        // can select a different tied value. The collapse fixes sample multiplicity, not global
697        // newest-write conflict resolution.
698        let (native_timestamps, _) = timestamp_array_to_primitive(input.column(self.time_index))
699            .ok_or_else(|| {
700                DataFusionError::Execution("Time index column is not a timestamp".into())
701            })?;
702        let native_values = native_timestamps.values().as_ref();
703        let first_duplicate = if native_timestamps.null_count() == 0 {
704            native_values
705                .windows(2)
706                .position(|pair| pair[0] == pair[1])
707                .map(|index| index + 1)
708        } else {
709            (1..input.num_rows()).find(|&i| {
710                !native_timestamps.is_null(i - 1)
711                    && !native_timestamps.is_null(i)
712                    && native_values[i - 1] == native_values[i]
713            })
714        };
715        let indices = first_duplicate.map(|first_duplicate| {
716            let mut indices = Vec::with_capacity(input.num_rows());
717            indices.extend(0..first_duplicate);
718            indices.extend((first_duplicate + 1..input.num_rows()).filter(|&i| {
719                native_timestamps.is_null(i)
720                    || native_timestamps.is_null(i - 1)
721                    || native_values[i] != native_values[i - 1]
722            }));
723            Int64Array::from_iter_values(indices.into_iter().map(|index| index as i64))
724        });
725        // Only sample columns need collapsing. Tags below still come from row zero of the
726        // original batch, and the aligned time index is replaced below. In particular, do not
727        // build a RecordBatch with collapsed sample columns and uncollapsed tags: Arrow requires
728        // all columns to have the same length.
729        let ts_column = match indices.as_ref() {
730            Some(indices) => compute::take(input.column(self.time_index), indices, None)?,
731            None => input.column(self.time_index).clone(),
732        };
733        let mut other_columns = (0..input.columns().len()).collect::<HashSet<_>>();
734        other_columns.remove(&self.time_index);
735        // calculate the range
736        let (ranges, (start, end)) = self.calculate_range_on_timestamps(&ts_column)?;
737        // ignore this if all ranges are empty
738        if ranges.iter().all(|(_, len)| *len == 0) {
739            return Ok(None);
740        }
741
742        // transform columns
743        let mut new_columns = input.columns().to_vec();
744        for index in self.field_columns.iter() {
745            let _ = other_columns.remove(index);
746            let column = match indices.as_ref() {
747                Some(indices) => compute::take(input.column(*index), indices, None)?,
748                None => input.column(*index).clone(),
749            };
750            let new_column = Arc::new(
751                RangeArray::from_ranges(column, ranges.clone())
752                    .map_err(|e| ArrowError::InvalidArgumentError(e.to_string()))?
753                    .into_dict(),
754            );
755            new_columns[*index] = new_column;
756        }
757
758        // The timestamp range payload is always millisecond ABI. A column is reusable as-is
759        // only when its own data type already is that ABI: the output schema fixes the
760        // dictionary value type, so a millisecond column carrying a timezone must be
761        // converted like any other native unit.
762        let timestamp_values: ArrayRef = if self.offset == 0
763            && matches!(
764                ts_column.data_type(),
765                DataType::Timestamp(TimeUnit::Millisecond, None)
766            ) {
767            ts_column.clone()
768        } else {
769            // Shift in wide native precision before truncating toward zero, preserving
770            // null validity.
771            let scale = nanoseconds_per_native_tick(self.time_unit);
772            let (timestamps, _) = timestamp_array_to_primitive(&ts_column).ok_or_else(|| {
773                DataFusionError::Execution("Time index column is not a timestamp".into())
774            })?;
775            // Single pass over the timestamps: an i128 intermediate holds any native
776            // timestamp shift; only the final millisecond result must fit i64. Null-free
777            // batches (the dominant PromQL case) take arrow's infallible `unary` kernel,
778            // which LLVM can vectorize, and report overflow through a shared cell. But
779            // `unary` visits NULL slots' underlying values, so a NULL slot holding an
780            // arbitrary overflowing value would raise a false batch-wide error; nullable
781            // batches therefore take `try_unary`, which applies the closure to valid rows
782            // only.
783            let offset_ns = (self.offset as i128) * 1_000_000;
784            let timestamp_values = if timestamps.null_count() == 0 {
785                let overflow = std::cell::Cell::new(false);
786                let shifted = compute::unary::<Int64Type, _, Int64Type>(&timestamps, |v| {
787                    let shifted_ns = (v as i128) * scale + offset_ns;
788                    let shifted_ms = shifted_ns / 1_000_000;
789                    match i64::try_from(shifted_ms) {
790                        Ok(ms) => ms,
791                        Err(_) => {
792                            overflow.set(true);
793                            0
794                        }
795                    }
796                });
797                if overflow.get() {
798                    return Err(DataFusionError::Execution(
799                        "RangeManipulate timestamp payload overflow".into(),
800                    ));
801                }
802                shifted
803            } else {
804                compute::try_unary::<Int64Type, _, Int64Type>(&timestamps, |v| {
805                    let shifted_ns = (v as i128) * scale + offset_ns;
806                    i64::try_from(shifted_ns / 1_000_000).map_err(|_| {
807                        ArrowError::ComputeError(
808                            "RangeManipulate timestamp payload overflow".into(),
809                        )
810                    })
811                })?
812            };
813            Arc::new(TimestampMillisecondArray::new(
814                timestamp_values.values().clone(),
815                timestamp_values.nulls().cloned(),
816            ))
817        };
818        let ts_range_column = RangeArray::from_ranges(timestamp_values, ranges.clone())
819            .map_err(|e| ArrowError::InvalidArgumentError(e.to_string()))?
820            .into_dict();
821        new_columns.push(Arc::new(ts_range_column));
822
823        // truncate other columns
824        let take_indices = Int64Array::from(vec![0; ranges.len()]);
825        for index in other_columns.into_iter() {
826            new_columns[index] = compute::take(&input.column(index), &take_indices, None)?;
827        }
828        // replace timestamp with the aligned one
829        let new_time_index = if ranges.len() != self.aligned_ts_array.len() {
830            Self::build_aligned_ts_array(start, end, self.interval)
831        } else {
832            self.aligned_ts_array.clone()
833        };
834        new_columns[self.time_index] = new_time_index;
835
836        RecordBatch::try_new(self.output_schema.clone(), new_columns)
837            .map(Some)
838            .map_err(|e| DataFusionError::ArrowError(Box::new(e), None))
839    }
840
841    fn build_aligned_ts_array(start: i64, end: i64, interval: i64) -> ArrayRef {
842        Arc::new(TimestampMillisecondArray::from_iter_values(
843            (start..=end).step_by(interval as _),
844        ))
845    }
846
847    /// Return values:
848    /// - A vector of tuples where each tuple contains the start index and length of the range.
849    /// - A tuple of the actual start/end timestamp used to calculate the range.
850    #[cfg(test)]
851    #[allow(clippy::type_complexity)]
852    fn calculate_range(
853        &self,
854        input: &RecordBatch,
855    ) -> DataFusionResult<(Vec<(u32, u32)>, (i64, i64))> {
856        self.calculate_range_on_timestamps(input.column(self.time_index))
857    }
858
859    #[allow(clippy::type_complexity)]
860    fn calculate_range_on_timestamps(
861        &self,
862        ts_column: &ArrayRef,
863    ) -> DataFusionResult<(Vec<(u32, u32)>, (i64, i64))> {
864        let scale = nanoseconds_per_native_tick(self.time_unit);
865        let (timestamps, _) = timestamp_array_to_primitive(ts_column).ok_or_else(|| {
866            DataFusionError::Execution("Time index column is not a timestamp".into())
867        })?;
868        let timestamp_null_count = timestamps.null_count();
869        let timestamps = timestamps.values();
870        let timestamp =
871            |index| (timestamps[index] as i128) * scale + (self.offset as i128) * 1_000_000;
872        let len = timestamps.len();
873        if len == 0 {
874            return Ok((vec![], (self.start, self.end)));
875        }
876
877        // Shorten the range using wide arithmetic so timestamps near the native
878        // type limits retain every query-aligned evaluation point.
879        let query_start = self.start as i128;
880        let query_end = self.end as i128;
881        let interval = self.interval as i128;
882        let first_ts = timestamp(0).div_euclid(1_000_000);
883        // Preserve the query's alignment pattern when optimizing start time.
884        let remainder = (first_ts - query_start).rem_euclid(interval);
885        let first_ts_aligned = first_ts + (interval - remainder).rem_euclid(interval);
886        let last_ts_with_range =
887            (timestamp(len - 1) + (self.range as i128) * 1_000_000).div_euclid(1_000_000);
888        let remainder = (last_ts_with_range - query_start).rem_euclid(interval);
889        let last_ts_aligned = last_ts_with_range - remainder;
890        let start = query_start.max(first_ts_aligned);
891        let end = query_end.min(last_ts_aligned);
892        if start > end {
893            return Ok((vec![], (self.start, self.end)));
894        }
895        // The intersection is within the declared i64 query bounds.
896        let start = start as i64;
897        let end = end as i64;
898
899        // Range membership is decided on shifted native ticks, before the
900        // timestamp-range payload is converted to its millisecond ABI. This
901        // keeps sub-millisecond samples distinct in a range; equal millisecond
902        // payload values are not a reason to deduplicate input samples.
903        //
904        // Calculate for every aligned timestamp (`curr_ts`), assuming ordered
905        // timestamps: the scan compares shifted samples in `i64` nanoseconds
906        // whenever they fit the batch, and in exact `i128` nanoseconds otherwise.
907        let narrow = (timestamp_null_count == 0)
908            .then(|| {
909                narrow_scan(
910                    self.time_unit,
911                    self.offset,
912                    self.range,
913                    timestamps,
914                    start,
915                    end,
916                )
917            })
918            .flatten();
919        let ranges = match narrow {
920            Some(scan) => scan_ranges::<i64>(self.interval, timestamps, start, end, &scan),
921            None => {
922                let scan = ScanNanoseconds::<i128>::wide(self.time_unit, self.offset, self.range);
923                scan_ranges::<i128>(self.interval, timestamps, start, end, &scan)
924            }
925        };
926
927        Ok((ranges, (start, end)))
928    }
929
930    /// Cursor scan of `timestamps` through the narrow `i64` carrier, for benchmarks.
931    ///
932    /// `bench_range_fn` times the fast path directly, next to the exact `i128` fallback in
933    /// [`RangeManipulateStream::bench_scan_wide`]; production selects the carrier through
934    /// [`narrow_scan`] inside `calculate_range`. Returns `None` when that carrier is
935    /// rejected, which is exactly the condition that makes the operator fall back to `i128`.
936    #[doc(hidden)]
937    pub fn bench_scan_narrow(
938        time_unit: TimeUnit,
939        offset: Millisecond,
940        window: Millisecond,
941        interval: Millisecond,
942        timestamps: &[i64],
943        start: Millisecond,
944        end: Millisecond,
945    ) -> Option<Vec<(u32, u32)>> {
946        let scan = narrow_scan(time_unit, offset, window, timestamps, start, end)?;
947        Some(scan_ranges::<i64>(interval, timestamps, start, end, &scan))
948    }
949
950    /// Cursor scan of `timestamps` through the exact `i128` carrier, for benchmarks.
951    ///
952    /// The carrier is exact for every input, so this times the fallback that
953    /// [`narrow_scan`] selects whenever the narrow baselines would overflow.
954    #[doc(hidden)]
955    pub fn bench_scan_wide(
956        time_unit: TimeUnit,
957        offset: Millisecond,
958        window: Millisecond,
959        interval: Millisecond,
960        timestamps: &[i64],
961        start: Millisecond,
962        end: Millisecond,
963    ) -> Vec<(u32, u32)> {
964        scan_ranges::<i128>(
965            interval,
966            timestamps,
967            start,
968            end,
969            &ScanNanoseconds::<i128>::wide(time_unit, offset, window),
970        )
971    }
972}
973
974/// Cursor scan over the aligned evaluation points, in `Ns` nanoseconds.
975fn scan_ranges<Ns: Nanoseconds>(
976    interval: Millisecond,
977    timestamps: &[i64],
978    start: Millisecond,
979    end: Millisecond,
980    scan: &ScanNanoseconds<Ns>,
981) -> Vec<(u32, u32)> {
982    let len = timestamps.len();
983    let mut ranges = Vec::new();
984    let mut left = 0usize;
985    let mut right = 0usize;
986    for curr_ts in (start..=end).step_by(interval as _) {
987        let curr_ts_ns = scan.instant(curr_ts);
988        let start_ts = scan.window_start(curr_ts_ns);
989
990        while left < len && scan.shift(timestamps[left]) <= start_ts {
991            left += 1;
992        }
993        right = right.max(left);
994        while right < len && scan.shift(timestamps[right]) <= curr_ts_ns {
995            right += 1;
996        }
997
998        if left == right {
999            ranges.push((0, 0));
1000        } else {
1001            ranges.push((left as _, (right - left) as _));
1002        }
1003    }
1004    ranges
1005}
1006
1007/// Cursor scan baselines in nanoseconds, shared with `InstantManipulate`.
1008///
1009/// The narrow `i64` baselines come from [`narrow_scan`], the exact ones from
1010/// [`ScanNanoseconds::wide`].
1011#[derive(Debug, Clone, Copy)]
1012pub(super) struct ScanNanoseconds<Ns> {
1013    /// Nanoseconds per timestamp tick.
1014    scale: Ns,
1015    /// The offset shift in nanoseconds.
1016    offset_ns: Ns,
1017    /// The range or lookback window in nanoseconds.
1018    window_ns: Ns,
1019}
1020
1021/// Nanoseconds carrier of the cursor scans: `i64` where [`narrow_scan`] proved that
1022/// the batch fits, `i128` for the exact fallback.
1023pub(super) trait Nanoseconds:
1024    Copy + PartialOrd + From<i64> + Add<Output = Self> + Sub<Output = Self> + Mul<Output = Self>
1025{
1026}
1027
1028impl<T> Nanoseconds for T where
1029    T: Copy + PartialOrd + From<i64> + Add<Output = Self> + Sub<Output = Self> + Mul<Output = Self>
1030{
1031}
1032
1033impl ScanNanoseconds<i128> {
1034    /// The `i128` baselines, exact for every input.
1035    pub(super) fn wide(time_unit: TimeUnit, offset: Millisecond, window: Millisecond) -> Self {
1036        Self {
1037            scale: nanoseconds_per_native_tick(time_unit),
1038            offset_ns: i128::from(offset) * 1_000_000,
1039            window_ns: i128::from(window) * 1_000_000,
1040        }
1041    }
1042}
1043
1044impl<Ns: Nanoseconds> ScanNanoseconds<Ns> {
1045    /// Shifted nanoseconds of an input timestamp.
1046    pub(super) fn shift(&self, timestamp: i64) -> Ns {
1047        Ns::from(timestamp) * self.scale + self.offset_ns
1048    }
1049
1050    /// Nanoseconds of an evaluation instant given in milliseconds.
1051    pub(super) fn instant(&self, millis: Millisecond) -> Ns {
1052        Ns::from(millis) * Ns::from(1_000_000)
1053    }
1054
1055    /// The exclusive lower boundary of the window ending at `instant`.
1056    pub(super) fn window_start(&self, instant: Ns) -> Ns {
1057        instant - self.window_ns
1058    }
1059}
1060
1061/// The narrow `i64` baselines of a sorted batch, or `None` when they would overflow.
1062pub(super) fn narrow_scan(
1063    time_unit: TimeUnit,
1064    offset: Millisecond,
1065    window: Millisecond,
1066    timestamps: &[i64],
1067    start: Millisecond,
1068    end: Millisecond,
1069) -> Option<ScanNanoseconds<i64>> {
1070    let scale = i64::try_from(nanoseconds_per_native_tick(time_unit)).ok()?;
1071    let offset_ns = offset.checked_mul(1_000_000)?;
1072    let window_ns = window.checked_mul(1_000_000)?;
1073    // Sorted samples, so the checked endpoints bound every interior shift and product.
1074    let shift = |timestamp: i64| timestamp.checked_mul(scale)?.checked_add(offset_ns);
1075    shift(*timestamps.first()?)?;
1076    shift(*timestamps.last()?)?;
1077    // The checked `[start, end]` bounds every instant and every `instant - window`.
1078    start.checked_mul(1_000_000)?.checked_sub(window_ns)?;
1079    end.checked_mul(1_000_000)?.checked_sub(window_ns)?;
1080    Some(ScanNanoseconds {
1081        scale,
1082        offset_ns,
1083        window_ns,
1084    })
1085}
1086
1087#[cfg(test)]
1088mod test {
1089    use datafusion::arrow::array::{
1090        ArrayRef, DictionaryArray, Float64Array, StringArray, TimestampMicrosecondArray,
1091        TimestampNanosecondArray, TimestampSecondArray,
1092    };
1093    use datafusion::arrow::buffer::NullBuffer;
1094    use datafusion::arrow::datatypes::{
1095        ArrowPrimitiveType, DataType, Field, Int64Type, Schema, TimestampMillisecondType,
1096    };
1097    use datafusion::common::ToDFSchema;
1098    use datafusion::datasource::memory::MemorySourceConfig;
1099    use datafusion::datasource::source::DataSourceExec;
1100    use datafusion::logical_expr::{
1101        EmptyRelation, Extension, LogicalPlan, UserDefinedLogicalNodeCore,
1102    };
1103    use datafusion::physical_expr::Partitioning;
1104    use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType};
1105    use datafusion::physical_plan::memory::MemoryStream;
1106    use datafusion::physical_plan::{ChildrenPropertiesMode, ReplaceChildrenOptions};
1107    use datafusion::prelude::SessionContext;
1108    use datatypes::arrow::array::TimestampMillisecondArray;
1109    use futures::FutureExt;
1110
1111    use super::*;
1112
1113    const TIME_INDEX_COLUMN: &str = "timestamp";
1114
1115    /// The `(offset, length)` selections of one cursor scan.
1116    type RangeSelections = Vec<(u32, u32)>;
1117
1118    fn project_batch(batch: &RecordBatch, indices: &[usize]) -> RecordBatch {
1119        let fields = indices
1120            .iter()
1121            .map(|&idx| batch.schema().field(idx).clone())
1122            .collect::<Vec<_>>();
1123        let columns = indices
1124            .iter()
1125            .map(|&idx| batch.column(idx).clone())
1126            .collect::<Vec<_>>();
1127        let schema = Arc::new(Schema::new(fields));
1128        RecordBatch::try_new(schema, columns).unwrap()
1129    }
1130
1131    fn prepare_test_data() -> DataSourceExec {
1132        let schema = Arc::new(Schema::new(vec![
1133            Field::new(TIME_INDEX_COLUMN, TimestampMillisecondType::DATA_TYPE, true),
1134            Field::new("value_1", DataType::Float64, true),
1135            Field::new("value_2", DataType::Float64, true),
1136            Field::new("path", DataType::Utf8, true),
1137        ]));
1138        let timestamp_column = Arc::new(TimestampMillisecondArray::from(vec![
1139            0, 30_000, 60_000, 90_000, 120_000, // every 30s
1140            180_000, 240_000, // every 60s
1141            241_000, 271_000, 291_000, // others
1142        ])) as _;
1143        let field_column: ArrayRef = Arc::new(Float64Array::from(vec![1.0; 10])) as _;
1144        let path_column = Arc::new(StringArray::from(vec!["foo"; 10])) as _;
1145        let data = RecordBatch::try_new(
1146            schema.clone(),
1147            vec![
1148                timestamp_column,
1149                field_column.clone(),
1150                field_column,
1151                path_column,
1152            ],
1153        )
1154        .unwrap();
1155
1156        DataSourceExec::new(Arc::new(
1157            MemorySourceConfig::try_new(&[vec![data]], schema, None).unwrap(),
1158        ))
1159    }
1160
1161    async fn do_normalize_test(
1162        start: Millisecond,
1163        end: Millisecond,
1164        interval: Millisecond,
1165        range: Millisecond,
1166        expected: String,
1167    ) {
1168        let memory_exec = Arc::new(prepare_test_data());
1169        let time_index = TIME_INDEX_COLUMN.to_string();
1170        let field_columns = vec!["value_1".to_string(), "value_2".to_string()];
1171        let manipulate_output_schema = SchemaRef::new(
1172            RangeManipulate::calculate_output_schema(
1173                &memory_exec.schema().to_dfschema_ref().unwrap(),
1174                &time_index,
1175                &field_columns,
1176            )
1177            .unwrap()
1178            .as_arrow()
1179            .clone(),
1180        );
1181        let properties = Arc::new(PlanProperties::new(
1182            EquivalenceProperties::new(manipulate_output_schema.clone()),
1183            Partitioning::UnknownPartitioning(1),
1184            EmissionType::Incremental,
1185            Boundedness::Bounded,
1186        ));
1187        let normalize_exec = Arc::new(RangeManipulateExec {
1188            offset: 0,
1189            start,
1190            end,
1191            interval,
1192            range,
1193            field_columns,
1194            output_schema: manipulate_output_schema,
1195            time_range_column: RangeManipulate::build_timestamp_range_name(&time_index),
1196            time_index_column: time_index,
1197            input: memory_exec,
1198            metric: ExecutionPlanMetricsSet::new(),
1199            properties,
1200        });
1201        let session_context = SessionContext::default();
1202        let result = datafusion::physical_plan::collect(normalize_exec, session_context.task_ctx())
1203            .await
1204            .unwrap();
1205        // DirectoryArray from RangeArray cannot be print as normal arrays.
1206        let result_literal: String = result
1207            .into_iter()
1208            .filter_map(|batch| {
1209                batch
1210                    .columns()
1211                    .iter()
1212                    .map(|array| {
1213                        if matches!(array.data_type(), &DataType::Dictionary(..)) {
1214                            let dict_array = array
1215                                .as_any()
1216                                .downcast_ref::<DictionaryArray<Int64Type>>()
1217                                .unwrap()
1218                                .clone();
1219                            format!("{:?}", RangeArray::try_new(dict_array).unwrap())
1220                        } else {
1221                            format!("{array:?}")
1222                        }
1223                    })
1224                    .reduce(|lhs, rhs| lhs + "\n" + &rhs)
1225            })
1226            .reduce(|lhs, rhs| lhs + "\n\n" + &rhs)
1227            .unwrap();
1228
1229        assert_eq!(result_literal, expected);
1230    }
1231
1232    #[tokio::test]
1233    async fn duplicate_native_timestamps_collapse_in_real_plan() {
1234        use datafusion::arrow::array::Float64Array;
1235
1236        let schema = Arc::new(Schema::new(vec![
1237            Field::new(
1238                TIME_INDEX_COLUMN,
1239                DataType::Timestamp(TimeUnit::Millisecond, None),
1240                false,
1241            ),
1242            Field::new("value", DataType::Float64, false),
1243        ]));
1244        let batch = RecordBatch::try_new(
1245            schema.clone(),
1246            vec![
1247                Arc::new(TimestampMillisecondArray::from(vec![
1248                    29_600, 29_600, 59_600, 59_600,
1249                ])),
1250                Arc::new(Float64Array::from(vec![100.0, 100.0, 102.0, 102.0])),
1251            ],
1252        )
1253        .unwrap();
1254        let plan = RangeManipulate::new(
1255            60_000,
1256            60_000,
1257            60_000,
1258            0,
1259            60_000,
1260            TIME_INDEX_COLUMN.to_string(),
1261            vec!["value".to_string()],
1262            LogicalPlan::EmptyRelation(EmptyRelation {
1263                produce_one_row: false,
1264                schema: schema.clone().to_dfschema_ref().unwrap(),
1265            }),
1266        )
1267        .unwrap();
1268        let input = Arc::new(DataSourceExec::new(Arc::new(
1269            MemorySourceConfig::try_new(&[vec![batch]], schema, None).unwrap(),
1270        )));
1271        let batches = datafusion::physical_plan::collect(
1272            plan.to_execution_plan(input),
1273            SessionContext::default().task_ctx(),
1274        )
1275        .await
1276        .unwrap();
1277        let out = &batches[0];
1278        let values = RangeArray::try_new(
1279            out.column(1)
1280                .as_any()
1281                .downcast_ref::<DictionaryArray<Int64Type>>()
1282                .unwrap()
1283                .clone(),
1284        )
1285        .unwrap();
1286        let timestamps = RangeArray::try_new(
1287            out.column(2)
1288                .as_any()
1289                .downcast_ref::<DictionaryArray<Int64Type>>()
1290                .unwrap()
1291                .clone(),
1292        )
1293        .unwrap();
1294        assert_eq!(
1295            values
1296                .get(0)
1297                .unwrap()
1298                .as_any()
1299                .downcast_ref::<Float64Array>()
1300                .unwrap()
1301                .values(),
1302            &[100.0, 102.0]
1303        );
1304        assert_eq!(
1305            timestamps
1306                .get(0)
1307                .unwrap()
1308                .as_any()
1309                .downcast_ref::<TimestampMillisecondArray>()
1310                .unwrap()
1311                .values(),
1312            &[29_600, 59_600]
1313        );
1314    }
1315
1316    #[tokio::test]
1317    async fn duplicate_timestamp_keeps_first_row_and_aligned_tags() {
1318        use datafusion::arrow::array::{Float64Array, StringArray};
1319
1320        // Rows sharing a timestamp but carrying different values and tags show which row the
1321        // collapse keeps, and that every column follows the same index instead of drifting apart.
1322        let schema = Arc::new(Schema::new(vec![
1323            Field::new(
1324                TIME_INDEX_COLUMN,
1325                DataType::Timestamp(TimeUnit::Millisecond, None),
1326                false,
1327            ),
1328            Field::new("value", DataType::Float64, false),
1329            Field::new("tag", DataType::Utf8, false),
1330        ]));
1331        let batch = RecordBatch::try_new(
1332            schema.clone(),
1333            vec![
1334                Arc::new(TimestampMillisecondArray::from(vec![
1335                    5_000, 10_000, 10_000, 15_000,
1336                ])),
1337                Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0, 4.0])),
1338                Arc::new(StringArray::from(vec!["a", "b", "c", "d"])),
1339            ],
1340        )
1341        .unwrap();
1342        let plan = RangeManipulate::new(
1343            20_000,
1344            20_000,
1345            20_000,
1346            0,
1347            20_000,
1348            TIME_INDEX_COLUMN.to_string(),
1349            vec!["value".to_string()],
1350            LogicalPlan::EmptyRelation(EmptyRelation {
1351                produce_one_row: false,
1352                schema: schema.clone().to_dfschema_ref().unwrap(),
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 batches = datafusion::physical_plan::collect(
1360            plan.to_execution_plan(input),
1361            SessionContext::default().task_ctx(),
1362        )
1363        .await
1364        .unwrap();
1365        let out = &batches[0];
1366        let values = RangeArray::try_new(
1367            out.column(1)
1368                .as_any()
1369                .downcast_ref::<DictionaryArray<Int64Type>>()
1370                .unwrap()
1371                .clone(),
1372        )
1373        .unwrap();
1374        let timestamps = RangeArray::try_new(
1375            out.column(3)
1376                .as_any()
1377                .downcast_ref::<DictionaryArray<Int64Type>>()
1378                .unwrap()
1379                .clone(),
1380        )
1381        .unwrap();
1382        // The first row of the tied pair is kept, matching what a deduplicating read returns, so
1383        // 2.0 survives and 3.0 is dropped.
1384        assert_eq!(
1385            values
1386                .get(0)
1387                .unwrap()
1388                .as_any()
1389                .downcast_ref::<Float64Array>()
1390                .unwrap()
1391                .values(),
1392            &[1.0, 2.0, 4.0]
1393        );
1394        assert_eq!(
1395            timestamps
1396                .get(0)
1397                .unwrap()
1398                .as_any()
1399                .downcast_ref::<TimestampMillisecondArray>()
1400                .unwrap()
1401                .values(),
1402            &[5_000, 10_000, 15_000]
1403        );
1404        // Tag columns are not range payloads, so the operator repeats the batch's first row for
1405        // every output row: a batch holds one series, so every row's tag is that series' tag.
1406        assert_eq!(
1407            out.column(2)
1408                .as_any()
1409                .downcast_ref::<StringArray>()
1410                .unwrap()
1411                .iter()
1412                .collect::<Vec<_>>(),
1413            vec![Some("a")]
1414        );
1415    }
1416
1417    #[test]
1418    fn duplicate_sample_range_udfs_report_deduplicated_values() {
1419        use crate::functions::{CountOverTime, Delta, IDelta, Increase, Rate};
1420        use datafusion::arrow::array::Float64Array;
1421        use datafusion::physical_plan::ColumnarValue;
1422        use datafusion_common::config::ConfigOptions;
1423        use datafusion_expr::ScalarFunctionArgs;
1424        let evaluate = |udf: datafusion::logical_expr::ScalarUDF, range_ms: Option<i64>| {
1425            let ts = RangeArray::from_ranges(
1426                Arc::new(TimestampMillisecondArray::from(vec![29_600, 59_600])),
1427                [(0, 2)],
1428            )
1429            .unwrap();
1430            let values =
1431                RangeArray::from_ranges(Arc::new(Float64Array::from(vec![100.0, 102.0])), [(0, 2)])
1432                    .unwrap();
1433            let mut args = vec![
1434                ColumnarValue::Array(Arc::new(ts.into_dict())),
1435                ColumnarValue::Array(Arc::new(values.into_dict())),
1436            ];
1437            let mut arg_fields = args
1438                .iter()
1439                .enumerate()
1440                .map(|(i, arg)| {
1441                    Arc::new(Field::new(
1442                        if i == 0 { "ts" } else { "value" },
1443                        arg.data_type(),
1444                        false,
1445                    ))
1446                })
1447                .collect::<Vec<_>>();
1448            if let Some(range_ms) = range_ms {
1449                let eval_ts: ArrayRef = Arc::new(TimestampMillisecondArray::from(vec![60_000]));
1450                args.push(ColumnarValue::Array(eval_ts));
1451                args.push(ColumnarValue::Scalar(
1452                    datafusion_common::ScalarValue::Int64(Some(range_ms)),
1453                ));
1454                arg_fields.push(Arc::new(Field::new("eval_ts", args[2].data_type(), false)));
1455                arg_fields.push(Arc::new(Field::new("range", DataType::Int64, false)));
1456            }
1457            let result = udf
1458                .invoke_with_args(ScalarFunctionArgs {
1459                    args,
1460                    arg_fields,
1461                    number_rows: 1,
1462                    return_field: Arc::new(Field::new("result", DataType::Float64, false)),
1463                    config_options: Arc::new(ConfigOptions::default()),
1464                })
1465                .unwrap();
1466            let array = match result {
1467                ColumnarValue::Array(a) => a,
1468                ColumnarValue::Scalar(s) => s.to_array_of_size(1).unwrap(),
1469            };
1470            array
1471                .as_any()
1472                .downcast_ref::<Float64Array>()
1473                .unwrap()
1474                .value(0)
1475        };
1476        assert_eq!(evaluate(CountOverTime::scalar_udf(), None), 2.0);
1477        assert_eq!(evaluate(Delta::scalar_udf(), Some(60_000)), 4.0);
1478        assert_eq!(evaluate(Increase::scalar_udf(), Some(60_000)), 4.0);
1479        assert!((evaluate(Rate::scalar_udf(), Some(60_000)) - 4.0 / 60.0).abs() < 0.0001);
1480        assert_eq!(evaluate(IDelta::<false>::scalar_udf(), None), 2.0);
1481        assert!((evaluate(IDelta::<true>::scalar_udf(), None) - 2.0 / 30.0).abs() < 0.0001);
1482    }
1483
1484    #[tokio::test]
1485    async fn distinct_submillisecond_timestamps_are_not_collapsed() {
1486        let schema = Arc::new(Schema::new(vec![
1487            Field::new(
1488                TIME_INDEX_COLUMN,
1489                DataType::Timestamp(TimeUnit::Microsecond, None),
1490                false,
1491            ),
1492            Field::new("value", DataType::Float64, false),
1493        ]));
1494        let batch = RecordBatch::try_new(
1495            schema.clone(),
1496            vec![
1497                Arc::new(TimestampMicrosecondArray::from(vec![1_000_001, 1_000_002])),
1498                Arc::new(Float64Array::from(vec![10.0, 20.0])),
1499            ],
1500        )
1501        .unwrap();
1502        let plan = RangeManipulate::new(
1503            1_001,
1504            1_001,
1505            1,
1506            0,
1507            1,
1508            TIME_INDEX_COLUMN.to_string(),
1509            vec!["value".to_string()],
1510            LogicalPlan::EmptyRelation(EmptyRelation {
1511                produce_one_row: false,
1512                schema: schema.clone().to_dfschema_ref().unwrap(),
1513            }),
1514        )
1515        .unwrap();
1516        let input = Arc::new(DataSourceExec::new(Arc::new(
1517            MemorySourceConfig::try_new(&[vec![batch]], schema, None).unwrap(),
1518        )));
1519        let batches = datafusion::physical_plan::collect(
1520            plan.to_execution_plan(input),
1521            SessionContext::default().task_ctx(),
1522        )
1523        .await
1524        .unwrap();
1525        let output = &batches[0];
1526        let values = RangeArray::try_new(
1527            output
1528                .column(1)
1529                .as_any()
1530                .downcast_ref::<DictionaryArray<Int64Type>>()
1531                .unwrap()
1532                .clone(),
1533        )
1534        .unwrap();
1535        let timestamps = RangeArray::try_new(
1536            output
1537                .column(2)
1538                .as_any()
1539                .downcast_ref::<DictionaryArray<Int64Type>>()
1540                .unwrap()
1541                .clone(),
1542        )
1543        .unwrap();
1544        assert_eq!(
1545            values
1546                .get(0)
1547                .unwrap()
1548                .as_any()
1549                .downcast_ref::<Float64Array>()
1550                .unwrap()
1551                .values(),
1552            &[10.0, 20.0]
1553        );
1554        assert_eq!(
1555            timestamps
1556                .get(0)
1557                .unwrap()
1558                .as_any()
1559                .downcast_ref::<TimestampMillisecondArray>()
1560                .unwrap()
1561                .values(),
1562            &[1_000, 1_000]
1563        );
1564    }
1565
1566    #[tokio::test]
1567    async fn native_timestamps_preserve_range_membership_and_ms_payload() {
1568        for (unit, ticks_per_ms) in [
1569            (TimeUnit::Microsecond, 1_000_i64),
1570            (TimeUnit::Nanosecond, 1_000_000_i64),
1571        ] {
1572            let lower = 1_000 * ticks_per_ms;
1573            let upper = 1_001 * ticks_per_ms;
1574            // Exclude the lower boundary and future sample; retain both native
1575            // samples in the same millisecond bucket and the exact upper sample.
1576            let timestamps = vec![lower, lower + 1, lower + 2, upper, upper + 1];
1577            let time: ArrayRef = match unit {
1578                TimeUnit::Microsecond => Arc::new(TimestampMicrosecondArray::from(timestamps)),
1579                TimeUnit::Nanosecond => Arc::new(TimestampNanosecondArray::from(timestamps)),
1580                _ => unreachable!(),
1581            };
1582            let schema = Arc::new(Schema::new(vec![
1583                Field::new(TIME_INDEX_COLUMN, DataType::Timestamp(unit, None), false),
1584                Field::new("value", DataType::Float64, true),
1585            ]));
1586            let batch = RecordBatch::try_new(
1587                schema.clone(),
1588                vec![
1589                    time,
1590                    Arc::new(Float64Array::from(vec![10.0, 20.0, 30.0, 40.0, 50.0])),
1591                ],
1592            )
1593            .unwrap();
1594            let logical_input = LogicalPlan::EmptyRelation(EmptyRelation {
1595                produce_one_row: false,
1596                schema: schema.clone().to_dfschema_ref().unwrap(),
1597            });
1598            let plan = RangeManipulate::new(
1599                1_001,
1600                1_001,
1601                1,
1602                0,
1603                1,
1604                TIME_INDEX_COLUMN.to_string(),
1605                vec!["value".to_string()],
1606                logical_input.clone(),
1607            )
1608            .unwrap();
1609            let output_time = Field::new(
1610                TIME_INDEX_COLUMN,
1611                DataType::Timestamp(TimeUnit::Millisecond, None),
1612                false,
1613            );
1614            let output_schema = Arc::new(Schema::new(vec![
1615                output_time.clone(),
1616                RangeArray::convert_field(&Field::new("value", DataType::Float64, true)),
1617                Field::new(
1618                    RangeManipulate::build_timestamp_range_name(TIME_INDEX_COLUMN),
1619                    RangeArray::convert_field(&output_time).data_type().clone(),
1620                    false,
1621                ),
1622            ]));
1623            assert_eq!(plan.schema().as_arrow(), output_schema.as_ref());
1624
1625            let rebuilt = RangeManipulate::deserialize(&plan.serialize())
1626                .unwrap()
1627                .with_exprs_and_inputs(vec![], vec![logical_input])
1628                .unwrap();
1629            assert_eq!(rebuilt.schema(), plan.schema());
1630            assert_eq!(rebuilt.input.schema().as_arrow(), schema.as_ref());
1631
1632            let input = Arc::new(DataSourceExec::new(Arc::new(
1633                MemorySourceConfig::try_new(&[vec![batch]], schema.clone(), None).unwrap(),
1634            )));
1635            let exec = rebuilt.to_execution_plan(input);
1636            assert_eq!(exec.schema(), output_schema);
1637            assert_eq!(exec.children()[0].schema(), schema);
1638
1639            let batches =
1640                datafusion::physical_plan::collect(exec, SessionContext::default().task_ctx())
1641                    .await
1642                    .unwrap();
1643            assert_eq!(batches.len(), 1, "{unit:?}");
1644            let output = &batches[0];
1645            assert_eq!(output.schema(), output_schema);
1646            assert_eq!(output.num_rows(), 1);
1647            assert_eq!(
1648                output
1649                    .column(0)
1650                    .as_any()
1651                    .downcast_ref::<TimestampMillisecondArray>()
1652                    .unwrap()
1653                    .values()
1654                    .as_ref(),
1655                &[1_001]
1656            );
1657
1658            // RangeArray packs offset/length into dictionary keys; Arrow dictionary
1659            // equality treats those packed keys as indices and cannot compare them.
1660            let values = RangeArray::try_new(
1661                output
1662                    .column(1)
1663                    .as_any()
1664                    .downcast_ref::<DictionaryArray<Int64Type>>()
1665                    .unwrap()
1666                    .clone(),
1667            )
1668            .unwrap();
1669            assert_eq!(values.get_offset_length(0), Some((1, 3)));
1670            assert_eq!(
1671                values.get(0).unwrap().to_data(),
1672                Float64Array::from(vec![20.0, 30.0, 40.0]).to_data()
1673            );
1674            let timestamps = RangeArray::try_new(
1675                output
1676                    .column(2)
1677                    .as_any()
1678                    .downcast_ref::<DictionaryArray<Int64Type>>()
1679                    .unwrap()
1680                    .clone(),
1681            )
1682            .unwrap();
1683            assert_eq!(timestamps.get_offset_length(0), Some((1, 3)));
1684            assert_eq!(
1685                timestamps.get(0).unwrap().to_data(),
1686                TimestampMillisecondArray::from(vec![1_000, 1_000, 1_001]).to_data()
1687            );
1688        }
1689    }
1690
1691    #[test]
1692    fn logical_offset_participates_in_ordering() {
1693        let input = LogicalPlan::EmptyRelation(EmptyRelation {
1694            produce_one_row: false,
1695            schema: prepare_test_data().schema().to_dfschema_ref().unwrap(),
1696        });
1697        let first = RangeManipulate::new(
1698            0,
1699            0,
1700            0,
1701            0,
1702            0,
1703            TIME_INDEX_COLUMN.to_string(),
1704            vec!["value_1".to_string()],
1705            input.clone(),
1706        )
1707        .unwrap();
1708        let second = RangeManipulate::new(
1709            0,
1710            0,
1711            0,
1712            1,
1713            0,
1714            TIME_INDEX_COLUMN.to_string(),
1715            vec!["value_1".to_string()],
1716            input,
1717        )
1718        .unwrap();
1719        assert_ne!(first, second);
1720        assert_eq!(first.partial_cmp(&second), Some(std::cmp::Ordering::Less));
1721    }
1722
1723    #[tokio::test]
1724    async fn logical_normalize_offset_survives_rebuild_and_executes() {
1725        for (name, time_unit, raw, offset, start, range, expected_payload) in [
1726            (
1727                "millisecond offset",
1728                TimeUnit::Millisecond,
1729                0,
1730                1_000,
1731                1_000,
1732                1_000,
1733                1_000,
1734            ),
1735            (
1736                "negative native lower limit with positive window",
1737                TimeUnit::Nanosecond,
1738                -9_223_112_837_000_000_000,
1739                -259_200_000,
1740                -9_223_372_037_000,
1741                300_000,
1742                -9_223_372_037_000,
1743            ),
1744            (
1745                "second timestamp with negative fractional offset",
1746                TimeUnit::Second,
1747                1,
1748                -500,
1749                1_000,
1750                1_000,
1751                500,
1752            ),
1753        ] {
1754            let schema = Arc::new(Schema::new(vec![
1755                Field::new(
1756                    TIME_INDEX_COLUMN,
1757                    DataType::Timestamp(time_unit, None),
1758                    false,
1759                ),
1760                Field::new("value", DataType::Float64, true),
1761            ]));
1762            let input = LogicalPlan::EmptyRelation(EmptyRelation {
1763                produce_one_row: false,
1764                schema: schema.clone().to_dfschema_ref().unwrap(),
1765            });
1766            let normalize = crate::extension_plan::SeriesNormalize::new(
1767                offset,
1768                TIME_INDEX_COLUMN,
1769                false,
1770                Vec::new(),
1771                input.clone(),
1772            );
1773            let normalize =
1774                crate::extension_plan::SeriesNormalize::deserialize(&normalize.serialize())
1775                    .unwrap()
1776                    .with_exprs_and_inputs(vec![], vec![input.clone()])
1777                    .unwrap();
1778            let normalized = LogicalPlan::Extension(Extension {
1779                node: Arc::new(normalize),
1780            });
1781            let fresh = RangeManipulate::new(
1782                start,
1783                start,
1784                1,
1785                offset,
1786                range,
1787                TIME_INDEX_COLUMN.to_string(),
1788                vec!["value".to_string()],
1789                input.clone(),
1790            )
1791            .unwrap()
1792            .with_exprs_and_inputs(vec![], vec![input.clone()])
1793            .unwrap();
1794            let serialized = RangeManipulate::new(
1795                start,
1796                start,
1797                1,
1798                offset,
1799                range,
1800                TIME_INDEX_COLUMN.to_string(),
1801                vec!["value".to_string()],
1802                normalized.clone(),
1803            )
1804            .unwrap();
1805            let decoded = RangeManipulate::deserialize(&serialized.serialize())
1806                .unwrap()
1807                .with_exprs_and_inputs(vec![], vec![normalized])
1808                .unwrap();
1809            let timestamp: ArrayRef = match time_unit {
1810                TimeUnit::Millisecond => Arc::new(TimestampMillisecondArray::from(vec![raw])),
1811                TimeUnit::Nanosecond => Arc::new(TimestampNanosecondArray::from(vec![raw])),
1812                TimeUnit::Second => Arc::new(TimestampSecondArray::from(vec![raw])),
1813                _ => unreachable!(),
1814            };
1815            let batch = RecordBatch::try_new(
1816                schema.clone(),
1817                vec![timestamp, Arc::new(Float64Array::from(vec![7.0]))],
1818            )
1819            .unwrap();
1820            for (mode, rebuilt) in [("fresh", fresh), ("decoded", decoded)] {
1821                let rebuilt = rebuilt
1822                    .with_exprs_and_inputs(vec![], vec![input.clone()])
1823                    .unwrap();
1824                assert_eq!(rebuilt.offset, offset, "{name}: {mode}");
1825                assert_eq!(rebuilt.input.schema(), input.schema(), "{name}: {mode}");
1826
1827                let empty_exec_input = Arc::new(DataSourceExec::new(Arc::new(
1828                    MemorySourceConfig::try_new(&[vec![]], schema.clone(), None).unwrap(),
1829                )));
1830                let exec_input = Arc::new(DataSourceExec::new(Arc::new(
1831                    MemorySourceConfig::try_new(&[vec![batch.clone()]], schema.clone(), None)
1832                        .unwrap(),
1833                )));
1834                let exec = rebuilt
1835                    .to_execution_plan(empty_exec_input)
1836                    .replace_children(
1837                        vec![exec_input],
1838                        ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
1839                    )
1840                    .unwrap();
1841                let output =
1842                    datafusion::physical_plan::collect(exec, SessionContext::default().task_ctx())
1843                        .await
1844                        .unwrap();
1845                assert_eq!(output.len(), 1, "{name}: {mode}");
1846                let output = &output[0];
1847                assert_eq!(output.num_rows(), 1, "{name}: {mode}");
1848                assert_eq!(
1849                    output
1850                        .column(0)
1851                        .as_any()
1852                        .downcast_ref::<TimestampMillisecondArray>()
1853                        .unwrap()
1854                        .value(0),
1855                    start,
1856                    "{name}: {mode}"
1857                );
1858                let values = RangeArray::try_new(
1859                    output
1860                        .column(1)
1861                        .as_any()
1862                        .downcast_ref::<DictionaryArray<Int64Type>>()
1863                        .unwrap()
1864                        .clone(),
1865                )
1866                .unwrap();
1867                assert_eq!(values.get_offset_length(0), Some((0, 1)), "{name}: {mode}");
1868                assert_eq!(
1869                    values.get(0).unwrap().to_data(),
1870                    Float64Array::from(vec![7.0]).to_data(),
1871                    "{name}: {mode}"
1872                );
1873                let timestamps = RangeArray::try_new(
1874                    output
1875                        .column(2)
1876                        .as_any()
1877                        .downcast_ref::<DictionaryArray<Int64Type>>()
1878                        .unwrap()
1879                        .clone(),
1880                )
1881                .unwrap();
1882                assert_eq!(
1883                    timestamps.get_offset_length(0),
1884                    Some((0, 1)),
1885                    "{name}: {mode}"
1886                );
1887                assert_eq!(
1888                    timestamps.get(0).unwrap().to_data(),
1889                    TimestampMillisecondArray::from(vec![expected_payload]).to_data(),
1890                    "{name}: {mode}"
1891                );
1892            }
1893        }
1894    }
1895
1896    #[tokio::test]
1897    async fn range_payload_preserves_null_timestamp_and_rejects_offset_overflow() {
1898        let schema = Arc::new(Schema::new(vec![
1899            Field::new(TIME_INDEX_COLUMN, TimestampMillisecondType::DATA_TYPE, true),
1900            Field::new("value", DataType::Float64, true),
1901        ]));
1902        let null_timestamp =
1903            TimestampMillisecondArray::new(vec![1_000].into(), Some(NullBuffer::from(vec![false])));
1904        let batch = RecordBatch::try_new(
1905            schema.clone(),
1906            vec![
1907                Arc::new(null_timestamp),
1908                Arc::new(Float64Array::from(vec![7.0])),
1909            ],
1910        )
1911        .unwrap();
1912        let input = Arc::new(DataSourceExec::new(Arc::new(
1913            MemorySourceConfig::try_new(&[vec![batch]], schema.clone(), None).unwrap(),
1914        )));
1915        let plan = RangeManipulate::new(
1916            1_000,
1917            1_000,
1918            1,
1919            0,
1920            1,
1921            TIME_INDEX_COLUMN.to_string(),
1922            vec!["value".to_string()],
1923            LogicalPlan::EmptyRelation(EmptyRelation {
1924                produce_one_row: false,
1925                schema: schema.clone().to_dfschema_ref().unwrap(),
1926            }),
1927        )
1928        .unwrap();
1929        let output = datafusion::physical_plan::collect(
1930            plan.to_execution_plan(input),
1931            SessionContext::default().task_ctx(),
1932        )
1933        .await
1934        .unwrap();
1935        let timestamps = RangeArray::try_new(
1936            output[0]
1937                .column(2)
1938                .as_any()
1939                .downcast_ref::<DictionaryArray<Int64Type>>()
1940                .unwrap()
1941                .clone(),
1942        )
1943        .unwrap();
1944        let payload = timestamps.get(0).unwrap();
1945        let payload = payload
1946            .as_any()
1947            .downcast_ref::<TimestampMillisecondArray>()
1948            .unwrap();
1949        assert_eq!(payload.len(), 1);
1950        assert!(!payload.is_valid(0));
1951
1952        let batch = RecordBatch::try_new(
1953            schema.clone(),
1954            vec![
1955                Arc::new(TimestampMillisecondArray::from(vec![0, i64::MAX])),
1956                Arc::new(Float64Array::from(vec![7.0, 8.0])),
1957            ],
1958        )
1959        .unwrap();
1960        let input = Arc::new(DataSourceExec::new(Arc::new(
1961            MemorySourceConfig::try_new(&[vec![batch]], schema.clone(), None).unwrap(),
1962        )));
1963        let normalized = crate::extension_plan::SeriesNormalize::new(
1964            1,
1965            TIME_INDEX_COLUMN,
1966            false,
1967            Vec::new(),
1968            LogicalPlan::EmptyRelation(EmptyRelation {
1969                produce_one_row: false,
1970                schema: schema.to_dfschema_ref().unwrap(),
1971            }),
1972        );
1973        let plan = RangeManipulate::new(
1974            1,
1975            1,
1976            1,
1977            1,
1978            1,
1979            TIME_INDEX_COLUMN.to_string(),
1980            vec!["value".to_string()],
1981            LogicalPlan::Extension(Extension {
1982                node: Arc::new(normalized),
1983            }),
1984        )
1985        .unwrap();
1986        let error = datafusion::physical_plan::collect(
1987            plan.to_execution_plan(input),
1988            SessionContext::default().task_ctx(),
1989        )
1990        .await
1991        .unwrap_err();
1992        assert!(error.to_string().contains("timestamp payload overflow"));
1993    }
1994
1995    #[tokio::test]
1996    async fn pruning_should_keep_time_and_value_columns_for_exec() {
1997        let schema = Arc::new(Schema::new(vec![
1998            Field::new(TIME_INDEX_COLUMN, TimestampMillisecondType::DATA_TYPE, true),
1999            Field::new("value_1", DataType::Float64, true),
2000            Field::new("value_2", DataType::Float64, true),
2001            Field::new("path", DataType::Utf8, true),
2002        ]));
2003        let df_schema = schema.clone().to_dfschema_ref().unwrap();
2004        let input = LogicalPlan::EmptyRelation(EmptyRelation {
2005            produce_one_row: false,
2006            schema: df_schema,
2007        });
2008        let plan = RangeManipulate::new(
2009            0,
2010            310_000,
2011            30_000,
2012            0,
2013            90_000,
2014            TIME_INDEX_COLUMN.to_string(),
2015            vec!["value_1".to_string(), "value_2".to_string()],
2016            input,
2017        )
2018        .unwrap();
2019
2020        // Simulate a parent projection requesting only the `path` column.
2021        let output_columns = [3usize];
2022        let required = plan.necessary_children_exprs(&output_columns).unwrap();
2023        let required = &required[0];
2024        assert_eq!(required.as_slice(), &[0, 1, 2, 3]);
2025
2026        let timestamp_column = Arc::new(TimestampMillisecondArray::from(vec![
2027            0, 30_000, 60_000, 90_000, 120_000, // every 30s
2028            180_000, 240_000, // every 60s
2029            241_000, 271_000, 291_000, // others
2030        ])) as _;
2031        let field_column: ArrayRef = Arc::new(Float64Array::from(vec![1.0; 10])) as _;
2032        let path_column = Arc::new(StringArray::from(vec!["foo"; 10])) as _;
2033        let input_batch = RecordBatch::try_new(
2034            schema,
2035            vec![
2036                timestamp_column,
2037                field_column.clone(),
2038                field_column,
2039                path_column,
2040            ],
2041        )
2042        .unwrap();
2043
2044        let projected = project_batch(&input_batch, required);
2045        let projected_schema = projected.schema();
2046        let memory_exec = Arc::new(DataSourceExec::new(Arc::new(
2047            MemorySourceConfig::try_new(&[vec![projected]], projected_schema, None).unwrap(),
2048        )));
2049        let range_exec = plan.to_execution_plan(memory_exec);
2050        let session_context = SessionContext::default();
2051        let output_batches =
2052            datafusion::physical_plan::collect(range_exec, session_context.task_ctx())
2053                .await
2054                .unwrap();
2055        assert_eq!(output_batches.len(), 1);
2056
2057        let output_batch = &output_batches[0];
2058        let path = output_batch
2059            .column(3)
2060            .as_any()
2061            .downcast_ref::<StringArray>()
2062            .unwrap();
2063        assert!(path.iter().all(|v| v == Some("foo")));
2064
2065        // Simulate the pre-fix pruning behavior: omit the timestamp/value columns from the child.
2066        let broken_required = [3usize];
2067        let broken = project_batch(&input_batch, &broken_required);
2068        let broken_schema = broken.schema();
2069        let broken_exec = Arc::new(DataSourceExec::new(Arc::new(
2070            MemorySourceConfig::try_new(&[vec![broken]], broken_schema, None).unwrap(),
2071        )));
2072        let broken_range_exec = plan.to_execution_plan(broken_exec);
2073        let session_context = SessionContext::default();
2074        let broken_result = std::panic::AssertUnwindSafe(async {
2075            datafusion::physical_plan::collect(broken_range_exec, session_context.task_ctx()).await
2076        })
2077        .catch_unwind()
2078        .await;
2079        assert!(broken_result.is_err());
2080    }
2081
2082    #[tokio::test]
2083    async fn interval_30s_range_90s() {
2084        let expected = String::from(
2085            "PrimitiveArray<Timestamp(ms)>\n[\n  \
2086                1970-01-01T00:00:00,\n  \
2087                1970-01-01T00:00:30,\n  \
2088                1970-01-01T00:01:00,\n  \
2089                1970-01-01T00:01:30,\n  \
2090                1970-01-01T00:02:00,\n  \
2091                1970-01-01T00:02:30,\n  \
2092                1970-01-01T00:03:00,\n  \
2093                1970-01-01T00:03:30,\n  \
2094                1970-01-01T00:04:00,\n  \
2095                1970-01-01T00:04:30,\n  \
2096                1970-01-01T00:05:00,\n\
2097            ]\nRangeArray { \
2098                base array: PrimitiveArray<Float64>\n[\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n], \
2099                ranges: [Some(0..1), Some(0..2), Some(0..3), Some(1..4), Some(2..5), Some(3..5), Some(4..6), Some(5..6), Some(5..7), Some(6..8), Some(6..10)] \
2100            }\nRangeArray { \
2101                base array: PrimitiveArray<Float64>\n[\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n], \
2102                ranges: [Some(0..1), Some(0..2), Some(0..3), Some(1..4), Some(2..5), Some(3..5), Some(4..6), Some(5..6), Some(5..7), Some(6..8), Some(6..10)] \
2103            }\nStringArray\n[\n  \"foo\",\n  \"foo\",\n  \"foo\",\n  \"foo\",\n  \"foo\",\n  \"foo\",\n  \"foo\",\n  \"foo\",\n  \"foo\",\n  \"foo\",\n  \"foo\",\n]\n\
2104            RangeArray { \
2105                base array: PrimitiveArray<Timestamp(ms)>\n[\n  1970-01-01T00:00:00,\n  1970-01-01T00:00:30,\n  1970-01-01T00:01:00,\n  1970-01-01T00:01:30,\n  1970-01-01T00:02:00,\n  1970-01-01T00:03:00,\n  1970-01-01T00:04:00,\n  1970-01-01T00:04:01,\n  1970-01-01T00:04:31,\n  1970-01-01T00:04:51,\n], \
2106                ranges: [Some(0..1), Some(0..2), Some(0..3), Some(1..4), Some(2..5), Some(3..5), Some(4..6), Some(5..6), Some(5..7), Some(6..8), Some(6..10)] \
2107            }",
2108        );
2109        do_normalize_test(0, 310_000, 30_000, 90_000, expected.clone()).await;
2110
2111        // dump large range
2112        do_normalize_test(-300000, 310_000, 30_000, 90_000, expected).await;
2113    }
2114
2115    #[tokio::test]
2116    async fn small_empty_range() {
2117        let expected = String::from(
2118            "PrimitiveArray<Timestamp(ms)>\n[\n  \
2119            1970-01-01T00:00:00.001,\n  \
2120            1970-01-01T00:00:03.001,\n  \
2121            1970-01-01T00:00:06.001,\n  \
2122            1970-01-01T00:00:09.001,\n\
2123        ]\nRangeArray { \
2124            base array: PrimitiveArray<Float64>\n[\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n], \
2125            ranges: [Some(0..1), Some(0..0), Some(0..0), Some(0..0)] \
2126        }\nRangeArray { \
2127            base array: PrimitiveArray<Float64>\n[\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n  1.0,\n], \
2128            ranges: [Some(0..1), Some(0..0), Some(0..0), Some(0..0)] \
2129        }\nStringArray\n[\n  \"foo\",\n  \"foo\",\n  \"foo\",\n  \"foo\",\n]\n\
2130        RangeArray { \
2131            base array: PrimitiveArray<Timestamp(ms)>\n[\n  1970-01-01T00:00:00,\n  1970-01-01T00:00:30,\n  1970-01-01T00:01:00,\n  1970-01-01T00:01:30,\n  1970-01-01T00:02:00,\n  1970-01-01T00:03:00,\n  1970-01-01T00:04:00,\n  1970-01-01T00:04:01,\n  1970-01-01T00:04:31,\n  1970-01-01T00:04:51,\n], \
2132            ranges: [Some(0..1), Some(0..0), Some(0..0), Some(0..0)] \
2133        }",
2134        );
2135        do_normalize_test(1, 10_001, 3_000, 1_000, expected).await;
2136    }
2137
2138    #[test]
2139    fn test_calculate_range_preserves_alignment() {
2140        // Test case: query starts at timestamp ending in 4000, step is 30s
2141        // Data starts at different alignment - should preserve query's 4000 pattern
2142        let schema = Arc::new(Schema::new(vec![Field::new(
2143            "timestamp",
2144            TimestampMillisecondType::DATA_TYPE,
2145            false,
2146        )]));
2147        let empty_stream = MemoryStream::try_new(vec![], schema.clone(), None).unwrap();
2148
2149        let stream = RangeManipulateStream {
2150            offset: 0,
2151            start: 1758093274000, // ends in 4000
2152            end: 1758093334000,   // ends in 4000
2153            interval: 30000,      // 30s step
2154            range: 60000,         // 60s lookback
2155            time_index: 0,
2156            time_unit: TimeUnit::Millisecond,
2157            field_columns: vec![],
2158            aligned_ts_array: Arc::new(TimestampMillisecondArray::from(vec![0i64; 0])),
2159            output_schema: schema.clone(),
2160            input: Box::pin(empty_stream),
2161            metric: BaselineMetrics::new(&ExecutionPlanMetricsSet::new(), 0),
2162            num_series: Count::new(),
2163        };
2164
2165        // Create test data with timestamps not aligned to query pattern
2166        let test_timestamps = vec![
2167            1758093260000, // ends in 0000 (different alignment)
2168            1758093290000, // ends in 0000
2169            1758093320000, // ends in 0000
2170        ];
2171        let ts_array = TimestampMillisecondArray::from(test_timestamps);
2172        let test_schema = Arc::new(Schema::new(vec![Field::new(
2173            "timestamp",
2174            TimestampMillisecondType::DATA_TYPE,
2175            false,
2176        )]));
2177        let batch = RecordBatch::try_new(test_schema, vec![Arc::new(ts_array)]).unwrap();
2178
2179        let (ranges, (start, end)) = stream.calculate_range(&batch).unwrap();
2180
2181        // Verify the optimized start preserves query alignment (should end in 4000)
2182        assert_eq!(
2183            start % 30000,
2184            1758093274000 % 30000,
2185            "Optimized start should preserve query alignment pattern"
2186        );
2187
2188        // Verify we generate correct number of ranges for the alignment
2189        let expected_timestamps: Vec<i64> = (start..=end).step_by(30000).collect();
2190        assert_eq!(ranges.len(), expected_timestamps.len());
2191
2192        // Verify all generated timestamps maintain the same alignment pattern
2193        for ts in expected_timestamps {
2194            assert_eq!(
2195                ts % 30000,
2196                1758093274000 % 30000,
2197                "All timestamps should maintain query alignment pattern"
2198            );
2199        }
2200    }
2201
2202    #[tokio::test]
2203    async fn no_intersection_batch_is_skipped_and_stream_continues() {
2204        let schema = Arc::new(Schema::new(vec![
2205            Field::new(
2206                TIME_INDEX_COLUMN,
2207                TimestampMillisecondType::DATA_TYPE,
2208                false,
2209            ),
2210            Field::new("value", DataType::Float64, false),
2211        ]));
2212        let input = LogicalPlan::EmptyRelation(EmptyRelation {
2213            produce_one_row: false,
2214            schema: schema.clone().to_dfschema_ref().unwrap(),
2215        });
2216        let plan = RangeManipulate::new(
2217            0,
2218            50,
2219            10,
2220            0,
2221            1,
2222            TIME_INDEX_COLUMN.to_string(),
2223            vec!["value".to_string()],
2224            input,
2225        )
2226        .unwrap();
2227        let no_intersection = RecordBatch::try_new(
2228            schema.clone(),
2229            vec![
2230                Arc::new(TimestampMillisecondArray::from(vec![100])),
2231                Arc::new(Float64Array::from(vec![1.0])),
2232            ],
2233        )
2234        .unwrap();
2235        let intersection = RecordBatch::try_new(
2236            schema.clone(),
2237            vec![
2238                Arc::new(TimestampMillisecondArray::from(vec![20])),
2239                Arc::new(Float64Array::from(vec![2.0])),
2240            ],
2241        )
2242        .unwrap();
2243        let input = Arc::new(DataSourceExec::new(Arc::new(
2244            MemorySourceConfig::try_new(&[vec![no_intersection, intersection]], schema, None)
2245                .unwrap(),
2246        )));
2247
2248        let batches = datafusion::physical_plan::collect(
2249            plan.to_execution_plan(input),
2250            SessionContext::default().task_ctx(),
2251        )
2252        .await
2253        .unwrap();
2254
2255        assert_eq!(batches.len(), 1);
2256        assert_eq!(batches[0].num_rows(), 1);
2257        let timestamps = batches[0]
2258            .column(0)
2259            .as_any()
2260            .downcast_ref::<TimestampMillisecondArray>()
2261            .unwrap();
2262        assert_eq!(timestamps.value(0), 20);
2263    }
2264
2265    fn calculate_range_for_test(
2266        query_start: i64,
2267        query_end: i64,
2268        interval: i64,
2269        range: i64,
2270        timestamps: &[i64],
2271    ) -> (Vec<(u32, u32)>, (i64, i64)) {
2272        let schema = Arc::new(Schema::new(vec![Field::new(
2273            TIME_INDEX_COLUMN,
2274            TimestampMillisecondType::DATA_TYPE,
2275            false,
2276        )]));
2277        let empty_stream = MemoryStream::try_new(vec![], schema.clone(), None).unwrap();
2278        let stream = RangeManipulateStream {
2279            offset: 0,
2280            start: query_start,
2281            end: query_end,
2282            interval,
2283            range,
2284            time_index: 0,
2285            time_unit: TimeUnit::Millisecond,
2286            field_columns: vec![],
2287            aligned_ts_array: Arc::new(TimestampMillisecondArray::from(vec![0i64; 0])),
2288            output_schema: schema.clone(),
2289            input: Box::pin(empty_stream),
2290            metric: BaselineMetrics::new(&ExecutionPlanMetricsSet::new(), 0),
2291            num_series: Count::new(),
2292        };
2293        let batch = RecordBatch::try_new(
2294            schema,
2295            vec![Arc::new(TimestampMillisecondArray::from(
2296                timestamps.to_vec(),
2297            ))],
2298        )
2299        .unwrap();
2300
2301        stream.calculate_range(&batch).unwrap()
2302    }
2303
2304    #[test]
2305    fn calculate_range_nullable_interior_timestamp_matches_wide_scan() {
2306        let schema = Arc::new(Schema::new(vec![Field::new(
2307            TIME_INDEX_COLUMN,
2308            DataType::Timestamp(TimeUnit::Second, None),
2309            true,
2310        )]));
2311        let stream = scan_stream_for_test(TimeUnit::Second, 0, 0, 1_000, 1_000, 1_000);
2312        let timestamps = [0, i64::MAX, 1];
2313        let array = TimestampSecondArray::new(
2314            timestamps.to_vec().into(),
2315            Some(NullBuffer::from(vec![false, false, true])),
2316        );
2317        let batch = RecordBatch::try_new(schema, vec![Arc::new(array)]).unwrap();
2318
2319        let (ranges, bounds) = stream.calculate_range(&batch).unwrap();
2320        let wide = RangeManipulateStream::bench_scan_wide(
2321            TimeUnit::Second,
2322            0,
2323            1_000,
2324            1_000,
2325            &timestamps,
2326            bounds.0,
2327            bounds.1,
2328        );
2329        assert_eq!(bounds, (0, 1_000));
2330        assert_eq!(ranges, vec![(0, 1), (0, 0)]);
2331        assert_eq!(ranges, wide);
2332    }
2333
2334    #[test]
2335    fn calculate_range_keeps_query_aligned_tail() {
2336        let (ranges, bounds) = calculate_range_for_test(4, 94, 30, 15, &[20, 50, 80]);
2337
2338        assert_eq!(bounds, (34, 94));
2339        assert_eq!(ranges, vec![(0, 1), (1, 1), (2, 1)]);
2340    }
2341
2342    /// Calculates exact offsets directly from the range predicate used by PromQL.
2343    ///
2344    /// Input timestamps are sorted and non-null. Interval is positive, range is
2345    /// nonnegative, and test values are chosen to avoid `i64` overflow.
2346    fn calculate_range_oracle(
2347        timestamps: &[i64],
2348        start: i64,
2349        end: i64,
2350        interval: i64,
2351        range: i64,
2352    ) -> Vec<(u32, u32)> {
2353        // Match `calculate_range`'s explicit empty-input early return.
2354        if timestamps.is_empty() || start > end {
2355            return vec![];
2356        }
2357
2358        (start..=end)
2359            .step_by(interval as usize)
2360            .map(|curr| {
2361                let mut offset = None;
2362                let mut length = 0;
2363                for (index, &ts) in timestamps.iter().enumerate() {
2364                    if ts > curr - range && ts <= curr {
2365                        offset.get_or_insert(index);
2366                        length += 1;
2367                    }
2368                }
2369                (offset.unwrap_or(0) as u32, length)
2370            })
2371            .collect()
2372    }
2373
2374    #[test]
2375    fn calculate_range_characterizes_returned_bounds() {
2376        let cases = [
2377            (
2378                "positive non-aligned last timestamp plus range",
2379                0,
2380                100,
2381                10,
2382                9,
2383                vec![13, 26],
2384                (20, 30),
2385            ),
2386            (
2387                "negative non-aligned last timestamp plus range",
2388                -50,
2389                50,
2390                10,
2391                9,
2392                vec![-37, -26],
2393                (-30, -20),
2394            ),
2395            (
2396                "query alignment not based on epoch",
2397                4,
2398                94,
2399                30,
2400                15,
2401                vec![20, 50, 80],
2402                (34, 94),
2403            ),
2404            ("leading data", 0, 100, 10, 10, vec![-10, 15], (0, 20)),
2405            (
2406                "trailing data past query end",
2407                0,
2408                100,
2409                10,
2410                10,
2411                vec![35, 45, 110],
2412                (40, 100),
2413            ),
2414            (
2415                "optimized start after query end",
2416                0,
2417                50,
2418                10,
2419                0,
2420                vec![100],
2421                (0, 50),
2422            ),
2423        ];
2424
2425        for (name, query_start, query_end, interval, range, timestamps, expected_bounds) in cases {
2426            let (_, bounds) =
2427                calculate_range_for_test(query_start, query_end, interval, range, &timestamps);
2428            assert_eq!(bounds, expected_bounds, "{name}");
2429        }
2430    }
2431
2432    #[test]
2433    fn calculate_range_keeps_extreme_range_tail() {
2434        let (ranges, bounds) =
2435            calculate_range_for_test(i64::MAX - 1, i64::MAX, 1, i64::MAX, &[i64::MAX]);
2436
2437        assert_eq!(bounds, (i64::MAX, i64::MAX));
2438        assert_eq!(ranges, vec![(0, 1)]);
2439    }
2440
2441    #[test]
2442    fn calculate_range_matches_bruteforce_oracle_for_deterministic_cases() {
2443        let cases = vec![
2444            (
2445                "duplicate lower and upper bounds",
2446                vec![0, 10, 10, 20, 20, 30],
2447                10,
2448                20,
2449                10,
2450                10,
2451            ),
2452            (
2453                "zero range excludes duplicates at current timestamp",
2454                vec![10, 10, 10],
2455                10,
2456                10,
2457                1,
2458                0,
2459            ),
2460            (
2461                "consecutive nonempty empty nonempty ranges",
2462                vec![10, 30],
2463                10,
2464                30,
2465                10,
2466                5,
2467            ),
2468            ("step smaller than range", vec![0, 4, 8, 12], 0, 12, 3, 5),
2469            ("step equal to range", vec![0, 5, 10, 15], 0, 15, 5, 5),
2470            ("step greater than range", vec![0, 7, 14, 21], 0, 21, 7, 3),
2471            (
2472                "negative sparse/tail timestamps",
2473                vec![-30, -20, -10, 0],
2474                -25,
2475                5,
2476                5,
2477                7,
2478            ),
2479            ("one sample", vec![42], 0, 100, 10, 15),
2480            ("empty input", vec![], -20, 20, 5, 10),
2481        ];
2482
2483        for (name, timestamps, query_start, query_end, interval, range) in cases {
2484            let (actual, (start, end)) =
2485                calculate_range_for_test(query_start, query_end, interval, range, &timestamps);
2486            let expected = calculate_range_oracle(&timestamps, start, end, interval, range);
2487            assert_eq!(actual, expected, "{name}");
2488        }
2489    }
2490
2491    #[test]
2492    fn calculate_range_positive_time_translated_regression() {
2493        let timestamps = [0, 10, 20, 30];
2494        let expected = vec![(0, 1), (1, 1), (1, 1), (2, 1), (2, 1), (3, 1), (3, 1)];
2495        let (actual, bounds) = calculate_range_for_test(5, 35, 5, 7, &timestamps);
2496
2497        assert_eq!(bounds, (5, 35));
2498        assert_eq!(
2499            calculate_range_oracle(&timestamps, bounds.0, bounds.1, 5, 7),
2500            expected
2501        );
2502        assert_eq!(actual, expected);
2503    }
2504
2505    #[test]
2506    fn calculate_range_matches_oracle_for_dense_positive_time_windows() {
2507        let timestamps = (0..=3_600).step_by(15).collect::<Vec<i64>>();
2508
2509        for (name, range) in [
2510            ("one minute", 60),
2511            ("five minutes", 300),
2512            ("one hour", 3_600),
2513        ] {
2514            let (actual, (start, end)) = calculate_range_for_test(0, 3_600, 15, range, &timestamps);
2515            assert_eq!((start, end), (0, 3_600), "{name} bounds");
2516            assert_eq!(
2517                actual,
2518                calculate_range_oracle(&timestamps, start, end, 15, range),
2519                "{name} window"
2520            );
2521        }
2522    }
2523
2524    #[derive(Clone, Copy)]
2525    struct TinyPrng(u64);
2526
2527    impl TinyPrng {
2528        fn next_u64(&mut self) -> u64 {
2529            self.0 ^= self.0 << 13;
2530            self.0 ^= self.0 >> 7;
2531            self.0 ^= self.0 << 17;
2532            self.0
2533        }
2534
2535        fn next_i64(&mut self, min: i64, max: i64) -> i64 {
2536            min + (self.next_u64() % (max - min + 1) as u64) as i64
2537        }
2538    }
2539
2540    #[test]
2541    fn calculate_range_matches_bruteforce_oracle_for_seeded_matrix() {
2542        let mut prng = TinyPrng(0x5eed_cafe_f00d_baad);
2543
2544        for case in 0..512 {
2545            let interval = prng.next_i64(1, 11);
2546            let range = prng.next_i64(0, 25);
2547            let query_start = prng.next_i64(-200, 200);
2548            let query_end = query_start + interval * prng.next_i64(0, 20);
2549            let mut timestamps = Vec::new();
2550            let mut timestamp = prng.next_i64(-250, 250);
2551            for _ in 0..prng.next_i64(0, 20) {
2552                timestamp += prng.next_i64(0, 7);
2553                timestamps.push(timestamp);
2554            }
2555
2556            let (actual, (start, end)) =
2557                calculate_range_for_test(query_start, query_end, interval, range, &timestamps);
2558            let expected = calculate_range_oracle(&timestamps, start, end, interval, range);
2559            let expected = if actual.is_empty() && !expected.is_empty() {
2560                assert!(
2561                    expected.iter().all(|(_, len)| *len == 0),
2562                    "case={case}, timestamps={timestamps:?}, query=({query_start}, {query_end}), \
2563                     interval={interval}, range={range}, bounds=({start}, {end}): \
2564                     no-intersection output must have no selected samples"
2565                );
2566                vec![]
2567            } else {
2568                expected
2569            };
2570            assert_eq!(
2571                actual, expected,
2572                "case={case}, timestamps={timestamps:?}, query=({query_start}, {query_end}), \
2573                 interval={interval}, range={range}, bounds=({start}, {end})"
2574            );
2575        }
2576    }
2577
2578    /// Nanoseconds per native tick, as kept by the narrow carrier.
2579    fn native_scale(time_unit: TimeUnit) -> i64 {
2580        i64::try_from(nanoseconds_per_native_tick(time_unit)).unwrap()
2581    }
2582
2583    /// Shifted nanoseconds of a sample, computed in the exact `i128` carrier.
2584    fn shifted_nanoseconds(time_unit: TimeUnit, offset: Millisecond, timestamp: i64) -> i128 {
2585        (timestamp as i128) * nanoseconds_per_native_tick(time_unit) + (offset as i128) * 1_000_000
2586    }
2587
2588    /// Shifted milliseconds of the instant grid of a sample, flooring like `calculate_range`.
2589    fn shifted_floor_millis(
2590        time_unit: TimeUnit,
2591        offset: Millisecond,
2592        timestamp: i64,
2593    ) -> Millisecond {
2594        Millisecond::try_from(
2595            shifted_nanoseconds(time_unit, offset, timestamp).div_euclid(1_000_000),
2596        )
2597        .unwrap()
2598    }
2599
2600    /// Shifted milliseconds kept by the range payload of a sample.
2601    ///
2602    /// The payload conversion truncates toward zero, so samples below the epoch may keep
2603    /// the millisecond above their own native tick.
2604    fn shifted_millis(time_unit: TimeUnit, offset: Millisecond, timestamp: i64) -> Millisecond {
2605        Millisecond::try_from(shifted_nanoseconds(time_unit, offset, timestamp) / 1_000_000)
2606            .unwrap()
2607    }
2608
2609    /// Stream whose cursor scan the differential tests below drive directly.
2610    fn scan_stream_for_test(
2611        time_unit: TimeUnit,
2612        offset: Millisecond,
2613        start: Millisecond,
2614        end: Millisecond,
2615        interval: Millisecond,
2616        range: Millisecond,
2617    ) -> RangeManipulateStream {
2618        let schema = Arc::new(Schema::new(vec![Field::new(
2619            TIME_INDEX_COLUMN,
2620            DataType::Timestamp(time_unit, None),
2621            false,
2622        )]));
2623        let empty_stream = MemoryStream::try_new(vec![], schema.clone(), None).unwrap();
2624        RangeManipulateStream {
2625            offset,
2626            start,
2627            end,
2628            interval,
2629            range,
2630            time_index: 0,
2631            time_unit,
2632            field_columns: vec![],
2633            aligned_ts_array: Arc::new(TimestampMillisecondArray::from(vec![0i64; 0])),
2634            output_schema: schema,
2635            input: Box::pin(empty_stream),
2636            metric: BaselineMetrics::new(&ExecutionPlanMetricsSet::new(), 0),
2637            num_series: Count::new(),
2638        }
2639    }
2640
2641    /// Exact expectations of the cursor scan, formulated outside the production loops.
2642    ///
2643    /// Every evaluation instant keeps the samples of `(instant - range, instant]` in
2644    /// shifted nanoseconds: the lower boundary is exclusive, the upper one inclusive, and
2645    /// native ticks below a millisecond stay distinct.
2646    fn scan_oracle(
2647        time_unit: TimeUnit,
2648        offset: Millisecond,
2649        range: Millisecond,
2650        timestamps: &[i64],
2651        start: Millisecond,
2652        end: Millisecond,
2653        interval: Millisecond,
2654    ) -> Vec<(u32, u32)> {
2655        let window_ns = (range as i128) * 1_000_000;
2656        let shifted = timestamps
2657            .iter()
2658            .map(|timestamp| shifted_nanoseconds(time_unit, offset, *timestamp))
2659            .collect::<Vec<_>>();
2660        (start..=end)
2661            .step_by(interval as usize)
2662            .map(|instant| {
2663                let instant_ns = (instant as i128) * 1_000_000;
2664                let mut offset = None;
2665                let mut length = 0u32;
2666                for (index, timestamp) in shifted.iter().enumerate() {
2667                    if *timestamp > instant_ns - window_ns && *timestamp <= instant_ns {
2668                        offset.get_or_insert(index as u32);
2669                        length += 1;
2670                    }
2671                }
2672                (offset.unwrap_or(0), length)
2673            })
2674            .collect()
2675    }
2676
2677    /// Runs the cursor scan of one input through both carriers.
2678    fn scan_both_carriers(
2679        stream: &RangeManipulateStream,
2680        timestamps: &[i64],
2681        start: Millisecond,
2682        end: Millisecond,
2683    ) -> (Option<RangeSelections>, RangeSelections) {
2684        let exact = scan_ranges::<i128>(
2685            stream.interval,
2686            timestamps,
2687            start,
2688            end,
2689            &ScanNanoseconds::<i128>::wide(stream.time_unit, stream.offset, stream.range),
2690        );
2691        let narrow = narrow_scan(
2692            stream.time_unit,
2693            stream.offset,
2694            stream.range,
2695            timestamps,
2696            start,
2697            end,
2698        )
2699        .map(|scan| scan_ranges::<i64>(stream.interval, timestamps, start, end, &scan));
2700        // The benchmark hooks must measure the very scans the operator runs.
2701        assert_eq!(
2702            RangeManipulateStream::bench_scan_wide(
2703                stream.time_unit,
2704                stream.offset,
2705                stream.range,
2706                stream.interval,
2707                timestamps,
2708                start,
2709                end,
2710            ),
2711            exact,
2712            "the i128 benchmark hook diverged from the cursor scan"
2713        );
2714        if let Some(narrow) = &narrow {
2715            assert_eq!(
2716                RangeManipulateStream::bench_scan_narrow(
2717                    stream.time_unit,
2718                    stream.offset,
2719                    stream.range,
2720                    stream.interval,
2721                    timestamps,
2722                    start,
2723                    end,
2724                ),
2725                Some(narrow.clone()),
2726                "the i64 benchmark hook diverged from the cursor scan"
2727            );
2728        } else {
2729            assert_eq!(
2730                RangeManipulateStream::bench_scan_narrow(
2731                    stream.time_unit,
2732                    stream.offset,
2733                    stream.range,
2734                    stream.interval,
2735                    timestamps,
2736                    start,
2737                    end,
2738                ),
2739                None,
2740                "the i64 benchmark hook must report the fallback"
2741            );
2742        }
2743        (narrow, exact)
2744    }
2745
2746    /// Asserts the narrow carrier was taken and matches the exact scan evaluation by evaluation.
2747    fn assert_narrow_carrier_agrees(
2748        narrow: Option<Vec<(u32, u32)>>,
2749        exact: &[(u32, u32)],
2750        context: &str,
2751    ) {
2752        let Some(narrow) = narrow else {
2753            panic!("the i64 carrier was expected, but narrow_scan rejected the input: {context}");
2754        };
2755        assert_eq!(
2756            narrow.len(),
2757            exact.len(),
2758            "carriers disagree on the number of evaluations: {context}"
2759        );
2760        for (evaluation, (narrow_range, exact_range)) in narrow.iter().zip(exact).enumerate() {
2761            assert_eq!(
2762                narrow_range, exact_range,
2763                "carriers disagree on evaluation {evaluation}: {context}"
2764            );
2765        }
2766    }
2767
2768    /// Replays the cursor scan in checked `i64` arithmetic.
2769    ///
2770    /// Reports whether the narrow carrier overflows on an operation the scan evaluates,
2771    /// which is what makes the `i128` fallback unavoidable for an input.
2772    fn narrow_carrier_overflows(
2773        time_unit: TimeUnit,
2774        offset: Millisecond,
2775        range: Millisecond,
2776        timestamps: &[i64],
2777        start: Millisecond,
2778        end: Millisecond,
2779        interval: Millisecond,
2780    ) -> bool {
2781        let Ok(scale) = i64::try_from(nanoseconds_per_native_tick(time_unit)) else {
2782            return true;
2783        };
2784        let Some(offset_ns) = offset.checked_mul(1_000_000) else {
2785            return true;
2786        };
2787        let Some(window_ns) = range.checked_mul(1_000_000) else {
2788            return true;
2789        };
2790        let shift = |timestamp: i64| timestamp.checked_mul(scale)?.checked_add(offset_ns);
2791        let mut left = 0usize;
2792        let mut right = 0usize;
2793        for instant in (start..=end).step_by(interval as usize) {
2794            let Some(instant_ns) = instant.checked_mul(1_000_000) else {
2795                return true;
2796            };
2797            let Some(window_start) = instant_ns.checked_sub(window_ns) else {
2798                return true;
2799            };
2800            while left < timestamps.len() {
2801                let Some(shifted) = shift(timestamps[left]) else {
2802                    return true;
2803                };
2804                if shifted <= window_start {
2805                    left += 1;
2806                } else {
2807                    break;
2808                }
2809            }
2810            right = right.max(left);
2811            while right < timestamps.len() {
2812                let Some(shifted) = shift(timestamps[right]) else {
2813                    return true;
2814                };
2815                if shifted <= instant_ns {
2816                    right += 1;
2817                } else {
2818                    break;
2819                }
2820            }
2821        }
2822        false
2823    }
2824
2825    /// The checked `i64` operation that makes [`narrow_scan`] reject an input, if any.
2826    ///
2827    /// Mirrors the rejection criteria of [`narrow_scan`] so the deterministic cases below
2828    /// can pin which boundary drives the `i128` fallback.
2829    fn narrow_carrier_rejection(
2830        time_unit: TimeUnit,
2831        offset: Millisecond,
2832        range: Millisecond,
2833        timestamps: &[i64],
2834        start: Millisecond,
2835        end: Millisecond,
2836    ) -> Option<&'static str> {
2837        let Ok(scale) = i64::try_from(nanoseconds_per_native_tick(time_unit)) else {
2838            return Some("nanoseconds per native tick do not fit i64");
2839        };
2840        let Some(offset_ns) = offset.checked_mul(1_000_000) else {
2841            return Some("offset in nanoseconds does not fit i64");
2842        };
2843        let Some(window_ns) = range.checked_mul(1_000_000) else {
2844            return Some("range in nanoseconds does not fit i64");
2845        };
2846        let shift = |timestamp: i64| timestamp.checked_mul(scale)?.checked_add(offset_ns);
2847        let (Some(first), Some(last)) = (timestamps.first(), timestamps.last()) else {
2848            return Some("empty batch has no endpoints");
2849        };
2850        if shift(*first).is_none() || shift(*last).is_none() {
2851            return Some("sample nanoseconds do not fit i64");
2852        }
2853        if start
2854            .checked_mul(1_000_000)
2855            .and_then(|nanoseconds| nanoseconds.checked_sub(window_ns))
2856            .is_none()
2857        {
2858            return Some("start bound in nanoseconds does not fit i64");
2859        }
2860        if end
2861            .checked_mul(1_000_000)
2862            .and_then(|nanoseconds| nanoseconds.checked_sub(window_ns))
2863            .is_none()
2864        {
2865            return Some("end bound in nanoseconds do not fit i64");
2866        }
2867        None
2868    }
2869
2870    #[test]
2871    fn scan_carriers_agree_on_range_boundaries_within_one_millisecond() {
2872        // Membership is `(lower, upper]` on native ticks: the sample exactly on the lower
2873        // boundary is dropped, the sample exactly on the upper boundary is kept, and the
2874        // three samples sharing one millisecond payload stay distinct and survive together.
2875        let cases = [
2876            (
2877                TimeUnit::Microsecond,
2878                vec![1_000_000, 1_000_001, 1_000_999, 1_001_000, 1_001_001],
2879            ),
2880            (
2881                TimeUnit::Nanosecond,
2882                vec![
2883                    1_000_000_000,
2884                    1_000_000_001,
2885                    1_000_000_999,
2886                    1_001_000_000,
2887                    1_001_000_001,
2888                ],
2889            ),
2890        ];
2891
2892        for (time_unit, timestamps) in cases {
2893            let (start, end, interval, range) = (1_001, 1_001, 1, 1);
2894            let context = format!("unit={time_unit:?}, timestamps={timestamps:?}");
2895            let stream = scan_stream_for_test(time_unit, 0, start, end, interval, range);
2896
2897            let oracle = scan_oracle(time_unit, 0, range, &timestamps, start, end, interval);
2898            assert_eq!(
2899                oracle,
2900                vec![(1, 3)],
2901                "{context}: (lower, upper] expectations"
2902            );
2903
2904            let (narrow, exact) = scan_both_carriers(&stream, &timestamps, start, end);
2905            assert_eq!(exact, oracle, "{context}: i128 carrier must stay exact");
2906            assert_narrow_carrier_agrees(narrow, &exact, &context);
2907
2908            // Two distinct native ticks below the upper boundary share the millisecond
2909            // payload `1_000`; they must not be deduplicated by going through that ABI.
2910            let payloads = timestamps[1..4]
2911                .iter()
2912                .map(|timestamp| shifted_millis(time_unit, 0, *timestamp))
2913                .collect::<Vec<_>>();
2914            assert_eq!(
2915                payloads,
2916                vec![1_000, 1_000, 1_001],
2917                "{context}: kept samples in one millisecond keep both native ticks"
2918            );
2919            assert_ne!(
2920                timestamps[1], timestamps[2],
2921                "{context}: the two ticks sharing a payload must stay distinct"
2922            );
2923        }
2924    }
2925
2926    #[test]
2927    fn scan_falls_back_to_i128_when_only_the_sample_tail_overflows() {
2928        // `narrow_scan` inspects the sorted endpoints, so a batch whose prefix fits the
2929        // `i64` carrier but whose tail does not has to take the exact carrier. The tail
2930        // sample stays outside every window, so the selections keep the prefix ones.
2931        struct Case {
2932            time_unit: TimeUnit,
2933            narrow_safe: Vec<i64>,
2934            overflowing: i64,
2935            start: Millisecond,
2936            end: Millisecond,
2937            interval: Millisecond,
2938            range: Millisecond,
2939        }
2940
2941        let cases = [
2942            Case {
2943                time_unit: TimeUnit::Second,
2944                narrow_safe: vec![9_223_372_034, 9_223_372_035, 9_223_372_036],
2945                overflowing: 9_223_372_037,
2946                start: 9_223_372_034_000,
2947                end: 9_223_372_036_000,
2948                interval: 1_000,
2949                range: 1_000,
2950            },
2951            Case {
2952                time_unit: TimeUnit::Millisecond,
2953                narrow_safe: vec![9_223_372_036_853, 9_223_372_036_854],
2954                overflowing: 9_223_372_036_855,
2955                start: 9_223_372_036_853,
2956                end: 9_223_372_036_854,
2957                interval: 1,
2958                range: 1,
2959            },
2960            Case {
2961                time_unit: TimeUnit::Microsecond,
2962                narrow_safe: vec![9_223_372_036_853_999, 9_223_372_036_854_000],
2963                overflowing: 9_223_372_036_854_776,
2964                start: 9_223_372_036_854,
2965                end: 9_223_372_036_854,
2966                interval: 1,
2967                range: 1,
2968            },
2969        ];
2970
2971        for case in cases {
2972            let Case {
2973                time_unit,
2974                narrow_safe,
2975                overflowing,
2976                start,
2977                end,
2978                interval,
2979                range,
2980            } = case;
2981            let context = format!("unit={time_unit:?}, timestamps={narrow_safe:?} + {overflowing}");
2982            let scale = native_scale(time_unit);
2983            // The prefix fits the narrow carrier, the tail sample does not...
2984            assert!(
2985                narrow_safe
2986                    .iter()
2987                    .all(|timestamp| timestamp.checked_mul(scale).is_some()),
2988                "{context}: prefix samples must fit i64 nanoseconds"
2989            );
2990            assert!(
2991                overflowing.checked_mul(scale).is_none(),
2992                "{context}: the tail sample must overflow i64 nanoseconds"
2993            );
2994            // ...even though the millisecond payload of the tail stays legal.
2995            let payload = shifted_millis(time_unit, 0, overflowing);
2996            assert!(
2997                payload.abs() < i64::MAX / 2,
2998                "{context}: the shifted payload {payload} must stay legal"
2999            );
3000
3001            let stream = scan_stream_for_test(time_unit, 0, start, end, interval, range);
3002
3003            let (narrow, prefix_exact) = scan_both_carriers(&stream, &narrow_safe, start, end);
3004            let prefix_oracle =
3005                scan_oracle(time_unit, 0, range, &narrow_safe, start, end, interval);
3006            assert_eq!(
3007                prefix_exact, prefix_oracle,
3008                "{context}: prefix i128 carrier"
3009            );
3010            assert_narrow_carrier_agrees(narrow, &prefix_exact, &format!("{context} prefix"));
3011
3012            let mut timestamps = narrow_safe.clone();
3013            timestamps.push(overflowing);
3014            assert_eq!(
3015                narrow_carrier_rejection(time_unit, 0, range, &timestamps, start, end),
3016                Some("sample nanoseconds do not fit i64"),
3017                "{context}: the overflowing endpoint must reject the narrow carrier"
3018            );
3019            assert!(
3020                narrow_carrier_overflows(time_unit, 0, range, &timestamps, start, end, interval),
3021                "{context}: the scan evaluates the overflowing tail"
3022            );
3023
3024            let (narrow, exact) = scan_both_carriers(&stream, &timestamps, start, end);
3025            assert!(
3026                narrow.is_none(),
3027                "{context}: the i128 carrier must be taken"
3028            );
3029            let oracle = scan_oracle(time_unit, 0, range, &timestamps, start, end, interval);
3030            assert_eq!(exact, oracle, "{context}: exact carrier on the full batch");
3031            assert_eq!(
3032                exact, prefix_exact,
3033                "{context}: the out-of-window tail keeps the prefix selections"
3034            );
3035        }
3036    }
3037
3038    #[test]
3039    fn scan_carriers_split_at_the_nanosecond_boundaries() {
3040        // Nanoseconds are not scaled, so the `i64` carrier is rejected by the query bounds:
3041        // below the millisecond of `i64::MIN` the start bound underflows, while a batch near
3042        // `i64::MAX` still fits. Both keep a legal millisecond payload.
3043        let low = i64::MIN;
3044        let low_floor = shifted_floor_millis(TimeUnit::Nanosecond, 0, low);
3045        assert_eq!(low_floor, -9_223_372_036_855);
3046        assert!(low_floor.checked_mul(1_000_000).is_none());
3047        assert_eq!(
3048            shifted_millis(TimeUnit::Nanosecond, 0, low),
3049            -9_223_372_036_854
3050        );
3051
3052        let timestamps = vec![low, low + 1_000_000];
3053        let (start, end, interval, range) = (low_floor, low_floor + 1, 1, 1);
3054        let context = format!("nanosecond floor, timestamps={timestamps:?}");
3055        let stream = scan_stream_for_test(TimeUnit::Nanosecond, 0, start, end, interval, range);
3056        assert_eq!(
3057            narrow_carrier_rejection(TimeUnit::Nanosecond, 0, range, &timestamps, start, end),
3058            Some("start bound in nanoseconds does not fit i64"),
3059            "{context}"
3060        );
3061        let (narrow, exact) = scan_both_carriers(&stream, &timestamps, start, end);
3062        assert!(
3063            narrow.is_none(),
3064            "{context}: the i128 carrier must be taken"
3065        );
3066        let oracle = scan_oracle(
3067            TimeUnit::Nanosecond,
3068            0,
3069            range,
3070            &timestamps,
3071            start,
3072            end,
3073            interval,
3074        );
3075        assert_eq!(exact, oracle, "{context}: i128 carrier must stay exact");
3076
3077        // The ceiling side still fits the narrow carrier, so both carriers have to agree.
3078        let high = i64::MAX - 2_000_000;
3079        let high_floor = shifted_floor_millis(TimeUnit::Nanosecond, 0, high);
3080        assert!(high_floor.checked_mul(1_000_000).is_some());
3081        let timestamps = vec![high, high + 1_000_000, high + 2_000_000];
3082        let (start, end, interval, range) = (high_floor, high_floor + 2, 1, 1);
3083        let context = format!("nanosecond ceiling, timestamps={timestamps:?}");
3084        let stream = scan_stream_for_test(TimeUnit::Nanosecond, 0, start, end, interval, range);
3085        assert_eq!(
3086            narrow_carrier_rejection(TimeUnit::Nanosecond, 0, range, &timestamps, start, end),
3087            None,
3088            "{context}: the ceiling batch still fits the narrow carrier"
3089        );
3090        let (narrow, exact) = scan_both_carriers(&stream, &timestamps, start, end);
3091        let oracle = scan_oracle(
3092            TimeUnit::Nanosecond,
3093            0,
3094            range,
3095            &timestamps,
3096            start,
3097            end,
3098            interval,
3099        );
3100        assert_eq!(exact, oracle, "{context}: i128 carrier must stay exact");
3101        assert_narrow_carrier_agrees(narrow, &exact, &context);
3102    }
3103
3104    #[test]
3105    fn scan_carriers_agree_for_seeded_native_precision_matrix() {
3106        // Seeded matrix over all four native precisions: positive and negative offsets,
3107        // negative times, sub-millisecond samples sharing a millisecond payload, and native
3108        // extremes whose intermediate product overflows while the shifted millisecond stays
3109        // legal.
3110        let units = [
3111            TimeUnit::Second,
3112            TimeUnit::Millisecond,
3113            TimeUnit::Microsecond,
3114            TimeUnit::Nanosecond,
3115        ];
3116        // `narrow_scan` rejects the two large offsets: `offset * 1ms` overflows `i64`.
3117        let offsets = [0, 1, -1, 250, -250, 9_223_372_036_855, -9_223_372_036_855];
3118        let extremes = |time_unit: TimeUnit| match time_unit {
3119            TimeUnit::Second => [-9_223_372_037, 9_223_372_037],
3120            TimeUnit::Millisecond => [-9_223_372_036_855, 9_223_372_036_855],
3121            TimeUnit::Microsecond => [-9_223_372_036_854_776, 9_223_372_036_854_776],
3122            TimeUnit::Nanosecond => [i64::MIN, i64::MAX],
3123        };
3124        let mut prng = TinyPrng(0x5ca1_ab1e_d1ff_0001);
3125        let mut narrow_cases = 0usize;
3126        let mut wide_cases = 0usize;
3127        let mut skipped = 0usize;
3128
3129        for case in 0..256usize {
3130            let time_unit = units[case % units.len()];
3131            let offset = offsets[(case / units.len()) % offsets.len()];
3132            let [low, high] = extremes(time_unit);
3133            let anchor = match prng.next_i64(0, 3) {
3134                0 => prng.next_i64(-2_000, 2_000),
3135                1 => -prng.next_i64(1, 2_000),
3136                2 => low,
3137                _ => high,
3138            };
3139            // Samples stay within a few milliseconds, with a native granularity small
3140            // enough that sub-millisecond samples share a millisecond payload.
3141            let step = match time_unit {
3142                TimeUnit::Second => 1,
3143                TimeUnit::Millisecond => 1,
3144                TimeUnit::Microsecond => 1 + prng.next_i64(0, 9),
3145                TimeUnit::Nanosecond => 1 + prng.next_i64(0, 999_999),
3146            };
3147            let count = 1 + prng.next_i64(0, 4);
3148            let ascending = anchor != high;
3149            let mut timestamps = (0..count)
3150                .map(|index| {
3151                    let delta = index * step;
3152                    if ascending {
3153                        anchor + delta
3154                    } else {
3155                        anchor - delta
3156                    }
3157                })
3158                .collect::<Vec<_>>();
3159            if !ascending {
3160                timestamps.reverse();
3161            }
3162            // The review shape keeps a legal millisecond payload; skip the extremes that the
3163            // large offsets push out of the millisecond space.
3164            let nanoseconds = timestamps
3165                .iter()
3166                .map(|timestamp| shifted_nanoseconds(time_unit, offset, *timestamp))
3167                .collect::<Vec<_>>();
3168            if nanoseconds
3169                .iter()
3170                .any(|value| i64::try_from(value / 1_000_000).is_err())
3171            {
3172                skipped += 1;
3173                continue;
3174            }
3175
3176            let first_millis = Millisecond::try_from(nanoseconds[0].div_euclid(1_000_000)).unwrap();
3177            let interval = prng.next_i64(1, 4);
3178            let points = 1 + prng.next_i64(0, 4);
3179            let start = first_millis - interval;
3180            let end = start + interval * (points - 1);
3181            let range = prng.next_i64(0, 3) * interval;
3182            let context = format!(
3183                "case={case}, unit={time_unit:?}, offset={offset}, start={start}, end={end}, \
3184                 interval={interval}, range={range}, timestamps={timestamps:?}"
3185            );
3186
3187            let stream = scan_stream_for_test(time_unit, offset, start, end, interval, range);
3188            let oracle = scan_oracle(time_unit, offset, range, &timestamps, start, end, interval);
3189            let (narrow, exact) = scan_both_carriers(&stream, &timestamps, start, end);
3190            assert_eq!(exact, oracle, "{context}: i128 carrier must stay exact");
3191
3192            match (
3193                &narrow,
3194                narrow_carrier_rejection(time_unit, offset, range, &timestamps, start, end),
3195            ) {
3196                (Some(_), None) => {
3197                    narrow_cases += 1;
3198                    assert!(
3199                        !narrow_carrier_overflows(
3200                            time_unit,
3201                            offset,
3202                            range,
3203                            &timestamps,
3204                            start,
3205                            end,
3206                            interval
3207                        ),
3208                        "{context}: every narrow operation must fit i64"
3209                    );
3210                    assert_narrow_carrier_agrees(narrow, &exact, &context);
3211                }
3212                (None, Some(_reason)) => {
3213                    wide_cases += 1;
3214                }
3215                (narrow, rejection) => panic!(
3216                    "{context}: narrow_scan and its mirror disagree, narrow={}, rejection={rejection:?}",
3217                    narrow.is_some()
3218                ),
3219            }
3220        }
3221
3222        assert!(
3223            narrow_cases > 0 && wide_cases > 0,
3224            "both carriers must be covered: {narrow_cases} narrow, {wide_cases} wide \
3225             ({skipped} skipped)"
3226        );
3227        assert!(
3228            skipped < 64,
3229            "the generator skipped too many cases: {skipped}"
3230        );
3231    }
3232
3233    /// Builds the single-column timestamp batch the cursor scans consume.
3234    fn timestamp_batch(time_unit: TimeUnit, timestamps: &[i64]) -> RecordBatch {
3235        let schema = Arc::new(Schema::new(vec![Field::new(
3236            TIME_INDEX_COLUMN,
3237            DataType::Timestamp(time_unit, None),
3238            false,
3239        )]));
3240        let array: ArrayRef = match time_unit {
3241            TimeUnit::Second => Arc::new(TimestampSecondArray::from(timestamps.to_vec())),
3242            TimeUnit::Millisecond => Arc::new(TimestampMillisecondArray::from(timestamps.to_vec())),
3243            TimeUnit::Microsecond => Arc::new(TimestampMicrosecondArray::from(timestamps.to_vec())),
3244            TimeUnit::Nanosecond => Arc::new(TimestampNanosecondArray::from(timestamps.to_vec())),
3245        };
3246        RecordBatch::try_new(schema, vec![array]).unwrap()
3247    }
3248
3249    #[test]
3250    fn calculate_range_dispatches_carriers_and_agrees_pointwise() {
3251        // Production dispatch, not the hooks: `calculate_range` must scan through the narrow
3252        // carrier while the batch fits and through the exact one once the shifted tail does
3253        // not, and both must select the same ranges for the bounds it derives itself.
3254        let narrow_timestamps = [0i64, 15_000, 30_000];
3255        let wide_timestamps = [9_223_372_036_853, 9_223_372_036_854, 9_223_372_036_855];
3256        let cases: [(&str, &[i64], Millisecond, Millisecond, bool); 2] = [
3257            ("narrow carrier", &narrow_timestamps, 0, 30_000, true),
3258            (
3259                "i128 fallback",
3260                &wide_timestamps,
3261                wide_timestamps[0],
3262                wide_timestamps[1],
3263                false,
3264            ),
3265        ];
3266
3267        for (name, timestamps, query_start, query_end, expect_narrow) in cases {
3268            let (interval, range) = if expect_narrow {
3269                (15_000, 15_000)
3270            } else {
3271                (1, 1)
3272            };
3273            // `scan_stream_for_test` panics on an empty batch when `narrow_scan` is asked for
3274            // the endpoints, so the batch below is never empty.
3275            let stream = scan_stream_for_test(
3276                TimeUnit::Millisecond,
3277                0,
3278                query_start,
3279                query_end,
3280                interval,
3281                range,
3282            );
3283            let batch = timestamp_batch(TimeUnit::Millisecond, timestamps);
3284            let (ranges, (start, end)) = stream.calculate_range(&batch).unwrap();
3285
3286            let oracle = scan_oracle(
3287                TimeUnit::Millisecond,
3288                0,
3289                range,
3290                timestamps,
3291                start,
3292                end,
3293                interval,
3294            );
3295            assert_eq!(ranges, oracle, "{name}: calculate_range pointwise");
3296
3297            let rejection =
3298                narrow_carrier_rejection(TimeUnit::Millisecond, 0, range, timestamps, start, end);
3299            assert_eq!(
3300                rejection.is_none(),
3301                expect_narrow,
3302                "{name}: carrier choice by narrow_scan, rejection={rejection:?}"
3303            );
3304            let (narrow, exact) = scan_both_carriers(&stream, timestamps, start, end);
3305            assert_eq!(exact, ranges, "{name}: i128 carrier is the oracle");
3306            assert_eq!(narrow.is_some(), expect_narrow, "{name}: narrow carrier");
3307            assert_eq!(
3308                RangeManipulateStream::bench_scan_wide(
3309                    stream.time_unit,
3310                    stream.offset,
3311                    stream.range,
3312                    interval,
3313                    timestamps,
3314                    start,
3315                    end,
3316                ),
3317                ranges,
3318                "{name}: the benchmark hook matches calculate_range"
3319            );
3320        }
3321    }
3322
3323    /// Stream whose single millisecond timestamp column is folded into the range payload.
3324    fn payload_stream(
3325        offset: Millisecond,
3326        start: Millisecond,
3327        end: Millisecond,
3328        interval: Millisecond,
3329        range: Millisecond,
3330    ) -> RangeManipulateStream {
3331        let payload_type = DataType::Timestamp(TimeUnit::Millisecond, None);
3332        let schema = Arc::new(Schema::new(vec![
3333            Field::new(TIME_INDEX_COLUMN, payload_type.clone(), false),
3334            Field::new(
3335                RangeManipulate::build_timestamp_range_name(TIME_INDEX_COLUMN),
3336                RangeArray::convert_data_type(payload_type),
3337                false,
3338            ),
3339        ]));
3340        let mut stream =
3341            scan_stream_for_test(TimeUnit::Millisecond, offset, start, end, interval, range);
3342        stream.output_schema = schema;
3343        stream
3344    }
3345
3346    /// Builds the single-column millisecond timestamp batch the payload tests consume.
3347    fn timestamp_batch_with_timezone(timezone: Option<&str>, timestamps: &[i64]) -> RecordBatch {
3348        let schema = Arc::new(Schema::new(vec![Field::new(
3349            TIME_INDEX_COLUMN,
3350            DataType::Timestamp(TimeUnit::Millisecond, timezone.map(Arc::from)),
3351            false,
3352        )]));
3353        let array: ArrayRef = Arc::new(
3354            TimestampMillisecondArray::from(timestamps.to_vec()).with_timezone_opt(timezone),
3355        );
3356        RecordBatch::try_new(schema, vec![array]).unwrap()
3357    }
3358
3359    /// Millisecond payload values of the range timestamp column of a manipulated batch.
3360    fn payload_millis(batch: &RecordBatch) -> Vec<i64> {
3361        let dict = batch
3362            .column(batch.num_columns() - 1)
3363            .as_any()
3364            .downcast_ref::<DictionaryArray<Int64Type>>()
3365            .expect("the timestamp range payload must be a folded dictionary")
3366            .clone();
3367        let payload = RangeArray::try_new(dict).unwrap();
3368        assert_eq!(
3369            payload.values().data_type(),
3370            &DataType::Timestamp(TimeUnit::Millisecond, None),
3371            "the payload value type must agree with the output schema"
3372        );
3373        payload
3374            .values()
3375            .as_any()
3376            .downcast_ref::<TimestampMillisecondArray>()
3377            .unwrap()
3378            .values()
3379            .to_vec()
3380    }
3381
3382    #[test]
3383    fn manipulate_timezone_time_index_skips_zero_copy_payload() {
3384        // Oracle-verified regression: `Timestamp(Millisecond, Some(tz))` passes a
3385        // `time_unit == Millisecond` check but is not the payload ABI. Cloning such a column
3386        // would stamp a dictionary value type disagreeing with the output schema and
3387        // `RecordBatch::try_new` would fail, so the reuse must be guarded on the data type.
3388        let timestamps = [0i64, 1_000, 2_000, 3_000];
3389        let stream = payload_stream(0, 0, 3_000, 1_000, 1_000);
3390
3391        // With offset 0 the data type is the only reason to convert, and the `try_unary`
3392        // path is the identity for a millisecond index.
3393        let zoned = stream
3394            .manipulate(timestamp_batch_with_timezone(Some("+08:00"), &timestamps))
3395            .expect("a timezone-carrying index must still build the output batch")
3396            .expect("the ranges are not all empty");
3397        assert_eq!(
3398            payload_millis(&zoned),
3399            timestamps,
3400            "the converted payload must keep the try_unary values"
3401        );
3402
3403        // The same logical samples without a timezone do take the zero-copy path; both paths
3404        // must agree on the payload.
3405        let plain = stream
3406            .manipulate(timestamp_batch(TimeUnit::Millisecond, &timestamps))
3407            .unwrap()
3408            .unwrap();
3409        assert_eq!(
3410            payload_millis(&plain),
3411            payload_millis(&zoned),
3412            "the zero-copy and the converted path must agree"
3413        );
3414    }
3415
3416    /// A NULL slot's underlying value must never trigger the payload overflow
3417    /// error: a nullable batch is converted with arrow's `try_unary`, which applies
3418    /// the closure to valid rows only. The same value as a *valid* sample must still
3419    /// error, through the null-free `unary` + overflow-cell path.
3420    #[test]
3421    fn manipulate_null_slot_underlying_value_does_not_overflow() {
3422        // Second-slot underlying value i64::MAX seconds overflows on conversion,
3423        // but the slot is NULL so the conversion must not see it. offset=1ms
3424        // keeps the batch off the zero-copy path. The query range must be bounded
3425        // and contain the first sample: with `end == 0` no range is selected and
3426        // the conversion (and so the regression) is skipped, while an unbounded
3427        // `end` makes the range scan visit `end - start` evaluation points.
3428        let timestamps = [0i64, i64::MAX];
3429        let validity = NullBuffer::from(vec![true, false]);
3430        let array: ArrayRef = Arc::new(TimestampSecondArray::new(
3431            timestamps.to_vec().into(),
3432            Some(validity),
3433        ));
3434        let schema = Arc::new(Schema::new(vec![Field::new(
3435            TIME_INDEX_COLUMN,
3436            DataType::Timestamp(TimeUnit::Second, None),
3437            true,
3438        )]));
3439        let batch = RecordBatch::try_new(schema, vec![array]).unwrap();
3440
3441        let mut stream = payload_stream(1, 0, 1_000, 1, 1_000);
3442        stream.time_unit = TimeUnit::Second;
3443        let output = stream
3444            .manipulate(batch)
3445            .expect("a NULL slot's underlying value must not overflow the payload")
3446            .expect("the first sample (0s + 1ms) is inside the query range");
3447        assert_eq!(
3448            payload_millis(&output)[0],
3449            1,
3450            "the valid sample must be converted to its shifted millisecond payload"
3451        );
3452
3453        // Same underlying value, but now valid: the conversion must reject it.
3454        let array: ArrayRef = Arc::new(TimestampSecondArray::from(timestamps.to_vec()));
3455        let schema = Arc::new(Schema::new(vec![Field::new(
3456            TIME_INDEX_COLUMN,
3457            DataType::Timestamp(TimeUnit::Second, None),
3458            false,
3459        )]));
3460        let batch = RecordBatch::try_new(schema, vec![array]).unwrap();
3461        let mut stream = payload_stream(1, 0, 1_000, 1, 1_000);
3462        stream.time_unit = TimeUnit::Second;
3463        let error = stream
3464            .manipulate(batch)
3465            .expect_err("a valid overflowing timestamp must still error");
3466        assert!(
3467            error.to_string().contains("timestamp payload overflow"),
3468            "unexpected error: {error}"
3469        );
3470    }
3471}