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}