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