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, RoutineAttributeClauses,
9 RoutineConfigAction, 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/// An input parameter as a SQL-standard body refers to it: the name the body uses, which follows `PostgreSQL`'s positional naming, and where the parameter's value arrives in a call.
31#[derive(Debug, Clone, Copy)]
32pub struct SQLBodyParameter<'a> {
33 /// The name the body uses, empty when the declared parameter at this position is unnamed.
34 pub name: &'a str,
35 pub parameter: &'a FunctionParam,
36 /// The parameter's 1-based position among the call's arguments, which include a procedure's output parameters.
37 pub call_position: usize,
38}
39
40/// One declared parameter of a user-defined function or procedure.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct FunctionParam {
43 /// Parameter name. Empty for unnamed parameters (`f(integer)`),
44 /// which are only addressable as `$n`.
45 pub name: String,
46 /// Raw type name as written (last segment, lower-cased by the
47 /// compiler; e.g. `int4`, `text`, `numeric`).
48 pub type_name: String,
49 /// Parsed relation and column identity for `%TYPE`; ordinary types have no reference.
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub type_reference: Option<RoutineColumnTypeReference>,
52 /// The type as the statement wrote it, as `TypeNameToString` spells it, which names a missing type until registration resolves the type.
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub written_type: Option<String>,
55 pub mode: FunctionParamMode,
56 /// `DEFAULT <expr>` for trailing input parameters.
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub default: Option<Expr>,
59 /// The analyzed default expression type, used when an omitted polymorphic input supplies call typing.
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub default_type: Option<RoutineDefaultType>,
62}
63
64/// The type of a stored routine default. Some NULL inputs retain a polymorphic
65/// pseudo-type without claiming that it is a concrete SQL value carrier.
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub enum RoutineDefaultType {
68 Concrete(super::ColumnType),
69 Polymorphic(String),
70}
71
72/// Structured relation-column identity carried by a routine `%TYPE` declaration until catalog binding resolves it to a concrete SQL type.
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct RoutineColumnTypeReference {
75 pub schema: Option<String>,
76 pub relation: String,
77 pub column: String,
78}
79
80impl RoutineColumnTypeReference {
81 pub fn new(schema: Option<String>, relation: String, column: String) -> Self {
82 Self {
83 schema,
84 relation,
85 column,
86 }
87 }
88
89 pub fn relation_reference(&self) -> String {
90 match self.schema.as_deref() {
91 Some(schema) => format!(
92 "{}.{}",
93 render_identifier_component(schema),
94 render_identifier_component(&self.relation)
95 ),
96 None => render_identifier_component(&self.relation),
97 }
98 }
99
100 pub fn type_reference(&self) -> String {
101 format!(
102 "{}.{}%type",
103 self.relation_reference(),
104 render_identifier_component(&self.column)
105 )
106 }
107}
108
109fn render_identifier_component(component: &str) -> String {
110 let can_render_bare = component
111 .bytes()
112 .enumerate()
113 .all(|(index, byte)| match byte {
114 b'a'..=b'z' | b'_' => true,
115 b'0'..=b'9' | b'$' => index != 0,
116 _ => false,
117 });
118 if can_render_bare && !component.is_empty() {
119 component.to_string()
120 } else {
121 format!("\"{}\"", component.replace('"', "\"\""))
122 }
123}
124
125/// Declared result shape of a user-defined function.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub enum FunctionReturns {
128 /// Procedures and functions whose result is shaped purely by
129 /// `OUT` parameters carry no explicit `RETURNS` clause.
130 None,
131 /// `RETURNS <type>` - includes `RETURNS void` and `RETURNS record`.
132 Scalar { type_name: String },
133 /// `RETURNS SETOF <type>`.
134 SetOf { type_name: String },
135 /// `RETURNS TABLE (...)`. The column list lives in
136 /// [`CreateFunction::params`] as [`FunctionParamMode::Table`]
137 /// entries; this variant just records the set-returning shape.
138 Table,
139}
140
141/// `IMMUTABLE` / `STABLE` / `VOLATILE` marker.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
143pub enum FunctionVolatility {
144 Immutable,
145 Stable,
146 #[default]
147 Volatile,
148}
149
150/// Body of a user-defined routine.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub enum FunctionBody {
153 /// `AS $$ ... $$` - raw source text, parsed per language at
154 /// registration time.
155 Source(String),
156 /// SQL-standard body (`BEGIN ATOMIC ... END` / `RETURN expr`)
157 /// compiled straight to statements.
158 Statements(Vec<Statement>),
159}
160
161/// How a SQL-standard body was written; `pg_get_function_sqlbody` reproduces it.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163pub enum SQLBodyForm {
164 /// `RETURN expr`, stored as one `SELECT expr`.
165 Return,
166 /// `BEGIN ATOMIC stmt; ... END`.
167 Atomic,
168}
169
170/// `CREATE [OR REPLACE] FUNCTION | PROCEDURE`.
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct CreateFunction {
173 /// Stable catalog identity. The engine assigns this once when the routine is created and preserves it across replacement and rename.
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub object_id: Option<[u8; 16]>,
176 /// The catalog tuple's replacement identity. A replacement changes this token while keeping `object_id`; older definitions use their object identity until first replacement.
177 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub catalog_revision: Option<[u8; 16]>,
179 /// The public OID allocated when the routine was created; replacement and rename keep it. Routines created before OIDs were recorded derive it from their identity.
180 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub catalog_oid: Option<u32>,
182 pub name: String,
183 pub or_replace: bool,
184 pub is_procedure: bool,
185 pub params: Vec<FunctionParam>,
186 pub returns: FunctionReturns,
187 /// Parsed `%TYPE` identity for a scalar or set return declaration until registration resolves it.
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub return_type_reference: Option<RoutineColumnTypeReference>,
190 /// The declared result type as the statement wrote it, without SETOF, until registration resolves it.
191 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub return_written_type: Option<String>,
193 /// Lower-cased language name (`plpgsql`, `sql`).
194 pub language: String,
195 pub body: FunctionBody,
196 /// The written form of a SQL-standard body. Definitions stored before the form was recorded omit it.
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub sql_body_form: Option<SQLBodyForm>,
199 /// 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.
200 #[serde(default, skip_serializing_if = "Vec::is_empty")]
201 pub creation_search_path: Vec<String>,
202 pub volatility: FunctionVolatility,
203 /// `STRICT` / `RETURNS NULL ON NULL INPUT` - the function is not
204 /// invoked when any input argument is NULL; the result is NULL.
205 pub strict: bool,
206 /// Catalog owner incarnation. Parsed declarations and anonymous blocks are unbound; registration binds the effective current role before publication.
207 #[serde(
208 default,
209 deserialize_with = "super::routine_security::deserialize_routine_owner"
210 )]
211 pub owner: Option<uqa_core::catalog_role::RoleIdentity>,
212 /// Execution identity and leakproofness, flattened to retain the catalog-definition wire shape.
213 #[serde(default, flatten)]
214 pub security: RoutineSecurityAttributes,
215 /// Parallel-safety classification.
216 #[serde(default)]
217 pub parallel: FunctionParallel,
218 /// Optional planner support routine identity.
219 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub support: Option<String>,
221 /// `COST`, the estimated execution cost in units of `cpu_operator_cost` that `pg_proc.procost` reports, a `float4` as `PostgreSQL` stores it; without one the language's default applies.
222 #[serde(
223 default,
224 skip_serializing_if = "Option::is_none",
225 with = "super::routine_estimate"
226 )]
227 pub cost: Option<f32>,
228 /// `ROWS`, the estimated number of rows a set-returning routine returns, which `pg_proc.prorows` reports; without one the default applies.
229 #[serde(
230 default,
231 skip_serializing_if = "Option::is_none",
232 with = "super::routine_estimate"
233 )]
234 pub rows: Option<f32>,
235 /// Effective per-routine configuration as `name=value` pairs in declaration order.
236 #[serde(default, skip_serializing_if = "Vec::is_empty")]
237 pub config: Vec<(String, String)>,
238 /// Creation-time configuration actions awaiting engine/session resolution. Registration consumes this list before persistence.
239 #[serde(default, skip_serializing_if = "Vec::is_empty")]
240 pub config_actions: Vec<RoutineConfigAction>,
241 /// The statement's attribute clauses in written order, which registration checks and consumes before persistence.
242 #[serde(default, skip_serializing_if = "RoutineAttributeClauses::is_empty")]
243 pub attribute_clauses: RoutineAttributeClauses,
244 /// Explicit execution privileges, including the owner's revocable EXECUTE. `None` means the `PostgreSQL` default (PUBLIC and owner EXECUTE); ownership always retains implicit grant options.
245 #[serde(default)]
246 pub execute_acl: Option<Vec<RoutineAclEntry>>,
247}
248
249impl CreateFunction {
250 /// Parameters that define routine identity: `IN` + `INOUT` + `VARIADIC`, in declaration order.
251 pub fn identity_params(&self) -> Vec<&FunctionParam> {
252 self.params
253 .iter()
254 .filter(|param| Self::is_identity_param(param))
255 .collect()
256 }
257
258 /// Number of parameters that define routine identity.
259 pub fn identity_arity(&self) -> usize {
260 self.params
261 .iter()
262 .filter(|param| Self::is_identity_param(param))
263 .count()
264 }
265
266 fn is_identity_param(param: &FunctionParam) -> bool {
267 matches!(
268 param.mode,
269 FunctionParamMode::In | FunctionParamMode::InOut | FunctionParamMode::Variadic
270 )
271 }
272
273 /// Parameters supplied by a call: identity parameters for functions and every non-`TABLE` parameter for procedures.
274 pub fn call_params(&self) -> Vec<&FunctionParam> {
275 self.params
276 .iter()
277 .filter(|param| self.is_call_param(param))
278 .collect()
279 }
280
281 /// Number of declared call parameters; a variadic parameter can consume multiple actual arguments.
282 pub fn call_arity(&self) -> usize {
283 self.params
284 .iter()
285 .filter(|param| self.is_call_param(param))
286 .count()
287 }
288
289 /// Minimum number of actual arguments for ordinary expanded notation; a variadic parameter accepts zero elements.
290 pub fn required_call_arity(&self) -> usize {
291 self.params
292 .iter()
293 .filter(|param| {
294 self.is_call_param(param)
295 && param.default.is_none()
296 && param.mode != FunctionParamMode::Variadic
297 })
298 .count()
299 }
300
301 fn is_call_param(&self, param: &FunctionParam) -> bool {
302 match param.mode {
303 FunctionParamMode::In | FunctionParamMode::InOut | FunctionParamMode::Variadic => true,
304 FunctionParamMode::Out => self.is_procedure,
305 FunctionParamMode::Table => false,
306 }
307 }
308
309 /// The parameters a SQL-standard body refers to, as `interpret_AS_clause` gives them to its parser: the input parameters in order, the one at position `n` going by the name of the parameter declared at position `n` among all parameters. When an output parameter precedes an input, the body therefore names that input by the output parameter's name, as `PostgreSQL` does.
310 pub fn sql_body_parameters(&self) -> Vec<SQLBodyParameter<'_>> {
311 let call_params = self.call_params();
312 self.identity_params()
313 .into_iter()
314 .enumerate()
315 .map(|(index, parameter)| SQLBodyParameter {
316 name: self
317 .params
318 .get(index)
319 .map_or("", |declared| declared.name.as_str()),
320 parameter,
321 call_position: call_params
322 .iter()
323 .position(|call| std::ptr::eq(*call, parameter))
324 .map_or(index + 1, |position| position + 1),
325 })
326 .collect()
327 }
328
329 /// Backward-compatible alias for [`Self::call_arity`].
330 pub fn signature_arity(&self) -> usize {
331 self.call_arity()
332 }
333
334 /// Backward-compatible alias for [`Self::required_call_arity`].
335 pub fn required_arity(&self) -> usize {
336 self.required_call_arity()
337 }
338
339 /// Backward-compatible alias for [`Self::call_params`].
340 pub fn signature_params(&self) -> Vec<&FunctionParam> {
341 self.call_params()
342 }
343
344 /// Parameters that shape the result row: `OUT` + `INOUT` +
345 /// `RETURNS TABLE` columns, in declaration order.
346 pub fn output_params(&self) -> Vec<&FunctionParam> {
347 self.params
348 .iter()
349 .filter(|p| {
350 matches!(
351 p.mode,
352 FunctionParamMode::Out | FunctionParamMode::InOut | FunctionParamMode::Table
353 )
354 })
355 .collect()
356 }
357
358 /// True when the routine produces a row set (`RETURNS SETOF` /
359 /// `RETURNS TABLE`).
360 pub fn returns_set(&self) -> bool {
361 matches!(
362 self.returns,
363 FunctionReturns::SetOf { .. } | FunctionReturns::Table
364 )
365 }
366}
367
368/// One `DROP FUNCTION` / `DROP PROCEDURE` target.
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct DropFunctionItem {
371 pub name: String,
372 /// `Some(types)` when the statement spelled an argument list
373 /// (`DROP FUNCTION f(int, int)` - matched by canonical argument
374 /// types); `None` for the bare-name form
375 /// (`DROP FUNCTION f`).
376 pub arg_types: Option<Vec<String>>,
377}
378
379/// `DROP FUNCTION [IF EXISTS] name[(argtypes)] [, ...]` and the
380/// `DROP PROCEDURE` equivalent.
381#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct DropFunctionStmt {
383 pub is_procedure: bool,
384 pub if_exists: bool,
385 #[serde(default)]
386 pub cascade: bool,
387 pub items: Vec<DropFunctionItem>,
388}