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/// Returns whether `expr` directly renames the group by expression `group_expr`.
1168///
1169/// Renames are identified only by expression name. Derived unary expressions are not group
1170/// keys, even when they reference a single group key column. A PromQL `count_values("label",
1171/// metric)` plan lands here too: it groups by the formatted sample value
1172/// (`prom_float_to_string(value)`) and projects that group key column as the generated label.
1173fn is_alias_of_group_expr(group_expr: &Expr, expr: &Expr) -> DfResult<bool> {
1174    Ok(group_expr.name_for_alias()? == expr.name_for_alias()?)
1175}
1176
1177/// Helper to find the innermost group by expr in schema, return None if no group by expr
1178#[derive(Debug, Clone, Default)]
1179pub struct FindGroupByFinalName {
1180    group_exprs: Option<HashSet<datafusion_expr::Expr>>,
1181}
1182
1183impl FindGroupByFinalName {
1184    pub fn get_group_expr_names(&self) -> Option<HashSet<String>> {
1185        self.group_exprs
1186            .as_ref()
1187            .map(|exprs| exprs.iter().map(|expr| expr.qualified_name().1).collect())
1188    }
1189}
1190
1191impl TreeNodeVisitor<'_> for FindGroupByFinalName {
1192    type Node = LogicalPlan;
1193
1194    fn f_down(&mut self, node: &Self::Node) -> datafusion_common::Result<TreeNodeRecursion> {
1195        if let LogicalPlan::Aggregate(aggregate) = node {
1196            self.group_exprs = Some(aggregate.group_expr.iter().cloned().collect());
1197            debug!(
1198                "FindGroupByFinalName: Get Group by exprs from Aggregate: {:?}",
1199                self.group_exprs
1200            );
1201        } else if let LogicalPlan::Distinct(distinct) = node {
1202            debug!("FindGroupByFinalName: Distinct: {}", node);
1203            match distinct {
1204                Distinct::All(input) => {
1205                    if let LogicalPlan::TableScan(table_scan) = &**input {
1206                        // get column from field_qualifier, projection and projected_schema:
1207                        let len = table_scan.projected_schema.fields().len();
1208                        let columns = (0..len)
1209                            .map(|f| {
1210                                let (qualifier, field) =
1211                                    table_scan.projected_schema.qualified_field(f);
1212                                datafusion_common::Column::new(qualifier.cloned(), field.name())
1213                            })
1214                            .map(datafusion_expr::Expr::Column);
1215                        self.group_exprs = Some(columns.collect());
1216                    } else {
1217                        self.group_exprs = Some(input.expressions().iter().cloned().collect())
1218                    }
1219                }
1220                Distinct::On(distinct_on) => {
1221                    self.group_exprs = Some(distinct_on.on_expr.iter().cloned().collect())
1222                }
1223            }
1224            debug!(
1225                "FindGroupByFinalName: Get Group by exprs from Distinct: {:?}",
1226                self.group_exprs
1227            );
1228        }
1229
1230        Ok(TreeNodeRecursion::Continue)
1231    }
1232
1233    /// Applies only direct, name-matched aliases when propagating group expressions upward.
1234    fn f_up(&mut self, node: &Self::Node) -> datafusion_common::Result<TreeNodeRecursion> {
1235        if let LogicalPlan::Projection(projection) = node {
1236            // Only direct aliases of group expressions rename group keys. Derived unary
1237            // expressions remain ordinary projected columns.
1238            for expr in &projection.expr {
1239                let Some(group_exprs) = &mut self.group_exprs else {
1240                    return Ok(TreeNodeRecursion::Continue);
1241                };
1242                if let datafusion_expr::Expr::Alias(alias) = expr {
1243                    // if a alias exist, replace with the new alias
1244                    let mut renamed_group_expr = None;
1245                    for group_expr in group_exprs.iter() {
1246                        if is_alias_of_group_expr(group_expr, alias.expr.as_ref())? {
1247                            renamed_group_expr = Some(group_expr.clone());
1248                            break;
1249                        }
1250                    }
1251                    if let Some(group_expr) = renamed_group_expr {
1252                        group_exprs.remove(&group_expr);
1253                        group_exprs.insert(expr.clone());
1254                    }
1255                }
1256            }
1257        }
1258        debug!("Aliased group by exprs: {:?}", self.group_exprs);
1259        Ok(TreeNodeRecursion::Continue)
1260    }
1261}
1262
1263/// Optionally add to the final select columns like `update_at` if the sink table has such column
1264/// (which doesn't necessary need to have exact name just need to be a extra timestamp column)
1265/// and `__ts_placeholder`(this column need to have exact this name and be a timestamp)
1266/// with values like `now()` and `0`
1267///
1268/// it also give existing columns alias to column in sink table if needed
1269#[derive(Debug)]
1270pub struct ColumnMatcherRewriter {
1271    /// Schema used to match the ordinary flow output. It excludes injected values.
1272    pub schema: SchemaRef,
1273    sink_schema: SchemaRef,
1274    values: BTreeMap<String, ScalarValue>,
1275    pub is_rewritten: bool,
1276    pub primary_key_indices: Vec<usize>,
1277    pub allow_partial: bool,
1278}
1279
1280impl ColumnMatcherRewriter {
1281    pub fn new(schema: SchemaRef, primary_key_indices: Vec<usize>, allow_partial: bool) -> Self {
1282        Self {
1283            schema: schema.clone(),
1284            sink_schema: schema,
1285            values: BTreeMap::new(),
1286            is_rewritten: false,
1287            primary_key_indices,
1288            allow_partial,
1289        }
1290    }
1291
1292    /// Match ordinary output against the sink schema excluding typed values, then inject those
1293    /// values in physical sink-column order.
1294    pub fn new_with_values(
1295        sink_schema: SchemaRef,
1296        primary_key_indices: Vec<usize>,
1297        allow_partial: bool,
1298        values: BTreeMap<String, ScalarValue>,
1299    ) -> Self {
1300        let value_names = values.keys().collect::<BTreeSet<_>>();
1301        let ordinary_columns = sink_schema
1302            .column_schemas()
1303            .iter()
1304            .filter(|column| !value_names.contains(&column.name))
1305            .cloned()
1306            .collect();
1307        let ordinary_schema = Arc::new(datatypes::schema::Schema::new(ordinary_columns));
1308        let ordinary_primary_key_indices = primary_key_indices
1309            .into_iter()
1310            .filter_map(|index| {
1311                let name = &sink_schema.column_schemas()[index].name;
1312                ordinary_schema
1313                    .column_schemas()
1314                    .iter()
1315                    .position(|column| column.name == *name)
1316            })
1317            .collect();
1318        Self {
1319            schema: ordinary_schema,
1320            sink_schema,
1321            values,
1322            is_rewritten: false,
1323            primary_key_indices: ordinary_primary_key_indices,
1324            allow_partial,
1325        }
1326    }
1327
1328    fn validate_values(&self, output_exprs: &[Expr]) -> DfResult<()> {
1329        let output_names = output_exprs
1330            .iter()
1331            .map(|expr| expr.qualified_name().1)
1332            .collect::<Vec<_>>();
1333        let duplicated = duplicate_names(&output_names);
1334        if !duplicated.is_empty() {
1335            return Err(DataFusionError::Plan(format!(
1336                "Flow output schema contains duplicate column(s) {:?}. {}",
1337                duplicated,
1338                format_flow_sink_schema_mismatch(output_exprs, self.sink_schema.as_ref())
1339            )));
1340        }
1341        for (name, value) in &self.values {
1342            let Some(column) = self
1343                .sink_schema
1344                .column_schemas()
1345                .iter()
1346                .find(|column| column.name == *name)
1347            else {
1348                return Err(DataFusionError::Plan(format!(
1349                    "Injected flow value column '{name}' is not found in sink schema"
1350                )));
1351            };
1352            if output_names.iter().any(|output| output == name) {
1353                return Err(DataFusionError::Plan(format!(
1354                    "Injected flow value column '{name}' collides with flow output"
1355                )));
1356            }
1357            let value_type = ConcreteDataType::from_arrow_type(&value.data_type());
1358            if value_type != column.data_type {
1359                return Err(DataFusionError::Plan(format!(
1360                    "Injected flow value column '{name}' has type {:?}, but sink column has type {:?}",
1361                    value_type, column.data_type
1362                )));
1363            }
1364        }
1365        Ok(())
1366    }
1367
1368    fn inject_values_in_physical_order(&self, exprs: Vec<Expr>) -> DfResult<Vec<Expr>> {
1369        if self.values.is_empty() {
1370            return Ok(exprs);
1371        }
1372        let mut ordinary = exprs
1373            .into_iter()
1374            .map(|expr| (expr.qualified_name().1, expr))
1375            .collect::<HashMap<_, _>>();
1376        self.sink_schema
1377            .column_schemas()
1378            .iter()
1379            .map(|column| {
1380                if let Some(value) = self.values.get(&column.name) {
1381                    Ok(Expr::Literal(value.clone(), None).alias(column.name.clone()))
1382                } else {
1383                    ordinary.remove(&column.name).ok_or_else(|| {
1384                        DataFusionError::Plan(format!(
1385                            "Flow output is missing sink column '{}' after schema matching",
1386                            column.name
1387                        ))
1388                    })
1389                }
1390            })
1391            .collect()
1392    }
1393
1394    /// modify the exprs in place so that it matches the schema and some auto columns are added
1395    fn modify_project_exprs(
1396        &mut self,
1397        mut exprs: Vec<Expr>,
1398        input_schema: &DFSchema,
1399    ) -> DfResult<Vec<Expr>> {
1400        self.validate_values(&exprs)?;
1401        if self.allow_partial {
1402            let exprs = self.modify_project_exprs_with_partial(exprs)?;
1403            return self.inject_values_in_physical_order(exprs);
1404        }
1405
1406        let original_exprs = exprs.clone();
1407
1408        let all_names = self
1409            .schema
1410            .column_schemas()
1411            .iter()
1412            .map(|c| c.name.clone())
1413            .collect::<BTreeSet<_>>();
1414        // add columns if have different column count
1415        let query_col_cnt = exprs.len();
1416        let table_col_cnt = self.schema.column_schemas().len();
1417        debug!("query_col_cnt={query_col_cnt}, table_col_cnt={table_col_cnt}");
1418
1419        let placeholder_ts_expr =
1420            datafusion::logical_expr::lit(ScalarValue::TimestampMillisecond(Some(0), None))
1421                .alias(AUTO_CREATED_PLACEHOLDER_TS_COL);
1422
1423        if query_col_cnt == table_col_cnt {
1424            // still need to add alias, see below
1425        } else if query_col_cnt + 1 == table_col_cnt {
1426            let last_col_schema = self.schema.column_schemas().last().unwrap();
1427
1428            // if time index column is auto created add it
1429            if last_col_schema.name == AUTO_CREATED_PLACEHOLDER_TS_COL
1430                && self.schema.timestamp_index() == Some(table_col_cnt - 1)
1431            {
1432                exprs.push(placeholder_ts_expr);
1433            } else if last_col_schema.data_type.is_timestamp() {
1434                // is the update at column
1435                exprs.push(datafusion::prelude::now().alias(&last_col_schema.name));
1436            } else {
1437                return Err(DataFusionError::Plan(format_flow_sink_schema_mismatch(
1438                    &original_exprs,
1439                    self.schema.as_ref(),
1440                )));
1441            }
1442        } else if query_col_cnt + 2 == table_col_cnt {
1443            let mut col_iter = self.schema.column_schemas().iter().rev();
1444            let last_col_schema = col_iter.next().unwrap();
1445            let second_last_col_schema = col_iter.next().unwrap();
1446            if second_last_col_schema.data_type.is_timestamp() {
1447                exprs.push(datafusion::prelude::now().alias(&second_last_col_schema.name));
1448            } else {
1449                return Err(DataFusionError::Plan(format!(
1450                    "Expect the second last column in the table to be timestamp column, found column {} with type {:?}",
1451                    second_last_col_schema.name, second_last_col_schema.data_type
1452                )));
1453            }
1454
1455            if last_col_schema.name == AUTO_CREATED_PLACEHOLDER_TS_COL
1456                && self.schema.timestamp_index() == Some(table_col_cnt - 1)
1457            {
1458                exprs.push(placeholder_ts_expr);
1459            } else {
1460                return Err(DataFusionError::Plan(format!(
1461                    "Expect timestamp column {}, found {:?}",
1462                    AUTO_CREATED_PLACEHOLDER_TS_COL, last_col_schema
1463                )));
1464            }
1465        } else {
1466            return Err(DataFusionError::Plan(format_flow_sink_schema_mismatch(
1467                &original_exprs,
1468                self.schema.as_ref(),
1469            )));
1470        }
1471
1472        self.inject_values_in_physical_order(self.match_extra_output_columns(
1473            exprs,
1474            input_schema,
1475            &original_exprs,
1476            &all_names,
1477        )?)
1478    }
1479
1480    /// Match flow output columns whose names are not in the sink schema by the same position only.
1481    ///
1482    /// This keeps the legacy "omit output aliases and map by position" behavior, but only when the
1483    /// sink column at the same index is actually missing from the flow output. If the extra output
1484    /// would be aliased to a sink column that already exists elsewhere, report a schema mismatch
1485    /// instead of guessing another sink column by type.
1486    ///
1487    /// In particular, this intentionally rejects cross-position remaps like
1488    /// `record_time_window2 -> record_time_window`: they are easy to confuse with real schema
1489    /// mismatches and should be fixed by giving the flow output the sink column name explicitly.
1490    fn match_extra_output_columns(
1491        &self,
1492        mut exprs: Vec<Expr>,
1493        input_schema: &DFSchema,
1494        original_exprs: &[Expr],
1495        all_names: &BTreeSet<String>,
1496    ) -> DfResult<Vec<Expr>> {
1497        let mut output_names = exprs
1498            .iter()
1499            .map(|expr| expr.qualified_name().1)
1500            .collect::<Vec<_>>();
1501        let output_name_set = output_names.iter().cloned().collect::<BTreeSet<_>>();
1502        let extra_expr_indices = output_names
1503            .iter()
1504            .enumerate()
1505            .filter_map(|(idx, name)| (!all_names.contains(name)).then_some(idx))
1506            .collect::<Vec<_>>();
1507        let missing_sink_indices = self
1508            .schema
1509            .column_schemas()
1510            .iter()
1511            .enumerate()
1512            .filter_map(|(idx, column)| (!output_name_set.contains(&column.name)).then_some(idx))
1513            .collect::<Vec<_>>();
1514
1515        if extra_expr_indices.is_empty() && missing_sink_indices.is_empty() {
1516            return Ok(exprs);
1517        }
1518
1519        if extra_expr_indices.len() != missing_sink_indices.len() {
1520            return Err(DataFusionError::Plan(format_flow_sink_schema_mismatch(
1521                original_exprs,
1522                self.schema.as_ref(),
1523            )));
1524        }
1525
1526        let mut positional_matches = Vec::new();
1527        for expr_idx in extra_expr_indices {
1528            if !missing_sink_indices.contains(&expr_idx) {
1529                return Err(DataFusionError::Plan(format_flow_sink_schema_mismatch(
1530                    original_exprs,
1531                    self.schema.as_ref(),
1532                )));
1533            }
1534
1535            let target_col_schema = &self.schema.column_schemas()[expr_idx];
1536            let expr_type =
1537                ConcreteDataType::from_arrow_type(&exprs[expr_idx].get_type(input_schema)?);
1538            if is_obviously_incompatible_positional_match(&expr_type, &target_col_schema.data_type)
1539            {
1540                return Err(DataFusionError::Plan(format!(
1541                    "Cannot match flow output column '{}' to sink column '{}' by position: incompatible data types, flow output type is {:?}, sink column type is {:?}. {}",
1542                    output_names[expr_idx],
1543                    target_col_schema.name,
1544                    expr_type,
1545                    target_col_schema.data_type,
1546                    format_flow_sink_schema_mismatch(original_exprs, self.schema.as_ref())
1547                )));
1548            }
1549
1550            let target_name = target_col_schema.name.clone();
1551            positional_matches.push(format!(
1552                "{} -> {} (flow output type: {:?}, sink column type: {:?})",
1553                output_names[expr_idx], target_name, expr_type, target_col_schema.data_type
1554            ));
1555            exprs[expr_idx] = exprs[expr_idx].clone().alias(target_name.clone());
1556            output_names[expr_idx] = target_name;
1557        }
1558
1559        if !positional_matches.is_empty() {
1560            debug!(
1561                "Matched flow output columns to sink columns by position: {:?}",
1562                positional_matches
1563            );
1564        }
1565
1566        let duplicated_output_names = duplicate_names(&output_names);
1567        if !duplicated_output_names.is_empty() {
1568            return Err(DataFusionError::Plan(format!(
1569                "Flow output schema contains duplicate column(s) after schema matching {:?}. {}",
1570                duplicated_output_names,
1571                format_flow_sink_schema_mismatch(&exprs, self.schema.as_ref())
1572            )));
1573        }
1574
1575        Ok(exprs)
1576    }
1577
1578    fn modify_project_exprs_with_partial(&mut self, exprs: Vec<Expr>) -> DfResult<Vec<Expr>> {
1579        let table_col_cnt = self.schema.column_schemas().len();
1580        let query_col_cnt = exprs.len();
1581
1582        if query_col_cnt > table_col_cnt {
1583            return Err(DataFusionError::Plan(format_flow_sink_schema_mismatch(
1584                &exprs,
1585                self.schema.as_ref(),
1586            )));
1587        }
1588
1589        let name_to_expr: HashMap<String, Expr> = exprs
1590            .clone()
1591            .into_iter()
1592            .map(|e| (e.qualified_name().1, e))
1593            .collect();
1594
1595        let required_columns = self.required_columns_for_partial();
1596        let missing: Vec<_> = required_columns
1597            .iter()
1598            .filter(|name| !name_to_expr.contains_key(*name))
1599            .cloned()
1600            .collect();
1601        if !missing.is_empty() {
1602            return Err(DataFusionError::Plan(format!(
1603                "Column(s) {:?} required by sink table are missing from flow output when merge_mode=last_non_null. {}",
1604                missing,
1605                format_flow_sink_schema_mismatch(&exprs, self.schema.as_ref())
1606            )));
1607        }
1608
1609        let placeholder_ts_expr =
1610            datafusion::logical_expr::lit(ScalarValue::TimestampMillisecond(Some(0), None))
1611                .alias(AUTO_CREATED_PLACEHOLDER_TS_COL);
1612
1613        let timestamp_index = self.schema.timestamp_index();
1614        let mut remap = name_to_expr;
1615        let mut new_exprs = Vec::with_capacity(table_col_cnt);
1616
1617        for (idx, col_schema) in self.schema.column_schemas().iter().enumerate() {
1618            let col_name = col_schema.name.clone();
1619            if let Some(expr) = remap.remove(&col_name) {
1620                let expr = if expr.qualified_name().1 == col_name {
1621                    expr
1622                } else {
1623                    expr.alias(col_name.clone())
1624                };
1625                new_exprs.push(expr);
1626                continue;
1627            }
1628
1629            if col_name == AUTO_CREATED_PLACEHOLDER_TS_COL && timestamp_index == Some(idx) {
1630                new_exprs.push(placeholder_ts_expr.clone());
1631                continue;
1632            }
1633
1634            if col_name == AUTO_CREATED_UPDATE_AT_TS_COL && col_schema.data_type.is_timestamp() {
1635                new_exprs.push(datafusion::prelude::now().alias(&col_name));
1636                continue;
1637            }
1638
1639            new_exprs.push(Self::null_expr(col_schema));
1640        }
1641
1642        if !remap.is_empty() {
1643            let extra: Vec<_> = remap.keys().cloned().collect();
1644            return Err(DataFusionError::Plan(format!(
1645                "Flow output has extra column(s) {:?} not found in sink schema when merge_mode=last_non_null. {}",
1646                extra,
1647                format_flow_sink_schema_mismatch(&exprs, self.schema.as_ref())
1648            )));
1649        }
1650
1651        Ok(new_exprs)
1652    }
1653
1654    fn null_expr(col_schema: &ColumnSchema) -> Expr {
1655        Expr::Literal(ScalarValue::Null, None).alias(col_schema.name.clone())
1656    }
1657
1658    fn required_columns_for_partial(&self) -> HashSet<String> {
1659        let mut required = HashSet::new();
1660        for idx in &self.primary_key_indices {
1661            if let Some(col) = self.schema.column_schemas().get(*idx) {
1662                required.insert(col.name.clone());
1663            }
1664        }
1665
1666        if let Some(ts_idx) = self.schema.timestamp_index()
1667            && let Some(col) = self.schema.column_schemas().get(ts_idx)
1668            && col.name != AUTO_CREATED_PLACEHOLDER_TS_COL
1669        {
1670            required.insert(col.name.clone());
1671        }
1672
1673        required
1674    }
1675}
1676
1677fn is_obviously_incompatible_positional_match(
1678    expr_type: &ConcreteDataType,
1679    sink_type: &ConcreteDataType,
1680) -> bool {
1681    // This is a coarse type-family guard for legacy positional aliasing, not a strict type equality
1682    // check. For example, numeric width/sign differences are allowed here and left to downstream
1683    // coercion, and untyped NULL can be coerced to any target type. Clearly different families such
1684    // as timestamp vs string are rejected early.
1685    if expr_type.is_null() || expr_type == sink_type {
1686        return false;
1687    }
1688
1689    expr_type.is_timestamp() != sink_type.is_timestamp()
1690        || expr_type.is_string() != sink_type.is_string()
1691        || expr_type.is_boolean() != sink_type.is_boolean()
1692        || expr_type.is_json() != sink_type.is_json()
1693        || expr_type.is_vector() != sink_type.is_vector()
1694}
1695
1696fn duplicate_names(names: &[String]) -> Vec<String> {
1697    let mut seen = HashSet::new();
1698    let mut duplicated = BTreeSet::new();
1699    for name in names {
1700        if !seen.insert(name.as_str()) {
1701            duplicated.insert(name.as_str());
1702        }
1703    }
1704    duplicated.into_iter().map(str::to_string).collect()
1705}
1706
1707fn format_flow_sink_schema_mismatch(
1708    query_exprs: &[Expr],
1709    sink_schema: &datatypes::schema::Schema,
1710) -> String {
1711    let flow_output_columns = query_exprs
1712        .iter()
1713        .map(|expr| expr.qualified_name().1)
1714        .collect::<Vec<_>>();
1715    let sink_table_columns = sink_schema
1716        .column_schemas()
1717        .iter()
1718        .map(|col| col.name.clone())
1719        .collect::<Vec<_>>();
1720
1721    let flow_output_set = flow_output_columns.iter().cloned().collect::<HashSet<_>>();
1722    let sink_table_set = sink_table_columns.iter().cloned().collect::<HashSet<_>>();
1723
1724    let mut extra_flow_columns = flow_output_columns
1725        .iter()
1726        .filter(|name| !sink_table_set.contains(*name))
1727        .cloned()
1728        .collect::<Vec<_>>();
1729    extra_flow_columns.sort();
1730    extra_flow_columns.dedup();
1731
1732    let mut missing_sink_columns = sink_table_columns
1733        .iter()
1734        .filter(|name| !flow_output_set.contains(*name))
1735        .cloned()
1736        .collect::<Vec<_>>();
1737    missing_sink_columns.sort();
1738    missing_sink_columns.dedup();
1739
1740    format!(
1741        "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: {:?}",
1742        flow_output_columns.len(),
1743        sink_table_columns.len(),
1744        flow_output_columns,
1745        sink_table_columns,
1746        extra_flow_columns,
1747        missing_sink_columns
1748    )
1749}
1750
1751impl TreeNodeRewriter for ColumnMatcherRewriter {
1752    type Node = LogicalPlan;
1753    fn f_down(&mut self, mut node: Self::Node) -> DfResult<Transformed<Self::Node>> {
1754        if self.is_rewritten {
1755            return Ok(Transformed::no(node));
1756        }
1757
1758        // if is distinct all, wrap it in a projection
1759        if let LogicalPlan::Distinct(Distinct::All(_)) = &node {
1760            let mut exprs = vec![];
1761
1762            for field in node.schema().fields().iter() {
1763                exprs.push(Expr::Column(datafusion::common::Column::new_unqualified(
1764                    field.name(),
1765                )));
1766            }
1767
1768            let projection =
1769                LogicalPlan::Projection(Projection::try_new(exprs, Arc::new(node.clone()))?);
1770
1771            node = projection;
1772        }
1773        // handle table_scan by wrap it in a projection
1774        else if let LogicalPlan::TableScan(table_scan) = node {
1775            let mut exprs = vec![];
1776
1777            for field in table_scan.projected_schema.fields().iter() {
1778                exprs.push(Expr::Column(datafusion::common::Column::new(
1779                    Some(table_scan.table_name.clone()),
1780                    field.name(),
1781                )));
1782            }
1783
1784            let projection = LogicalPlan::Projection(Projection::try_new(
1785                exprs,
1786                Arc::new(LogicalPlan::TableScan(table_scan)),
1787            )?);
1788
1789            node = projection;
1790        }
1791
1792        // only do rewrite if found the outermost projection
1793        // if the outermost node is projection, can rewrite the exprs
1794        // if not, wrap it in a projection
1795        if let LogicalPlan::Projection(project) = &node {
1796            let exprs = project.expr.clone();
1797            let exprs = self.modify_project_exprs(exprs, project.input.schema())?;
1798
1799            self.is_rewritten = true;
1800            let new_plan =
1801                node.with_new_exprs(exprs, node.inputs().into_iter().cloned().collect())?;
1802            Ok(Transformed::yes(new_plan))
1803        } else {
1804            // wrap the logical plan in a projection
1805            let mut exprs = vec![];
1806            for field in node.schema().fields().iter() {
1807                exprs.push(Expr::Column(datafusion::common::Column::new_unqualified(
1808                    field.name(),
1809                )));
1810            }
1811            let exprs = self.modify_project_exprs(exprs, node.schema())?;
1812            self.is_rewritten = true;
1813            let new_plan =
1814                LogicalPlan::Projection(Projection::try_new(exprs, Arc::new(node.clone()))?);
1815            Ok(Transformed::yes(new_plan))
1816        }
1817    }
1818
1819    /// We might add new columns, so we need to recompute the schema
1820    fn f_up(&mut self, node: Self::Node) -> DfResult<Transformed<Self::Node>> {
1821        node.recompute_schema().map(Transformed::yes)
1822    }
1823}
1824
1825/// Find out the `Filter` Node corresponding to innermost(deepest) `WHERE` and add a new filter expr to it
1826#[derive(Debug)]
1827pub struct AddFilterRewriter {
1828    extra_filter: Expr,
1829    is_rewritten: bool,
1830}
1831
1832impl AddFilterRewriter {
1833    pub fn new(filter: Expr) -> Self {
1834        Self {
1835            extra_filter: filter,
1836            is_rewritten: false,
1837        }
1838    }
1839}
1840
1841impl TreeNodeRewriter for AddFilterRewriter {
1842    type Node = LogicalPlan;
1843    fn f_up(&mut self, node: Self::Node) -> DfResult<Transformed<Self::Node>> {
1844        if self.is_rewritten {
1845            return Ok(Transformed::no(node));
1846        }
1847        match node {
1848            LogicalPlan::Filter(mut filter) => {
1849                filter.predicate = filter.predicate.and(self.extra_filter.clone());
1850                self.is_rewritten = true;
1851                Ok(Transformed::yes(LogicalPlan::Filter(filter)))
1852            }
1853            LogicalPlan::TableScan(_) => {
1854                // add a new filter
1855                let filter =
1856                    datafusion_expr::Filter::try_new(self.extra_filter.clone(), Arc::new(node))?;
1857                self.is_rewritten = true;
1858                Ok(Transformed::yes(LogicalPlan::Filter(filter)))
1859            }
1860            _ => Ok(Transformed::no(node)),
1861        }
1862    }
1863}