razeri_plugin_contract/routine_form.rs
1// Copyright (C) 2026 Gabriel Shea <gshea@ideapivot.com> - All Rights Reserved
2
3//! **Which routine grammar this engine speaks** — the dialect's answer, not a
4//! core crate's guess about its name.
5//!
6//! A stored procedure, a scalar function and a table function are the three
7//! places where an engine's SQL stops resembling every other engine's. Razeri
8//! composes the routine's *content* — its parameters, its body, its comment —
9//! and asks the dialect which grammar to spell it in.
10//!
11//! All three enums are `#[non_exhaustive]`: Razeri adds a form when it has
12//! measured one, and a dialect must state a fallback rather than assume the
13//! list it compiled against is the list that ships.
14//!
15//! # If none of these is your engine
16//!
17//! Pick the closest measured form and *run the result* — the near-neighbour
18//! trap is real, and a form that almost fits fails at `CREATE`, not at review.
19//! If nothing fits, [`ProcedureForm::Unsupported`] and
20//! [`TableFunctionForm::Unsupported`] carry your own words, and those words are
21//! what the author reads instead of another engine's.
22
23/// Which stored-procedure grammar this engine speaks.
24///
25/// The default is [`ProcedureForm::Agnostic`], Razeri's own annotated form. It
26/// parses nowhere: it is the honest answer for a dialect that has not said, and
27/// the conformance kit reports it as an unanswered obligation.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29#[non_exhaustive]
30pub enum ProcedureForm {
31 /// PL/pgSQL — a dollar-quoted body with `DECLARE`, `SELECT … INTO`,
32 /// `RAISE`, `IF` and an `EXCEPTION` block. `PostgreSQL`, Citus and
33 /// `Redshift`, all three measured deploying and running.
34 PlPgSql,
35 /// `MySQL`/`MariaDB`'s compound statement — the same block structure with
36 /// `DELIMITER`-free `BEGIN … END` and no dollar quoting.
37 MySql,
38 /// T-SQL — `CREATE PROCEDURE … AS BEGIN … END`, `@`-prefixed parameters.
39 TSql,
40 /// Snowflake Scripting.
41 SnowflakeScripting,
42 /// Databricks SQL scripting.
43 Databricks,
44 /// `ClickHouse`.
45 ClickHouse,
46 /// `GoogleSQL` scripting — `BigQuery`'s, whose body is not quoted at all.
47 GoogleSql,
48 /// Razeri's agnostic annotated form: ANSI SQL carrying `@razeri:` headers.
49 /// It is a design artifact, not something an engine parses.
50 #[default]
51 Agnostic,
52 /// **This engine has no stored procedures.** The string is the orientation
53 /// an author reads instead of a syntax error — say what this engine offers
54 /// in their place (`DuckDB`'s macros, `SQLite`'s application-level logic),
55 /// because the sentence that helps is the one about *this* engine.
56 Unsupported(&'static str),
57}
58
59/// Which scalar-function grammar this engine speaks.
60///
61/// The default is [`ScalarFunctionForm::PostgreSql`], which is what an
62/// unlisted dialect has always been handed. It is a guess and a dangerous one
63/// on a near neighbour: `LANGUAGE SQL IMMUTABLE PARALLEL SAFE` is a *"syntax
64/// error at or near `PARALLEL`"* on `Redshift`, an engine that descends from
65/// `PostgreSQL` and reports itself as one. State your form.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
67#[non_exhaustive]
68pub enum ScalarFunctionForm {
69 /// `CREATE FUNCTION … RETURNS … LANGUAGE SQL IMMUTABLE PARALLEL SAFE AS
70 /// $$…$$` — `PostgreSQL`, Citus and Razeri's agnostic renderer.
71 #[default]
72 PostgreSql,
73 /// `DuckDB`'s `CREATE MACRO`.
74 DuckDb,
75 /// T-SQL's scalar UDF.
76 TSql,
77 /// `ClickHouse`'s lambda `CREATE FUNCTION … AS (x) -> …`.
78 ClickHouse,
79 /// Snowflake's UDF.
80 Snowflake,
81 /// `MySQL`/`MariaDB`'s `CREATE FUNCTION … DETERMINISTIC`.
82 MySql,
83 /// Databricks SQL's UDF.
84 Databricks,
85 /// `GoogleSQL`'s `CREATE FUNCTION` — `BigQuery`'s.
86 GoogleSql,
87 /// `Redshift`'s: the volatility is mandatory rather than optional, there is
88 /// no `PARALLEL SAFE`, `LANGUAGE` follows the body rather than preceding
89 /// it, and the body addresses its parameters as `$1` and never by name.
90 Redshift,
91}
92
93/// Which **table-valued function** grammar this engine speaks — the routine a
94/// query selects `FROM`, returning columns rather than one value.
95///
96/// The default is [`TableFunctionForm::PostgreSql`], which is what an unlisted
97/// dialect has always been handed. Four engines in Razeri's own set have no
98/// table-valued function they can render, and each says so in **its own words**
99/// through [`TableFunctionForm::Unsupported`] — because a refusal written once
100/// per *kind* tells a `Redshift` author about `MySQL`, and the sentence that
101/// helps is the one about their engine.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
103#[non_exhaustive]
104pub enum TableFunctionForm {
105 /// `CREATE FUNCTION … RETURNS TABLE(…)` / `RETURNS SETOF …` over a
106 /// dollar-quoted body — `PostgreSQL`, Citus, and Razeri's agnostic
107 /// renderer.
108 #[default]
109 PostgreSql,
110 /// `DuckDB`'s table `MACRO`: `CREATE MACRO f(x) AS TABLE SELECT …`. There
111 /// is no `RETURNS` clause at all, which is why riding the `PostgreSQL` arm
112 /// was `Parser Error: syntax error at or near "RETURNS"`.
113 DuckDb,
114 /// `GoogleSQL`'s `CREATE TABLE FUNCTION` — `BigQuery`'s.
115 GoogleSql,
116 /// T-SQL's two shapes: the inline TVF (`RETURNS TABLE AS RETURN (…)`) and
117 /// the multi-statement one (`RETURNS @t TABLE(…) BEGIN … RETURN END`).
118 TSql,
119 /// `ClickHouse`'s parameterized view, addressed with `{param:Type}`.
120 ClickHouse,
121 /// Snowflake's UDTF, including the staged-handler registration.
122 Snowflake,
123 /// Databricks SQL's `CREATE FUNCTION … RETURNS TABLE … RETURN <query>`.
124 Databricks,
125 /// **This engine has no table-valued function.**
126 ///
127 /// The string is the orientation an author reads — at build time, when
128 /// Razeri refuses to write the routine, and again in the rendered refusal
129 /// if anything asks the renderer directly. Write it fix-first: say what
130 /// this engine offers *instead* before saying what it lacks, name the
131 /// `artifact:` values a designer would reach for, and name the near form if
132 /// there is one, because a near form the author will find on their own is
133 /// the thing that costs them a day.
134 ///
135 /// It is prose that lands inside a SQL comment, so it must never spell a
136 /// complete statement.
137 Unsupported(&'static str),
138}
139
140impl TableFunctionForm {
141 /// The dialect's own refusal wording, or `None` where this engine has the
142 /// form.
143 ///
144 /// The one accessor both surfaces read — the build door that declines to
145 /// write the routine, and the renderer that would otherwise emit DDL — so
146 /// neither one carries a second copy of the words.
147 #[must_use]
148 pub const fn refusal(self) -> Option<&'static str> {
149 match self {
150 TableFunctionForm::Unsupported(orientation) => Some(orientation),
151 _ => None,
152 }
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 /// The defaults are what an undecided dialect gets, and they are the
161 /// behaviour Razeri has always had for an unlisted platform.
162 #[test]
163 fn the_defaults_are_the_agnostic_procedure_and_the_postgres_functions() {
164 assert_eq!(ProcedureForm::default(), ProcedureForm::Agnostic);
165 assert_eq!(ScalarFunctionForm::default(), ScalarFunctionForm::PostgreSql);
166 assert_eq!(TableFunctionForm::default(), TableFunctionForm::PostgreSql);
167 }
168
169 /// A form that renders carries no refusal; an `Unsupported` one carries the
170 /// dialect's own sentence and nothing else does.
171 #[test]
172 fn only_the_unsupported_form_carries_refusal_wording() {
173 assert_eq!(TableFunctionForm::PostgreSql.refusal(), None);
174 assert_eq!(TableFunctionForm::GoogleSql.refusal(), None);
175 assert_eq!(
176 TableFunctionForm::Unsupported("my engine has none").refusal(),
177 Some("my engine has none")
178 );
179 }
180}