razeri-plugin-contract 0.3.0

The dialect contract Razeri plugins are written against — rendering traits, data types, platform identity
Documentation
// Copyright (C) 2026 Gabriel Shea
// Licensed under either of the Apache License, Version 2.0 or the MIT
// license, at your option.

//! **How this engine performs an upsert** — a shape the dialect describes, not
//! a switch it flips.
//!
//! Razeri's generated load is an upsert: match a row by its key, update it if
//! it is there, insert it if it is not. Engines spell that four ways, and the
//! question *"do you support MERGE?"* has no honest answer on most of them:
//!
//! - some have `MERGE`, and place restrictions on it;
//! - some have no `MERGE` and their own upsert statement instead;
//! - some have no upsert **statement** at all and an upserting TABLE ENGINE
//!   instead. `ClickHouse` is that, measured: no `MERGE`, no
//!   `ON DUPLICATE KEY`, no `ON CONFLICT` — every statement-level spelling
//!   refused on the live engine — and a `ReplacingMergeTree` that collapses
//!   rows sharing the sorting key in background merges. Razeri composes the
//!   load in that idiom: plain versioned `INSERT`s, deduplicated by the
//!   engine, read back through the engine's own read-time guarantee;
//! - some have none of the above, and the honest answer is a refusal.
//!
//! A boolean cannot say this. An engine that merely lacks `MERGE` is not
//! thereby a `MySQL`-style upsert engine, and defaulting it to another
//! vendor's grammar emits SQL it does not have — which is why the answer is a
//! shape, not a flag.
//!
//! ## Two refusals, and why they are different values
//!
//! [`MergeShape::Unsupported`] is a dialect saying *"I measured, and my engine
//! has neither — here is what it has instead"*. [`MergeShape::None`] is the
//! default: nobody has said anything. Both refuse, and the reader is owed a
//! different sentence by each, which is why they are not one variant.

/// How this engine performs the upsert Razeri's load is.
///
/// The default is [`MergeShape::None`]: an engine Razeri has not been told
/// about gets a refusal rather than another vendor's statement.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum MergeShape {
    /// `MERGE INTO … USING … WHEN MATCHED … WHEN NOT MATCHED …`, with whatever
    /// this engine's `MERGE` will not do.
    Merge(MergeRestrictions),
    /// No `MERGE`; this engine has an upsert statement of its own.
    Upsert(UpsertForm),
    /// No upsert **statement** at all — the TABLE ENGINE deduplicates, and the
    /// composed load leans into that instead of borrowing a grammar.
    ///
    /// The load is a plain `INSERT … SELECT`; rows that share the target's
    /// sorting key collapse in the engine's background merges, the greatest
    /// value of [`InsertDedup::version_column`] deciding the survivor; and a
    /// read that needs the collapsed answer *now* says
    /// [`InsertDedup::final_read_modifier`]. Razeri's deterministic surrogate
    /// key is what makes this sound: re-inserting a member computes the same
    /// key every time, so there is no lookup and no identity race — the two
    /// halves an upsert statement exists to coordinate.
    ///
    /// This composes a load ([`MergeShape::composes_a_load`] is `true`), and it
    /// is still not a `MERGE`: [`MergeShape::merges`] stays `false`, so every
    /// statement family that needs a real `MERGE` — the single-`WHEN` identity
    /// sync included — takes its join-based spelling here, never a correlated
    /// one. The engine that carries this shape refuses correlated subqueries
    /// outright, which is measured, not assumed.
    InsertDedup(InsertDedup),
    /// **Neither, measured — and this engine's own sentence about it.**
    ///
    /// Razeri composes no load here, and the string is what the reader gets
    /// instead: it lands in the generated `loads.sql` where the load would have
    /// been, so it is the only thing an operator has to act on. Write it
    /// fix-first — what this engine offers *instead* before what it lacks, and
    /// name the near form, because a near form the reader will find alone is
    /// the thing that costs them a day.
    ///
    /// It is prose that lands inside a SQL comment, so it must never spell a
    /// complete statement.
    Unsupported(&'static str),
    /// **The dialect has not answered.**
    ///
    /// Razeri refuses rather than emitting a statement from a different
    /// vendor's grammar, and the conformance kit reports the silence. A dialect
    /// whose engine really has neither says so with
    /// [`MergeShape::Unsupported`], which is a claim rather than a silence and
    /// carries the words to prove it.
    #[default]
    None,
}

