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 return if matches!(cast_expr.cast_type(), DataType::Timestamp(_, _)) {
226 is_time_index_expr(plan, cast_expr.expr())
227 } else {
228 Ok(false)
229 };
230 }
231
232 if let Some(scalar_function_expr) = expr.downcast_ref::<ScalarFunctionExpr>() {
233 return if is_supported_time_index_wrapper(scalar_function_expr)
234 && scalar_function_expr.args().len() == 1
235 {
236 is_time_index_expr(plan, &scalar_function_expr.args()[0])
237 } else {
238 Ok(false)
239 };
240 }
241
242 Ok(false)
243}
244
245fn is_time_index_column(
246 plan: &Arc<dyn ExecutionPlan>,
247 column_expr: &PhysicalColumn,
248) -> DataFusionResult<bool> {
249 if let Some(projection) = plan.downcast_ref::<ProjectionExec>() {
250 let Some(projection_expr) = projection.expr().get(column_expr.index()) else {
251 return Ok(false);
252 };
253 return is_time_index_expr(projection.input(), &projection_expr.expr);
254 }
255
256 if let Some(filter) = plan.downcast_ref::<FilterExec>() {
257 let child_column_expr = filter
258 .projection()
259 .as_ref()
260 .and_then(|projection| projection.get(column_expr.index()).copied())
261 .map(|input_index| {
262 PhysicalColumn::new(
263 filter.input().schema().field(input_index).name(),
264 input_index,
265 )
266 })
267 .unwrap_or_else(|| column_expr.clone());
268 let child_expr = Arc::new(child_column_expr) as Arc<dyn PhysicalExpr>;
269 return is_time_index_expr(filter.input(), &child_expr);
270 }
271
272 if let Some(region_scan_exec) = plan.downcast_ref::<RegionScanExec>() {
273 let schema = plan.schema();
274 let column_field = schema.field(column_expr.index());
275 return Ok(
276 matches!(column_field.data_type(), DataType::Timestamp(_, _))
277 && *column_field.name() == region_scan_exec.time_index(),
278 );
279 }
280
281 let Some(child) = passthrough_child(plan.as_ref()) else {
282 return Ok(false);
283 };
284 let child_expr = Arc::new(column_expr.clone()) as Arc<dyn PhysicalExpr>;
285 is_time_index_expr(&child, &child_expr)
286}
287
288fn passthrough_child(plan: &dyn ExecutionPlan) -> Option<Arc<dyn ExecutionPlan>> {
289 if plan.is::<CoalescePartitionsExec>()
290 || plan.is::<RepartitionExec>()
291 || plan.is::<CooperativeExec>()
292 {
293 return schema_preserving_child(plan);
294 }
295
296 None
297}
298
299fn schema_preserving_child(plan: &dyn ExecutionPlan) -> Option<Arc<dyn ExecutionPlan>> {
300 let child = plan.children().first().cloned().cloned()?;
301 (plan.schema().as_ref() == child.schema().as_ref()).then_some(child)
302}
303
304fn is_supported_time_index_wrapper(expr: &ScalarFunctionExpr) -> bool {
305 (expr.name().eq_ignore_ascii_case("to_timestamp")
306 || expr.name().eq_ignore_ascii_case("to_timestamp_seconds")
307 || expr.name().eq_ignore_ascii_case("to_timestamp_millis")
308 || expr.name().eq_ignore_ascii_case("to_timestamp_micros")
309 || expr.name().eq_ignore_ascii_case("to_timestamp_nanos"))
310 && matches!(expr.return_type(), DataType::Timestamp(_, _))
311}
312
313fn remove_repartition(
315 plan: Arc<dyn ExecutionPlan>,
316) -> DataFusionResult<Transformed<Arc<dyn ExecutionPlan>>> {
317 plan.transform_down(|plan| {
318 if plan.is::<FilterExec>() {
319 let maybe_repartition = plan.children()[0];
321 if maybe_repartition.is::<RepartitionExec>() {
322 let maybe_scan = maybe_repartition.children()[0];
323 if maybe_scan.is::<RegionScanExec>() {
324 let new_filter = plan.clone().replace_children(
325 vec![maybe_scan.clone()],
326 ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
327 )?;
328 return Ok(Transformed::yes(new_filter));
329 }
330 }
331 }
332
333 Ok(Transformed::no(plan))
334 })
335}
336
337#[cfg(test)]
338mod test {
339 use std::sync::Arc;
340
341 use api::v1::SemanticType;
342 use arrow_schema::{Field, TimeUnit};
343 use common_recordbatch::RecordBatches;
344 use datafusion::config::ConfigOptions;
345 use datafusion::physical_plan::filter::FilterExecBuilder;
346 use datafusion_common::ScalarValue;
347 use datafusion_functions::datetime::to_timestamp_millis;
348 use datafusion_physical_expr::expressions::{CastExpr, Literal};
349 use datatypes::data_type::ConcreteDataType;
350 use datatypes::schema::{ColumnSchema, Schema};
351 use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder};
352 use store_api::region_engine::SinglePartitionScanner;
353 use store_api::storage::{RegionId, ScanRequest};
354
355 use super::*;
356
357 #[test]
358 fn test_keep_ordinary_sort_when_scan_has_no_partition_ranges() {
359 let ordering = datafusion_physical_expr::LexOrdering::new([
360 datafusion_physical_expr::PhysicalSortExpr {
361 expr: Arc::new(PhysicalColumn::new("ts", 1)),
362 options: Default::default(),
363 },
364 ])
365 .unwrap();
366 let sort = Arc::new(SortExec::new(ordering, new_region_scan()));
367 let optimized =
368 WindowedSortPhysicalRule::do_optimize(sort, &ConfigOptions::default()).unwrap();
369 assert!(optimized.is::<SortExec>());
370 }
371
372 #[test]
373 fn test_is_time_index_expr_tracks_aliases_through_projection() {
374 let scan = new_region_scan();
375 let projection = Arc::new(
376 ProjectionExec::try_new(
377 vec![(
378 Arc::new(PhysicalColumn::new("ts", 1)) as Arc<dyn PhysicalExpr>,
379 "alias_ts".to_string(),
380 )],
381 scan,
382 )
383 .unwrap(),
384 ) as Arc<dyn ExecutionPlan>;
385 let expr = Arc::new(PhysicalColumn::new("alias_ts", 0)) as Arc<dyn PhysicalExpr>;
386
387 assert!(is_time_index_expr(&projection, &expr).unwrap());
388 }
389
390 #[test]
391 fn test_is_time_index_expr_tracks_multi_level_aliases() {
392 let scan = new_region_scan();
393 let first_projection = Arc::new(
394 ProjectionExec::try_new(
395 vec![(
396 Arc::new(PhysicalColumn::new("ts", 1)) as Arc<dyn PhysicalExpr>,
397 "alias_1".to_string(),
398 )],
399 scan,
400 )
401 .unwrap(),
402 ) as Arc<dyn ExecutionPlan>;
403 let second_projection = Arc::new(
404 ProjectionExec::try_new(
405 vec![(
406 Arc::new(PhysicalColumn::new("alias_1", 0)) as Arc<dyn PhysicalExpr>,
407 "alias_2".to_string(),
408 )],
409 first_projection,
410 )
411 .unwrap(),
412 ) as Arc<dyn ExecutionPlan>;
413 let expr = Arc::new(PhysicalColumn::new("alias_2", 0)) as Arc<dyn PhysicalExpr>;
414
415 assert!(is_time_index_expr(&second_projection, &expr).unwrap());
416 }
417
418 #[test]
419 fn test_is_time_index_expr_tracks_wrapped_aliases_through_projection() {
420 let scan = new_region_scan();
421 let config = Arc::new(ConfigOptions::default());
422 let return_field = Arc::new(Field::new(
423 "ts",
424 DataType::Timestamp(TimeUnit::Millisecond, None),
425 false,
426 ));
427 let projection = Arc::new(
428 ProjectionExec::try_new(
429 vec![(
430 Arc::new(ScalarFunctionExpr::new(
431 "to_timestamp_millis",
432 to_timestamp_millis(config.as_ref()),
433 vec![Arc::new(PhysicalColumn::new("ts", 1))],
434 return_field,
435 config,
436 )) as Arc<dyn PhysicalExpr>,
437 "ts".to_string(),
438 )],
439 scan,
440 )
441 .unwrap(),
442 ) as Arc<dyn ExecutionPlan>;
443 let expr = Arc::new(PhysicalColumn::new("ts", 0)) as Arc<dyn PhysicalExpr>;
444
445 assert!(is_time_index_expr(&projection, &expr).unwrap());
446 }
447
448 #[test]
449 fn test_is_time_index_expr_tracks_cast_aliases_through_projection() {
450 let scan = new_region_scan();
451 let projection = Arc::new(
452 ProjectionExec::try_new(
453 vec![(
454 Arc::new(CastExpr::new(
455 Arc::new(PhysicalColumn::new("ts", 1)),
456 DataType::Timestamp(TimeUnit::Millisecond, None),
457 None,
458 )) as Arc<dyn PhysicalExpr>,
459 "ts_ms".to_string(),
460 )],
461 scan,
462 )
463 .unwrap(),
464 ) as Arc<dyn ExecutionPlan>;
465 let expr = Arc::new(PhysicalColumn::new("ts_ms", 0)) as Arc<dyn PhysicalExpr>;
466
467 assert!(is_time_index_expr(&projection, &expr).unwrap());
468 }
469
470 #[test]
471 fn test_is_time_index_expr_rejects_unsupported_wrappers() {
472 let scan = new_region_scan();
473 let config = Arc::new(ConfigOptions::default());
474 let return_field = Arc::new(Field::new(
475 "ts",
476 DataType::Timestamp(TimeUnit::Millisecond, None),
477 false,
478 ));
479 let projection = Arc::new(
480 ProjectionExec::try_new(
481 vec![(
482 Arc::new(ScalarFunctionExpr::new(
483 "date_trunc",
484 to_timestamp_millis(config.as_ref()),
485 vec![Arc::new(PhysicalColumn::new("ts", 1))],
486 return_field,
487 config,
488 )) as Arc<dyn PhysicalExpr>,
489 "ts".to_string(),
490 )],
491 scan,
492 )
493 .unwrap(),
494 ) as Arc<dyn ExecutionPlan>;
495 let expr = Arc::new(PhysicalColumn::new("ts", 0)) as Arc<dyn PhysicalExpr>;
496
497 assert!(!is_time_index_expr(&projection, &expr).unwrap());
498 }
499
500 #[test]
501 fn test_is_supported_time_index_wrapper_ignores_function_name_case() {
502 let config = Arc::new(ConfigOptions::default());
503 let return_field = Arc::new(Field::new(
504 "ts",
505 DataType::Timestamp(TimeUnit::Millisecond, None),
506 false,
507 ));
508 let expr = ScalarFunctionExpr::new(
509 "To_Timestamp_Millis",
510 to_timestamp_millis(config.as_ref()),
511 vec![Arc::new(PhysicalColumn::new("ts", 1))],
512 return_field,
513 config,
514 );
515
516 assert!(is_supported_time_index_wrapper(&expr));
517 }
518
519 #[test]
520 fn test_is_time_index_expr_rejects_non_timestamp_casts() {
521 let scan = new_region_scan();
522 let cast_expr = Arc::new(CastExpr::new(
523 Arc::new(PhysicalColumn::new("ts", 1)),
524 DataType::Timestamp(TimeUnit::Millisecond, None),
525 None,
526 )) as Arc<dyn PhysicalExpr>;
527 assert!(is_time_index_expr(&scan, &cast_expr).unwrap());
528
529 let non_timestamp_cast = Arc::new(CastExpr::new(
530 Arc::new(PhysicalColumn::new("ts", 1)),
531 DataType::Int64,
532 None,
533 )) as Arc<dyn PhysicalExpr>;
534 assert!(!is_time_index_expr(&scan, &non_timestamp_cast).unwrap());
535 }
536
537 #[test]
538 fn test_is_time_index_expr_tracks_time_index_through_filter() {
539 let scan = new_region_scan();
540 let filter = Arc::new(
541 FilterExec::try_new(
542 Arc::new(Literal::new(ScalarValue::Boolean(Some(true)))),
543 scan,
544 )
545 .unwrap(),
546 ) as Arc<dyn ExecutionPlan>;
547 let expr = Arc::new(PhysicalColumn::new("ts", 1)) as Arc<dyn PhysicalExpr>;
548
549 assert!(is_time_index_expr(&filter, &expr).unwrap());
550 }
551
552 #[test]
553 fn test_is_time_index_expr_tracks_time_index_through_passthrough_wrapper_and_filter_projection()
554 {
555 let scan = new_region_scan();
556 let projected_filter = Arc::new(
557 FilterExecBuilder::new(
558 Arc::new(Literal::new(ScalarValue::Boolean(Some(true)))),
559 scan,
560 )
561 .apply_projection(Some(vec![1]))
562 .unwrap()
563 .build()
564 .unwrap(),
565 ) as Arc<dyn ExecutionPlan>;
566 let cooperative =
567 Arc::new(CooperativeExec::new(projected_filter)) as Arc<dyn ExecutionPlan>;
568 let expr = Arc::new(PhysicalColumn::new("ts", 0)) as Arc<dyn PhysicalExpr>;
569
570 assert!(is_time_index_expr(&cooperative, &expr).unwrap());
571 }
572
573 #[test]
574 fn test_schema_preserving_child_rejects_schema_changing_projection() {
575 let scan = new_region_scan();
576 let projection = ProjectionExec::try_new(
577 vec![(
578 Arc::new(PhysicalColumn::new("ts", 1)) as Arc<dyn PhysicalExpr>,
579 "ts".to_string(),
580 )],
581 scan,
582 )
583 .unwrap();
584
585 assert!(schema_preserving_child(&projection).is_none());
586 }
587
588 #[test]
589 fn test_cooperative_exec_satisfies_passthrough_schema_contract() {
590 let child = new_region_scan();
591 let plan = Arc::new(CooperativeExec::new(child.clone())) as Arc<dyn ExecutionPlan>;
592
593 assert_passthrough_schema_contract(plan, child);
594 }
595
596 #[test]
597 fn test_repartition_exec_satisfies_passthrough_schema_contract() {
598 let child = new_region_scan();
599 let plan = Arc::new(
600 RepartitionExec::try_new(
601 child.clone(),
602 datafusion_physical_expr::Partitioning::RoundRobinBatch(2),
603 )
604 .unwrap(),
605 ) as Arc<dyn ExecutionPlan>;
606
607 assert_passthrough_schema_contract(plan, child);
608 }
609
610 #[test]
611 fn test_coalesce_partitions_exec_satisfies_passthrough_schema_contract() {
612 let child = new_region_scan();
613 let plan = Arc::new(CoalescePartitionsExec::new(child.clone())) as Arc<dyn ExecutionPlan>;
614
615 assert_passthrough_schema_contract(plan, child);
616 }
617
618 fn assert_passthrough_schema_contract(
619 plan: Arc<dyn ExecutionPlan>,
620 child: Arc<dyn ExecutionPlan>,
621 ) {
622 assert_eq!(plan.schema().as_ref(), child.schema().as_ref());
623
624 let passthrough = passthrough_child(plan.as_ref()).expect("wrapper should preserve schema");
625 assert_eq!(passthrough.schema().as_ref(), child.schema().as_ref());
626 }
627
628 fn new_region_scan() -> Arc<dyn ExecutionPlan> {
629 let schema = Arc::new(Schema::new(vec![
630 ColumnSchema::new("value", ConcreteDataType::int32_datatype(), false),
631 ColumnSchema::new(
632 "ts",
633 ConcreteDataType::timestamp_nanosecond_datatype(),
634 false,
635 ),
636 ]));
637 let recordbatches = RecordBatches::try_new(schema.clone(), vec![]).unwrap();
638 let stream = recordbatches.as_stream();
639
640 let mut builder = RegionMetadataBuilder::new(RegionId::new(1, 1));
641 builder
642 .push_column_metadata(ColumnMetadata {
643 column_schema: ColumnSchema::new(
644 "value",
645 ConcreteDataType::int32_datatype(),
646 false,
647 ),
648 semantic_type: SemanticType::Field,
649 column_id: 1,
650 })
651 .push_column_metadata(ColumnMetadata {
652 column_schema: ColumnSchema::new(
653 "ts",
654 ConcreteDataType::timestamp_nanosecond_datatype(),
655 false,
656 ),
657 semantic_type: SemanticType::Timestamp,
658 column_id: 2,
659 });
660
661 let scanner = Box::new(SinglePartitionScanner::new(
662 stream,
663 false,
664 Arc::new(builder.build().unwrap()),
665 None,
666 ));
667 Arc::new(RegionScanExec::new(scanner, ScanRequest::default(), None).unwrap())
668 }
669}