pub const NULL_SID: i64 = -9_223_372_036_854_775_806_i64;
pub const HELPER_FUNCTION_NAME: &str = "generate_sid";
pub const DOTTED_CAPITAL_I: char = '\u{0130}';
pub const CAPITAL_SIGMA: char = '\u{03A3}';
pub const SMALL_SIGMA: char = '\u{03C3}';
pub const PRE_FOLD_SUBSTITUTIONS: &[(char, char)] =
&[(DOTTED_CAPITAL_I, 'I'), (CAPITAL_SIGMA, SMALL_SIGMA)];
use crate::sid_spelling::{Concat, Lower, 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 normalize(spelling: SidSpelling, expr: &str) -> String {
let trimmed = normalize_trim(spelling, expr);
let Lower { function, replace, pattern_literal_prefix: prefix, match_collation } =
spelling.lower;
let subject = match match_collation {
Some(collation) => format!("{trimmed} COLLATE {collation}"),
None => trimmed,
};
let substituted = PRE_FOLD_SUBSTITUTIONS.iter().fold(subject, |expr, (from, to)| {
format!("{replace}({expr}, {prefix}'{from}', {prefix}'{to}')")
});
format!("{function}({substituted})")
}
#[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})))")
}
}
}
fn nest_pairwise(name: &str, normalized: &[String]) -> String {
let mut parts = normalized.iter();
let Some(first) = parts.next() else { return format!("{name}()") };
parts.fold(first.clone(), |left, next| format!("{name}({name}({left}, '|'), {next})"))
}
#[must_use]
pub fn sid_derivation(text_args: &[String], spelling: SidSpelling, site: HashSite<'_>) -> String {
let normalized: Vec<String> = text_args.iter().map(|arg| normalize(spelling, arg)).collect();
let joined = if normalized.len() == 1 {
normalized[0].clone()
} else {
match spelling.concat {
Concat::Function(name) => format!("{name}({})", normalized.join(", '|', ")),
Concat::FunctionBinary(name) => nest_pairwise(name, &normalized),
Concat::Plus => normalized.join(" + '|' + "),
}
};
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,
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, &qualified, parts)
}
#[must_use]
pub fn sid_expression_through(spelling: SidSpelling, 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, site))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sid_spelling::ANSI_SID_SPELLING;
const HELPER_ROUTED: SidSpelling =
SidSpelling { platform: "test", hash: ("pg_xxhash64(", ")"), ..ANSI_SID_SPELLING };
#[test]
fn an_empty_key_renders_no_guard_rather_than_an_empty_one() {
assert!(!sid_expression(HELPER_ROUTED, None, &[]).contains("CASE WHEN"));
}
#[test]
fn a_spelling_pre_replaces_both_divergent_code_points() {
assert_eq!(
normalize(HELPER_ROUTED, "x"),
"LOWER(REPLACE(REPLACE(TRIM(x), '\u{0130}', 'I'), '\u{03A3}', '\u{03C3}'))"
);
}
#[test]
fn a_collation_sensitive_replacement_pins_its_subject_and_prefixes_its_pattern() {
let pinned = SidSpelling {
lower: Lower {
pattern_literal_prefix: "N",
match_collation: Some("Latin1_General_BIN2"),
..ANSI_SID_SPELLING.lower
},
..HELPER_ROUTED
};
assert_eq!(
normalize(pinned, "x"),
"LOWER(REPLACE(REPLACE(TRIM(x) COLLATE Latin1_General_BIN2, N'\u{0130}', N'I'), \
N'\u{03A3}', N'\u{03C3}'))"
);
}
#[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, Some("acme"), &["x".to_owned()]);
let bare = sid_expression(external, None, &["x".to_owned()]);
assert_eq!(
qualified, bare,
"passing a schema moved the key function's name — the artifact is not in a schema \
Razeri created"
);
assert!(!qualified.contains("acme."), "the emitted call qualifies an installed artifact");
}
#[test]
fn the_helper_site_and_the_native_site_normalize_identically() {
let args = ["a".to_owned(), "b".to_owned()];
let native = sid_derivation(&args, HELPER_ROUTED, HashSite::Native);
let helper = sid_derivation(&args, HELPER_ROUTED, HashSite::Helper(HELPER_FUNCTION_NAME));
let a = "LOWER(REPLACE(REPLACE(TRIM(a), '\u{0130}', 'I'), '\u{03A3}', '\u{03C3}'))";
let b = "LOWER(REPLACE(REPLACE(TRIM(b), '\u{0130}', 'I'), '\u{03A3}', '\u{03C3}'))";
assert_eq!(native, format!("pg_xxhash64(CONCAT({a}, '|', {b}))"));
assert_eq!(helper, format!("generate_sid(CONCAT({a}, '|', {b}))"));
}
#[test]
fn any_null_part_reaches_the_sentinel_and_the_guard_tests_the_input() {
let composite = sid_expression(HELPER_ROUTED, None, &["a".to_owned(), "b".to_owned()]);
assert!(composite.starts_with("CASE WHEN a IS NULL OR b IS NULL THEN "));
assert!(composite.contains(&NULL_SID.to_string()));
assert!(!composite.contains("COALESCE"));
}
#[test]
fn a_binary_concat_nests_one_call_per_pair() {
let spelling = SidSpelling { concat: Concat::FunctionBinary("CONCAT"), ..HELPER_ROUTED };
let part = |arg: &str| normalize(spelling, arg);
let two = ["a".to_owned(), "b".to_owned()];
assert_eq!(
sid_derivation(&two, spelling, HashSite::Native),
format!("pg_xxhash64(CONCAT(CONCAT({}, '|'), {}))", part("a"), part("b"))
);
let three = ["a".to_owned(), "b".to_owned(), "c".to_owned()];
assert_eq!(
sid_derivation(&three, spelling, HashSite::Native),
format!(
"pg_xxhash64(CONCAT(CONCAT(CONCAT(CONCAT({}, '|'), {}), '|'), {}))",
part("a"),
part("b"),
part("c")
)
);
}
#[test]
fn a_binary_concat_leaves_a_one_part_key_unjoined() {
let binary = SidSpelling { concat: Concat::FunctionBinary("CONCAT"), ..HELPER_ROUTED };
let one = ["a".to_owned()];
assert_eq!(
sid_derivation(&one, binary, HashSite::Native),
sid_derivation(&one, HELPER_ROUTED, HashSite::Native)
);
assert!(!sid_derivation(&one, binary, HashSite::Native).contains("CONCAT"));
}
#[test]
fn a_binary_concat_renders_the_bare_call_for_an_empty_key() {
let binary = SidSpelling { concat: Concat::FunctionBinary("CONCAT"), ..HELPER_ROUTED };
assert_eq!(
sid_derivation(&[], binary, HashSite::Native),
sid_derivation(&[], HELPER_ROUTED, HashSite::Native)
);
assert_eq!(sid_derivation(&[], binary, HashSite::Native), "pg_xxhash64(CONCAT())");
}
#[test]
fn the_three_join_forms_concatenate_the_same_items_in_the_same_order() {
let parts = ["a".to_owned(), "b".to_owned(), "c".to_owned()];
let items = |spelling| {
let rendered = sid_derivation(&parts, spelling, HashSite::Native);
let mut found = Vec::new();
for arg in ["a", "b", "c"] {
let normalized = normalize(spelling, arg);
let at = rendered.find(&normalized).expect("every part is joined");
found.push((at, arg));
}
found.sort_unstable();
(
found.into_iter().map(|(_, arg)| arg).collect::<Vec<_>>(),
rendered.matches("'|'").count(),
)
};
let variadic = items(HELPER_ROUTED);
let binary =
items(SidSpelling { concat: Concat::FunctionBinary("CONCAT"), ..HELPER_ROUTED });
let operator = items(SidSpelling { concat: Concat::Plus, ..HELPER_ROUTED });
assert_eq!(variadic, (vec!["a", "b", "c"], 2));
assert_eq!(binary, variadic);
assert_eq!(operator, variadic);
}
#[test]
fn the_sentinel_is_two_above_the_smallest_integer() {
assert_eq!(NULL_SID, i64::MIN + 2);
}
}