Skip to main content

uqa_sql/routines/
compilation.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Routine language validation and body lowering over declared types and fresh binding inputs.
8
9use super::{
10    declaration::{
11        resolve_plpgsql_datum_types, routine_definition_error, routine_parameter_regrole_constants,
12        validate_routine_declaration, RoutineTypeCatalog,
13    },
14    merge_columns::StoredMergeColumnCatalog,
15    CompiledFunctionBody, RoutineResolution,
16};
17use crate::{
18    ast::{ColumnType, CreateFunction, FunctionBody, FunctionReturns, Statement},
19    binding::{
20        snapshot::BindingSnapshot,
21        stored_relations::{
22            self, StoredQueryBindingContext, StoredQueryNamespace, StoredQuerySequences,
23            StoredRelationCatalog,
24        },
25    },
26    catalog::{
27        regrole_dependencies::{StoredRegroleConstants, StoredRegroleResolver},
28        resolution::RelationLookupMode,
29    },
30    plan::UnifiedPlan,
31    plpgsql::PlpgsqlCatalog,
32    type_resolution::canonical_routine_type_name,
33    SQLError, ScalarExpr,
34};
35
36pub trait RoutineParserCatalog {
37    fn plpgsql_catalog(&self) -> Result<PlpgsqlCatalog, SQLError>;
38    fn parser_settings(&self) -> crate::parser::ParserSettings {
39        crate::parser::ParserSettings::default()
40    }
41    fn parser_notice(&self, _notice: crate::SQLNotice) {}
42}
43pub trait RoutineCompilationCatalog: crate::schema::dependencies::oid_alias::OidAliasInput {
44    fn has_registered_aggregate_function(&self, name: &str) -> bool;
45    fn binding_snapshot(&self) -> Result<BindingSnapshot, SQLError>;
46    fn stored_query_namespace(&self) -> StoredQueryNamespace;
47}
48#[derive(Clone, Copy)]
49pub struct RoutineCompilationContext<'a> {
50    pub types: &'a dyn RoutineTypeCatalog,
51    pub parsers: &'a dyn RoutineParserCatalog,
52    pub catalog: &'a dyn RoutineCompilationCatalog,
53    pub routines: &'a dyn RoutineResolution,
54    pub relations: &'a dyn StoredRelationCatalog,
55    pub sequences: &'a dyn StoredQuerySequences,
56    pub merge: &'a dyn StoredMergeColumnCatalog,
57    pub regroles: &'a dyn StoredRegroleResolver,
58}
59
60pub fn compile_function_body(
61    context: &RoutineCompilationContext<'_>,
62    def: &CreateFunction,
63) -> Result<CompiledFunctionBody, SQLError> {
64    compile_function_body_inner(
65        context,
66        def,
67        false,
68        crate::plpgsql::PLpgSQLCompileMode::Validate,
69    )
70}
71
72/// Compile executable procedural structure without validating unreached SQL fragments.
73pub fn compile_function_body_for_execution(
74    context: &RoutineCompilationContext<'_>,
75    def: &CreateFunction,
76) -> Result<CompiledFunctionBody, SQLError> {
77    compile_function_body_inner(
78        context,
79        def,
80        false,
81        crate::plpgsql::PLpgSQLCompileMode::Runtime,
82    )
83}
84
85pub fn compile_persisted_function_body(
86    context: &RoutineCompilationContext<'_>,
87    def: &CreateFunction,
88) -> Result<CompiledFunctionBody, SQLError> {
89    compile_function_body_inner(
90        context,
91        def,
92        true,
93        crate::plpgsql::PLpgSQLCompileMode::Validate,
94    )
95}
96
97/// The body `CREATE FUNCTION` compiles under `check_function_bodies = off`: the declaration is checked as always, a SQL-standard body, which the statement itself analyzes, is compiled, and a body given as a string is left unexamined, `None`, for each session to compile when it first calls the routine.
98pub fn defer_function_body(
99    context: &RoutineCompilationContext<'_>,
100    def: &CreateFunction,
101) -> Result<Option<CompiledFunctionBody>, SQLError> {
102    if matches!(def.body, FunctionBody::Statements(_)) {
103        return compile_function_body(context, def).map(Some);
104    }
105    validate_routine_signature(context, def)?.reject_with(context.regroles)?;
106    Ok(None)
107}
108
109/// The checks `CREATE FUNCTION` makes whatever `check_function_bodies` says: the language and the body form it accepts, the declared types, and the role constants of parameter defaults, which are returned for the body's own checks.
110pub(super) fn validate_routine_signature(
111    context: &RoutineCompilationContext<'_>,
112    def: &CreateFunction,
113) -> Result<StoredRegroleConstants, SQLError> {
114    if !matches!(def.language.as_str(), "plpgsql" | "sql") {
115        return Err(SQLError::Routine {
116            sqlstate: "42704".into(),
117            message: format!("language \"{}\" does not exist", def.language),
118        });
119    }
120    if def.language == "plpgsql" && matches!(def.body, FunctionBody::Statements(_)) {
121        return Err(routine_definition_error(
122            "inline SQL function body only valid for language SQL",
123        ));
124    }
125    let stored_regrole_constants = routine_parameter_regrole_constants(context.types, def);
126    stored_regrole_constants.validate_inputs_with(context.regroles)?;
127    validate_routine_declaration(def)?;
128    Ok(stored_regrole_constants)
129}
130
131/// PL/pgSQL's compiler rejects declared arguments of a trigger function, which reads its arguments from `TG_ARGV`.
132fn reject_trigger_function_arguments(def: &CreateFunction) -> Result<(), SQLError> {
133    let returns_trigger = matches!(
134        &def.returns,
135        FunctionReturns::Scalar { type_name } if canonical_routine_type_name(type_name) == "trigger"
136    );
137    if returns_trigger && def.identity_arity() != 0 {
138        return Err(SQLError::Diagnostic {
139            sqlstate: "42P13".into(),
140            message: "trigger functions cannot have declared arguments".into(),
141            detail: None,
142            hint: Some(
143                "The arguments of the trigger can be accessed through TG_NARGS and TG_ARGV instead."
144                    .into(),
145            ),
146        });
147    }
148    Ok(())
149}
150
151fn compile_function_body_inner(
152    context: &RoutineCompilationContext<'_>,
153    def: &CreateFunction,
154    persisted_definition: bool,
155    mode: crate::plpgsql::PLpgSQLCompileMode,
156) -> Result<CompiledFunctionBody, SQLError> {
157    with_parser_context(context.parsers, || {
158        compile_body(context, def, persisted_definition, mode)
159    })
160}
161
162/// Read live lexical settings only after the routine's own configuration has been applied, and deliver parser notices even if compilation fails.
163pub fn with_parser_context<T>(
164    parsers: &dyn RoutineParserCatalog,
165    compile: impl FnOnce() -> Result<T, SQLError>,
166) -> Result<T, SQLError> {
167    let (result, metadata) = crate::parser::with_settings(parsers.parser_settings(), compile);
168    for notice in metadata.notices.iter() {
169        parsers.parser_notice(notice.clone());
170    }
171    result
172}
173
174fn compile_body(
175    context: &RoutineCompilationContext<'_>,
176    def: &CreateFunction,
177    persisted_definition: bool,
178    mode: crate::plpgsql::PLpgSQLCompileMode,
179) -> Result<CompiledFunctionBody, SQLError> {
180    let mut stored_regrole_constants = validate_routine_signature(context, def)?;
181    match def.language.as_str() {
182        "plpgsql" => {
183            stored_regrole_constants.reject_with(context.regroles)?;
184            reject_trigger_function_arguments(def)?;
185            let catalog = context.parsers.plpgsql_catalog()?;
186            let mut function =
187                crate::plpgsql::parse_function_with_catalog_mode(def, &catalog, mode)?;
188            resolve_plpgsql_datum_types(context.types, &mut function)?;
189            Ok(CompiledFunctionBody::PLpgSQL(function))
190        }
191        "sql" => {
192            let (statements, bind_catalog_dependencies) = match &def.body {
193                FunctionBody::Source(source) => {
194                    stored_regrole_constants.reject_with(context.regroles)?;
195                    (crate::compile(source)?, false)
196                }
197                FunctionBody::Statements(statements) => (statements.clone(), true),
198            };
199            let mut plans = compile_sql_routine_plans(
200                context,
201                def,
202                statements,
203                bind_catalog_dependencies,
204                persisted_definition && matches!(def.body, FunctionBody::Statements(_)),
205            )?;
206            if bind_catalog_dependencies {
207                for plan in &mut plans {
208                    stored_regrole_constants.collect_plan(plan);
209                }
210                stored_regrole_constants.reject_with(context.regroles)?;
211            }
212            Ok(CompiledFunctionBody::SQL(plans))
213        }
214        _ => unreachable!("routine language was validated above"),
215    }
216}
217
218fn compile_sql_routine_plans(
219    context: &RoutineCompilationContext<'_>,
220    def: &CreateFunction,
221    statements: Vec<Statement>,
222    bind_catalog_dependencies: bool,
223    persisted_definition: bool,
224) -> Result<Vec<UnifiedPlan>, SQLError> {
225    let positional_parameters =
226        super::body_validation::routine_parameter_values(context.types, def);
227    let parameters = super::body_parameters::sql_body_parameter_scope(def, &positional_parameters)?;
228    statements
229        .into_iter()
230        .map(|statement| {
231            let mut plan = lower_sql_routine_statement(
232                context,
233                statement,
234                SQLRoutineLowering {
235                    bind_catalog_dependencies,
236                    persisted_definition,
237                    preserve_target_expressions: false,
238                },
239            )?;
240            // A SQL-standard body keeps definition-time names. Source syntax
241            // stays unbound so statement reanalysis can use the current path;
242            // successful first-use analysis retains its own type identities.
243            if bind_catalog_dependencies {
244                let binding = context.catalog.binding_snapshot()?;
245                crate::binding::bind_routine_parameter_references(
246                    context.routines,
247                    &mut plan,
248                    &positional_parameters,
249                    &binding.context(),
250                    &parameters,
251                )?;
252                if let UnifiedPlan::Query(query) = &mut plan {
253                    crate::binding::bind_query_plan_routines_for_storage(
254                        context.routines,
255                        query,
256                        &positional_parameters,
257                        &binding.context(),
258                        None,
259                    )?;
260                }
261            }
262            // Stored definitions retain their analyzed logical expressions;
263            // immutable evaluation belongs to invocation planning.
264            Ok(plan)
265        })
266        .collect()
267}
268
269/// How a SQL routine statement is lowered from its stored syntax.
270#[derive(Clone, Copy)]
271pub struct SQLRoutineLowering {
272    /// Bind relations, sequences and `MERGE` target columns of catalog-owned syntax.
273    pub bind_catalog_dependencies: bool,
274    /// The syntax comes from a persisted definition, whose legacy call markers are upgraded.
275    pub persisted_definition: bool,
276    /// Keep `MERGE` target columns as written.
277    pub preserve_target_expressions: bool,
278}
279
280/// Lower one statement of a SQL routine body with its relations bound, before routine calls are bound.
281pub fn lower_sql_routine_statement(
282    context: &RoutineCompilationContext<'_>,
283    mut statement: Statement,
284    lowering: SQLRoutineLowering,
285) -> Result<UnifiedPlan, SQLError> {
286    if lowering.bind_catalog_dependencies {
287        validate_sql_standard_statement(&statement)?;
288    }
289    if lowering.bind_catalog_dependencies && !lowering.preserve_target_expressions {
290        super::merge_columns::normalize_stored_merge_target_columns(context.merge, &mut statement)?;
291    }
292    let mut plan = UnifiedPlan::lower_with(statement, &|name: &str| {
293        context.catalog.has_registered_aggregate_function(name)
294    });
295    if lowering.persisted_definition {
296        plan.rewrite_scalar_expressions(&mut |expression| {
297            let ScalarExpr::Func {
298                name,
299                binding,
300                args,
301                ..
302            } = expression
303            else {
304                return;
305            };
306            crate::ast::FunctionBinding::upgrade_legacy_serialized_dispatch(name, binding);
307            crate::ast::FunctionBinding::upgrade_legacy_array_slices(
308                binding,
309                args.len(),
310                |index| matches!(args[index], ScalarExpr::Literal(uqa_core::Value::Null)),
311            );
312        });
313    }
314    if lowering.bind_catalog_dependencies {
315        match &mut plan {
316            UnifiedPlan::Query(query) => {
317                let namespace = context.catalog.stored_query_namespace();
318                stored_relations::bind_stored_query_relations(
319                    &StoredQueryBindingContext {
320                        relations: context.relations,
321                        lookup_mode: if lowering.persisted_definition {
322                            RelationLookupMode::Bound
323                        } else {
324                            RelationLookupMode::Dynamic
325                        },
326                        sequences: context.sequences,
327                        temporary_schema: &namespace.temporary_schema,
328                        transition_relations: &namespace.transition_relations,
329                    },
330                    query,
331                    "SQL routine body",
332                    false,
333                    lowering.persisted_definition,
334                )?;
335            }
336            UnifiedPlan::Command(_) => {
337                crate::binding::stored_routines::mark_catalog_statement_relations_bound(&mut plan)?;
338            }
339        }
340    }
341    Ok(plan)
342}
343
344/// Foreign server and table utilities are analyzed when it executes and cannot be retained as an analyzed SQL-standard body. Quoted source bodies keep their separate execution-time path.
345pub(super) fn validate_sql_standard_statement(statement: &Statement) -> Result<(), SQLError> {
346    if matches!(statement, Statement::Drop(drop) if drop.kind == crate::ast::DropKind::ForeignServer)
347    {
348        return Err(SQLError::Unsupported(
349            "DROP SERVER is not yet supported in unquoted SQL function body".into(),
350        ));
351    }
352    if matches!(statement, Statement::Drop(drop) if drop.kind == crate::ast::DropKind::ForeignWrapper)
353    {
354        return Err(SQLError::Unsupported(
355            "DROP FOREIGN DATA WRAPPER is not yet supported in unquoted SQL function body".into(),
356        ));
357    }
358    if matches!(statement, Statement::CreateForeignWrapper(_)) {
359        return Err(SQLError::Unsupported(
360            "CREATE FOREIGN DATA WRAPPER is not yet supported in unquoted SQL function body".into(),
361        ));
362    }
363    if matches!(statement, Statement::CreateForeignServer(_)) {
364        return Err(SQLError::Unsupported(
365            "CREATE SERVER is not yet supported in unquoted SQL function body".into(),
366        ));
367    }
368    if matches!(
369        statement,
370        Statement::CreateForeignTable(_) | Statement::CreateForeignTableDefinition(_)
371    ) {
372        return Err(SQLError::Unsupported(
373            "CREATE FOREIGN TABLE is not yet supported in unquoted SQL function body".into(),
374        ));
375    }
376    Ok(())
377}
378
379/// A successfully analyzed body keeps resolved type identities independently of
380/// its raw source. Domain coercions still resolve current constraint definitions.
381pub fn bind_analyzed_sql_body_types(
382    types: &dyn RoutineTypeCatalog,
383    plan: &mut UnifiedPlan,
384) -> Result<(), SQLError> {
385    crate::binding::stored_types::bind_unified_plan_type_identities(plan, &mut |name| {
386        let resolved = types.resolve_catalog_column_type(name);
387        let mut element = resolved.as_ref();
388        while let Some(ColumnType::Array(inner)) = element {
389            element = Some(inner.as_ref());
390        }
391        let domain = matches!(element, Some(ColumnType::Domain { .. }));
392        Ok(resolved.filter(|_| !domain))
393    })
394}
395
396#[cfg(test)]
397mod tests;