razeri-plugin-contract 0.1.0

The dialect contract Razeri plugins are written against — rendering traits, data types, platform identity
Documentation
// Copyright (C) 2026 Gabriel Shea <gshea@ideapivot.com> - All Rights Reserved

//! **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 three 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 neither, and the honest answer is a refusal. `ClickHouse` is
//!   that, measured: no `MERGE`, no `ON DUPLICATE KEY`, no `ON CONFLICT`, and a
//!   de-duplication that is a table engine's background behaviour rather than a
//!   statement — so Razeri writes it no load and says why.
//!
//! The flag this replaced had two branches and a platform for which neither
//! ran: `true` emitted a `MERGE` the engine rejected twice, and `false` emitted
//! **`MySQL`'s** `INSERT … ON DUPLICATE KEY UPDATE`, which that engine does not
//! have at all. An engine reached the second branch by *not* being `MySQL`,
//! which is not a reason to spell like it.
//!
//! ## 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),
    /// **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 }
    }
}

/// 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` — and it took the shape to get there. The old
    /// boolean had a third: an engine reached this statement by answering
    /// `false` to *"do you support MERGE?"*, so every engine that was merely
    /// not a merge engine inherited `MySQL`'s grammar by default, and
    /// `ClickHouse` shipped it in its `loads.sql` for as long as that boolean
    /// existed. Naming the form turned that default into a claim; the claim was
    /// then put to the engine, which answered *"Syntax error … failed at
    /// position 68 (`DUPLICATE`)"*, and `ClickHouse` now answers
    /// [`MergeShape::Unsupported`].
    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,
        }
    }

    /// **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. It used to ask a dialect's *name*, which is how the two
    /// engines that refuse came to be one hard-coded string and one silent
    /// fall-through.
    #[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 reshape.
    #[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 two 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),
        ] {
            assert_eq!(shape.refusal(), None, "{shape:?} composes a load");
            assert!(shape.composes_a_load(), "{shape:?} composes a load");
        }
    }

    /// 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());
    }
}