razeri-plugin-contract 0.1.1

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.

//! Agnostic dialect and ANSI data-type rendering.
//!
//! [`AnsiDialect`] is a zero-dependency [`SqlRenderer`] for the
//! `Agnostic` pseudo-platform.  It uses ANSI-standard SQL defaults and
//! double-quote identifier quoting, matching the `DialectFamily::Razeri`
//! conventions.
//!
//! [`render_data_type_ansi`] is a reusable free function that maps
//! [`DataType`] to ANSI SQL type names.

use crate::{
    DataType,
    data_type::{FloatPrecision, IntegerSize, JsonStorageFormat},
};

use crate::{DdlRenderer, FormulaRenderer, SqlRenderer};

/// Render a [`DataType`] as an ANSI SQL type name.
///
/// This is the shared implementation used by [`AnsiDialect`] and dialect
/// plugins for platforms that follow standard SQL naming.
#[must_use]
pub fn render_data_type_ansi(dt: &DataType) -> String {
    match dt {
        DataType::Integer(i) => match i.size {
            IntegerSize::Tiny => "TINYINT",
            IntegerSize::Small => "SMALLINT",
            IntegerSize::Standard => "INTEGER",
            IntegerSize::Big => "BIGINT",
        }
        .to_string(),
        DataType::Decimal(d) => format!("DECIMAL({}, {})", d.precision, d.scale),
        DataType::Float(f) => match f.precision {
            FloatPrecision::Single => "FLOAT",
            FloatPrecision::Double => "DOUBLE PRECISION",
        }
        .to_string(),
        DataType::Boolean(_) => "BOOLEAN".to_string(),
        DataType::Char(c) => format!("CHAR({})", c.length),
        DataType::VarChar(vc) => match vc.max_length {
            Some(len) => format!("VARCHAR({len})"),
            None => "TEXT".to_string(),
        },
        DataType::Text(_) => "TEXT".to_string(),
        DataType::Binary(b) => format!("BINARY({})", b.length),
        DataType::VarBinary(vb) => match vb.max_length {
            Some(len) => format!("VARBINARY({len})"),
            None => "BLOB".to_string(),
        },
        DataType::Date(_) => "DATE".to_string(),
        DataType::Time(t) => {
            if t.precision > 0 {
                format!("TIME({})", t.precision)
            } else {
                "TIME".to_string()
            }
        }
        DataType::Timestamp(ts) => {
            if ts.precision > 0 {
                format!("TIMESTAMP({})", ts.precision)
            } else {
                "TIMESTAMP".to_string()
            }
        }
        DataType::TimestampTz(ts) => {
            let base = "TIMESTAMP WITH TIME ZONE";
            if ts.precision > 0 { format!("{base}({})", ts.precision) } else { base.to_string() }
        }
        DataType::DateTime(dt_inner) => {
            if dt_inner.precision > 0 {
                format!("DATETIME({})", dt_inner.precision)
            } else {
                "DATETIME".to_string()
            }
        }
        DataType::Interval(_) => "INTERVAL".to_string(),
        DataType::Uuid(_) => "UUID".to_string(),
        DataType::Json(j) => match j.storage_format {
            JsonStorageFormat::Binary => "JSONB".to_string(),
            JsonStorageFormat::Text => "JSON".to_string(),
        },
        DataType::Array(arr) => {
            let elem = render_data_type_ansi(&arr.element_type);
            format!("{elem}[]")
        }
        DataType::Map(_) => "JSON".to_string(),
    }
}

