1use 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#[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 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 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 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 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 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 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 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 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 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 pub fn manipulate(&self, input: RecordBatch) -> DataFusionResult<Option<RecordBatch>> {
685 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 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 let (ranges, (start, end)) = self.calculate_range_on_timestamps(&ts_column)?;
737 if ranges.iter().all(|(_, len)| *len == 0) {
739 return Ok(None);
740 }
741
742 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 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 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 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>(×tamps, |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>(×tamps, |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 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 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 #[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 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 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 let start = start as i64;
897 let end = end as i64;
898
899 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 #[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 #[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
974fn 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#[derive(Debug, Clone, Copy)]
1012pub(super) struct ScanNanoseconds<Ns> {
1013 scale: Ns,
1015 offset_ns: Ns,
1017 window_ns: Ns,
1019}
1020
1021pub(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 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 pub(super) fn shift(&self, timestamp: i64) -> Ns {
1047 Ns::from(timestamp) * self.scale + self.offset_ns
1048 }
1049
1050 pub(super) fn instant(&self, millis: Millisecond) -> Ns {
1052 Ns::from(millis) * Ns::from(1_000_000)
1053 }
1054
1055 pub(super) fn window_start(&self, instant: Ns) -> Ns {
1057 instant - self.window_ns
1058 }
1059}
1060
1061pub(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 let shift = |timestamp: i64| timestamp.checked_mul(scale)?.checked_add(offset_ns);
1075 shift(*timestamps.first()?)?;
1076 shift(*timestamps.last()?)?;
1077 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 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, 180_000, 240_000, 241_000, 271_000, 291_000, ])) 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 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 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 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 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 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 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 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, 180_000, 240_000, 241_000, 271_000, 291_000, ])) 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 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 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 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, end: 1758093334000, interval: 30000, range: 60000, 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 let test_timestamps = vec![
2167 1758093260000, 1758093290000, 1758093320000, ];
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 assert_eq!(
2183 start % 30000,
2184 1758093274000 % 30000,
2185 "Optimized start should preserve query alignment pattern"
2186 );
2187
2188 let expected_timestamps: Vec<i64> = (start..=end).step_by(30000).collect();
2190 assert_eq!(ranges.len(), expected_timestamps.len());
2191
2192 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 ×tamps,
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 fn calculate_range_oracle(
2347 timestamps: &[i64],
2348 start: i64,
2349 end: i64,
2350 interval: i64,
2351 range: i64,
2352 ) -> Vec<(u32, u32)> {
2353 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, ×tamps);
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, ×tamps);
2486 let expected = calculate_range_oracle(×tamps, 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, ×tamps);
2496
2497 assert_eq!(bounds, (5, 35));
2498 assert_eq!(
2499 calculate_range_oracle(×tamps, 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, ×tamps);
2515 assert_eq!((start, end), (0, 3_600), "{name} bounds");
2516 assert_eq!(
2517 actual,
2518 calculate_range_oracle(×tamps, 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, ×tamps);
2558 let expected = calculate_range_oracle(×tamps, 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 fn native_scale(time_unit: TimeUnit) -> i64 {
2580 i64::try_from(nanoseconds_per_native_tick(time_unit)).unwrap()
2581 }
2582
2583 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 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 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 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 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 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 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 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 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 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 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, ×tamps, 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, ×tamps, start, end);
2905 assert_eq!(exact, oracle, "{context}: i128 carrier must stay exact");
2906 assert_narrow_carrier_agrees(narrow, &exact, &context);
2907
2908 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 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 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 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, ×tamps, 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, ×tamps, start, end, interval),
3021 "{context}: the scan evaluates the overflowing tail"
3022 );
3023
3024 let (narrow, exact) = scan_both_carriers(&stream, ×tamps, 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, ×tamps, 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 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, ×tamps, start, end),
3058 Some("start bound in nanoseconds does not fit i64"),
3059 "{context}"
3060 );
3061 let (narrow, exact) = scan_both_carriers(&stream, ×tamps, 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 ×tamps,
3071 start,
3072 end,
3073 interval,
3074 );
3075 assert_eq!(exact, oracle, "{context}: i128 carrier must stay exact");
3076
3077 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, ×tamps, start, end),
3087 None,
3088 "{context}: the ceiling batch still fits the narrow carrier"
3089 );
3090 let (narrow, exact) = scan_both_carriers(&stream, ×tamps, start, end);
3091 let oracle = scan_oracle(
3092 TimeUnit::Nanosecond,
3093 0,
3094 range,
3095 ×tamps,
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 let units = [
3111 TimeUnit::Second,
3112 TimeUnit::Millisecond,
3113 TimeUnit::Microsecond,
3114 TimeUnit::Nanosecond,
3115 ];
3116 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 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 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, ×tamps, start, end, interval);
3189 let (narrow, exact) = scan_both_carriers(&stream, ×tamps, 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, ×tamps, start, end),
3195 ) {
3196 (Some(_), None) => {
3197 narrow_cases += 1;
3198 assert!(
3199 !narrow_carrier_overflows(
3200 time_unit,
3201 offset,
3202 range,
3203 ×tamps,
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 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 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 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 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 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 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 let timestamps = [0i64, 1_000, 2_000, 3_000];
3389 let stream = payload_stream(0, 0, 3_000, 1_000, 1_000);
3390
3391 let zoned = stream
3394 .manipulate(timestamp_batch_with_timezone(Some("+08:00"), ×tamps))
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 let plain = stream
3406 .manipulate(timestamp_batch(TimeUnit::Millisecond, ×tamps))
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 #[test]
3421 fn manipulate_null_slot_underlying_value_does_not_overflow() {
3422 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 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}