Skip to main content

razeri_plugin_contract/
sid.rs

1// Copyright (C) 2026 Gabriel Shea
2// Licensed under either of the Apache License, Version 2.0 or the MIT
3// license, at your option.
4
5//! **The one derivation of a surrogate key**, composed from the vocabulary a
6//! dialect states.
7//!
8//! [`SidSpelling`] says what one engine calls `LOWER` and `TRIM`, how it joins
9//! strings, how it casts a value to text and how it spells a 64-bit hash. This
10//! module turns those five answers into the expression a warehouse runs:
11//!
12//! ```text
13//! guard( hash( normalize(part₁) ‖ '|' ‖ … ‖ normalize(partₙ) ) )
14//! ```
15//!
16//! It is published so that a dialect author can render their own key
17//! expression and run it. Without it, *"the same key on every engine"* is a
18//! claim the person making it cannot check — and a key that differs by one
19//! character keys a warehouse to numbers nothing else reproduces.
20//!
21//! # What a dialect decides, and what it does not
22//!
23//! The **vocabulary** is the dialect's, and it is the only thing that varies.
24//! The **derivation** is fixed: there is no table of engine names here and no
25//! way to add one, so a third party spells their own engine's key by stating a
26//! [`SidSpelling`] and nothing else.
27//!
28//! Two expressions built from the same five fields differ **only** in those
29//! fields. That is what makes the claim a proof rather than an assumption, and
30//! it is why a dialect states a vocabulary instead of writing an expression.
31//!
32//! # The parts of the derivation
33//!
34//! - **normalize** is `LOWER(TRIM(x))` as this engine spells it, and it is the
35//!   only normalization permitted — the natural value alone, never salted, so
36//!   one natural key reaches one number in every context. The canonical reading
37//!   is the SQL majority's: trim the ASCII space and nothing else, fold case by
38//!   the locale-independent simple mapping, one code point to one;
39//! - the parts are normalized **then** joined, never the other way round. A
40//!   `TRIM` over a concatenation reaches only the outer edges, which would
41//!   leave interior spaces beside the separator inside the hash input;
42//! - **guard** is the [`NULL_SID`] sentinel, and it fires when **any** part is
43//!   NULL: any NULL component keys the whole member to the sentinel;
44//! - **hash** is either this engine's own spelling or a call to the
45//!   [`HELPER_FUNCTION_NAME`] function, whose body is that same spelling. The
46//!   two sites are one derivation — see [`HashSite`].
47//!
48//! # Why the normalization can appear twice
49//!
50//! Where the hash is reached through the helper, the helper normalizes its own
51//! input as well, so the rendered call reads `generate_sid(LOWER(TRIM(x)))` and
52//! the normalization runs twice. It is the same string both times: `TRIM` of an
53//! already-trimmed value and `LOWER` of an already-lowered one are identities,
54//! so no key moves. Spelling it at the call site anyway is what makes every
55//! engine one shape — a composite must be normalized before it is joined, and
56//! the helper only ever sees the joined result.
57
58/// **The sentinel a member with any NULL key part is keyed to**, `(-2^63) + 2`.
59///
60/// One number, on every engine, for one reason: a NULL natural key is an
61/// absence rather than a value, and two absences are not the same member. It
62/// sits two above the minimum so that a warehouse can still reserve the
63/// minimum itself.
64pub const NULL_SID: i64 = -9_223_372_036_854_775_806_i64;
65
66/// **The name of the surrogate-key function — the only one.**
67///
68/// One public name everywhere, whoever defines it. On engines where the hash is
69/// an ordinary SQL expression, Razeri emits a function of this name whose body
70/// is the derivation below. On engines where the hash lives outside SQL — a
71/// compiled library, an assembly, a registered handler — the dialect's
72/// prerequisites install an artifact under this same name and
73/// [`SidSpelling::hash`] names it directly, so nothing emits a second
74/// definition.
75///
76/// A second name is a second contract. There is no alias and no transitional
77/// spelling.
78pub const HELPER_FUNCTION_NAME: &str = "generate_sid";
79
80/// `İ`, LATIN CAPITAL LETTER I WITH DOT ABOVE (U+0130).
81///
82/// The **only** code point whose simple and full Unicode lowercase mappings
83/// differ with no context at all: the full mapping adds a combining dot the
84/// simple one does not. Named so the character cannot be mistaken for an
85/// ordinary `I` at a glance in the one place it is load-bearing.
86///
87/// It is not the whole story. Full case conversion also carries a *conditional*
88/// mapping — [`CAPITAL_SIGMA`] — which is a different fact with a different
89/// proof. Both are in [`PRE_FOLD_SUBSTITUTIONS`], for the same purpose and for
90/// different reasons.
91pub const DOTTED_CAPITAL_I: char = '\u{0130}';
92
93/// `Σ`, GREEK CAPITAL LETTER SIGMA (U+03A3).
94///
95/// Its **simple** lowercase is [`SMALL_SIGMA`] in every position. Its **full**
96/// lowercase is [`SMALL_SIGMA`] too — *except* word-finally, where Unicode's
97/// final-sigma condition makes it `ς` (U+03C2). It is the one
98/// language-independent conditional mapping there is.
99///
100/// **Word-finally, not string-finally**: an engine returns `ς` for a sigma
101/// before a space and before a hyphen alike, so a substitution that handled
102/// only the last character of a value would be a local approximation. Replacing
103/// every `Σ` is exact, because the simple mapping has no context to lose.
104pub const CAPITAL_SIGMA: char = '\u{03A3}';
105
106/// `σ`, GREEK SMALL LETTER SIGMA (U+03C3) — the **simple** lowercase of
107/// [`CAPITAL_SIGMA`] in every position, and therefore what the canonical
108/// normalization produces.
109///
110/// Note the direction: `ς` is left alone. Its own simple lowercase is itself,
111/// so `ς` and `σ` are two code points and two keys. Folding one into the other
112/// would be a third normalization rather than a repair.
113pub const SMALL_SIGMA: char = '\u{03C3}';
114
115/// **What has to be replaced before an engine's `LOWER` runs**, so that the
116/// composition is exactly the canonical simple case mapping.
117///
118/// Applied in order, each substitution wrapping the previous one, by
119/// [`normalize`] on every engine. The list is a property of the canonical fold
120/// rather than of any engine: every engine whose `LOWER` performs full case
121/// conversion needs this identical pair and differs only in what it calls
122/// `LOWER` and `REPLACE`.
123///
124/// Each entry maps a code point to a spelling whose full and simple lowercase
125/// agree, so the engine's own `LOWER` can then run over the whole value
126/// untouched:
127///
128/// - [`DOTTED_CAPITAL_I`] → `'I'`, whose lowercase is a bare `i` everywhere —
129///   the *unconditional* divergence;
130/// - [`CAPITAL_SIGMA`] → [`SMALL_SIGMA`], which `LOWER` then leaves alone
131///   because it is already lowercase — the *conditional* one.
132pub const PRE_FOLD_SUBSTITUTIONS: &[(char, char)] =
133    &[(DOTTED_CAPITAL_I, 'I'), (CAPITAL_SIGMA, SMALL_SIGMA)];
134
135use crate::sid_spelling::{Concat, Lower, SidSpelling, TextCast, Trim};
136
137/// Where the hash itself is spelled.
138#[derive(Debug, Clone, Copy)]
139pub enum HashSite<'a> {
140    /// Inline this engine's own hash spelling, from [`SidSpelling::hash`].
141    Native,
142    /// Inline this engine's own hash spelling, in the schema Razeri installed
143    /// it in — [`SidPrerequisiteHome::Schema`](crate::SidPrerequisiteHome).
144    ///
145    /// The schema, a dot, and then [`SidSpelling::hash`] exactly as [`Native`]
146    /// writes it. So a dialect that names a home owes a `hash` whose opening
147    /// text BEGINS with the function's own name: `razeri_xxh64(` qualifies to
148    /// `razeri.razeri_xxh64(`, while a spelling that opened with a cast —
149    /// `CAST(xxHash64(` — would put the schema in front of the cast and name
150    /// nothing. Every engine that opens with a cast has a server-global
151    /// function and names no home, which is why the two facts have never met.
152    ///
153    /// [`Native`]: Self::Native
154    NativeIn(&'a str),
155    /// Call the [`HELPER_FUNCTION_NAME`] function by this — possibly
156    /// schema-qualified — name. Its body is the [`HashSite::Native`] spelling,
157    /// so the two sites are the same derivation.
158    ///
159    /// **The qualifier is the function's home, not the caller's schema.** One
160    /// function is created per deployment unit, so a call from any other schema
161    /// of that unit has to name the schema that has it. A caller that qualifies
162    /// with its own schema names a function nothing created.
163    Helper(&'a str),
164}
165
166/// **`TRIM(expr)` in this engine's spelling** — the space around a value
167/// taken off, and nothing else.
168///
169/// Public because the canonical normalization is not the only thing that has
170/// to take the space off a value the source wrote: reading an amount the
171/// source spelled its own way starts the same way, and two spellings of
172/// `TRIM` would be two answers to one question.
173///
174/// Parameters:
175/// - `spelling`: this engine's vocabulary;
176/// - `expr`: an already-rendered value expression.
177#[must_use]
178pub fn normalize_trim(spelling: SidSpelling, expr: &str) -> String {
179    match spelling.trim {
180        Trim::Function(name) => format!("{name}({expr})"),
181        Trim::LtrimRtrim => format!("LTRIM(RTRIM({expr}))"),
182        Trim::FunctionWithSpaceSet(name) => format!("{name}({expr}, ' ')"),
183    }
184}
185
186/// **`LOWER(TRIM(expr))` in this engine's spelling** — the canonical
187/// normalization, and the only one permitted: the natural value alone, never
188/// salted.
189///
190/// Public because what an engine's fold and trim return is a separate
191/// measurement from what its hash returns, and a test that can make only the
192/// first one still proves half the claim. A test that re-spelled the
193/// normalization would be testing itself.
194///
195/// Parameters:
196/// - `spelling`: this engine's vocabulary;
197/// - `expr`: an already-rendered value expression.
198#[must_use]
199pub fn normalize(spelling: SidSpelling, expr: &str) -> String {
200    let trimmed = normalize_trim(spelling, expr);
201    let Lower { function, replace, pattern_literal_prefix: prefix, match_collation } =
202        spelling.lower;
203    // The subject is pinned to a binary collation where the engine would
204    // otherwise match a replacement pattern under the value's own collation —
205    // which, under a case-insensitive one, also replaces the final sigma the
206    // canonical mapping leaves alone.
207    let subject = match match_collation {
208        Some(collation) => format!("{trimmed} COLLATE {collation}"),
209        None => trimmed,
210    };
211    let substituted = PRE_FOLD_SUBSTITUTIONS.iter().fold(subject, |expr, (from, to)| {
212        format!("{replace}({expr}, {prefix}'{from}', {prefix}'{to}')")
213    });
214    format!("{function}({substituted})")
215}
216
217/// **`CAST(expr AS text)` in this engine's spelling**, total over NULL.
218///
219/// Public for the reason [`normalize_trim`] is public: turning a value into
220/// text is not something only the key derivation does. A caller that has to
221/// read a value back out of an engine as text — because the transport it
222/// arrives over encodes the engine's own types in a form the same engine will
223/// not accept back — needs this engine's cast and not a second opinion about
224/// it. Two spellings of one cast are two answers to one question.
225///
226/// Parameters:
227/// - `spelling`: this engine's vocabulary;
228/// - `expr`: an already-rendered value expression.
229#[must_use]
230pub fn cast_to_text(spelling: SidSpelling, expr: &str) -> String {
231    match spelling.cast_type {
232        TextCast::Direct(ty) => format!("CAST({expr} AS {ty})"),
233        TextCast::ClickHouseNullTolerant(ty) => {
234            format!("assumeNotNull(CAST({expr} AS Nullable({ty})))")
235        }
236    }
237}
238
239/// **The join, written for an engine whose `CONCAT` takes exactly two
240/// arguments**: the parts and the separators between them, folded left to
241/// right, one call per pair.
242///
243/// The items are the same items the variadic call lists, in the same order, so
244/// the concatenated string is the same string and the key does not move.
245///
246/// An empty key has nothing to nest, and the bare call is what the variadic
247/// form renders for it — this form answers the same way rather than inventing a
248/// second degenerate spelling.
249fn nest_pairwise(name: &str, normalized: &[String]) -> String {
250    let mut parts = normalized.iter();
251    let Some(first) = parts.next() else { return format!("{name}()") };
252    parts.fold(first.clone(), |left, next| format!("{name}({name}({left}, '|'), {next})"))
253}
254
255/// **The derivation**: normalize each part, join on `'|'`, hash the result.
256///
257/// `text_args` are already-rendered value expressions, already cast to text
258/// where the caller needs it — a column reference inside a load statement, a
259/// parameter name inside the key function's own body. Callers name the value
260/// differently, so composing the name here would mean knowing which caller was
261/// asking.
262///
263/// The result carries **no NULL guard**; [`guard_null_members`] wraps this
264/// rather than each engine's arm spelling a guard of its own.
265///
266/// Parameters:
267/// - `text_args`: the key's parts, already cast to text;
268/// - `spelling`: this engine's vocabulary;
269/// - `site`: whether the hash is inlined or reached through the function.
270#[must_use]
271pub fn sid_derivation(text_args: &[String], spelling: SidSpelling, site: HashSite<'_>) -> String {
272    let normalized: Vec<String> = text_args.iter().map(|arg| normalize(spelling, arg)).collect();
273    let joined = if normalized.len() == 1 {
274        normalized[0].clone()
275    } else {
276        match spelling.concat {
277            Concat::Function(name) => format!("{name}({})", normalized.join(", '|', ")),
278            Concat::FunctionBinary(name) => nest_pairwise(name, &normalized),
279            Concat::Plus => normalized.join(" + '|' + "),
280        }
281    };
282    match site {
283        HashSite::Native => format!("{}{joined}{}", spelling.hash.0, spelling.hash.1),
284        HashSite::NativeIn(home) => {
285            format!("{home}.{}{joined}{}", spelling.hash.0, spelling.hash.1)
286        }
287        HashSite::Helper(name) => format!("{name}({joined})"),
288    }
289}
290
291/// **The [`NULL_SID`] sentinel, wrapped around the derivation.**
292///
293/// `raw_args` are the arguments **before** any cast — a cast preserves
294/// NULL-ness, and `CAST(x AS TEXT) IS NULL` only makes the guard harder to
295/// read.
296///
297/// **Any NULL component of a composite keys the whole member to the sentinel.**
298/// That closes a collision as well as an omission: on several engines
299/// `CONCAT('x', '|', NULL)` is `'x|'`, byte-identical to
300/// `CONCAT('x', '|', '')`, so `('boston', NULL)` and `('boston', '')` would be
301/// one member. Testing the components before they are joined is the only place
302/// the distinction still exists.
303///
304/// The guard is a `CASE WHEN` rather than a `COALESCE` because it tests the
305/// **input**. A `COALESCE` over the hash's *output* reaches the sentinel only
306/// where the hash propagates NULL, and on at least one engine the hash of NULL
307/// is a perfectly ordinary number.
308///
309/// An empty key renders no guard rather than an empty one.
310///
311/// Parameters:
312/// - `raw_args`: the key's parts as the caller wrote them, before any cast;
313/// - `derivation`: the expression [`sid_derivation`] produced.
314#[must_use]
315pub fn guard_null_members(raw_args: &[String], derivation: String) -> String {
316    if raw_args.is_empty() {
317        return derivation;
318    }
319    let test = raw_args.iter().map(|arg| format!("{arg} IS NULL")).collect::<Vec<_>>().join(" OR ");
320    format!("CASE WHEN {test} THEN {NULL_SID} ELSE {derivation} END")
321}
322
323/// **The surrogate key as one expression**: cast, derive, guard.
324///
325/// This is the whole composition, and it is what every producer of a key
326/// expression emits — the load that keys a dimension, the body of the
327/// per-entity key function, the formula a design can name. Rendering it for
328/// your own engine and running it is how you check that your
329/// [`SidSpelling`] reaches the number every other engine reaches.
330///
331/// ```
332/// use razeri_plugin_contract::{ANSI_SID_SPELLING, sid};
333///
334/// let key = sid::sid_expression(ANSI_SID_SPELLING, None, &["t.city".to_owned()]);
335/// assert!(key.starts_with("CASE WHEN t.city IS NULL THEN "));
336/// assert!(key.contains("generate_sid("));
337/// ```
338///
339/// # `helper_home` is the key function's schema, never the caller's
340///
341/// The only thing a schema does here is qualify the call to
342/// [`HELPER_FUNCTION_NAME`] on the engines whose
343/// [`SidSpelling::through_helper`] is true. Where the key function is an
344/// installed artifact instead, its own name is rendered and this argument is
345/// ignored entirely — on at least one engine a compiled function lives server
346/// globally and a qualified call to it does not resolve at all.
347///
348/// So the schema this wants is the one the function was **created** in, which
349/// is a property of the deployment unit rather than of whichever entity is
350/// being keyed. `None` renders the call unqualified, which is correct wherever
351/// nothing is qualified: an engine without schemas, or a design that names no
352/// schema.
353///
354/// Parameters:
355/// - `spelling`: this engine's vocabulary;
356/// - `helper_home`: the schema the key function was created in, where the call
357///   needs qualifying;
358/// - `parts`: the key's parts as raw value expressions, in order.
359#[must_use]
360pub fn sid_expression(
361    spelling: SidSpelling,
362    helper_home: Option<&str>,
363    parts: &[String],
364) -> String {
365    let qualified = helper_home
366        .map_or_else(|| HELPER_FUNCTION_NAME.to_owned(), |s| format!("{s}.{HELPER_FUNCTION_NAME}"));
367    sid_expression_through(spelling, &qualified, parts)
368}
369
370/// **The same expression, with the helper's name already spelled by the
371/// target.**
372///
373/// [`sid_expression`] writes the helper's name the way most engines read it —
374/// the schema, a dot, the name, all bare. That is a name on every engine that
375/// leaves an unquoted word alone and a *different* name on one that folds it,
376/// where the function was created under the spelling that engine's own DDL
377/// wrote. A caller holding the dialect spells the name first and hands it in
378/// here, so the call and the `CREATE FUNCTION` say the same word.
379///
380/// Parameters:
381/// - `spelling`: this engine's vocabulary;
382/// - `helper`: the key helper's name, qualified and spelled;
383/// - `parts`: the key's parts as raw value expressions, in order.
384#[must_use]
385pub fn sid_expression_through(spelling: SidSpelling, helper: &str, parts: &[String]) -> String {
386    let cast: Vec<String> = parts.iter().map(|arg| cast_to_text(spelling, arg)).collect();
387    let site = if spelling.through_helper { HashSite::Helper(helper) } else { HashSite::Native };
388    guard_null_members(parts, sid_derivation(&cast, spelling, site))
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394    use crate::sid_spelling::ANSI_SID_SPELLING;
395
396    /// A vocabulary for the composition tests. It is a fixture, not an engine:
397    /// what is under test here is the derivation, and every real engine's
398    /// vocabulary comes off its own dialect.
399    const HELPER_ROUTED: SidSpelling =
400        SidSpelling { platform: "test", hash: ("pg_xxhash64(", ")"), ..ANSI_SID_SPELLING };
401
402    /// An empty key cannot be guarded, and must not render `CASE WHEN  THEN` —
403    /// an empty test is worse than no test.
404    #[test]
405    fn an_empty_key_renders_no_guard_rather_than_an_empty_one() {
406        assert!(!sid_expression(HELPER_ROUTED, None, &[]).contains("CASE WHEN"));
407    }
408
409    /// **Every spelling renders both substitutions, in order.**
410    ///
411    /// Whether an engine's `LOWER` is the simple mapping or the full one is
412    /// decided by the collation its database was created with rather than by
413    /// the engine, so the substitution is unconditional: it costs nothing where
414    /// the lowercase is already simple, and it is the difference between one
415    /// number and two where it is not.
416    #[test]
417    fn a_spelling_pre_replaces_both_divergent_code_points() {
418        assert_eq!(
419            normalize(HELPER_ROUTED, "x"),
420            "LOWER(REPLACE(REPLACE(TRIM(x), '\u{0130}', 'I'), '\u{03A3}', '\u{03C3}'))"
421        );
422    }
423
424    /// **An engine that matches a replacement pattern under the value's own
425    /// collation gets the subject pinned and the pattern prefixed.**
426    #[test]
427    fn a_collation_sensitive_replacement_pins_its_subject_and_prefixes_its_pattern() {
428        let pinned = SidSpelling {
429            lower: Lower {
430                pattern_literal_prefix: "N",
431                match_collation: Some("Latin1_General_BIN2"),
432                ..ANSI_SID_SPELLING.lower
433            },
434            ..HELPER_ROUTED
435        };
436        assert_eq!(
437            normalize(pinned, "x"),
438            "LOWER(REPLACE(REPLACE(TRIM(x) COLLATE Latin1_General_BIN2, N'\u{0130}', N'I'), \
439             N'\u{03A3}', N'\u{03C3}'))"
440        );
441    }
442
443    /// **A spelling that does not route through the key function ignores the
444    /// schema.**
445    ///
446    /// An engine whose key function is an installed artifact must render a
447    /// **bare** call: the artifact is registered where that engine registers
448    /// artifacts, not in a schema Razeri created.
449    #[test]
450    fn a_spelling_that_inlines_its_artifact_ignores_the_schema_it_is_passed() {
451        let external =
452            SidSpelling { hash: ("generate_sid(", ")"), through_helper: false, ..HELPER_ROUTED };
453        let qualified = sid_expression(external, Some("acme"), &["x".to_owned()]);
454        let bare = sid_expression(external, None, &["x".to_owned()]);
455        assert_eq!(
456            qualified, bare,
457            "passing a schema moved the key function's name — the artifact is not in a schema \
458             Razeri created"
459        );
460        assert!(!qualified.contains("acme."), "the emitted call qualifies an installed artifact");
461    }
462
463    /// The call through the key function and the inlined native spelling are
464    /// the same derivation over the same normalized input — the only difference
465    /// is where the hash is written down.
466    #[test]
467    fn the_helper_site_and_the_native_site_normalize_identically() {
468        let args = ["a".to_owned(), "b".to_owned()];
469        let native = sid_derivation(&args, HELPER_ROUTED, HashSite::Native);
470        let helper = sid_derivation(&args, HELPER_ROUTED, HashSite::Helper(HELPER_FUNCTION_NAME));
471        let a = "LOWER(REPLACE(REPLACE(TRIM(a), '\u{0130}', 'I'), '\u{03A3}', '\u{03C3}'))";
472        let b = "LOWER(REPLACE(REPLACE(TRIM(b), '\u{0130}', 'I'), '\u{03A3}', '\u{03C3}'))";
473        assert_eq!(native, format!("pg_xxhash64(CONCAT({a}, '|', {b}))"));
474        assert_eq!(helper, format!("generate_sid(CONCAT({a}, '|', {b}))"));
475    }
476
477    /// **Any NULL part keys the whole member to the sentinel**, and the guard
478    /// tests the parts rather than the hash's result.
479    #[test]
480    fn any_null_part_reaches_the_sentinel_and_the_guard_tests_the_input() {
481        let composite = sid_expression(HELPER_ROUTED, None, &["a".to_owned(), "b".to_owned()]);
482        assert!(composite.starts_with("CASE WHEN a IS NULL OR b IS NULL THEN "));
483        assert!(composite.contains(&NULL_SID.to_string()));
484        assert!(!composite.contains("COALESCE"));
485    }
486
487    /// **An engine whose `CONCAT` takes exactly two arguments gets one call per
488    /// pair**, nested left to right, over the same items the variadic call
489    /// lists.
490    #[test]
491    fn a_binary_concat_nests_one_call_per_pair() {
492        let spelling = SidSpelling { concat: Concat::FunctionBinary("CONCAT"), ..HELPER_ROUTED };
493        let part = |arg: &str| normalize(spelling, arg);
494        let two = ["a".to_owned(), "b".to_owned()];
495        assert_eq!(
496            sid_derivation(&two, spelling, HashSite::Native),
497            format!("pg_xxhash64(CONCAT(CONCAT({}, '|'), {}))", part("a"), part("b"))
498        );
499        let three = ["a".to_owned(), "b".to_owned(), "c".to_owned()];
500        assert_eq!(
501            sid_derivation(&three, spelling, HashSite::Native),
502            format!(
503                "pg_xxhash64(CONCAT(CONCAT(CONCAT(CONCAT({}, '|'), {}), '|'), {}))",
504                part("a"),
505                part("b"),
506                part("c")
507            )
508        );
509    }
510
511    /// **A one-part key is not joined at all**, on this form as on the other
512    /// two: there is nothing to join, and a one-argument `CONCAT` is a call an
513    /// engine with a two-argument one does not have either.
514    #[test]
515    fn a_binary_concat_leaves_a_one_part_key_unjoined() {
516        let binary = SidSpelling { concat: Concat::FunctionBinary("CONCAT"), ..HELPER_ROUTED };
517        let one = ["a".to_owned()];
518        assert_eq!(
519            sid_derivation(&one, binary, HashSite::Native),
520            sid_derivation(&one, HELPER_ROUTED, HashSite::Native)
521        );
522        assert!(!sid_derivation(&one, binary, HashSite::Native).contains("CONCAT"));
523    }
524
525    /// An empty key renders the bare call — the same degenerate spelling the
526    /// variadic form renders, so the two forms stay comparable where there is
527    /// nothing to join.
528    #[test]
529    fn a_binary_concat_renders_the_bare_call_for_an_empty_key() {
530        let binary = SidSpelling { concat: Concat::FunctionBinary("CONCAT"), ..HELPER_ROUTED };
531        assert_eq!(
532            sid_derivation(&[], binary, HashSite::Native),
533            sid_derivation(&[], HELPER_ROUTED, HashSite::Native)
534        );
535        assert_eq!(sid_derivation(&[], binary, HashSite::Native), "pg_xxhash64(CONCAT())");
536    }
537
538    /// **The three join forms are three spellings of one string.** Read the
539    /// items out of each rendering and they are the same items in the same
540    /// order — which is why a dialect may answer whichever one its engine
541    /// parses without moving a key.
542    #[test]
543    fn the_three_join_forms_concatenate_the_same_items_in_the_same_order() {
544        let parts = ["a".to_owned(), "b".to_owned(), "c".to_owned()];
545        let items = |spelling| {
546            let rendered = sid_derivation(&parts, spelling, HashSite::Native);
547            let mut found = Vec::new();
548            for arg in ["a", "b", "c"] {
549                let normalized = normalize(spelling, arg);
550                let at = rendered.find(&normalized).expect("every part is joined");
551                found.push((at, arg));
552            }
553            found.sort_unstable();
554            (
555                found.into_iter().map(|(_, arg)| arg).collect::<Vec<_>>(),
556                rendered.matches("'|'").count(),
557            )
558        };
559        let variadic = items(HELPER_ROUTED);
560        let binary =
561            items(SidSpelling { concat: Concat::FunctionBinary("CONCAT"), ..HELPER_ROUTED });
562        let operator = items(SidSpelling { concat: Concat::Plus, ..HELPER_ROUTED });
563        assert_eq!(variadic, (vec!["a", "b", "c"], 2));
564        assert_eq!(binary, variadic);
565        assert_eq!(operator, variadic);
566    }
567
568    /// The sentinel is two above the smallest 64-bit integer, and it is spelled
569    /// once.
570    #[test]
571    fn the_sentinel_is_two_above_the_smallest_integer() {
572        assert_eq!(NULL_SID, i64::MIN + 2);
573    }
574}