Skip to main content

razeri_plugin_contract/
routine_form.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//! **Which routine grammar this engine speaks** — the dialect's answer, not a
6//! core crate's guess about its name.
7//!
8//! A stored procedure, a scalar function and a table function are the three
9//! places where an engine's SQL stops resembling every other engine's. Razeri
10//! composes the routine's *content* — its parameters, its body, its comment —
11//! and asks the dialect which grammar to spell it in.
12//!
13//! All three enums are `#[non_exhaustive]`: Razeri adds a form when it has
14//! measured one, and a dialect must state a fallback rather than assume the
15//! list it compiled against is the list that ships.
16//!
17//! # If none of these is your engine
18//!
19//! There are three honest answers, and they say three different things.
20//!
21//! - **A near neighbour.** Pick the closest measured form and *run the result*
22//!   — the near-neighbour trap is real, and a form that almost fits fails at
23//!   `CREATE`, not at review;
24//! - **`Custom`, on any of the three.** Your engine has this routine kind and
25//!   spells it in a grammar Razeri does not carry — Oracle's PL/SQL is the
26//!   worked case. `Custom` names your grammar and hands the rendering to your
27//!   dialect: Razeri substitutes nothing and refuses nothing, and your
28//!   dialect's own render method spells the whole `CREATE`. See
29//!   [`RoutineSpec`](crate::routine_spec::RoutineSpec) for what Razeri hands
30//!   you and [`SqlRenderer::render_procedure`](crate::SqlRenderer::render_procedure)
31//!   for the method you owe;
32//! - **`Unsupported`, on any of the three.** Your engine has no routine of this
33//!   kind at any spelling. It carries your own words, and those words are what
34//!   the author reads instead of another engine's.
35//!
36//! `Custom` and `Unsupported` are opposite claims and the difference matters to
37//! a reader: one says *this engine has it and Razeri cannot spell it*, the
38//! other says *this engine does not have it*. Answering `Unsupported` for a
39//! grammar Razeri merely lacks tells every reader something false about the
40//! engine.
41
42/// Which stored-procedure grammar this engine speaks.
43///
44/// The default is [`ProcedureForm::Agnostic`], Razeri's own annotated form. It
45/// parses nowhere: it is the honest answer for a dialect that has not said, and
46/// the conformance kit reports it as an unanswered obligation.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48#[non_exhaustive]
49pub enum ProcedureForm {
50    /// PL/pgSQL — a dollar-quoted body with `DECLARE`, `SELECT … INTO`,
51    /// `RAISE`, `IF` and an `EXCEPTION` block. `PostgreSQL`, Citus and
52    /// `Redshift`, all three measured deploying and running.
53    PlPgSql,
54    /// `MySQL`/`MariaDB`'s compound statement — the same block structure with
55    /// `DELIMITER`-free `BEGIN … END` and no dollar quoting.
56    MySql,
57    /// T-SQL — `CREATE PROCEDURE … AS BEGIN … END`, `@`-prefixed parameters.
58    TSql,
59    /// Snowflake Scripting.
60    SnowflakeScripting,
61    /// Databricks SQL scripting.
62    Databricks,
63    /// `ClickHouse`.
64    ClickHouse,
65    /// `GoogleSQL` scripting — `BigQuery`'s, whose body is not quoted at all.
66    GoogleSql,
67    /// Razeri's agnostic annotated form: ANSI SQL carrying `@razeri:` headers.
68    /// It is a design artifact, not something an engine parses.
69    #[default]
70    Agnostic,
71    /// **This engine has stored procedures and spells them in a grammar Razeri
72    /// does not carry.** The string is the grammar's own name — `"PL/SQL"`,
73    /// `"Teradata SPL"` — and it is what a reader of a report or a rendered
74    /// comment sees.
75    ///
76    /// Answering this moves the rendering to your dialect and moves nothing
77    /// else: Razeri neither substitutes a near neighbour nor refuses, and every
78    /// question it would have answered from a named grammar it asks your
79    /// dialect instead. The method it asks is
80    /// [`SqlRenderer::render_procedure`](crate::SqlRenderer::render_procedure),
81    /// and **a dialect that answers `Custom` without overriding it has
82    /// promised a rendering it does not provide** — the conformance kit
83    /// reports that as blocking, and the rendered SQL says the same thing in a
84    /// comment where the procedure would have been.
85    Custom(&'static str),
86    /// **This engine has no stored procedures.** The string is the orientation
87    /// an author reads instead of a syntax error — say what this engine offers
88    /// in their place (`DuckDB`'s macros, `SQLite`'s application-level logic),
89    /// because the sentence that helps is the one about *this* engine.
90    ///
91    /// Not the answer for a grammar Razeri cannot spell — that is
92    /// [`Custom`](Self::Custom), and the difference is the difference between a
93    /// fact about the engine and a fact about Razeri.
94    Unsupported(&'static str),
95}
96
97impl ProcedureForm {
98    /// The dialect's own refusal wording, or `None` where this engine has
99    /// stored procedures.
100    #[must_use]
101    pub const fn refusal(self) -> Option<&'static str> {
102        match self {
103            ProcedureForm::Unsupported(orientation) => Some(orientation),
104            _ => None,
105        }
106    }
107
108    /// The name of the grammar this dialect renders itself, or `None` where
109    /// Razeri renders the procedure.
110    ///
111    /// The one accessor every surface reads — the renderer that delegates, the
112    /// conformance kit that checks the dialect kept its promise, and any report
113    /// that names the grammar — so none of them carries a second reading of the
114    /// variant.
115    #[must_use]
116    pub const fn custom_grammar(self) -> Option<&'static str> {
117        match self {
118            ProcedureForm::Custom(grammar) => Some(grammar),
119            _ => None,
120        }
121    }
122}
123
124/// Which scalar-function grammar this engine speaks.
125///
126/// The default is [`ScalarFunctionForm::PostgreSql`], which is what an
127/// unlisted dialect has always been handed. It is a guess and a dangerous one
128/// on a near neighbour: `LANGUAGE SQL IMMUTABLE PARALLEL SAFE` is a *"syntax
129/// error at or near `PARALLEL`"* on `Redshift`, an engine that descends from
130/// `PostgreSQL` and reports itself as one. State your form.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
132#[non_exhaustive]
133pub enum ScalarFunctionForm {
134    /// `CREATE FUNCTION … RETURNS … LANGUAGE SQL IMMUTABLE PARALLEL SAFE AS
135    /// $$…$$` — `PostgreSQL`, Citus and Razeri's agnostic renderer.
136    #[default]
137    PostgreSql,
138    /// `DuckDB`'s `CREATE MACRO`.
139    DuckDb,
140    /// T-SQL's scalar UDF.
141    TSql,
142    /// `ClickHouse`'s lambda `CREATE FUNCTION … AS (x) -> …`.
143    ClickHouse,
144    /// Snowflake's UDF.
145    Snowflake,
146    /// `MySQL`/`MariaDB`'s `CREATE FUNCTION … DETERMINISTIC`.
147    MySql,
148    /// Databricks SQL's UDF.
149    Databricks,
150    /// `GoogleSQL`'s `CREATE FUNCTION` — `BigQuery`'s.
151    GoogleSql,
152    /// `Redshift`'s: the volatility is mandatory rather than optional, there is
153    /// no `PARALLEL SAFE`, `LANGUAGE` follows the body rather than preceding
154    /// it, and the body addresses its parameters as `$1` and never by name.
155    Redshift,
156    /// **This engine has scalar functions and spells them in a grammar Razeri
157    /// does not carry.** The string is the grammar's own name.
158    ///
159    /// Your dialect renders the whole `CREATE`, through
160    /// [`SqlRenderer::render_scalar_function`](crate::SqlRenderer::render_scalar_function).
161    /// Razeri substitutes no near neighbour and refuses nothing; a dialect that
162    /// answers `Custom` without overriding that method has promised a rendering
163    /// it does not provide, and the conformance kit reports it.
164    Custom(&'static str),
165    /// **This engine has no scalar function.**
166    ///
167    /// Rare — rare enough that no engine Razeri has measured is one — and the
168    /// default was a silent `PostgreSql` for want of anywhere to say it. The
169    /// string is the orientation an author reads: say what this engine offers
170    /// instead before saying what it lacks, and name the near form if there is
171    /// one.
172    ///
173    /// It is prose that lands inside a SQL comment, so it must never spell a
174    /// complete statement.
175    Unsupported(&'static str),
176}
177
178impl ScalarFunctionForm {
179    /// The dialect's own refusal wording, or `None` where this engine has a
180    /// scalar function.
181    #[must_use]
182    pub const fn refusal(self) -> Option<&'static str> {
183        match self {
184            ScalarFunctionForm::Unsupported(orientation) => Some(orientation),
185            _ => None,
186        }
187    }
188
189    /// The name of the grammar this dialect renders itself, or `None` where
190    /// Razeri renders the function.
191    #[must_use]
192    pub const fn custom_grammar(self) -> Option<&'static str> {
193        match self {
194            ScalarFunctionForm::Custom(grammar) => Some(grammar),
195            _ => None,
196        }
197    }
198}
199
200/// Which **table-valued function** grammar this engine speaks — the routine a
201/// query selects `FROM`, returning columns rather than one value.
202///
203/// The default is [`TableFunctionForm::PostgreSql`], which is what an unlisted
204/// dialect has always been handed. Four engines in Razeri's own set have no
205/// table-valued function they can render, and each says so in **its own words**
206/// through [`TableFunctionForm::Unsupported`] — because a refusal written once
207/// per *kind* tells a `Redshift` author about `MySQL`, and the sentence that
208/// helps is the one about their engine.
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
210#[non_exhaustive]
211pub enum TableFunctionForm {
212    /// `CREATE FUNCTION … RETURNS TABLE(…)` / `RETURNS SETOF …` over a
213    /// dollar-quoted body — `PostgreSQL`, Citus, and Razeri's agnostic
214    /// renderer.
215    #[default]
216    PostgreSql,
217    /// `DuckDB`'s table `MACRO`: `CREATE MACRO f(x) AS TABLE SELECT …`. There
218    /// is no `RETURNS` clause at all, which is why riding the `PostgreSQL` arm
219    /// was `Parser Error: syntax error at or near "RETURNS"`.
220    DuckDb,
221    /// `GoogleSQL`'s `CREATE TABLE FUNCTION` — `BigQuery`'s.
222    GoogleSql,
223    /// T-SQL's two shapes: the inline TVF (`RETURNS TABLE AS RETURN (…)`) and
224    /// the multi-statement one (`RETURNS @t TABLE(…) BEGIN … RETURN END`).
225    TSql,
226    /// `ClickHouse`'s parameterized view, addressed with `{param:Type}`.
227    ClickHouse,
228    /// Snowflake's UDTF, including the staged-handler registration.
229    Snowflake,
230    /// Databricks SQL's `CREATE FUNCTION … RETURNS TABLE … RETURN <query>`.
231    Databricks,
232    /// **This engine has no table-valued function.**
233    ///
234    /// The string is the orientation an author reads — at build time, when
235    /// Razeri refuses to write the routine, and again in the rendered refusal
236    /// if anything asks the renderer directly. Write it fix-first: say what
237    /// this engine offers *instead* before saying what it lacks, name the
238    /// `artifact:` values a designer would reach for, and name the near form if
239    /// there is one, because a near form the author will find on their own is
240    /// the thing that costs them a day.
241    ///
242    /// It is prose that lands inside a SQL comment, so it must never spell a
243    /// complete statement.
244    Unsupported(&'static str),
245    /// **This engine has a table-valued function and spells it in a grammar
246    /// Razeri does not carry.** The string is the grammar's own name.
247    ///
248    /// Your dialect renders the whole `CREATE`, through
249    /// [`SqlRenderer::render_table_function`](crate::SqlRenderer::render_table_function).
250    /// It is the opposite claim to [`Unsupported`](Self::Unsupported): the
251    /// build door writes the routine rather than declining it, because this
252    /// engine has one.
253    Custom(&'static str),
254}
255
256impl TableFunctionForm {
257    /// The dialect's own refusal wording, or `None` where this engine has the
258    /// form.
259    ///
260    /// The one accessor both surfaces read — the build door that declines to
261    /// write the routine, and the renderer that would otherwise emit DDL — so
262    /// neither one carries a second copy of the words.
263    ///
264    /// [`Custom`](Self::Custom) answers `None` here, and that is the whole
265    /// point of it: an engine whose grammar Razeri cannot spell still **has** a
266    /// table-valued function, so the door that asks *may I write this at all*
267    /// gets a yes.
268    #[must_use]
269    pub const fn refusal(self) -> Option<&'static str> {
270        match self {
271            TableFunctionForm::Unsupported(orientation) => Some(orientation),
272            _ => None,
273        }
274    }
275
276    /// The name of the grammar this dialect renders itself, or `None` where
277    /// Razeri renders the function.
278    #[must_use]
279    pub const fn custom_grammar(self) -> Option<&'static str> {
280        match self {
281            TableFunctionForm::Custom(grammar) => Some(grammar),
282            _ => None,
283        }
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    /// The defaults are what an undecided dialect gets, and they are the
292    /// behaviour Razeri has always had for an unlisted platform.
293    #[test]
294    fn the_defaults_are_the_agnostic_procedure_and_the_postgres_functions() {
295        assert_eq!(ProcedureForm::default(), ProcedureForm::Agnostic);
296        assert_eq!(ScalarFunctionForm::default(), ScalarFunctionForm::PostgreSql);
297        assert_eq!(TableFunctionForm::default(), TableFunctionForm::PostgreSql);
298    }
299
300    /// A form that renders carries no refusal; an `Unsupported` one carries the
301    /// dialect's own sentence and nothing else does.
302    #[test]
303    fn only_the_unsupported_form_carries_refusal_wording() {
304        assert_eq!(TableFunctionForm::PostgreSql.refusal(), None);
305        assert_eq!(TableFunctionForm::GoogleSql.refusal(), None);
306        assert_eq!(
307            TableFunctionForm::Unsupported("my engine has none").refusal(),
308            Some("my engine has none")
309        );
310    }
311
312    /// **`Custom` is a claim that the engine HAS the routine kind**, so it
313    /// carries no refusal on any of the three enums. Every door that asks
314    /// *"may I write this at all"* reads `refusal()`, and a `Custom` that
315    /// answered a sentence there would have Razeri decline to write a routine
316    /// the engine is perfectly able to run.
317    #[test]
318    fn a_custom_grammar_is_not_a_refusal_on_any_of_the_three() {
319        assert_eq!(ProcedureForm::Custom("PL/SQL").refusal(), None);
320        assert_eq!(ScalarFunctionForm::Custom("PL/SQL").refusal(), None);
321        assert_eq!(TableFunctionForm::Custom("PL/SQL").refusal(), None);
322    }
323
324    /// The grammar's name reaches every reader from one accessor, and only
325    /// `Custom` has one to give.
326    #[test]
327    fn only_the_custom_form_names_a_grammar() {
328        assert_eq!(ProcedureForm::Custom("PL/SQL").custom_grammar(), Some("PL/SQL"));
329        assert_eq!(ScalarFunctionForm::Custom("PL/SQL").custom_grammar(), Some("PL/SQL"));
330        assert_eq!(TableFunctionForm::Custom("PL/SQL").custom_grammar(), Some("PL/SQL"));
331        assert_eq!(ProcedureForm::TSql.custom_grammar(), None);
332        assert_eq!(ScalarFunctionForm::Redshift.custom_grammar(), None);
333        assert_eq!(TableFunctionForm::DuckDb.custom_grammar(), None);
334        assert_eq!(ProcedureForm::Unsupported("none here").custom_grammar(), None);
335    }
336
337    /// **The scalar-function enum can now say what the other two always
338    /// could.** An engine with no scalar function had one reachable answer, and
339    /// it was `PostgreSql` — a grammar, silently, for an engine that has none.
340    #[test]
341    fn the_scalar_function_form_can_refuse_in_its_own_words() {
342        assert_eq!(
343            ScalarFunctionForm::Unsupported("no scalar function here").refusal(),
344            Some("no scalar function here")
345        );
346        assert_eq!(ScalarFunctionForm::PostgreSql.refusal(), None);
347    }
348}