use crate::{schema, ColumnType, Dialect, Entity};
use sea_query::{Alias, ColumnDef, Index, Table, Value};
pub struct Column {
name: String,
ty: ColumnType,
nullable: bool,
default: Option<Value>,
keyed: bool,
}
impl Column {
pub fn new(name: impl Into<String>, ty: ColumnType) -> Self {
Self { name: name.into(), ty, nullable: true, default: None, keyed: false }
}
pub fn not_null(mut self) -> Self {
self.nullable = false;
self
}
pub fn null(mut self) -> Self {
self.nullable = true;
self
}
pub fn default(mut self, value: impl Into<Value>) -> Self {
self.default = Some(value.into());
self
}
pub fn keyed(mut self) -> Self {
self.keyed = true;
self
}
fn to_def(&self) -> ColumnDef {
let mut def = ColumnDef::new(Alias::new(self.name.clone()));
schema::apply_type(&mut def, self.ty, self.keyed);
if self.nullable {
def.null();
} else {
def.not_null();
}
if let Some(v) = &self.default {
def.default(v.clone());
}
def
}
}
pub struct Migration {
name: &'static str,
ops: Vec<Box<dyn Fn(Dialect) -> Vec<String>>>,
}
impl Migration {
pub fn new(name: &'static str) -> Self {
Self { name, ops: Vec::new() }
}
pub fn name(&self) -> &'static str {
self.name
}
fn push_stmt<S>(mut self, stmt: S) -> Self
where
S: sea_query::SchemaStatementBuilder + 'static,
{
self.ops.push(Box::new(move |d: Dialect| vec![d.build_schema(&stmt)]));
self
}
pub fn create_table<E: Entity + 'static>(mut self) -> Self {
self.ops.push(Box::new(|d| schema::schema_ddl::<E>(d)));
self
}
pub fn statement<S>(self, stmt: S) -> Self
where
S: sea_query::SchemaStatementBuilder + 'static,
{
self.push_stmt(stmt)
}
pub fn add_column(self, table: &str, column: Column) -> Self {
let mut def = column.to_def();
let mut stmt = Table::alter();
stmt.table(Alias::new(table)).add_column(&mut def);
self.push_stmt(stmt)
}
pub fn drop_column(self, table: &str, column: &str) -> Self {
let mut stmt = Table::alter();
stmt.table(Alias::new(table)).drop_column(Alias::new(column));
self.push_stmt(stmt)
}
pub fn rename_column(self, table: &str, from: &str, to: &str) -> Self {
let mut stmt = Table::alter();
stmt.table(Alias::new(table)).rename_column(Alias::new(from), Alias::new(to));
self.push_stmt(stmt)
}
pub fn rename_table(self, from: &str, to: &str) -> Self {
let mut stmt = Table::rename();
stmt.table(Alias::new(from), Alias::new(to));
self.push_stmt(stmt)
}
pub fn drop_table(self, table: &str) -> Self {
let mut stmt = Table::drop();
stmt.table(Alias::new(table)).if_exists();
self.push_stmt(stmt)
}
pub fn create_index<'c, I>(self, name: &str, table: &str, columns: I) -> Self
where
I: IntoIterator<Item = &'c str>,
{
self.index(name, table, columns, false)
}
pub fn create_unique_index<'c, I>(self, name: &str, table: &str, columns: I) -> Self
where
I: IntoIterator<Item = &'c str>,
{
self.index(name, table, columns, true)
}
fn index<'c, I>(self, name: &str, table: &str, columns: I, unique: bool) -> Self
where
I: IntoIterator<Item = &'c str>,
{
let mut stmt = Index::create();
stmt.name(name).table(Alias::new(table)).if_not_exists();
if unique {
stmt.unique();
}
for c in columns {
stmt.col(Alias::new(c));
}
self.push_stmt(stmt)
}
pub fn drop_index(self, name: &str, table: &str) -> Self {
let mut stmt = Index::drop();
stmt.name(name).table(Alias::new(table));
self.push_stmt(stmt)
}
pub fn raw(mut self, dialect: Dialect, sql: impl Into<String>) -> Self {
let sql = sql.into();
self.ops.push(Box::new(move |d| if d == dialect { vec![sql.clone()] } else { Vec::new() }));
self
}
pub fn raw_all(mut self, sql: impl Into<String>) -> Self {
let sql = sql.into();
self.ops.push(Box::new(move |_| vec![sql.clone()]));
self
}
pub fn render(&self, dialect: Dialect) -> Vec<String> {
self.ops.iter().flat_map(|op| op(dialect)).collect()
}
}
impl<X: crate::Executor> crate::migrate::Migration<X> for Migration {
fn name(&self) -> &'static str {
self.name
}
fn up<'a>(&'a self, db: &'a X) -> crate::migrate::StepFuture<'a> {
Box::pin(async move {
for sql in self.render(db.dialect()) {
db.execute(&sql, Vec::new()).await?;
}
Ok(())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_column_renders_per_dialect() {
let m = Migration::new("0002_add_phone")
.add_column("users", Column::new("phone", ColumnType::Text).keyed());
let sqlite = m.render(Dialect::Sqlite).join("\n");
let mysql = m.render(Dialect::MySql).join("\n");
assert!(sqlite.contains("ALTER TABLE"), "{sqlite}");
assert!(sqlite.contains("phone"));
assert!(mysql.contains("varchar") || mysql.contains("VARCHAR"), "{mysql}");
assert!(mysql.contains('`'), "mysql quoting");
assert!(sqlite.contains('"'), "sqlite quoting");
}
#[test]
fn not_null_with_default_backfills() {
let m = Migration::new("0003_add_flag").add_column(
"users",
Column::new("verified", ColumnType::Bool).not_null().default(false),
);
let sql = m.render(Dialect::Sqlite).join("\n");
assert!(sql.to_uppercase().contains("NOT NULL"), "{sql}");
assert!(sql.to_uppercase().contains("DEFAULT"), "{sql}");
}
#[test]
fn index_ops_render() {
let create = Migration::new("a")
.create_index("idx_users_phone", "users", ["phone"])
.render(Dialect::Sqlite)
.join("\n");
assert!(create.contains("CREATE INDEX"), "{create}");
assert!(create.contains("idx_users_phone"));
let uniq = Migration::new("b")
.create_unique_index("uq_users_phone", "users", ["phone", "region"])
.render(Dialect::MySql)
.join("\n");
assert!(uniq.to_uppercase().contains("UNIQUE"), "{uniq}");
let drop = Migration::new("c")
.drop_index("idx_users_phone", "users")
.render(Dialect::Sqlite)
.join("\n");
assert!(drop.to_uppercase().contains("DROP INDEX"), "{drop}");
}
#[test]
fn rename_and_drop_table() {
let rename = Migration::new("a")
.rename_table("old_users", "users")
.render(Dialect::MySql)
.join("\n");
assert!(rename.to_uppercase().contains("RENAME"), "{rename}");
let drop = Migration::new("b").drop_table("dead").render(Dialect::Sqlite).join("\n");
assert!(drop.to_uppercase().contains("DROP TABLE"), "{drop}");
assert!(drop.to_uppercase().contains("IF EXISTS"), "{drop}");
}
#[test]
fn statement_bridges_sea_query() {
use sea_query::{Alias, ColumnDef, Table};
let mut create = Table::create();
create
.table(Alias::new("widgets"))
.if_not_exists()
.col(ColumnDef::new(Alias::new("id")).big_integer().primary_key());
let m = Migration::new("0001").statement(create);
let sqlite = m.render(Dialect::Sqlite).join("\n");
assert!(sqlite.to_uppercase().contains("CREATE TABLE"), "{sqlite}");
assert!(sqlite.contains("widgets"));
assert!(m.render(Dialect::MySql).join("\n").contains('`'));
}
#[test]
fn raw_is_dialect_scoped() {
let m = Migration::new("a")
.raw(Dialect::MySql, "ALTER TABLE t MODIFY c TEXT")
.raw(Dialect::Sqlite, "-- noop on sqlite");
assert_eq!(m.render(Dialect::MySql), vec!["ALTER TABLE t MODIFY c TEXT"]);
assert_eq!(m.render(Dialect::Sqlite), vec!["-- noop on sqlite"]);
assert!(m.render(Dialect::Postgres).is_empty());
}
#[test]
fn ops_apply_in_call_order() {
let m = Migration::new("ordered")
.add_column("t", Column::new("a", ColumnType::Int))
.add_column("t", Column::new("b", ColumnType::Int));
let rendered = m.render(Dialect::Sqlite);
assert_eq!(rendered.len(), 2);
assert!(rendered[0].contains("\"a\""), "{:?}", rendered);
assert!(rendered[1].contains("\"b\""), "{:?}", rendered);
}
}