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