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
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}