1use std::sync::Arc;
16
17use arrow_schema::DataType;
18use datafusion::physical_optimizer::PhysicalOptimizerRule;
19use datafusion::physical_plan::coalesce_partitions::CoalescePartitionsExec;
20use datafusion::physical_plan::coop::CooperativeExec;
21use datafusion::physical_plan::filter::FilterExec;
22use datafusion::physical_plan::projection::ProjectionExec;
23use datafusion::physical_plan::repartition::RepartitionExec;
24use datafusion::physical_plan::sorts::sort::SortExec;
25use datafusion::physical_plan::sorts::sort_preserving_merge::SortPreservingMergeExec;
26use datafusion::physical_plan::{ChildrenPropertiesMode, ExecutionPlan, ReplaceChildrenOptions};
27use datafusion_common::Result as DataFusionResult;
28use datafusion_common::tree_node::{Transformed, TreeNode};
29use datafusion_physical_expr::expressions::{CastExpr, Column as PhysicalColumn};
30use datafusion_physical_expr::{PhysicalExpr, ScalarFunctionExpr};
31use store_api::region_engine::PartitionRange;
32use table::table::scan::RegionScanExec;
33
34use crate::part_sort::PartSortExec;
35use crate::window_sort::WindowedSortExec;
36
37#[derive(Debug)]
47pub struct WindowedSortPhysicalRule;
48
49impl PhysicalOptimizerRule for WindowedSortPhysicalRule {
50 fn optimize(
51 &self,
52 plan: Arc<dyn ExecutionPlan>,
53 config: &datafusion::config::ConfigOptions,
54 ) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
55 Self::do_optimize(plan, config)
56 }
57
58 fn name(&self) -> &str {
59 "WindowedSortRule"
60 }
61
62 fn schema_check(&self) -> bool {
63 false
64 }
65}
66
67impl WindowedSortPhysicalRule {
68 fn do_optimize(
69 plan: Arc<dyn ExecutionPlan>,
70 _config: &datafusion::config::ConfigOptions,
71 ) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
72 let result = plan
73 .transform_down(|plan| {
74 if let Some(sort_exec) = plan.downcast_ref::<SortExec>() {
75 if sort_exec.expr().len() != 1 {
77 return Ok(Transformed::no(plan));
78 }
79
80 let preserve_partitioning = sort_exec.preserve_partitioning();
81
82 let sort_input = remove_repartition(sort_exec.input().clone())?.data;
83
84 let Some(scanner_info) = fetch_partition_range(sort_input.clone())? else {
86 return Ok(Transformed::no(plan));
87 };
88 if scanner_info.partition_ranges.is_empty() {
90 return Ok(Transformed::no(plan));
91 }
92 let input_schema = sort_input.schema();
93
94 let first_sort_expr = sort_exec.expr().first();
95 if let Some(column_expr) = first_sort_expr.expr.downcast_ref::<PhysicalColumn>()
96 && matches!(
97 input_schema.field(column_expr.index()).data_type(),
98 DataType::Timestamp(_, _)
99 )
100 && is_time_index_expr(&sort_input, &first_sort_expr.expr)?
101 && sort_exec.fetch().is_none()
102 {
104 } else {
105 return Ok(Transformed::no(plan));
106 }
107
108 let new_input = if scanner_info.tag_columns.is_empty()
112 && !first_sort_expr.options.descending
113 {
114 sort_input
115 } else {
116 Arc::new(PartSortExec::try_new(
117 first_sort_expr.clone(),
118 sort_exec.fetch(),
119 scanner_info.partition_ranges.clone(),
120 sort_input,
121 )?)
122 };
123
124 let windowed_sort_exec = WindowedSortExec::try_new(
125 first_sort_expr.clone(),
126 sort_exec.fetch(),
127 scanner_info.partition_ranges,
128 new_input,
129 )?;
130
131 if !preserve_partitioning {
132 let order_preserving_merge = SortPreservingMergeExec::new(
133 sort_exec.expr().clone(),
134 Arc::new(windowed_sort_exec),
135 );
136 return Ok(Transformed {
137 data: Arc::new(order_preserving_merge),
138 transformed: true,
139 tnr: datafusion_common::tree_node::TreeNodeRecursion::Stop,
140 });
141 } else {
142 return Ok(Transformed {
143 data: Arc::new(windowed_sort_exec),
144 transformed: true,
145 tnr: datafusion_common::tree_node::TreeNodeRecursion::Stop,
146 });
147 }
148 }
149
150 Ok(Transformed::no(plan))
151 })?
152 .data;
153
154 Ok(result)
155 }
156}
157
158#[derive(Debug)]
159struct ScannerInfo {
160 partition_ranges: Vec<Vec<PartitionRange>>,
161 tag_columns: Vec<String>,
162}
163
164fn fetch_partition_range(input: Arc<dyn ExecutionPlan>) -> DataFusionResult<Option<ScannerInfo>> {
165 let mut partition_ranges = None;
166 let mut tag_columns = None;
167
168 input.transform_up(|plan| {
169 if plan.is::<CooperativeExec>() {
170 return Ok(Transformed::no(plan));
171 }
172
173 if plan.is::<RepartitionExec>()
175 || plan.is::<CoalescePartitionsExec>()
176 || plan.is::<SortExec>()
177 || plan.is::<WindowedSortExec>()
178 {
179 partition_ranges = None;
180 }
181
182 if !(plan.is::<ProjectionExec>() || plan.is::<FilterExec>()) {
185 partition_ranges = None;
186 }
187
188 if let Some(region_scan_exec) = plan.downcast_ref::<RegionScanExec>() {
189 if region_scan_exec.distribution()
191 == Some(store_api::storage::TimeSeriesDistribution::PerSeries)
192 {
193 partition_ranges = None;
194 return Ok(Transformed::no(plan));
195 }
196
197 partition_ranges = Some(region_scan_exec.get_uncollapsed_partition_ranges());
198 tag_columns = Some(region_scan_exec.tag_columns());
199
200 region_scan_exec.with_distinguish_partition_range(true);
201 }
202
203 Ok(Transformed::no(plan))
204 })?;
205
206 let result = (|| {
207 Some(ScannerInfo {
208 partition_ranges: partition_ranges?,
209 tag_columns: tag_columns?,
210 })
211 })();
212
213 Ok(result)
214}
215
216fn is_time_index_expr(
217 plan: &Arc<dyn ExecutionPlan>,
218 expr: &Arc<dyn PhysicalExpr>,
219) -> DataFusionResult<bool> {
220 if let Some(column_expr) = expr.downcast_ref::<PhysicalColumn>() {
221 return is_time_index_column(plan, column_expr);
222 }
223
224 if let Some(cast_expr) = expr.downcast_ref::<CastExpr>() {
225 let source_type = cast_expr.expr().data_type(plan.schema().as_ref())?;
226 return if preserves_timestamp_values(cast_expr.cast_type(), &source_type) {
227 is_time_index_expr(plan, cast_expr.expr())
228 } else {
229 Ok(false)
230 };
231 }
232
233 if let Some(scalar_function_expr) = expr.downcast_ref::<ScalarFunctionExpr>() {
234 if !is_supported_time_index_wrapper(scalar_function_expr)
235 || scalar_function_expr.args().len() != 1
236 {
237 return Ok(false);
238 }
239
240 let arg = &scalar_function_expr.args()[0];
246 let return_type = scalar_function_expr.data_type(plan.schema().as_ref())?;
247 let arg_type = arg.data_type(plan.schema().as_ref())?;
248 return if preserves_timestamp_values(&return_type, &arg_type) {
249 is_time_index_expr(plan, arg)
250 } else {
251 Ok(false)
252 };
253 }
254
255 Ok(false)
256}
257
258fn preserves_timestamp_values(target: &DataType, source: &DataType) -> bool {
267 matches!(
268 (target, source),
269 (
270 DataType::Timestamp(target_unit, target_tz),
271 DataType::Timestamp(source_unit, source_tz)
272 ) if target_unit == source_unit && (source_tz.is_some() || target_tz.is_none())
273 )
274}
275
276fn is_time_index_column(
277 plan: &Arc<dyn ExecutionPlan>,
278 column_expr: &PhysicalColumn,
279) -> DataFusionResult<bool> {
280 if let Some(projection) = plan.downcast_ref::<ProjectionExec>() {
281 let Some(projection_expr) = projection.expr().get(column_expr.index()) else {
282 return Ok(false);
283 };
284 return is_time_index_expr(projection.input(), &projection_expr.expr);
285 }
286
287 if let Some(filter) = plan.downcast_ref::<FilterExec>() {
288 let child_column_expr = filter
289 .projection()
290 .as_ref()
291 .and_then(|projection| projection.get(column_expr.index()).copied())
292 .map(|input_index| {
293 PhysicalColumn::new(
294 filter.input().schema().field(input_index).name(),
295 input_index,
296 )
297 })
298 .unwrap_or_else(|| column_expr.clone());
299 let child_expr = Arc::new(child_column_expr) as Arc<dyn PhysicalExpr>;
300 return is_time_index_expr(filter.input(), &child_expr);
301 }
302
303 if let Some(region_scan_exec) = plan.downcast_ref::<RegionScanExec>() {
304 let schema = plan.schema();
305 let column_field = schema.field(column_expr.index());
306 return Ok(
307 matches!(column_field.data_type(), DataType::Timestamp(_, _))
308 && *column_field.name() == region_scan_exec.time_index(),
309 );
310 }
311
312 let Some(child) = passthrough_child(plan.as_ref()) else {
313 return Ok(false);
314 };
315 let child_expr = Arc::new(column_expr.clone()) as Arc<dyn PhysicalExpr>;
316 is_time_index_expr(&child, &child_expr)
317}
318
319fn passthrough_child(plan: &dyn ExecutionPlan) -> Option<Arc<dyn ExecutionPlan>> {
320 if plan.is::<CoalescePartitionsExec>()
321 || plan.is::<RepartitionExec>()
322 || plan.is::<CooperativeExec>()
323 {
324 return schema_preserving_child(plan);
325 }
326
327 None
328}
329
330fn schema_preserving_child(plan: &dyn ExecutionPlan) -> Option<Arc<dyn ExecutionPlan>> {
331 let child = plan.children().first().cloned().cloned()?;
332 (plan.schema().as_ref() == child.schema().as_ref()).then_some(child)
333}
334
335fn is_supported_time_index_wrapper(expr: &ScalarFunctionExpr) -> bool {
336 (expr.name().eq_ignore_ascii_case("to_timestamp")
337 || expr.name().eq_ignore_ascii_case("to_timestamp_seconds")
338 || expr.name().eq_ignore_ascii_case("to_timestamp_millis")
339 || expr.name().eq_ignore_ascii_case("to_timestamp_micros")
340 || expr.name().eq_ignore_ascii_case("to_timestamp_nanos"))
341 && matches!(expr.return_type(), DataType::Timestamp(_, _))
342}
343
344fn remove_repartition(
346 plan: Arc<dyn ExecutionPlan>,
347) -> DataFusionResult<Transformed<Arc<dyn ExecutionPlan>>> {
348 plan.transform_down(|plan| {
349 if plan.is::<FilterExec>() {
350 let maybe_repartition = plan.children()[0];
352 if maybe_repartition.is::<RepartitionExec>() {
353 let maybe_scan = maybe_repartition.children()[0];
354 if maybe_scan.is::<RegionScanExec>() {
355 let new_filter = plan.clone().replace_children(
356 vec![maybe_scan.clone()],
357 ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
358 )?;
359 return Ok(Transformed::yes(new_filter));
360 }
361 }
362 }
363
364 Ok(Transformed::no(plan))
365 })
366}
367
368#[cfg(test)]
369mod test {
370 use std::sync::Arc;
371
372 use api::v1::SemanticType;
373 use arrow_schema::{Field, TimeUnit};
374 use common_recordbatch::RecordBatches;
375 use datafusion::config::ConfigOptions;
376 use datafusion::physical_plan::filter::FilterExecBuilder;
377 use datafusion_common::ScalarValue;
378 use datafusion_functions::datetime::{to_timestamp, to_timestamp_millis, to_timestamp_seconds};
379 use datafusion_physical_expr::expressions::{CastExpr, Literal};
380 use datatypes::data_type::ConcreteDataType;
381 use datatypes::schema::{ColumnSchema, Schema};
382 use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder};
383 use store_api::region_engine::SinglePartitionScanner;
384 use store_api::storage::{RegionId, ScanRequest};
385
386 use super::*;
387
388 #[test]
389 fn test_keep_ordinary_sort_when_scan_has_no_partition_ranges() {
390 let ordering = datafusion_physical_expr::LexOrdering::new([
391 datafusion_physical_expr::PhysicalSortExpr {
392 expr: Arc::new(PhysicalColumn::new("ts", 1)),
393 options: Default::default(),
394 },
395 ])
396 .unwrap();
397 let sort = Arc::new(SortExec::new(ordering, new_region_scan()));
398 let optimized =
399 WindowedSortPhysicalRule::do_optimize(sort, &ConfigOptions::default()).unwrap();
400 assert!(optimized.is::<SortExec>());
401 }
402
403 #[test]
404 fn test_is_time_index_expr_tracks_aliases_through_projection() {
405 let scan = new_region_scan();
406 let projection = Arc::new(
407 ProjectionExec::try_new(
408 vec![(
409 Arc::new(PhysicalColumn::new("ts", 1)) as Arc<dyn PhysicalExpr>,
410 "alias_ts".to_string(),
411 )],
412 scan,
413 )
414 .unwrap(),
415 ) as Arc<dyn ExecutionPlan>;
416 let expr = Arc::new(PhysicalColumn::new("alias_ts", 0)) as Arc<dyn PhysicalExpr>;
417
418 assert!(is_time_index_expr(&projection, &expr).unwrap());
419 }
420
421 #[test]
422 fn test_is_time_index_expr_tracks_multi_level_aliases() {
423 let scan = new_region_scan();
424 let first_projection = Arc::new(
425 ProjectionExec::try_new(
426 vec![(
427 Arc::new(PhysicalColumn::new("ts", 1)) as Arc<dyn PhysicalExpr>,
428 "alias_1".to_string(),
429 )],
430 scan,
431 )
432 .unwrap(),
433 ) as Arc<dyn ExecutionPlan>;
434 let second_projection = Arc::new(
435 ProjectionExec::try_new(
436 vec![(
437 Arc::new(PhysicalColumn::new("alias_1", 0)) as Arc<dyn PhysicalExpr>,
438 "alias_2".to_string(),
439 )],
440 first_projection,
441 )
442 .unwrap(),
443 ) as Arc<dyn ExecutionPlan>;
444 let expr = Arc::new(PhysicalColumn::new("alias_2", 0)) as Arc<dyn PhysicalExpr>;
445
446 assert!(is_time_index_expr(&second_projection, &expr).unwrap());
447 }
448
449 #[test]
450 fn test_is_time_index_expr_tracks_value_preserving_wrapper_through_projection() {
451 let scan = new_region_scan_with_unit(TimeUnit::Millisecond);
452 let config = Arc::new(ConfigOptions::default());
453 let wrapper = ScalarFunctionExpr::try_new(
454 to_timestamp_millis(config.as_ref()),
455 vec![Arc::new(PhysicalColumn::new("ts", 1))],
456 scan.schema().as_ref(),
457 config,
458 )
459 .unwrap();
460 assert_eq!(
462 wrapper.data_type(scan.schema().as_ref()).unwrap(),
463 DataType::Timestamp(TimeUnit::Millisecond, None)
464 );
465 let projection = Arc::new(
466 ProjectionExec::try_new(
467 vec![(Arc::new(wrapper) as Arc<dyn PhysicalExpr>, "ts".to_string())],
468 scan,
469 )
470 .unwrap(),
471 ) as Arc<dyn ExecutionPlan>;
472 let expr = Arc::new(PhysicalColumn::new("ts", 0)) as Arc<dyn PhysicalExpr>;
473
474 assert!(is_time_index_expr(&projection, &expr).unwrap());
476 }
477
478 #[test]
479 fn test_is_time_index_expr_rejects_unit_changing_wrapper() {
480 let config = Arc::new(ConfigOptions::default());
481
482 let nanosecond_scan = new_region_scan();
485 let truncates_to_millis = Arc::new(
486 ScalarFunctionExpr::try_new(
487 to_timestamp_millis(config.as_ref()),
488 vec![Arc::new(PhysicalColumn::new("ts", 1))],
489 nanosecond_scan.schema().as_ref(),
490 config.clone(),
491 )
492 .unwrap(),
493 ) as Arc<dyn PhysicalExpr>;
494 assert_eq!(
495 truncates_to_millis
496 .data_type(nanosecond_scan.schema().as_ref())
497 .unwrap(),
498 DataType::Timestamp(TimeUnit::Millisecond, None)
499 );
500 assert!(!is_time_index_expr(&nanosecond_scan, &truncates_to_millis).unwrap());
501
502 let millisecond_scan = new_region_scan_with_unit(TimeUnit::Millisecond);
504 let truncates_to_seconds = Arc::new(
505 ScalarFunctionExpr::try_new(
506 to_timestamp_seconds(config.as_ref()),
507 vec![Arc::new(PhysicalColumn::new("ts", 1))],
508 millisecond_scan.schema().as_ref(),
509 config.clone(),
510 )
511 .unwrap(),
512 ) as Arc<dyn PhysicalExpr>;
513 assert_eq!(
514 truncates_to_seconds
515 .data_type(millisecond_scan.schema().as_ref())
516 .unwrap(),
517 DataType::Timestamp(TimeUnit::Second, None)
518 );
519 assert!(!is_time_index_expr(&millisecond_scan, &truncates_to_seconds).unwrap());
520
521 let from_integer = Arc::new(
523 ScalarFunctionExpr::try_new(
524 to_timestamp_millis(config.as_ref()),
525 vec![Arc::new(Literal::new(ScalarValue::Int64(Some(0))))],
526 nanosecond_scan.schema().as_ref(),
527 config,
528 )
529 .unwrap(),
530 ) as Arc<dyn PhysicalExpr>;
531 assert!(!is_time_index_expr(&nanosecond_scan, &from_integer).unwrap());
532 }
533
534 #[test]
535 fn test_is_time_index_expr_rejects_unit_changing_cast_through_projection() {
536 let scan = new_region_scan();
537 let projection = Arc::new(
538 ProjectionExec::try_new(
539 vec![(
540 Arc::new(CastExpr::new(
541 Arc::new(PhysicalColumn::new("ts", 1)),
542 DataType::Timestamp(TimeUnit::Millisecond, None),
543 None,
544 )) as Arc<dyn PhysicalExpr>,
545 "ts_ms".to_string(),
546 )],
547 scan,
548 )
549 .unwrap(),
550 ) as Arc<dyn ExecutionPlan>;
551 let expr = Arc::new(PhysicalColumn::new("ts_ms", 0)) as Arc<dyn PhysicalExpr>;
552
553 assert!(!is_time_index_expr(&projection, &expr).unwrap());
555 }
556
557 #[test]
558 fn test_is_time_index_expr_rejects_timezone_adding_cast() {
559 let scan = new_region_scan();
560
561 let adds_timezone = Arc::new(CastExpr::new(
563 Arc::new(PhysicalColumn::new("ts", 1)),
564 DataType::Timestamp(TimeUnit::Nanosecond, Some(Arc::from("+07:00"))),
565 None,
566 )) as Arc<dyn PhysicalExpr>;
567 assert!(!is_time_index_expr(&scan, &adds_timezone).unwrap());
568
569 let adds_utc_timezone = Arc::new(CastExpr::new(
571 Arc::new(PhysicalColumn::new("ts", 1)),
572 DataType::Timestamp(TimeUnit::Nanosecond, Some(Arc::from("UTC"))),
573 None,
574 )) as Arc<dyn PhysicalExpr>;
575 assert!(!is_time_index_expr(&scan, &adds_utc_timezone).unwrap());
576
577 let mut config = ConfigOptions::default();
581 config.execution.time_zone = Some("+08:00".to_string());
582 let config = Arc::new(config);
583 let aware_source = ScalarFunctionExpr::try_new(
584 to_timestamp(config.as_ref()),
585 vec![Arc::new(PhysicalColumn::new("ts", 1))],
586 scan.schema().as_ref(),
587 config.clone(),
588 )
589 .unwrap();
590 let projection = Arc::new(
591 ProjectionExec::try_new(
592 vec![(
593 Arc::new(aware_source) as Arc<dyn PhysicalExpr>,
594 "ts_aware".to_string(),
595 )],
596 scan,
597 )
598 .unwrap(),
599 ) as Arc<dyn ExecutionPlan>;
600 assert_eq!(
601 projection.schema().field(0).data_type(),
602 &DataType::Timestamp(TimeUnit::Nanosecond, Some(Arc::from("+08:00")))
603 );
604 let expr = Arc::new(PhysicalColumn::new("ts_aware", 0)) as Arc<dyn PhysicalExpr>;
605
606 assert!(!is_time_index_expr(&projection, &expr).unwrap());
607
608 let aware_cast = Arc::new(CastExpr::new(
612 Arc::new(PhysicalColumn::new("ts_aware", 0)),
613 DataType::Timestamp(TimeUnit::Nanosecond, Some(Arc::from("+07:00"))),
614 None,
615 )) as Arc<dyn PhysicalExpr>;
616
617 assert!(!is_time_index_expr(&projection, &aware_cast).unwrap());
618 }
619
620 #[test]
621 fn test_preserves_timestamp_values() {
622 let naive_nanosecond = DataType::Timestamp(TimeUnit::Nanosecond, None);
623 let naive_millisecond = DataType::Timestamp(TimeUnit::Millisecond, None);
624 let aware_nanosecond = DataType::Timestamp(TimeUnit::Nanosecond, Some(Arc::from("+08:00")));
625 let other_aware_nanosecond =
626 DataType::Timestamp(TimeUnit::Nanosecond, Some(Arc::from("+07:00")));
627
628 assert!(preserves_timestamp_values(
630 &naive_nanosecond,
631 &naive_nanosecond
632 ));
633 assert!(preserves_timestamp_values(
634 &aware_nanosecond,
635 &other_aware_nanosecond
636 ));
637
638 assert!(!preserves_timestamp_values(
641 &naive_millisecond,
642 &naive_nanosecond
643 ));
644 assert!(!preserves_timestamp_values(
645 &aware_nanosecond,
646 &naive_nanosecond
647 ));
648 assert!(!preserves_timestamp_values(
649 &naive_nanosecond,
650 &DataType::Int64
651 ));
652 assert!(!preserves_timestamp_values(
653 &DataType::Int64,
654 &naive_nanosecond
655 ));
656
657 assert!(preserves_timestamp_values(
660 &naive_nanosecond,
661 &aware_nanosecond
662 ));
663 }
664
665 #[test]
666 fn test_is_time_index_expr_rejects_unsupported_wrappers() {
667 let scan = new_region_scan();
668 let config = Arc::new(ConfigOptions::default());
669 let return_field = Arc::new(Field::new(
670 "ts",
671 DataType::Timestamp(TimeUnit::Millisecond, None),
672 false,
673 ));
674 let projection = Arc::new(
675 ProjectionExec::try_new(
676 vec![(
677 Arc::new(ScalarFunctionExpr::new(
678 "date_trunc",
679 to_timestamp_millis(config.as_ref()),
680 vec![Arc::new(PhysicalColumn::new("ts", 1))],
681 return_field,
682 config,
683 )) as Arc<dyn PhysicalExpr>,
684 "ts".to_string(),
685 )],
686 scan,
687 )
688 .unwrap(),
689 ) as Arc<dyn ExecutionPlan>;
690 let expr = Arc::new(PhysicalColumn::new("ts", 0)) as Arc<dyn PhysicalExpr>;
691
692 assert!(!is_time_index_expr(&projection, &expr).unwrap());
693 }
694
695 #[test]
696 fn test_is_supported_time_index_wrapper_ignores_function_name_case() {
697 let config = Arc::new(ConfigOptions::default());
698 let return_field = Arc::new(Field::new(
699 "ts",
700 DataType::Timestamp(TimeUnit::Millisecond, None),
701 false,
702 ));
703 let expr = ScalarFunctionExpr::new(
704 "To_Timestamp_Millis",
705 to_timestamp_millis(config.as_ref()),
706 vec![Arc::new(PhysicalColumn::new("ts", 1))],
707 return_field,
708 config,
709 );
710
711 assert!(is_supported_time_index_wrapper(&expr));
712 }
713
714 #[test]
715 fn test_is_time_index_expr_rejects_non_timestamp_casts() {
716 let scan = new_region_scan();
717 let cast_expr = Arc::new(CastExpr::new(
718 Arc::new(PhysicalColumn::new("ts", 1)),
719 DataType::Timestamp(TimeUnit::Millisecond, None),
720 None,
721 )) as Arc<dyn PhysicalExpr>;
722 assert!(!is_time_index_expr(&scan, &cast_expr).unwrap());
724
725 let non_timestamp_cast = Arc::new(CastExpr::new(
726 Arc::new(PhysicalColumn::new("ts", 1)),
727 DataType::Int64,
728 None,
729 )) as Arc<dyn PhysicalExpr>;
730 assert!(!is_time_index_expr(&scan, &non_timestamp_cast).unwrap());
731 }
732
733 #[test]
734 fn test_is_time_index_expr_tracks_time_index_through_filter() {
735 let scan = new_region_scan();
736 let filter = Arc::new(
737 FilterExec::try_new(
738 Arc::new(Literal::new(ScalarValue::Boolean(Some(true)))),
739 scan,
740 )
741 .unwrap(),
742 ) as Arc<dyn ExecutionPlan>;
743 let expr = Arc::new(PhysicalColumn::new("ts", 1)) as Arc<dyn PhysicalExpr>;
744
745 assert!(is_time_index_expr(&filter, &expr).unwrap());
746 }
747
748 #[test]
749 fn test_is_time_index_expr_tracks_time_index_through_passthrough_wrapper_and_filter_projection()
750 {
751 let scan = new_region_scan();
752 let projected_filter = Arc::new(
753 FilterExecBuilder::new(
754 Arc::new(Literal::new(ScalarValue::Boolean(Some(true)))),
755 scan,
756 )
757 .apply_projection(Some(vec![1]))
758 .unwrap()
759 .build()
760 .unwrap(),
761 ) as Arc<dyn ExecutionPlan>;
762 let cooperative =
763 Arc::new(CooperativeExec::new(projected_filter)) as Arc<dyn ExecutionPlan>;
764 let expr = Arc::new(PhysicalColumn::new("ts", 0)) as Arc<dyn PhysicalExpr>;
765
766 assert!(is_time_index_expr(&cooperative, &expr).unwrap());
767 }
768
769 #[test]
770 fn test_schema_preserving_child_rejects_schema_changing_projection() {
771 let scan = new_region_scan();
772 let projection = ProjectionExec::try_new(
773 vec![(
774 Arc::new(PhysicalColumn::new("ts", 1)) as Arc<dyn PhysicalExpr>,
775 "ts".to_string(),
776 )],
777 scan,
778 )
779 .unwrap();
780
781 assert!(schema_preserving_child(&projection).is_none());
782 }
783
784 #[test]
785 fn test_cooperative_exec_satisfies_passthrough_schema_contract() {
786 let child = new_region_scan();
787 let plan = Arc::new(CooperativeExec::new(child.clone())) as Arc<dyn ExecutionPlan>;
788
789 assert_passthrough_schema_contract(plan, child);
790 }
791
792 #[test]
793 fn test_repartition_exec_satisfies_passthrough_schema_contract() {
794 let child = new_region_scan();
795 let plan = Arc::new(
796 RepartitionExec::try_new(
797 child.clone(),
798 datafusion_physical_expr::Partitioning::RoundRobinBatch(2),
799 )
800 .unwrap(),
801 ) as Arc<dyn ExecutionPlan>;
802
803 assert_passthrough_schema_contract(plan, child);
804 }
805
806 #[test]
807 fn test_coalesce_partitions_exec_satisfies_passthrough_schema_contract() {
808 let child = new_region_scan();
809 let plan = Arc::new(CoalescePartitionsExec::new(child.clone())) as Arc<dyn ExecutionPlan>;
810
811 assert_passthrough_schema_contract(plan, child);
812 }
813
814 fn assert_passthrough_schema_contract(
815 plan: Arc<dyn ExecutionPlan>,
816 child: Arc<dyn ExecutionPlan>,
817 ) {
818 assert_eq!(plan.schema().as_ref(), child.schema().as_ref());
819
820 let passthrough = passthrough_child(plan.as_ref()).expect("wrapper should preserve schema");
821 assert_eq!(passthrough.schema().as_ref(), child.schema().as_ref());
822 }
823
824 fn new_region_scan() -> Arc<dyn ExecutionPlan> {
825 new_region_scan_with_unit(TimeUnit::Nanosecond)
826 }
827
828 fn new_region_scan_with_unit(unit: TimeUnit) -> Arc<dyn ExecutionPlan> {
829 let timestamp_type = ConcreteDataType::timestamp_datatype(unit.into());
830 let schema = Arc::new(Schema::new(vec![
831 ColumnSchema::new("value", ConcreteDataType::int32_datatype(), false),
832 ColumnSchema::new("ts", timestamp_type.clone(), false),
833 ]));
834 let recordbatches = RecordBatches::try_new(schema.clone(), vec![]).unwrap();
835 let stream = recordbatches.as_stream();
836
837 let mut builder = RegionMetadataBuilder::new(RegionId::new(1, 1));
838 builder
839 .push_column_metadata(ColumnMetadata {
840 column_schema: ColumnSchema::new(
841 "value",
842 ConcreteDataType::int32_datatype(),
843 false,
844 ),
845 semantic_type: SemanticType::Field,
846 column_id: 1,
847 })
848 .push_column_metadata(ColumnMetadata {
849 column_schema: ColumnSchema::new("ts", timestamp_type, false),
850 semantic_type: SemanticType::Timestamp,
851 column_id: 2,
852 });
853
854 let scanner = Box::new(SinglePartitionScanner::new(
855 stream,
856 false,
857 Arc::new(builder.build().unwrap()),
858 None,
859 ));
860 Arc::new(RegionScanExec::new(scanner, ScanRequest::default(), None).unwrap())
861 }
862}