razeri_plugin_contract/routine_render/
table.rs1use super::{
8 inline_parameters, note_above, or_replace, plain_name, quoted_name, quoted_text,
9 refusal_comment,
10};
11use crate::routine_form::TableFunctionForm;
12use crate::routine_spec::{ParameterDirection, RoutineBody, RoutineReturns, RoutineSpec};
13
14#[must_use]
21pub fn render_table_function_as(form: TableFunctionForm, routine: &RoutineSpec) -> String {
22 match form {
25 TableFunctionForm::PostgreSql => replacing_postgres(routine),
26 TableFunctionForm::TSql => t_sql(routine),
27 TableFunctionForm::Snowflake => snowflake(routine),
28 TableFunctionForm::Databricks => databricks(routine),
29 TableFunctionForm::GoogleSql => google_sql(routine),
30 TableFunctionForm::DuckDb => duckdb(routine),
31 TableFunctionForm::ClickHouse => clickhouse(routine),
32 TableFunctionForm::Custom(grammar) => note_above(
33 &format!(
34 "You asked for {grammar}, which is your dialect's own grammar and not one spelled \
35 here. The PostgreSQL form below is the nearest neutral starting point; the named \
36 forms are reachable by naming one."
37 ),
38 &postgres(routine),
39 ),
40 TableFunctionForm::Unsupported(orientation) => {
41 refusal_comment("table function", &routine.name, orientation)
42 }
43 }
44}
45
46fn row_shape(routine: &RoutineSpec) -> String {
48 match &routine.returns {
49 RoutineReturns::Columns(columns) => format!(
50 "TABLE({})",
51 columns
52 .iter()
53 .map(|c| format!("{} {}", c.name, c.data_type))
54 .collect::<Vec<_>>()
55 .join(", ")
56 ),
57 RoutineReturns::RowsOf(relation) => format!("SETOF {relation}"),
58 RoutineReturns::Value(data_type) => format!("SETOF {data_type}"),
59 RoutineReturns::Inferred | RoutineReturns::Nothing => "TABLE".to_owned(),
60 }
61}
62
63fn body(routine: &RoutineSpec) -> String {
65 match &routine.body {
66 RoutineBody::Query(query) | RoutineBody::Block(query) => query.clone(),
67 RoutineBody::Expression(expression) => expression.clone(),
68 RoutineBody::Statements(statements) => statements.join(";\n"),
69 }
70}
71
72fn replacing_postgres(routine: &RoutineSpec) -> String {
82 if !routine.or_replace {
83 return postgres(routine);
84 }
85 let inputs = routine
86 .parameters
87 .iter()
88 .filter(|parameter| parameter.direction != ParameterDirection::Out)
89 .map(|parameter| parameter.data_type.as_str())
90 .collect::<Vec<_>>()
91 .join(", ");
92 format!("DROP FUNCTION IF EXISTS {}({inputs});\n{}", plain_name(routine), postgres(routine))
93}
94
95fn postgres(routine: &RoutineSpec) -> String {
98 format!(
99 "CREATE {}FUNCTION {}({})\nRETURNS {}\nLANGUAGE SQL\nAS $$\n{}\n$$;",
100 or_replace(routine),
101 plain_name(routine),
102 inline_parameters(routine),
103 row_shape(routine),
104 body(routine)
105 )
106}
107
108fn t_sql(routine: &RoutineSpec) -> String {
111 let create = if routine.or_replace { "CREATE OR ALTER FUNCTION" } else { "CREATE FUNCTION" };
112 let mut lines = vec![format!("{create} {}(", quoted_name(routine, '[', ']'))];
113 let last = routine.parameters.len().saturating_sub(1);
114 for (index, parameter) in routine.parameters.iter().enumerate() {
115 let comma = if index == last { "" } else { "," };
116 lines.push(format!(" @{} {}{}", parameter.name, parameter.data_type, comma));
117 }
118 lines.push(")".to_owned());
119 lines.push("RETURNS TABLE".to_owned());
120 lines.push("AS".to_owned());
121 lines.push("RETURN (".to_owned());
122 lines.push(format!(" {}", body(routine)));
123 lines.push(")".to_owned());
124 lines.join("\n")
125}
126
127fn snowflake(routine: &RoutineSpec) -> String {
129 let mut lines = vec![
130 format!(
131 "CREATE {}FUNCTION {}({})",
132 or_replace(routine),
133 plain_name(routine),
134 inline_parameters(routine)
135 ),
136 format!("RETURNS {}", row_shape(routine)),
137 "LANGUAGE SQL".to_owned(),
138 ];
139 if let Some(comment) = &routine.comment {
140 lines.push(format!("COMMENT = '{}'", quoted_text(comment)));
141 }
142 lines.push("AS $$".to_owned());
143 lines.push(body(routine));
144 lines.push("$$;".to_owned());
145 lines.join("\n")
146}
147
148fn databricks(routine: &RoutineSpec) -> String {
150 let mut lines = vec![
151 format!(
152 "CREATE {}FUNCTION {}({})",
153 or_replace(routine),
154 plain_name(routine),
155 inline_parameters(routine)
156 ),
157 format!("RETURNS {}", row_shape(routine)),
158 ];
159 if let Some(comment) = &routine.comment {
160 lines.push(format!("COMMENT '{}'", quoted_text(comment)));
161 }
162 lines.push(format!("RETURN {}", body(routine)));
163 lines.join("\n")
164}
165
166fn google_sql(routine: &RoutineSpec) -> String {
168 format!(
169 "CREATE {}TABLE FUNCTION {}({}) AS (\n{}\n);",
170 or_replace(routine),
171 quoted_name(routine, '`', '`'),
172 inline_parameters(routine),
173 body(routine)
174 )
175}
176
177fn duckdb(routine: &RoutineSpec) -> String {
180 let parameters =
181 routine.parameters.iter().map(|p| p.name.clone()).collect::<Vec<_>>().join(", ");
182 format!(
183 "CREATE {}MACRO {}({}) AS TABLE\n{}",
184 or_replace(routine),
185 plain_name(routine),
186 parameters,
187 body(routine)
188 )
189}
190
191fn clickhouse(routine: &RoutineSpec) -> String {
195 format!("CREATE {}VIEW {} AS\n{}", or_replace(routine), plain_name(routine), body(routine))
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use crate::routine_spec::{RoutineColumn, RoutineKind, RoutineParameter};
202
203 fn a_function() -> RoutineSpec {
204 let mut spec = RoutineSpec::new(RoutineKind::TableFunction, "customer_at");
205 spec.schema = Some("retail_core".to_owned());
206 spec.or_replace = true;
207 spec.comment = Some("The customers as they stood.".to_owned());
208 spec.parameters = vec![RoutineParameter {
209 name: "p_as_of".to_owned(),
210 data_type: "DATE".to_owned(),
211 declared_type: None,
212 direction: ParameterDirection::In,
213 default: None,
214 }];
215 spec.returns = RoutineReturns::Columns(vec![
216 RoutineColumn {
217 name: "customer_sid".to_owned(),
218 data_type: "BIGINT".to_owned(),
219 declared_type: None,
220 },
221 RoutineColumn {
222 name: "customer_name".to_owned(),
223 data_type: "VARCHAR(200)".to_owned(),
224 declared_type: None,
225 },
226 ]);
227 spec.body =
228 RoutineBody::Query("SELECT customer_sid, customer_name FROM customer".to_owned());
229 spec
230 }
231
232 #[test]
234 fn the_postgres_form_declares_the_row_shape() {
235 assert_eq!(
236 render_table_function_as(TableFunctionForm::PostgreSql, &a_function()),
237 "DROP FUNCTION IF EXISTS retail_core.customer_at(DATE);\nCREATE OR REPLACE FUNCTION retail_core.customer_at(p_as_of DATE)\nRETURNS \
238 TABLE(customer_sid BIGINT, customer_name VARCHAR(200))\nLANGUAGE SQL\nAS \
239 $$\nSELECT customer_sid, customer_name FROM customer\n$$;"
240 );
241 }
242
243 #[test]
250 fn a_replaced_postgres_function_is_dropped_first_by_its_input_types() {
251 let mut spec = a_function();
252 spec.parameters.push(RoutineParameter {
253 name: "p_seen".to_owned(),
254 data_type: "BIGINT".to_owned(),
255 declared_type: None,
256 direction: ParameterDirection::Out,
257 default: None,
258 });
259 let rendered = render_table_function_as(TableFunctionForm::PostgreSql, &spec);
260 assert!(
261 rendered.starts_with(
262 "DROP FUNCTION IF EXISTS retail_core.customer_at(DATE);\nCREATE OR REPLACE \
263 FUNCTION retail_core.customer_at("
264 ),
265 "{rendered}"
266 );
267 spec.or_replace = false;
268 let created = render_table_function_as(TableFunctionForm::PostgreSql, &spec);
269 assert!(created.starts_with("CREATE FUNCTION"), "{created}");
270 let custom = render_table_function_as(TableFunctionForm::Custom("PL/SQL"), &a_function());
271 assert!(!custom.contains("DROP FUNCTION"), "{custom}");
272 }
273
274 #[test]
278 fn rows_of_a_relation_are_setof_and_not_a_column_list() {
279 let mut spec = a_function();
280 spec.returns = RoutineReturns::RowsOf("customer".to_owned());
281 assert!(
282 render_table_function_as(TableFunctionForm::PostgreSql, &spec)
283 .contains("RETURNS SETOF customer\n")
284 );
285 }
286
287 #[test]
290 fn the_t_sql_form_declares_no_columns() {
291 let rendered = render_table_function_as(TableFunctionForm::TSql, &a_function());
292 assert_eq!(
293 rendered,
294 "CREATE OR ALTER FUNCTION [retail_core].[customer_at](\n @p_as_of \
295 DATE\n)\nRETURNS TABLE\nAS\nRETURN (\n SELECT customer_sid, customer_name FROM \
296 customer\n)"
297 );
298 assert!(!rendered.contains("customer_sid BIGINT"), "{rendered}");
299 }
300
301 #[test]
304 fn the_duckdb_macro_has_no_returns_clause() {
305 let rendered = render_table_function_as(TableFunctionForm::DuckDb, &a_function());
306 assert_eq!(
307 rendered,
308 "CREATE OR REPLACE MACRO retail_core.customer_at(p_as_of) AS TABLE\nSELECT \
309 customer_sid, customer_name FROM customer"
310 );
311 assert!(!rendered.contains("RETURNS"), "{rendered}");
312 }
313
314 #[test]
316 fn the_remaining_forms_carry_their_own_clauses() {
317 assert!(
318 render_table_function_as(TableFunctionForm::Snowflake, &a_function())
319 .contains("\nLANGUAGE SQL\nCOMMENT = 'The customers as they stood.'\nAS $$\n")
320 );
321 assert!(
322 render_table_function_as(TableFunctionForm::Databricks, &a_function())
323 .ends_with("RETURN SELECT customer_sid, customer_name FROM customer")
324 );
325 assert!(
326 render_table_function_as(TableFunctionForm::GoogleSql, &a_function())
327 .starts_with("CREATE OR REPLACE TABLE FUNCTION `retail_core`.`customer_at`(")
328 );
329 }
330
331 #[test]
335 fn the_clickhouse_form_is_a_view_and_carries_no_parameter_list() {
336 let rendered = render_table_function_as(TableFunctionForm::ClickHouse, &a_function());
337 assert_eq!(
338 rendered,
339 "CREATE OR REPLACE VIEW retail_core.customer_at AS\nSELECT customer_sid, \
340 customer_name FROM customer"
341 );
342 assert!(!rendered.contains("p_as_of"), "{rendered}");
343 }
344
345 #[test]
348 fn an_unsupported_form_spells_no_statement() {
349 let rendered = render_table_function_as(
350 TableFunctionForm::Unsupported(
351 "a VIEW returns rows here, with the parameter in the \
352 caller's own WHERE clause",
353 ),
354 &a_function(),
355 );
356 assert!(rendered.contains("a VIEW returns rows here"));
357 assert!(!rendered.contains("CREATE"), "{rendered}");
358 }
359}