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}