Skip to main content

flow/batching_mode/
utils.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! some utils for helping with batching mode
16
17use 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/// Describes how one aggregate output field should be merged with the
61/// corresponding existing field in the sink table.
62///
63/// `output_field_name` is the final output/sink schema field name produced by
64/// the delta plan and read from the sink table. It is not a DataFusion `Column`
65/// reference. It may contain dots or other non-identifier characters when the
66/// query keeps DataFusion's raw aggregate output name, e.g.
67/// `max(numbers_with_ts.number)`.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct IncrementalAggregateMergeColumn {
70    /// Delta-plan field containing the aggregate result/state column. Repeated
71    /// projections intentionally share this field while using distinct outputs.
72    pub input_field_name: String,
73    /// Final output/sink field name for the aggregate result/state column.
74    ///
75    pub output_field_name: String,
76    pub merge_op: IncrementalAggregateMergeOp,
77}
78
79impl IncrementalAggregateMergeColumn {
80    /// Create a new merge column whose delta and output fields have the same name.
81    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/// Analysis result for an incremental aggregate plan.
107///
108/// `group_key_names` and each merge column's `output_field_name` are final
109/// output/sink schema field names used to project both the delta plan and the
110/// sink table before the left-join merge. They are not DataFusion logical-plan
111/// `Column` references; callers must attach qualifiers structurally instead of
112/// formatting qualified names as strings.
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct IncrementalAggregateAnalysis {
115    /// Final output/sink field names for group keys used as merge join keys.
116    pub group_key_names: Vec<String>,
117    pub merge_columns: Vec<IncrementalAggregateMergeColumn>,
118    /// Literal output fields that can be passed through from the delta plan.
119    pub literal_columns: Vec<String>,
120    /// Final output field order from the original aggregate plan.
121    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    // Supported final shape: optional output Projection directly over one
179    // Aggregate. Post-aggregate filters (HAVING), ordering, limits,
180    // distinct/window/union/extension nodes are intentionally not accepted.
181    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        // Supported aggregate input shape: optional WHERE filter over a table scan.
206        // SubqueryAlias is a transparent naming wrapper for `FROM table AS alias`.
207        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 expression name and projected output field, in projection order.
225    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                    // Only a direct aggregate output column has the same
264                    // merge semantics as the original resolver. In particular,
265                    // do not mine aggregate columns through CAST/TryCast or
266                    // other output wrappers.
267                    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        // Preserve state-family parameters; value coercion is handled by the aggregate.
362        "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's binary merge form is admitted because its state argument is
384        // already the aggregate result stored by the sink.
385        "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    // qualified_name() returns (Option<String>, String) where the second
402    // element is the unqualified column/alias name. This relies on
403    // DataFusion's internal naming convention: aggregate expressions
404    // emit a column named after the aggregate itself (e.g. "SUM(x)").
405    // Keep every matching projection occurrence because DataFusion can share
406    // one aggregate input field for identical expressions.
407    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                // Auto-created sink columns injected by ColumnMatcherRewriter
441                // are not part of the original aggregate semantics and must
442                // not prevent incremental aggregate rewrites.
443                && 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
600/// Rewrites one incremental aggregate delta plan by left-joining it with the
601/// existing sink-table state and projecting merged aggregate outputs.
602///
603/// For a grouped aggregate such as:
604///
605/// ```text
606/// SELECT max(number) AS number, ts FROM numbers_with_ts GROUP BY ts
607/// ```
608///
609/// the rewrite is roughly:
610///
611/// ```text
612/// delta = SELECT ts, number FROM <delta_plan> AS __flow_delta
613/// sink_scan = SELECT * FROM <sink_table> [WHERE <sink_dirty_filter>]
614/// sink  = SELECT ts, number FROM sink_scan AS __flow_sink
615/// SELECT
616///   CASE
617///     WHEN __flow_sink.number IS NULL THEN __flow_delta.number
618///     WHEN __flow_delta.number >= __flow_sink.number THEN __flow_delta.number
619///     ELSE __flow_sink.number
620///   END AS number,
621///   __flow_delta.ts AS ts
622/// FROM delta
623/// LEFT JOIN sink
624///   ON __flow_delta.ts IS NOT DISTINCT FROM __flow_sink.ts
625/// ```
626///
627/// If `sink_dirty_filter` is provided, it is applied to the sink table scan
628/// before projection, aliasing, and the left join. The predicate must reference
629/// raw sink table columns structurally (unqualified), before the `__flow_sink`
630/// alias exists.
631pub 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
1016/// Convert sql to datafusion logical plan
1017/// Also support TQL (but only Eval not Explain or Analyze)
1018pub 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
1083/// Generate a plan that matches the schema of the sink table
1084/// from given sql by alias and adding auto columns
1085pub(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
1105/// Generate a schema-matching plan with extension-owned typed sink values.
1106pub(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    /// A dialect that forces identifiers to be quoted when they contain
1135    /// anything other than lowercase alphanumerics and underscores, or start
1136    /// with a digit.
1137    ///
1138    /// Unquoted identifiers are normalized to lowercase by the SQL parser, so
1139    /// uppercase letters need quoting to preserve case. Special characters
1140    /// (e.g. ':' in Prometheus-style table names like
1141    /// `kube_pod_cpu_cores:sum`, '.', '-', spaces) and digit-leading names
1142    /// (e.g. `123metrics`) would produce invalid SQL if left unquoted, so they
1143    /// are quoted as well. SQL keywords are intentionally not checked here:
1144    /// quoting every ALL_KEYWORDS member would also quote common column names
1145    /// like `number`; the unparse failure path has an InsertIntoPlan fallback.
1146    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    // first make all column qualified
1159    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/// Helper to find the innermost group by expr in schema, return None if no group by expr
1168#[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                        // get column from field_qualifier, projection and projected_schema:
1197                        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    /// deal with projection when going up with group exprs
1224    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                    // if a alias exist, replace with the new alias
1232                    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/// Optionally add to the final select columns like `update_at` if the sink table has such column
1250/// (which doesn't necessary need to have exact name just need to be a extra timestamp column)
1251/// and `__ts_placeholder`(this column need to have exact this name and be a timestamp)
1252/// with values like `now()` and `0`
1253///
1254/// it also give existing columns alias to column in sink table if needed
1255#[derive(Debug)]
1256pub struct ColumnMatcherRewriter {
1257    /// Schema used to match the ordinary flow output. It excludes injected values.
1258    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    /// Match ordinary output against the sink schema excluding typed values, then inject those
1279    /// values in physical sink-column order.
1280    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    /// modify the exprs in place so that it matches the schema and some auto columns are added
1381    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        // add columns if have different column count
1401        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            // still need to add alias, see below
1411        } else if query_col_cnt + 1 == table_col_cnt {
1412            let last_col_schema = self.schema.column_schemas().last().unwrap();
1413
1414            // if time index column is auto created add it
1415            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                // is the update at column
1421                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    /// Match flow output columns whose names are not in the sink schema by the same position only.
1467    ///
1468    /// This keeps the legacy "omit output aliases and map by position" behavior, but only when the
1469    /// sink column at the same index is actually missing from the flow output. If the extra output
1470    /// would be aliased to a sink column that already exists elsewhere, report a schema mismatch
1471    /// instead of guessing another sink column by type.
1472    ///
1473    /// In particular, this intentionally rejects cross-position remaps like
1474    /// `record_time_window2 -> record_time_window`: they are easy to confuse with real schema
1475    /// mismatches and should be fixed by giving the flow output the sink column name explicitly.
1476    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    // This is a coarse type-family guard for legacy positional aliasing, not a strict type equality
1668    // check. For example, numeric width/sign differences are allowed here and left to downstream
1669    // coercion, and untyped NULL can be coerced to any target type. Clearly different families such
1670    // as timestamp vs string are rejected early.
1671    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 is distinct all, wrap it in a projection
1745        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        // handle table_scan by wrap it in a projection
1760        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        // only do rewrite if found the outermost projection
1779        // if the outermost node is projection, can rewrite the exprs
1780        // if not, wrap it in a projection
1781        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            // wrap the logical plan in a projection
1791            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    /// We might add new columns, so we need to recompute the schema
1806    fn f_up(&mut self, node: Self::Node) -> DfResult<Transformed<Self::Node>> {
1807        node.recompute_schema().map(Transformed::yes)
1808    }
1809}
1810
1811/// Find out the `Filter` Node corresponding to innermost(deepest) `WHERE` and add a new filter expr to it
1812#[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                // add a new filter
1841                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}