Skip to main content

uqa_sql/routines/
call.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Procedure argument validation, static overload selection, and declared result schemas.
8
9use crate::{
10    assignment::conversion::column_type_name,
11    ast::ColumnType,
12    ir::{analyze_expression_call_arguments, ScalarCallArgument},
13    plan::ExpressionPlan,
14    routines::{
15        declaration::RoutineTypeCatalog,
16        resolution::{RoutineCallKind, RoutineOverloadContext},
17    },
18    RowSchema, SQLError, ScalarExpr,
19};
20use uqa_core::Value;
21
22/// Reject forbidden subqueries before the caller captures its statement catalog scope.
23pub fn validate_call_arguments(arguments: &[ExpressionPlan]) -> Result<(), SQLError> {
24    if arguments
25        .iter()
26        .any(|argument| !argument.subqueries.is_empty())
27    {
28        return Err(SQLError::Unsupported(
29            "cannot use subquery in CALL argument".into(),
30        ));
31    }
32    Ok(())
33}
34
35/// Unknown string and NULL literals participate in procedure overload resolution without a concrete type.
36pub fn infer_call_argument_types(
37    arguments: &[ExpressionPlan],
38    decoded: &[ScalarCallArgument<'_>],
39    infer: &mut dyn FnMut(&ExpressionPlan) -> Result<Option<ColumnType>, SQLError>,
40) -> Result<Vec<Option<ColumnType>>, SQLError> {
41    arguments
42        .iter()
43        .zip(decoded)
44        .map(|(argument, call_argument)| {
45            if matches!(
46                call_argument.value,
47                ScalarExpr::Literal(Value::Str(_) | Value::Null)
48            ) {
49                Ok(None)
50            } else {
51                infer(argument)
52            }
53        })
54        .collect()
55}
56
57/// The syntax metadata needed before the caller captures the scope used for result description.
58pub struct ProcedureCallAnalysis<'a> {
59    arguments: &'a [ExpressionPlan],
60    decoded: Vec<ScalarCallArgument<'a>>,
61    names: Vec<Option<String>>,
62    explicit_variadic: bool,
63}
64
65impl<'a> ProcedureCallAnalysis<'a> {
66    pub fn new(arguments: &'a [ExpressionPlan]) -> Result<Self, SQLError> {
67        validate_call_arguments(arguments)?;
68        let (decoded, explicit_variadic) = analyze_expression_call_arguments(arguments)?;
69        let names = decoded
70            .iter()
71            .map(|argument| argument.name.map(str::to_string))
72            .collect();
73        Ok(Self {
74            arguments,
75            decoded,
76            names,
77            explicit_variadic,
78        })
79    }
80
81    pub fn result_schema(
82        &self,
83        name: &str,
84        overloads: &RoutineOverloadContext<'_>,
85        types: &dyn RoutineTypeCatalog,
86        infer: &mut dyn FnMut(&ExpressionPlan) -> Result<Option<ColumnType>, SQLError>,
87    ) -> Result<Option<RowSchema>, SQLError> {
88        let resolved = self.resolve(name, overloads, infer)?;
89        super::invocation::call_output_schema(
90            types,
91            &resolved.function.def,
92            &resolved.invocation.parameter_types,
93        )
94    }
95
96    pub(crate) fn resolve(
97        &self,
98        name: &str,
99        overloads: &RoutineOverloadContext<'_>,
100        infer: &mut dyn FnMut(&ExpressionPlan) -> Result<Option<ColumnType>, SQLError>,
101    ) -> Result<super::StaticFunctionMatch, SQLError> {
102        let argument_types = infer_call_argument_types(self.arguments, &self.decoded, infer)?;
103        let Some(resolved) = overloads.resolve_static_sql_routine_match(
104            name,
105            None,
106            &self.names,
107            &argument_types,
108            self.explicit_variadic,
109            RoutineCallKind::Procedure,
110        )?
111        else {
112            let signature = argument_types
113                .iter()
114                .map(|argument| {
115                    argument
116                        .as_ref()
117                        .map_or_else(|| "unknown", column_type_name)
118                })
119                .collect::<Vec<_>>()
120                .join(", ");
121            return Err(SQLError::Diagnostic {
122                sqlstate: "42883".into(),
123                message: format!("procedure {name}({signature}) does not exist"),
124                detail: None,
125                hint: Some(
126                    "No procedure matches the given name and argument types. You might need to add explicit type casts."
127                        .into(),
128                ),
129            });
130        };
131        Ok(resolved)
132    }
133}