use heck::ToSnakeCase;
use sea_orm::DbErr;
use sea_orm_migration::{
prelude::*,
schema::{
big_integer, big_integer_null, big_pk_auto, boolean, boolean_null, date, date_null, double,
double_null, integer, integer_null, pk_uuid, string, string_null, string_uniq, text,
text_null, timestamp_with_time_zone, timestamp_with_time_zone_null, uuid, uuid_null,
uuid_uniq,
},
sea_query::{ForeignKeyAction, Table},
};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum ColType {
PkAuto,
PkUuid,
String,
StringNull,
StringUniq,
Text,
TextNull,
Integer,
IntegerNull,
BigInteger,
BigIntegerNull,
Double,
DoubleNull,
Boolean,
BooleanNull,
BooleanWithDefault(bool),
Date,
DateNull,
TimestampWithTimeZone,
TimestampWithTimeZoneNull,
Uuid,
UuidNull,
UuidUniq,
JsonBinary,
JsonBinaryNull,
}
impl ColType {
fn to_def(&self, name: Alias) -> ColumnDef {
match self {
Self::PkAuto => big_pk_auto(name),
Self::PkUuid => pk_uuid(name),
Self::String => string(name),
Self::StringNull => string_null(name),
Self::StringUniq => string_uniq(name),
Self::Text => text(name),
Self::TextNull => text_null(name),
Self::Integer => integer(name),
Self::IntegerNull => integer_null(name),
Self::BigInteger => big_integer(name),
Self::BigIntegerNull => big_integer_null(name),
Self::Double => double(name),
Self::DoubleNull => double_null(name),
Self::Boolean => boolean(name),
Self::BooleanNull => boolean_null(name),
Self::BooleanWithDefault(value) => boolean(name).default(*value).take(),
Self::Date => date(name),
Self::DateNull => date_null(name),
Self::TimestampWithTimeZone => timestamp_with_time_zone(name),
Self::TimestampWithTimeZoneNull => timestamp_with_time_zone_null(name),
Self::Uuid => uuid(name),
Self::UuidNull => uuid_null(name),
Self::UuidUniq => uuid_uniq(name),
Self::JsonBinary => ColumnDef::new(name).json_binary().not_null().take(),
Self::JsonBinaryNull => ColumnDef::new(name).json_binary().null().take(),
}
}
}
fn normalize_table(table: &str) -> std::string::String {
cruet::to_plural(table).to_snake_case()
}
fn reference_id(table: &str) -> std::string::String {
format!("{}_id", cruet::to_singular(table).to_snake_case())
}
pub async fn create_table(
m: &SchemaManager<'_>,
table: &str,
cols: &[(&str, ColType)],
refs: &[(&str, &str)],
) -> Result<(), DbErr> {
let table = normalize_table(table);
let mut stmt = Table::create()
.table(Alias::new(&table))
.if_not_exists()
.take();
stmt.col(timestamp_with_time_zone(Alias::new("created_at")).default(Expr::current_timestamp()));
stmt.col(timestamp_with_time_zone(Alias::new("updated_at")).default(Expr::current_timestamp()));
for (name, col) in cols {
stmt.col(col.to_def(Alias::new(*name)));
}
for (parent, column) in refs {
let (parent, nullable) = parent.strip_suffix('?').map_or_else(
|| (normalize_table(parent), false),
|stripped| (normalize_table(stripped), true),
);
let column = if column.is_empty() {
reference_id(&parent)
} else {
(*column).to_string()
};
if !cols.iter().any(|(name, _)| *name == column) {
let col = if nullable {
ColType::BigIntegerNull
} else {
ColType::BigInteger
};
stmt.col(col.to_def(Alias::new(&column)));
}
let mut fk = ForeignKey::create();
fk.name(format!("fk-{table}-{column}-to-{parent}"));
fk.from(Alias::new(&table), Alias::new(&column));
fk.to(Alias::new(parent), Alias::new("id"));
if nullable {
fk.on_delete(ForeignKeyAction::SetNull);
fk.on_update(ForeignKeyAction::NoAction);
} else {
fk.on_delete(ForeignKeyAction::Cascade);
fk.on_update(ForeignKeyAction::Cascade);
}
stmt.foreign_key(&mut fk);
}
m.create_table(stmt).await
}
pub async fn drop_table(m: &SchemaManager<'_>, table: &str) -> Result<(), DbErr> {
m.drop_table(
Table::drop()
.table(Alias::new(normalize_table(table)))
.to_owned(),
)
.await
}
pub async fn add_column(
m: &SchemaManager<'_>,
table: &str,
name: &str,
col: ColType,
) -> Result<(), DbErr> {
m.alter_table(
Table::alter()
.table(Alias::new(normalize_table(table)))
.add_column(col.to_def(Alias::new(name)))
.to_owned(),
)
.await
}
pub async fn remove_column(m: &SchemaManager<'_>, table: &str, name: &str) -> Result<(), DbErr> {
m.alter_table(
Table::alter()
.table(Alias::new(normalize_table(table)))
.drop_column(Alias::new(name))
.to_owned(),
)
.await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn table_names_are_pluralised_snake_case() {
assert_eq!(normalize_table("user"), "users");
assert_eq!(normalize_table("users"), "users");
assert_eq!(normalize_table("CarePathway"), "care_pathways");
}
#[test]
fn reference_columns_are_singular() {
assert_eq!(reference_id("users"), "user_id");
}
}