Skip to main content

uqa_sql/binding/
routine_parameters.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! The parameters of a SQL routine as the outermost scope of the names in its body, resolved as `PostgreSQL`'s `sql_fn_post_column_ref` resolves them: a name is a parameter only when no column of any query level and no relation takes it, so a column of a queried table shadows the parameter of the same name, which the routine's own name then qualifies.
8
9use super::{BindingContext, QueryPlan, RowSchema, SQLError, SQLParam, ScalarExpr, SchemaScope};
10use crate::ast::{ColumnType, InternalRelationId};
11use crate::catalog::resolution::RelationLookupMode;
12use crate::plan::{CommandPlan, ExpressionPlan, ProjectionPlan, UnifiedPlan};
13use crate::routines::RoutineResolution;
14use std::sync::LazyLock;
15
16/// The opaque relation whose attributes mark the parameter slots of every row scope the parameter layer reaches, so that a name resolves to a parameter only through the layer itself.
17static ROUTINE_PARAMETERS: LazyLock<InternalRelationId> =
18    LazyLock::new(InternalRelationId::allocate);
19
20/// The parameters a SQL routine's body can name, in positional order.
21#[derive(Clone)]
22pub struct RoutineParameterScope {
23    /// The routine's unqualified name, which qualifies its parameter names.
24    function: String,
25    /// The parameter names; an unnamed parameter has an empty name and is reachable only by position.
26    names: Vec<String>,
27    schema: RowSchema,
28}
29
30impl RoutineParameterScope {
31    /// The parameters `names`, typed by `types`, of the routine whose unqualified name is `function`.
32    #[must_use]
33    pub fn new(function: &str, names: Vec<String>, types: Vec<Option<ColumnType>>) -> Self {
34        let visible = RowSchema::with_qualified_types(function, names.clone(), types.clone());
35        let marked = RowSchema::with_internal_relation_types(*ROUTINE_PARAMETERS, types);
36        Self {
37            function: function.to_string(),
38            names,
39            schema: RowSchema::with_trailing_internal_aliases(&visible, &marked),
40        }
41    }
42
43    /// The parameters as a row scope, the outermost scope of every name in the body.
44    #[must_use]
45    pub const fn schema(&self) -> &RowSchema {
46        &self.schema
47    }
48
49    /// The position of the parameter that occupies `slot` of `schema`.
50    fn position_at(schema: &RowSchema, slot: usize) -> Option<usize> {
51        schema
52            .unique_internal_column_for_slot(slot)
53            .filter(|column| column.relation() == *ROUTINE_PARAMETERS)
54            .map(crate::ast::InternalColumnRef::attribute)
55    }
56
57    /// Whether the parameter layer is part of `schema`.
58    fn reaches(schema: &RowSchema) -> bool {
59        schema.internal_slot(ROUTINE_PARAMETERS.column(0)).is_some()
60    }
61
62    /// The position of the parameter that `expression` names in `schema`, or `None` when it names a column, a relation, or nothing the layer holds.
63    fn parameter(&self, expression: &ScalarExpr, schema: &RowSchema) -> Option<usize> {
64        match expression {
65            ScalarExpr::Column(name) => {
66                let position = Self::position_at(schema, schema.column_slot(name)?)?;
67                // A relation of that name makes the name a whole-row reference, which the parser tries before the parameters.
68                (!self.names_relation(schema, name)).then_some(position)
69            }
70            ScalarExpr::QualifiedColumn { qualifier, column } => {
71                match schema.qualified_slot(qualifier, column) {
72                    Some(slot) => Self::position_at(schema, slot),
73                    // A relation that takes the routine's name but lacks the column leaves the name to the parameter, since the parser finds no column for it.
74                    None if *qualifier == self.function && Self::reaches(schema) => self
75                        .names
76                        .iter()
77                        .position(|name| !name.is_empty() && name == column),
78                    None => None,
79                }
80            }
81            _ => None,
82        }
83    }
84
85    /// Whether `name` is a relation visible in `schema`. The layer's own qualifier, the routine's name, is visible only where no relation takes that name.
86    fn names_relation(&self, schema: &RowSchema, name: &str) -> bool {
87        schema.has_qualifier(name)
88            && (name != self.function
89                || !self.names.iter().any(|parameter| {
90                    schema
91                        .qualified_slot(name, parameter)
92                        .and_then(|slot| Self::position_at(schema, slot))
93                        .is_some()
94                }))
95    }
96}
97
98/// The output name each select list item takes from the column it names, which the item keeps when the name turns out to be a parameter: `PostgreSQL` names the output column of a parameter reference after the reference as written.
99pub(super) fn column_labels(projections: &[ProjectionPlan]) -> Vec<Option<String>> {
100    projections
101        .iter()
102        .map(|projection| match &projection.expr {
103            ScalarExpr::Column(name) | ScalarExpr::QualifiedColumn { column: name, .. }
104                if projection.alias.is_none() =>
105            {
106                Some(name.clone())
107            }
108            _ => None,
109        })
110        .collect()
111}
112
113/// Name each select list item that `labels` took from a column name and that now refers to a parameter after that column name.
114pub(super) fn keep_column_labels(projections: &mut [ProjectionPlan], labels: Vec<Option<String>>) {
115    for (projection, label) in projections.iter_mut().zip(labels) {
116        if let Some(label) = label {
117            if matches!(projection.expr, ScalarExpr::Param(_)) {
118                projection.alias = Some(label);
119            }
120        }
121    }
122}
123
124impl SchemaScope {
125    /// Resolve a name at the point ordered semantic analysis visits it.
126    pub(super) fn routine_parameter_reference(
127        &self,
128        expression: &ScalarExpr,
129        schema: &RowSchema,
130    ) -> Option<usize> {
131        self.routine_parameters
132            .as_ref()
133            .and_then(|parameters| parameters.parameter(expression, schema))
134            .map(|position| position + 1)
135    }
136
137    /// Replace each reference in `expression` that resolves to a parameter of the routine whose body is bound with the positional parameter it names. `schema` is the row scope `expression` resolves against.
138    pub(super) fn canonicalize_routine_parameters(
139        &self,
140        expression: &mut ScalarExpr,
141        schema: &RowSchema,
142    ) {
143        let Some(parameters) = self.routine_parameters.as_ref() else {
144            return;
145        };
146        crate::plan::rewrite_scalar_expression(expression, &mut |node| {
147            if let Some(position) = parameters.parameter(node, schema) {
148                *node = ScalarExpr::Param(position + 1);
149            }
150        });
151    }
152
153    /// Walk one statement with `outer`, the parameters of the routine whose body it belongs to, as its outermost scope.
154    pub(super) fn bind_statement_parameters(
155        &mut self,
156        routines: &dyn RoutineResolution,
157        plan: &mut UnifiedPlan,
158        params: &[SQLParam],
159        outer: Option<&RowSchema>,
160    ) -> Result<(), SQLError> {
161        let command = match plan {
162            UnifiedPlan::Query(query) => {
163                return self.bind_query_parameters(routines, query, params, outer);
164            }
165            UnifiedPlan::Command(command) => command.as_mut(),
166        };
167        match command {
168            CommandPlan::Explain { body, .. } => {
169                self.bind_statement_parameters(routines, body, params, outer)
170            }
171            CommandPlan::CreateTableAs { query, .. }
172            | CommandPlan::CreateMaterializedView { query, .. }
173            | CommandPlan::DeclareCursor { query, .. } => {
174                self.bind_query_parameters(routines, query, params, outer)
175            }
176            CommandPlan::Call { args, .. } => {
177                for argument in args {
178                    self.bind_expression_parameters(routines, argument, params, outer)?;
179                }
180                Ok(())
181            }
182            command if command.mutation_target().is_some() => {
183                self.set_command_lookup_mode(command);
184                self.bind_command_routines_for_storage(routines, command, params, outer)
185            }
186            // A utility statement is not analyzed with the routine's parameters: `PostgreSQL` runs it as written.
187            _ => Ok(()),
188        }
189    }
190
191    /// Resolve the parameter references of a query, looking its relations up as bound identities or by name as its `relations_bound` flag records.
192    fn bind_query_parameters(
193        &mut self,
194        routines: &dyn RoutineResolution,
195        query: &mut QueryPlan,
196        params: &[SQLParam],
197        outer: Option<&RowSchema>,
198    ) -> Result<(), SQLError> {
199        let previous = self.resolution.set_lookup_mode(if query.relations_bound {
200            RelationLookupMode::Bound
201        } else {
202            RelationLookupMode::Dynamic
203        });
204        let result = self
205            .bind_query_routines_for_storage(routines, query, params, outer)
206            .map(|_| ());
207        self.resolution.set_lookup_mode(previous);
208        result
209    }
210
211    fn bind_expression_parameters(
212        &mut self,
213        routines: &dyn RoutineResolution,
214        expression: &mut ExpressionPlan,
215        params: &[SQLParam],
216        outer: Option<&RowSchema>,
217    ) -> Result<(), SQLError> {
218        for subquery in &mut expression.subqueries {
219            self.bind_query_parameters(routines, subquery, params, outer)?;
220        }
221        let schema = outer.cloned().unwrap_or_default();
222        self.canonicalize_routine_parameters(&mut expression.scalar, &schema);
223        self.resolve_variable_sites(&mut expression.scalar, &schema);
224        Ok(())
225    }
226}
227
228/// Resolve the names in one statement of a SQL routine's body that refer to the routine's parameters, against the catalog that `ctes` describes, as `PostgreSQL`'s parser resolves them with the hooks `sql_fn_parser_setup` installs. The routine calls the statement makes are left to its analysis.
229pub fn bind_routine_parameter_references(
230    routines: &dyn RoutineResolution,
231    plan: &mut UnifiedPlan,
232    params: &[SQLParam],
233    ctes: &BindingContext,
234    parameters: &RoutineParameterScope,
235) -> Result<(), SQLError> {
236    let mut scope = SchemaScope::for_analysis(ctes)?;
237    scope.routine_parameters = Some(parameters.clone());
238    scope.binds_routine_identities = false;
239    // Parameter lookup must retain the written expression sites for stored-body binding.
240    scope.preserve_syntax_shape = true;
241    scope.bind_statement_parameters(routines, plan, params, Some(parameters.schema()))
242}