rydantic 0.7.0-beta.4

Type-driven validation, JSON Schema, Cap'n Proto, Axum and SQLx models for Rust
Documentation
use super::{field_error, Column, Dialect, SqlBackend, SqlError, SqlModel, SqlType};

pub(super) fn check_identifiers<T: SqlModel, DB: SqlBackend>() -> Result<(), SqlError> {
    let limit = match DB::DIALECT {
        Dialect::Sqlite => return Ok(()),
        Dialect::Postgres => 63,
        Dialect::MySql => 64,
    };
    for name in std::iter::once(T::TABLE).chain(T::COLUMNS.iter().flat_map(|column| {
        std::iter::once(column.name)
            .chain(column.rename_from)
            .chain(
                column
                    .references
                    .into_iter()
                    .flat_map(|reference| [reference.table, reference.column]),
            )
    })) {
        if name.len() > limit {
            return Err(field_error(
                "sql_identifier_length",
                "identifier exceeds the database's supported length",
            )
            .at(name));
        }
    }
    Ok(())
}

pub(super) fn index_name<T: SqlModel>(column: &Column) -> String {
    let name = format!("rydantic_{}_{}_{}", T::TABLE.len(), T::TABLE, column.name);
    if name.len() <= 63 {
        return name;
    }
    let hash = name.bytes().fold(0xcbf29ce484222325u64, |hash, byte| {
        (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3)
    });
    format!("rydantic_{hash:016x}")
}
pub(super) fn declaration(column: &Column, dialect: Dialect) -> String {
    match (dialect, column.sql_type) {
        (Dialect::Sqlite, SqlType::Integer | SqlType::Boolean) => "INTEGER".into(),
        (Dialect::Sqlite, SqlType::Real) => "REAL".into(),
        (Dialect::Sqlite, SqlType::Text) => "TEXT".into(),
        (Dialect::Sqlite, SqlType::Blob) => "BLOB".into(),
        (_, SqlType::Integer) => "BIGINT".into(),
        (_, SqlType::Boolean) => "BOOLEAN".into(),
        (Dialect::Postgres, SqlType::Real) => "DOUBLE PRECISION".into(),
        (Dialect::Postgres, SqlType::Text) => "TEXT".into(),
        (Dialect::Postgres, SqlType::Blob) => "BYTEA".into(),
        (Dialect::MySql, SqlType::Real) => "DOUBLE".into(),
        (Dialect::MySql, SqlType::Text | SqlType::Blob) => {
            let bounded = column.mysql_length().is_some();
            match (column.sql_type, bounded) {
                (SqlType::Text, true) => format!("VARCHAR({})", column.length.unwrap_or(191)),
                (SqlType::Blob, true) => format!("VARBINARY({})", column.length.unwrap_or(191)),
                (SqlType::Text, false) => "LONGTEXT".into(),
                _ => "LONGBLOB".into(),
            }
        }
    }
}
pub(super) fn table<T: SqlModel, DB: SqlBackend>() -> String {
    let dialect = DB::DIALECT;
    let q = |value: &str| dialect.quote(value);
    let mut definitions = Vec::new();
    let mut references = Vec::new();
    for column in T::COLUMNS {
        let mut definition = format!("{} {}", q(column.name), declaration(column, dialect));
        if column.auto_increment && dialect == Dialect::Postgres {
            definition.push_str(" GENERATED BY DEFAULT AS IDENTITY");
        }
        if column.primary_key {
            definition.push_str(" PRIMARY KEY");
        }
        if column.auto_increment && dialect == Dialect::Sqlite {
            definition.push_str(" AUTOINCREMENT");
        } else if !column.nullable || column.primary_key {
            definition.push_str(" NOT NULL");
        }
        if column.auto_increment && dialect == Dialect::MySql {
            definition.push_str(" AUTO_INCREMENT");
        }
        if column.unique && !column.primary_key {
            definition.push_str(" UNIQUE");
        }
        if let Some(default) = column.default {
            definition.push_str(&format!(" DEFAULT {}", default.sql(dialect)));
        }
        if let Some(reference) = column.references {
            let clause = format!(
                "REFERENCES {} ({}) ON DELETE {}",
                q(reference.table),
                q(reference.column),
                reference.on_delete.sql()
            );
            if dialect == Dialect::MySql {
                let source = format!("{}_{}_{}", T::TABLE.len(), T::TABLE, column.name);
                let hash = source.bytes().fold(0xcbf29ce484222325u64, |hash, byte| {
                    (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3)
                });
                references.push(format!(
                    "CONSTRAINT {} FOREIGN KEY ({}) {clause}",
                    q(&format!("rydantic_fk_{hash:016x}")),
                    q(column.name)
                ));
            } else {
                definition.push(' ');
                definition.push_str(&clause);
            }
        }
        definitions.push(definition);
    }
    definitions.extend(references);
    if dialect == Dialect::MySql {
        for column in T::COLUMNS
            .iter()
            .filter(|c| c.index && !c.unique && !c.primary_key)
        {
            definitions.push(format!(
                "INDEX {} ({})",
                q(&index_name::<T>(column)),
                q(column.name)
            ));
        }
    }
    let mut statement = format!(
        "CREATE TABLE IF NOT EXISTS {} ({})",
        q(T::TABLE),
        definitions.join(", ")
    );
    if dialect == Dialect::MySql {
        statement.push_str(" ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin");
    }
    statement
}
pub(super) fn indexes<T: SqlModel, DB: SqlBackend>() -> Vec<String> {
    if DB::DIALECT == Dialect::MySql {
        return Vec::new();
    }
    T::COLUMNS
        .iter()
        .filter(|c| c.index && !c.unique && !c.primary_key)
        .map(|column| {
            format!(
                "CREATE INDEX IF NOT EXISTS {} ON {} ({})",
                DB::DIALECT.quote(&index_name::<T>(column)),
                DB::DIALECT.quote(T::TABLE),
                DB::DIALECT.quote(column.name)
            )
        })
        .collect()
}