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 argument_types = infer_call_argument_types(self.arguments, &self.decoded, infer)?;
89        let Some(resolved) = overloads.resolve_static_sql_routine_match(
90            name,
91            None,
92            &self.names,
93            &argument_types,
94            self.explicit_variadic,
95            RoutineCallKind::Procedure,
96        )?
97        else {
98            let signature = argument_types
99                .iter()
100                .map(|argument| {
101                    argument
102                        .as_ref()
103                        .map_or_else(|| "unknown", column_type_name)
104                })
105                .collect::<Vec<_>>()
106                .join(", ");
107            return Err(SQLError::Routine {
108                sqlstate: "42883".into(),
109                message: format!("procedure {name}({signature}) does not exist"),
110            });
111        };
112        super::invocation::call_output_schema(
113            types,
114            &resolved.function.def,
115            &resolved.invocation.parameter_types,
116        )
117    }
118}