1use std::collections::BTreeSet;
19use std::sync::Arc;
20
21use api::helper::pb_value_to_value_ref;
22use arrow::array::{
23 TimestampMicrosecondArray, TimestampMillisecondArray, TimestampNanosecondArray,
24 TimestampSecondArray,
25};
26use catalog::CatalogManagerRef;
27use common_error::ext::BoxedError;
28use common_recordbatch::DfRecordBatch;
29use common_telemetry::warn;
30use common_time::Timestamp;
31use common_time::timestamp::TimeUnit;
32use datafusion::error::Result as DfResult;
33use datafusion::execution::SessionState;
34use datafusion::logical_expr::Expr;
35use datafusion::physical_planner::{DefaultPhysicalPlanner, PhysicalPlanner};
36use datafusion_common::tree_node::{
37 Transformed, TreeNode, TreeNodeRecursion, TreeNodeRewriter, TreeNodeVisitor,
38};
39use datafusion_common::{DFSchema, TableReference};
40use datafusion_expr::physical_planning_context::PhysicalPlanningContext;
41use datafusion_expr::{ColumnarValue, LogicalPlan};
42use datafusion_physical_expr::PhysicalExprRef;
43use datatypes::prelude::{ConcreteDataType, DataType};
44use datatypes::schema::TIME_INDEX_KEY;
45use datatypes::value::Value;
46use datatypes::vectors::{
47 TimestampMicrosecondVector, TimestampMillisecondVector, TimestampNanosecondVector,
48 TimestampSecondVector, Vector,
49};
50use itertools::Itertools;
51use session::context::QueryContextRef;
52use snafu::{OptionExt, ResultExt, ensure};
53
54use crate::Error;
55use crate::adapter::util::from_proto_to_data_type;
56use crate::error::{
57 ArrowSnafu, DatafusionSnafu, DatatypesSnafu, ExternalSnafu, PlanSnafu, TimeSnafu,
58 UnexpectedSnafu,
59};
60
61const DEFAULT_TEST_TIMESTAMP: Timestamp = Timestamp::new_second(17_0000_0000);
63
64#[derive(Default)]
65struct TimeWindowPlanShape {
66 table_scan_count: usize,
67 aggregate_count: usize,
68 has_unsupported_pruning_node: bool,
69}
70
71impl TimeWindowPlanShape {
72 fn should_skip_time_window_expr(&self) -> bool {
73 self.has_unsupported_pruning_node || self.table_scan_count != 1 || self.aggregate_count > 1
74 }
75
76 fn should_stop_inspection(&self) -> bool {
77 self.has_unsupported_pruning_node || self.table_scan_count > 1 || self.aggregate_count > 1
82 }
83}
84
85impl TreeNodeVisitor<'_> for TimeWindowPlanShape {
86 type Node = LogicalPlan;
87
88 fn f_down(&mut self, node: &Self::Node) -> DfResult<TreeNodeRecursion> {
89 match node {
90 LogicalPlan::TableScan(_) => {
91 self.table_scan_count += 1;
92 }
93 LogicalPlan::Aggregate(_) => {
94 self.aggregate_count += 1;
95 }
96 LogicalPlan::Join(_)
101 | LogicalPlan::Window(_)
102 | LogicalPlan::Union(_)
103 | LogicalPlan::Distinct(_)
104 | LogicalPlan::Limit(_)
105 | LogicalPlan::Sort(_)
106 | LogicalPlan::Extension(_)
107 | LogicalPlan::Dml(_)
108 | LogicalPlan::Ddl(_)
109 | LogicalPlan::Unnest(_)
110 | LogicalPlan::RecursiveQuery(_) => {
111 self.has_unsupported_pruning_node = true;
112 }
113 _ => {}
114 }
115
116 if self.should_stop_inspection() {
121 Ok(TreeNodeRecursion::Stop)
122 } else {
123 Ok(TreeNodeRecursion::Continue)
124 }
125 }
126}
127
128fn inspect_time_window_plan_shape(plan: &LogicalPlan) -> DfResult<TimeWindowPlanShape> {
129 let mut shape = TimeWindowPlanShape::default();
130 plan.visit_with_subqueries(&mut shape)?;
131 Ok(shape)
132}
133
134fn should_skip_time_window_expr(plan: &LogicalPlan) -> bool {
135 let Ok(shape) = inspect_time_window_plan_shape(plan) else {
136 return true;
137 };
138
139 shape.should_skip_time_window_expr()
140}
141
142#[derive(Debug, Clone)]
152pub struct TimeWindowExpr {
153 phy_expr: PhysicalExprRef,
154 pub column_name: String,
155 logical_expr: Expr,
156 df_schema: DFSchema,
157 eval_time_window_size: Option<std::time::Duration>,
158 eval_time_original: Option<Timestamp>,
159}
160
161impl std::fmt::Display for TimeWindowExpr {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 f.debug_struct("TimeWindowExpr")
164 .field("phy_expr", &self.phy_expr.to_string())
165 .field("column_name", &self.column_name)
166 .field("logical_expr", &self.logical_expr.to_string())
167 .field("df_schema", &self.df_schema)
168 .finish()
169 }
170}
171
172impl TimeWindowExpr {
173 pub fn time_window_size(&self) -> &Option<std::time::Duration> {
175 &self.eval_time_window_size
176 }
177
178 pub fn from_expr(
179 expr: &Expr,
180 column_name: &str,
181 df_schema: &DFSchema,
182 session: &SessionState,
183 ) -> Result<Self, Error> {
184 let phy_expr: PhysicalExprRef = to_phy_expr(expr, df_schema, session)?;
185 let mut zelf = Self {
186 phy_expr,
187 column_name: column_name.to_string(),
188 logical_expr: expr.clone(),
189 df_schema: df_schema.clone(),
190 eval_time_window_size: None,
191 eval_time_original: None,
192 };
193 let test_ts = DEFAULT_TEST_TIMESTAMP;
194 let (lower, upper) = zelf.eval(test_ts)?;
195 let time_window_size = match (lower, upper) {
196 (Some(l), Some(u)) => u.sub(&l).map(|r| r.to_std()).transpose().map_err(|_| {
197 UnexpectedSnafu {
198 reason: format!(
199 "Expect upper bound older than lower bound, found upper={u:?} and lower={l:?}"
200 ),
201 }
202 .build()
203 })?,
204 _ => None,
205 };
206 zelf.eval_time_window_size = time_window_size;
207 zelf.eval_time_original = lower;
208
209 Ok(zelf)
210 }
211
212 pub fn eval(
214 &self,
215 current: Timestamp,
216 ) -> Result<(Option<Timestamp>, Option<Timestamp>), Error> {
217 fn compute_distance(time_diff_ns: i64, stride_ns: i64) -> i64 {
218 if stride_ns == 0 {
219 return time_diff_ns;
220 }
221 let time_delta = time_diff_ns - (time_diff_ns % stride_ns);
223
224 if time_diff_ns < 0 && time_delta != time_diff_ns {
225 time_delta - stride_ns
228 } else {
229 time_delta
230 }
231 }
232
233 if let (Some(original), Some(window_size)) =
236 (self.eval_time_original, self.eval_time_window_size)
237 {
238 let time_diff_ns = current.sub(&original).and_then(|s|s.num_nanoseconds()).with_context(||UnexpectedSnafu {
240 reason: format!(
241 "Failed to compute time difference between current {current:?} and original {original:?}"
242 ),
243 })?;
244
245 let window_size_ns = window_size.as_nanos() as i64;
246
247 let distance_ns = compute_distance(time_diff_ns, window_size_ns);
248
249 let lower_bound = if distance_ns >= 0 {
250 original.add_duration(std::time::Duration::from_nanos(distance_ns as u64))
251 } else {
252 original.sub_duration(std::time::Duration::from_nanos((-distance_ns) as u64))
253 }
254 .context(TimeSnafu)?;
255 let upper_bound = lower_bound.add_duration(window_size).context(TimeSnafu)?;
256
257 return Ok((Some(lower_bound), Some(upper_bound)));
258 }
259
260 let lower_bound =
261 calc_expr_time_window_lower_bound(&self.phy_expr, &self.df_schema, current)?;
262 let upper_bound =
263 probe_expr_time_window_upper_bound(&self.phy_expr, &self.df_schema, current)?;
264 Ok((lower_bound, upper_bound))
265 }
266
267 pub async fn handle_rows(
271 &self,
272 rows_list: Vec<api::v1::Rows>,
273 ) -> Result<BTreeSet<Timestamp>, Error> {
274 let mut time_windows = BTreeSet::new();
275
276 for rows in rows_list {
277 let ts_col_index = rows
280 .schema
281 .iter()
282 .map(|col| col.column_name.clone())
283 .position(|name| name == self.column_name);
284 let Some(ts_col_index) = ts_col_index else {
285 warn!("can't found time index column in schema: {:?}", rows.schema);
286 continue;
287 };
288 let col_schema = &rows.schema[ts_col_index];
289 let cdt = from_proto_to_data_type(col_schema)?;
290
291 let mut vector = cdt.create_mutable_vector(rows.rows.len());
292 for row in rows.rows {
293 let value = pb_value_to_value_ref(&row.values[ts_col_index], None);
294 vector
295 .try_push_value_ref(&value)
296 .map_err(BoxedError::new)
297 .context(ExternalSnafu)?;
298 }
299 let vector = vector.to_vector();
300
301 let df_schema = create_df_schema_for_ts_column(&self.column_name, cdt)?;
302
303 let rb =
304 DfRecordBatch::try_new(df_schema.inner().clone(), vec![vector.to_arrow_array()])
305 .with_context(|_e| ArrowSnafu {
306 context: format!(
307 "Failed to create record batch from {df_schema:?} and {vector:?}"
308 ),
309 })?;
310
311 let eval_res = self
312 .phy_expr
313 .evaluate(&rb)
314 .with_context(|_| DatafusionSnafu {
315 context: format!(
316 "Failed to evaluate physical expression {:?} on {rb:?}",
317 self.phy_expr
318 ),
319 })?;
320
321 let res = columnar_to_ts_vector(&eval_res)?;
322
323 for ts in res.into_iter().flatten() {
324 time_windows.insert(ts);
325 }
326 }
327
328 Ok(time_windows)
329 }
330}
331
332fn create_df_schema_for_ts_column(name: &str, cdt: ConcreteDataType) -> Result<DFSchema, Error> {
333 let arrow_schema = Arc::new(arrow_schema::Schema::new(vec![arrow_schema::Field::new(
334 name,
335 cdt.as_arrow_type(),
336 false,
337 )]));
338
339 let df_schema = DFSchema::from_field_specific_qualified_schema(
340 vec![Some(TableReference::bare("TimeIndexOnlyTable"))],
341 &arrow_schema,
342 )
343 .with_context(|_e| DatafusionSnafu {
344 context: format!("Failed to create DFSchema from arrow schema {arrow_schema:?}"),
345 })?;
346
347 Ok(df_schema)
348}
349
350fn columnar_to_ts_vector(columnar: &ColumnarValue) -> Result<Vec<Option<Timestamp>>, Error> {
352 let val = match columnar {
353 datafusion_expr::ColumnarValue::Array(array) => {
354 let ty = array.data_type();
355 let ty = ConcreteDataType::from_arrow_type(ty);
356 let time_unit = if let ConcreteDataType::Timestamp(ty) = ty {
357 ty.unit()
358 } else {
359 return UnexpectedSnafu {
360 reason: format!("Non-timestamp type: {ty:?}"),
361 }
362 .fail();
363 };
364
365 match time_unit {
366 TimeUnit::Second => array
367 .as_ref()
368 .as_any()
369 .downcast_ref::<TimestampSecondArray>()
370 .with_context(|| PlanSnafu {
371 reason: format!("Failed to create vector from arrow array {array:?}"),
372 })?
373 .values()
374 .iter()
375 .map(|d| Some(Timestamp::new(*d, time_unit)))
376 .collect_vec(),
377 TimeUnit::Millisecond => array
378 .as_ref()
379 .as_any()
380 .downcast_ref::<TimestampMillisecondArray>()
381 .with_context(|| PlanSnafu {
382 reason: format!("Failed to create vector from arrow array {array:?}"),
383 })?
384 .values()
385 .iter()
386 .map(|d| Some(Timestamp::new(*d, time_unit)))
387 .collect_vec(),
388 TimeUnit::Microsecond => array
389 .as_ref()
390 .as_any()
391 .downcast_ref::<TimestampMicrosecondArray>()
392 .with_context(|| PlanSnafu {
393 reason: format!("Failed to create vector from arrow array {array:?}"),
394 })?
395 .values()
396 .iter()
397 .map(|d| Some(Timestamp::new(*d, time_unit)))
398 .collect_vec(),
399 TimeUnit::Nanosecond => array
400 .as_ref()
401 .as_any()
402 .downcast_ref::<TimestampNanosecondArray>()
403 .with_context(|| PlanSnafu {
404 reason: format!("Failed to create vector from arrow array {array:?}"),
405 })?
406 .values()
407 .iter()
408 .map(|d| Some(Timestamp::new(*d, time_unit)))
409 .collect_vec(),
410 }
411 }
412 datafusion_expr::ColumnarValue::Scalar(scalar) => {
413 let value = Value::try_from(scalar.clone()).with_context(|_| DatatypesSnafu {
414 extra: format!("Failed to convert scalar {scalar:?} to value"),
415 })?;
416 let ts = value.as_timestamp().context(UnexpectedSnafu {
417 reason: format!("Expect Timestamp, found {:?}", value),
418 })?;
419 vec![Some(ts)]
420 }
421 };
422 Ok(val)
423}
424
425pub async fn find_time_window_expr(
432 plan: &LogicalPlan,
433 catalog_man: CatalogManagerRef,
434 query_ctx: QueryContextRef,
435) -> Result<(String, Option<datafusion_expr::Expr>, TimeUnit, DFSchema), Error> {
436 if should_skip_time_window_expr(plan) {
443 return Ok((
446 String::new(),
447 None,
448 TimeUnit::Millisecond,
449 DFSchema::empty(),
450 ));
451 }
452
453 let mut table_name = None;
454
455 plan.apply(|plan| {
457 let LogicalPlan::TableScan(table_scan) = plan else {
458 return Ok(TreeNodeRecursion::Continue);
459 };
460 table_name = Some(table_scan.table_name.clone());
461 Ok(TreeNodeRecursion::Stop)
462 })
463 .with_context(|_| DatafusionSnafu {
464 context: format!("Can't find table source in plan {plan:?}"),
465 })?;
466 let Some(table_name) = table_name else {
467 UnexpectedSnafu {
468 reason: format!("Can't find table source in plan {plan:?}"),
469 }
470 .fail()?
471 };
472
473 let current_schema = query_ctx.current_schema();
474
475 let catalog_name = table_name.catalog().unwrap_or(query_ctx.current_catalog());
476 let schema_name = table_name.schema().unwrap_or(¤t_schema);
477 let table_name = table_name.table();
478
479 let Some(table_ref) = catalog_man
480 .table(catalog_name, schema_name, table_name, Some(&query_ctx))
481 .await
482 .map_err(BoxedError::new)
483 .context(ExternalSnafu)?
484 else {
485 UnexpectedSnafu {
486 reason: format!(
487 "Can't find table {table_name:?} in catalog {catalog_name:?}/{schema_name:?}"
488 ),
489 }
490 .fail()?
491 };
492
493 let schema = &table_ref.table_info().meta.schema;
494
495 let ts_index = schema.timestamp_column().with_context(|| UnexpectedSnafu {
496 reason: format!("Can't find timestamp column in table {table_name:?}"),
497 })?;
498
499 let ts_col_name = ts_index.name.clone();
500
501 let expected_time_unit = ts_index.data_type.as_timestamp().with_context(|| UnexpectedSnafu {
502 reason: format!(
503 "Expected timestamp column {ts_col_name:?} in table {table_name:?} to be timestamp, but got {ts_index:?}"
504 ),
505 })?.unit();
506
507 let arrow_schema = Arc::new(arrow_schema::Schema::new(vec![arrow_schema::Field::new(
508 ts_col_name.clone(),
509 ts_index.data_type.as_arrow_type(),
510 false,
511 )]));
512
513 let df_schema = DFSchema::from_field_specific_qualified_schema(
514 vec![Some(TableReference::bare(table_name))],
515 &arrow_schema,
516 )
517 .with_context(|_e| DatafusionSnafu {
518 context: format!("Failed to create DFSchema from arrow schema {arrow_schema:?}"),
519 })?;
520
521 let mut aggr_expr = None;
523 let mut time_window_expr: Option<Expr> = None;
524
525 let find_inner_aggr_expr = |plan: &LogicalPlan| {
526 if let LogicalPlan::Aggregate(aggregate) = plan {
527 aggr_expr = Some(aggregate.clone());
528 };
529
530 Ok(TreeNodeRecursion::Continue)
531 };
532 plan.apply(find_inner_aggr_expr)
533 .with_context(|_| DatafusionSnafu {
534 context: format!("Can't find aggr expr in plan {plan:?}"),
535 })?;
536
537 if let Some(aggregate) = aggr_expr {
538 for group_expr in &aggregate.group_expr {
539 let refs = group_expr.column_refs();
540 if refs.len() != 1 {
541 continue;
542 }
543 let ref_col = refs.iter().next().unwrap();
544
545 let index = aggregate.input.schema().maybe_index_of_column(ref_col);
546 let Some(index) = index else {
547 continue;
548 };
549 let field = aggregate.input.schema().field(index);
550
551 let is_time_index =
553 field.metadata().get(TIME_INDEX_KEY).map(|s| s.as_str()) == Some("true");
554
555 if is_time_index {
556 let rewrite_column = group_expr.clone();
557 let rewritten = rewrite_column
558 .rewrite(&mut RewriteColumn {
559 table_name: table_name.to_string(),
560 })
561 .with_context(|_| DatafusionSnafu {
562 context: format!("Rewrite expr failed, expr={:?}", group_expr),
563 })?
564 .data;
565 struct RewriteColumn {
566 table_name: String,
567 }
568
569 impl TreeNodeRewriter for RewriteColumn {
570 type Node = Expr;
571 fn f_down(&mut self, node: Self::Node) -> DfResult<Transformed<Self::Node>> {
572 let Expr::Column(mut column) = node else {
573 return Ok(Transformed::no(node));
574 };
575
576 column.relation = Some(TableReference::bare(self.table_name.clone()));
577
578 Ok(Transformed::yes(Expr::Column(column)))
579 }
580 }
581
582 time_window_expr = Some(rewritten);
583 break;
584 }
585 }
586 Ok((ts_col_name, time_window_expr, expected_time_unit, df_schema))
587 } else {
588 Ok((ts_col_name, None, expected_time_unit, df_schema))
590 }
591}
592
593#[cfg(test)]
605pub async fn find_plan_time_window_bound(
606 plan: &LogicalPlan,
607 current: Timestamp,
608 query_ctx: QueryContextRef,
609 engine: query::QueryEngineRef,
610) -> Result<(String, Option<Timestamp>, Option<Timestamp>), Error> {
611 let catalog_man = engine.engine_state().catalog_manager();
613
614 let (ts_col_name, time_window_expr, expected_time_unit, df_schema) =
615 find_time_window_expr(plan, catalog_man.clone(), query_ctx).await?;
616 let new_current = current
618 .convert_to(expected_time_unit)
619 .with_context(|| UnexpectedSnafu {
620 reason: format!("Failed to cast current timestamp {current:?} to {expected_time_unit}"),
621 })?;
622
623 if let Some(time_window_expr) = time_window_expr {
625 let phy_expr = to_phy_expr(
626 &time_window_expr,
627 &df_schema,
628 &engine.engine_state().session_state(),
629 )?;
630 let lower_bound = calc_expr_time_window_lower_bound(&phy_expr, &df_schema, new_current)?;
631 let upper_bound = probe_expr_time_window_upper_bound(&phy_expr, &df_schema, new_current)?;
632 Ok((ts_col_name, lower_bound, upper_bound))
633 } else {
634 Ok((ts_col_name, None, None))
635 }
636}
637
638fn calc_expr_time_window_lower_bound(
647 phy_expr: &PhysicalExprRef,
648 df_schema: &DFSchema,
649 current: Timestamp,
650) -> Result<Option<Timestamp>, Error> {
651 let cur_time_window = eval_phy_time_window_expr(phy_expr, df_schema, current)?;
652 let input_time_unit = cur_time_window.unit();
653 Ok(cur_time_window.convert_to(input_time_unit))
654}
655
656fn probe_expr_time_window_upper_bound(
658 phy_expr: &PhysicalExprRef,
659 df_schema: &DFSchema,
660 current: Timestamp,
661) -> Result<Option<Timestamp>, Error> {
662 use std::cmp::Ordering;
664
665 let cur_time_window = eval_phy_time_window_expr(phy_expr, df_schema, current)?;
666
667 let mut offset: i64 = 1;
669 let mut lower_bound = Some(current);
670 let upper_bound;
671 loop {
673 let Some(next_val) = current.value().checked_add(offset) else {
674 return Ok(None);
676 };
677
678 let next_time_probe = common_time::Timestamp::new(next_val, current.unit());
679
680 let next_time_window = eval_phy_time_window_expr(phy_expr, df_schema, next_time_probe)?;
681
682 match next_time_window.cmp(&cur_time_window) {
683 Ordering::Less => UnexpectedSnafu {
684 reason: format!(
685 "Unsupported time window expression, expect monotonic increasing for time window expression {phy_expr:?}"
686 ),
687 }
688 .fail()?,
689 Ordering::Equal => {
690 lower_bound = Some(next_time_probe);
691 }
692 Ordering::Greater => {
693 upper_bound = Some(next_time_probe);
694 break
695 }
696 }
697
698 let Some(new_offset) = offset.checked_mul(2) else {
699 return Ok(None);
701 };
702 offset = new_offset;
703 }
704
705 binary_search_expr(
708 lower_bound,
709 upper_bound,
710 cur_time_window,
711 phy_expr,
712 df_schema,
713 )
714 .map(Some)
715}
716
717fn binary_search_expr(
718 lower_bound: Option<Timestamp>,
719 upper_bound: Option<Timestamp>,
720 cur_time_window: Timestamp,
721 phy_expr: &PhysicalExprRef,
722 df_schema: &DFSchema,
723) -> Result<Timestamp, Error> {
724 ensure!(
725 lower_bound.map(|v| v.unit()) == upper_bound.map(|v| v.unit()),
726 UnexpectedSnafu {
727 reason: format!(
728 " unit mismatch for time window expression {phy_expr:?}, found {lower_bound:?} and {upper_bound:?}"
729 ),
730 }
731 );
732
733 let output_unit = upper_bound
734 .context(UnexpectedSnafu {
735 reason: "should have lower bound",
736 })?
737 .unit();
738
739 let mut low = lower_bound
740 .context(UnexpectedSnafu {
741 reason: "should have lower bound",
742 })?
743 .value();
744 let mut high = upper_bound
745 .context(UnexpectedSnafu {
746 reason: "should have upper bound",
747 })?
748 .value();
749 while low < high {
750 let mid = (low + high) / 2;
751 let mid_probe = common_time::Timestamp::new(mid, output_unit);
752 let mid_time_window = eval_phy_time_window_expr(phy_expr, df_schema, mid_probe)?;
753
754 match mid_time_window.cmp(&cur_time_window) {
755 std::cmp::Ordering::Less => UnexpectedSnafu {
756 reason: format!("Binary search failed for time window expression {phy_expr:?}"),
757 }
758 .fail()?,
759 std::cmp::Ordering::Equal => low = mid + 1,
760 std::cmp::Ordering::Greater => high = mid,
761 }
762 }
763
764 let final_upper_bound_for_time_window = common_time::Timestamp::new(high, output_unit);
765 Ok(final_upper_bound_for_time_window)
766}
767
768fn eval_phy_time_window_expr(
770 phy: &PhysicalExprRef,
771 df_schema: &DFSchema,
772 input_value: Timestamp,
773) -> Result<Timestamp, Error> {
774 let schema_ty = df_schema.field(0).data_type();
775 let schema_cdt = ConcreteDataType::from_arrow_type(schema_ty);
776 let schema_unit = if let ConcreteDataType::Timestamp(ts) = schema_cdt {
777 ts.unit()
778 } else {
779 return UnexpectedSnafu {
780 reason: format!("Expect Timestamp, found {:?}", schema_cdt),
781 }
782 .fail();
783 };
784 let input_value = input_value
785 .convert_to(schema_unit)
786 .with_context(|| UnexpectedSnafu {
787 reason: format!("Failed to convert timestamp {input_value:?} to {schema_unit}"),
788 })?;
789 let ts_vector = match schema_unit {
790 TimeUnit::Second => {
791 TimestampSecondVector::from_vec(vec![input_value.value()]).to_arrow_array()
792 }
793 TimeUnit::Millisecond => {
794 TimestampMillisecondVector::from_vec(vec![input_value.value()]).to_arrow_array()
795 }
796 TimeUnit::Microsecond => {
797 TimestampMicrosecondVector::from_vec(vec![input_value.value()]).to_arrow_array()
798 }
799 TimeUnit::Nanosecond => {
800 TimestampNanosecondVector::from_vec(vec![input_value.value()]).to_arrow_array()
801 }
802 };
803
804 let rb = DfRecordBatch::try_new(df_schema.inner().clone(), vec![ts_vector.clone()])
805 .with_context(|_| ArrowSnafu {
806 context: format!("Failed to create record batch from {df_schema:?} and {ts_vector:?}"),
807 })?;
808
809 let eval_res = phy.evaluate(&rb).with_context(|_| DatafusionSnafu {
810 context: format!("Failed to evaluate physical expression {phy:?} on {rb:?}"),
811 })?;
812
813 if let Some(Some(ts)) = columnar_to_ts_vector(&eval_res)?.first() {
814 Ok(*ts)
815 } else {
816 UnexpectedSnafu {
817 reason: format!(
818 "Expected timestamp in expression {phy:?} but got {:?}",
819 eval_res
820 ),
821 }
822 .fail()?
823 }
824}
825
826fn to_phy_expr(
827 expr: &Expr,
828 df_schema: &DFSchema,
829 session: &SessionState,
830) -> Result<PhysicalExprRef, Error> {
831 let phy_planner = DefaultPhysicalPlanner::default();
832
833 let phy_expr: PhysicalExprRef = phy_planner
834 .create_physical_expr(
838 expr,
839 df_schema,
840 session,
841 &PhysicalPlanningContext::default(),
842 )
843 .with_context(|_e| DatafusionSnafu {
844 context: format!(
845 "Failed to create physical expression from {expr:?} using {df_schema:?}"
846 ),
847 })?;
848 Ok(phy_expr)
849}
850
851#[cfg(test)]
852mod test {
853 use datafusion_common::tree_node::TreeNode;
854 use pretty_assertions::assert_eq;
855 use session::context::QueryContext;
856
857 use super::*;
858 use crate::batching_mode::utils::{AddFilterRewriter, df_plan_to_sql, sql_to_df_plan};
859 use crate::test_utils::create_test_query_engine;
860
861 #[tokio::test]
862 async fn test_plan_time_window_lower_bound() {
863 use datafusion_expr::{col, lit};
864 let query_engine = create_test_query_engine();
865 let ctx = QueryContext::arc();
866
867 let testcases = [
868 (
870 "SELECT arrow_cast(date_bin(INTERVAL '1 MINS', numbers_with_ts.ts), 'Timestamp(Second, None)') AS ts FROM numbers_with_ts GROUP BY ts;",
871 Timestamp::new(1740394109, TimeUnit::Second),
872 (
873 "ts".to_string(),
874 Some(Timestamp::new(1740394109000, TimeUnit::Millisecond)),
875 Some(Timestamp::new(1740394109001, TimeUnit::Millisecond)),
876 ),
877 r#"SELECT arrow_cast(date_bin(INTERVAL '1 MINS', numbers_with_ts.ts), 'Timestamp(Second, None)') AS ts FROM numbers_with_ts WHERE ((ts >= CAST('2025-02-24 10:48:29' AS TIMESTAMP)) AND (ts <= CAST('2025-02-24 10:48:29.001' AS TIMESTAMP))) GROUP BY numbers_with_ts.ts"#,
878 ),
879 (
881 "SELECT arrow_cast(date_bin(INTERVAL '1 MINS', numbers_with_ts.ts), 'Timestamp(Second, None)') AS time_window FROM numbers_with_ts GROUP BY time_window;",
882 Timestamp::new(1740394109, TimeUnit::Second),
883 (
884 "ts".to_string(),
885 Some(Timestamp::new(1740394080, TimeUnit::Second)),
886 Some(Timestamp::new(1740394140, TimeUnit::Second)),
887 ),
888 "SELECT arrow_cast(date_bin(INTERVAL '1 MINS', numbers_with_ts.ts), 'Timestamp(Second, None)') AS time_window FROM numbers_with_ts WHERE ((ts >= CAST('2025-02-24 10:48:00' AS TIMESTAMP)) AND (ts <= CAST('2025-02-24 10:49:00' AS TIMESTAMP))) GROUP BY arrow_cast(date_bin(INTERVAL '1 MINS', numbers_with_ts.ts), 'Timestamp(Second, None)')",
889 ),
890 (
892 "SELECT arrow_cast(date_bin(INTERVAL '1 MINS', numbers_with_ts.ts), 'Timestamp(Second, None)') AS time_window FROM numbers_with_ts WHERE number in (2, 3, 4) GROUP BY time_window;",
893 Timestamp::new(1740394109, TimeUnit::Second),
894 (
895 "ts".to_string(),
896 Some(Timestamp::new(1740394080, TimeUnit::Second)),
897 Some(Timestamp::new(1740394140, TimeUnit::Second)),
898 ),
899 "SELECT arrow_cast(date_bin(INTERVAL '1 MINS', numbers_with_ts.ts), 'Timestamp(Second, None)') AS time_window FROM numbers_with_ts WHERE numbers_with_ts.number IN (2, 3, 4) AND ((ts >= CAST('2025-02-24 10:48:00' AS TIMESTAMP)) AND (ts <= CAST('2025-02-24 10:49:00' AS TIMESTAMP))) GROUP BY arrow_cast(date_bin(INTERVAL '1 MINS', numbers_with_ts.ts), 'Timestamp(Second, None)')",
900 ),
901 (
903 "SELECT arrow_cast(date_bin(INTERVAL '1 MINS', numbers_with_ts.ts), 'Timestamp(Second, None)') AS time_window FROM numbers_with_ts WHERE number BETWEEN 2 AND 4 GROUP BY time_window;",
904 Timestamp::new(1740394109, TimeUnit::Second),
905 (
906 "ts".to_string(),
907 Some(Timestamp::new(1740394080, TimeUnit::Second)),
908 Some(Timestamp::new(1740394140, TimeUnit::Second)),
909 ),
910 "SELECT arrow_cast(date_bin(INTERVAL '1 MINS', numbers_with_ts.ts), 'Timestamp(Second, None)') AS time_window FROM numbers_with_ts WHERE (numbers_with_ts.number BETWEEN 2 AND 4) AND ((ts >= CAST('2025-02-24 10:48:00' AS TIMESTAMP)) AND (ts <= CAST('2025-02-24 10:49:00' AS TIMESTAMP))) GROUP BY arrow_cast(date_bin(INTERVAL '1 MINS', numbers_with_ts.ts), 'Timestamp(Second, None)')",
911 ),
912 (
914 "SELECT date_bin('5 minutes', ts) FROM numbers_with_ts;",
915 Timestamp::new(23, TimeUnit::Millisecond),
916 ("ts".to_string(), None, None),
917 "SELECT date_bin('5 minutes', ts) FROM numbers_with_ts;",
918 ),
919 (
921 "SELECT date_bin('5 minutes', ts) as time_window FROM numbers_with_ts GROUP BY time_window;",
922 Timestamp::new(23, TimeUnit::Nanosecond),
923 (
924 "ts".to_string(),
925 Some(Timestamp::new(0, TimeUnit::Millisecond)),
926 Some(Timestamp::new(300000, TimeUnit::Millisecond)),
927 ),
928 "SELECT date_bin('5 minutes', numbers_with_ts.ts) AS time_window FROM numbers_with_ts WHERE ((ts >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (ts <= CAST('1970-01-01 00:05:00' AS TIMESTAMP))) GROUP BY date_bin('5 minutes', numbers_with_ts.ts)",
929 ),
930 (
932 "SELECT date_bin('5 minutes', ts) as time_window FROM numbers_with_ts GROUP BY time_window;",
933 Timestamp::new(0, TimeUnit::Nanosecond),
934 (
935 "ts".to_string(),
936 Some(Timestamp::new(0, TimeUnit::Millisecond)),
937 Some(Timestamp::new(300000, TimeUnit::Millisecond)),
938 ),
939 "SELECT date_bin('5 minutes', numbers_with_ts.ts) AS time_window FROM numbers_with_ts WHERE ((ts >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (ts <= CAST('1970-01-01 00:05:00' AS TIMESTAMP))) GROUP BY date_bin('5 minutes', numbers_with_ts.ts)",
940 ),
941 (
943 "SELECT date_bin('5 minutes', ts) as time_window FROM numbers_with_ts GROUP BY time_window;",
944 Timestamp::new(23_000_000, TimeUnit::Nanosecond),
945 (
946 "ts".to_string(),
947 Some(Timestamp::new(0, TimeUnit::Millisecond)),
948 Some(Timestamp::new(300000, TimeUnit::Millisecond)),
949 ),
950 "SELECT date_bin('5 minutes', numbers_with_ts.ts) AS time_window FROM numbers_with_ts WHERE ((ts >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (ts <= CAST('1970-01-01 00:05:00' AS TIMESTAMP))) GROUP BY date_bin('5 minutes', numbers_with_ts.ts)",
951 ),
952 (
954 "SELECT sum(number) as sum_up, date_bin('5 minutes', ts) as time_window FROM numbers_with_ts GROUP BY time_window;",
955 Timestamp::new(23, TimeUnit::Millisecond),
956 (
957 "ts".to_string(),
958 Some(Timestamp::new(0, TimeUnit::Millisecond)),
959 Some(Timestamp::new(300000, TimeUnit::Millisecond)),
960 ),
961 "SELECT sum(numbers_with_ts.number) AS sum_up, date_bin('5 minutes', numbers_with_ts.ts) AS time_window FROM numbers_with_ts WHERE ((ts >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (ts <= CAST('1970-01-01 00:05:00' AS TIMESTAMP))) GROUP BY date_bin('5 minutes', numbers_with_ts.ts)",
962 ),
963 (
965 "SELECT number, date_bin('5 minutes', ts) as time_window FROM numbers_with_ts GROUP BY time_window, number;",
966 Timestamp::new(23, TimeUnit::Millisecond),
967 (
968 "ts".to_string(),
969 Some(Timestamp::new(0, TimeUnit::Millisecond)),
970 Some(Timestamp::new(300000, TimeUnit::Millisecond)),
971 ),
972 "SELECT numbers_with_ts.number, date_bin('5 minutes', numbers_with_ts.ts) AS time_window FROM numbers_with_ts WHERE ((ts >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (ts <= CAST('1970-01-01 00:05:00' AS TIMESTAMP))) GROUP BY date_bin('5 minutes', numbers_with_ts.ts), numbers_with_ts.number",
973 ),
974 (
976 "SELECT number, time_window FROM (SELECT number, date_bin('5 minutes', ts) as time_window FROM numbers_with_ts GROUP BY time_window, number);",
977 Timestamp::new(23, TimeUnit::Millisecond),
978 (
979 "ts".to_string(),
980 Some(Timestamp::new(0, TimeUnit::Millisecond)),
981 Some(Timestamp::new(300000, TimeUnit::Millisecond)),
982 ),
983 "SELECT numbers_with_ts.number, time_window FROM (SELECT numbers_with_ts.number, date_bin('5 minutes', numbers_with_ts.ts) AS time_window FROM numbers_with_ts WHERE ((ts >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (ts <= CAST('1970-01-01 00:05:00' AS TIMESTAMP))) GROUP BY date_bin('5 minutes', numbers_with_ts.ts), numbers_with_ts.number)",
984 ),
985 (
987 "with cte as (select number, date_bin('5 minutes', ts) as time_window from numbers_with_ts GROUP BY time_window, number) select number, time_window from cte;",
988 Timestamp::new(23, TimeUnit::Millisecond),
989 (
990 "ts".to_string(),
991 Some(Timestamp::new(0, TimeUnit::Millisecond)),
992 Some(Timestamp::new(300000, TimeUnit::Millisecond)),
993 ),
994 "SELECT cte.number, cte.time_window FROM (SELECT numbers_with_ts.number, date_bin('5 minutes', numbers_with_ts.ts) AS time_window FROM numbers_with_ts WHERE ((ts >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (ts <= CAST('1970-01-01 00:05:00' AS TIMESTAMP))) GROUP BY date_bin('5 minutes', numbers_with_ts.ts), numbers_with_ts.number) AS cte",
995 ),
996 (
998 "SELECT sum(number), number, date_bin('5 minutes', ts) as time_window, bucket_name FROM (SELECT number, ts, case when number < 5 THEN 'bucket_0_5' when number >= 5 THEN 'bucket_5_inf' END as bucket_name FROM numbers_with_ts) GROUP BY number, time_window, bucket_name;",
999 Timestamp::new(23, TimeUnit::Millisecond),
1000 (
1001 "ts".to_string(),
1002 Some(Timestamp::new(0, TimeUnit::Millisecond)),
1003 Some(Timestamp::new(300000, TimeUnit::Millisecond)),
1004 ),
1005 "SELECT sum(number), number, date_bin('5 minutes', ts) AS time_window, bucket_name FROM (SELECT numbers_with_ts.number, numbers_with_ts.ts, CASE WHEN (numbers_with_ts.number < 5) THEN 'bucket_0_5' WHEN (numbers_with_ts.number >= 5) THEN 'bucket_5_inf' END AS bucket_name FROM numbers_with_ts WHERE ((ts >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (ts <= CAST('1970-01-01 00:05:00' AS TIMESTAMP)))) GROUP BY number, date_bin('5 minutes', ts), bucket_name",
1006 ),
1007 (
1009 "SELECT sum(number), number, date_bin('5 minutes', ts) as time_window, bucket_name FROM (SELECT number, ts, case when number < 5 THEN 'bucket_0_5' when number >= 5 THEN 'bucket_5_inf' END as bucket_name FROM numbers_with_ts) as cte GROUP BY number, time_window, bucket_name;",
1010 Timestamp::new(23, TimeUnit::Millisecond),
1011 (
1012 "ts".to_string(),
1013 Some(Timestamp::new(0, TimeUnit::Millisecond)),
1014 Some(Timestamp::new(300000, TimeUnit::Millisecond)),
1015 ),
1016 "SELECT sum(cte.number), cte.number, date_bin('5 minutes', cte.ts) AS time_window, cte.bucket_name FROM (SELECT numbers_with_ts.number, numbers_with_ts.ts, CASE WHEN (numbers_with_ts.number < 5) THEN 'bucket_0_5' WHEN (numbers_with_ts.number >= 5) THEN 'bucket_5_inf' END AS bucket_name FROM numbers_with_ts WHERE ((ts >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (ts <= CAST('1970-01-01 00:05:00' AS TIMESTAMP)))) AS cte GROUP BY cte.number, date_bin('5 minutes', cte.ts), cte.bucket_name",
1017 ),
1018 ];
1019
1020 for (sql, current, expected, expected_unparsed) in testcases {
1021 let plan = sql_to_df_plan(ctx.clone(), query_engine.clone(), sql, true)
1022 .await
1023 .unwrap();
1024
1025 let real =
1026 find_plan_time_window_bound(&plan, current, ctx.clone(), query_engine.clone())
1027 .await
1028 .unwrap();
1029 assert_eq!(expected, real);
1030
1031 let plan = sql_to_df_plan(ctx.clone(), query_engine.clone(), sql, false)
1032 .await
1033 .unwrap();
1034 let (col_name, lower, upper) = real;
1035 let new_sql = if let Some(lower) = lower {
1036 let to_df_literal = |value| {
1037 let value = Value::from(value);
1038
1039 value.try_to_scalar_value(&value.data_type()).unwrap()
1040 };
1041 let lower = to_df_literal(lower);
1042 let upper = to_df_literal(upper.unwrap());
1043 let expr = col(&col_name)
1044 .gt_eq(lit(lower))
1045 .and(col(&col_name).lt_eq(lit(upper)));
1046 let mut add_filter = AddFilterRewriter::new(expr);
1047 let plan = plan.rewrite(&mut add_filter).unwrap().data;
1048 df_plan_to_sql(&plan).unwrap()
1049 } else {
1050 sql.to_string()
1051 };
1052 assert_eq!(expected_unparsed, new_sql);
1053 }
1054 }
1055
1056 #[tokio::test]
1057 async fn test_complex_plans_skip_time_window_expr() {
1058 let query_engine = create_test_query_engine();
1059 let ctx = QueryContext::arc();
1060
1061 let testcases = [
1062 r#"
1066SELECT
1067 l.number,
1068 date_bin('5 minutes', l.ts) AS time_window
1069FROM numbers_with_ts l
1070JOIN numbers_with_ts r ON l.number = r.number
1071GROUP BY l.number, time_window
1072"#,
1073 r#"
1076SELECT number, time_window
1077FROM (
1078 SELECT
1079 number,
1080 time_window,
1081 row_number() OVER (PARTITION BY number ORDER BY time_window DESC) AS rn
1082 FROM (
1083 SELECT number, date_bin('5 minutes', ts) AS time_window
1084 FROM numbers_with_ts
1085 GROUP BY number, time_window
1086 )
1087)
1088WHERE rn = 1
1089"#,
1090 r#"
1092SELECT date_bin('5 minutes', ts) AS time_window
1093FROM numbers_with_ts
1094GROUP BY time_window
1095UNION ALL
1096SELECT date_bin('5 minutes', ts) AS time_window
1097FROM numbers_with_ts
1098GROUP BY time_window
1099"#,
1100 r#"
1103SELECT max(cnt)
1104FROM (
1105 SELECT date_bin('5 minutes', ts) AS time_window, count(number) AS cnt
1106 FROM numbers_with_ts
1107 GROUP BY time_window
1108)
1109"#,
1110 r#"
1113SELECT date_bin('5 minutes', ts) AS time_window
1114FROM numbers_with_ts
1115WHERE number IN (SELECT number FROM numbers_with_ts)
1116GROUP BY time_window
1117"#,
1118 r#"
1122SELECT date_bin('5 minutes', ts) AS time_window
1123FROM numbers_with_ts
1124GROUP BY time_window
1125ORDER BY time_window
1126"#,
1127 r#"
1130SELECT DISTINCT time_window
1131FROM (
1132 SELECT date_bin('5 minutes', ts) AS time_window
1133 FROM numbers_with_ts
1134 GROUP BY time_window
1135)
1136"#,
1137 r#"
1139SELECT date_bin('5 minutes', ts) AS time_window
1140FROM numbers_with_ts
1141GROUP BY time_window
1142LIMIT 10
1143"#,
1144 r#"
1148SELECT date_bin('5 minutes', l.ts) AS time_window
1149FROM numbers_with_ts l, numbers_with_ts r
1150GROUP BY time_window
1151"#,
1152 r#"
1156SELECT date_bin('5 minutes', l.ts) AS time_window
1157FROM numbers_with_ts l CROSS JOIN (VALUES (1)) AS v(x)
1158GROUP BY time_window
1159"#,
1160 ];
1161
1162 for sql in testcases {
1163 let plan = sql_to_df_plan(ctx.clone(), query_engine.clone(), sql, true)
1164 .await
1165 .unwrap();
1166 let (_, lower, upper) = find_plan_time_window_bound(
1167 &plan,
1168 Timestamp::new(23, TimeUnit::Millisecond),
1169 ctx.clone(),
1170 query_engine.clone(),
1171 )
1172 .await
1173 .unwrap();
1174
1175 assert_eq!(None, lower, "query should not have TWE: {sql}");
1176 assert_eq!(None, upper, "query should not have TWE: {sql}");
1177 }
1178 }
1179
1180 #[tokio::test]
1181 async fn test_simple_single_source_aggregate_keeps_time_window_expr() {
1182 let query_engine = create_test_query_engine();
1183 let ctx = QueryContext::arc();
1184
1185 let sql = r#"
1186SELECT max(number) AS max_number, date_bin('5 minutes', ts) AS time_window
1187FROM numbers_with_ts
1188GROUP BY time_window
1189"#;
1190 let plan = sql_to_df_plan(ctx.clone(), query_engine.clone(), sql, true)
1191 .await
1192 .unwrap();
1193 let (_, lower, upper) = find_plan_time_window_bound(
1194 &plan,
1195 Timestamp::new(23, TimeUnit::Millisecond),
1196 ctx.clone(),
1197 query_engine.clone(),
1198 )
1199 .await
1200 .unwrap();
1201
1202 assert_eq!(Some(Timestamp::new(0, TimeUnit::Millisecond)), lower);
1203 assert_eq!(Some(Timestamp::new(300000, TimeUnit::Millisecond)), upper);
1204 }
1205}