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
29//!
30//! Every `data_type` here is a string this dialect's own
31//! [`render_data_type`](crate::SqlRenderer::render_data_type) produced. A
32//! dialect asked to render a routine is never also asked to translate a type it
33//! already answered for, and there is no second reading of the type vocabulary
34//! for the two answers to drift apart in.
35
36/// Which of the three routine kinds this is.
37///
38/// A dialect's three render methods each receive one kind and only that kind,
39/// so this is here for a renderer that shares one code path between them and
40/// for a report that names what it is looking at.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum RoutineKind {
43 /// A stored procedure — called, returns nothing.
44 Procedure,
45 /// A function returning one value.
46 ScalarFunction,
47 /// A function a query selects `FROM`, returning rows.
48 TableFunction,
49}
50
51/// Which way a parameter carries its value.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
53pub enum ParameterDirection {
54 /// Read by the routine. The only direction a function's parameters take.
55 #[default]
56 In,
57 /// Written by the routine and read by its caller.
58 Out,
59 /// Both.
60 InOut,
61 /// Any number of values of the parameter's type, collected into the last
62 /// parameter.
63 Variadic,
64}
65
66/// One parameter of the routine.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct RoutineParameter {
69 /// The parameter's name, as Razeri composed it and **unquoted** — a
70 /// dialect that decorates parameter names (T-SQL's `@`, `Redshift`'s
71 /// positional `$1`) applies its own decoration.
72 ///
73 /// Empty where the routine declares a type and no name, which some engines
74 /// allow and which a renderer must then address by position.
75 pub name: String,
76 /// The parameter's type, already rendered by this dialect.
77 pub data_type: String,
78 /// Which way the value travels.
79 pub direction: ParameterDirection,
80 /// The default expression, where the routine declares one.
81 pub default: Option<String>,
82}
83
84/// One local variable the routine declares before its body runs.
85///
86/// Empty for functions; a procedure's declarations are the ones Razeri's
87/// composed load needs.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct RoutineDeclaration {
90 /// The variable's name, unquoted and undecorated.
91 pub name: String,
92 /// Its type, already rendered by this dialect.
93 pub data_type: String,
94 /// The initial value, where the routine declares one.
95 pub default: Option<String>,
96}
97
98/// One column of a table-valued function's row.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct RoutineColumn {
101 /// The column's name, unquoted.
102 pub name: String,
103 /// Its type, already rendered by this dialect.
104 pub data_type: String,
105}
106
107/// What the routine gives back.
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum RoutineReturns {
110 /// Nothing — a procedure.
111 Nothing,
112 /// One value of this type.
113 Value(String),
114 /// Rows of these columns.
115 Columns(Vec<RoutineColumn>),
116 /// Rows shaped like this existing table or type.
117 RowsOf(String),
118 /// Rows whose shape the engine reads off the query.
119 Inferred,
120}
121
122/// The routine's body, in the shape Razeri composed it.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum RoutineBody {
125 /// A single expression the function's value is.
126 Expression(String),
127 /// A query whose rows the function returns.
128 Query(String),
129 /// Statements, in order, each without its terminator. How they are
130 /// separated and how the block around them is opened and closed is the
131 /// engine's business.
132 Statements(Vec<String>),
133 /// A procedural block the design supplied already framed. Emit it as it
134 /// stands, inside whatever this engine wraps a body in.
135 Block(String),
136}
137
138/// **A routine Razeri composed, for a dialect that spells its own grammar.**
139///
140/// Handed to [`SqlRenderer::render_procedure`](crate::SqlRenderer::render_procedure)
141/// and its two siblings when the dialect answered `Custom`.
142///
143/// It is `#[non_exhaustive]`: Razeri may carry one more fact about a routine
144/// later, and a dialect built against today's contract must keep compiling.
145/// Read the fields; do not construct one outside Razeri except in a test, where
146/// [`RoutineSpec::new`] gives you an empty routine of a kind to fill in.
147#[derive(Debug, Clone, PartialEq, Eq)]
148#[non_exhaustive]
149pub struct RoutineSpec {
150 /// Which of the three kinds this is.
151 pub kind: RoutineKind,
152 /// The schema the routine is created in, where the design names one.
153 pub schema: Option<String>,
154 /// The routine's name, unquoted.
155 pub name: String,
156 /// Whether the design asked for a replace rather than a plain create. An
157 /// engine with no `OR REPLACE` says so its own way — a `DROP` in front, or
158 /// nothing at all.
159 pub or_replace: bool,
160 /// The routine's parameters, in order.
161 pub parameters: Vec<RoutineParameter>,
162 /// Local variables the body reads, in order. Empty for a function.
163 pub declarations: Vec<RoutineDeclaration>,
164 /// What the routine gives back.
165 pub returns: RoutineReturns,
166 /// The body.
167 pub body: RoutineBody,
168 /// The description a reader of the DDL should find on the routine. Where
169 /// the engine has no comment clause, it belongs in a comment.
170 pub comment: Option<String>,
171}
172
173impl RoutineSpec {
174 /// **An empty routine of one kind**, to fill in.
175 ///
176 /// The constructor exists so that a dialect's own tests can build the spec
177 /// their render method is going to receive without waiting for Razeri to
178 /// compose one, and so the struct can stay `#[non_exhaustive]` — a field
179 /// added later reaches those tests as a default rather than as a broken
180 /// build.
181 ///
182 /// Parameters:
183 /// - `kind`: which of the three routine kinds this is;
184 /// - `name`: the routine's name, unquoted.
185 #[must_use]
186 pub fn new(kind: RoutineKind, name: impl Into<String>) -> Self {
187 Self {
188 kind,
189 schema: None,
190 name: name.into(),
191 or_replace: false,
192 parameters: Vec::new(),
193 declarations: Vec::new(),
194 returns: match kind {
195 RoutineKind::Procedure => RoutineReturns::Nothing,
196 RoutineKind::ScalarFunction => RoutineReturns::Value(String::new()),
197 RoutineKind::TableFunction => RoutineReturns::Columns(Vec::new()),
198 },
199 body: RoutineBody::Statements(Vec::new()),
200 comment: None,
201 }
202 }
203
204 /// **The name a `CREATE` writes**, schema included where there is one.
205 ///
206 /// Parameters:
207 /// - `quote`: how this engine quotes one identifier.
208 #[must_use]
209 pub fn qualified_name(&self, quote: impl Fn(&str) -> String) -> String {
210 match &self.schema {
211 Some(schema) => format!("{}.{}", quote(schema), quote(&self.name)),
212 None => quote(&self.name),
213 }
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 /// A new spec of each kind returns what that kind returns, so a dialect
222 /// filling one in does not have to remember which.
223 #[test]
224 fn a_new_spec_returns_what_its_kind_returns() {
225 assert_eq!(
226 RoutineSpec::new(RoutineKind::Procedure, "load_customer").returns,
227 RoutineReturns::Nothing
228 );
229 assert_eq!(
230 RoutineSpec::new(RoutineKind::ScalarFunction, "generate_sid").returns,
231 RoutineReturns::Value(String::new())
232 );
233 assert_eq!(
234 RoutineSpec::new(RoutineKind::TableFunction, "customer_at").returns,
235 RoutineReturns::Columns(Vec::new())
236 );
237 }
238
239 /// The qualified name is the engine's quoting over Razeri's names, and a
240 /// routine with no schema is one name and not a leading dot.
241 #[test]
242 fn the_qualified_name_quotes_with_the_engine_and_omits_an_absent_schema() {
243 let quote = |name: &str| format!("\"{name}\"");
244 let mut spec = RoutineSpec::new(RoutineKind::Procedure, "load_customer");
245 assert_eq!(spec.qualified_name(quote), "\"load_customer\"");
246 spec.schema = Some("retail_core".to_owned());
247 assert_eq!(spec.qualified_name(quote), "\"retail_core\".\"load_customer\"");
248 }
249}