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: &[(String, Expr)],
268    r: &mut dyn VariableResolver,
269) -> Result<Vec<(String, Expr)>> {
270    items
271        .iter()
272        .map(|(name, e)| Ok((name.clone(), bind_expr(e, r)?)))
273        .collect()
274}
275
276pub(super) fn bind_ctes(items: &[CTE], r: &mut dyn VariableResolver) -> Result<Vec<CTE>> {
277    items
278        .iter()
279        .map(|cte| {
280            Ok(CTE {
281                name: cte.name.clone(),
282                columns: cte.columns.clone(),
283                recursive: cte.recursive,
284                materialization: cte.materialization,
285                search: cte.search.clone(),
286                cycle: cte
287                    .cycle
288                    .as_ref()
289                    .map(|cycle| -> Result<crate::ast::CteCycleClause> {
290                        Ok(crate::ast::CteCycleClause {
291                            columns: cycle.columns.clone(),
292                            mark_column: cycle.mark_column.clone(),
293                            mark_value: bind_expr(&cycle.mark_value, r)?,
294                            mark_default: bind_expr(&cycle.mark_default, r)?,
295                            path_column: cycle.path_column.clone(),
296                        })
297                    })
298                    .transpose()?,
299                body: crate::ast::CteBody::try_from(bind_statement(
300                    &cte.body.clone().into_statement(),
301                    r,
302                )?)?,
303            })
304        })
305        .collect()
306}
307
308pub(super) fn bind_rows(
309    rows: &[Vec<Expr>],
310    r: &mut dyn VariableResolver,
311) -> Result<Vec<Vec<Expr>>> {
312    rows.iter().map(|row| bind_exprs(row, r)).collect()
313}
314
315/// Rewrite a `SELECT` body, substituting resolvable variables.
316pub fn bind_select(stmt: &SelectStmt, r: &mut dyn VariableResolver) -> Result<SelectStmt> {
317    Ok(SelectStmt {
318        projections: bind_projections(&stmt.projections, r)?,
319        values: bind_rows(&stmt.values, r)?,
320        from: match stmt.from.as_ref() {
321            Some(f) => Some(bind_from(f, r)?),
322            None => None,
323        },
324        r#where: bind_opt_expr(stmt.r#where.as_ref(), r)?,
325        group_by: bind_exprs(&stmt.group_by, r)?,
326        grouping_sets: stmt
327            .grouping_sets
328            .iter()
329            .map(|set| bind_exprs(set, r))
330            .collect::<Result<Vec<_>>>()?,
331        group_distinct: stmt.group_distinct,
332        having: bind_opt_expr(stmt.having.as_ref(), r)?,
333        order_by: bind_order_by(&stmt.order_by, r)?,
334        limit: bind_opt_expr(stmt.limit.as_ref(), r)?,
335        with_ties: stmt.with_ties,
336        offset: bind_opt_expr(stmt.offset.as_ref(), r)?,
337        with: bind_ctes(&stmt.with, r)?,
338        set_op: match stmt.set_op.as_ref() {
339            Some(op) => Some(Box::new(crate::ast::SetOp {
340                kind: op.kind,
341                all: op.all,
342                left: op
343                    .left
344                    .as_ref()
345                    .map(|left| bind_select(left, r).map(Box::new))
346                    .transpose()?,
347                right: bind_select(&op.right, r)?,
348                combined_order_by: bind_order_by(&op.combined_order_by, r)?,
349                combined_limit: bind_opt_expr(op.combined_limit.as_ref(), r)?,
350                combined_with_ties: op.combined_with_ties,
351                combined_offset: bind_opt_expr(op.combined_offset.as_ref(), r)?,
352            })),
353            None => None,
354        },
355        distinct: stmt.distinct,
356        distinct_on: bind_exprs(&stmt.distinct_on, r)?,
357        locking: stmt.locking.clone(),
358    })
359}
360
361pub(super) fn bind_from(from: &FromClause, r: &mut dyn VariableResolver) -> Result<FromClause> {
362    Ok(match from {
363        FromClause::Table { .. } => from.clone(),
364        FromClause::Join {
365            left,
366            right,
367            kind,
368            on,
369            using,
370            natural,
371            alias,
372            column_aliases,
373            lateral,
374        } => FromClause::Join {
375            left: Box::new(bind_from(left, r)?),
376            right: Box::new(bind_from(right, r)?),
377            kind: *kind,
378            on: bind_opt_expr(on.as_ref(), r)?,
379            using: using.clone(),
380            natural: *natural,
381            alias: alias.clone(),
382            column_aliases: column_aliases.clone(),
383            lateral: *lateral,
384        },
385        FromClause::Values {
386            rows,
387            alias,
388            column_aliases,
389            internal_relation,
390            internal_column_types,
391        } => FromClause::Values {
392            rows: bind_rows(rows, r)?,
393            alias: alias.clone(),
394            column_aliases: column_aliases.clone(),
395            internal_relation: *internal_relation,
396            internal_column_types: internal_column_types.clone(),
397        },
398        FromClause::Function {
399            name,
400            binding,
401            output_name,
402            relations,
403            args,
404            alias,
405            column_aliases,
406            ordinality,
407            column_types,
408        } => FromClause::Function {
409            name: name.clone(),
410            binding: binding.clone(),
411            output_name: output_name.clone(),
412            relations: relations.clone(),
413            args: bind_exprs(args, r)?,
414            alias: alias.clone(),
415            column_aliases: column_aliases.clone(),
416            ordinality: *ordinality,
417            column_types: column_types.clone(),
418        },
419        FromClause::FunctionGroup {
420            functions,
421            alias,
422            column_aliases,
423            ordinality,
424        } => FromClause::FunctionGroup {
425            functions: functions
426                .iter()
427                .map(|function| {
428                    Ok(crate::ast::TableFunction {
429                        name: function.name.clone(),
430                        binding: function.binding.clone(),
431                        output_name: function.output_name.clone(),
432                        relations: function.relations.clone(),
433                        args: bind_exprs(&function.args, r)?,
434                        column_aliases: function.column_aliases.clone(),
435                        column_types: function.column_types.clone(),
436                    })
437                })
438                .collect::<Result<Vec<_>>>()?,
439            alias: alias.clone(),
440            column_aliases: column_aliases.clone(),
441            ordinality: *ordinality,
442        },
443        FromClause::Subquery {
444            body,
445            alias,
446            column_aliases,
447        } => FromClause::Subquery {
448            body: Box::new(bind_select(body, r)?),
449            alias: alias.clone(),
450            column_aliases: column_aliases.clone(),
451        },
452    })
453}
454
455/// Rewrite a full statement, substituting resolvable variables in
456/// every expression position. Statements without expression payloads
457/// pass through unchanged.
458#[expect(
459    clippy::too_many_lines,
460    reason = "PL/pgSQL lowering preserves parser order and datum validation"
461)]
462pub fn bind_statement(stmt: &Statement, r: &mut dyn VariableResolver) -> Result<Statement> {
463    Ok(match stmt {
464        Statement::Select(body) => Statement::Select(Box::new(bind_select(body, r)?)),
465        Statement::Insert(insert) => {
466            let mut out = insert.clone();
467            out.with = bind_ctes(&insert.with, r)?;
468            out.rows = bind_rows(&insert.rows, r)?;
469            out.select_source = match insert.select_source.as_ref() {
470                Some(body) => Some(Box::new(bind_select(body, r)?)),
471                None => None,
472            };
473            out.on_conflict = match insert.on_conflict.as_ref() {
474                Some(oc) => Some(crate::ast::OnConflict {
475                    predicate: bind_opt_expr(oc.predicate.as_deref(), r)?.map(Box::new),
476                    constraint: oc.constraint.clone(),
477                    conflict_columns: oc.conflict_columns.clone(),
478                    expressions: oc
479                        .expressions
480                        .iter()
481                        .map(|expr| bind_expr(expr, r))
482                        .collect::<Result<Vec<_>>>()?,
483                    action: match &oc.action {
484                        crate::ast::OnConflictAction::Nothing => {
485                            crate::ast::OnConflictAction::Nothing
486                        }
487                        crate::ast::OnConflictAction::Update {
488                            assignments,
489                            r#where,
490                        } => crate::ast::OnConflictAction::Update {
491                            assignments: bind_assignments(assignments, r)?,
492                            r#where: bind_opt_expr(r#where.as_deref(), r)?.map(Box::new),
493                        },
494                    },
495                }),
496                None => None,
497            };
498            out.returning = bind_projections(&insert.returning, r)?;
499            Statement::Insert(out)
500        }
501        Statement::Update(update) => {
502            let mut out = update.clone();
503            out.assignments = bind_assignments(&update.assignments, r)?;
504            out.r#where = bind_opt_expr(update.r#where.as_ref(), r)?;
505            out.with = bind_ctes(&update.with, r)?;
506            out.from = match update.from.as_ref() {
507                Some(f) => Some(bind_from(f, r)?),
508                None => None,
509            };
510            out.returning = bind_projections(&update.returning, r)?;
511            Statement::Update(out)
512        }
513        Statement::Delete(delete) => {
514            let mut out = delete.clone();
515            out.r#where = bind_opt_expr(delete.r#where.as_ref(), r)?;
516            out.with = bind_ctes(&delete.with, r)?;
517            out.using = match delete.using.as_ref() {
518                Some(f) => Some(bind_from(f, r)?),
519                None => None,
520            };
521            out.returning = bind_projections(&delete.returning, r)?;
522            Statement::Delete(out)
523        }
524        Statement::Values { rows } => Statement::Values {
525            rows: bind_rows(rows, r)?,
526        },
527        Statement::CreateTableAs {
528            name,
529            if_not_exists,
530            column_names,
531            with_no_data,
532            persistence,
533            on_commit,
534            body,
535        } => Statement::CreateTableAs {
536            name: name.clone(),
537            if_not_exists: *if_not_exists,
538            column_names: column_names.clone(),
539            with_no_data: *with_no_data,
540            persistence: *persistence,
541            on_commit: *on_commit,
542            body: Box::new(bind_select(body, r)?),
543        },
544        Statement::CreateMaterializedView {
545            name,
546            column_names,
547            if_not_exists,
548            with_no_data,
549            options,
550            body,
551        } => Statement::CreateMaterializedView {
552            name: name.clone(),
553            column_names: column_names.clone(),
554            if_not_exists: *if_not_exists,
555            with_no_data: *with_no_data,
556            options: options.clone(),
557            body: Box::new(bind_select(body, r)?),
558        },
559        Statement::Explain {
560            analyze,
561            verbose,
562            format,
563            body,
564        } => Statement::Explain {
565            analyze: *analyze,
566            verbose: *verbose,
567            format: format.clone(),
568            body: Box::new(bind_statement(body, r)?),
569        },
570        Statement::DeclareCursor(cursor) => {
571            let mut out = cursor.clone();
572            out.query = Box::new(bind_select(&cursor.query, r)?);
573            Statement::DeclareCursor(out)
574        }
575        Statement::Merge(merge) => {
576            let mut out = merge.clone();
577            out.with = bind_ctes(&merge.with, r)?;
578            out.source = bind_from(&merge.source, r)?;
579            out.join_condition = bind_expr(&merge.join_condition, r)?;
580            out.when_clauses = merge
581                .when_clauses
582                .iter()
583                .map(|w| bind_merge_when(w, r))
584                .collect::<Result<Vec<_>>>()?;
585            out.returning = bind_projections(&merge.returning, r)?;
586            Statement::Merge(out)
587        }
588        Statement::Call { name, args } => Statement::Call {
589            name: name.clone(),
590            args: bind_exprs(args, r)?,
591        },
592        other => other.clone(),
593    })
594}
595
596pub(super) fn bind_merge_when(when: &MergeWhen, r: &mut dyn VariableResolver) -> Result<MergeWhen> {
597    Ok(match when {
598        MergeWhen::UpdateMatched {
599            condition,
600            assignments,
601        } => MergeWhen::UpdateMatched {
602            condition: bind_opt_expr(condition.as_ref(), r)?,
603            assignments: bind_assignments(assignments, r)?,
604        },
605        MergeWhen::DeleteMatched { condition } => MergeWhen::DeleteMatched {
606            condition: bind_opt_expr(condition.as_ref(), r)?,
607        },
608        MergeWhen::UpdateNotMatchedBySource {
609            condition,
610            assignments,
611        } => MergeWhen::UpdateNotMatchedBySource {
612            condition: bind_opt_expr(condition.as_ref(), r)?,
613            assignments: bind_assignments(assignments, r)?,
614        },
615        MergeWhen::DeleteNotMatchedBySource { condition } => MergeWhen::DeleteNotMatchedBySource {
616            condition: bind_opt_expr(condition.as_ref(), r)?,
617        },
618        MergeWhen::InsertNotMatched {
619            condition,
620            columns,
621            values,
622        } => MergeWhen::InsertNotMatched {
623            condition: bind_opt_expr(condition.as_ref(), r)?,
624            columns: columns.clone(),
625            values: bind_exprs(values, r)?,
626        },
627        MergeWhen::NothingMatched { condition } => MergeWhen::NothingMatched {
628            condition: bind_opt_expr(condition.as_ref(), r)?,
629        },
630        MergeWhen::NothingNotMatched { condition } => MergeWhen::NothingNotMatched {
631            condition: bind_opt_expr(condition.as_ref(), r)?,
632        },
633        MergeWhen::NothingNotMatchedBySource { condition } => {
634            MergeWhen::NothingNotMatchedBySource {
635                condition: bind_opt_expr(condition.as_ref(), r)?,
636            }
637        }
638    })
639}