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. The compiler leaves this empty and registration captures the effective current user; persisted definitions always carry a role name.
162    #[serde(default)]
163    pub owner: String,
164    /// Execution identity and leakproofness, flattened to retain the catalog-definition wire shape.
165    #[serde(default, flatten)]
166    pub security: RoutineSecurityAttributes,
167    /// Parallel-safety classification.
168    #[serde(default)]
169    pub parallel: FunctionParallel,
170    /// Optional planner support routine identity.
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub support: Option<String>,
173    /// Effective per-routine configuration as `name=value` pairs in declaration order.
174    #[serde(default, skip_serializing_if = "Vec::is_empty")]
175    pub config: Vec<(String, String)>,
176    /// Creation-time configuration actions awaiting engine/session resolution. Registration consumes this list before persistence.
177    #[serde(default, skip_serializing_if = "Vec::is_empty")]
178    pub config_actions: Vec<RoutineConfigAction>,
179    /// Explicit execution privileges. `None` means the `PostgreSQL` default (`PUBLIC=EXECUTE`).
180    #[serde(default, skip_serializing_if = "Option::is_none")]
181    pub execute_acl: Option<Vec<RoutineAclEntry>>,
182}
183
184impl CreateFunction {
185    /// Parameters that define routine identity: `IN` + `INOUT` + `VARIADIC`, in declaration order.
186    pub fn identity_params(&self) -> Vec<&FunctionParam> {
187        self.params
188            .iter()
189            .filter(|param| Self::is_identity_param(param))
190            .collect()
191    }
192
193    /// Number of parameters that define routine identity.
194    pub fn identity_arity(&self) -> usize {
195        self.params
196            .iter()
197            .filter(|param| Self::is_identity_param(param))
198            .count()
199    }
200
201    fn is_identity_param(param: &FunctionParam) -> bool {
202        matches!(
203            param.mode,
204            FunctionParamMode::In | FunctionParamMode::InOut | FunctionParamMode::Variadic
205        )
206    }
207
208    /// Parameters supplied by a call: identity parameters for functions and every non-`TABLE` parameter for procedures.
209    pub fn call_params(&self) -> Vec<&FunctionParam> {
210        self.params
211            .iter()
212            .filter(|param| self.is_call_param(param))
213            .collect()
214    }
215
216    /// Number of declared call parameters; a variadic parameter can consume multiple actual arguments.
217    pub fn call_arity(&self) -> usize {
218        self.params
219            .iter()
220            .filter(|param| self.is_call_param(param))
221            .count()
222    }
223
224    /// Minimum number of actual arguments for ordinary expanded notation; a variadic parameter accepts zero elements.
225    pub fn required_call_arity(&self) -> usize {
226        self.params
227            .iter()
228            .filter(|param| {
229                self.is_call_param(param)
230                    && param.default.is_none()
231                    && param.mode != FunctionParamMode::Variadic
232            })
233            .count()
234    }
235
236    fn is_call_param(&self, param: &FunctionParam) -> bool {
237        match param.mode {
238            FunctionParamMode::In | FunctionParamMode::InOut | FunctionParamMode::Variadic => true,
239            FunctionParamMode::Out => self.is_procedure,
240            FunctionParamMode::Table => false,
241        }
242    }
243
244    /// Backward-compatible alias for [`Self::call_arity`].
245    pub fn signature_arity(&self) -> usize {
246        self.call_arity()
247    }
248
249    /// Backward-compatible alias for [`Self::required_call_arity`].
250    pub fn required_arity(&self) -> usize {
251        self.required_call_arity()
252    }
253
254    /// Backward-compatible alias for [`Self::call_params`].
255    pub fn signature_params(&self) -> Vec<&FunctionParam> {
256        self.call_params()
257    }
258
259    /// Parameters that shape the result row: `OUT` + `INOUT` +
260    /// `RETURNS TABLE` columns, in declaration order.
261    pub fn output_params(&self) -> Vec<&FunctionParam> {
262        self.params
263            .iter()
264            .filter(|p| {
265                matches!(
266                    p.mode,
267                    FunctionParamMode::Out | FunctionParamMode::InOut | FunctionParamMode::Table
268                )
269            })
270            .collect()
271    }
272
273    /// True when the routine produces a row set (`RETURNS SETOF` /
274    /// `RETURNS TABLE`).
275    pub fn returns_set(&self) -> bool {
276        matches!(
277            self.returns,
278            FunctionReturns::SetOf { .. } | FunctionReturns::Table
279        )
280    }
281}
282
283/// One `DROP FUNCTION` / `DROP PROCEDURE` target.
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct DropFunctionItem {
286    pub name: String,
287    /// `Some(types)` when the statement spelled an argument list
288    /// (`DROP FUNCTION f(int, int)` - matched by canonical argument
289    /// types); `None` for the bare-name form
290    /// (`DROP FUNCTION f`).
291    pub arg_types: Option<Vec<String>>,
292}
293
294/// `DROP FUNCTION [IF EXISTS] name[(argtypes)] [, ...]` and the
295/// `DROP PROCEDURE` equivalent.
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct DropFunctionStmt {
298    pub is_procedure: bool,
299    pub if_exists: bool,
300    #[serde(default)]
301    pub cascade: bool,
302    pub items: Vec<DropFunctionItem>,
303}