Skip to main content

razeri_plugin_contract/routine_render/
mod.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 named routine grammars, rendered where a plugin can reach them.**
6//!
7//! A dialect states which grammar its engine speaks through
8//! [`ProcedureForm`](crate::ProcedureForm),
9//! [`ScalarFunctionForm`](crate::ScalarFunctionForm) and
10//! [`TableFunctionForm`](crate::TableFunctionForm). Where the answer is one of
11//! the named grammars, Razeri spells it. Where the answer is `Custom`, the
12//! dialect spells the whole `CREATE` itself — and until this module existed, a
13//! dialect whose grammar was *"PL/pgSQL with one tweak"* had to write the whole
14//! thing from nothing, with no way to see, reuse or diff against the grammar it
15//! was almost the same as.
16//!
17//! These three functions are that grammar, addressable:
18//!
19//! ```no_run
20//! use razeri_plugin_contract::routine_render::render_procedure_as;
21//! use razeri_plugin_contract::{ProcedureForm, RoutineSpec};
22//!
23//! # fn spell(routine: &RoutineSpec) -> Option<String> {
24//! // Start from the neighbouring grammar, then say what is different.
25//! let near = render_procedure_as(ProcedureForm::PlPgSql, routine);
26//! Some(near.replace("LANGUAGE plpgsql", "LANGUAGE plpgsql_with_one_tweak"))
27//! # }
28//! ```
29//!
30//! # What these render from, and what that means
31//!
32//! They render from a [`RoutineSpec`] — the routine as a `Custom` dialect
33//! receives it: names, parameters with their types already spelled, local
34//! declarations, what it returns, the body's statements, the comment. That is
35//! everything a `CREATE` needs, and it is deliberately not the design Razeri
36//! composed it from.
37//!
38//! So the output is **the form's grammar around the statements it is given**. A
39//! statement's own spelling was decided before the spec was built: where Razeri
40//! composes a routine from its own design it spells each statement for the
41//! engine first, and where a caller builds a spec by hand the statements are
42//! whatever the caller wrote. Neither is this module's business, and that is
43//! why one function can answer for every form.
44//!
45//! # Which forms answer with SQL
46//!
47//! Every form that names a SQL grammar. Two [`ProcedureForm`](crate::ProcedureForm) variants do not
48//! name one: `ClickHouse` deploys a procedure as an executable program with an
49//! XML declaration beside it, and `Databricks` as a Python function — neither is
50//! a `CREATE` any engine parses, and neither can be spelled from a spec. Both
51//! answer here with the agnostic form under a comment saying so, because a
52//! wrong `CREATE` is worse than a plain sentence.
53//!
54//! `Custom` answers with the agnostic form too, under a comment naming the
55//! grammar. It is not an error to ask: a dialect that wants a neutral starting
56//! point, or a fallback branch that reaches this with its own form, gets
57//! something to read rather than a panic.
58//!
59//! `Unsupported` answers with the dialect's own words in a comment, and never a
60//! statement — the same sentence, in the same place, that Razeri puts where the
61//! routine would have been.
62
63mod procedure;
64mod scalar;
65mod table;
66
67pub use procedure::render_procedure_as;
68pub use scalar::render_scalar_function_as;
69pub use table::render_table_function_as;
70
71use crate::routine_spec::{RoutineParameter, RoutineSpec};
72
73/// The name a `CREATE` writes, unquoted: `schema.name`, or `name` alone.
74pub(crate) fn plain_name(routine: &RoutineSpec) -> String {
75    routine.qualified_name(std::borrow::ToOwned::to_owned)
76}
77
78/// The name under one engine's quoting.
79pub(crate) fn quoted_name(routine: &RoutineSpec, open: char, close: char) -> String {
80    routine.qualified_name(|part| format!("{open}{part}{close}"))
81}
82
83/// A single-quoted SQL string, with the quotes inside it doubled.
84pub(crate) fn quoted_text(text: &str) -> String {
85    text.replace('\'', "''")
86}
87
88/// `OR REPLACE `, or nothing.
89pub(crate) fn or_replace(routine: &RoutineSpec) -> &'static str {
90    if routine.or_replace { "OR REPLACE " } else { "" }
91}
92
93/// One parameter as `name type`, the shape most grammars take.
94pub(crate) fn name_and_type(parameter: &RoutineParameter) -> String {
95    if parameter.name.is_empty() {
96        parameter.data_type.clone()
97    } else {
98        format!("{} {}", parameter.name, parameter.data_type)
99    }
100}
101
102/// Every parameter on one line, comma separated.
103pub(crate) fn inline_parameters(routine: &RoutineSpec) -> String {
104    routine.parameters.iter().map(name_and_type).collect::<Vec<_>>().join(", ")
105}
106
107/// Just the types, which is how several engines identify a routine they are
108/// commenting on or dropping.
109pub(crate) fn parameter_types(routine: &RoutineSpec) -> String {
110    routine.parameters.iter().map(|p| p.data_type.clone()).collect::<Vec<_>>().join(", ")
111}
112
113/// A statement with its first line indented and the rest left as composed — a
114/// block the design supplied already framed keeps the shape it was framed in.
115pub(crate) fn indented(statement: &str, pad: &str) -> String {
116    format!("{pad}{statement}")
117}
118
119/// The body's statements, each terminated once, at one level of indentation.
120///
121/// A statement Razeri composed already carries its terminator and a statement a
122/// caller wrote by hand often does not, so this adds one only where there is
123/// none — two `;` in a row is a syntax error on several engines and a silent
124/// empty statement on the rest.
125pub(crate) fn terminated_statements(
126    statements: &[String],
127    pad: &str,
128    terminator: &str,
129) -> Vec<String> {
130    statements
131        .iter()
132        .map(|statement| {
133            let statement = indented(statement, pad);
134            if statement.trim_end().ends_with(terminator) {
135                statement
136            } else {
137                format!("{statement}{terminator}")
138            }
139        })
140        .collect()
141}
142
143/// **A refusal, as a comment.** Never a statement: it lands where DDL was
144/// expected, and text that parses would define something over the real name.
145pub(crate) fn refusal_comment(kind: &str, name: &str, orientation: &str) -> String {
146    format!("-- No {kind} named `{name}` is written here.\n-- {orientation}")
147}
148
149/// **A note above an agnostic rendering**, for the forms that have no `CREATE`
150/// to give.
151pub(crate) fn note_above(note: &str, rendered: &str) -> String {
152    format!("-- {note}\n{rendered}")
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::routine_spec::{RoutineKind, RoutineSpec};
159
160    fn a_spec() -> RoutineSpec {
161        let mut spec = RoutineSpec::new(RoutineKind::Procedure, "load_customer");
162        spec.schema = Some("retail_core".to_owned());
163        spec
164    }
165
166    /// The name a grammar writes is the name the spec carries, under that
167    /// grammar's quoting and no other.
168    #[test]
169    fn a_name_is_quoted_the_way_the_grammar_asked() {
170        assert_eq!(plain_name(&a_spec()), "retail_core.load_customer");
171        assert_eq!(quoted_name(&a_spec(), '[', ']'), "[retail_core].[load_customer]");
172        assert_eq!(quoted_name(&a_spec(), '`', '`'), "`retail_core`.`load_customer`");
173    }
174
175    /// A quote inside a description does not end the description.
176    #[test]
177    fn a_quote_inside_a_description_is_doubled() {
178        assert_eq!(quoted_text("the day's rows"), "the day''s rows");
179    }
180
181    /// **A statement is terminated once.** A statement that arrived with its
182    /// terminator keeps the one it has, because two in a row is a syntax error
183    /// on several engines and a silent empty statement on the rest.
184    #[test]
185    fn a_statement_is_terminated_once_and_not_twice() {
186        let statements = vec!["DELETE FROM stage".to_owned(), "IF x THEN NULL; END IF;".to_owned()];
187        assert_eq!(
188            terminated_statements(&statements, "    ", ";"),
189            vec!["    DELETE FROM stage;".to_owned(), "    IF x THEN NULL; END IF;".to_owned()]
190        );
191    }
192}