Skip to main content

uqa_sql/binding/
variable_sites.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! The names in a `PL/pgSQL` statement that the function's variables take, checked against what the statement itself can see, as `PostgreSQL`'s `plpgsql_post_column_ref` checks them: a variable whose name a column or relation of the statement also takes is ambiguous unless `plpgsql.variable_conflict` chooses one of them, and an output column that a bare name in ORDER BY, GROUP BY or DISTINCT ON names takes the name before the parser asks for a variable.
8
9use super::{
10    projection_columns, BindingContext, QueryBlockPlan, RowSchema, SQLError, SQLParam, ScalarExpr,
11    SchemaScope,
12};
13use crate::plan::{ProjectionPlan, UnifiedPlan};
14use crate::routines::RoutineResolution;
15
16/// How a name that a `PL/pgSQL` variable takes resolves in the statement it appears in.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum VariableSiteResolution {
19    /// Nothing the statement can see there takes the name, so the variable does.
20    Variable,
21    /// A column or relation the statement can see there takes the name too.
22    Column,
23    /// An output column takes the name.
24    Output,
25}
26
27/// The variable sites of the statement a scope binds.
28pub(super) struct VariableSites {
29    /// The names as written; the site that the positional parameter `n` stands for is at index `n - 1`.
30    names: Vec<ScalarExpr>,
31    resolutions: Vec<VariableSiteResolution>,
32}
33
34impl VariableSites {
35    /// The site that `expression` stands for.
36    fn site(&self, expression: &ScalarExpr) -> Option<usize> {
37        match expression {
38            ScalarExpr::Param(number) => number
39                .checked_sub(1)
40                .filter(|site| *site < self.names.len()),
41            _ => None,
42        }
43    }
44
45    /// The output name a select list item that is the site takes: the name as written.
46    fn label(&self, site: usize) -> Option<&str> {
47        match &self.names[site] {
48            ScalarExpr::Column(name) | ScalarExpr::QualifiedColumn { column: name, .. } => {
49                Some(name)
50            }
51            _ => None,
52        }
53    }
54
55    /// The site's name when it is written without a qualifier.
56    fn bare_name(&self, site: usize) -> Option<&str> {
57        match &self.names[site] {
58            ScalarExpr::Column(name) => Some(name),
59            _ => None,
60        }
61    }
62}
63
64/// Whether a column or relation of `schema` takes `name`.
65fn takes(schema: &RowSchema, name: &ScalarExpr) -> bool {
66    match name {
67        ScalarExpr::Column(name) => {
68            schema.has_unqualified_column(name)
69                || schema.column_is_ambiguous(name)
70                || schema.has_qualifier(name)
71        }
72        ScalarExpr::QualifiedColumn { qualifier, column } => {
73            schema.has_qualified_column(qualifier, column)
74                || schema.qualified_column_is_ambiguous(qualifier, column)
75        }
76        _ => false,
77    }
78}
79
80impl SchemaScope {
81    /// Record each variable site in `expression` that a column or relation of `schema` also takes, and let the column take it for the rest of the analysis, as the parser's own resolution does before its hook reports the conflict.
82    pub(super) fn resolve_variable_sites(
83        &mut self,
84        expression: &mut ScalarExpr,
85        schema: &RowSchema,
86    ) {
87        let Some(sites) = self.variable_sites.as_mut() else {
88            return;
89        };
90        crate::plan::rewrite_scalar_expression(expression, &mut |node| {
91            if let Some(site) = sites.site(node) {
92                if sites.resolutions[site] == VariableSiteResolution::Variable
93                    && takes(schema, &sites.names[site])
94                {
95                    sites.resolutions[site] = VariableSiteResolution::Column;
96                    node.clone_from(&sites.names[site]);
97                }
98            }
99        });
100    }
101
102    /// The output names of a select list, a variable site taking the name it is written with.
103    pub(super) fn output_names(&self, projections: &[ProjectionPlan]) -> Vec<String> {
104        let mut names = projection_columns(projections);
105        if let Some(sites) = self.variable_sites.as_ref() {
106            for (name, projection) in names.iter_mut().zip(projections) {
107                if projection.alias.is_some() {
108                    continue;
109                }
110                if let Some(label) = sites
111                    .site(&projection.expr)
112                    .and_then(|site| sites.label(site))
113                {
114                    label.clone_into(name);
115                }
116            }
117        }
118        names
119    }
120
121    /// Whether `expression`, an ORDER BY or DISTINCT ON item, is a variable site whose bare name an output column takes, as `findTargetlistEntrySQL92` matches it before anything else; records the site.
122    pub(super) fn output_takes_variable_site(
123        &mut self,
124        expression: &ScalarExpr,
125        output_names: &[String],
126    ) -> bool {
127        let Some(sites) = self.variable_sites.as_mut() else {
128            return false;
129        };
130        let Some(site) = sites.site(expression) else {
131            return false;
132        };
133        if !sites
134            .bare_name(site)
135            .is_some_and(|name| output_names.iter().any(|output| output == name))
136        {
137            return false;
138        }
139        sites.resolutions[site] = VariableSiteResolution::Output;
140        true
141    }
142
143    /// Replace each GROUP BY item that is a variable site whose bare name no column of the query's own sources has but an output column does with that output column's expression, as `findTargetlistEntrySQL92` resolves it; records the site.
144    pub(super) fn bind_grouping_variable_sites(
145        &mut self,
146        block: &mut QueryBlockPlan,
147        source: &RowSchema,
148    ) {
149        let output_names = self.output_names(&block.projections);
150        let Some(sites) = self.variable_sites.as_mut() else {
151            return;
152        };
153        for item in block
154            .group_by
155            .iter_mut()
156            .chain(block.grouping_sets.iter_mut().flatten())
157        {
158            let Some(site) = sites.site(item) else {
159                continue;
160            };
161            let Some(name) = sites.bare_name(site) else {
162                continue;
163            };
164            if source.has_unqualified_column(name) || source.column_is_ambiguous(name) {
165                continue;
166            }
167            let Some(position) = output_names.iter().position(|output| output == name) else {
168                continue;
169            };
170            sites.resolutions[site] = VariableSiteResolution::Output;
171            item.clone_from(&block.projections[position].expr);
172        }
173    }
174}
175
176/// Find how each variable site of a `PL/pgSQL` statement resolves in the statement. `plan` is the statement with each name that the function's variables take replaced by the positional parameter whose number is the site's, typed by `params`, and `names` holds those names as written, in site order.
177pub fn resolve_variable_sites(
178    routines: &dyn RoutineResolution,
179    plan: &mut UnifiedPlan,
180    params: &[SQLParam],
181    ctes: &BindingContext,
182    names: Vec<ScalarExpr>,
183) -> Result<Vec<VariableSiteResolution>, SQLError> {
184    let mut scope = SchemaScope::for_analysis(ctes)?;
185    scope.binds_routine_identities = false;
186    scope.variable_sites = Some(VariableSites {
187        resolutions: vec![VariableSiteResolution::Variable; names.len()],
188        names,
189    });
190    let walked = scope.bind_statement_parameters(routines, plan, params, None);
191    let resolutions = scope
192        .variable_sites
193        .take()
194        .map(|sites| sites.resolutions)
195        .unwrap_or_default();
196    match walked {
197        Ok(()) => Ok(resolutions),
198        // The walk meets a name in the order the parser analyzes it, so a conflict it met before an error that analysis finds later, such as an ungrouped column, is what the parser reports.
199        Err(_) if resolutions.contains(&VariableSiteResolution::Column) => Ok(resolutions),
200        Err(error) => Err(error),
201    }
202}