pub const NULL_SID: i64 = -9_223_372_036_854_775_806_i64;
pub const HELPER_FUNCTION_NAME: &str = "generate_sid";
use crate::ExpressionRenderer;
use crate::sid_spelling::{Concat, SidSpelling, TextCast, Trim};
#[derive(Debug, Clone, Copy)]
pub enum HashSite<'a> {
Native,
NativeIn(&'a str),
Helper(&'a str),
}
#[must_use]
pub fn normalize_trim(spelling: SidSpelling, expr: &str) -> String {
match spelling.trim {
Trim::Function(name) => format!("{name}({expr})"),
Trim::LtrimRtrim => format!("LTRIM(RTRIM({expr}))"),
Trim::FunctionWithSpaceSet(name) => format!("{name}({expr}, ' ')"),
}
}
#[must_use]
pub fn cast_to_text(spelling: SidSpelling, expr: &str) -> String {
match spelling.cast_type {
TextCast::Direct(ty) => format!("CAST({expr} AS {ty})"),
TextCast::ClickHouseNullTolerant(ty) => {
format!("assumeNotNull(CAST({expr} AS Nullable({ty})))")
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Backslash {
AsWritten,
Doubled,
}
impl Backslash {
#[must_use]
pub fn of(dialect: &dyn ExpressionRenderer) -> Self {
if dialect.render_missing_value("v", &["\\"]).contains("'\\\\'") {
Self::Doubled
} else {
Self::AsWritten
}
}
#[must_use]
pub fn literal(self, prefix: &str, text: &str) -> String {
let quoted = text.replace('\'', "''");
match self {
Self::AsWritten => format!("{prefix}'{quoted}'"),
Self::Doubled => format!("{prefix}'{}'", quoted.replace('\\', "\\\\")),
}
}
}
#[must_use]
pub fn escape_part(part: &str, spelling: SidSpelling, backslash: Backslash) -> String {
let words = spelling.lower;
let literal = |text: &str| backslash.literal(words.pattern_literal_prefix, text);
let subject = match words.match_collation {
Some(collation) => format!("{part} COLLATE {collation}"),
None => part.to_owned(),
};
let doubled = format!("{}({subject}, {}, {})", words.replace, literal("\\"), literal("\\\\"));
format!("{}({doubled}, {}, {})", words.replace, literal("|"), literal("\\|"))
}
fn nest_pairwise(name: &str, parts: &[String]) -> String {
let mut rest = parts.iter();
let Some(first) = rest.next() else { return format!("{name}()") };
rest.fold(first.clone(), |left, next| format!("{name}({name}({left}, '|'), {next})"))
}
#[must_use]
pub fn sid_derivation(
text_args: &[String],
spelling: SidSpelling,
backslash: Backslash,
site: HashSite<'_>,
) -> String {
let joined = if text_args.len() == 1 {
text_args[0].clone()
} else {
let parts: Vec<String> =
text_args.iter().map(|part| escape_part(part, spelling, backslash)).collect();
match spelling.concat {
Concat::Function(name) => format!("{name}({})", parts.join(", '|', ")),
Concat::FunctionBinary(name) => nest_pairwise(name, &parts),
Concat::Plus => parts.join(" + '|' + "),
Concat::Operator(operator) => parts.join(&format!(" {operator} '|' {operator} ")),
}
};
match site {
HashSite::Native => format!("{}{joined}{}", spelling.hash.0, spelling.hash.1),
HashSite::NativeIn(home) => {
format!("{home}.{}{joined}{}", spelling.hash.0, spelling.hash.1)
}
HashSite::Helper(name) => format!("{name}({joined})"),
}
}
#[must_use]
pub fn guard_null_members(raw_args: &[String], derivation: String) -> String {
if raw_args.is_empty() {
return derivation;
}
let test = raw_args.iter().map(|arg| format!("{arg} IS NULL")).collect::<Vec<_>>().join(" OR ");
format!("CASE WHEN {test} THEN {NULL_SID} ELSE {derivation} END")
}
#[must_use]
pub fn sid_expression(
spelling: SidSpelling,
backslash: Backslash,
helper_home: Option<&str>,
parts: &[String],
) -> String {
let qualified = helper_home
.map_or_else(|| HELPER_FUNCTION_NAME.to_owned(), |s| format!("{s}.{HELPER_FUNCTION_NAME}"));
sid_expression_through(spelling, backslash, &qualified, parts)
}
#[must_use]
pub fn sid_expression_through(
spelling: SidSpelling,
backslash: Backslash,
helper: &str,
parts: &[String],
) -> String {
let cast: Vec<String> = parts.iter().map(|arg| cast_to_text(spelling, arg)).collect();
let site = if spelling.through_helper { HashSite::Helper(helper) } else { HashSite::Native };
guard_null_members(parts, sid_derivation(&cast, spelling, backslash, site))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sid_spelling::{ANSI_SID_SPELLING, Lower};
macro_rules! for_test_renderer {
($name:ident) => {
impl crate::SqlRenderer for $name {
fn render_data_type(&self, _: &crate::DataType) -> String {
String::new()
}
fn quote_identifier(&self, name: &str) -> String {
name.to_owned()
}
fn qualify_name(&self, _: Option<&str>, name: &str) -> String {
name.to_owned()
}
fn render_current_timestamp(&self) -> String {
String::new()
}
}
};
}
const HELPER_ROUTED: SidSpelling =
SidSpelling { platform: "test", hash: ("pg_xxhash64(", ")"), ..ANSI_SID_SPELLING };
const STD: Backslash = Backslash::AsWritten;
const A_B: &str = "REPLACE(REPLACE(a, '\\', '\\\\'), '|', '\\|'), '|', \
REPLACE(REPLACE(b, '\\', '\\\\'), '|', '\\|')";
#[test]
fn a_part_is_hashed_as_it_is_cast() {
assert_eq!(
sid_expression(HELPER_ROUTED, STD, None, &["x".to_owned()]),
format!("CASE WHEN x IS NULL THEN {NULL_SID} ELSE generate_sid(CAST(x AS TEXT)) END")
);
let native = SidSpelling { through_helper: false, ..HELPER_ROUTED };
for spelling in [HELPER_ROUTED, native] {
for backslash in [STD, Backslash::Doubled] {
let rendered = sid_expression(spelling, backslash, Some("acme"), &["x".to_owned()]);
for fold in ["TRIM(", "LOWER(", "REPLACE("] {
assert!(!rendered.contains(fold), "{fold} in {rendered}");
}
}
}
}
#[test]
fn a_composite_escapes_each_part_before_the_join() {
let native = SidSpelling { through_helper: false, ..HELPER_ROUTED };
assert_eq!(
sid_derivation(&["a".to_owned(), "b".to_owned()], native, STD, HashSite::Native),
format!("pg_xxhash64(CONCAT({A_B}))")
);
assert_eq!(
escape_part("a", ANSI_SID_SPELLING, Backslash::Doubled),
"REPLACE(REPLACE(a, '\\\\', '\\\\\\\\'), '|', '\\\\|')"
);
let tsql_like = SidSpelling {
lower: Lower {
function: "LOWER",
replace: "REPLACE",
pattern_literal_prefix: "N",
match_collation: Some("Latin1_General_100_BIN2"),
},
..ANSI_SID_SPELLING
};
assert_eq!(
escape_part("a", tsql_like, STD),
"REPLACE(REPLACE(a COLLATE Latin1_General_100_BIN2, N'\\', N'\\\\'), N'|', N'\\|')"
);
let clickhouse_like = SidSpelling {
lower: Lower { replace: "replaceAll", ..ANSI_SID_SPELLING.lower },
..ANSI_SID_SPELLING
};
assert!(
escape_part("a", clickhouse_like, Backslash::Doubled)
.starts_with("replaceAll(replaceAll(a, "),
"the engine's own replacement function"
);
}
#[test]
fn the_backslash_is_read_from_the_dialects_own_literal() {
struct Escaping;
struct Standard;
for_test_renderer!(Escaping);
for_test_renderer!(Standard);
impl ExpressionRenderer for Standard {}
impl ExpressionRenderer for Escaping {
fn render_missing_value(&self, text: &str, words: &[&str]) -> String {
let listed: Vec<String> =
words.iter().map(|w| format!("'{}'", w.replace('\\', "\\\\"))).collect();
format!("CASE WHEN {text} IN ({}) THEN NULL ELSE {text} END", listed.join(", "))
}
}
assert_eq!(Backslash::of(&Escaping), Backslash::Doubled);
assert_eq!(Backslash::of(&Standard), Backslash::AsWritten);
assert_eq!(Backslash::Doubled.literal("", "\\|"), "'\\\\|'");
assert_eq!(Backslash::AsWritten.literal("N", "\\|"), "N'\\|'");
assert_eq!(Backslash::AsWritten.literal("", "O'Hare"), "'O''Hare'");
}
#[test]
fn the_helper_site_and_the_native_site_hash_the_same_text() {
let args = ["a".to_owned(), "b".to_owned()];
assert_eq!(
sid_derivation(&args, HELPER_ROUTED, STD, HashSite::Native),
format!("pg_xxhash64(CONCAT({A_B}))")
);
assert_eq!(
sid_derivation(&args, HELPER_ROUTED, STD, HashSite::Helper(HELPER_FUNCTION_NAME)),
format!("generate_sid(CONCAT({A_B}))")
);
assert_eq!(
sid_derivation(&args, HELPER_ROUTED, STD, HashSite::NativeIn("razeri")),
format!("razeri.pg_xxhash64(CONCAT({A_B}))")
);
}
#[test]
fn the_four_join_forms_join_the_same_items() {
let three = ["a".to_owned(), "b".to_owned(), "c".to_owned()];
let with = |concat| {
sid_derivation(&three, SidSpelling { concat, ..HELPER_ROUTED }, STD, HashSite::Native)
};
let [a, b, c] = three.clone().map(|part| escape_part(&part, HELPER_ROUTED, STD));
assert_eq!(
with(Concat::Function("CONCAT")),
format!("pg_xxhash64(CONCAT({a}, '|', {b}, '|', {c}))")
);
assert_eq!(
with(Concat::FunctionBinary("CONCAT")),
format!("pg_xxhash64(CONCAT(CONCAT(CONCAT(CONCAT({a}, '|'), {b}), '|'), {c}))")
);
assert_eq!(with(Concat::Plus), format!("pg_xxhash64({a} + '|' + {b} + '|' + {c})"));
assert_eq!(
with(Concat::Operator("||")),
format!("pg_xxhash64({a} || '|' || {b} || '|' || {c})")
);
let one = ["a".to_owned()];
assert_eq!(
sid_derivation(
&one,
SidSpelling { concat: Concat::Plus, ..HELPER_ROUTED },
STD,
HashSite::Native
),
"pg_xxhash64(a)",
"a one-part key is neither escaped nor joined"
);
assert_eq!(
sid_derivation(
&[],
SidSpelling { concat: Concat::FunctionBinary("CONCAT"), ..HELPER_ROUTED },
STD,
HashSite::Native
),
"pg_xxhash64(CONCAT())"
);
}
#[test]
fn any_null_part_reaches_the_sentinel() {
let composite = sid_expression(HELPER_ROUTED, STD, None, &["a".to_owned(), "b".to_owned()]);
assert!(composite.starts_with("CASE WHEN a IS NULL OR b IS NULL THEN "), "{composite}");
assert!(!sid_expression(HELPER_ROUTED, STD, None, &[]).contains("CASE WHEN"));
}
#[test]
fn a_spelling_that_inlines_its_artifact_ignores_the_schema_it_is_passed() {
let external =
SidSpelling { hash: ("generate_sid(", ")"), through_helper: false, ..HELPER_ROUTED };
let qualified = sid_expression(external, STD, Some("acme"), &["x".to_owned()]);
let bare = sid_expression(external, STD, None, &["x".to_owned()]);
assert_eq!(qualified, bare);
assert!(!qualified.contains("acme."), "the emitted call qualifies an installed artifact");
}
#[test]
fn an_operator_join_writes_the_operator_between_each_part_and_the_separator() {
let spelling = SidSpelling { concat: Concat::Operator("||"), ..HELPER_ROUTED };
let three = ["a".to_owned(), "b".to_owned(), "c".to_owned()];
let [a, b, c] = three.clone().map(|part| escape_part(&part, spelling, STD));
assert_eq!(
sid_derivation(&three, spelling, STD, HashSite::Native),
format!("pg_xxhash64({a} || '|' || {b} || '|' || {c})")
);
assert_eq!(
sid_derivation(&["a".to_owned()], spelling, STD, HashSite::Native),
"pg_xxhash64(a)"
);
}
#[test]
fn the_sentinel_is_two_above_the_smallest_integer() {
assert_eq!(NULL_SID, i64::MIN + 2);
}
}