Skip to main content

razeri_plugin_contract/
routine_spec.rs

1// Copyright (C) 2026 Gabriel Shea
2// Licensed under either of the Apache License, Version 2.0 or the MIT
3// license, at your option.
4
5//! **The routine Razeri composed, handed to a dialect that spells its own
6//! grammar.**
7//!
8//! Razeri decides *what* a routine is — its name, its parameters, the
9//! statements in its body, the comment a reader will find on it — and a
10//! [`ProcedureForm`](crate::ProcedureForm),
11//! [`ScalarFunctionForm`](crate::ScalarFunctionForm) or
12//! [`TableFunctionForm`](crate::TableFunctionForm) decides *how it is spelled*.
13//! Where the form is one Razeri carries, Razeri spells it. Where the form is
14//! `Custom`, this struct is what Razeri hands the dialect instead, and the
15//! dialect returns the whole `CREATE`.
16//!
17//! # What is in here and what is not
18//!
19//! Everything a `CREATE` needs that Razeri decided: the name, the schema, the
20//! parameters with their types already rendered in **this engine's** spelling,
21//! the local declarations, what the routine returns, the body, and the comment.
22//!
23//! Nothing about how any of it is punctuated. Where the keyword goes, how the
24//! body is quoted, whether the statement ends in `;` or `;` and a slash on a
25//! line of its own — those are the engine's, which is the whole reason the
26//! dialect is being asked.
27//!
28//! # The types are already rendered, and the type itself is still here
29//!
30//! Every `data_type` here is a string this dialect produced. A dialect asked to
31//! render a routine is never also asked to translate a type it already answered
32//! for, and there is no second reading of the type vocabulary for the two
33//! answers to drift apart in.
34//!
35//! **A parameter's type is not always spelled the way a column's is.** Oracle
36//! takes `VARCHAR2(100)` on a column and refuses it in a parameter list, where
37//! the bare `VARCHAR2` is required. So a parameter's string comes from
38//! [`render_parameter_data_type`](crate::SqlRenderer::render_parameter_data_type),
39//! which is `render_data_type` for every engine that spells the two the same
40//! way and the place to say so for an engine that does not.
41//!
42//! Where Razeri composed the routine from a typed design, `declared_type`
43//! carries the [`DataType`] the string came from, so a renderer that needs to
44//! look at the type rather than at its spelling can — a `Some` there is the type
45//! Razeri decided, and a `None` says the design supplied this type as text and
46//! there is nothing more to read.
47
48use crate::data_type::DataType;
49
50/// Which of the three routine kinds this is.
51///
52/// A dialect's three render methods each receive one kind and only that kind,
53/// so this is here for a renderer that shares one code path between them and
54/// for a report that names what it is looking at.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum RoutineKind {
57    /// A stored procedure — called, returns nothing.
58    Procedure,
59    /// A function returning one value.
60    ScalarFunction,
61    /// A function a query selects `FROM`, returning rows.
62    TableFunction,
63}
64
65/// **Whose privileges the routine runs with.**
66///
67/// Every engine that has routines has this choice and every engine spells it
68/// differently — `SECURITY INVOKER` / `SECURITY DEFINER` on `PostgreSQL`,
69/// `EXECUTE AS CALLER` / `EXECUTE AS OWNER` on T-SQL and Snowflake,
70/// `AUTHID CURRENT_USER` / `AUTHID DEFINER` on Oracle — so the choice is here
71/// and the spelling is the renderer's.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
73pub enum RoutineSecurity {
74    /// The privileges of whoever calls it. The default, and the safer one: a
75    /// routine that runs as its caller cannot reach past what the caller
76    /// already could.
77    #[default]
78    Caller,
79    /// The privileges of whoever owns it.
80    Owner,
81}
82
83/// Which way a parameter carries its value.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
85pub enum ParameterDirection {
86    /// Read by the routine. The only direction a function's parameters take.
87    #[default]
88    In,
89    /// Written by the routine and read by its caller.
90    Out,
91    /// Both.
92    InOut,
93    /// Any number of values of the parameter's type, collected into the last
94    /// parameter.
95    Variadic,
96}
97
98/// One parameter of the routine.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct RoutineParameter {
101    /// The parameter's name, as Razeri composed it and **unquoted** — a
102    /// dialect that decorates parameter names (T-SQL's `@`, `Redshift`'s
103    /// positional `$1`) applies its own decoration.
104    ///
105    /// Empty where the routine declares a type and no name, which some engines
106    /// allow and which a renderer must then address by position.
107    pub name: String,
108    /// The parameter's type, in the spelling this dialect's
109    /// [`render_parameter_data_type`](crate::SqlRenderer::render_parameter_data_type)
110    /// gave it.
111    pub data_type: String,
112    /// The type the string above was rendered from, where Razeri composed this
113    /// routine from a typed design; `None` where the design supplied the type as
114    /// text.
115    pub declared_type: Option<DataType>,
116    /// Which way the value travels.
117    pub direction: ParameterDirection,
118    /// The default expression, where the routine declares one.
119    pub default: Option<String>,
120}
121
122/// One local variable the routine declares before its body runs.
123///
124/// Empty for functions; a procedure's declarations are the ones Razeri's
125/// composed load needs.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct RoutineDeclaration {
128    /// The variable's name, unquoted and undecorated.
129    pub name: String,
130    /// Its type, in this dialect's column spelling — a declaration is the one
131    /// place Oracle keeps the precision a parameter list refuses, so this is
132    /// [`render_data_type`](crate::SqlRenderer::render_data_type) and not the
133    /// parameter spelling.
134    pub data_type: String,
135    /// The type the string above was rendered from, where Razeri composed this
136    /// routine from a typed design.
137    pub declared_type: Option<DataType>,
138    /// The initial value, where the routine declares one.
139    pub default: Option<String>,
140}
141
142/// One column of a table-valued function's row.
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct RoutineColumn {
145    /// The column's name, unquoted.
146    pub name: String,
147    /// Its type, already rendered by this dialect.
148    pub data_type: String,
149    /// The type the string above was rendered from, where Razeri composed this
150    /// routine from a typed design.
151    pub declared_type: Option<DataType>,
152}
153
154/// What the routine gives back.
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub enum RoutineReturns {
157    /// Nothing — a procedure.
158    Nothing,
159    /// One value of this type.
160    Value(String),
161    /// Rows of these columns.
162    Columns(Vec<RoutineColumn>),
163    /// Rows shaped like this existing table or type.
164    RowsOf(String),
165    /// Rows whose shape the engine reads off the query.
166    Inferred,
167}
168
169/// The routine's body, in the shape Razeri composed it.
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub enum RoutineBody {
172    /// A single expression the function's value is.
173    Expression(String),
174    /// A query whose rows the function returns.
175    Query(String),
176    /// Statements, in order, each as Razeri composed it — which means a
177    /// statement that needs a `;` already carries one, and a block statement
178    /// carries the `;` its own grammar puts inside it.
179    ///
180    /// How they are separated, and how the block around them is opened and
181    /// closed, is still the engine's business. An engine that ends a statement
182    /// some other way reads the last character rather than assuming: a `;`
183    /// stripped off `END IF;` takes a keyword's punctuation, not a separator.
184    Statements(Vec<String>),
185    /// A procedural block the design supplied already framed. Emit it as it
186    /// stands, inside whatever this engine wraps a body in.
187    Block(String),
188}
189
190/// **A routine Razeri composed, for a dialect that spells its own grammar.**
191///
192/// Handed to [`SqlRenderer::render_procedure`](crate::SqlRenderer::render_procedure)
193/// and its two siblings when the dialect answered `Custom`.
194///
195/// It is `#[non_exhaustive]`: Razeri may carry one more fact about a routine
196/// later, and a dialect built against today's contract must keep compiling.
197/// Read the fields; do not construct one outside Razeri except in a test, where
198/// [`RoutineSpec::new`] gives you an empty routine of a kind to fill in.
199#[derive(Debug, Clone, PartialEq, Eq)]
200#[non_exhaustive]
201pub struct RoutineSpec {
202    /// Which of the three kinds this is.
203    pub kind: RoutineKind,
204    /// The schema the routine is created in, where the design names one.
205    pub schema: Option<String>,
206    /// The routine's name, unquoted.
207    pub name: String,
208    /// Whether the design asked for a replace rather than a plain create. An
209    /// engine with no `OR REPLACE` says so its own way — a `DROP` in front, or
210    /// nothing at all.
211    pub or_replace: bool,
212    /// The routine's parameters, in order.
213    pub parameters: Vec<RoutineParameter>,
214    /// Local variables the body reads, in order. Empty for a function.
215    pub declarations: Vec<RoutineDeclaration>,
216    /// What the routine gives back.
217    pub returns: RoutineReturns,
218    /// Whose privileges the routine runs with.
219    pub security: RoutineSecurity,
220    /// The body.
221    pub body: RoutineBody,
222    /// The description a reader of the DDL should find on the routine. Where
223    /// the engine has no comment clause, it belongs in a comment.
224    pub comment: Option<String>,
225}
226
227impl RoutineSpec {
228    /// **An empty routine of one kind**, to fill in.
229    ///
230    /// The constructor exists so that a dialect's own tests can build the spec
231    /// their render method is going to receive without waiting for Razeri to
232    /// compose one, and so the struct can stay `#[non_exhaustive]` — a field
233    /// added later reaches those tests as a default rather than as a broken
234    /// build.
235    ///
236    /// Parameters:
237    /// - `kind`: which of the three routine kinds this is;
238    /// - `name`: the routine's name, unquoted.
239    #[must_use]
240    pub fn new(kind: RoutineKind, name: impl Into<String>) -> Self {
241        Self {
242            kind,
243            schema: None,
244            name: name.into(),
245            or_replace: false,
246            parameters: Vec::new(),
247            declarations: Vec::new(),
248            returns: match kind {
249                RoutineKind::Procedure => RoutineReturns::Nothing,
250                RoutineKind::ScalarFunction => RoutineReturns::Value(String::new()),
251                RoutineKind::TableFunction => RoutineReturns::Columns(Vec::new()),
252            },
253            security: RoutineSecurity::Caller,
254            body: RoutineBody::Statements(Vec::new()),
255            comment: None,
256        }
257    }
258
259    /// **The name a `CREATE` writes**, schema included where there is one.
260    ///
261    /// Parameters:
262    /// - `quote`: how this engine quotes one identifier.
263    #[must_use]
264    pub fn qualified_name(&self, quote: impl Fn(&str) -> String) -> String {
265        match &self.schema {
266            Some(schema) => format!("{}.{}", quote(schema), quote(&self.name)),
267            None => quote(&self.name),
268        }
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    /// A new spec of each kind returns what that kind returns, so a dialect
277    /// filling one in does not have to remember which.
278    #[test]
279    fn a_new_spec_returns_what_its_kind_returns() {
280        assert_eq!(
281            RoutineSpec::new(RoutineKind::Procedure, "load_customer").returns,
282            RoutineReturns::Nothing
283        );
284        assert_eq!(
285            RoutineSpec::new(RoutineKind::ScalarFunction, "generate_sid").returns,
286            RoutineReturns::Value(String::new())
287        );
288        assert_eq!(
289            RoutineSpec::new(RoutineKind::TableFunction, "customer_at").returns,
290            RoutineReturns::Columns(Vec::new())
291        );
292    }
293
294    /// The qualified name is the engine's quoting over Razeri's names, and a
295    /// routine with no schema is one name and not a leading dot.
296    #[test]
297    fn the_qualified_name_quotes_with_the_engine_and_omits_an_absent_schema() {
298        let quote = |name: &str| format!("\"{name}\"");
299        let mut spec = RoutineSpec::new(RoutineKind::Procedure, "load_customer");
300        assert_eq!(spec.qualified_name(quote), "\"load_customer\"");
301        spec.schema = Some("retail_core".to_owned());
302        assert_eq!(spec.qualified_name(quote), "\"retail_core\".\"load_customer\"");
303    }
304}