razeri-plugin-contract 0.3.2

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.

//! Dialect Family Definitions
//!
//! This module defines the SQL dialect families used across the Razeri workspace.
//! Dialect families group platforms that share similar SQL syntax patterns.

/// **The family a platform belongs to** — and a family is a claim about
/// spelling, not about ancestry.
///
/// Platforms in one family share syntax, so a family membership *licenses*
/// spellings. That is why a third party must be able to name their own family
/// rather than being asked to pick the closest of Razeri's: grouping `Redshift`
/// with `PostgreSql` would hand it `ON CONFLICT`, `RETURNING`, `QUALIFY`, named
/// windows and `CHECK` constraints, every one of which it rejects.
///
/// # Naming your own
///
/// [`DialectFamily::Custom`] carries your family's name, and Razeri never
/// pretends to know what it licenses:
///
/// ```
/// use razeri_plugin_contract::DialectFamily;
///
/// const MY_FAMILY: DialectFamily = DialectFamily::Custom("teradata");
/// assert_eq!(MY_FAMILY.name(), "teradata");
/// ```
///
/// # Why an escape variant rather than a string newtype
///
/// A string-backed identity would have made every one of Razeri's own
/// references to a family (`DialectFamily::PostgreSql` and its siblings) an
/// associated constant, which reads the same and matches worse: the compiler
/// stops knowing the named set is closed over Razeri's own arms, and a typo in
/// a family name becomes a silently new family instead of a compile error. The
/// enum keeps the named set exhaustive where Razeri owns it, and
/// `#[non_exhaustive]` makes every *other* crate — including this repository's
/// own, one tier up — state a fallback, which is the honesty a third party's
/// family needs and the guarantee that adding a measured family later breaks
/// nobody's build.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum DialectFamily {
    /// `PostgreSQL` and PostgreSQL-compatible (`CitusDB`, `CockroachDB`, etc.)
    PostgreSql,
    /// Microsoft T-SQL (SQL Server, Azure SQL)
    TSql,
    /// Snowflake SQL
    Snowflake,
    /// Apache Spark SQL (Databricks, EMR, etc.)
    SparkSql,
    /// `ClickHouse` SQL
    ClickHouse,
    /// Razeri Agnostic SQL (ANSI SQL with @razeri: annotations)
    Razeri,
    /// MySQL/MariaDB SQL
    ///
    /// **Spelled `mariadb`, not what `snake_case` would make of two capitals.**
    /// `serde`'s rule reads `MariaDB` as three words and answers `maria_d_b`,
    /// which is nobody's name for this family and reached every consumer that
    /// reads the serialized form. The rename says the name.
    #[serde(rename = "mariadb")]
    MariaDB,
    /// Google `BigQuery`'s `GoogleSQL`
    BigQuery,
    /// Amazon `Redshift`
    ///
    /// **Its own family, and not [`Self::PostgreSql`].** The engine descends
    /// from `PostgreSQL` 8.0 and reports itself as one, but the SQL a family
    /// membership would license does not run: no `ON CONFLICT`, no
    /// `RETURNING`, no `CHECK` constraint, no indexes, no array or JSON type,
    /// no `QUALIFY`, no named windows, and `TEXT` silently meaning
    /// `VARCHAR(256)`. Grouping it with `PostgreSQL` would hand it exactly the
    /// spellings it rejects.
    Redshift,
    /// **A family Razeri has not measured**, carrying its own name.
    ///
    /// This is how a third-party dialect says *"my engine is its own family"*
    /// without editing anything of Razeri's. Nothing here is inferred from the
    /// name: a custom family licenses no spelling, and every question Razeri
    /// would otherwise answer from a family membership is asked of the dialect
    /// itself instead.
    Custom(&'static str),
}

impl DialectFamily {
    /// The family's own name — its `serde` spelling for the named arms, and
    /// the string it carries for [`DialectFamily::Custom`].
    #[must_use]
    pub const fn name(&self) -> &'static str {
        match self {
            Self::PostgreSql => "postgre_sql",
            Self::TSql => "t_sql",
            Self::Snowflake => "snowflake",
            Self::SparkSql => "spark_sql",
            Self::ClickHouse => "click_house",
            Self::Razeri => "razeri",
            Self::MariaDB => "mariadb",
            Self::BigQuery => "big_query",
            Self::Redshift => "redshift",
            Self::Custom(name) => name,
        }
    }

    /// Returns the identifier quoting character(s) for this dialect family.
    ///
    /// A [`DialectFamily::Custom`] family gets ANSI's double quotes, because a
    /// name is not evidence: the dialect's own
    /// [`quote_identifier`](crate::SqlRenderer::quote_identifier) is where an
    /// engine says how it quotes, and this answer is only the family-level
    /// convenience its callers ask for.
    #[must_use]
    pub const fn quote_chars(&self) -> (&'static str, &'static str) {
        match self {
            Self::PostgreSql
            | Self::Snowflake
            | Self::Razeri
            | Self::Redshift
            | Self::Custom(_) => ("\"", "\""),
            Self::TSql => ("[", "]"),
            Self::SparkSql | Self::ClickHouse | Self::MariaDB | Self::BigQuery => ("`", "`"),
        }
    }
}

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

    #[test]
    fn test_quote_chars() {
        assert_eq!(DialectFamily::PostgreSql.quote_chars(), ("\"", "\""));
        assert_eq!(DialectFamily::TSql.quote_chars(), ("[", "]"));
        assert_eq!(DialectFamily::ClickHouse.quote_chars(), ("`", "`"));
    }

    /// A family Razeri has never seen carries its own name and licenses
    /// nothing. Quoting falls to ANSI here because the dialect's own
    /// `quote_identifier` is where the engine actually answers.
    #[test]
    fn a_custom_family_carries_its_name_and_is_licensed_nothing() {
        let mine = DialectFamily::Custom("teradata");
        assert_eq!(mine.name(), "teradata");
        assert_eq!(mine.quote_chars(), ("\"", "\""));
        assert_ne!(mine, DialectFamily::PostgreSql);
    }

    /// Every named arm answers under its own serialized spelling, so a report
    /// that prints a family and a payload that serializes one agree.
    #[test]
    fn every_named_family_reports_its_serde_spelling() {
        for (family, spelled) in [
            (DialectFamily::PostgreSql, "postgre_sql"),
            (DialectFamily::TSql, "t_sql"),
            (DialectFamily::MariaDB, "mariadb"),
            (DialectFamily::BigQuery, "big_query"),
            (DialectFamily::Redshift, "redshift"),
        ] {
            assert_eq!(family.name(), spelled);
            assert_eq!(
                serde_json::to_string(&family).expect("a family serializes"),
                format!("\"{spelled}\"")
            );
        }
    }
}