Skip to main content

uqa_sql/routines/
body_validation.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! The body checks of `PostgreSQL`'s SQL-language validator, `fmgr_sql_validator`, which also hold when a body runs: each statement is analyzed against the catalog before it runs, a reference to a parameter the routine lacks is an error, a `CALL` of a procedure with output arguments is rejected, and the final statement is checked against the declared result.
8
9use super::{
10    body_parameters::{sql_body_parameter_scope, sql_body_parameters},
11    call::ProcedureCallAnalysis,
12    compilation::RoutineCompilationContext,
13    declaration::RoutineTypeCatalog,
14    resolution::RoutineOverloadContext,
15    result_check::check_sql_function_result,
16    CompiledFunctionBody,
17};
18use crate::{
19    ast::{ColumnType, CreateFunction, FunctionBody},
20    binding::{
21        bind_routine_parameter_references,
22        statements::{AnalyzedResult, StatementBindingScope},
23    },
24    plan::{CommandPlan, ExpressionPlan, UnifiedPlan},
25    type_resolution::routine_polymorphic_type,
26    SQLError, SQLParam, ScalarExpr,
27};
28use uqa_core::Value;
29
30/// The catalog `CREATE FUNCTION` validates a SQL body against.
31pub struct SQLBodyValidationContext<'a> {
32    pub compilation: RoutineCompilationContext<'a>,
33    pub overloads: RoutineOverloadContext<'a>,
34}
35
36/// Whether a SQL body can be analyzed without the argument types of a call: `PostgreSQL` only parses the body of a routine with polymorphic arguments until a call supplies them.
37#[must_use]
38pub fn sql_body_is_analyzable(def: &CreateFunction) -> bool {
39    !def.identity_params()
40        .iter()
41        .any(|parameter| routine_polymorphic_type(&parameter.type_name).is_some())
42}
43
44/// Validate a compiled SQL body as `CREATE FUNCTION` does: analyze each statement in order against the catalog, without running any of them, and when `check_result` holds, check the final statement against the declared result.
45pub fn validate_sql_function_body(
46    context: &SQLBodyValidationContext<'_>,
47    def: &CreateFunction,
48    body: &CompiledFunctionBody,
49    check_result: bool,
50) -> Result<(), SQLError> {
51    let CompiledFunctionBody::SQL(plans) = body else {
52        return Ok(());
53    };
54    if !sql_body_is_analyzable(def) {
55        return Ok(());
56    }
57    let params = routine_parameter_values(context.compilation.types, def);
58    let scope = context.compilation.catalog.binding_snapshot()?;
59    // A SQL-standard body resolved its parameter names when it was compiled.
60    let parameters = matches!(def.body, FunctionBody::Source(_))
61        .then(|| sql_body_parameter_scope(def, &params))
62        .transpose()?;
63    let mut last = None;
64    for plan in plans {
65        let mut statement = plan.clone();
66        // Statement inputs are analyzed before the name-lowering pass derives its
67        // output schema, so a later bad name cannot hide an earlier input error.
68        let query_result = if matches!(&statement, UnifiedPlan::Command(command) if matches!(command.as_ref(), CommandPlan::Call { .. }))
69        {
70            None
71        } else {
72            Some(crate::binding::analyze_routine_body_inputs(
73                context.compilation.routines,
74                &statement,
75                &params,
76                &scope.context(),
77                context.compilation.catalog,
78                parameters.as_ref(),
79            )?)
80        };
81        if let Some(parameters) = &parameters {
82            bind_routine_parameter_references(
83                context.compilation.routines,
84                &mut statement,
85                &params,
86                &scope.context(),
87                parameters,
88            )?;
89        }
90        last = Some(match query_result {
91            Some(result) => result,
92            None => analyze_body_statement(context, &statement, &params, &scope)?,
93        });
94    }
95    if check_result {
96        check_sql_function_result(context.compilation.types, def, last.as_ref())?;
97    }
98    Ok(())
99}
100
101fn analyze_body_statement(
102    context: &SQLBodyValidationContext<'_>,
103    plan: &UnifiedPlan,
104    params: &[SQLParam],
105    scope: &dyn StatementBindingScope,
106) -> Result<AnalyzedResult, SQLError> {
107    if let UnifiedPlan::Command(command) = plan {
108        if let CommandPlan::Call { name, args } = command.as_ref() {
109            let binding = scope.binding_context()?;
110            let call = ProcedureCallAnalysis::new(args)?;
111            let infer = |argument: &ExpressionPlan, target: Option<&ColumnType>| {
112                crate::binding::analyze_routine_body_argument(
113                    context.compilation.routines,
114                    argument,
115                    params,
116                    &binding,
117                    context.compilation.catalog,
118                    target,
119                )
120            };
121            let resolved = call.resolve(name, &context.overloads, &mut |argument| {
122                infer(argument, None)
123            })?;
124            let (arguments, _) = crate::ir::analyze_expression_call_arguments(args)?;
125            for (argument, target) in arguments.iter().zip(&resolved.invocation.argument_targets) {
126                if matches!(
127                    argument.value,
128                    ScalarExpr::Literal(Value::Str(_) | Value::Null)
129                ) {
130                    let target = context
131                        .compilation
132                        .types
133                        .resolve_catalog_column_type_name(target)?;
134                    infer(
135                        &ExpressionPlan {
136                            scalar: argument.value.clone(),
137                            subqueries: Vec::new(),
138                        },
139                        Some(&target),
140                    )?;
141                }
142            }
143            if !resolved.function.def.output_params().is_empty() {
144                return Err(output_argument_call_error());
145            }
146            return Ok(AnalyzedResult::Command);
147        }
148    }
149    crate::binding::analyze_routine_body_inputs(
150        context.compilation.routines,
151        plan,
152        params,
153        &scope.binding_context()?,
154        context.compilation.catalog,
155        None,
156    )
157}
158
159/// Resolve the procedure a `CALL` names and reject one with output arguments, which `PostgreSQL` does not support in SQL functions.
160pub fn reject_output_argument_call(
161    overloads: &RoutineOverloadContext<'_>,
162    types: &dyn RoutineTypeCatalog,
163    name: &str,
164    arguments: &[ExpressionPlan],
165    infer: &mut dyn FnMut(&ExpressionPlan) -> Result<Option<ColumnType>, SQLError>,
166) -> Result<(), SQLError> {
167    if ProcedureCallAnalysis::new(arguments)?
168        .result_schema(name, overloads, types, infer)?
169        .is_some()
170    {
171        return Err(output_argument_call_error());
172    }
173    Ok(())
174}
175
176fn output_argument_call_error() -> SQLError {
177    SQLError::Routine {
178        sqlstate: "0A000".into(),
179        message: "calling procedures with output arguments is not supported in SQL functions"
180            .into(),
181    }
182}
183
184/// Reject a reference to a parameter the routine does not declare, as `PostgreSQL`'s parser reports it.
185pub fn reject_undefined_parameters(
186    plan: &mut UnifiedPlan,
187    declared: usize,
188) -> Result<(), SQLError> {
189    let mut undefined = None;
190    plan.rewrite_scalar_expressions(&mut |expression| {
191        if let ScalarExpr::Param(index) = expression {
192            if *index > declared && undefined.is_none() {
193                undefined = Some(*index);
194            }
195        }
196    });
197    match undefined {
198        Some(index) => Err(SQLError::Routine {
199            sqlstate: "42P02".into(),
200            message: format!("there is no parameter ${index}"),
201        }),
202        None => Ok(()),
203    }
204}
205
206/// Typed placeholders for the parameters the body names, against which analysis types references to them.
207#[must_use]
208pub fn routine_parameter_values(
209    types: &dyn RoutineTypeCatalog,
210    def: &CreateFunction,
211) -> Vec<SQLParam> {
212    sql_body_parameters(def)
213        .iter()
214        .map(|parameter| {
215            match types
216                .resolve_catalog_column_type(&parameter.type_name)
217                .or_else(|| ColumnType::from_sql_name(&parameter.type_name).ok())
218            {
219                Some(ty) => SQLParam::typed_scalar(Value::Null, ty),
220                None => SQLParam::scalar(Value::Null),
221            }
222        })
223        .collect()
224}