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