/// **The ANSI `CAST` target spelling for a [`DataType`]** — a *different*
/// mapping from [`render_data_type_ansi`], on purpose.
///
/// A column declaration and a `CAST` target are two grammars on most engines.
/// The clearest divergence is the smallest integer: a column may be declared
/// `TINYINT`, but ANSI's narrowest portable `CAST` target is `SMALLINT`, and an
/// engine that has no one-byte integer answers a binder error rather than
/// widening for you.
///
/// This is what [`SqlRenderer::render_cast_type`]
/// returns until a dialect overrides it.
#[must_use]
pub fn render_cast_type_ansi(dt: &DataType) -> String {
    match dt {
        DataType::Boolean(_) => "BOOLEAN".to_owned(),
        DataType::Integer(i) => match i.size {
            IntegerSize::Tiny | IntegerSize::Small => "SMALLINT".to_owned(),
            IntegerSize::Standard => "INTEGER".to_owned(),
            IntegerSize::Big => "BIGINT".to_owned(),
        },
        DataType::Decimal(d) => format!("DECIMAL({}, {})", d.precision, d.scale),
        DataType::Float(f) => match f.precision {
            FloatPrecision::Double => "DOUBLE PRECISION".to_owned(),
            FloatPrecision::Single => "REAL".to_owned(),
        },
        DataType::Char(c) => format!("CHAR({})", c.length),
        DataType::VarChar(v) => match v.max_length {
            Some(n) => format!("VARCHAR({n})"),
            None => "VARCHAR".to_owned(),
        },
        DataType::Date(_) => "DATE".to_owned(),
        DataType::Time(_) => "TIME".to_owned(),
        DataType::Timestamp(_) | DataType::DateTime(_) => "TIMESTAMP".to_owned(),
        DataType::TimestampTz(_) => "TIMESTAMP WITH TIME ZONE".to_owned(),
        DataType::Binary(_) => "BINARY".to_owned(),
        DataType::VarBinary(_) => "VARBINARY".to_owned(),
        DataType::Json(_) => "JSON".to_owned(),
        DataType::Uuid(_) => "UUID".to_owned(),
        _ => "TEXT".to_owned(),
    }
}

/// Zero-config [`SqlRenderer`] for the Agnostic pseudo-platform.
///
/// Uses ANSI SQL type names and double-quote identifier quoting.
/// Intended for parameterless `to_sql()` / `to_ddl()` convenience
/// methods that need a default renderer.
pub struct AnsiDialect;

impl SqlRenderer for AnsiDialect {
    fn render_data_type(&self, dt: &DataType) -> String {
        render_data_type_ansi(dt)
    }

    fn quote_identifier(&self, name: &str) -> String {
        format!("\"{name}\"")
    }

    fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
        match schema {
            Some(s) => format!("{}.{}", self.quote_identifier(s), self.quote_identifier(name)),
            None => self.quote_identifier(name),
        }
    }

    fn render_current_timestamp(&self) -> String {
        "CURRENT_TIMESTAMP".to_string()
    }

    fn is_agnostic_dialect(&self) -> bool {
        true
    }

    fn dialect_name(&self) -> &'static str {
        "ansi"
    }

    fn to_formula_renderer(&self) -> Box<dyn FormulaRenderer> {
        Box::new(Self)
    }

    /// ANSI's vocabulary **on purpose**, and named so — not the guess a dialect
    /// that has decided nothing inherits.
    fn sid_spelling(&self) -> crate::SidSpelling {
        crate::ANSI_SID_SPELLING
    }
}

impl DdlRenderer for AnsiDialect {}

impl FormulaRenderer for AnsiDialect {}

impl crate::ReplicationRenderer for AnsiDialect {}

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

    #[test]
    fn agnostic_is_agnostic() {
        let d = AnsiDialect;
        assert!(d.is_agnostic_dialect());
        assert_eq!(d.dialect_name(), "ansi");
    }

    #[test]
    fn agnostic_quotes_with_double_quotes() {
        let d = AnsiDialect;
        assert_eq!(d.quote_identifier("my_table"), "\"my_table\"");
    }

    #[test]
    fn agnostic_qualifies_name() {
        let d = AnsiDialect;
        assert_eq!(d.qualify_name(Some("public"), "users"), "\"public\".\"users\"");
        assert_eq!(d.qualify_name(None, "users"), "\"users\"");
    }

    #[test]
    fn ansi_data_type_integer() {
        assert_eq!(render_data_type_ansi(&DataType::int()), "INTEGER");
        assert_eq!(render_data_type_ansi(&DataType::bigint()), "BIGINT");
        assert_eq!(render_data_type_ansi(&DataType::smallint()), "SMALLINT");
    }

    #[test]
    fn ansi_data_type_varchar() {
        assert_eq!(render_data_type_ansi(&DataType::varchar(100)), "VARCHAR(100)");
        assert_eq!(render_data_type_ansi(&DataType::text()), "TEXT");
    }

    #[test]
    fn ansi_data_type_timestamp() {
        assert_eq!(render_data_type_ansi(&DataType::timestamp()), "TIMESTAMP(6)");
        assert_eq!(render_data_type_ansi(&DataType::date()), "DATE");
    }
}