use super::column_builder::{ColumnDefinition, DefaultValue};
use crate::schema::{RustTypeMapping, TableSchema};
#[derive(Debug, Clone, PartialEq)]
pub enum Operation {
CreateTable(CreateTableOp),
DropTable(DropTableOp),
RenameTable(RenameTableOp),
AddColumn(AddColumnOp),
DropColumn(DropColumnOp),
AlterColumn(AlterColumnOp),
RenameColumn(RenameColumnOp),
CreateIndex(CreateIndexOp),
DropIndex(DropIndexOp),
AddForeignKey(AddForeignKeyOp),
DropForeignKey(DropForeignKeyOp),
RunSql(RawSqlOp),
}
impl Operation {
#[must_use]
pub fn drop_table(name: impl Into<String>) -> Self {
Self::DropTable(DropTableOp {
name: name.into(),
if_exists: false,
cascade: false,
})
}
#[must_use]
pub fn drop_table_if_exists(name: impl Into<String>) -> Self {
Self::DropTable(DropTableOp {
name: name.into(),
if_exists: true,
cascade: false,
})
}
#[must_use]
pub fn rename_table(old_name: impl Into<String>, new_name: impl Into<String>) -> Self {
Self::RenameTable(RenameTableOp {
old_name: old_name.into(),
new_name: new_name.into(),
})
}
#[must_use]
pub fn add_column(table: impl Into<String>, column: ColumnDefinition) -> Self {
Self::AddColumn(AddColumnOp {
table: table.into(),
column,
})
}
#[must_use]
pub fn drop_column(table: impl Into<String>, column: impl Into<String>) -> Self {
Self::DropColumn(DropColumnOp {
table: table.into(),
column: column.into(),
})
}
#[must_use]
pub fn rename_column(
table: impl Into<String>,
old_name: impl Into<String>,
new_name: impl Into<String>,
) -> Self {
Self::RenameColumn(RenameColumnOp {
table: table.into(),
old_name: old_name.into(),
new_name: new_name.into(),
})
}
#[must_use]
pub fn run_sql(sql: impl Into<String>) -> Self {
Self::RunSql(RawSqlOp {
up_sql: sql.into(),
down_sql: None,
})
}
#[must_use]
pub fn run_sql_reversible(up_sql: impl Into<String>, down_sql: impl Into<String>) -> Self {
Self::RunSql(RawSqlOp {
up_sql: up_sql.into(),
down_sql: Some(down_sql.into()),
})
}
#[must_use]
pub fn reverse(&self) -> Option<Self> {
match self {
Self::CreateTable(op) => Some(Self::drop_table(&op.name)),
Self::DropTable(_) => None, Self::RenameTable(op) => {
Some(Self::rename_table(op.new_name.clone(), op.old_name.clone()))
}
Self::AddColumn(op) => Some(Self::drop_column(&op.table, &op.column.name)),
Self::DropColumn(_) => None, Self::AlterColumn(_) => None, Self::RenameColumn(op) => Some(Self::rename_column(
&op.table,
op.new_name.clone(),
op.old_name.clone(),
)),
Self::CreateIndex(op) => Some(Self::DropIndex(DropIndexOp {
name: op.name.clone(),
table: Some(op.table.clone()),
if_exists: false,
})),
Self::DropIndex(_) => None, Self::AddForeignKey(op) => op.name.as_ref().map(|name| {
Self::DropForeignKey(DropForeignKeyOp {
table: op.table.clone(),
name: name.clone(),
})
}),
Self::DropForeignKey(_) => None, Self::RunSql(op) => op.down_sql.as_ref().map(|down| Self::run_sql(down.clone())),
}
}
#[must_use]
pub fn is_reversible(&self) -> bool {
self.reverse().is_some()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CreateTableOp {
pub name: String,
pub columns: Vec<ColumnDefinition>,
pub constraints: Vec<TableConstraint>,
pub if_not_exists: bool,
}
impl CreateTableOp {
pub fn from_table<T: TableSchema>(dialect: &impl RustTypeMapping) -> Self {
let columns = T::SCHEMA
.iter()
.map(|col| {
let inner = strip_option(col.rust_type);
let data_type = dialect.map_type(inner);
let mut def = ColumnDefinition::new(col.name, data_type);
def.nullable = col.nullable;
def.primary_key = col.primary_key;
def.unique = col.unique;
def.autoincrement = col.autoincrement;
if let Some(expr) = col.default_expr {
def.default = Some(DefaultValue::Expression(expr.to_string()));
}
def
})
.collect();
Self {
name: T::NAME.to_string(),
columns,
constraints: vec![],
if_not_exists: false,
}
}
pub fn from_table_if_not_exists<T: TableSchema>(dialect: &impl RustTypeMapping) -> Self {
let mut op = Self::from_table::<T>(dialect);
op.if_not_exists = true;
op
}
}
pub(super) fn strip_option(rust_type: &str) -> &str {
rust_type
.strip_prefix("Option<")
.and_then(|s| s.strip_suffix('>'))
.unwrap_or(rust_type)
}
impl From<CreateTableOp> for Operation {
fn from(op: CreateTableOp) -> Self {
Self::CreateTable(op)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TableConstraint {
PrimaryKey {
name: Option<String>,
columns: Vec<String>,
},
Unique {
name: Option<String>,
columns: Vec<String>,
},
ForeignKey {
name: Option<String>,
columns: Vec<String>,
references_table: String,
references_columns: Vec<String>,
on_delete: Option<super::column_builder::ForeignKeyAction>,
on_update: Option<super::column_builder::ForeignKeyAction>,
},
Check {
name: Option<String>,
expression: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DropTableOp {
pub name: String,
pub if_exists: bool,
pub cascade: bool,
}
impl From<DropTableOp> for Operation {
fn from(op: DropTableOp) -> Self {
Self::DropTable(op)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenameTableOp {
pub old_name: String,
pub new_name: String,
}
impl From<RenameTableOp> for Operation {
fn from(op: RenameTableOp) -> Self {
Self::RenameTable(op)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AddColumnOp {
pub table: String,
pub column: ColumnDefinition,
}
impl From<AddColumnOp> for Operation {
fn from(op: AddColumnOp) -> Self {
Self::AddColumn(op)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DropColumnOp {
pub table: String,
pub column: String,
}
impl From<DropColumnOp> for Operation {
fn from(op: DropColumnOp) -> Self {
Self::DropColumn(op)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AlterColumnChange {
SetDataType(crate::ast::DataType),
SetNullable(bool),
SetDefault(super::column_builder::DefaultValue),
DropDefault,
SetUnique(bool),
SetAutoincrement(bool),
}
#[derive(Debug, Clone, PartialEq)]
pub struct AlterColumnOp {
pub table: String,
pub column: String,
pub change: AlterColumnChange,
}
impl From<AlterColumnOp> for Operation {
fn from(op: AlterColumnOp) -> Self {
Self::AlterColumn(op)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenameColumnOp {
pub table: String,
pub old_name: String,
pub new_name: String,
}
impl From<RenameColumnOp> for Operation {
fn from(op: RenameColumnOp) -> Self {
Self::RenameColumn(op)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IndexType {
#[default]
BTree,
Hash,
Gist,
Gin,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateIndexOp {
pub name: String,
pub table: String,
pub columns: Vec<String>,
pub unique: bool,
pub index_type: IndexType,
pub if_not_exists: bool,
pub condition: Option<String>,
}
impl From<CreateIndexOp> for Operation {
fn from(op: CreateIndexOp) -> Self {
Self::CreateIndex(op)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DropIndexOp {
pub name: String,
pub table: Option<String>,
pub if_exists: bool,
}
impl From<DropIndexOp> for Operation {
fn from(op: DropIndexOp) -> Self {
Self::DropIndex(op)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddForeignKeyOp {
pub table: String,
pub name: Option<String>,
pub columns: Vec<String>,
pub references_table: String,
pub references_columns: Vec<String>,
pub on_delete: Option<super::column_builder::ForeignKeyAction>,
pub on_update: Option<super::column_builder::ForeignKeyAction>,
}
impl From<AddForeignKeyOp> for Operation {
fn from(op: AddForeignKeyOp) -> Self {
Self::AddForeignKey(op)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DropForeignKeyOp {
pub table: String,
pub name: String,
}
impl From<DropForeignKeyOp> for Operation {
fn from(op: DropForeignKeyOp) -> Self {
Self::DropForeignKey(op)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawSqlOp {
pub up_sql: String,
pub down_sql: Option<String>,
}
impl From<RawSqlOp> for Operation {
fn from(op: RawSqlOp) -> Self {
Self::RunSql(op)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::migrations::column_builder::{ForeignKeyAction, bigint, varchar};
#[test]
fn test_drop_table_operation() {
let op = Operation::drop_table("users");
match op {
Operation::DropTable(drop) => {
assert_eq!(drop.name, "users");
assert!(!drop.if_exists);
assert!(!drop.cascade);
}
_ => panic!("Expected DropTable operation"),
}
}
#[test]
fn test_rename_table_operation() {
let op = Operation::rename_table("old_name", "new_name");
match op {
Operation::RenameTable(rename) => {
assert_eq!(rename.old_name, "old_name");
assert_eq!(rename.new_name, "new_name");
}
_ => panic!("Expected RenameTable operation"),
}
}
#[test]
fn test_add_column_operation() {
let col = varchar("email", 255).not_null().build();
let op = Operation::add_column("users", col);
match op {
Operation::AddColumn(add) => {
assert_eq!(add.table, "users");
assert_eq!(add.column.name, "email");
}
_ => panic!("Expected AddColumn operation"),
}
}
#[test]
fn test_reverse_operations() {
let create = CreateTableOp {
name: "users".to_string(),
columns: vec![bigint("id").primary_key().build()],
constraints: vec![],
if_not_exists: false,
};
let op = Operation::CreateTable(create);
let reversed = op.reverse().expect("Should be reversible");
match reversed {
Operation::DropTable(drop) => assert_eq!(drop.name, "users"),
_ => panic!("Expected DropTable"),
}
let rename = Operation::rename_table("old", "new");
let reversed = rename.reverse().expect("Should be reversible");
match reversed {
Operation::RenameTable(r) => {
assert_eq!(r.old_name, "new");
assert_eq!(r.new_name, "old");
}
_ => panic!("Expected RenameTable"),
}
let add = Operation::add_column("users", varchar("email", 255).build());
let reversed = add.reverse().expect("Should be reversible");
match reversed {
Operation::DropColumn(drop) => {
assert_eq!(drop.table, "users");
assert_eq!(drop.column, "email");
}
_ => panic!("Expected DropColumn"),
}
let drop = Operation::drop_table("users");
assert!(drop.reverse().is_none());
}
#[test]
fn test_raw_sql_reversibility() {
let op = Operation::run_sql("INSERT INTO config VALUES ('key', 'value')");
assert!(!op.is_reversible());
let op = Operation::run_sql_reversible(
"INSERT INTO config VALUES ('key', 'value')",
"DELETE FROM config WHERE key = 'key'",
);
assert!(op.is_reversible());
}
#[test]
fn test_table_constraint() {
let pk = TableConstraint::PrimaryKey {
name: Some("pk_users".to_string()),
columns: vec!["id".to_string()],
};
match pk {
TableConstraint::PrimaryKey { name, columns } => {
assert_eq!(name, Some("pk_users".to_string()));
assert_eq!(columns, vec!["id"]);
}
_ => panic!("Expected PrimaryKey"),
}
let fk = TableConstraint::ForeignKey {
name: Some("fk_user_company".to_string()),
columns: vec!["company_id".to_string()],
references_table: "companies".to_string(),
references_columns: vec!["id".to_string()],
on_delete: Some(ForeignKeyAction::Cascade),
on_update: None,
};
match fk {
TableConstraint::ForeignKey {
references_table,
on_delete,
..
} => {
assert_eq!(references_table, "companies");
assert_eq!(on_delete, Some(ForeignKeyAction::Cascade));
}
_ => panic!("Expected ForeignKey"),
}
}
}