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::{RoutineBody, RoutineReturns, RoutineSpec};
13
14#[must_use]
21pub fn render_table_function_as(form: TableFunctionForm, routine: &RoutineSpec) -> String {
22 match form {
25 TableFunctionForm::PostgreSql => 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 postgres(routine: &RoutineSpec) -> String {
75 format!(
76 "CREATE {}FUNCTION {}({})\nRETURNS {}\nLANGUAGE SQL\nAS $$\n{}\n$$;",
77 or_replace(routine),
78 plain_name(routine),
79 inline_parameters(routine),
80 row_shape(routine),
81 body(routine)
82 )
83}
84
85fn t_sql(routine: &RoutineSpec) -> String {
88 let create = if routine.or_replace { "CREATE OR ALTER FUNCTION" } else { "CREATE FUNCTION" };
89 let mut lines = vec![format!("{create} {}(", quoted_name(routine, '[', ']'))];
90 let last = routine.parameters.len().saturating_sub(1);
91 for (index, parameter) in routine.parameters.iter().enumerate() {
92 let comma = if index == last { "" } else { "," };
93 lines.push(format!(" @{} {}{}", parameter.name, parameter.data_type, comma));
94 }
95 lines.push(")".to_owned());
96 lines.push("RETURNS TABLE".to_owned());
97 lines.push("AS".to_owned());
98 lines.push("RETURN (".to_owned());
99 lines.push(format!(" {}", body(routine)));
100 lines.push(")".to_owned());
101 lines.join("\n")
102}
103
104fn snowflake(routine: &RoutineSpec) -> String {
106 let mut lines = vec![
107 format!(
108 "CREATE {}FUNCTION {}({})",
109 or_replace(routine),
110 plain_name(routine),
111 inline_parameters(routine)
112 ),
113 format!("RETURNS {}", row_shape(routine)),
114 "LANGUAGE SQL".to_owned(),
115 ];
116 if let Some(comment) = &routine.comment {
117 lines.push(format!("COMMENT = '{}'", quoted_text(comment)));
118 }
119 lines.push("AS $$".to_owned());
120 lines.push(body(routine));
121 lines.push("$$;".to_owned());
122 lines.join("\n")
123}
124
125fn databricks(routine: &RoutineSpec) -> String {
127 let mut lines = vec![
128 format!(
129 "CREATE {}FUNCTION {}({})",
130 or_replace(routine),
131 plain_name(routine),
132 inline_parameters(routine)
133 ),
134 format!("RETURNS {}", row_shape(routine)),
135 ];
136 if let Some(comment) = &routine.comment {
137 lines.push(format!("COMMENT '{}'", quoted_text(comment)));
138 }
139 lines.push(format!("RETURN {}", body(routine)));
140 lines.join("\n")
141}
142
143fn google_sql(routine: &RoutineSpec) -> String {
145 format!(
146 "CREATE {}TABLE FUNCTION {}({}) AS (\n{}\n);",
147 or_replace(routine),
148 quoted_name(routine, '`', '`'),
149 inline_parameters(routine),
150 body(routine)
151 )
152}
153
154fn duckdb(routine: &RoutineSpec) -> String {
157 let parameters =
158 routine.parameters.iter().map(|p| p.name.clone()).collect::<Vec<_>>().join(", ");
159 format!(
160 "CREATE {}MACRO {}({}) AS TABLE\n{}",
161 or_replace(routine),
162 plain_name(routine),
163 parameters,
164 body(routine)
165 )
166}
167
168fn clickhouse(routine: &RoutineSpec) -> String {
172 format!("CREATE {}VIEW {} AS\n{}", or_replace(routine), plain_name(routine), body(routine))
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::routine_spec::{ParameterDirection, RoutineColumn, RoutineKind, RoutineParameter};
179
180 fn a_function() -> RoutineSpec {
181 let mut spec = RoutineSpec::new(RoutineKind::TableFunction, "customer_at");
182 spec.schema = Some("retail_core".to_owned());
183 spec.or_replace = true;
184 spec.comment = Some("The customers as they stood.".to_owned());
185 spec.parameters = vec![RoutineParameter {
186 name: "p_as_of".to_owned(),
187 data_type: "DATE".to_owned(),
188 declared_type: None,
189 direction: ParameterDirection::In,
190 default: None,
191 }];
192 spec.returns = RoutineReturns::Columns(vec![
193 RoutineColumn {
194 name: "customer_sid".to_owned(),
195 data_type: "BIGINT".to_owned(),
196 declared_type: None,
197 },
198 RoutineColumn {
199 name: "customer_name".to_owned(),
200 data_type: "VARCHAR(200)".to_owned(),
201 declared_type: None,
202 },
203 ]);
204 spec.body =
205 RoutineBody::Query("SELECT customer_sid, customer_name FROM customer".to_owned());
206 spec
207 }
208
209 #[test]
211 fn the_postgres_form_declares_the_row_shape() {
212 assert_eq!(
213 render_table_function_as(TableFunctionForm::PostgreSql, &a_function()),
214 "CREATE OR REPLACE FUNCTION retail_core.customer_at(p_as_of DATE)\nRETURNS \
215 TABLE(customer_sid BIGINT, customer_name VARCHAR(200))\nLANGUAGE SQL\nAS \
216 $$\nSELECT customer_sid, customer_name FROM customer\n$$;"
217 );
218 }
219
220 #[test]
224 fn rows_of_a_relation_are_setof_and_not_a_column_list() {
225 let mut spec = a_function();
226 spec.returns = RoutineReturns::RowsOf("customer".to_owned());
227 assert!(
228 render_table_function_as(TableFunctionForm::PostgreSql, &spec)
229 .contains("RETURNS SETOF customer\n")
230 );
231 }
232
233 #[test]
236 fn the_t_sql_form_declares_no_columns() {
237 let rendered = render_table_function_as(TableFunctionForm::TSql, &a_function());
238 assert_eq!(
239 rendered,
240 "CREATE OR ALTER FUNCTION [retail_core].[customer_at](\n @p_as_of \
241 DATE\n)\nRETURNS TABLE\nAS\nRETURN (\n SELECT customer_sid, customer_name FROM \
242 customer\n)"
243 );
244 assert!(!rendered.contains("customer_sid BIGINT"), "{rendered}");
245 }
246
247 #[test]
250 fn the_duckdb_macro_has_no_returns_clause() {
251 let rendered = render_table_function_as(TableFunctionForm::DuckDb, &a_function());
252 assert_eq!(
253 rendered,
254 "CREATE OR REPLACE MACRO retail_core.customer_at(p_as_of) AS TABLE\nSELECT \
255 customer_sid, customer_name FROM customer"
256 );
257 assert!(!rendered.contains("RETURNS"), "{rendered}");
258 }
259
260 #[test]
262 fn the_remaining_forms_carry_their_own_clauses() {
263 assert!(
264 render_table_function_as(TableFunctionForm::Snowflake, &a_function())
265 .contains("\nLANGUAGE SQL\nCOMMENT = 'The customers as they stood.'\nAS $$\n")
266 );
267 assert!(
268 render_table_function_as(TableFunctionForm::Databricks, &a_function())
269 .ends_with("RETURN SELECT customer_sid, customer_name FROM customer")
270 );
271 assert!(
272 render_table_function_as(TableFunctionForm::GoogleSql, &a_function())
273 .starts_with("CREATE OR REPLACE TABLE FUNCTION `retail_core`.`customer_at`(")
274 );
275 }
276
277 #[test]
281 fn the_clickhouse_form_is_a_view_and_carries_no_parameter_list() {
282 let rendered = render_table_function_as(TableFunctionForm::ClickHouse, &a_function());
283 assert_eq!(
284 rendered,
285 "CREATE OR REPLACE VIEW retail_core.customer_at AS\nSELECT customer_sid, \
286 customer_name FROM customer"
287 );
288 assert!(!rendered.contains("p_as_of"), "{rendered}");
289 }
290
291 #[test]
294 fn an_unsupported_form_spells_no_statement() {
295 let rendered = render_table_function_as(
296 TableFunctionForm::Unsupported(
297 "a VIEW returns rows here, with the parameter in the \
298 caller's own WHERE clause",
299 ),
300 &a_function(),
301 );
302 assert!(rendered.contains("a VIEW returns rows here"));
303 assert!(!rendered.contains("CREATE"), "{rendered}");
304 }
305}