/// What this engine's `MERGE` will not do.
///
/// Everything defaults to *no restriction*, so a dialect names only what it
/// has measured. Each field exists because an engine answered an error, not
/// because a manual implied one.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct MergeRestrictions {
    /// **This engine refuses a `MERGE` carrying only one `WHEN` clause.**
    ///
    /// `Redshift`'s answer is *"MERGE statement with a single WHEN clause is
    /// not enabled"* — a whole statement rejected, not a clause ignored, and
    /// with no session setting that lifts it.
    ///
    /// It matters because Razeri composes one merge of each kind: the core
    /// load, which carries `WHEN MATCHED` and `WHEN NOT MATCHED` and is fine,
    /// and the identity load, which carries one `WHEN NOT MATCHED` and is not.
    /// An engine that sets this takes the portable `INSERT … WHERE NOT EXISTS`
    /// and `DELETE … WHERE NOT EXISTS` pair for the second, which is the same
    /// two effects in two statements.
    pub requires_two_when_clauses: bool,
}

impl MergeRestrictions {
    /// **No restriction Razeri has measured** — the ordinary answer for an
    /// engine whose `MERGE` takes what Razeri composes.
    #[must_use]
    pub const fn none() -> Self {
        Self { requires_two_when_clauses: false }
    }

    /// **This engine refuses a `MERGE` carrying only one `WHEN` clause.** See
    /// [`MergeRestrictions::requires_two_when_clauses`].
    #[must_use]
    pub const fn requiring_two_when_clauses() -> Self {
        Self { requires_two_when_clauses: true }
    }
}

/// How an engine whose TABLE ENGINE deduplicates takes Razeri's load.
///
/// Both fields are spellings the composed load and the generated views embed,
/// so they live on the shape rather than as separate renderer hooks — one fact,
/// one place, nothing for a consistency gate to hold together.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct InsertDedup {
    /// **The Razeri-owned audit column that decides the surviving row** when
    /// two rows share the sorting key. Mandatory: without a version column the
    /// engine keeps whichever row a background merge saw last, which is
    /// nondeterministic, and Razeri refuses nondeterminism. The materializer
    /// verifies the named column is one the versioned table really carries; a
    /// design that cannot carry it is refused by name, never downgraded.
    pub version_column: &'static str,
    /// The modifier a read of the versioned table appends when it needs the
    /// engine's one-row-per-key guarantee at read time rather than after the
    /// next background merge (`FINAL` on the engine that carries this shape).
    /// It is a read COST, which is why only the generated views say it and the
    /// load itself never does.
    pub final_read_modifier: &'static str,
}

impl InsertDedup {
    /// The shape, with both spellings stated.
    #[must_use]
    pub const fn new(version_column: &'static str, final_read_modifier: &'static str) -> Self {
        Self { version_column, final_read_modifier }
    }
}

/// The upsert statement an engine without `MERGE` spells instead.
///
/// A form is added here when Razeri has *composed* the load in it, not when an
/// engine is known to have one: naming a form Razeri cannot write is how a
/// dialect ends up emitting another vendor's grammar.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum UpsertForm {
    /// `INSERT … ON DUPLICATE KEY UPDATE`. It reads the table's own unique keys
    /// rather than naming a conflict target, so an engine that spells
    /// `ON CONFLICT` is not this.
    ///
    /// **Two engines have it and exactly two of Razeri's dialects name it** —
    /// `MySQL` and `MariaDB`. Naming the form is a claim about an engine,
    /// never a default: an engine that merely lacks `MERGE` does not thereby
    /// speak this grammar. `ClickHouse`, asked, answers *"Syntax error …
    /// failed at position 68 (`DUPLICATE`)"* and states
    /// [`MergeShape::Unsupported`] instead.
    OnDuplicateKeyUpdate,
}

impl MergeShape {
    /// Whether Razeri's two-clause core load can be a `MERGE` here.
    #[must_use]
    pub const fn merges(&self) -> bool {
        matches!(self, Self::Merge(_))
    }

    /// Whether a **one**-clause `MERGE` runs here — the identity load's shape.
    ///
    /// An engine with `MERGE` can still answer no, and one does.
    #[must_use]
    pub const fn merges_a_single_when_clause(&self) -> bool {
        match self {
            Self::Merge(restrictions) => !restrictions.requires_two_when_clauses,
            _ => false,
        }
    }

    /// The engine's own upsert statement, where it has one instead of `MERGE`.
    #[must_use]
    pub const fn upsert_form(&self) -> Option<UpsertForm> {
        match self {
            Self::Upsert(form) => Some(*form),
            _ => None,
        }
    }

    /// The insert-and-deduplicate composition, where the table engine is the
    /// upsert — see [`MergeShape::InsertDedup`].
    #[must_use]
    pub const fn insert_dedup(&self) -> Option<InsertDedup> {
        match self {
            Self::InsertDedup(form) => Some(*form),
            _ => None,
        }
    }

