use super::{
inline_parameters, note_above, or_replace, plain_name, quoted_name, quoted_text,
refusal_comment,
};
use crate::routine_form::TableFunctionForm;
use crate::routine_spec::{RoutineBody, RoutineReturns, RoutineSpec};
#[must_use]
pub fn render_table_function_as(form: TableFunctionForm, routine: &RoutineSpec) -> String {
match form {
TableFunctionForm::PostgreSql => postgres(routine),
TableFunctionForm::TSql => t_sql(routine),
TableFunctionForm::Snowflake => snowflake(routine),
TableFunctionForm::Databricks => databricks(routine),
TableFunctionForm::GoogleSql => google_sql(routine),
TableFunctionForm::DuckDb => duckdb(routine),
TableFunctionForm::ClickHouse => clickhouse(routine),
TableFunctionForm::Custom(grammar) => note_above(
&format!(
"You asked for {grammar}, which is your dialect's own grammar and not one spelled \
here. The PostgreSQL form below is the nearest neutral starting point; the named \
forms are reachable by naming one."
),
&postgres(routine),
),
TableFunctionForm::Unsupported(orientation) => {
refusal_comment("table function", &routine.name, orientation)
}
}
}
fn row_shape(routine: &RoutineSpec) -> String {
match &routine.returns {
RoutineReturns::Columns(columns) => format!(
"TABLE({})",
columns
.iter()
.map(|c| format!("{} {}", c.name, c.data_type))
.collect::<Vec<_>>()
.join(", ")
),
RoutineReturns::RowsOf(relation) => format!("SETOF {relation}"),
RoutineReturns::Value(data_type) => format!("SETOF {data_type}"),
RoutineReturns::Inferred | RoutineReturns::Nothing => "TABLE".to_owned(),
}
}
fn body(routine: &RoutineSpec) -> String {
match &routine.body {
RoutineBody::Query(query) | RoutineBody::Block(query) => query.clone(),
RoutineBody::Expression(expression) => expression.clone(),
RoutineBody::Statements(statements) => statements.join(";\n"),
}
}
fn postgres(routine: &RoutineSpec) -> String {
format!(
"CREATE {}FUNCTION {}({})\nRETURNS {}\nLANGUAGE SQL\nAS $$\n{}\n$$;",
or_replace(routine),
plain_name(routine),
inline_parameters(routine),
row_shape(routine),
body(routine)
)
}
fn t_sql(routine: &RoutineSpec) -> String {
let create = if routine.or_replace { "CREATE OR ALTER FUNCTION" } else { "CREATE FUNCTION" };
let mut lines = vec![format!("{create} {}(", quoted_name(routine, '[', ']'))];
let last = routine.parameters.len().saturating_sub(1);
for (index, parameter) in routine.parameters.iter().enumerate() {
let comma = if index == last { "" } else { "," };
lines.push(format!(" @{} {}{}", parameter.name, parameter.data_type, comma));
}
lines.push(")".to_owned());
lines.push("RETURNS TABLE".to_owned());
lines.push("AS".to_owned());
lines.push("RETURN (".to_owned());
lines.push(format!(" {}", body(routine)));
lines.push(")".to_owned());
lines.join("\n")
}
fn snowflake(routine: &RoutineSpec) -> String {
let mut lines = vec![
format!(
"CREATE {}FUNCTION {}({})",
or_replace(routine),
plain_name(routine),
inline_parameters(routine)
),
format!("RETURNS {}", row_shape(routine)),
"LANGUAGE SQL".to_owned(),
];
if let Some(comment) = &routine.comment {
lines.push(format!("COMMENT = '{}'", quoted_text(comment)));
}
lines.push("AS $$".to_owned());
lines.push(body(routine));
lines.push("$$;".to_owned());
lines.join("\n")
}
fn databricks(routine: &RoutineSpec) -> String {
let mut lines = vec![
format!(
"CREATE {}FUNCTION {}({})",
or_replace(routine),
plain_name(routine),
inline_parameters(routine)
),
format!("RETURNS {}", row_shape(routine)),
];
if let Some(comment) = &routine.comment {
lines.push(format!("COMMENT '{}'", quoted_text(comment)));
}
lines.push(format!("RETURN {}", body(routine)));
lines.join("\n")
}
fn google_sql(routine: &RoutineSpec) -> String {
format!(
"CREATE {}TABLE FUNCTION {}({}) AS (\n{}\n);",
or_replace(routine),
quoted_name(routine, '`', '`'),
inline_parameters(routine),
body(routine)
)
}
fn duckdb(routine: &RoutineSpec) -> String {
let parameters =
routine.parameters.iter().map(|p| p.name.clone()).collect::<Vec<_>>().join(", ");
format!(
"CREATE {}MACRO {}({}) AS TABLE\n{}",
or_replace(routine),
plain_name(routine),
parameters,
body(routine)
)
}
fn clickhouse(routine: &RoutineSpec) -> String {
format!("CREATE {}VIEW {} AS\n{}", or_replace(routine), plain_name(routine), body(routine))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::routine_spec::{ParameterDirection, RoutineColumn, RoutineKind, RoutineParameter};
fn a_function() -> RoutineSpec {
let mut spec = RoutineSpec::new(RoutineKind::TableFunction, "customer_at");
spec.schema = Some("retail_core".to_owned());
spec.or_replace = true;
spec.comment = Some("The customers as they stood.".to_owned());
spec.parameters = vec![RoutineParameter {
name: "p_as_of".to_owned(),
data_type: "DATE".to_owned(),
declared_type: None,
direction: ParameterDirection::In,
default: None,
}];
spec.returns = RoutineReturns::Columns(vec![
RoutineColumn {
name: "customer_sid".to_owned(),
data_type: "BIGINT".to_owned(),
declared_type: None,
},
RoutineColumn {
name: "customer_name".to_owned(),
data_type: "VARCHAR(200)".to_owned(),
declared_type: None,
},
]);
spec.body =
RoutineBody::Query("SELECT customer_sid, customer_name FROM customer".to_owned());
spec
}
#[test]
fn the_postgres_form_declares_the_row_shape() {
assert_eq!(
render_table_function_as(TableFunctionForm::PostgreSql, &a_function()),
"CREATE OR REPLACE FUNCTION retail_core.customer_at(p_as_of DATE)\nRETURNS \
TABLE(customer_sid BIGINT, customer_name VARCHAR(200))\nLANGUAGE SQL\nAS \
$$\nSELECT customer_sid, customer_name FROM customer\n$$;"
);
}
#[test]
fn rows_of_a_relation_are_setof_and_not_a_column_list() {
let mut spec = a_function();
spec.returns = RoutineReturns::RowsOf("customer".to_owned());
assert!(
render_table_function_as(TableFunctionForm::PostgreSql, &spec)
.contains("RETURNS SETOF customer\n")
);
}
#[test]
fn the_t_sql_form_declares_no_columns() {
let rendered = render_table_function_as(TableFunctionForm::TSql, &a_function());
assert_eq!(
rendered,
"CREATE OR ALTER FUNCTION [retail_core].[customer_at](\n @p_as_of \
DATE\n)\nRETURNS TABLE\nAS\nRETURN (\n SELECT customer_sid, customer_name FROM \
customer\n)"
);
assert!(!rendered.contains("customer_sid BIGINT"), "{rendered}");
}
#[test]
fn the_duckdb_macro_has_no_returns_clause() {
let rendered = render_table_function_as(TableFunctionForm::DuckDb, &a_function());
assert_eq!(
rendered,
"CREATE OR REPLACE MACRO retail_core.customer_at(p_as_of) AS TABLE\nSELECT \
customer_sid, customer_name FROM customer"
);
assert!(!rendered.contains("RETURNS"), "{rendered}");
}
#[test]
fn the_remaining_forms_carry_their_own_clauses() {
assert!(
render_table_function_as(TableFunctionForm::Snowflake, &a_function())
.contains("\nLANGUAGE SQL\nCOMMENT = 'The customers as they stood.'\nAS $$\n")
);
assert!(
render_table_function_as(TableFunctionForm::Databricks, &a_function())
.ends_with("RETURN SELECT customer_sid, customer_name FROM customer")
);
assert!(
render_table_function_as(TableFunctionForm::GoogleSql, &a_function())
.starts_with("CREATE OR REPLACE TABLE FUNCTION `retail_core`.`customer_at`(")
);
}
#[test]
fn the_clickhouse_form_is_a_view_and_carries_no_parameter_list() {
let rendered = render_table_function_as(TableFunctionForm::ClickHouse, &a_function());
assert_eq!(
rendered,
"CREATE OR REPLACE VIEW retail_core.customer_at AS\nSELECT customer_sid, \
customer_name FROM customer"
);
assert!(!rendered.contains("p_as_of"), "{rendered}");
}
#[test]
fn an_unsupported_form_spells_no_statement() {
let rendered = render_table_function_as(
TableFunctionForm::Unsupported(
"a VIEW returns rows here, with the parameter in the \
caller's own WHERE clause",
),
&a_function(),
);
assert!(rendered.contains("a VIEW returns rows here"));
assert!(!rendered.contains("CREATE"), "{rendered}");
}
}