use crate::DataTypeRef;
use crate::SqlRenderer;
pub trait FormulaRenderer: SqlRenderer {
fn render_string_agg(
&self,
expr: &str,
sep: &str,
distinct: bool,
order: Option<&str>,
) -> String {
let d = if distinct { "DISTINCT " } else { "" };
match order {
Some(o) => format!("STRING_AGG({d}{expr}, '{sep}' ORDER BY {o})"),
None => format!("STRING_AGG({d}{expr}, '{sep}')"),
}
}
fn render_concat_ws(&self, separator: &str, parts: &[&str]) -> String {
format!("CONCAT_WS('{separator}', {})", parts.join(", "))
}
fn render_first_value(&self, expr: &str, ignore_nulls: bool) -> String {
if ignore_nulls {
format!("FIRST_VALUE({expr}) IGNORE NULLS")
} else {
format!("FIRST_VALUE({expr})")
}
}
fn render_stddev(&self, expr: &str, population: bool) -> String {
let func = if population { "STDDEV_POP" } else { "STDDEV_SAMP" };
format!("{func}({expr})")
}
fn render_variance(&self, expr: &str, population: bool) -> String {
let func = if population { "VAR_POP" } else { "VAR_SAMP" };
format!("{func}({expr})")
}
fn render_percentile(&self, expr: &str, percentile: f64, continuous: bool) -> String {
let func = if continuous { "PERCENTILE_CONT" } else { "PERCENTILE_DISC" };
format!("{func}({percentile}) WITHIN GROUP (ORDER BY {expr})")
}
fn render_median(&self, expr: &str) -> String {
format!("PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY {expr})")
}
fn render_count_if(&self, condition: &str) -> String {
format!("COUNT(CASE WHEN {condition} THEN 1 END)")
}
fn render_sum_if(&self, expr: &str, condition: &str) -> String {
format!("SUM(CASE WHEN {condition} THEN {expr} END)")
}
fn render_avg_if(&self, expr: &str, condition: &str) -> String {
format!("AVG(CASE WHEN {condition} THEN {expr} END)")
}
fn render_split(&self, expr: &str, delimiter: &str) -> String {
format!("STRING_TO_ARRAY({expr}, '{delimiter}')")
}
fn render_regexp_like(&self, expr: &str, pattern: &str) -> String {
format!("REGEXP_LIKE({expr}, '{pattern}')")
}
fn render_regexp_replace(&self, expr: &str, pattern: &str, replacement: &str) -> String {
format!("REGEXP_REPLACE({expr}, '{pattern}', '{replacement}')")
}
fn render_regexp_extract(&self, expr: &str, pattern: &str, group: Option<usize>) -> String {
match group {
Some(g) => format!("REGEXP_SUBSTR({expr}, '{pattern}', 1, 1, 'e', {g})"),
None => format!("REGEXP_SUBSTR({expr}, '{pattern}')"),
}
}
fn render_nvl2(&self, expr: &str, not_null: &str, is_null: &str) -> String {
format!("CASE WHEN {expr} IS NOT NULL THEN {not_null} ELSE {is_null} END")
}
fn render_multi_if(&self, conditions: &[(&str, &str)], else_: &str) -> String {
use std::fmt::Write;
let mut sql = "CASE".to_string();
for (cond, then_expr) in conditions {
let _ = write!(sql, " WHEN {cond} THEN {then_expr}");
}
let _ = write!(sql, " ELSE {else_} END");
sql
}
fn render_power_operator(&self) -> Option<String> {
Some("^".to_string())
}
fn render_ilike_operator(&self) -> Option<String> {
Some("ILIKE".to_string())
}
fn render_not_ilike_operator(&self) -> Option<String> {
Some("NOT ILIKE".to_string())
}
fn render_concat_operator(&self) -> Option<String> {
Some("||".to_string())
}
#[deprecated(
since = "0.14.0",
note = "answer `render_bitwise_operator(BitwiseOperator::Xor)` instead, which can say \
the engine has none"
)]
fn render_bitwise_xor_operator(&self) -> String {
"^".to_string()
}
fn render_bitwise_operator(&self, operator: BitwiseOperator) -> Option<String> {
Some(match operator {
BitwiseOperator::And => "&".to_owned(),
BitwiseOperator::Or => "|".to_owned(),
#[allow(deprecated)]
BitwiseOperator::Xor => self.render_bitwise_xor_operator(),
BitwiseOperator::Not => "~".to_owned(),
BitwiseOperator::ShiftLeft => "<<".to_owned(),
BitwiseOperator::ShiftRight => ">>".to_owned(),
})
}
fn render_try_cast_str(&self, expr: &str, type_str: &str) -> String {
format!("CAST({expr} AS {type_str})")
}
fn render_convert(&self, expr: &str, type_str: &str, style: Option<i32>) -> String {
let _ = style;
format!("CAST({expr} AS {type_str})")
}
fn render_safe_cast(&self, expr: &str, type_str: &str) -> String {
format!("CAST({expr} AS {type_str})")
}
fn render_if_null(&self, expr: &str, replacement: &str) -> String {
format!("COALESCE({expr}, {replacement})")
}
fn render_named_param(&self, name: &str) -> String {
format!(":{name}")
}
fn render_positional_param(&self, pos: usize) -> String {
let _ = pos;
"?".to_string()
}
fn render_data_type_ref(&self, dt: &DataTypeRef) -> String {
match dt {
DataTypeRef::Int => "INT".to_string(),
DataTypeRef::BigInt => "BIGINT".to_string(),
DataTypeRef::SmallInt => "SMALLINT".to_string(),
DataTypeRef::TinyInt => "TINYINT".to_string(),
DataTypeRef::Float => "FLOAT".to_string(),
DataTypeRef::Double => "DOUBLE PRECISION".to_string(),
DataTypeRef::Real => "REAL".to_string(),
DataTypeRef::Decimal { precision, scale } => format!("DECIMAL({precision}, {scale})"),
DataTypeRef::Numeric { precision, scale } => format!("NUMERIC({precision}, {scale})"),
DataTypeRef::Varchar { length } => match length {
Some(len) => format!("VARCHAR({len})"),
None => "VARCHAR".to_string(),
},
DataTypeRef::Char { length } => format!("CHAR({length})"),
DataTypeRef::Text | DataTypeRef::Xml => "TEXT".to_string(),
DataTypeRef::NVarchar { length } => match length {
Some(len) => format!("NVARCHAR({len})"),
None => "NVARCHAR(MAX)".to_string(),
},
DataTypeRef::NChar { length } => format!("NCHAR({length})"),
DataTypeRef::Binary { length } => match length {
Some(len) => format!("BINARY({len})"),
None => "BINARY".to_string(),
},
DataTypeRef::VarBinary { length } => match length {
Some(len) => format!("VARBINARY({len})"),
None => "VARBINARY".to_string(),
},
DataTypeRef::Blob => "BLOB".to_string(),
DataTypeRef::Date => "DATE".to_string(),
DataTypeRef::Time { precision } => match precision {
Some(p) => format!("TIME({p})"),
None => "TIME".to_string(),
},
DataTypeRef::DateTime => "DATETIME".to_string(),
DataTypeRef::DateTime2 { precision } => match precision {
Some(p) => format!("DATETIME2({p})"),
None => "DATETIME2".to_string(),
},
DataTypeRef::Timestamp { precision } => match precision {
Some(p) => format!("TIMESTAMP({p})"),
None => "TIMESTAMP".to_string(),
},
DataTypeRef::TimestampTz { precision } => match precision {
Some(p) => format!("TIMESTAMP({p}) WITH TIME ZONE"),
None => "TIMESTAMP WITH TIME ZONE".to_string(),
},
DataTypeRef::Interval => "INTERVAL".to_string(),
DataTypeRef::Boolean => "BOOLEAN".to_string(),
DataTypeRef::Bit => "BIT".to_string(),
DataTypeRef::Json => "VARIANT".to_string(),
DataTypeRef::Jsonb | DataTypeRef::Variant => "JSON".to_string(),
DataTypeRef::Uuid => "UUID".to_string(),
DataTypeRef::Custom(name) => name.clone(),
}
}
}
#[must_use]
pub fn render_data_type_ref_ansi(dt: &DataTypeRef) -> String {
match dt {
DataTypeRef::Int => "INT".to_string(),
DataTypeRef::BigInt => "BIGINT".to_string(),
DataTypeRef::SmallInt => "SMALLINT".to_string(),
DataTypeRef::TinyInt => "TINYINT".to_string(),
DataTypeRef::Float => "FLOAT".to_string(),
DataTypeRef::Double => "DOUBLE PRECISION".to_string(),
DataTypeRef::Real => "REAL".to_string(),
DataTypeRef::Decimal { precision, scale } => format!("DECIMAL({precision}, {scale})"),
DataTypeRef::Numeric { precision, scale } => format!("NUMERIC({precision}, {scale})"),
DataTypeRef::Varchar { length } => match length {
Some(len) => format!("VARCHAR({len})"),
None => "VARCHAR".to_string(),
},
DataTypeRef::Char { length } => format!("CHAR({length})"),
DataTypeRef::Text | DataTypeRef::Xml => "TEXT".to_string(),
DataTypeRef::NVarchar { length } => match length {
Some(len) => format!("NVARCHAR({len})"),
None => "NVARCHAR(MAX)".to_string(),
},
DataTypeRef::NChar { length } => format!("NCHAR({length})"),
DataTypeRef::Binary { length } => match length {
Some(len) => format!("BINARY({len})"),
None => "BINARY".to_string(),
},
DataTypeRef::VarBinary { length } => match length {
Some(len) => format!("VARBINARY({len})"),
None => "VARBINARY".to_string(),
},
DataTypeRef::Blob => "BLOB".to_string(),
DataTypeRef::Date => "DATE".to_string(),
DataTypeRef::Time { precision } => match precision {
Some(p) => format!("TIME({p})"),
None => "TIME".to_string(),
},
DataTypeRef::DateTime => "DATETIME".to_string(),
DataTypeRef::DateTime2 { precision } => match precision {
Some(p) => format!("DATETIME2({p})"),
None => "DATETIME2".to_string(),
},
DataTypeRef::Timestamp { precision } => match precision {
Some(p) => format!("TIMESTAMP({p})"),
None => "TIMESTAMP".to_string(),
},
DataTypeRef::TimestampTz { precision } => match precision {
Some(p) => format!("TIMESTAMP({p}) WITH TIME ZONE"),
None => "TIMESTAMP WITH TIME ZONE".to_string(),
},
DataTypeRef::Interval => "INTERVAL".to_string(),
DataTypeRef::Boolean => "BOOLEAN".to_string(),
DataTypeRef::Bit => "BIT".to_string(),
DataTypeRef::Json => "VARIANT".to_string(),
DataTypeRef::Jsonb | DataTypeRef::Variant => "JSON".to_string(),
DataTypeRef::Uuid => "UUID".to_string(),
DataTypeRef::Custom(name) => name.clone(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BitwiseOperator {
And,
Or,
Xor,
Not,
ShiftLeft,
ShiftRight,
}
impl BitwiseOperator {
pub const ALL: [Self; 6] =
[Self::And, Self::Or, Self::Xor, Self::Not, Self::ShiftLeft, Self::ShiftRight];
#[must_use]
pub const fn standard_spelling(self) -> &'static str {
match self {
Self::And => "&",
Self::Or => "|",
Self::Xor => "^",
Self::Not => "~",
Self::ShiftLeft => "<<",
Self::ShiftRight => ">>",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DataType;
struct TestDialect;
impl SqlRenderer for TestDialect {
fn render_data_type(&self, _dt: &DataType) -> String {
"TEXT".to_string()
}
fn quote_identifier(&self, name: &str) -> String {
format!("\"{name}\"")
}
fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
match schema {
Some(s) => format!("\"{s}\".\"{name}\""),
None => format!("\"{name}\""),
}
}
fn render_current_timestamp(&self) -> String {
"CURRENT_TIMESTAMP".to_string()
}
}
impl FormulaRenderer for TestDialect {}
#[test]
fn formula_renderer_defaults_render_string_agg_with_order() {
let d = TestDialect;
assert_eq!(
d.render_string_agg("name", ",", false, Some("name ASC")),
"STRING_AGG(name, ',' ORDER BY name ASC)"
);
}
#[test]
fn formula_renderer_defaults_render_string_agg_with_distinct() {
let d = TestDialect;
assert_eq!(d.render_string_agg("name", ",", true, None), "STRING_AGG(DISTINCT name, ',')");
}
#[test]
fn formula_renderer_defaults_render_count_if() {
let d = TestDialect;
assert_eq!(d.render_count_if("x > 0"), "COUNT(CASE WHEN x > 0 THEN 1 END)");
}
#[test]
fn a_dialect_that_states_no_spelling_renders_the_guess_and_is_named_as_one() {
use crate::sid_spelling::UNKNOWN_PLATFORM;
let d = TestDialect;
let spelling = crate::SqlRenderer::sid_spelling(&d);
assert_eq!(spelling.platform, UNKNOWN_PLATFORM);
assert_eq!(spelling.hash, ("xxhash64(", ")"));
}
}