1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
18use std::sync::Arc;
19
20use catalog::CatalogManagerRef;
21use common_error::ext::BoxedError;
22use common_function::aggrs::aggr_wrapper::get_aggr_func;
23use common_telemetry::debug;
24use datafusion::arrow::datatypes::DataType as ArrowDataType;
25use datafusion::datasource::DefaultTableSource;
26use datafusion::error::Result as DfResult;
27use datafusion::logical_expr::Expr;
28use datafusion::sql::unparser::Unparser;
29use datafusion_common::tree_node::{
30 Transformed, TreeNode as _, TreeNodeRecursion, TreeNodeRewriter, TreeNodeVisitor,
31};
32use datafusion_common::{
33 Column, DFSchema, DataFusionError, NullEquality, ScalarValue, TableReference,
34};
35use datafusion_expr::logical_plan::{Aggregate, TableScanBuilder};
36use datafusion_expr::{
37 Distinct, ExprSchemable, JoinType, LogicalPlan, LogicalPlanBuilder, Operator, Projection, and,
38 binary_expr, bitwise_and, bitwise_or, bitwise_xor, is_null, or, when,
39};
40use datatypes::prelude::ConcreteDataType;
41use datatypes::schema::{ColumnSchema, SchemaRef};
42use query::QueryEngineRef;
43use query::parser::{DEFAULT_LOOKBACK_STRING, PromQuery, QueryLanguageParser, QueryStatement};
44use session::context::QueryContextRef;
45use snafu::{OptionExt, ResultExt, ensure};
46use sql::parser::{ParseOptions, ParserContext};
47use sql::statements::statement::Statement;
48use sql::statements::tql::Tql;
49use table::TableRef;
50use table::table::adapter::DfTableProviderAdapter;
51
52use crate::adapter::{AUTO_CREATED_PLACEHOLDER_TS_COL, AUTO_CREATED_UPDATE_AT_TS_COL};
53use crate::df_optimizer::apply_df_optimizer;
54use crate::error::{DatafusionSnafu, ExternalSnafu, InvalidQuerySnafu, TableNotFoundSnafu};
55use crate::{Error, TableName};
56
57#[cfg(test)]
58mod test;
59
60#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct IncrementalAggregateMergeColumn {
70 pub input_field_name: String,
73 pub output_field_name: String,
76 pub merge_op: IncrementalAggregateMergeOp,
77}
78
79impl IncrementalAggregateMergeColumn {
80 pub fn new(output_field_name: String, merge_op: IncrementalAggregateMergeOp) -> Self {
82 Self {
83 input_field_name: output_field_name.clone(),
84 output_field_name,
85 merge_op,
86 }
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum IncrementalAggregateMergeOp {
92 Sum,
93 Min,
94 Max,
95 BoolAnd,
96 BoolOr,
97 BitAnd,
98 BitOr,
99 BitXor,
100 StateDeltaMerge {
101 function_name: &'static str,
102 params: Vec<Expr>,
103 },
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct IncrementalAggregateAnalysis {
115 pub group_key_names: Vec<String>,
117 pub merge_columns: Vec<IncrementalAggregateMergeColumn>,
118 pub literal_columns: Vec<String>,
120 pub output_field_names: Vec<String>,
122 pub unsupported_exprs: Vec<String>,
123}
124
125fn unqualified_col(name: impl Into<String>) -> Expr {
126 Expr::Column(Column::from_name(name.into()))
127}
128
129fn qualified_col(qualifier: &str, name: impl Into<String>) -> Expr {
130 Expr::Column(Column::new(Some(qualifier), name.into()))
131}
132
133fn qualified_column(qualifier: &str, name: impl Into<String>) -> Column {
134 Column::new(Some(qualifier), name.into())
135}
136
137fn find_group_key_names(plan: &LogicalPlan) -> Result<Vec<String>, Error> {
138 let mut group_finder = FindGroupByFinalName::default();
139 plan.visit(&mut group_finder)
140 .with_context(|_| DatafusionSnafu {
141 context: format!("Failed to inspect group-by columns from logical plan: {plan:?}"),
142 })?;
143
144 let mut group_key_names = group_finder
145 .get_group_expr_names()
146 .unwrap_or_default()
147 .into_iter()
148 .collect::<Vec<_>>();
149 group_key_names.sort();
150 Ok(group_key_names)
151}
152
153fn has_grouping_set(plan: &LogicalPlan) -> bool {
154 match plan {
155 LogicalPlan::Aggregate(aggregate) => aggregate
156 .group_expr
157 .iter()
158 .any(|expr| matches!(expr, Expr::GroupingSet(_))),
159 _ => plan.inputs().into_iter().any(has_grouping_set),
160 }
161}
162
163fn has_aggregate(plan: &LogicalPlan) -> bool {
164 match plan {
165 LogicalPlan::Aggregate(_) => true,
166 _ => plan.inputs().into_iter().any(has_aggregate),
167 }
168}
169
170fn peel_subquery_aliases(mut plan: &LogicalPlan) -> &LogicalPlan {
171 while let LogicalPlan::SubqueryAlias(alias) = plan {
172 plan = alias.input.as_ref();
173 }
174 plan
175}
176
177fn extract_incremental_aggregate(plan: &LogicalPlan) -> Result<Option<&Aggregate>, String> {
178 let plan = match plan {
182 LogicalPlan::Projection(projection) => projection.input.as_ref(),
183 _ => plan,
184 };
185
186 match plan {
187 LogicalPlan::Aggregate(aggregate) => {
188 check_input_plan_shape(aggregate.input.as_ref())?;
189 Ok(Some(aggregate))
190 }
191 LogicalPlan::Filter(filter) if has_aggregate(filter.input.as_ref()) => Err(
192 "unsupported post-aggregate filter (HAVING) in incremental aggregate rewrite"
193 .to_string(),
194 ),
195 _ if has_aggregate(plan) => Err(
196 "unsupported post-aggregate plan shape in incremental aggregate rewrite".to_string(),
197 ),
198 _ => Ok(None),
199 }
200}
201
202fn check_input_plan_shape(plan: &LogicalPlan) -> Result<(), String> {
203 let plan = peel_subquery_aliases(plan);
204 match plan {
205 LogicalPlan::TableScan(_) => Ok(()),
208 LogicalPlan::Filter(filter) => match peel_subquery_aliases(filter.input.as_ref()) {
209 LogicalPlan::TableScan(_) => Ok(()),
210 _ => Err(
211 "unsupported aggregate input plan shape in incremental aggregate rewrite"
212 .to_string(),
213 ),
214 },
215 _ => Err(
216 "unsupported aggregate input plan shape in incremental aggregate rewrite".to_string(),
217 ),
218 }
219}
220
221#[derive(Debug, Default)]
222struct OutputProjectionInfo {
223 has_top_level_projection: bool,
224 aggregate_outputs: Vec<(String, String)>,
226 literal_columns: HashSet<String>,
227 output_field_names: Vec<String>,
228}
229
230impl OutputProjectionInfo {
231 fn output_field_name_set(&self) -> HashSet<String> {
232 self.output_field_names.iter().cloned().collect()
233 }
234
235 fn duplicate_output_names(&self) -> Vec<String> {
236 let mut seen = HashSet::new();
237 let mut duplicates = BTreeSet::new();
238 for name in &self.output_field_names {
239 if !seen.insert(name.clone()) {
240 duplicates.insert(name.clone());
241 }
242 }
243 duplicates.into_iter().collect()
244 }
245}
246
247fn collect_output_projection_info(plan: &LogicalPlan) -> OutputProjectionInfo {
248 let mut projection_info = OutputProjectionInfo {
249 has_top_level_projection: matches!(plan, LogicalPlan::Projection(_)),
250 output_field_names: plan
251 .schema()
252 .fields()
253 .iter()
254 .map(|field| field.name().clone())
255 .collect(),
256 ..Default::default()
257 };
258
259 if let LogicalPlan::Projection(projection) = plan {
260 for expr in &projection.expr {
261 match expr {
262 Expr::Alias(alias) => {
263 let alias_name = alias.name.clone();
268 if let Expr::Column(column) = alias.expr.as_ref() {
269 projection_info
270 .aggregate_outputs
271 .push((column.name.clone(), alias_name));
272 } else if let Expr::Alias(inner_alias) = alias.expr.as_ref()
273 && inner_alias.name.eq_ignore_ascii_case("count(*)")
274 && let Expr::Column(column) = inner_alias.expr.as_ref()
275 {
276 projection_info
277 .aggregate_outputs
278 .push((column.name.clone(), alias_name));
279 } else if is_passthrough_output_column(&alias_name, alias.expr.as_ref()) {
280 projection_info.literal_columns.insert(alias_name);
281 }
282 }
283 Expr::Column(col) => {
284 projection_info
285 .aggregate_outputs
286 .push((col.name.clone(), col.name.clone()));
287 }
288 Expr::Literal(_, _) => {
289 projection_info
290 .literal_columns
291 .insert(expr.qualified_name().1);
292 }
293 _ => {}
294 }
295 }
296 }
297
298 if projection_info
299 .output_field_names
300 .iter()
301 .any(|name| name == AUTO_CREATED_PLACEHOLDER_TS_COL)
302 {
303 projection_info
304 .literal_columns
305 .insert(AUTO_CREATED_PLACEHOLDER_TS_COL.to_string());
306 }
307
308 projection_info
309}
310
311fn is_passthrough_output_column(alias_name: &str, expr: &Expr) -> bool {
312 matches!(expr, Expr::Literal(_, _))
313 || match alias_name {
314 AUTO_CREATED_UPDATE_AT_TS_COL => expr == &datafusion::prelude::now(),
315 AUTO_CREATED_PLACEHOLDER_TS_COL => is_literal_or_cast_literal(expr),
316 _ => false,
317 }
318}
319
320fn is_literal_or_cast_literal(expr: &Expr) -> bool {
321 match expr {
322 Expr::Literal(_, _) => true,
323 Expr::Cast(cast) => is_literal_or_cast_literal(cast.expr.as_ref()),
324 Expr::TryCast(cast) => is_literal_or_cast_literal(cast.expr.as_ref()),
325 _ => false,
326 }
327}
328
329fn merge_op_for_aggregate_expr(
330 aggr_expr: &Expr,
331 input_schema: &DFSchema,
332) -> Result<IncrementalAggregateMergeOp, String> {
333 let Some(aggr_func) = get_aggr_func(aggr_expr) else {
334 return Err(aggr_expr.to_string());
335 };
336 if aggr_func.params.distinct {
337 return Err(format!("unsupported DISTINCT aggregate: {aggr_expr}"));
338 }
339 if !aggr_func.params.order_by.is_empty() {
340 return Err(format!("unsupported aggregate ORDER BY: {aggr_expr}"));
341 }
342 if aggr_func.params.null_treatment.is_some() {
343 return Err(format!("unsupported aggregate NULL treatment: {aggr_expr}"));
344 }
345
346 let state_delta_merge = |function_name, params| {
347 Ok(IncrementalAggregateMergeOp::StateDeltaMerge {
348 function_name,
349 params,
350 })
351 };
352 match aggr_func.func.name().to_ascii_lowercase().as_str() {
353 "sum" | "count" => Ok(IncrementalAggregateMergeOp::Sum),
354 "min" => Ok(IncrementalAggregateMergeOp::Min),
355 "max" => Ok(IncrementalAggregateMergeOp::Max),
356 "bool_and" => Ok(IncrementalAggregateMergeOp::BoolAnd),
357 "bool_or" => Ok(IncrementalAggregateMergeOp::BoolOr),
358 "bit_and" => Ok(IncrementalAggregateMergeOp::BitAnd),
359 "bit_or" => Ok(IncrementalAggregateMergeOp::BitOr),
360 "bit_xor" => Ok(IncrementalAggregateMergeOp::BitXor),
361 "avg_state" if aggr_func.params.args.len() == 1 => {
363 state_delta_merge("__avg_state_delta_merge", vec![])
364 }
365 "hll" if aggr_func.params.args.len() == 1 => state_delta_merge("__hll_delta_merge", vec![]),
366 "stddev_pop_state" if aggr_func.params.args.len() == 1 => {
367 state_delta_merge("__stddev_pop_state_delta_merge", vec![])
368 }
369 "uddsketch_state" if aggr_func.params.args.len() == 3 => {
370 let [bucket_size, error_rate, _] = aggr_func.params.args.as_slice() else {
371 unreachable!();
372 };
373 if !matches!(bucket_size, Expr::Literal(ScalarValue::Int64(Some(_)), _))
374 || !matches!(error_rate, Expr::Literal(ScalarValue::Float64(Some(_)), _))
375 {
376 return Err(aggr_expr.to_string());
377 }
378 state_delta_merge(
379 "__uddsketch_state_delta_merge",
380 vec![bucket_size.clone(), error_rate.clone()],
381 )
382 }
383 "avg_merge"
386 if aggr_func.params.args.len() == 1
387 && aggr_func.params.args[0].get_type(input_schema).ok()
388 == Some(ArrowDataType::Binary) =>
389 {
390 state_delta_merge("__avg_state_delta_merge", vec![])
391 }
392 _ => Err(aggr_expr.to_string()),
393 }
394}
395
396fn resolve_aggregate_output_fields(
397 aggr_expr: &Expr,
398 projection_info: &OutputProjectionInfo,
399 output_field_name_set: &HashSet<String>,
400) -> Vec<(String, String)> {
401 let raw_name = aggr_expr.qualified_name().1;
408 if projection_info.has_top_level_projection {
409 projection_info
410 .aggregate_outputs
411 .iter()
412 .filter(|(input_name, _)| input_name == &raw_name)
413 .cloned()
414 .collect()
415 } else if output_field_name_set.contains(&raw_name) {
416 vec![(raw_name.clone(), raw_name)]
417 } else {
418 vec![]
419 }
420}
421
422fn find_uncovered_output_fields(
423 projection_info: &OutputProjectionInfo,
424 group_key_names: &[String],
425 merge_columns: &[IncrementalAggregateMergeColumn],
426) -> Vec<String> {
427 let group_key_names = group_key_names.iter().cloned().collect::<HashSet<_>>();
428 let merge_column_names = merge_columns
429 .iter()
430 .map(|c| c.output_field_name.clone())
431 .collect::<HashSet<_>>();
432
433 projection_info
434 .output_field_names
435 .iter()
436 .filter(|name| {
437 !group_key_names.contains(*name)
438 && !merge_column_names.contains(*name)
439 && !projection_info.literal_columns.contains(*name)
440 && name.as_str() != AUTO_CREATED_UPDATE_AT_TS_COL
444 && name.as_str() != AUTO_CREATED_PLACEHOLDER_TS_COL
445 })
446 .cloned()
447 .collect()
448}
449
450fn find_unsupported_group_key_projection_outputs(
451 plan: &LogicalPlan,
452 aggregate: &Aggregate,
453 group_key_names: &[String],
454) -> Vec<String> {
455 let LogicalPlan::Projection(projection) = plan else {
456 return vec![];
457 };
458
459 let group_key_names = group_key_names.iter().cloned().collect::<HashSet<_>>();
460 let group_expr_names = aggregate
461 .group_expr
462 .iter()
463 .filter_map(|expr| expr.name_for_alias().ok())
464 .collect::<HashSet<_>>();
465 projection
466 .expr
467 .iter()
468 .filter_map(|expr| {
469 let output_name = expr.qualified_name().1;
470 if !group_key_names.contains(&output_name) {
471 return None;
472 }
473
474 let source_name = match expr {
475 Expr::Alias(alias) => alias.expr.name_for_alias().ok(),
476 _ => expr.name_for_alias().ok(),
477 };
478 if source_name.is_some_and(|name| group_expr_names.contains(&name)) {
479 None
480 } else {
481 Some(format!(
482 "unsupported group key output field is not a transparent group expression: {output_name}"
483 ))
484 }
485 })
486 .collect()
487}
488
489pub fn analyze_incremental_aggregate_plan(
490 plan: &LogicalPlan,
491) -> Result<Option<IncrementalAggregateAnalysis>, Error> {
492 let group_key_names = find_group_key_names(plan)?;
493 let aggregate = match extract_incremental_aggregate(plan) {
494 Ok(Some(aggregate)) => aggregate,
495 Ok(None) => return Ok(None),
496 Err(reason) => {
497 let projection_info = collect_output_projection_info(plan);
498 let mut unsupported_exprs = projection_info
499 .duplicate_output_names()
500 .into_iter()
501 .map(|name| format!("duplicate output field name: {name}"))
502 .collect::<Vec<_>>();
503 unsupported_exprs.push(reason);
504 return Ok(Some(IncrementalAggregateAnalysis {
505 group_key_names,
506 merge_columns: vec![],
507 literal_columns: vec![],
508 output_field_names: projection_info.output_field_names,
509 unsupported_exprs,
510 }));
511 }
512 };
513 let aggr_exprs = aggregate.aggr_expr.clone();
514 let projection_info = collect_output_projection_info(plan);
515 let output_field_name_set = projection_info.output_field_name_set();
516
517 let mut merge_columns = Vec::with_capacity(aggr_exprs.len());
518 let mut unsupported_exprs = projection_info
519 .duplicate_output_names()
520 .into_iter()
521 .map(|name| format!("duplicate output field name: {name}"))
522 .collect::<Vec<_>>();
523 if has_grouping_set(plan) {
524 unsupported_exprs.push(
525 "unsupported GROUPING SETS/CUBE/ROLLUP in incremental aggregate rewrite".to_string(),
526 );
527 }
528 if group_key_names.is_empty() {
529 unsupported_exprs
530 .push("unsupported global aggregate in incremental aggregate rewrite".to_string());
531 }
532 unsupported_exprs.extend(find_unsupported_group_key_projection_outputs(
533 plan,
534 aggregate,
535 &group_key_names,
536 ));
537 for aggr_expr in aggr_exprs {
538 let merge_op = match merge_op_for_aggregate_expr(&aggr_expr, aggregate.input.schema()) {
539 Ok(merge_op) => merge_op,
540 Err(reason) => {
541 unsupported_exprs.push(reason);
542 continue;
543 }
544 };
545 let aggregate_outputs =
546 resolve_aggregate_output_fields(&aggr_expr, &projection_info, &output_field_name_set);
547 if aggregate_outputs.is_empty() {
548 unsupported_exprs.push(aggr_expr.to_string());
549 continue;
550 }
551 let Some((_, input_field_name)) = aggregate_outputs.first() else {
552 continue;
553 };
554 let input_field_name = input_field_name.clone();
555 for (_, output_field_name) in aggregate_outputs {
556 merge_columns.push(IncrementalAggregateMergeColumn {
557 input_field_name: input_field_name.clone(),
558 output_field_name,
559 merge_op: merge_op.clone(),
560 });
561 }
562 }
563 if projection_info.has_top_level_projection {
564 let output_positions = projection_info
565 .output_field_names
566 .iter()
567 .enumerate()
568 .map(|(position, name)| (name.as_str(), position))
569 .collect::<HashMap<_, _>>();
570 merge_columns.sort_by_key(|column| {
571 output_positions
572 .get(column.output_field_name.as_str())
573 .copied()
574 .unwrap_or(usize::MAX)
575 });
576 }
577 unsupported_exprs.extend(
578 find_uncovered_output_fields(&projection_info, &group_key_names, &merge_columns)
579 .into_iter()
580 .map(|name| format!("unsupported output field: {name}")),
581 );
582 if !unsupported_exprs.is_empty() {
583 merge_columns.clear();
584 }
585 let mut literal_columns = projection_info
586 .literal_columns
587 .into_iter()
588 .collect::<Vec<_>>();
589 literal_columns.sort();
590
591 Ok(Some(IncrementalAggregateAnalysis {
592 group_key_names,
593 merge_columns,
594 literal_columns,
595 output_field_names: projection_info.output_field_names,
596 unsupported_exprs,
597 }))
598}
599
600pub async fn rewrite_incremental_aggregate_with_sink_merge(
632 delta_plan: &LogicalPlan,
633 analysis: &IncrementalAggregateAnalysis,
634 engine: &QueryEngineRef,
635 sink_table: TableRef,
636 sink_table_name: &TableName,
637 sink_dirty_filter: Option<Expr>,
638) -> Result<LogicalPlan, Error> {
639 ensure!(
640 analysis.unsupported_exprs.is_empty(),
641 InvalidQuerySnafu {
642 reason: format!(
643 "UNSUPPORTED_INCREMENTAL_AGG: unsupported aggregate expressions {:?}",
644 analysis.unsupported_exprs
645 )
646 }
647 );
648
649 ensure!(
650 !analysis.merge_columns.is_empty(),
651 InvalidQuerySnafu {
652 reason:
653 "UNSUPPORTED_INCREMENTAL_AGG: aggregate query has no mergeable aggregate columns"
654 .to_string()
655 }
656 );
657
658 ensure!(
659 !analysis.group_key_names.is_empty(),
660 InvalidQuerySnafu {
661 reason: "UNSUPPORTED_INCREMENTAL_AGG: global aggregate query is not supported"
662 .to_string()
663 }
664 );
665
666 let delta_alias = "__flow_delta";
667 let sink_alias = "__flow_sink";
668
669 let state_merge = analysis.merge_columns.iter().any(|column| {
670 matches!(
671 &column.merge_op,
672 IncrementalAggregateMergeOp::StateDeltaMerge { .. }
673 )
674 });
675 let mut selected_columns = analysis.group_key_names.clone();
676 selected_columns.extend(
677 analysis
678 .merge_columns
679 .iter()
680 .map(|c| c.output_field_name.clone()),
681 );
682 let mut selected_column_names = HashSet::new();
683 selected_columns.retain(|name| selected_column_names.insert(name.clone()));
684 let mut delta_selected_columns = analysis.group_key_names.clone();
685 delta_selected_columns.extend(
686 analysis
687 .merge_columns
688 .iter()
689 .map(|c| c.input_field_name.clone()),
690 );
691 delta_selected_columns.extend(analysis.literal_columns.iter().cloned());
692 let mut delta_selected_column_names = HashSet::new();
693 delta_selected_columns.retain(|name| delta_selected_column_names.insert(name.clone()));
694
695 let delta_selected_exprs = delta_selected_columns
696 .iter()
697 .cloned()
698 .map(unqualified_col)
699 .collect::<Vec<_>>();
700 let delta_selected = LogicalPlanBuilder::from(delta_plan.clone())
701 .project(delta_selected_exprs)
702 .with_context(|_| DatafusionSnafu {
703 context: "Failed to project delta plan for incremental sink merge".to_string(),
704 })?
705 .alias(delta_alias)
706 .with_context(|_| DatafusionSnafu {
707 context: "Failed to alias delta plan for incremental sink merge".to_string(),
708 })?
709 .build()
710 .with_context(|_| DatafusionSnafu {
711 context: "Failed to build projected delta plan for incremental sink merge".to_string(),
712 })?;
713
714 let table_provider = Arc::new(DfTableProviderAdapter::new(sink_table));
715 let table_source = Arc::new(DefaultTableSource::new(table_provider));
716 let sink_scan = LogicalPlan::TableScan(
717 TableScanBuilder::new(
718 TableReference::Full {
719 catalog: sink_table_name[0].clone().into(),
720 schema: sink_table_name[1].clone().into(),
721 table: sink_table_name[2].clone().into(),
722 },
723 table_source,
724 )
725 .build()
726 .with_context(|_| DatafusionSnafu {
727 context: "Failed to build sink table scan for incremental sink merge".to_string(),
728 })?,
729 );
730
731 let sink_selected_exprs = selected_columns
732 .iter()
733 .cloned()
734 .map(unqualified_col)
735 .collect::<Vec<_>>();
736 let sink_input = if let Some(predicate) = sink_dirty_filter {
737 LogicalPlanBuilder::from(sink_scan)
738 .filter(predicate)
739 .with_context(|_| DatafusionSnafu {
740 context: "Failed to filter sink table scan for incremental sink merge".to_string(),
741 })?
742 .build()
743 .with_context(|_| DatafusionSnafu {
744 context: "Failed to build filtered sink plan for incremental sink merge"
745 .to_string(),
746 })?
747 } else {
748 sink_scan
749 };
750
751 let sink_selected = LogicalPlanBuilder::from(sink_input)
752 .project(sink_selected_exprs)
753 .with_context(|_| DatafusionSnafu {
754 context: "Failed to project sink table scan for incremental sink merge".to_string(),
755 })?
756 .alias(sink_alias)
757 .with_context(|_| DatafusionSnafu {
758 context: "Failed to alias sink plan for incremental sink merge".to_string(),
759 })?
760 .build()
761 .with_context(|_| DatafusionSnafu {
762 context: "Failed to build projected sink plan for incremental sink merge".to_string(),
763 })?;
764
765 let join_keys = (
766 analysis
767 .group_key_names
768 .iter()
769 .cloned()
770 .map(|c| qualified_column(delta_alias, c))
771 .collect::<Vec<_>>(),
772 analysis
773 .group_key_names
774 .iter()
775 .cloned()
776 .map(|c| qualified_column(sink_alias, c))
777 .collect::<Vec<_>>(),
778 );
779
780 let joined = LogicalPlanBuilder::from(delta_selected)
781 .join_detailed(
782 sink_selected,
783 JoinType::Left,
784 join_keys,
785 None,
786 NullEquality::NullEqualsNull,
787 )
788 .with_context(|_| DatafusionSnafu {
789 context: "Failed to left join delta and sink plans for incremental sink merge"
790 .to_string(),
791 })?
792 .build()
793 .with_context(|_| DatafusionSnafu {
794 context: "Failed to build left join plan for incremental sink merge".to_string(),
795 })?;
796
797 let group_key_names = analysis.group_key_names.iter().collect::<HashSet<_>>();
798 let literal_columns = analysis.literal_columns.iter().collect::<HashSet<_>>();
799 let merge_columns = analysis
800 .merge_columns
801 .iter()
802 .map(|c| (&c.output_field_name, c))
803 .collect::<HashMap<_, _>>();
804
805 let mut ordinary_exprs = Vec::with_capacity(analysis.output_field_names.len());
806 let mut state_aggr_exprs = Vec::new();
807 for output_field_name in &analysis.output_field_names {
808 if group_key_names.contains(output_field_name)
809 || literal_columns.contains(output_field_name)
810 {
811 let expr =
812 qualified_col(delta_alias, output_field_name.clone()).alias(output_field_name);
813 ordinary_exprs.push(expr);
814 } else if let Some(merge_col) = merge_columns.get(output_field_name) {
815 if matches!(
816 &merge_col.merge_op,
817 IncrementalAggregateMergeOp::StateDeltaMerge { .. }
818 ) {
819 state_aggr_exprs.push(build_state_delta_merge_expr(engine, merge_col)?);
820 } else {
821 let expr = build_left_join_merge_expr(delta_alias, sink_alias, merge_col)?;
822 ordinary_exprs.push(expr);
823 }
824 } else {
825 return InvalidQuerySnafu {
826 reason: format!(
827 "UNSUPPORTED_INCREMENTAL_AGG: output field {output_field_name} is not covered by group keys, literals, or merge columns"
828 ),
829 }
830 .fail();
831 }
832 }
833
834 if state_merge {
835 let aggregated = LogicalPlanBuilder::from(joined)
836 .aggregate(ordinary_exprs, state_aggr_exprs)
837 .with_context(|_| DatafusionSnafu {
838 context: "Failed to aggregate state delta merge plan".to_string(),
839 })?
840 .build()
841 .with_context(|_| DatafusionSnafu {
842 context: "Failed to build state delta merge plan".to_string(),
843 })?;
844 let output_exprs = analysis
845 .output_field_names
846 .iter()
847 .cloned()
848 .map(unqualified_col)
849 .collect::<Vec<_>>();
850 LogicalPlanBuilder::from(aggregated)
851 .project(output_exprs)
852 .with_context(|_| DatafusionSnafu {
853 context: "Failed to project state delta merge plan".to_string(),
854 })?
855 .build()
856 .with_context(|_| DatafusionSnafu {
857 context: "Failed to finalize incremental aggregate sink merge plan".to_string(),
858 })
859 } else {
860 LogicalPlanBuilder::from(joined)
861 .project(ordinary_exprs)
862 .with_context(|_| DatafusionSnafu {
863 context: "Failed to build projection merge plan for incremental sink merge"
864 .to_string(),
865 })?
866 .build()
867 .with_context(|_| DatafusionSnafu {
868 context: "Failed to finalize incremental aggregate sink merge plan".to_string(),
869 })
870 }
871}
872
873fn build_state_delta_merge_expr(
874 engine: &QueryEngineRef,
875 merge_col: &IncrementalAggregateMergeColumn,
876) -> Result<Expr, Error> {
877 let (function_name, params) = match &merge_col.merge_op {
878 IncrementalAggregateMergeOp::StateDeltaMerge {
879 function_name,
880 params,
881 } => (*function_name, params.clone()),
882 _ => {
883 return InvalidQuerySnafu {
884 reason: "non-state aggregate passed to state delta merge".to_string(),
885 }
886 .fail();
887 }
888 };
889 let Some(udaf) = engine
890 .engine_state()
891 .aggr_function(function_name)
892 .or_else(|| {
893 engine
894 .engine_state()
895 .session_state()
896 .aggregate_functions()
897 .get(function_name)
898 .map(|udaf| udaf.as_ref().clone())
899 })
900 else {
901 return InvalidQuerySnafu {
902 reason: format!("Aggregate function {function_name} is not registered"),
903 }
904 .fail();
905 };
906 let mut args = params;
907 args.push(qualified_col(
908 "__flow_delta",
909 merge_col.input_field_name.clone(),
910 ));
911 args.push(qualified_col(
912 "__flow_sink",
913 merge_col.output_field_name.clone(),
914 ));
915 Ok(udaf.call(args).alias(merge_col.output_field_name.clone()))
916}
917
918fn build_left_join_merge_expr(
919 delta_alias: &str,
920 sink_alias: &str,
921 merge_col: &IncrementalAggregateMergeColumn,
922) -> Result<Expr, Error> {
923 let left = qualified_col(delta_alias, merge_col.input_field_name.clone());
924 let right = qualified_col(sink_alias, merge_col.output_field_name.clone());
925 let merged = match merge_col.merge_op.clone() {
926 IncrementalAggregateMergeOp::Sum => when(is_null(left.clone()), right.clone())
927 .when(is_null(right.clone()), left.clone())
928 .otherwise(binary_expr(left.clone(), Operator::Plus, right.clone()))
929 .with_context(|_| DatafusionSnafu {
930 context: "Failed to build SUM merge expression".to_string(),
931 })?,
932 IncrementalAggregateMergeOp::Min => when(is_null(right.clone()), left.clone())
933 .when(left.clone().lt_eq(right.clone()), left.clone())
934 .otherwise(right.clone())
935 .with_context(|_| DatafusionSnafu {
936 context: "Failed to build MIN merge expression".to_string(),
937 })?,
938 IncrementalAggregateMergeOp::Max => when(is_null(right.clone()), left.clone())
939 .when(left.clone().gt_eq(right.clone()), left.clone())
940 .otherwise(right.clone())
941 .with_context(|_| DatafusionSnafu {
942 context: "Failed to build MAX merge expression".to_string(),
943 })?,
944 IncrementalAggregateMergeOp::BoolAnd => when(is_null(left.clone()), right.clone())
945 .when(is_null(right.clone()), left.clone())
946 .otherwise(and(left.clone(), right.clone()))
947 .with_context(|_| DatafusionSnafu {
948 context: "Failed to build BOOL_AND merge expression".to_string(),
949 })?,
950 IncrementalAggregateMergeOp::BoolOr => when(is_null(left.clone()), right.clone())
951 .when(is_null(right.clone()), left.clone())
952 .otherwise(or(left.clone(), right.clone()))
953 .with_context(|_| DatafusionSnafu {
954 context: "Failed to build BOOL_OR merge expression".to_string(),
955 })?,
956 IncrementalAggregateMergeOp::BitAnd => when(is_null(left.clone()), right.clone())
957 .when(is_null(right.clone()), left.clone())
958 .otherwise(bitwise_and(left.clone(), right.clone()))
959 .with_context(|_| DatafusionSnafu {
960 context: "Failed to build BIT_AND merge expression".to_string(),
961 })?,
962 IncrementalAggregateMergeOp::BitOr => when(is_null(left.clone()), right.clone())
963 .when(is_null(right.clone()), left.clone())
964 .otherwise(bitwise_or(left.clone(), right.clone()))
965 .with_context(|_| DatafusionSnafu {
966 context: "Failed to build BIT_OR merge expression".to_string(),
967 })?,
968 IncrementalAggregateMergeOp::BitXor => when(is_null(left.clone()), right.clone())
969 .when(is_null(right.clone()), left.clone())
970 .otherwise(bitwise_xor(left.clone(), right.clone()))
971 .with_context(|_| DatafusionSnafu {
972 context: "Failed to build BIT_XOR merge expression".to_string(),
973 })?,
974 IncrementalAggregateMergeOp::StateDeltaMerge { .. } => {
975 return InvalidQuerySnafu {
976 reason: "state aggregate must be built with its delta UDAF".to_string(),
977 }
978 .fail();
979 }
980 };
981 Ok(merged.alias(merge_col.output_field_name.clone()))
982}
983
984pub async fn get_table_info_df_schema(
985 catalog_mr: CatalogManagerRef,
986 table_name: TableName,
987) -> Result<(TableRef, Arc<DFSchema>), Error> {
988 let full_table_name = table_name.clone().join(".");
989 let table = catalog_mr
990 .table(&table_name[0], &table_name[1], &table_name[2], None)
991 .await
992 .map_err(BoxedError::new)
993 .context(ExternalSnafu)?
994 .context(TableNotFoundSnafu {
995 name: &full_table_name,
996 })?;
997 let table_info = table.table_info();
998
999 let schema = table_info.meta.schema.clone();
1000
1001 let df_schema: Arc<DFSchema> = Arc::new(
1002 schema
1003 .arrow_schema()
1004 .clone()
1005 .try_into()
1006 .with_context(|_| DatafusionSnafu {
1007 context: format!(
1008 "Failed to convert arrow schema to datafusion schema, arrow_schema={:?}",
1009 schema.arrow_schema()
1010 ),
1011 })?,
1012 );
1013 Ok((table, df_schema))
1014}
1015
1016pub async fn sql_to_df_plan(
1019 query_ctx: QueryContextRef,
1020 engine: QueryEngineRef,
1021 sql: &str,
1022 optimize: bool,
1023) -> Result<LogicalPlan, Error> {
1024 let scheduled_time = query::options::parse_scheduled_time_datetime(&query_ctx.extensions())
1025 .map_err(BoxedError::new)
1026 .context(ExternalSnafu)?;
1027 let stmts = ParserContext::create_with_dialect(
1028 sql,
1029 query_ctx.sql_dialect(),
1030 ParseOptions { scheduled_time },
1031 )
1032 .map_err(BoxedError::new)
1033 .context(ExternalSnafu)?;
1034
1035 ensure!(
1036 stmts.len() == 1,
1037 InvalidQuerySnafu {
1038 reason: format!("Expect only one statement, found {}", stmts.len())
1039 }
1040 );
1041 let stmt = &stmts[0];
1042 let query_stmt = match stmt {
1043 Statement::Tql(tql) => match tql {
1044 Tql::Eval(eval) => {
1045 let eval = eval.clone();
1046 let promql = PromQuery {
1047 start: eval.start,
1048 end: eval.end,
1049 step: eval.step,
1050 query: eval.query,
1051 lookback: eval
1052 .lookback
1053 .unwrap_or_else(|| DEFAULT_LOOKBACK_STRING.to_string()),
1054 alias: eval.alias.clone(),
1055 };
1056
1057 QueryLanguageParser::parse_promql(&promql, &query_ctx)
1058 .map_err(BoxedError::new)
1059 .context(ExternalSnafu)?
1060 }
1061 _ => InvalidQuerySnafu {
1062 reason: format!("TQL statement {tql:?} is not supported, expect only TQL EVAL"),
1063 }
1064 .fail()?,
1065 },
1066 _ => QueryStatement::Sql(stmt.clone()),
1067 };
1068 let plan = engine
1069 .planner()
1070 .plan(&query_stmt, query_ctx.clone())
1071 .await
1072 .map_err(BoxedError::new)
1073 .context(ExternalSnafu)?;
1074
1075 let plan = if optimize {
1076 apply_df_optimizer(plan, &query_ctx).await?
1077 } else {
1078 plan
1079 };
1080 Ok(plan)
1081}
1082
1083pub(crate) async fn gen_plan_with_matching_schema(
1086 sql: &str,
1087 query_ctx: QueryContextRef,
1088 engine: QueryEngineRef,
1089 sink_table_schema: SchemaRef,
1090 primary_key_indices: &[usize],
1091 allow_partial: bool,
1092) -> Result<LogicalPlan, Error> {
1093 gen_plan_with_matching_schema_and_values(
1094 sql,
1095 query_ctx,
1096 engine,
1097 sink_table_schema,
1098 primary_key_indices,
1099 allow_partial,
1100 &BTreeMap::new(),
1101 )
1102 .await
1103}
1104
1105pub(crate) async fn gen_plan_with_matching_schema_and_values(
1107 sql: &str,
1108 query_ctx: QueryContextRef,
1109 engine: QueryEngineRef,
1110 sink_table_schema: SchemaRef,
1111 primary_key_indices: &[usize],
1112 allow_partial: bool,
1113 values: &BTreeMap<String, ScalarValue>,
1114) -> Result<LogicalPlan, Error> {
1115 let plan = sql_to_df_plan(query_ctx.clone(), engine.clone(), sql, false).await?;
1116
1117 let mut add_auto_column = ColumnMatcherRewriter::new_with_values(
1118 sink_table_schema,
1119 primary_key_indices.to_vec(),
1120 allow_partial,
1121 values.clone(),
1122 );
1123 let plan = plan
1124 .clone()
1125 .rewrite(&mut add_auto_column)
1126 .with_context(|_| DatafusionSnafu {
1127 context: "Failed to rewrite plan".to_string(),
1128 })?
1129 .data;
1130 Ok(plan)
1131}
1132
1133pub fn df_plan_to_sql(plan: &LogicalPlan) -> Result<String, Error> {
1134 struct ForceQuoteIdentifiers;
1147 impl datafusion::sql::unparser::dialect::Dialect for ForceQuoteIdentifiers {
1148 fn identifier_quote_style(&self, identifier: &str) -> Option<char> {
1149 let is_plain = !identifier.is_empty()
1150 && !identifier.starts_with(|c: char| c.is_ascii_digit())
1151 && identifier
1152 .chars()
1153 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_');
1154 if is_plain { None } else { Some('"') }
1155 }
1156 }
1157 let unparser = Unparser::new(&ForceQuoteIdentifiers);
1158 let sql = unparser
1160 .plan_to_sql(plan)
1161 .with_context(|_e| DatafusionSnafu {
1162 context: format!("Failed to unparse logical plan {plan:?}"),
1163 })?;
1164 Ok(sql.to_string())
1165}
1166
1167#[derive(Debug, Clone, Default)]
1169pub struct FindGroupByFinalName {
1170 group_exprs: Option<HashSet<datafusion_expr::Expr>>,
1171}
1172
1173impl FindGroupByFinalName {
1174 pub fn get_group_expr_names(&self) -> Option<HashSet<String>> {
1175 self.group_exprs
1176 .as_ref()
1177 .map(|exprs| exprs.iter().map(|expr| expr.qualified_name().1).collect())
1178 }
1179}
1180
1181impl TreeNodeVisitor<'_> for FindGroupByFinalName {
1182 type Node = LogicalPlan;
1183
1184 fn f_down(&mut self, node: &Self::Node) -> datafusion_common::Result<TreeNodeRecursion> {
1185 if let LogicalPlan::Aggregate(aggregate) = node {
1186 self.group_exprs = Some(aggregate.group_expr.iter().cloned().collect());
1187 debug!(
1188 "FindGroupByFinalName: Get Group by exprs from Aggregate: {:?}",
1189 self.group_exprs
1190 );
1191 } else if let LogicalPlan::Distinct(distinct) = node {
1192 debug!("FindGroupByFinalName: Distinct: {}", node);
1193 match distinct {
1194 Distinct::All(input) => {
1195 if let LogicalPlan::TableScan(table_scan) = &**input {
1196 let len = table_scan.projected_schema.fields().len();
1198 let columns = (0..len)
1199 .map(|f| {
1200 let (qualifier, field) =
1201 table_scan.projected_schema.qualified_field(f);
1202 datafusion_common::Column::new(qualifier.cloned(), field.name())
1203 })
1204 .map(datafusion_expr::Expr::Column);
1205 self.group_exprs = Some(columns.collect());
1206 } else {
1207 self.group_exprs = Some(input.expressions().iter().cloned().collect())
1208 }
1209 }
1210 Distinct::On(distinct_on) => {
1211 self.group_exprs = Some(distinct_on.on_expr.iter().cloned().collect())
1212 }
1213 }
1214 debug!(
1215 "FindGroupByFinalName: Get Group by exprs from Distinct: {:?}",
1216 self.group_exprs
1217 );
1218 }
1219
1220 Ok(TreeNodeRecursion::Continue)
1221 }
1222
1223 fn f_up(&mut self, node: &Self::Node) -> datafusion_common::Result<TreeNodeRecursion> {
1225 if let LogicalPlan::Projection(projection) = node {
1226 for expr in &projection.expr {
1227 let Some(group_exprs) = &mut self.group_exprs else {
1228 return Ok(TreeNodeRecursion::Continue);
1229 };
1230 if let datafusion_expr::Expr::Alias(alias) = expr {
1231 let mut new_group_exprs = group_exprs.clone();
1233 for group_expr in group_exprs.iter() {
1234 if group_expr.name_for_alias()? == alias.expr.name_for_alias()? {
1235 new_group_exprs.remove(group_expr);
1236 new_group_exprs.insert(expr.clone());
1237 break;
1238 }
1239 }
1240 *group_exprs = new_group_exprs;
1241 }
1242 }
1243 }
1244 debug!("Aliased group by exprs: {:?}", self.group_exprs);
1245 Ok(TreeNodeRecursion::Continue)
1246 }
1247}
1248
1249#[derive(Debug)]
1256pub struct ColumnMatcherRewriter {
1257 pub schema: SchemaRef,
1259 sink_schema: SchemaRef,
1260 values: BTreeMap<String, ScalarValue>,
1261 pub is_rewritten: bool,
1262 pub primary_key_indices: Vec<usize>,
1263 pub allow_partial: bool,
1264}
1265
1266impl ColumnMatcherRewriter {
1267 pub fn new(schema: SchemaRef, primary_key_indices: Vec<usize>, allow_partial: bool) -> Self {
1268 Self {
1269 schema: schema.clone(),
1270 sink_schema: schema,
1271 values: BTreeMap::new(),
1272 is_rewritten: false,
1273 primary_key_indices,
1274 allow_partial,
1275 }
1276 }
1277
1278 pub fn new_with_values(
1281 sink_schema: SchemaRef,
1282 primary_key_indices: Vec<usize>,
1283 allow_partial: bool,
1284 values: BTreeMap<String, ScalarValue>,
1285 ) -> Self {
1286 let value_names = values.keys().collect::<BTreeSet<_>>();
1287 let ordinary_columns = sink_schema
1288 .column_schemas()
1289 .iter()
1290 .filter(|column| !value_names.contains(&column.name))
1291 .cloned()
1292 .collect();
1293 let ordinary_schema = Arc::new(datatypes::schema::Schema::new(ordinary_columns));
1294 let ordinary_primary_key_indices = primary_key_indices
1295 .into_iter()
1296 .filter_map(|index| {
1297 let name = &sink_schema.column_schemas()[index].name;
1298 ordinary_schema
1299 .column_schemas()
1300 .iter()
1301 .position(|column| column.name == *name)
1302 })
1303 .collect();
1304 Self {
1305 schema: ordinary_schema,
1306 sink_schema,
1307 values,
1308 is_rewritten: false,
1309 primary_key_indices: ordinary_primary_key_indices,
1310 allow_partial,
1311 }
1312 }
1313
1314 fn validate_values(&self, output_exprs: &[Expr]) -> DfResult<()> {
1315 let output_names = output_exprs
1316 .iter()
1317 .map(|expr| expr.qualified_name().1)
1318 .collect::<Vec<_>>();
1319 let duplicated = duplicate_names(&output_names);
1320 if !duplicated.is_empty() {
1321 return Err(DataFusionError::Plan(format!(
1322 "Flow output schema contains duplicate column(s) {:?}. {}",
1323 duplicated,
1324 format_flow_sink_schema_mismatch(output_exprs, self.sink_schema.as_ref())
1325 )));
1326 }
1327 for (name, value) in &self.values {
1328 let Some(column) = self
1329 .sink_schema
1330 .column_schemas()
1331 .iter()
1332 .find(|column| column.name == *name)
1333 else {
1334 return Err(DataFusionError::Plan(format!(
1335 "Injected flow value column '{name}' is not found in sink schema"
1336 )));
1337 };
1338 if output_names.iter().any(|output| output == name) {
1339 return Err(DataFusionError::Plan(format!(
1340 "Injected flow value column '{name}' collides with flow output"
1341 )));
1342 }
1343 let value_type = ConcreteDataType::from_arrow_type(&value.data_type());
1344 if value_type != column.data_type {
1345 return Err(DataFusionError::Plan(format!(
1346 "Injected flow value column '{name}' has type {:?}, but sink column has type {:?}",
1347 value_type, column.data_type
1348 )));
1349 }
1350 }
1351 Ok(())
1352 }
1353
1354 fn inject_values_in_physical_order(&self, exprs: Vec<Expr>) -> DfResult<Vec<Expr>> {
1355 if self.values.is_empty() {
1356 return Ok(exprs);
1357 }
1358 let mut ordinary = exprs
1359 .into_iter()
1360 .map(|expr| (expr.qualified_name().1, expr))
1361 .collect::<HashMap<_, _>>();
1362 self.sink_schema
1363 .column_schemas()
1364 .iter()
1365 .map(|column| {
1366 if let Some(value) = self.values.get(&column.name) {
1367 Ok(Expr::Literal(value.clone(), None).alias(column.name.clone()))
1368 } else {
1369 ordinary.remove(&column.name).ok_or_else(|| {
1370 DataFusionError::Plan(format!(
1371 "Flow output is missing sink column '{}' after schema matching",
1372 column.name
1373 ))
1374 })
1375 }
1376 })
1377 .collect()
1378 }
1379
1380 fn modify_project_exprs(
1382 &mut self,
1383 mut exprs: Vec<Expr>,
1384 input_schema: &DFSchema,
1385 ) -> DfResult<Vec<Expr>> {
1386 self.validate_values(&exprs)?;
1387 if self.allow_partial {
1388 let exprs = self.modify_project_exprs_with_partial(exprs)?;
1389 return self.inject_values_in_physical_order(exprs);
1390 }
1391
1392 let original_exprs = exprs.clone();
1393
1394 let all_names = self
1395 .schema
1396 .column_schemas()
1397 .iter()
1398 .map(|c| c.name.clone())
1399 .collect::<BTreeSet<_>>();
1400 let query_col_cnt = exprs.len();
1402 let table_col_cnt = self.schema.column_schemas().len();
1403 debug!("query_col_cnt={query_col_cnt}, table_col_cnt={table_col_cnt}");
1404
1405 let placeholder_ts_expr =
1406 datafusion::logical_expr::lit(ScalarValue::TimestampMillisecond(Some(0), None))
1407 .alias(AUTO_CREATED_PLACEHOLDER_TS_COL);
1408
1409 if query_col_cnt == table_col_cnt {
1410 } else if query_col_cnt + 1 == table_col_cnt {
1412 let last_col_schema = self.schema.column_schemas().last().unwrap();
1413
1414 if last_col_schema.name == AUTO_CREATED_PLACEHOLDER_TS_COL
1416 && self.schema.timestamp_index() == Some(table_col_cnt - 1)
1417 {
1418 exprs.push(placeholder_ts_expr);
1419 } else if last_col_schema.data_type.is_timestamp() {
1420 exprs.push(datafusion::prelude::now().alias(&last_col_schema.name));
1422 } else {
1423 return Err(DataFusionError::Plan(format_flow_sink_schema_mismatch(
1424 &original_exprs,
1425 self.schema.as_ref(),
1426 )));
1427 }
1428 } else if query_col_cnt + 2 == table_col_cnt {
1429 let mut col_iter = self.schema.column_schemas().iter().rev();
1430 let last_col_schema = col_iter.next().unwrap();
1431 let second_last_col_schema = col_iter.next().unwrap();
1432 if second_last_col_schema.data_type.is_timestamp() {
1433 exprs.push(datafusion::prelude::now().alias(&second_last_col_schema.name));
1434 } else {
1435 return Err(DataFusionError::Plan(format!(
1436 "Expect the second last column in the table to be timestamp column, found column {} with type {:?}",
1437 second_last_col_schema.name, second_last_col_schema.data_type
1438 )));
1439 }
1440
1441 if last_col_schema.name == AUTO_CREATED_PLACEHOLDER_TS_COL
1442 && self.schema.timestamp_index() == Some(table_col_cnt - 1)
1443 {
1444 exprs.push(placeholder_ts_expr);
1445 } else {
1446 return Err(DataFusionError::Plan(format!(
1447 "Expect timestamp column {}, found {:?}",
1448 AUTO_CREATED_PLACEHOLDER_TS_COL, last_col_schema
1449 )));
1450 }
1451 } else {
1452 return Err(DataFusionError::Plan(format_flow_sink_schema_mismatch(
1453 &original_exprs,
1454 self.schema.as_ref(),
1455 )));
1456 }
1457
1458 self.inject_values_in_physical_order(self.match_extra_output_columns(
1459 exprs,
1460 input_schema,
1461 &original_exprs,
1462 &all_names,
1463 )?)
1464 }
1465
1466 fn match_extra_output_columns(
1477 &self,
1478 mut exprs: Vec<Expr>,
1479 input_schema: &DFSchema,
1480 original_exprs: &[Expr],
1481 all_names: &BTreeSet<String>,
1482 ) -> DfResult<Vec<Expr>> {
1483 let mut output_names = exprs
1484 .iter()
1485 .map(|expr| expr.qualified_name().1)
1486 .collect::<Vec<_>>();
1487 let output_name_set = output_names.iter().cloned().collect::<BTreeSet<_>>();
1488 let extra_expr_indices = output_names
1489 .iter()
1490 .enumerate()
1491 .filter_map(|(idx, name)| (!all_names.contains(name)).then_some(idx))
1492 .collect::<Vec<_>>();
1493 let missing_sink_indices = self
1494 .schema
1495 .column_schemas()
1496 .iter()
1497 .enumerate()
1498 .filter_map(|(idx, column)| (!output_name_set.contains(&column.name)).then_some(idx))
1499 .collect::<Vec<_>>();
1500
1501 if extra_expr_indices.is_empty() && missing_sink_indices.is_empty() {
1502 return Ok(exprs);
1503 }
1504
1505 if extra_expr_indices.len() != missing_sink_indices.len() {
1506 return Err(DataFusionError::Plan(format_flow_sink_schema_mismatch(
1507 original_exprs,
1508 self.schema.as_ref(),
1509 )));
1510 }
1511
1512 let mut positional_matches = Vec::new();
1513 for expr_idx in extra_expr_indices {
1514 if !missing_sink_indices.contains(&expr_idx) {
1515 return Err(DataFusionError::Plan(format_flow_sink_schema_mismatch(
1516 original_exprs,
1517 self.schema.as_ref(),
1518 )));
1519 }
1520
1521 let target_col_schema = &self.schema.column_schemas()[expr_idx];
1522 let expr_type =
1523 ConcreteDataType::from_arrow_type(&exprs[expr_idx].get_type(input_schema)?);
1524 if is_obviously_incompatible_positional_match(&expr_type, &target_col_schema.data_type)
1525 {
1526 return Err(DataFusionError::Plan(format!(
1527 "Cannot match flow output column '{}' to sink column '{}' by position: incompatible data types, flow output type is {:?}, sink column type is {:?}. {}",
1528 output_names[expr_idx],
1529 target_col_schema.name,
1530 expr_type,
1531 target_col_schema.data_type,
1532 format_flow_sink_schema_mismatch(original_exprs, self.schema.as_ref())
1533 )));
1534 }
1535
1536 let target_name = target_col_schema.name.clone();
1537 positional_matches.push(format!(
1538 "{} -> {} (flow output type: {:?}, sink column type: {:?})",
1539 output_names[expr_idx], target_name, expr_type, target_col_schema.data_type
1540 ));
1541 exprs[expr_idx] = exprs[expr_idx].clone().alias(target_name.clone());
1542 output_names[expr_idx] = target_name;
1543 }
1544
1545 if !positional_matches.is_empty() {
1546 debug!(
1547 "Matched flow output columns to sink columns by position: {:?}",
1548 positional_matches
1549 );
1550 }
1551
1552 let duplicated_output_names = duplicate_names(&output_names);
1553 if !duplicated_output_names.is_empty() {
1554 return Err(DataFusionError::Plan(format!(
1555 "Flow output schema contains duplicate column(s) after schema matching {:?}. {}",
1556 duplicated_output_names,
1557 format_flow_sink_schema_mismatch(&exprs, self.schema.as_ref())
1558 )));
1559 }
1560
1561 Ok(exprs)
1562 }
1563
1564 fn modify_project_exprs_with_partial(&mut self, exprs: Vec<Expr>) -> DfResult<Vec<Expr>> {
1565 let table_col_cnt = self.schema.column_schemas().len();
1566 let query_col_cnt = exprs.len();
1567
1568 if query_col_cnt > table_col_cnt {
1569 return Err(DataFusionError::Plan(format_flow_sink_schema_mismatch(
1570 &exprs,
1571 self.schema.as_ref(),
1572 )));
1573 }
1574
1575 let name_to_expr: HashMap<String, Expr> = exprs
1576 .clone()
1577 .into_iter()
1578 .map(|e| (e.qualified_name().1, e))
1579 .collect();
1580
1581 let required_columns = self.required_columns_for_partial();
1582 let missing: Vec<_> = required_columns
1583 .iter()
1584 .filter(|name| !name_to_expr.contains_key(*name))
1585 .cloned()
1586 .collect();
1587 if !missing.is_empty() {
1588 return Err(DataFusionError::Plan(format!(
1589 "Column(s) {:?} required by sink table are missing from flow output when merge_mode=last_non_null. {}",
1590 missing,
1591 format_flow_sink_schema_mismatch(&exprs, self.schema.as_ref())
1592 )));
1593 }
1594
1595 let placeholder_ts_expr =
1596 datafusion::logical_expr::lit(ScalarValue::TimestampMillisecond(Some(0), None))
1597 .alias(AUTO_CREATED_PLACEHOLDER_TS_COL);
1598
1599 let timestamp_index = self.schema.timestamp_index();
1600 let mut remap = name_to_expr;
1601 let mut new_exprs = Vec::with_capacity(table_col_cnt);
1602
1603 for (idx, col_schema) in self.schema.column_schemas().iter().enumerate() {
1604 let col_name = col_schema.name.clone();
1605 if let Some(expr) = remap.remove(&col_name) {
1606 let expr = if expr.qualified_name().1 == col_name {
1607 expr
1608 } else {
1609 expr.alias(col_name.clone())
1610 };
1611 new_exprs.push(expr);
1612 continue;
1613 }
1614
1615 if col_name == AUTO_CREATED_PLACEHOLDER_TS_COL && timestamp_index == Some(idx) {
1616 new_exprs.push(placeholder_ts_expr.clone());
1617 continue;
1618 }
1619
1620 if col_name == AUTO_CREATED_UPDATE_AT_TS_COL && col_schema.data_type.is_timestamp() {
1621 new_exprs.push(datafusion::prelude::now().alias(&col_name));
1622 continue;
1623 }
1624
1625 new_exprs.push(Self::null_expr(col_schema));
1626 }
1627
1628 if !remap.is_empty() {
1629 let extra: Vec<_> = remap.keys().cloned().collect();
1630 return Err(DataFusionError::Plan(format!(
1631 "Flow output has extra column(s) {:?} not found in sink schema when merge_mode=last_non_null. {}",
1632 extra,
1633 format_flow_sink_schema_mismatch(&exprs, self.schema.as_ref())
1634 )));
1635 }
1636
1637 Ok(new_exprs)
1638 }
1639
1640 fn null_expr(col_schema: &ColumnSchema) -> Expr {
1641 Expr::Literal(ScalarValue::Null, None).alias(col_schema.name.clone())
1642 }
1643
1644 fn required_columns_for_partial(&self) -> HashSet<String> {
1645 let mut required = HashSet::new();
1646 for idx in &self.primary_key_indices {
1647 if let Some(col) = self.schema.column_schemas().get(*idx) {
1648 required.insert(col.name.clone());
1649 }
1650 }
1651
1652 if let Some(ts_idx) = self.schema.timestamp_index()
1653 && let Some(col) = self.schema.column_schemas().get(ts_idx)
1654 && col.name != AUTO_CREATED_PLACEHOLDER_TS_COL
1655 {
1656 required.insert(col.name.clone());
1657 }
1658
1659 required
1660 }
1661}
1662
1663fn is_obviously_incompatible_positional_match(
1664 expr_type: &ConcreteDataType,
1665 sink_type: &ConcreteDataType,
1666) -> bool {
1667 if expr_type.is_null() || expr_type == sink_type {
1672 return false;
1673 }
1674
1675 expr_type.is_timestamp() != sink_type.is_timestamp()
1676 || expr_type.is_string() != sink_type.is_string()
1677 || expr_type.is_boolean() != sink_type.is_boolean()
1678 || expr_type.is_json() != sink_type.is_json()
1679 || expr_type.is_vector() != sink_type.is_vector()
1680}
1681
1682fn duplicate_names(names: &[String]) -> Vec<String> {
1683 let mut seen = HashSet::new();
1684 let mut duplicated = BTreeSet::new();
1685 for name in names {
1686 if !seen.insert(name.as_str()) {
1687 duplicated.insert(name.as_str());
1688 }
1689 }
1690 duplicated.into_iter().map(str::to_string).collect()
1691}
1692
1693fn format_flow_sink_schema_mismatch(
1694 query_exprs: &[Expr],
1695 sink_schema: &datatypes::schema::Schema,
1696) -> String {
1697 let flow_output_columns = query_exprs
1698 .iter()
1699 .map(|expr| expr.qualified_name().1)
1700 .collect::<Vec<_>>();
1701 let sink_table_columns = sink_schema
1702 .column_schemas()
1703 .iter()
1704 .map(|col| col.name.clone())
1705 .collect::<Vec<_>>();
1706
1707 let flow_output_set = flow_output_columns.iter().cloned().collect::<HashSet<_>>();
1708 let sink_table_set = sink_table_columns.iter().cloned().collect::<HashSet<_>>();
1709
1710 let mut extra_flow_columns = flow_output_columns
1711 .iter()
1712 .filter(|name| !sink_table_set.contains(*name))
1713 .cloned()
1714 .collect::<Vec<_>>();
1715 extra_flow_columns.sort();
1716 extra_flow_columns.dedup();
1717
1718 let mut missing_sink_columns = sink_table_columns
1719 .iter()
1720 .filter(|name| !flow_output_set.contains(*name))
1721 .cloned()
1722 .collect::<Vec<_>>();
1723 missing_sink_columns.sort();
1724 missing_sink_columns.dedup();
1725
1726 format!(
1727 "Flow output schema does not match sink table schema: found {} flow output columns and {} sink table columns. flow output columns: {:?}, sink table columns: {:?}, extra flow columns not in sink: {:?}, missing sink columns from flow output: {:?}",
1728 flow_output_columns.len(),
1729 sink_table_columns.len(),
1730 flow_output_columns,
1731 sink_table_columns,
1732 extra_flow_columns,
1733 missing_sink_columns
1734 )
1735}
1736
1737impl TreeNodeRewriter for ColumnMatcherRewriter {
1738 type Node = LogicalPlan;
1739 fn f_down(&mut self, mut node: Self::Node) -> DfResult<Transformed<Self::Node>> {
1740 if self.is_rewritten {
1741 return Ok(Transformed::no(node));
1742 }
1743
1744 if let LogicalPlan::Distinct(Distinct::All(_)) = &node {
1746 let mut exprs = vec![];
1747
1748 for field in node.schema().fields().iter() {
1749 exprs.push(Expr::Column(datafusion::common::Column::new_unqualified(
1750 field.name(),
1751 )));
1752 }
1753
1754 let projection =
1755 LogicalPlan::Projection(Projection::try_new(exprs, Arc::new(node.clone()))?);
1756
1757 node = projection;
1758 }
1759 else if let LogicalPlan::TableScan(table_scan) = node {
1761 let mut exprs = vec![];
1762
1763 for field in table_scan.projected_schema.fields().iter() {
1764 exprs.push(Expr::Column(datafusion::common::Column::new(
1765 Some(table_scan.table_name.clone()),
1766 field.name(),
1767 )));
1768 }
1769
1770 let projection = LogicalPlan::Projection(Projection::try_new(
1771 exprs,
1772 Arc::new(LogicalPlan::TableScan(table_scan)),
1773 )?);
1774
1775 node = projection;
1776 }
1777
1778 if let LogicalPlan::Projection(project) = &node {
1782 let exprs = project.expr.clone();
1783 let exprs = self.modify_project_exprs(exprs, project.input.schema())?;
1784
1785 self.is_rewritten = true;
1786 let new_plan =
1787 node.with_new_exprs(exprs, node.inputs().into_iter().cloned().collect())?;
1788 Ok(Transformed::yes(new_plan))
1789 } else {
1790 let mut exprs = vec![];
1792 for field in node.schema().fields().iter() {
1793 exprs.push(Expr::Column(datafusion::common::Column::new_unqualified(
1794 field.name(),
1795 )));
1796 }
1797 let exprs = self.modify_project_exprs(exprs, node.schema())?;
1798 self.is_rewritten = true;
1799 let new_plan =
1800 LogicalPlan::Projection(Projection::try_new(exprs, Arc::new(node.clone()))?);
1801 Ok(Transformed::yes(new_plan))
1802 }
1803 }
1804
1805 fn f_up(&mut self, node: Self::Node) -> DfResult<Transformed<Self::Node>> {
1807 node.recompute_schema().map(Transformed::yes)
1808 }
1809}
1810
1811#[derive(Debug)]
1813pub struct AddFilterRewriter {
1814 extra_filter: Expr,
1815 is_rewritten: bool,
1816}
1817
1818impl AddFilterRewriter {
1819 pub fn new(filter: Expr) -> Self {
1820 Self {
1821 extra_filter: filter,
1822 is_rewritten: false,
1823 }
1824 }
1825}
1826
1827impl TreeNodeRewriter for AddFilterRewriter {
1828 type Node = LogicalPlan;
1829 fn f_up(&mut self, node: Self::Node) -> DfResult<Transformed<Self::Node>> {
1830 if self.is_rewritten {
1831 return Ok(Transformed::no(node));
1832 }
1833 match node {
1834 LogicalPlan::Filter(mut filter) => {
1835 filter.predicate = filter.predicate.and(self.extra_filter.clone());
1836 self.is_rewritten = true;
1837 Ok(Transformed::yes(LogicalPlan::Filter(filter)))
1838 }
1839 LogicalPlan::TableScan(_) => {
1840 let filter =
1842 datafusion_expr::Filter::try_new(self.extra_filter.clone(), Arc::new(node))?;
1843 self.is_rewritten = true;
1844 Ok(Transformed::yes(LogicalPlan::Filter(filter)))
1845 }
1846 _ => Ok(Transformed::no(node)),
1847 }
1848 }
1849}