1use 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
30pub struct SQLBodyValidationContext<'a> {
32 pub compilation: RoutineCompilationContext<'a>,
33 pub overloads: RoutineOverloadContext<'a>,
34}
35
36#[must_use]
38pub fn sql_body_is_analyzable(def: &CreateFunction) -> bool {
39 !def.identity_params()
40 .iter()
41 .any(|parameter| routine_polymorphic_type(¶meter.type_name).is_some())
42}
43
44pub 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 let parameters = matches!(def.body, FunctionBody::Source(_))
61 .then(|| sql_body_parameter_scope(def, ¶ms))
62 .transpose()?;
63 let mut last = None;
64 for plan in plans {
65 let mut statement = plan.clone();
66 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 ¶ms,
76 &scope.context(),
77 context.compilation.catalog,
78 parameters.as_ref(),
79 )?)
80 };
81 if let Some(parameters) = ¶meters {
82 bind_routine_parameter_references(
83 context.compilation.routines,
84 &mut statement,
85 ¶ms,
86 &scope.context(),
87 parameters,
88 )?;
89 }
90 last = Some(match query_result {
91 Some(result) => result,
92 None => analyze_body_statement(context, &statement, ¶ms, &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
159pub 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
184pub 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#[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(¶meter.type_name)
217 .or_else(|| ColumnType::from_sql_name(¶meter.type_name).ok())
218 {
219 Some(ty) => SQLParam::typed_scalar(Value::Null, ty),
220 None => SQLParam::scalar(Value::Null),
221 }
222 })
223 .collect()
224}