Skip to main content

uqa_sql/routines/
dependencies.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Bind stored routine relation, column, and routine identities against fresh catalog inputs.
8
9use super::compilation::RoutineCompilationContext;
10use crate::{
11    ast::{CreateFunction, FunctionBody},
12    binding::{
13        bind_expression_plan_routines_for_storage,
14        stored_columns::StoredSourceCatalog,
15        stored_relations::bind_stored_statement_relations,
16        stored_routines::{bind_catalog_statement_routines, CatalogRoutineContext},
17        syntax_sites::expression_syntax_sites,
18    },
19    catalog::{resolution::RelationLookupMode, stored_ast},
20    plan::ExpressionPlan,
21    RowSchema, SQLError,
22};
23
24#[derive(Clone, Copy)]
25pub enum RoutineCompilationMode {
26    Definition,
27    Persisted,
28}
29
30pub fn bind_routine_definition_dependencies(
31    context: &RoutineCompilationContext<'_>,
32    sources: &dyn StoredSourceCatalog,
33    def: &mut CreateFunction,
34    mode: RoutineCompilationMode,
35) -> Result<bool, SQLError> {
36    let mut changed = false;
37    if matches!(mode, RoutineCompilationMode::Persisted) {
38        if let FunctionBody::Statements(statements) = &mut def.body {
39            for statement in statements {
40                changed |= crate::binding::legacy_patterns::restore_statement(
41                    statement,
42                    context.routines,
43                )?;
44            }
45        }
46        for default in def
47            .params
48            .iter_mut()
49            .filter_map(|parameter| parameter.default.as_mut())
50        {
51            changed |=
52                crate::binding::legacy_patterns::restore_expression(default, context.routines)
53                    .map_err(SQLError::Internal)?;
54        }
55    }
56    changed |= bind_sql_standard_body_relations(context, sources, def, mode)?;
57    for parameter in &mut def.params {
58        let Some(default) = &mut parameter.default else {
59            continue;
60        };
61        let lowered = ExpressionPlan::lower_with(default.clone(), &|name: &str| {
62            context.catalog.has_registered_aggregate_function(name)
63        });
64        let mut plan = lowered.clone();
65        let binding = context.catalog.binding_snapshot()?;
66        let default_type = bind_expression_plan_routines_for_storage(
67            context.routines,
68            &mut plan,
69            &[],
70            &binding.context(),
71            &RowSchema::default(),
72        )?;
73        let default_type =
74            if crate::type_resolution::routine_polymorphic_type(&parameter.type_name).is_some() {
75                default_type
76            } else {
77                Some(
78                    context
79                        .types
80                        .resolve_catalog_column_type_name(&parameter.type_name)?
81                        .without_type_modifiers(),
82                )
83            };
84        let default_type =
85            super::defaults::default_expression_type(&parameter.type_name, default, default_type);
86        if parameter.default_type != default_type {
87            parameter.default_type = default_type;
88            changed = true;
89        }
90        let sites = expression_syntax_sites(&lowered, &plan)?;
91        changed |= stored_ast::bind_stored_expression_sites(default, &sites)?;
92        // A default is assigned to its parameter.
93        if let Some(ty) = user_defined_type(context, &parameter.type_name)? {
94            changed |= stored_ast::fold_assigned_stored_literal(
95                default,
96                &ty,
97                context.routines.enum_labels(),
98            )?;
99        }
100    }
101    Ok(changed)
102}
103
104fn bind_sql_standard_body_relations(
105    context: &RoutineCompilationContext<'_>,
106    sources: &dyn StoredSourceCatalog,
107    def: &mut CreateFunction,
108    mode: RoutineCompilationMode,
109) -> Result<bool, SQLError> {
110    let FunctionBody::Statements(statements) = &mut def.body else {
111        return Ok(false);
112    };
113    let mut changed = false;
114    for statement in statements {
115        super::compilation::validate_sql_standard_statement(statement)?;
116        changed |= bind_stored_statement_relations(
117            context.relations,
118            statement,
119            match mode {
120                RoutineCompilationMode::Definition => RelationLookupMode::Dynamic,
121                RoutineCompilationMode::Persisted => RelationLookupMode::Bound,
122            },
123            matches!(mode, RoutineCompilationMode::Persisted),
124            "SQL routine body",
125        )?;
126        changed |=
127            super::merge_columns::bind_stored_merge_target_columns(context.merge, statement)?;
128        changed |= sources.stored_source_columns().bind_statement(statement)?;
129    }
130    Ok(changed)
131}
132
133/// Bind a copy of every statement of a SQL-standard body and carry the bound routine identities, user-defined type identities and enum constants back into the stored statements, then convert the result literals the final statement assigns to the declared result. A name resolves to a parameter only when no column of its statement takes it, as `sql_fn_post_column_ref` resolves it; the final statement's result is checked when the body is validated.
134pub fn bind_sql_standard_body_routines(
135    context: &RoutineCompilationContext<'_>,
136    def: &mut CreateFunction,
137    mode: RoutineCompilationMode,
138) -> Result<bool, SQLError> {
139    if !matches!(def.body, FunctionBody::Statements(_)) {
140        return Ok(false);
141    }
142    // Declaration errors precede body input functions, which can execute domain checks.
143    super::compilation::validate_routine_signature(context, def)?;
144    let positional = super::body_validation::routine_parameter_values(context.types, def);
145    let parameters = super::body_parameters::sql_body_parameter_scope(def, &positional)?;
146    let result_types = def_result_types(context, &def.params, &def.returns)?;
147    let FunctionBody::Statements(statements) = &mut def.body else {
148        return Ok(false);
149    };
150    let lowering = super::compilation::SQLRoutineLowering {
151        bind_catalog_dependencies: true,
152        persisted_definition: matches!(mode, RoutineCompilationMode::Persisted),
153        preserve_target_expressions: true,
154    };
155    let mut changed = false;
156    for statement in statements.iter_mut() {
157        let mut lowered =
158            super::compilation::lower_sql_routine_statement(context, statement.clone(), lowering)?;
159        let binding = context.catalog.binding_snapshot()?;
160        crate::binding::bind_routine_parameter_references(
161            context.routines,
162            &mut lowered,
163            &positional,
164            &binding.context(),
165            &parameters,
166        )?;
167        let routines = bind_catalog_statement_routines(
168            &CatalogRoutineContext {
169                routines: context.routines,
170                binding: &binding.context(),
171            },
172            &lowered,
173            &positional,
174        )?;
175        changed |= stored_ast::bind_stored_statement_sites(statement, &routines.sites)?;
176    }
177    if let Some(statement) = statements.last_mut() {
178        changed |= fold_sql_function_result(context, result_types, statement)?;
179    }
180    Ok(changed)
181}
182
183/// A declared routine type that is user-defined, the only kind whose `unknown` literals parse analysis converts to stored enum constants. Declarations name user-defined types by identity.
184fn user_defined_type(
185    context: &RoutineCompilationContext<'_>,
186    name: &str,
187) -> Result<Option<crate::ColumnType>, SQLError> {
188    if crate::ast::UserTypeIdentity::parse(name).is_none() {
189        return Ok(None);
190    }
191    context.routines.resolve_type_name(name)
192}
193
194/// The column types a SQL function's final statement is coerced to: the `OUT` and `TABLE` parameters, or else the declared result. Built-in and pseudo-type columns are `None`.
195fn def_result_types(
196    context: &RoutineCompilationContext<'_>,
197    params: &[crate::ast::FunctionParam],
198    returns: &crate::ast::FunctionReturns,
199) -> Result<Vec<Option<crate::ColumnType>>, SQLError> {
200    use crate::ast::{FunctionParamMode, FunctionReturns};
201    let outputs = params
202        .iter()
203        .filter(|parameter| {
204            matches!(
205                parameter.mode,
206                FunctionParamMode::Out | FunctionParamMode::InOut | FunctionParamMode::Table
207            )
208        })
209        .collect::<Vec<_>>();
210    let names = if outputs.is_empty() {
211        match returns {
212            FunctionReturns::Scalar { type_name } | FunctionReturns::SetOf { type_name } => {
213                vec![type_name.as_str()]
214            }
215            FunctionReturns::None | FunctionReturns::Table => Vec::new(),
216        }
217    } else {
218        outputs
219            .iter()
220            .map(|parameter| parameter.type_name.as_str())
221            .collect()
222    };
223    names
224        .into_iter()
225        .map(|name| user_defined_type(context, name))
226        .collect()
227}
228
229/// `check_sql_fn_retval` coerces each result column of the final statement to its declared type; an `unknown` literal selected directly becomes a constant of that type.
230fn fold_sql_function_result(
231    context: &RoutineCompilationContext<'_>,
232    types: Vec<Option<crate::ColumnType>>,
233    statement: &mut crate::ast::Statement,
234) -> Result<bool, SQLError> {
235    let crate::ast::Statement::Select(select) = statement else {
236        return Ok(false);
237    };
238    if types.is_empty()
239        || select.set_op.is_some()
240        || !select.values.is_empty()
241        || select.projections.len() != types.len()
242    {
243        return Ok(false);
244    }
245    let mut changed = false;
246    for (projection, ty) in select.projections.iter_mut().zip(&types) {
247        let Some(ty) = ty else {
248            continue;
249        };
250        changed |= stored_ast::fold_assigned_stored_literal(
251            &mut projection.expr,
252            ty,
253            context.routines.enum_labels(),
254        )?;
255    }
256    Ok(changed)
257}