Skip to main content

uqa_sql/ast/
routines.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7use super::{
8    Deserialize, Expr, FunctionParallel, RoutineAclEntry, RoutineConfigAction,
9    RoutineSecurityAttributes, Serialize, Statement,
10};
11
12/// Parameter mode of a `CREATE FUNCTION` / `CREATE PROCEDURE`
13/// argument. Mirrors `PostgreSQL`'s `FunctionParameterMode`.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15pub enum FunctionParamMode {
16    /// `IN` (also the default when no mode is written).
17    In,
18    /// `OUT` - shapes the result row, not part of a function's call
19    /// signature (but part of a procedure's).
20    Out,
21    /// `INOUT` - accepted as input and returned in the result row.
22    InOut,
23    /// `VARIADIC` - a trailing array parameter that accepts either expanded element arguments or one explicit `VARIADIC` array argument.
24    Variadic,
25    /// `RETURNS TABLE (col type, ...)` column. Behaves like an `OUT`
26    /// parameter of a set-returning function.
27    Table,
28}
29
30/// One declared parameter of a user-defined function or procedure.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct FunctionParam {
33    /// Parameter name. Empty for unnamed parameters (`f(integer)`),
34    /// which are only addressable as `$n`.
35    pub name: String,
36    /// Raw type name as written (last segment, lower-cased by the
37    /// compiler; e.g. `int4`, `text`, `numeric`).
38    pub type_name: String,
39    /// Parsed relation and column identity for `%TYPE`; ordinary types have no reference.
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub type_reference: Option<RoutineColumnTypeReference>,
42    pub mode: FunctionParamMode,
43    /// `DEFAULT <expr>` for trailing input parameters.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub default: Option<Expr>,
46}
47
48/// Structured relation-column identity carried by a routine `%TYPE` declaration until catalog binding resolves it to a concrete SQL type.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct RoutineColumnTypeReference {
51    pub schema: Option<String>,
52    pub relation: String,
53    pub column: String,
54}
55
56impl RoutineColumnTypeReference {
57    pub fn new(schema: Option<String>, relation: String, column: String) -> Self {
58        Self {
59            schema,
60            relation,
61            column,
62        }
63    }
64
65    pub fn relation_reference(&self) -> String {
66        match self.schema.as_deref() {
67            Some(schema) => format!(
68                "{}.{}",
69                render_identifier_component(schema),
70                render_identifier_component(&self.relation)
71            ),
72            None => render_identifier_component(&self.relation),
73        }
74    }
75
76    pub fn type_reference(&self) -> String {
77        format!(
78            "{}.{}%type",
79            self.relation_reference(),
80            render_identifier_component(&self.column)
81        )
82    }
83}
84
85fn render_identifier_component(component: &str) -> String {
86    let can_render_bare = component
87        .bytes()
88        .enumerate()
89        .all(|(index, byte)| match byte {
90            b'a'..=b'z' | b'_' => true,
91            b'0'..=b'9' | b'$' => index != 0,
92            _ => false,
93        });
94    if can_render_bare && !component.is_empty() {
95        component.to_string()
96    } else {
97        format!("\"{}\"", component.replace('"', "\"\""))
98    }
99}
100
101/// Declared result shape of a user-defined function.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub enum FunctionReturns {
104    /// Procedures and functions whose result is shaped purely by
105    /// `OUT` parameters carry no explicit `RETURNS` clause.
106    None,
107    /// `RETURNS <type>` - includes `RETURNS void` and `RETURNS record`.
108    Scalar { type_name: String },
109    /// `RETURNS SETOF <type>`.
110    SetOf { type_name: String },
111    /// `RETURNS TABLE (...)`. The column list lives in
112    /// [`CreateFunction::params`] as [`FunctionParamMode::Table`]
113    /// entries; this variant just records the set-returning shape.
114    Table,
115}
116
117/// `IMMUTABLE` / `STABLE` / `VOLATILE` marker.
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
119pub enum FunctionVolatility {
120    Immutable,
121    Stable,
122    #[default]
123    Volatile,
124}
125
126/// Body of a user-defined routine.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub enum FunctionBody {
129    /// `AS $$ ... $$` - raw source text, parsed per language at
130    /// registration time.
131    Source(String),
132    /// SQL-standard body (`BEGIN ATOMIC ... END` / `RETURN expr`)
133    /// compiled straight to statements.
134    Statements(Vec<Statement>),
135}
136
137/// `CREATE [OR REPLACE] FUNCTION | PROCEDURE`.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct CreateFunction {
140    /// Stable catalog identity. The engine assigns this once when the routine is created and preserves it across replacement and rename.
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub object_id: Option<[u8; 16]>,
143    pub name: String,
144    pub or_replace: bool,
145    pub is_procedure: bool,
146    pub params: Vec<FunctionParam>,
147    pub returns: FunctionReturns,
148    /// Parsed `%TYPE` identity for a scalar or set return declaration until registration resolves it.
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub return_type_reference: Option<RoutineColumnTypeReference>,
151    /// Lower-cased language name (`plpgsql`, `sql`).
152    pub language: String,
153    pub body: FunctionBody,
154    /// Effective schema search path captured when a SQL-standard body or parameter default is catalog-bound. String and PL/pgSQL bodies keep dynamic lookup, but their parameter defaults still use this captured path.
155    #[serde(default, skip_serializing_if = "Vec::is_empty")]
156    pub creation_search_path: Vec<String>,
157    pub volatility: FunctionVolatility,
158    /// `STRICT` / `RETURNS NULL ON NULL INPUT` - the function is not
159    /// invoked when any input argument is NULL; the result is NULL.
160    pub strict: bool,
161    /// Catalog owner incarnation. Parsed declarations and anonymous blocks are unbound; registration binds the effective current role before publication.
162    #[serde(
163        default,
164        deserialize_with = "super::routine_security::deserialize_routine_owner"
165    )]
166    pub owner: Option<uqa_core::catalog_role::RoleIdentity>,
167    /// Execution identity and leakproofness, flattened to retain the catalog-definition wire shape.
168    #[serde(default, flatten)]
169    pub security: RoutineSecurityAttributes,
170    /// Parallel-safety classification.
171    #[serde(default)]
172    pub parallel: FunctionParallel,
173    /// Optional planner support routine identity.
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub support: Option<String>,
176    /// Effective per-routine configuration as `name=value` pairs in declaration order.
177    #[serde(default, skip_serializing_if = "Vec::is_empty")]
178    pub config: Vec<(String, String)>,
179    /// Creation-time configuration actions awaiting engine/session resolution. Registration consumes this list before persistence.
180    #[serde(default, skip_serializing_if = "Vec::is_empty")]
181    pub config_actions: Vec<RoutineConfigAction>,
182    /// Explicit execution privileges, including the owner's revocable EXECUTE. `None` means the `PostgreSQL` default (PUBLIC and owner EXECUTE); ownership always retains implicit grant options.
183    #[serde(default)]
184    pub execute_acl: Option<Vec<RoutineAclEntry>>,
185}
186
187impl CreateFunction {
188    /// Parameters that define routine identity: `IN` + `INOUT` + `VARIADIC`, in declaration order.
189    pub fn identity_params(&self) -> Vec<&FunctionParam> {
190        self.params
191            .iter()
192            .filter(|param| Self::is_identity_param(param))
193            .collect()
194    }
195
196    /// Number of parameters that define routine identity.
197    pub fn identity_arity(&self) -> usize {
198        self.params
199            .iter()
200            .filter(|param| Self::is_identity_param(param))
201            .count()
202    }
203
204    fn is_identity_param(param: &FunctionParam) -> bool {
205        matches!(
206            param.mode,
207            FunctionParamMode::In | FunctionParamMode::InOut | FunctionParamMode::Variadic
208        )
209    }
210
211    /// Parameters supplied by a call: identity parameters for functions and every non-`TABLE` parameter for procedures.
212    pub fn call_params(&self) -> Vec<&FunctionParam> {
213        self.params
214            .iter()
215            .filter(|param| self.is_call_param(param))
216            .collect()
217    }
218
219    /// Number of declared call parameters; a variadic parameter can consume multiple actual arguments.
220    pub fn call_arity(&self) -> usize {
221        self.params
222            .iter()
223            .filter(|param| self.is_call_param(param))
224            .count()
225    }
226
227    /// Minimum number of actual arguments for ordinary expanded notation; a variadic parameter accepts zero elements.
228    pub fn required_call_arity(&self) -> usize {
229        self.params
230            .iter()
231            .filter(|param| {
232                self.is_call_param(param)
233                    && param.default.is_none()
234                    && param.mode != FunctionParamMode::Variadic
235            })
236            .count()
237    }
238
239    fn is_call_param(&self, param: &FunctionParam) -> bool {
240        match param.mode {
241            FunctionParamMode::In | FunctionParamMode::InOut | FunctionParamMode::Variadic => true,
242            FunctionParamMode::Out => self.is_procedure,
243            FunctionParamMode::Table => false,
244        }
245    }
246
247    /// Backward-compatible alias for [`Self::call_arity`].
248    pub fn signature_arity(&self) -> usize {
249        self.call_arity()
250    }
251
252    /// Backward-compatible alias for [`Self::required_call_arity`].
253    pub fn required_arity(&self) -> usize {
254        self.required_call_arity()
255    }
256
257    /// Backward-compatible alias for [`Self::call_params`].
258    pub fn signature_params(&self) -> Vec<&FunctionParam> {
259        self.call_params()
260    }
261
262    /// Parameters that shape the result row: `OUT` + `INOUT` +
263    /// `RETURNS TABLE` columns, in declaration order.
264    pub fn output_params(&self) -> Vec<&FunctionParam> {
265        self.params
266            .iter()
267            .filter(|p| {
268                matches!(
269                    p.mode,
270                    FunctionParamMode::Out | FunctionParamMode::InOut | FunctionParamMode::Table
271                )
272            })
273            .collect()
274    }
275
276    /// True when the routine produces a row set (`RETURNS SETOF` /
277    /// `RETURNS TABLE`).
278    pub fn returns_set(&self) -> bool {
279        matches!(
280            self.returns,
281            FunctionReturns::SetOf { .. } | FunctionReturns::Table
282        )
283    }
284}
285
286/// One `DROP FUNCTION` / `DROP PROCEDURE` target.
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct DropFunctionItem {
289    pub name: String,
290    /// `Some(types)` when the statement spelled an argument list
291    /// (`DROP FUNCTION f(int, int)` - matched by canonical argument
292    /// types); `None` for the bare-name form
293    /// (`DROP FUNCTION f`).
294    pub arg_types: Option<Vec<String>>,
295}
296
297/// `DROP FUNCTION [IF EXISTS] name[(argtypes)] [, ...]` and the
298/// `DROP PROCEDURE` equivalent.
299#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct DropFunctionStmt {
301    pub is_procedure: bool,
302    pub if_exists: bool,
303    #[serde(default)]
304    pub cascade: bool,
305    pub items: Vec<DropFunctionItem>,
306}