Skip to main content

uqa_sql/routines/
invocation.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Routine invocation metadata, concrete signatures, and anonymous-record assignment rules.
8use crate::{
9    assignment::routines::RoutineValueContext,
10    ast::{
11        ColumnType, CreateFunction, FunctionParamMode, FunctionReturns, RoutineInvocationBinding,
12    },
13    expr::value_type_name,
14    routines::declaration::RoutineTypeCatalog,
15    SQLError,
16};
17use uqa_core::Value;
18pub fn output_column_names(def: &CreateFunction) -> Vec<String> {
19    def.output_params()
20        .iter()
21        .enumerate()
22        .map(|(idx, p)| {
23            if p.name.is_empty() {
24                format!("column{}", idx + 1)
25            } else {
26                p.name.clone()
27            }
28        })
29        .collect()
30}
31
32pub fn call_signature(name: &str, args: &[(Option<String>, Value)]) -> String {
33    let types = args
34        .iter()
35        .map(|(arg_name, value)| match arg_name {
36            Some(arg_name) => format!("{arg_name} => {}", value_type_name(value)),
37            None => value_type_name(value).to_string(),
38        })
39        .collect::<Vec<_>>()
40        .join(", ");
41    format!("{name}({types})")
42}
43
44pub fn routine_resolution_error(
45    kind: &str,
46    name: &str,
47    args: &[(Option<String>, Value)],
48    suffix: &str,
49) -> SQLError {
50    SQLError::Routine {
51        sqlstate: if suffix == "is not unique" {
52            "42725".into()
53        } else {
54            "42883".into()
55        },
56        message: format!("{kind} {} {suffix}", call_signature(name, args)),
57    }
58}
59
60pub fn runtime_argument_types(
61    args: &[(Option<String>, Value)],
62) -> Result<Vec<Option<ColumnType>>, SQLError> {
63    args.iter()
64        .map(|(_, value)| {
65            if matches!(value, Value::Null) {
66                Ok(None)
67            } else {
68                ColumnType::from_sql_name(value_type_name(value)).map(Some)
69            }
70        })
71        .collect()
72}
73
74pub fn specialized_definition(
75    definition: &CreateFunction,
76    invocation: &RoutineInvocationBinding,
77) -> Result<Option<CreateFunction>, SQLError> {
78    if invocation.parameter_types.len() != definition.params.len() {
79        return Err(SQLError::Internal(format!(
80            "routine `{}` has {} concrete parameter types for {} parameters",
81            definition.name,
82            invocation.parameter_types.len(),
83            definition.params.len()
84        )));
85    }
86    let parameters_match = definition
87        .params
88        .iter()
89        .zip(&invocation.parameter_types)
90        .all(|(parameter, type_name)| parameter.type_name == *type_name);
91    let return_type_matches = match (&invocation.return_type, &definition.returns) {
92        (Some(concrete), FunctionReturns::Scalar { type_name })
93        | (Some(concrete), FunctionReturns::SetOf { type_name }) => concrete == type_name,
94        (None, _) | (Some(_), FunctionReturns::None | FunctionReturns::Table) => true,
95    };
96    if parameters_match && return_type_matches {
97        return Ok(None);
98    }
99    let mut specialized = definition.clone();
100    for (parameter, type_name) in specialized
101        .params
102        .iter_mut()
103        .zip(&invocation.parameter_types)
104    {
105        parameter.type_name.clone_from(type_name);
106    }
107    if let Some(return_type) = &invocation.return_type {
108        match &mut specialized.returns {
109            FunctionReturns::Scalar { type_name } | FunctionReturns::SetOf { type_name } => {
110                type_name.clone_from(return_type);
111            }
112            FunctionReturns::None | FunctionReturns::Table => {}
113        }
114    }
115    Ok(Some(specialized))
116}
117
118pub fn runtime_record_column_type(value: &Value) -> Option<crate::ast::ColumnType> {
119    if matches!(value, Value::Null) {
120        return None;
121    }
122    crate::ast::ColumnType::from_sql_name(crate::expr::value_type_name(value)).ok()
123}
124
125pub fn coerce_anonymous_record_value(
126    context: &dyn RoutineValueContext,
127    value: &Value,
128    type_name: &str,
129) -> Result<Value, SQLError> {
130    let target = context
131        .catalog_column_type(type_name)
132        .or_else(|| crate::ast::ColumnType::from_sql_name(type_name).ok());
133    let Some(target) = target else {
134        return crate::assignment::routines::coerce_routine_value(context, value, type_name);
135    };
136    crate::assignment::conversion::convert_value_to_column_type_with_context(
137        context,
138        value.clone(),
139        &target,
140    )
141    .map_err(|error| match error {
142        SQLError::TypeMismatch(message) if message.starts_with("value too long for type ") => {
143            SQLError::Routine {
144                sqlstate: "22001".into(),
145                message,
146            }
147        }
148        other => other,
149    })
150}
151
152pub fn anonymous_record_shape_error() -> SQLError {
153    SQLError::Routine {
154        sqlstate: "42P13".into(),
155        message: "return type mismatch in function declared to return record".into(),
156    }
157}
158pub fn call_output_schema(
159    catalog: &dyn RoutineTypeCatalog,
160    definition: &crate::ast::CreateFunction,
161    parameter_types: &[String],
162) -> Result<Option<crate::RowSchema>, SQLError> {
163    let output_indices = definition
164        .params
165        .iter()
166        .enumerate()
167        .filter_map(|(index, parameter)| {
168            matches!(
169                parameter.mode,
170                FunctionParamMode::Out | FunctionParamMode::InOut | FunctionParamMode::Table
171            )
172            .then_some(index)
173        })
174        .collect::<Vec<_>>();
175    if output_indices.is_empty() {
176        return Ok(None);
177    }
178    let columns = output_column_names(definition);
179    let column_types = output_indices
180        .into_iter()
181        .map(|index| {
182            catalog
183                .resolve_catalog_column_type(&parameter_types[index])
184                .or_else(|| crate::ast::ColumnType::from_sql_name(&parameter_types[index]).ok())
185                .map(Some)
186                .ok_or_else(|| {
187                    SQLError::TypeMismatch(format!("unknown type `{}`", parameter_types[index]))
188                })
189        })
190        .collect::<Result<Vec<_>, SQLError>>()?;
191    Ok(Some(crate::RowSchema::with_types(columns, column_types)))
192}