Skip to main content

uqa_sql/plpgsql/
binding.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Variable binding across expressions, queries, and statements.
8
9use super::{Expr, FromClause, MergeWhen, Projection, Result, SelectStmt, Statement, Value, CTE};
10use crate::ast::InternalColumnRef;
11
12/// Runtime datum value together with the concrete SQL type declared by PL/pgSQL. The type is optional for composite fields and pseudo-types whose runtime carrier already identifies their category.
13#[derive(Debug, Clone)]
14pub struct ResolvedVariable {
15    pub value: Value,
16    pub declared_type: Option<String>,
17}
18
19impl ResolvedVariable {
20    #[must_use]
21    pub fn untyped(value: Value) -> Self {
22        Self {
23            value,
24            declared_type: None,
25        }
26    }
27
28    fn into_expression(self) -> Expr {
29        match self.declared_type {
30            Some(ty) => Expr::TypedLiteral {
31                value: self.value,
32                ty,
33            },
34            None => Expr::Literal(self.value),
35        }
36    }
37}
38
39/// Resolves routine variables while a compiled expression / statement
40/// is being specialized for one execution.
41pub trait VariableResolver {
42    /// Concrete parameter metadata for a declared datum; catalog implementations
43    /// preserve user-type identity instead of inferring from a carrier value.
44    fn parameter_type(&self, name: &str) -> Option<crate::ColumnType> {
45        crate::ColumnType::from_sql_name(name).ok()
46    }
47
48    /// Current value of an unqualified name. `Ok(None)` leaves the
49    /// column reference for the engine to resolve.
50    fn resolve_name(&mut self, name: &str) -> Result<Option<ResolvedVariable>>;
51    /// Current value of `qualifier.column` (record field access).
52    fn resolve_qualified(
53        &mut self,
54        qualifier: &str,
55        column: &str,
56    ) -> Result<Option<ResolvedVariable>>;
57    /// Value of a positional `$n` reference (function arguments).
58    fn resolve_param(&mut self, index: usize) -> Result<Option<ResolvedVariable>>;
59
60    /// Optional expression-level rewrite hook. The default preserves the variable-substitution behavior used by PL/pgSQL while allowing catalog lifecycle code to rewrite a reference without fabricating a literal value.
61    fn rewrite_name(&mut self, name: &str) -> Result<Option<Expr>> {
62        Ok(self
63            .resolve_name(name)?
64            .map(ResolvedVariable::into_expression))
65    }
66
67    /// Expression-level counterpart of [`Self::resolve_qualified`].
68    fn rewrite_qualified(&mut self, qualifier: &str, column: &str) -> Result<Option<Expr>> {
69        Ok(self
70            .resolve_qualified(qualifier, column)?
71            .map(ResolvedVariable::into_expression))
72    }
73
74    /// Expand `qualifier.*` when the containing SQL construct treats it as a
75    /// list item. Scalar binding deliberately does not call this hook because
76    /// `PostgreSQL` treats the same syntax as a composite whole-row value in
77    /// scalar contexts.
78    fn rewrite_qualified_star(&mut self, _qualifier: &str) -> Result<Option<Vec<Expr>>> {
79        Ok(None)
80    }
81
82    /// Resolve `qualifier.*` when it appears in a scalar context and therefore denotes one composite whole-row value rather than a projection list.
83    fn rewrite_qualified_whole_row(&mut self, _qualifier: &str) -> Result<Option<Expr>> {
84        Ok(None)
85    }
86
87    /// Expression-level counterpart of [`Self::resolve_param`].
88    fn rewrite_param(&mut self, index: usize) -> Result<Option<Expr>> {
89        Ok(self
90            .resolve_param(index)?
91            .map(ResolvedVariable::into_expression))
92    }
93
94    /// Observe or replace an executor-only structural column reference. SQL
95    /// variable resolvers normally leave these untouched.
96    fn rewrite_internal(&mut self, _column: InternalColumnRef) -> Result<Option<Expr>> {
97        Ok(None)
98    }
99}
100
101/// Rewrite an expression, substituting resolvable variable references
102/// with literals. References the resolver declines stay untouched.
103#[expect(
104    clippy::too_many_lines,
105    reason = "PL/pgSQL lowering preserves parser order and datum validation"
106)]
107pub fn bind_expr(expr: &Expr, r: &mut dyn VariableResolver) -> Result<Expr> {
108    Ok(match expr {
109        Expr::Column(name) => match r.rewrite_name(name)? {
110            Some(value) => value,
111            None => expr.clone(),
112        },
113        Expr::QualifiedColumn {
114            qualifier, column, ..
115        } => match r.rewrite_qualified(qualifier, column)? {
116            Some(value) => value,
117            None => expr.clone(),
118        },
119        Expr::Param(index) => match r.rewrite_param(*index)? {
120            Some(value) => value,
121            None => expr.clone(),
122        },
123        Expr::InternalColumn(column) => match r.rewrite_internal(*column)? {
124            Some(value) => value,
125            None => expr.clone(),
126        },
127        Expr::QualifiedStar(qualifier) => r
128            .rewrite_qualified_whole_row(qualifier)?
129            .unwrap_or_else(|| expr.clone()),
130        Expr::Default | Expr::Literal(_) | Expr::TypedLiteral { .. } | Expr::Star => expr.clone(),
131        Expr::Func {
132            order_syntax,
133            name,
134            binding,
135            args,
136            distinct,
137            order_by,
138            filter,
139        } => Expr::Func {
140            order_syntax: *order_syntax,
141            name: name.clone(),
142            binding: binding.clone(),
143            args: bind_exprs(args, r)?,
144            distinct: *distinct,
145            order_by: bind_order_by(order_by, r)?,
146            filter: match filter {
147                Some(f) => Some(Box::new(bind_expr(f, r)?)),
148                None => None,
149            },
150        },
151        Expr::Array(items) => Expr::Array(bind_exprs(items, r)?),
152        Expr::Row(items) => Expr::Row(bind_exprs(items, r)?),
153        Expr::CompositeRow { items, binding } => Expr::CompositeRow {
154            items: bind_exprs(items, r)?,
155            binding: binding.clone(),
156        },
157        Expr::Binary { op, lhs, rhs } => Expr::Binary {
158            op: *op,
159            lhs: Box::new(bind_expr(lhs, r)?),
160            rhs: Box::new(bind_expr(rhs, r)?),
161        },
162        Expr::UnaryMinus(inner) => Expr::UnaryMinus(Box::new(bind_expr(inner, r)?)),
163        Expr::Not(inner) => Expr::Not(Box::new(bind_expr(inner, r)?)),
164        Expr::And(items) => Expr::And(bind_exprs(items, r)?),
165        Expr::Or(items) => Expr::Or(bind_exprs(items, r)?),
166        Expr::IsNull { expr, negated } => Expr::IsNull {
167            expr: Box::new(bind_expr(expr, r)?),
168            negated: *negated,
169        },
170        Expr::Between { expr, low, high } => Expr::Between {
171            expr: Box::new(bind_expr(expr, r)?),
172            low: Box::new(bind_expr(low, r)?),
173            high: Box::new(bind_expr(high, r)?),
174        },
175        Expr::InList {
176            expr,
177            list,
178            negated,
179        } => Expr::InList {
180            expr: Box::new(bind_expr(expr, r)?),
181            list: bind_exprs(list, r)?,
182            negated: *negated,
183        },
184        Expr::WindowCall {
185            name,
186            args,
187            spec,
188            filter,
189            modifiers,
190        } => Expr::WindowCall {
191            modifiers: *modifiers,
192            name: name.clone(),
193            args: bind_exprs(args, r)?,
194            spec: Box::new(bind_window_spec(spec, r)?),
195            filter: match filter {
196                Some(f) => Some(Box::new(bind_expr(f, r)?)),
197                None => None,
198            },
199        },
200        Expr::Case {
201            base,
202            when,
203            else_branch,
204        } => Expr::Case {
205            base: match base {
206                Some(b) => Some(Box::new(bind_expr(b, r)?)),
207                None => None,
208            },
209            when: when
210                .iter()
211                .map(|(c, v)| Ok((bind_expr(c, r)?, bind_expr(v, r)?)))
212                .collect::<Result<Vec<_>>>()?,
213            else_branch: match else_branch {
214                Some(e) => Some(Box::new(bind_expr(e, r)?)),
215                None => None,
216            },
217        },
218        Expr::Cast { expr, ty, implicit } => Expr::Cast {
219            implicit: *implicit,
220            expr: Box::new(bind_expr(expr, r)?),
221            ty: ty.clone(),
222        },
223        Expr::ScalarSubquery(body) => Expr::ScalarSubquery(Box::new(bind_select(body, r)?)),
224        Expr::Exists { body, negated } => Expr::Exists {
225            body: Box::new(bind_select(body, r)?),
226            negated: *negated,
227        },
228        Expr::InSubquery {
229            expr,
230            body,
231            negated,
232        } => Expr::InSubquery {
233            expr: Box::new(bind_expr(expr, r)?),
234            body: Box::new(bind_select(body, r)?),
235            negated: *negated,
236        },
237    })
238}
239
240/// A frame offset may name a PL/pgSQL variable, which becomes a parameter of the query.
241fn bind_frame_bound(
242    bound: &crate::ast::FrameBound,
243    r: &mut dyn VariableResolver,
244) -> Result<crate::ast::FrameBound> {
245    use crate::ast::FrameBound;
246    Ok(match bound {
247        FrameBound::Preceding(offset) => FrameBound::Preceding(Box::new(bind_expr(offset, r)?)),
248        FrameBound::Following(offset) => FrameBound::Following(Box::new(bind_expr(offset, r)?)),
249        FrameBound::UnboundedPreceding => FrameBound::UnboundedPreceding,
250        FrameBound::UnboundedFollowing => FrameBound::UnboundedFollowing,
251        FrameBound::CurrentRow => FrameBound::CurrentRow,
252    })
253}
254
255pub(super) fn bind_exprs(exprs: &[Expr], r: &mut dyn VariableResolver) -> Result<Vec<Expr>> {
256    exprs.iter().map(|e| bind_expr(e, r)).collect()
257}
258
259pub(super) fn bind_opt_expr(
260    expr: Option<&Expr>,
261    r: &mut dyn VariableResolver,
262) -> Result<Option<Expr>> {
263    match expr {
264        Some(e) => Ok(Some(bind_expr(e, r)?)),
265        None => Ok(None),
266    }
267}
268
269pub(super) fn bind_order_by(
270    items: &[crate::ast::OrderBy],
271    r: &mut dyn VariableResolver,
272) -> Result<Vec<crate::ast::OrderBy>> {
273    items
274        .iter()
275        .map(|o| {
276            Ok(crate::ast::OrderBy {
277                expr: bind_expr(&o.expr, r)?,
278                descending: o.descending,
279                nulls: o.nulls,
280            })
281        })
282        .collect()
283}
284
285pub(super) fn bind_projections(
286    items: &[Projection],
287    r: &mut dyn VariableResolver,
288) -> Result<Vec<Projection>> {
289    items
290        .iter()
291        .map(|p| {
292            Ok(Projection {
293                expr: bind_expr(&p.expr, r)?,
294                alias: p.alias.clone(),
295            })
296        })
297        .collect()
298}
299
300pub(super) fn bind_assignments(
301    items: &[(crate::ast::AssignmentTargets, Expr)],
302    r: &mut dyn VariableResolver,
303) -> Result<Vec<(crate::ast::AssignmentTargets, Expr)>> {
304    items
305        .iter()
306        .map(|(target, expression)| {
307            Ok((
308                {
309                    let mut group = target.clone();
310                    for target in group.targets_mut() {
311                        *target = bind_assignment_target(target, r)?;
312                    }
313                    group
314                },
315                bind_expr(expression, r)?,
316            ))
317        })
318        .collect()
319}
320
321fn bind_assignment_target(
322    target: &crate::ast::AssignmentTarget,
323    resolver: &mut dyn VariableResolver,
324) -> Result<crate::ast::AssignmentTarget> {
325    let mut target = target.clone();
326    for expression in target.expressions_mut() {
327        *expression = bind_expr(expression, resolver)?;
328    }
329    Ok(target)
330}
331
332pub(super) fn bind_ctes(items: &[CTE], r: &mut dyn VariableResolver) -> Result<Vec<CTE>> {
333    items
334        .iter()
335        .map(|cte| {
336            Ok(CTE {
337                name: cte.name.clone(),
338                columns: cte.columns.clone(),
339                recursive: cte.recursive,
340                materialization: cte.materialization,
341                search: cte.search.clone(),
342                cycle: cte
343                    .cycle
344                    .as_ref()
345                    .map(|cycle| -> Result<crate::ast::CteCycleClause> {
346                        Ok(crate::ast::CteCycleClause {
347                            columns: cycle.columns.clone(),
348                            mark_column: cycle.mark_column.clone(),
349                            mark_value: bind_expr(&cycle.mark_value, r)?,
350                            mark_default: bind_expr(&cycle.mark_default, r)?,
351                            path_column: cycle.path_column.clone(),
352                        })
353                    })
354                    .transpose()?,
355                body: crate::ast::CteBody::try_from(bind_statement(
356                    &cte.body.clone().into_statement(),
357                    r,
358                )?)?,
359            })
360        })
361        .collect()
362}
363
364pub(super) fn bind_rows(
365    rows: &[Vec<Expr>],
366    r: &mut dyn VariableResolver,
367) -> Result<Vec<Vec<Expr>>> {
368    rows.iter().map(|row| bind_exprs(row, r)).collect()
369}
370
371fn bind_window_spec(
372    spec: &crate::ast::WindowSpec,
373    r: &mut dyn VariableResolver,
374) -> Result<crate::ast::WindowSpec> {
375    Ok(crate::ast::WindowSpec {
376        definition: spec.definition,
377        raw_definition: None,
378        reference: spec.reference.clone(),
379        partition_by: bind_exprs(&spec.partition_by, r)?,
380        order_by: bind_order_by(&spec.order_by, r)?,
381        frame: spec
382            .frame
383            .as_ref()
384            .map(|frame| -> Result<crate::ast::WindowFrame> {
385                Ok(crate::ast::WindowFrame {
386                    start: bind_frame_bound(&frame.start, r)?,
387                    end: bind_frame_bound(&frame.end, r)?,
388                    ..frame.clone()
389                })
390            })
391            .transpose()?,
392    })
393}
394
395/// Rewrite a `SELECT` body, substituting resolvable variables.
396pub fn bind_select(stmt: &SelectStmt, r: &mut dyn VariableResolver) -> Result<SelectStmt> {
397    Ok(SelectStmt {
398        windows: stmt
399            .windows
400            .iter()
401            .map(|window| {
402                Ok(crate::ast::WindowDefinition {
403                    name: window.name.clone(),
404                    inherited: window.inherited,
405                    spec: bind_window_spec(&window.spec, r)?,
406                })
407            })
408            .collect::<Result<Vec<_>>>()?,
409        projections: bind_projections(&stmt.projections, r)?,
410        values: bind_rows(&stmt.values, r)?,
411        from: match stmt.from.as_ref() {
412            Some(f) => Some(bind_from(f, r)?),
413            None => None,
414        },
415        r#where: bind_opt_expr(stmt.r#where.as_ref(), r)?,
416        group_by: bind_exprs(&stmt.group_by, r)?,
417        grouping_sets: stmt
418            .grouping_sets
419            .iter()
420            .map(|set| bind_exprs(set, r))
421            .collect::<Result<Vec<_>>>()?,
422        group_distinct: stmt.group_distinct,
423        having: bind_opt_expr(stmt.having.as_ref(), r)?,
424        order_by: bind_order_by(&stmt.order_by, r)?,
425        limit: bind_opt_expr(stmt.limit.as_ref(), r)?,
426        with_ties: stmt.with_ties,
427        offset: bind_opt_expr(stmt.offset.as_ref(), r)?,
428        with: bind_ctes(&stmt.with, r)?,
429        set_op: match stmt.set_op.as_ref() {
430            Some(op) => Some(Box::new(crate::ast::SetOp {
431                kind: op.kind,
432                all: op.all,
433                left: op
434                    .left
435                    .as_ref()
436                    .map(|left| bind_select(left, r).map(Box::new))
437                    .transpose()?,
438                right: bind_select(&op.right, r)?,
439                combined_order_by: bind_order_by(&op.combined_order_by, r)?,
440                combined_limit: bind_opt_expr(op.combined_limit.as_ref(), r)?,
441                combined_with_ties: op.combined_with_ties,
442                combined_offset: bind_opt_expr(op.combined_offset.as_ref(), r)?,
443            })),
444            None => None,
445        },
446        distinct: stmt.distinct,
447        distinct_on: bind_exprs(&stmt.distinct_on, r)?,
448        locking: stmt.locking.clone(),
449    })
450}
451
452pub(super) fn bind_from(from: &FromClause, r: &mut dyn VariableResolver) -> Result<FromClause> {
453    Ok(match from {
454        FromClause::Table { .. } => from.clone(),
455        FromClause::Join {
456            left,
457            right,
458            kind,
459            on,
460            using,
461            natural,
462            alias,
463            column_aliases,
464            lateral,
465        } => FromClause::Join {
466            left: Box::new(bind_from(left, r)?),
467            right: Box::new(bind_from(right, r)?),
468            kind: *kind,
469            on: bind_opt_expr(on.as_ref(), r)?,
470            using: using.clone(),
471            natural: *natural,
472            alias: alias.clone(),
473            column_aliases: column_aliases.clone(),
474            lateral: *lateral,
475        },
476        FromClause::Values {
477            rows,
478            alias,
479            column_aliases,
480            internal_relation,
481            internal_column_types,
482        } => FromClause::Values {
483            rows: bind_rows(rows, r)?,
484            alias: alias.clone(),
485            column_aliases: column_aliases.clone(),
486            internal_relation: *internal_relation,
487            internal_column_types: internal_column_types.clone(),
488        },
489        FromClause::Function {
490            name,
491            binding,
492            output_name,
493            relations,
494            args,
495            alias,
496            column_aliases,
497            ordinality,
498            column_types,
499        } => FromClause::Function {
500            name: name.clone(),
501            binding: binding.clone(),
502            output_name: output_name.clone(),
503            relations: relations.clone(),
504            args: bind_exprs(args, r)?,
505            alias: alias.clone(),
506            column_aliases: column_aliases.clone(),
507            ordinality: *ordinality,
508            column_types: column_types.clone(),
509        },
510        FromClause::FunctionGroup {
511            functions,
512            alias,
513            column_aliases,
514            ordinality,
515        } => FromClause::FunctionGroup {
516            functions: functions
517                .iter()
518                .map(|function| {
519                    Ok(crate::ast::TableFunction {
520                        name: function.name.clone(),
521                        binding: function.binding.clone(),
522                        output_name: function.output_name.clone(),
523                        relations: function.relations.clone(),
524                        args: bind_exprs(&function.args, r)?,
525                        column_aliases: function.column_aliases.clone(),
526                        column_types: function.column_types.clone(),
527                    })
528                })
529                .collect::<Result<Vec<_>>>()?,
530            alias: alias.clone(),
531            column_aliases: column_aliases.clone(),
532            ordinality: *ordinality,
533        },
534        FromClause::Subquery {
535            body,
536            alias,
537            column_aliases,
538        } => FromClause::Subquery {
539            body: Box::new(bind_select(body, r)?),
540            alias: alias.clone(),
541            column_aliases: column_aliases.clone(),
542        },
543    })
544}
545
546/// Rewrite a full statement, substituting resolvable variables in
547/// every expression position. Statements without expression payloads
548/// pass through unchanged.
549#[expect(
550    clippy::too_many_lines,
551    reason = "PL/pgSQL lowering preserves parser order and datum validation"
552)]
553pub fn bind_statement(stmt: &Statement, r: &mut dyn VariableResolver) -> Result<Statement> {
554    Ok(match stmt {
555        Statement::Select(body) => Statement::Select(Box::new(bind_select(body, r)?)),
556        Statement::Insert(insert) => {
557            let mut out = insert.clone();
558            out.columns = insert
559                .columns
560                .iter()
561                .map(|target| bind_assignment_target(target, r))
562                .collect::<Result<_>>()?;
563            out.with = bind_ctes(&insert.with, r)?;
564            out.rows = bind_rows(&insert.rows, r)?;
565            out.select_source = match insert.select_source.as_ref() {
566                Some(body) => Some(Box::new(bind_select(body, r)?)),
567                None => None,
568            };
569            out.on_conflict = match insert.on_conflict.as_ref() {
570                Some(oc) => Some(crate::ast::OnConflict {
571                    predicate: bind_opt_expr(oc.predicate.as_deref(), r)?.map(Box::new),
572                    constraint: oc.constraint.clone(),
573                    conflict_columns: oc.conflict_columns.clone(),
574                    expressions: oc
575                        .expressions
576                        .iter()
577                        .map(|expr| bind_expr(expr, r))
578                        .collect::<Result<Vec<_>>>()?,
579                    action: match &oc.action {
580                        crate::ast::OnConflictAction::Nothing => {
581                            crate::ast::OnConflictAction::Nothing
582                        }
583                        crate::ast::OnConflictAction::Update {
584                            assignments,
585                            r#where,
586                        } => crate::ast::OnConflictAction::Update {
587                            assignments: bind_assignments(assignments, r)?,
588                            r#where: bind_opt_expr(r#where.as_deref(), r)?.map(Box::new),
589                        },
590                    },
591                }),
592                None => None,
593            };
594            out.returning = bind_projections(&insert.returning, r)?;
595            Statement::Insert(out)
596        }
597        Statement::Update(update) => {
598            let mut out = update.clone();
599            out.assignments = bind_assignments(&update.assignments, r)?;
600            out.r#where = bind_opt_expr(update.r#where.as_ref(), r)?;
601            out.with = bind_ctes(&update.with, r)?;
602            out.from = match update.from.as_ref() {
603                Some(f) => Some(bind_from(f, r)?),
604                None => None,
605            };
606            out.returning = bind_projections(&update.returning, r)?;
607            Statement::Update(out)
608        }
609        Statement::Delete(delete) => {
610            let mut out = delete.clone();
611            out.r#where = bind_opt_expr(delete.r#where.as_ref(), r)?;
612            out.with = bind_ctes(&delete.with, r)?;
613            out.using = match delete.using.as_ref() {
614                Some(f) => Some(bind_from(f, r)?),
615                None => None,
616            };
617            out.returning = bind_projections(&delete.returning, r)?;
618            Statement::Delete(out)
619        }
620        Statement::Values { rows } => Statement::Values {
621            rows: bind_rows(rows, r)?,
622        },
623        Statement::CreateTableAs {
624            name,
625            if_not_exists,
626            column_names,
627            with_no_data,
628            select_into,
629            persistence,
630            on_commit,
631            body,
632        } => Statement::CreateTableAs {
633            name: name.clone(),
634            if_not_exists: *if_not_exists,
635            column_names: column_names.clone(),
636            with_no_data: *with_no_data,
637            select_into: *select_into,
638            persistence: *persistence,
639            on_commit: *on_commit,
640            body: Box::new(bind_select(body, r)?),
641        },
642        Statement::CreateMaterializedView {
643            name,
644            column_names,
645            if_not_exists,
646            with_no_data,
647            options,
648            body,
649        } => Statement::CreateMaterializedView {
650            name: name.clone(),
651            column_names: column_names.clone(),
652            if_not_exists: *if_not_exists,
653            with_no_data: *with_no_data,
654            options: options.clone(),
655            body: Box::new(bind_select(body, r)?),
656        },
657        Statement::Explain {
658            analyze,
659            verbose,
660            format,
661            body,
662        } => Statement::Explain {
663            analyze: *analyze,
664            verbose: *verbose,
665            format: format.clone(),
666            body: Box::new(bind_statement(body, r)?),
667        },
668        Statement::DeclareCursor(cursor) => {
669            let mut out = cursor.clone();
670            out.query = Box::new(bind_select(&cursor.query, r)?);
671            Statement::DeclareCursor(out)
672        }
673        Statement::Merge(merge) => {
674            let mut out = merge.clone();
675            out.with = bind_ctes(&merge.with, r)?;
676            out.source = bind_from(&merge.source, r)?;
677            out.join_condition = bind_expr(&merge.join_condition, r)?;
678            out.when_clauses = merge
679                .when_clauses
680                .iter()
681                .map(|w| bind_merge_when(w, r))
682                .collect::<Result<Vec<_>>>()?;
683            out.returning = bind_projections(&merge.returning, r)?;
684            Statement::Merge(out)
685        }
686        Statement::Call { name, args } => Statement::Call {
687            name: name.clone(),
688            args: bind_exprs(args, r)?,
689        },
690        other => other.clone(),
691    })
692}
693
694pub(super) fn bind_merge_when(when: &MergeWhen, r: &mut dyn VariableResolver) -> Result<MergeWhen> {
695    Ok(match when {
696        MergeWhen::UpdateMatched {
697            condition,
698            assignments,
699        } => MergeWhen::UpdateMatched {
700            condition: bind_opt_expr(condition.as_ref(), r)?,
701            assignments: bind_assignments(assignments, r)?,
702        },
703        MergeWhen::DeleteMatched { condition } => MergeWhen::DeleteMatched {
704            condition: bind_opt_expr(condition.as_ref(), r)?,
705        },
706        MergeWhen::UpdateNotMatchedBySource {
707            condition,
708            assignments,
709        } => MergeWhen::UpdateNotMatchedBySource {
710            condition: bind_opt_expr(condition.as_ref(), r)?,
711            assignments: bind_assignments(assignments, r)?,
712        },
713        MergeWhen::DeleteNotMatchedBySource { condition } => MergeWhen::DeleteNotMatchedBySource {
714            condition: bind_opt_expr(condition.as_ref(), r)?,
715        },
716        MergeWhen::InsertNotMatched {
717            condition,
718            columns,
719            overriding,
720            values,
721        } => MergeWhen::InsertNotMatched {
722            condition: bind_opt_expr(condition.as_ref(), r)?,
723            columns: columns
724                .iter()
725                .map(|target| bind_assignment_target(target, r))
726                .collect::<Result<_>>()?,
727            overriding: *overriding,
728            values: bind_exprs(values, r)?,
729        },
730        MergeWhen::NothingMatched { condition } => MergeWhen::NothingMatched {
731            condition: bind_opt_expr(condition.as_ref(), r)?,
732        },
733        MergeWhen::NothingNotMatched { condition } => MergeWhen::NothingNotMatched {
734            condition: bind_opt_expr(condition.as_ref(), r)?,
735        },
736        MergeWhen::NothingNotMatchedBySource { condition } => {
737            MergeWhen::NothingNotMatchedBySource {
738                condition: bind_opt_expr(condition.as_ref(), r)?,
739            }
740        }
741    })
742}