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