    /// **Why Razeri cannot compose a load here**, in words, or `None` if it
    /// can. The sentence is the one a reader acts on.
    ///
    /// The one accessor every surface reads — the materializer that declines to
    /// compose the load, and the `loads.sql` that says so where an operator
    /// opens it — so neither one holds a second copy of the words.
    #[must_use]
    pub const fn refusal(&self) -> Option<&'static str> {
        match self {
            Self::Unsupported(orientation) => Some(orientation),
            Self::None => Some(
                "this engine states no MERGE and no upsert statement Razeri can compose, so \
                 there is no generated load for it. A dialect says which it has by returning \
                 MergeShape::Merge or MergeShape::Upsert; until then Razeri refuses rather than \
                 emitting another engine's grammar.",
            ),
            _ => None,
        }
    }

    /// Whether Razeri composes a load for this engine at all.
    ///
    /// The one question the materializer asks before it builds the load
    /// procedures — asked of the shape, never of a dialect's name.
    #[must_use]
    pub const fn composes_a_load(&self) -> bool {
        self.refusal().is_none()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A dialect that has said nothing gets a refusal, not `MySQL`'s
    /// statement. That inversion is the whole point of the type.
    #[test]
    fn a_dialect_that_states_nothing_refuses_rather_than_borrowing_a_grammar() {
        let shape = MergeShape::default();
        assert!(!shape.merges());
        assert!(!shape.merges_a_single_when_clause());
        assert_eq!(shape.upsert_form(), None);
        assert!(shape.refusal().is_some());
        assert!(!shape.composes_a_load());
    }

    /// **A measured refusal and a silent one are different values**, and the
    /// reader gets a different sentence from each. They were one variant, and a
    /// dialect that had measured could not say so.
    #[test]
    fn a_measured_refusal_carries_the_engines_own_sentence_and_a_silent_one_does_not() {
        let measured = MergeShape::Unsupported("this engine de-duplicates in the table engine");
        assert_eq!(
            measured.refusal(),
            Some("this engine de-duplicates in the table engine"),
            "a stated refusal answers in its own words"
        );
        assert_ne!(
            MergeShape::None.refusal(),
            measured.refusal(),
            "silence and a measured answer must not read the same"
        );
        assert!(!measured.composes_a_load());
        assert!(!measured.merges());
        assert_eq!(measured.upsert_form(), None);
    }

    /// The shapes that DO compose a load carry no refusal, so nothing
    /// downstream has to special-case them.
    #[test]
    fn a_shape_that_composes_a_load_carries_no_refusal() {
        for shape in [
            MergeShape::Merge(MergeRestrictions::none()),
            MergeShape::Merge(MergeRestrictions::requiring_two_when_clauses()),
            MergeShape::Upsert(UpsertForm::OnDuplicateKeyUpdate),
            MergeShape::InsertDedup(InsertDedup::new("_loaded_at", "FINAL")),
        ] {
            assert_eq!(shape.refusal(), None, "{shape:?} composes a load");
            assert!(shape.composes_a_load(), "{shape:?} composes a load");
        }
    }

    /// **Insert-and-dedup composes a load and is still not a `MERGE`.** Every
    /// family that needs a real `MERGE` — the single-`WHEN` identity sync
    /// included — must keep taking its non-merge spelling on this shape, and
    /// the two spellings the composition embeds travel on the shape itself.
    #[test]
    fn insert_dedup_composes_a_load_without_claiming_a_merge() {
        let shape = MergeShape::InsertDedup(InsertDedup::new("_loaded_at", "FINAL"));
        assert!(shape.composes_a_load());
        assert!(!shape.merges());
        assert!(!shape.merges_a_single_when_clause());
        assert_eq!(shape.upsert_form(), None, "the engine has no upsert statement");
        let form = shape.insert_dedup().expect("the composition's spellings");
        assert_eq!(form.version_column, "_loaded_at");
        assert_eq!(form.final_read_modifier, "FINAL");
        assert_eq!(
            MergeShape::Merge(MergeRestrictions::none()).insert_dedup(),
            None,
            "a merge engine is not an engine-dedup engine"
        );
    }

    /// An engine can have `MERGE` and still refuse the one-clause form. Both
    /// halves used to be separate booleans and only one of them was asked.
    #[test]
    fn a_merge_with_a_clause_minimum_answers_the_two_questions_differently() {
        let restricted = MergeShape::Merge(MergeRestrictions::requiring_two_when_clauses());
        assert!(restricted.merges());
        assert!(!restricted.merges_a_single_when_clause());

        let unrestricted = MergeShape::Merge(MergeRestrictions::none());
        assert!(unrestricted.merges());
        assert!(unrestricted.merges_a_single_when_clause());
    }

    /// An upsert engine is not a merge engine, and neither is a refusal.
    #[test]
    fn an_upsert_engine_is_not_a_merge_engine() {
        let upsert = MergeShape::Upsert(UpsertForm::OnDuplicateKeyUpdate);
        assert!(!upsert.merges());
        assert_eq!(upsert.upsert_form(), Some(UpsertForm::OnDuplicateKeyUpdate));
        assert!(upsert.refusal().is_none());
    }
}