rusticx-sql 1.0.0

Shared SQL dialect compiler (DDL + DML) for the Rusticx multi-database ORM
Documentation
use rusticx_core::column::ColumnType;

/// SQL dialect controls syntax differences between backends.
pub trait SqlDialect: Send + Sync {
    /// Placeholder style: `$1` for Postgres, `?` for MySQL.
    fn placeholder(&self, index: usize) -> String;

    /// Quote an identifier (table name, column name).
    fn quote_ident(&self, name: &str) -> String;

    /// Map a ColumnType to dialect-specific SQL type string.
    fn sql_type(&self, col_type: &ColumnType) -> String;

    /// Whether RETURNING clause is supported (Postgres yes, MySQL no).
    fn supports_returning(&self) -> bool;

    /// Whether INSERT OR IGNORE / ON CONFLICT DO NOTHING is supported.
    fn upsert_syntax(&self) -> UpsertSyntax;
}

#[derive(Debug, Clone, Copy)]
pub enum UpsertSyntax {
    /// PostgreSQL: ON CONFLICT DO NOTHING
    OnConflict,
    /// MySQL: INSERT IGNORE
    InsertIgnore,
    /// Not supported
    None,
}

// ── Postgres dialect ──────────────────────────────────────────────────────────

pub struct PostgresDialect;

impl SqlDialect for PostgresDialect {
    fn placeholder(&self, index: usize) -> String {
        format!("${index}")
    }

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

    fn sql_type(&self, col_type: &ColumnType) -> String {
        match col_type {
            ColumnType::Bool => "BOOLEAN".into(),
            ColumnType::SmallInt => "SMALLINT".into(),
            ColumnType::Int => "INTEGER".into(),
            ColumnType::BigInt => "BIGINT".into(),
            ColumnType::Float => "REAL".into(),
            ColumnType::Double => "DOUBLE PRECISION".into(),
            ColumnType::Decimal { precision, scale } => format!("DECIMAL({precision},{scale})"),
            ColumnType::Text => "TEXT".into(),
            ColumnType::Varchar(n) => format!("VARCHAR({n})"),
            ColumnType::Char(n) => format!("CHAR({n})"),
            ColumnType::Bytes => "BYTEA".into(),
            ColumnType::Uuid => "UUID".into(),
            ColumnType::Timestamp => "TIMESTAMP".into(),
            ColumnType::TimestampTz => "TIMESTAMPTZ".into(),
            ColumnType::Date => "DATE".into(),
            ColumnType::Time => "TIME".into(),
            ColumnType::Json => "JSON".into(),
            ColumnType::Jsonb => "JSONB".into(),
            ColumnType::Array(inner) => format!("{}[]", self.sql_type(inner)),
            ColumnType::Dynamic => "JSONB".into(),
        }
    }

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

    fn upsert_syntax(&self) -> UpsertSyntax {
        UpsertSyntax::OnConflict
    }
}

// ── MySQL / MariaDB dialect ───────────────────────────────────────────────────

pub struct MysqlDialect;

impl SqlDialect for MysqlDialect {
    fn placeholder(&self, _index: usize) -> String {
        "?".into()
    }

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

    fn sql_type(&self, col_type: &ColumnType) -> String {
        match col_type {
            ColumnType::Bool => "TINYINT(1)".into(),
            ColumnType::SmallInt => "SMALLINT".into(),
            ColumnType::Int => "INT".into(),
            ColumnType::BigInt => "BIGINT".into(),
            ColumnType::Float => "FLOAT".into(),
            ColumnType::Double => "DOUBLE".into(),
            ColumnType::Decimal { precision, scale } => format!("DECIMAL({precision},{scale})"),
            ColumnType::Text => "TEXT".into(),
            ColumnType::Varchar(n) => format!("VARCHAR({n})"),
            ColumnType::Char(n) => format!("CHAR({n})"),
            ColumnType::Bytes => "BLOB".into(),
            ColumnType::Uuid => "CHAR(36)".into(),
            ColumnType::Timestamp | ColumnType::TimestampTz => "DATETIME(6)".into(),
            ColumnType::Date => "DATE".into(),
            ColumnType::Time => "TIME".into(),
            ColumnType::Json | ColumnType::Jsonb => "JSON".into(),
            ColumnType::Array(_) => "JSON".into(), // MySQL has no array type
            ColumnType::Dynamic => "JSON".into(),
        }
    }

    fn supports_returning(&self) -> bool {
        false
    }

    fn upsert_syntax(&self) -> UpsertSyntax {
        UpsertSyntax::InsertIgnore
    }
}