razeri_plugin_contract/routine_render/
scalar.rs1use super::{
8 inline_parameters, note_above, or_replace, plain_name, quoted_name, quoted_text,
9 refusal_comment,
10};
11use crate::routine_form::ScalarFunctionForm;
12use crate::routine_spec::{RoutineBody, RoutineReturns, RoutineSpec};
13
14#[must_use]
22pub fn render_scalar_function_as(form: ScalarFunctionForm, routine: &RoutineSpec) -> String {
23 match form {
26 ScalarFunctionForm::PostgreSql => postgres(routine),
27 ScalarFunctionForm::Redshift => redshift(routine),
28 ScalarFunctionForm::MySql => my_sql(routine),
29 ScalarFunctionForm::TSql => t_sql(routine),
30 ScalarFunctionForm::Snowflake => snowflake(routine),
31 ScalarFunctionForm::Databricks => databricks(routine),
32 ScalarFunctionForm::GoogleSql => google_sql(routine),
33 ScalarFunctionForm::DuckDb => duckdb(routine),
34 ScalarFunctionForm::ClickHouse => clickhouse(routine),
35 ScalarFunctionForm::Custom(grammar) => note_above(
36 &format!(
37 "You asked for {grammar}, which is your dialect's own grammar and not one spelled \
38 here. The PostgreSQL form below is the nearest neutral starting point; the named \
39 forms are reachable by naming one."
40 ),
41 &postgres(routine),
42 ),
43 ScalarFunctionForm::Unsupported(orientation) => {
44 refusal_comment("function", &routine.name, orientation)
45 }
46 }
47}
48
49fn returns(routine: &RoutineSpec) -> String {
52 match &routine.returns {
53 RoutineReturns::Value(data_type) => data_type.clone(),
54 _ => String::new(),
55 }
56}
57
58fn body(routine: &RoutineSpec) -> String {
60 match &routine.body {
61 RoutineBody::Expression(expression) => expression.clone(),
62 RoutineBody::Query(query) | RoutineBody::Block(query) => query.clone(),
63 RoutineBody::Statements(statements) => statements.join("; "),
64 }
65}
66
67fn postgres(routine: &RoutineSpec) -> String {
69 format!(
70 "CREATE {}FUNCTION {}({})\nRETURNS {}\nLANGUAGE SQL\nRETURN {};",
71 or_replace(routine),
72 plain_name(routine),
73 inline_parameters(routine),
74 returns(routine),
75 body(routine)
76 )
77}
78
79fn redshift(routine: &RoutineSpec) -> String {
83 let mut expression = body(routine);
84 for (index, parameter) in routine.parameters.iter().enumerate() {
85 if !parameter.name.is_empty() {
86 expression = expression.replace(¶meter.name, &format!("${}", index + 1));
87 }
88 }
89 format!(
90 "CREATE {}FUNCTION {}({})\nRETURNS {}\nIMMUTABLE\nAS $$\n SELECT {}\n$$ LANGUAGE sql;",
91 or_replace(routine),
92 plain_name(routine),
93 inline_parameters(routine),
94 returns(routine),
95 expression
96 )
97}
98
99fn my_sql(routine: &RoutineSpec) -> String {
102 let mut lines = Vec::new();
103 if routine.or_replace {
104 lines.push(format!("DROP FUNCTION IF EXISTS {};", plain_name(routine)));
105 }
106 lines.push(format!("CREATE FUNCTION {}({})", plain_name(routine), inline_parameters(routine)));
107 lines.push(format!("RETURNS {}", returns(routine)));
108 lines.push("READS SQL DATA".to_owned());
109 if let Some(comment) = &routine.comment {
110 lines.push(format!("COMMENT '{}'", quoted_text(comment)));
111 }
112 lines.push(format!("RETURN {}", body(routine)));
113 lines.join("\n")
114}
115
116fn t_sql(routine: &RoutineSpec) -> String {
119 let create = if routine.or_replace { "CREATE OR ALTER FUNCTION" } else { "CREATE FUNCTION" };
120 let mut lines = vec![format!("{create} {}(", quoted_name(routine, '[', ']'))];
121 let last = routine.parameters.len().saturating_sub(1);
122 for (index, parameter) in routine.parameters.iter().enumerate() {
123 let comma = if index == last { "" } else { "," };
124 lines.push(format!(" @{} {}{}", parameter.name, parameter.data_type, comma));
125 }
126 lines.push(")".to_owned());
127 lines.push(format!("RETURNS {}", returns(routine)));
128 lines.push("AS".to_owned());
129 lines.push("BEGIN".to_owned());
130 lines.push(format!(" RETURN {}", body(routine)));
131 lines.push("END".to_owned());
132 lines.join("\n")
133}
134
135fn snowflake(routine: &RoutineSpec) -> String {
137 let mut lines = vec![
138 format!(
139 "CREATE {}FUNCTION {}({})",
140 or_replace(routine),
141 plain_name(routine),
142 inline_parameters(routine)
143 ),
144 format!("RETURNS {}", returns(routine)),
145 "LANGUAGE SQL".to_owned(),
146 ];
147 if let Some(comment) = &routine.comment {
148 lines.push(format!("COMMENT = '{}'", quoted_text(comment)));
149 }
150 lines.push("AS $$".to_owned());
151 lines.push(format!("RETURN {};", body(routine)));
152 lines.push("$$;".to_owned());
153 lines.join("\n")
154}
155
156fn databricks(routine: &RoutineSpec) -> String {
158 let mut lines = vec![
159 format!(
160 "CREATE {}FUNCTION {}({})",
161 or_replace(routine),
162 plain_name(routine),
163 inline_parameters(routine)
164 ),
165 format!("RETURNS {}", returns(routine)),
166 ];
167 if let Some(comment) = &routine.comment {
168 lines.push(format!("COMMENT '{}'", quoted_text(comment)));
169 }
170 lines.push(format!("RETURN {}", body(routine)));
171 lines.join("\n")
172}
173
174fn google_sql(routine: &RoutineSpec) -> String {
176 format!(
177 "CREATE {}FUNCTION {}({})\nRETURNS {}\nAS (\n{}\n);",
178 or_replace(routine),
179 quoted_name(routine, '`', '`'),
180 inline_parameters(routine),
181 returns(routine),
182 body(routine)
183 )
184}
185
186fn duckdb(routine: &RoutineSpec) -> String {
190 let parameters =
191 routine.parameters.iter().map(|p| p.name.clone()).collect::<Vec<_>>().join(", ");
192 format!(
193 "CREATE {}MACRO {}({}) AS (\n{}\n);",
194 or_replace(routine),
195 plain_name(routine),
196 parameters,
197 body(routine)
198 )
199}
200
201fn clickhouse(routine: &RoutineSpec) -> String {
204 let parameters =
205 routine.parameters.iter().map(|p| p.name.clone()).collect::<Vec<_>>().join(", ");
206 format!(
207 "CREATE {}FUNCTION {} AS ({}) -> {}",
208 or_replace(routine),
209 routine.name,
210 parameters,
211 body(routine)
212 )
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::routine_spec::{RoutineKind, RoutineParameter};
219
220 fn a_function() -> RoutineSpec {
221 let mut spec = RoutineSpec::new(RoutineKind::ScalarFunction, "busy_day");
222 spec.schema = Some("retail_core".to_owned());
223 spec.or_replace = true;
224 spec.comment = Some("Whether the day was busy.".to_owned());
225 spec.parameters = vec![
226 RoutineParameter {
227 name: "p_rows".to_owned(),
228 data_type: "NUMERIC(19, 4)".to_owned(),
229 declared_type: None,
230 direction: crate::routine_spec::ParameterDirection::In,
231 default: None,
232 },
233 RoutineParameter {
234 name: "p_threshold".to_owned(),
235 data_type: "BIGINT".to_owned(),
236 declared_type: None,
237 direction: crate::routine_spec::ParameterDirection::In,
238 default: None,
239 },
240 ];
241 spec.returns = RoutineReturns::Value("BOOLEAN".to_owned());
242 spec.body = RoutineBody::Expression("p_rows > p_threshold".to_owned());
243 spec
244 }
245
246 #[test]
248 fn the_postgres_form_returns_the_expression() {
249 assert_eq!(
250 render_scalar_function_as(ScalarFunctionForm::PostgreSql, &a_function()),
251 "CREATE OR REPLACE FUNCTION retail_core.busy_day(p_rows NUMERIC(19, 4), p_threshold \
252 BIGINT)\nRETURNS BOOLEAN\nLANGUAGE SQL\nRETURN p_rows > p_threshold;"
253 );
254 }
255
256 #[test]
260 fn the_redshift_form_is_positional_and_puts_language_last() {
261 assert_eq!(
262 render_scalar_function_as(ScalarFunctionForm::Redshift, &a_function()),
263 "CREATE OR REPLACE FUNCTION retail_core.busy_day(p_rows NUMERIC(19, 4), p_threshold \
264 BIGINT)\nRETURNS BOOLEAN\nIMMUTABLE\nAS $$\n SELECT $1 > $2\n$$ LANGUAGE sql;"
265 );
266 }
267
268 #[test]
270 fn the_t_sql_form_brackets_the_name_and_returns_from_a_block() {
271 assert_eq!(
272 render_scalar_function_as(ScalarFunctionForm::TSql, &a_function()),
273 "CREATE OR ALTER FUNCTION [retail_core].[busy_day](\n @p_rows NUMERIC(19, 4),\n \
274 @p_threshold BIGINT\n)\nRETURNS BOOLEAN\nAS\nBEGIN\n RETURN p_rows > \
275 p_threshold\nEND"
276 );
277 }
278
279 #[test]
281 fn the_mysql_form_drops_first() {
282 let rendered = render_scalar_function_as(ScalarFunctionForm::MySql, &a_function());
283 assert!(rendered.starts_with("DROP FUNCTION IF EXISTS retail_core.busy_day;\n"));
284 assert!(rendered.contains("\nREADS SQL DATA\nCOMMENT 'Whether the day was busy.'\n"));
285 }
286
287 #[test]
290 fn the_macro_forms_declare_no_types() {
291 let duck = render_scalar_function_as(ScalarFunctionForm::DuckDb, &a_function());
292 assert_eq!(
293 duck,
294 "CREATE OR REPLACE MACRO retail_core.busy_day(p_rows, p_threshold) AS (\np_rows > \
295 p_threshold\n);"
296 );
297 let click = render_scalar_function_as(ScalarFunctionForm::ClickHouse, &a_function());
298 assert_eq!(
299 click,
300 "CREATE OR REPLACE FUNCTION busy_day AS (p_rows, p_threshold) -> p_rows > p_threshold"
301 );
302 for rendered in [duck, click] {
303 assert!(!rendered.contains("NUMERIC"), "{rendered}");
304 }
305 }
306
307 #[test]
309 fn the_snowflake_and_google_forms_carry_their_own_clauses() {
310 assert_eq!(
311 render_scalar_function_as(ScalarFunctionForm::Snowflake, &a_function()),
312 "CREATE OR REPLACE FUNCTION retail_core.busy_day(p_rows NUMERIC(19, 4), p_threshold \
313 BIGINT)\nRETURNS BOOLEAN\nLANGUAGE SQL\nCOMMENT = 'Whether the day was \
314 busy.'\nAS $$\nRETURN p_rows > p_threshold;\n$$;"
315 );
316 assert_eq!(
317 render_scalar_function_as(ScalarFunctionForm::GoogleSql, &a_function()),
318 "CREATE OR REPLACE FUNCTION `retail_core`.`busy_day`(p_rows NUMERIC(19, 4), \
319 p_threshold BIGINT)\nRETURNS BOOLEAN\nAS (\np_rows > p_threshold\n);"
320 );
321 }
322
323 #[test]
326 fn an_unsupported_form_spells_no_statement() {
327 let rendered = render_scalar_function_as(
328 ScalarFunctionForm::Unsupported("compute the value in the SELECT that needs it"),
329 &a_function(),
330 );
331 assert!(rendered.contains("compute the value in the SELECT that needs it"));
332 assert!(!rendered.contains("CREATE"), "{rendered}");
333 }
334}