Skip to main content

uqa_sql/binding/
syntax_sites.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Bound copies of stored syntax, read back in the order of that syntax. Catalog-owned expressions and statements are stored as syntax and bound by lowering a copy; binding keeps the copy's shape, so reading the lowered copy and its bound form side by side, in the order the stored syntax visitor walks the syntax, pairs every routine call, cast and `unknown` literal with the syntax node it came from. The lowered copy tells which bound nodes stand for syntax literals: variable binding may replace column references by typed placeholders, which are not syntax.
8
9use super::stored_routines::BoundRoutineReference;
10use crate::plan::CommandPlan;
11use crate::plan::{CtePlanBody, ExpressionPlan, QueryPlan, RelationalPlan, SourcePlan};
12use crate::{SQLError, ScalarExpr, ScalarFrameBound};
13use uqa_core::Value;
14
15/// An expression site of stored syntax and what binding recorded for it.
16#[derive(Debug, Clone, PartialEq)]
17pub enum ValueSite {
18    /// A collation clause removed by the selected operand coercion.
19    DiscardCollation,
20    /// A ROW DISTINCT predicate expanded into its selected field comparisons.
21    RowDistinct,
22    /// Positions grouped into a scalar-array comparison; the other IN items become individual comparisons.
23    Membership { array_items: Vec<usize> },
24    /// A cast, with the type name binding gave it.
25    Cast(String),
26    /// A ROW constructor bound to its original composite attribute positions.
27    CompositeRow(crate::ast::CompositeRowBinding),
28    /// An `unknown` literal that binding left unconverted.
29    Literal,
30    /// An `unknown` literal that binding converted to a typed constant.
31    Constant { value: Value, ty: String },
32    /// A cast binding wrapped around the syntax, coercing an operand to the type its operator declares, as a `RelabelType` or an implicit coercion does.
33    Relabel(String),
34    /// A function's written ordering syntax, including a legacy call whose selected binding recovered the distinction.
35    FunctionOrder(crate::ast::FunctionOrderSyntax),
36    /// Any other node of stored syntax, which keeps the sites in step with the syntax so that a relabel reaches the node it wraps.
37    Node,
38}
39
40/// The casts a node begins with.
41fn cast_depth(expression: &ScalarExpr) -> usize {
42    let mut depth = 0;
43    let mut node = expression;
44    while let ScalarExpr::Cast { expr, .. } = node {
45        depth += 1;
46        node = expr;
47    }
48    depth
49}
50
51/// Everything binding recorded for one piece of stored syntax, in syntax order.
52#[derive(Debug, Default)]
53pub struct SyntaxSites {
54    /// Routine calls, each reported after its arguments as the syntax visitor reports calls.
55    pub routines: Vec<BoundRoutineReference>,
56    /// Casts, `unknown` literals and function ordering syntax in pre-order.
57    pub values: Vec<ValueSite>,
58}
59
60/// Sites of a stored expression: `lowered` is the plan lowered from the syntax and `bound` its bound copy. The expression visitor reads a site at every node, so the casts binding adds around a node reach it as relabels.
61pub fn expression_syntax_sites(
62    lowered: &ExpressionPlan,
63    bound: &ExpressionPlan,
64) -> Result<SyntaxSites, SQLError> {
65    let mut walk = Walk::default();
66    walk.scalar(
67        &lowered.scalar,
68        &bound.scalar,
69        (&lowered.subqueries, &bound.subqueries),
70    )?;
71    Ok(walk.sites)
72}
73
74/// Sites of a stored statement lowered to a query.
75pub fn query_syntax_sites(lowered: &QueryPlan, bound: &QueryPlan) -> Result<SyntaxSites, SQLError> {
76    let mut walk = Walk::default();
77    walk.query(lowered, bound, false)?;
78    Ok(walk.sites)
79}
80
81/// Sites from the query adapter for a stored command; its final output is synthetic and has no syntax node.
82pub(super) fn command_query_syntax_sites(
83    lowered: &QueryPlan,
84    bound: &QueryPlan,
85) -> Result<SyntaxSites, SQLError> {
86    let mut walk = Walk::default();
87    walk.query(lowered, bound, true)?;
88    Ok(walk.sites)
89}
90
91fn syntax_projections(
92    projections: &[crate::plan::ProjectionPlan],
93    command_root: bool,
94) -> Result<&[crate::plan::ProjectionPlan], SQLError> {
95    if command_root {
96        projections
97            .split_last()
98            .map(|(_, syntax)| syntax)
99            .ok_or_else(|| shape_error("command output"))
100    } else {
101        Ok(projections)
102    }
103}
104
105type Subqueries<'a> = (&'a [QueryPlan], &'a [QueryPlan]);
106
107fn shape_error(what: &str) -> SQLError {
108    SQLError::Internal(format!("bound stored syntax no longer matches its {what}"))
109}
110
111fn pairs<'a, T>(
112    lowered: &'a [T],
113    bound: &'a [T],
114    what: &str,
115) -> Result<impl Iterator<Item = (&'a T, &'a T)>, SQLError> {
116    if lowered.len() != bound.len() {
117        return Err(shape_error(what));
118    }
119    Ok(lowered.iter().zip(bound))
120}
121
122fn optional_pair<'a, T>(
123    lowered: Option<&'a T>,
124    bound: Option<&'a T>,
125    what: &str,
126) -> Result<Option<(&'a T, &'a T)>, SQLError> {
127    match (lowered, bound) {
128        (Some(lowered), Some(bound)) => Ok(Some((lowered, bound))),
129        (None, None) => Ok(None),
130        _ => Err(shape_error(what)),
131    }
132}
133
134#[derive(Default)]
135struct Walk {
136    sites: SyntaxSites,
137}
138
139impl Walk {
140    fn query(
141        &mut self,
142        lowered: &QueryPlan,
143        bound: &QueryPlan,
144        command_root: bool,
145    ) -> Result<(), SQLError> {
146        for (lowered, bound) in pairs(&lowered.ctes, &bound.ctes, "common table expressions")? {
147            self.cte_body(&lowered.body, &bound.body)?;
148            if let Some((lowered, bound)) =
149                optional_pair(lowered.cycle.as_ref(), bound.cycle.as_ref(), "CYCLE clause")?
150            {
151                self.scalar(&lowered.mark_value, &bound.mark_value, (&[], &[]))?;
152                self.scalar(&lowered.mark_default, &bound.mark_default, (&[], &[]))?;
153            }
154        }
155        match (&lowered.root, &bound.root) {
156            (RelationalPlan::QueryBlock(lowered), RelationalPlan::QueryBlock(bound)) => {
157                let subqueries = (&lowered.subqueries[..], &bound.subqueries[..]);
158                if let Some((lowered, bound)) =
159                    optional_pair(lowered.from.as_ref(), bound.from.as_ref(), "FROM clause")?
160                {
161                    self.source(lowered, bound, subqueries)?;
162                }
163                for (lowered, bound) in pairs(
164                    syntax_projections(&lowered.projections, command_root)?,
165                    syntax_projections(&bound.projections, command_root)?,
166                    "select list",
167                )? {
168                    self.scalar(&lowered.expr, &bound.expr, subqueries)?;
169                }
170                self.optional(lowered.r#where.as_ref(), bound.r#where.as_ref(), subqueries)?;
171                self.scalars(&lowered.group_by, &bound.group_by, subqueries)?;
172                if lowered.grouping_sets.len() != bound.grouping_sets.len() {
173                    return Err(shape_error("grouping sets"));
174                }
175                for (lowered, bound) in lowered.grouping_sets.iter().zip(&bound.grouping_sets) {
176                    self.scalars(lowered, bound, subqueries)?;
177                }
178                self.optional(lowered.having.as_ref(), bound.having.as_ref(), subqueries)?;
179                for (lowered, bound) in pairs(&lowered.order_by, &bound.order_by, "ORDER BY")? {
180                    self.scalar(&lowered.expr, &bound.expr, subqueries)?;
181                }
182                self.optional(lowered.limit.as_ref(), bound.limit.as_ref(), subqueries)?;
183                self.optional(lowered.offset.as_ref(), bound.offset.as_ref(), subqueries)?;
184                self.scalars(&lowered.distinct_on, &bound.distinct_on, subqueries)?;
185                for (lowered, bound) in
186                    pairs(&lowered.windows, &bound.windows, "WINDOW definitions")?
187                {
188                    if lowered.name != bound.name || lowered.inherited != bound.inherited {
189                        return Err(shape_error("WINDOW identity"));
190                    }
191                    let lowered: Vec<_> = lowered.spec.expressions().collect();
192                    let bound: Vec<_> = bound.spec.expressions().collect();
193                    for (lowered, bound) in pairs(&lowered, &bound, "WINDOW expressions")? {
194                        self.scalar(lowered, bound, subqueries)?;
195                    }
196                }
197                Ok(())
198            }
199            (
200                RelationalPlan::SetOp {
201                    left,
202                    right,
203                    order_by,
204                    limit,
205                    offset,
206                    subqueries,
207                    ..
208                },
209                RelationalPlan::SetOp {
210                    left: bound_left,
211                    right: bound_right,
212                    order_by: bound_order,
213                    limit: bound_limit,
214                    offset: bound_offset,
215                    subqueries: bound_subqueries,
216                    ..
217                },
218            ) => {
219                self.query(left, bound_left, false)?;
220                self.query(right, bound_right, false)?;
221                let subqueries = (&subqueries[..], &bound_subqueries[..]);
222                for (lowered, bound) in pairs(order_by, bound_order, "ORDER BY")? {
223                    self.scalar(&lowered.expr, &bound.expr, subqueries)?;
224                }
225                self.optional(limit.as_deref(), bound_limit.as_deref(), subqueries)?;
226                self.optional(offset.as_deref(), bound_offset.as_deref(), subqueries)
227            }
228            (
229                RelationalPlan::Values { rows, subqueries },
230                RelationalPlan::Values {
231                    rows: bound_rows,
232                    subqueries: bound_subqueries,
233                },
234            ) => {
235                let subqueries = (&subqueries[..], &bound_subqueries[..]);
236                for (lowered, bound) in pairs(rows, bound_rows, "VALUES list")? {
237                    self.scalars(lowered, bound, subqueries)?;
238                }
239                Ok(())
240            }
241            _ => Err(shape_error("query")),
242        }
243    }
244
245    /// A data-modifying `WITH` query, in the order the syntax visitor walks the statement.
246    fn cte_body(&mut self, lowered: &CtePlanBody, bound: &CtePlanBody) -> Result<(), SQLError> {
247        match (lowered, bound) {
248            (CtePlanBody::Query(lowered), CtePlanBody::Query(bound)) => {
249                self.query(lowered, bound, false)
250            }
251            (CtePlanBody::Command(lowered), CtePlanBody::Command(bound)) => {
252                self.command(lowered, bound)
253            }
254            _ => Err(shape_error("WITH query")),
255        }
256    }
257
258    fn command(&mut self, lowered: &CommandPlan, bound: &CommandPlan) -> Result<(), SQLError> {
259        for (lowered, bound) in pairs(lowered.ctes(), bound.ctes(), "common table expressions")? {
260            self.cte_body(&lowered.body, &bound.body)?;
261            if let Some((lowered, bound)) =
262                optional_pair(lowered.cycle.as_ref(), bound.cycle.as_ref(), "CYCLE clause")?
263            {
264                self.scalar(&lowered.mark_value, &bound.mark_value, (&[], &[]))?;
265                self.scalar(&lowered.mark_default, &bound.mark_default, (&[], &[]))?;
266            }
267        }
268        let subqueries = (lowered.scalar_subqueries(), bound.scalar_subqueries());
269        match (lowered, bound) {
270            (CommandPlan::Insert(lowered), CommandPlan::Insert(bound)) => {
271                if let Some((lowered, bound)) = optional_pair(
272                    lowered.source.as_deref(),
273                    bound.source.as_deref(),
274                    "INSERT source",
275                )? {
276                    self.query(lowered, bound, false)?;
277                }
278            }
279            (CommandPlan::Update(_), CommandPlan::Update(_))
280            | (CommandPlan::Delete(_), CommandPlan::Delete(_))
281            | (CommandPlan::Merge(_), CommandPlan::Merge(_)) => {
282                if let Some((lowered, bound)) = optional_pair(
283                    lowered.source_input(),
284                    bound.source_input(),
285                    "command source",
286                )? {
287                    self.source(lowered, bound, subqueries)?;
288                }
289            }
290            _ => return Err(shape_error("WITH command")),
291        }
292        let lowered = syntax_expressions(lowered);
293        let bound = syntax_expressions(bound);
294        for (lowered, bound) in pairs(&lowered, &bound, "command expressions")? {
295            self.scalar(lowered, bound, subqueries)?;
296        }
297        Ok(())
298    }
299
300    fn source(
301        &mut self,
302        lowered: &SourcePlan,
303        bound: &SourcePlan,
304        subqueries: Subqueries<'_>,
305    ) -> Result<(), SQLError> {
306        match (lowered, bound) {
307            (SourcePlan::Table { .. }, SourcePlan::Table { .. }) => Ok(()),
308            (
309                SourcePlan::Join {
310                    left, right, on, ..
311                },
312                SourcePlan::Join {
313                    left: bound_left,
314                    right: bound_right,
315                    on: bound_on,
316                    ..
317                },
318            ) => {
319                self.source(left, bound_left, subqueries)?;
320                self.source(right, bound_right, subqueries)?;
321                self.optional(on.as_ref(), bound_on.as_ref(), subqueries)
322            }
323            (
324                SourcePlan::Values { rows, .. },
325                SourcePlan::Values {
326                    rows: bound_rows, ..
327                },
328            ) => {
329                for (lowered, bound) in pairs(rows, bound_rows, "VALUES list")? {
330                    self.scalars(lowered, bound, subqueries)?;
331                }
332                Ok(())
333            }
334            (
335                SourcePlan::Function { args, .. },
336                SourcePlan::Function {
337                    name,
338                    binding,
339                    args: bound_args,
340                    ..
341                },
342            ) => {
343                self.sites.routines.push(BoundRoutineReference {
344                    name: name.clone(),
345                    binding: binding.clone(),
346                });
347                self.scalars(args, bound_args, subqueries)
348            }
349            (
350                SourcePlan::FunctionGroup { functions, .. },
351                SourcePlan::FunctionGroup {
352                    functions: bound_functions,
353                    ..
354                },
355            ) => {
356                for (lowered, bound) in pairs(functions, bound_functions, "ROWS FROM list")? {
357                    self.sites.routines.push(BoundRoutineReference {
358                        name: bound.name.clone(),
359                        binding: bound.binding.clone(),
360                    });
361                    self.scalars(&lowered.args, &bound.args, subqueries)?;
362                }
363                Ok(())
364            }
365            (SourcePlan::Subquery { body, .. }, SourcePlan::Subquery { body: bound, .. }) => {
366                self.query(body, bound, false)
367            }
368            _ => Err(shape_error("FROM item")),
369        }
370    }
371
372    fn optional(
373        &mut self,
374        lowered: Option<&ScalarExpr>,
375        bound: Option<&ScalarExpr>,
376        subqueries: Subqueries<'_>,
377    ) -> Result<(), SQLError> {
378        if let Some((lowered, bound)) = optional_pair(lowered, bound, "clause")? {
379            self.scalar(lowered, bound, subqueries)?;
380        }
381        Ok(())
382    }
383
384    fn scalars(
385        &mut self,
386        lowered: &[ScalarExpr],
387        bound: &[ScalarExpr],
388        subqueries: Subqueries<'_>,
389    ) -> Result<(), SQLError> {
390        for (lowered, bound) in pairs(lowered, bound, "expression list")? {
391            self.scalar(lowered, bound, subqueries)?;
392        }
393        Ok(())
394    }
395
396    fn subquery(
397        &mut self,
398        index: usize,
399        bound_index: usize,
400        subqueries: Subqueries<'_>,
401    ) -> Result<(), SQLError> {
402        match (subqueries.0.get(index), subqueries.1.get(bound_index)) {
403            (Some(lowered), Some(bound)) => self.query(lowered, bound, false),
404            _ => Err(SQLError::Internal(format!(
405                "stored syntax cannot resolve subquery slot {index}"
406            ))),
407        }
408    }
409
410    #[expect(
411        clippy::too_many_lines,
412        reason = "one syntax-order walk covers every scalar variant"
413    )]
414    fn scalar(
415        &mut self,
416        lowered: &ScalarExpr,
417        bound: &ScalarExpr,
418        subqueries: Subqueries<'_>,
419    ) -> Result<(), SQLError> {
420        let mut bound = bound;
421        while let ScalarExpr::Collated { expr, .. } = bound {
422            bound = expr;
423        }
424        if let Some(clause) = crate::catalog::collations::expression::scalar_clause(lowered)? {
425            let mut operand = bound;
426            while let ScalarExpr::Cast { expr, .. } = operand {
427                operand = expr;
428            }
429            if crate::catalog::collations::expression::scalar_clause(operand)?.is_none() {
430                self.sites.values.push(ValueSite::DiscardCollation);
431                return self.scalar(clause.argument, bound, subqueries);
432            }
433        }
434        if crate::type_resolution::row_distinct::expanded(lowered, bound) {
435            let mut rewritten = lowered.clone();
436            crate::type_resolution::row_distinct::rewrite_scalar(&mut rewritten)?;
437            self.sites.values.push(ValueSite::RowDistinct);
438            return self.scalar(&rewritten, bound, subqueries);
439        }
440        if let ScalarExpr::CompositeRow { items, binding, .. } = bound {
441            let mut source = match lowered {
442                ScalarExpr::Cast { expr, .. } => expr.as_ref(),
443                expression => expression,
444            };
445            while let ScalarExpr::Cast { expr, ty, .. } = source {
446                if crate::ColumnType::from_sql_name(ty).ok() != Some(crate::ColumnType::Record) {
447                    break;
448                }
449                source = expr;
450            }
451            if let ScalarExpr::Row(original)
452            | ScalarExpr::CompositeRow {
453                items: original, ..
454            } = source
455            {
456                self.sites
457                    .values
458                    .push(ValueSite::CompositeRow(binding.clone()));
459                return self.scalars(original, items, subqueries);
460            }
461        }
462        if let Some(shape) = crate::type_resolution::membership::stored_shape(lowered, bound) {
463            let ScalarExpr::InList {
464                expr,
465                list,
466                negated,
467            } = lowered
468            else {
469                unreachable!()
470            };
471            self.sites.values.push(ValueSite::Membership {
472                array_items: shape.array_items.clone(),
473            });
474            let rewritten = crate::type_resolution::membership::rewrite(
475                expr.clone(),
476                list.clone(),
477                *negated,
478                &shape,
479                &uqa_core::memory::ProductionControl::uncontrolled(),
480            )?;
481            return self.scalar(&rewritten, bound, subqueries);
482        }
483        // Binding wraps an operand in the casts its operator needs, outside the casts the syntax writes.
484        for _ in 0..cast_depth(bound).saturating_sub(cast_depth(lowered)) {
485            let ScalarExpr::Cast { expr, ty, .. } = bound else {
486                return Err(shape_error("relabel"));
487            };
488            self.sites.values.push(ValueSite::Relabel(ty.clone()));
489            bound = expr;
490        }
491        match lowered {
492            ScalarExpr::Literal(Value::Str(_) | Value::Null) => {
493                self.sites.values.push(match bound {
494                    ScalarExpr::TypedLiteral { value, ty, .. } => ValueSite::Constant {
495                        value: value.clone(),
496                        ty: ty.clone(),
497                    },
498                    ScalarExpr::Literal(Value::Str(_) | Value::Null) => ValueSite::Literal,
499                    _ => return Err(shape_error("literal")),
500                });
501                return Ok(());
502            }
503            ScalarExpr::Cast { .. } => {
504                let ScalarExpr::Cast { ty, .. } = bound else {
505                    return Err(shape_error("cast"));
506                };
507                self.sites.values.push(ValueSite::Cast(ty.clone()));
508            }
509            ScalarExpr::Func { order_syntax, .. } => {
510                let ScalarExpr::Func {
511                    order_syntax: bound,
512                    ..
513                } = bound
514                else {
515                    return Err(shape_error("function ordering"));
516                };
517                if !order_syntax.is_legacy() && order_syntax != bound {
518                    return Err(shape_error("function ordering"));
519                }
520                self.sites.values.push(ValueSite::FunctionOrder(*bound));
521            }
522            _ => self.sites.values.push(ValueSite::Node),
523        }
524        match (lowered, bound) {
525            (
526                ScalarExpr::Func {
527                    args,
528                    order_by,
529                    filter,
530                    ..
531                },
532                ScalarExpr::Func {
533                    name,
534                    binding,
535                    args: bound_args,
536                    order_by: bound_order,
537                    filter: bound_filter,
538                    ..
539                },
540            ) => {
541                self.scalars(args, bound_args, subqueries)?;
542                for (lowered, bound) in pairs(order_by, bound_order, "aggregate ORDER BY")? {
543                    self.scalar(&lowered.expr, &bound.expr, subqueries)?;
544                }
545                self.optional(filter.as_deref(), bound_filter.as_deref(), subqueries)?;
546                self.sites.routines.push(BoundRoutineReference {
547                    name: name.clone(),
548                    binding: binding.clone(),
549                });
550                Ok(())
551            }
552            (ScalarExpr::Array(items), ScalarExpr::Array(bound))
553            | (ScalarExpr::Row(items), ScalarExpr::Row(bound))
554            | (ScalarExpr::And(items), ScalarExpr::And(bound))
555            | (ScalarExpr::Or(items), ScalarExpr::Or(bound)) => {
556                self.scalars(items, bound, subqueries)
557            }
558            (
559                ScalarExpr::Binary { lhs, rhs, .. },
560                ScalarExpr::Binary {
561                    lhs: bound_lhs,
562                    rhs: bound_rhs,
563                    ..
564                },
565            ) => {
566                self.scalar(lhs, bound_lhs, subqueries)?;
567                self.scalar(rhs, bound_rhs, subqueries)
568            }
569            (ScalarExpr::UnaryMinus(inner), ScalarExpr::UnaryMinus(bound))
570            | (ScalarExpr::Not(inner), ScalarExpr::Not(bound))
571            | (ScalarExpr::IsNull { expr: inner, .. }, ScalarExpr::IsNull { expr: bound, .. })
572            | (ScalarExpr::Cast { expr: inner, .. }, ScalarExpr::Cast { expr: bound, .. }) => {
573                self.scalar(inner, bound, subqueries)
574            }
575            (
576                ScalarExpr::Between { expr, low, high },
577                ScalarExpr::Between {
578                    expr: bound_expr,
579                    low: bound_low,
580                    high: bound_high,
581                },
582            ) => {
583                self.scalar(expr, bound_expr, subqueries)?;
584                self.scalar(low, bound_low, subqueries)?;
585                self.scalar(high, bound_high, subqueries)
586            }
587            (
588                ScalarExpr::InList { expr, list, .. },
589                ScalarExpr::InList {
590                    expr: bound_expr,
591                    list: bound_list,
592                    ..
593                },
594            ) => {
595                self.scalar(expr, bound_expr, subqueries)?;
596                self.scalars(list, bound_list, subqueries)
597            }
598            (
599                ScalarExpr::WindowCall {
600                    args, spec, filter, ..
601                },
602                ScalarExpr::WindowCall {
603                    name,
604                    args: bound_args,
605                    spec: bound_spec,
606                    filter: bound_filter,
607                    ..
608                },
609            ) => {
610                self.scalars(args, bound_args, subqueries)?;
611                if let Some((lowered, bound)) =
612                    optional_pair(filter.as_deref(), bound_filter.as_deref(), "window FILTER")?
613                {
614                    self.scalar(lowered, bound, subqueries)?;
615                }
616                self.scalars(&spec.partition_by, &bound_spec.partition_by, subqueries)?;
617                for (lowered, bound) in
618                    pairs(&spec.order_by, &bound_spec.order_by, "window ORDER BY")?
619                {
620                    self.scalar(&lowered.expr, &bound.expr, subqueries)?;
621                }
622                if let Some((lowered, bound)) = optional_pair(
623                    spec.frame.as_ref(),
624                    bound_spec.frame.as_ref(),
625                    "window frame",
626                )? {
627                    self.frame_bound(&lowered.start, &bound.start, subqueries)?;
628                    self.frame_bound(&lowered.end, &bound.end, subqueries)?;
629                }
630                self.sites.routines.push(BoundRoutineReference {
631                    name: name.clone(),
632                    binding: None,
633                });
634                Ok(())
635            }
636            (
637                ScalarExpr::Case {
638                    base,
639                    when,
640                    else_branch,
641                },
642                ScalarExpr::Case {
643                    base: bound_base,
644                    when: bound_when,
645                    else_branch: bound_else,
646                },
647            ) => {
648                self.optional(base.as_deref(), bound_base.as_deref(), subqueries)?;
649                for ((condition, result), (bound_condition, bound_result)) in
650                    pairs(when, bound_when, "CASE arms")?
651                {
652                    self.scalar(condition, bound_condition, subqueries)?;
653                    self.scalar(result, bound_result, subqueries)?;
654                }
655                self.optional(else_branch.as_deref(), bound_else.as_deref(), subqueries)
656            }
657            (ScalarExpr::ScalarSubquery(index), ScalarExpr::ScalarSubquery(bound))
658            | (
659                ScalarExpr::Exists {
660                    subquery: index, ..
661                },
662                ScalarExpr::Exists {
663                    subquery: bound, ..
664                },
665            ) => self.subquery(*index, *bound, subqueries),
666            (
667                ScalarExpr::InSubquery {
668                    expr,
669                    subquery: index,
670                    ..
671                },
672                ScalarExpr::InSubquery {
673                    expr: bound_expr,
674                    subquery: bound,
675                    ..
676                },
677            ) => {
678                self.scalar(expr, bound_expr, subqueries)?;
679                self.subquery(*index, *bound, subqueries)
680            }
681            // Leaves: binding may replace a column by its structural reference or a typed placeholder.
682            (
683                ScalarExpr::Star
684                | ScalarExpr::QualifiedStar(_)
685                | ScalarExpr::Default
686                | ScalarExpr::Column(_)
687                | ScalarExpr::Position(_)
688                | ScalarExpr::InternalColumn(_)
689                | ScalarExpr::QualifiedColumn { .. }
690                | ScalarExpr::Literal(_)
691                | ScalarExpr::TypedLiteral { .. }
692                | ScalarExpr::Param(_),
693                _,
694            ) => Ok(()),
695            _ => Err(shape_error("expression")),
696        }
697    }
698
699    fn frame_bound(
700        &mut self,
701        lowered: &ScalarFrameBound,
702        bound: &ScalarFrameBound,
703        subqueries: Subqueries<'_>,
704    ) -> Result<(), SQLError> {
705        match (lowered, bound) {
706            (ScalarFrameBound::Preceding(lowered), ScalarFrameBound::Preceding(bound))
707            | (ScalarFrameBound::Following(lowered), ScalarFrameBound::Following(bound)) => {
708                self.scalar(lowered, bound, subqueries)
709            }
710            (
711                ScalarFrameBound::UnboundedPreceding
712                | ScalarFrameBound::UnboundedFollowing
713                | ScalarFrameBound::CurrentRow,
714                _,
715            ) => Ok(()),
716            _ => Err(shape_error("window frame")),
717        }
718    }
719}
720
721/// The scalar expressions a data-modifying `WITH` query owns in syntax order; planner-added view checks and target predicates have no syntax.
722fn syntax_expressions(command: &CommandPlan) -> Vec<&ScalarExpr> {
723    use crate::plan::{ConflictActionPlan, MergeWhenPlan};
724    let mut expressions = Vec::new();
725    match command {
726        CommandPlan::Insert(plan) => {
727            expressions.extend(
728                plan.columns
729                    .iter()
730                    .flat_map(crate::ast::AssignmentTarget::expressions),
731            );
732            expressions.extend(plan.rows.iter().flatten());
733            if let Some(conflict) = &plan.on_conflict {
734                expressions.extend(&conflict.expressions);
735                expressions.extend(conflict.predicate.as_deref());
736                if let ConflictActionPlan::Update {
737                    assignments,
738                    predicate,
739                } = &conflict.action
740                {
741                    expressions.extend(
742                        assignments
743                            .iter()
744                            .flat_map(crate::plan::AssignmentPlan::expressions),
745                    );
746                    expressions.extend(predicate.as_deref());
747                }
748            }
749            expressions.extend(plan.returning.iter().map(|projection| &projection.expr));
750        }
751        CommandPlan::Update(plan) => {
752            expressions.extend(
753                plan.assignments
754                    .iter()
755                    .flat_map(crate::plan::AssignmentPlan::expressions),
756            );
757            expressions.extend(plan.predicate.as_ref());
758            expressions.extend(plan.returning.iter().map(|projection| &projection.expr));
759        }
760        CommandPlan::Delete(plan) => {
761            expressions.extend(plan.predicate.as_ref());
762            expressions.extend(plan.returning.iter().map(|projection| &projection.expr));
763        }
764        CommandPlan::Merge(plan) => {
765            expressions.push(&plan.join_condition);
766            for clause in &plan.when_clauses {
767                match clause {
768                    MergeWhenPlan::UpdateMatched {
769                        condition,
770                        assignments,
771                    }
772                    | MergeWhenPlan::UpdateNotMatchedBySource {
773                        condition,
774                        assignments,
775                    } => {
776                        expressions.extend(condition.as_ref());
777                        expressions.extend(
778                            assignments
779                                .iter()
780                                .flat_map(crate::plan::AssignmentPlan::expressions),
781                        );
782                    }
783                    MergeWhenPlan::InsertNotMatched {
784                        condition,
785                        columns,
786                        values,
787                        ..
788                    } => {
789                        expressions.extend(condition.as_ref());
790                        expressions.extend(
791                            columns
792                                .iter()
793                                .flat_map(crate::ast::AssignmentTarget::expressions),
794                        );
795                        expressions.extend(values.iter());
796                    }
797                    MergeWhenPlan::DeleteMatched { condition }
798                    | MergeWhenPlan::DeleteNotMatchedBySource { condition }
799                    | MergeWhenPlan::NothingMatched { condition }
800                    | MergeWhenPlan::NothingNotMatched { condition }
801                    | MergeWhenPlan::NothingNotMatchedBySource { condition } => {
802                        expressions.extend(condition.as_ref());
803                    }
804                }
805            }
806            expressions.extend(plan.returning.iter().map(|projection| &projection.expr));
807        }
808        _ => {}
809    }
810    expressions
811}