use sea_query::{Alias, Index, Query, Table};
use vespertide_core::{ColumnType, TableConstraint, TableDef};
use super::helpers::{
build_drop_enum_type_sql, build_sqlite_temp_table_create, recreate_indexes_after_rebuild,
};
use super::rename_table::build_rename_table;
use super::types::{BuiltQuery, DatabaseBackend};
pub fn build_delete_column(
backend: &DatabaseBackend,
table: &str,
column: &str,
column_type: Option<&ColumnType>,
current_schema: &[TableDef],
pending_constraints: &[vespertide_core::TableConstraint],
) -> Vec<BuiltQuery> {
let mut stmts = Vec::new();
if *backend == DatabaseBackend::Sqlite
&& let Some(table_def) = current_schema.iter().find(|t| t.name == table)
{
if let Some(col_def) = table_def.columns.iter().find(|c| c.name == column)
&& let ColumnType::Complex(vespertide_core::ComplexColumnType::Enum { .. }) =
&col_def.r#type
{
return build_delete_column_sqlite_temp_table(
table,
column,
table_def,
column_type,
pending_constraints,
);
}
for constraint in &table_def.constraints {
match constraint {
TableConstraint::Check { expr, .. } => {
if expr.contains(&format!("\"{}\"", column)) || expr.contains(column) {
return build_delete_column_sqlite_temp_table(
table,
column,
table_def,
column_type,
pending_constraints,
);
}
continue;
}
_ if !constraint.columns().iter().any(|c| c == column) => continue,
TableConstraint::ForeignKey { .. } | TableConstraint::PrimaryKey { .. } => {
return build_delete_column_sqlite_temp_table(
table,
column,
table_def,
column_type,
pending_constraints,
);
}
TableConstraint::Unique { name, columns } => {
let index_name = vespertide_naming::build_unique_constraint_name(
table,
columns,
name.as_deref(),
);
let drop_idx = Index::drop()
.name(&index_name)
.table(Alias::new(table))
.to_owned();
stmts.push(BuiltQuery::DropIndex(Box::new(drop_idx)));
}
TableConstraint::Index { name, columns } => {
let index_name =
vespertide_naming::build_index_name(table, columns, name.as_deref());
let drop_idx = Index::drop()
.name(&index_name)
.table(Alias::new(table))
.to_owned();
stmts.push(BuiltQuery::DropIndex(Box::new(drop_idx)));
}
}
}
}
let stmt = Table::alter()
.table(Alias::new(table))
.drop_column(Alias::new(column))
.to_owned();
stmts.push(BuiltQuery::AlterTable(Box::new(stmt)));
if let Some(col_type) = column_type
&& let Some(drop_type_sql) = build_drop_enum_type_sql(table, col_type)
{
stmts.push(BuiltQuery::Raw(drop_type_sql));
}
stmts
}
fn build_delete_column_sqlite_temp_table(
table: &str,
column: &str,
table_def: &TableDef,
column_type: Option<&ColumnType>,
pending_constraints: &[vespertide_core::TableConstraint],
) -> Vec<BuiltQuery> {
let mut stmts = Vec::new();
let temp_table = format!("{}_temp", table);
let new_columns: Vec<_> = table_def
.columns
.iter()
.filter(|c| c.name != column)
.cloned()
.collect();
let new_constraints: Vec<_> = table_def
.constraints
.iter()
.filter(|c| {
if let TableConstraint::Check { expr, .. } = c {
return !expr.contains(&format!("\"{}\"", column)) && !expr.contains(column);
}
!c.columns().iter().any(|col| col == column)
})
.cloned()
.collect();
let create_query = build_sqlite_temp_table_create(
&DatabaseBackend::Sqlite,
&temp_table,
table,
&new_columns,
&new_constraints,
);
stmts.push(create_query);
let column_aliases: Vec<Alias> = new_columns.iter().map(|c| Alias::new(&c.name)).collect();
let mut select_query = Query::select();
for col_alias in &column_aliases {
select_query = select_query.column(col_alias.clone()).to_owned();
}
select_query = select_query.from(Alias::new(table)).to_owned();
let insert_stmt = Query::insert()
.into_table(Alias::new(&temp_table))
.columns(column_aliases.clone())
.select_from(select_query)
.unwrap()
.to_owned();
stmts.push(BuiltQuery::Insert(Box::new(insert_stmt)));
let drop_table = Table::drop().table(Alias::new(table)).to_owned();
stmts.push(BuiltQuery::DropTable(Box::new(drop_table)));
stmts.push(build_rename_table(&temp_table, table));
stmts.extend(recreate_indexes_after_rebuild(
table,
&new_constraints,
pending_constraints,
));
if let Some(col_type) = column_type
&& let Some(drop_type_sql) = build_drop_enum_type_sql(table, col_type)
{
stmts.push(BuiltQuery::Raw(drop_type_sql));
}
stmts
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sql::types::DatabaseBackend;
use insta::{assert_snapshot, with_settings};
use rstest::rstest;
use vespertide_core::{ColumnDef, ComplexColumnType, SimpleColumnType};
fn col(name: &str, ty: ColumnType) -> ColumnDef {
ColumnDef {
name: name.to_string(),
r#type: ty,
nullable: true,
default: None,
comment: None,
primary_key: None,
unique: None,
index: None,
foreign_key: None,
}
}
#[rstest]
#[case::delete_column_postgres(
"delete_column_postgres",
DatabaseBackend::Postgres,
&["ALTER TABLE \"users\" DROP COLUMN \"email\""]
)]
#[case::delete_column_mysql(
"delete_column_mysql",
DatabaseBackend::MySql,
&["ALTER TABLE `users` DROP COLUMN `email`"]
)]
#[case::delete_column_sqlite(
"delete_column_sqlite",
DatabaseBackend::Sqlite,
&["ALTER TABLE \"users\" DROP COLUMN \"email\""]
)]
fn test_delete_column(
#[case] title: &str,
#[case] backend: DatabaseBackend,
#[case] expected: &[&str],
) {
let result = build_delete_column(&backend, "users", "email", None, &[], &[]);
let sql = result[0].build(backend);
for exp in expected {
assert!(
sql.contains(exp),
"Expected SQL to contain '{}', got: {}",
exp,
sql
);
}
with_settings!({ snapshot_suffix => format!("delete_column_{}", title) }, {
assert_snapshot!(sql);
});
}
#[test]
fn test_delete_enum_column_postgres() {
use vespertide_core::EnumValues;
let enum_type = ColumnType::Complex(ComplexColumnType::Enum {
name: "status".into(),
values: EnumValues::String(vec!["active".into(), "inactive".into()]),
});
let result = build_delete_column(
&DatabaseBackend::Postgres,
"users",
"status",
Some(&enum_type),
&[],
&[],
);
assert_eq!(result.len(), 2);
let alter_sql = result[0].build(DatabaseBackend::Postgres);
assert!(alter_sql.contains("DROP COLUMN"));
let drop_type_sql = result[1].build(DatabaseBackend::Postgres);
assert!(drop_type_sql.contains("DROP TYPE \"users_status\""));
let drop_type_mysql = result[1].build(DatabaseBackend::MySql);
assert!(drop_type_mysql.is_empty());
}
#[test]
fn test_delete_non_enum_column_no_drop_type() {
let text_type = ColumnType::Simple(SimpleColumnType::Text);
let result = build_delete_column(
&DatabaseBackend::Postgres,
"users",
"name",
Some(&text_type),
&[],
&[],
);
assert_eq!(result.len(), 1);
}
#[test]
fn test_delete_column_sqlite_drops_unique_constraint_first() {
let schema = vec![TableDef {
name: "gift".into(),
description: None,
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
col("gift_code", ColumnType::Simple(SimpleColumnType::Text)),
],
constraints: vec![TableConstraint::Unique {
name: None,
columns: vec!["gift_code".into()],
}],
}];
let result = build_delete_column(
&DatabaseBackend::Sqlite,
"gift",
"gift_code",
None,
&schema,
&[],
);
assert_eq!(result.len(), 2);
let drop_index_sql = result[0].build(DatabaseBackend::Sqlite);
assert!(
drop_index_sql.contains("DROP INDEX"),
"Expected DROP INDEX, got: {}",
drop_index_sql
);
assert!(
drop_index_sql.contains("uq_gift__gift_code"),
"Expected index name uq_gift__gift_code, got: {}",
drop_index_sql
);
let drop_column_sql = result[1].build(DatabaseBackend::Sqlite);
assert!(
drop_column_sql.contains("DROP COLUMN"),
"Expected DROP COLUMN, got: {}",
drop_column_sql
);
}
#[test]
fn test_delete_column_sqlite_drops_index_constraint_first() {
let schema = vec![TableDef {
name: "users".into(),
description: None,
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
col("email", ColumnType::Simple(SimpleColumnType::Text)),
],
constraints: vec![TableConstraint::Index {
name: None,
columns: vec!["email".into()],
}],
}];
let result = build_delete_column(
&DatabaseBackend::Sqlite,
"users",
"email",
None,
&schema,
&[],
);
assert_eq!(result.len(), 2);
let drop_index_sql = result[0].build(DatabaseBackend::Sqlite);
assert!(drop_index_sql.contains("DROP INDEX"));
assert!(drop_index_sql.contains("ix_users__email"));
let drop_column_sql = result[1].build(DatabaseBackend::Sqlite);
assert!(drop_column_sql.contains("DROP COLUMN"));
}
#[test]
fn test_delete_column_postgres_does_not_drop_constraints() {
let schema = vec![TableDef {
name: "gift".into(),
description: None,
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
col("gift_code", ColumnType::Simple(SimpleColumnType::Text)),
],
constraints: vec![TableConstraint::Unique {
name: None,
columns: vec!["gift_code".into()],
}],
}];
let result = build_delete_column(
&DatabaseBackend::Postgres,
"gift",
"gift_code",
None,
&schema,
&[],
);
assert_eq!(result.len(), 1);
let drop_column_sql = result[0].build(DatabaseBackend::Postgres);
assert!(drop_column_sql.contains("DROP COLUMN"));
}
#[test]
fn test_delete_column_sqlite_with_named_unique_constraint() {
let schema = vec![TableDef {
name: "gift".into(),
description: None,
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
col("gift_code", ColumnType::Simple(SimpleColumnType::Text)),
],
constraints: vec![TableConstraint::Unique {
name: Some("gift_code".into()),
columns: vec!["gift_code".into()],
}],
}];
let result = build_delete_column(
&DatabaseBackend::Sqlite,
"gift",
"gift_code",
None,
&schema,
&[],
);
assert_eq!(result.len(), 2);
let drop_index_sql = result[0].build(DatabaseBackend::Sqlite);
assert!(
drop_index_sql.contains("uq_gift__gift_code"),
"Expected uq_gift__gift_code, got: {}",
drop_index_sql
);
}
#[test]
fn test_delete_column_sqlite_with_fk_uses_temp_table() {
let schema = vec![TableDef {
name: "gift".into(),
description: None,
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
col("sender_id", ColumnType::Simple(SimpleColumnType::BigInt)),
col("message", ColumnType::Simple(SimpleColumnType::Text)),
],
constraints: vec![TableConstraint::ForeignKey {
name: None,
columns: vec!["sender_id".into()],
ref_table: "user".into(),
ref_columns: vec!["id".into()],
on_delete: None,
on_update: None,
}],
}];
let result = build_delete_column(
&DatabaseBackend::Sqlite,
"gift",
"sender_id",
None,
&schema,
&[],
);
assert!(
result.len() >= 4,
"Expected at least 4 statements for temp table approach, got: {}",
result.len()
);
let all_sql: Vec<String> = result
.iter()
.map(|q| q.build(DatabaseBackend::Sqlite))
.collect();
let combined_sql = all_sql.join("\n");
assert!(
combined_sql.contains("CREATE TABLE") && combined_sql.contains("gift_temp"),
"Expected CREATE TABLE gift_temp, got: {}",
combined_sql
);
assert!(
!combined_sql.contains("\"sender_id\"") || combined_sql.contains("DROP TABLE"),
"New table should not contain sender_id column"
);
assert!(
combined_sql.contains("INSERT INTO"),
"Expected INSERT INTO for data copy, got: {}",
combined_sql
);
assert!(
combined_sql.contains("DROP TABLE") && combined_sql.contains("\"gift\""),
"Expected DROP TABLE gift, got: {}",
combined_sql
);
assert!(
combined_sql.contains("RENAME"),
"Expected RENAME for temp table, got: {}",
combined_sql
);
}
#[test]
fn test_delete_column_sqlite_with_fk_preserves_other_columns() {
let schema = vec![TableDef {
name: "gift".into(),
description: None,
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
col("sender_id", ColumnType::Simple(SimpleColumnType::BigInt)),
col("receiver_id", ColumnType::Simple(SimpleColumnType::BigInt)),
col("message", ColumnType::Simple(SimpleColumnType::Text)),
],
constraints: vec![
TableConstraint::ForeignKey {
name: None,
columns: vec!["sender_id".into()],
ref_table: "user".into(),
ref_columns: vec!["id".into()],
on_delete: None,
on_update: None,
},
TableConstraint::Index {
name: None,
columns: vec!["receiver_id".into()],
},
],
}];
let result = build_delete_column(
&DatabaseBackend::Sqlite,
"gift",
"sender_id",
None,
&schema,
&[],
);
let all_sql: Vec<String> = result
.iter()
.map(|q| q.build(DatabaseBackend::Sqlite))
.collect();
let combined_sql = all_sql.join("\n");
assert!(combined_sql.contains("\"id\""), "Should preserve id column");
assert!(
combined_sql.contains("\"receiver_id\""),
"Should preserve receiver_id column"
);
assert!(
combined_sql.contains("\"message\""),
"Should preserve message column"
);
assert!(
combined_sql.contains("CREATE INDEX") && combined_sql.contains("ix_gift__receiver_id"),
"Should recreate index on receiver_id, got: {}",
combined_sql
);
}
#[test]
fn test_delete_column_postgres_with_fk_does_not_use_temp_table() {
let schema = vec![TableDef {
name: "gift".into(),
description: None,
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
col("sender_id", ColumnType::Simple(SimpleColumnType::BigInt)),
],
constraints: vec![TableConstraint::ForeignKey {
name: None,
columns: vec!["sender_id".into()],
ref_table: "user".into(),
ref_columns: vec!["id".into()],
on_delete: None,
on_update: None,
}],
}];
let result = build_delete_column(
&DatabaseBackend::Postgres,
"gift",
"sender_id",
None,
&schema,
&[],
);
assert_eq!(
result.len(),
1,
"PostgreSQL should only have 1 statement, got: {}",
result.len()
);
let sql = result[0].build(DatabaseBackend::Postgres);
assert!(
sql.contains("DROP COLUMN"),
"Expected DROP COLUMN, got: {}",
sql
);
assert!(
!sql.contains("gift_temp"),
"PostgreSQL should not use temp table"
);
}
#[test]
fn test_delete_column_sqlite_with_pk_uses_temp_table() {
let schema = vec![TableDef {
name: "order_items".into(),
description: None,
columns: vec![
col("order_id", ColumnType::Simple(SimpleColumnType::Integer)),
col("product_id", ColumnType::Simple(SimpleColumnType::Integer)),
col("quantity", ColumnType::Simple(SimpleColumnType::Integer)),
],
constraints: vec![TableConstraint::PrimaryKey {
auto_increment: false,
columns: vec!["order_id".into(), "product_id".into()],
}],
}];
let result = build_delete_column(
&DatabaseBackend::Sqlite,
"order_items",
"product_id",
None,
&schema,
&[],
);
assert!(
result.len() >= 4,
"Expected at least 4 statements for temp table approach, got: {}",
result.len()
);
let all_sql: Vec<String> = result
.iter()
.map(|q| q.build(DatabaseBackend::Sqlite))
.collect();
let combined_sql = all_sql.join("\n");
assert!(
combined_sql.contains("order_items_temp"),
"Should use temp table approach for PK column deletion"
);
}
#[test]
fn test_delete_column_sqlite_unique_on_different_column_not_dropped() {
let schema = vec![TableDef {
name: "users".into(),
description: None,
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
col("email", ColumnType::Simple(SimpleColumnType::Text)),
col("nickname", ColumnType::Simple(SimpleColumnType::Text)),
],
constraints: vec![
TableConstraint::Unique {
name: None,
columns: vec!["email".into()],
},
],
}];
let result = build_delete_column(
&DatabaseBackend::Sqlite,
"users",
"nickname",
None,
&schema,
&[],
);
assert_eq!(
result.len(),
1,
"Should not drop UNIQUE on email when deleting nickname, got: {} statements",
result.len()
);
let sql = result[0].build(DatabaseBackend::Sqlite);
assert!(
sql.contains("DROP COLUMN"),
"Expected DROP COLUMN, got: {}",
sql
);
assert!(
!sql.contains("DROP INDEX"),
"Should NOT drop the email UNIQUE constraint when deleting nickname"
);
}
#[test]
fn test_delete_column_sqlite_temp_table_filters_constraints_correctly() {
let schema = vec![TableDef {
name: "orders".into(),
description: None,
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
col("user_id", ColumnType::Simple(SimpleColumnType::BigInt)),
col("status", ColumnType::Simple(SimpleColumnType::Text)),
col(
"created_at",
ColumnType::Simple(SimpleColumnType::Timestamp),
),
],
constraints: vec![
TableConstraint::ForeignKey {
name: None,
columns: vec!["user_id".into()],
ref_table: "users".into(),
ref_columns: vec!["id".into()],
on_delete: None,
on_update: None,
},
TableConstraint::Index {
name: None,
columns: vec!["created_at".into()],
},
TableConstraint::ForeignKey {
name: None,
columns: vec!["status".into()],
ref_table: "statuses".into(),
ref_columns: vec!["code".into()],
on_delete: None,
on_update: None,
},
],
}];
let result = build_delete_column(
&DatabaseBackend::Sqlite,
"orders",
"user_id",
None,
&schema,
&[],
);
let all_sql: Vec<String> = result
.iter()
.map(|q| q.build(DatabaseBackend::Sqlite))
.collect();
let combined_sql = all_sql.join("\n");
assert!(
combined_sql.contains("orders_temp"),
"Should use temp table approach for FK column deletion"
);
assert!(
combined_sql.contains("ix_orders__created_at"),
"Index on created_at should be recreated, got: {}",
combined_sql
);
assert!(
combined_sql.contains("REFERENCES \"statuses\""),
"FK on status should be preserved, got: {}",
combined_sql
);
let fk_patterns = combined_sql.matches("REFERENCES").count();
assert_eq!(
fk_patterns, 1,
"Only the FK on status should exist (not the one on user_id), got: {}",
combined_sql
);
}
fn build_sql_snapshot(result: &[BuiltQuery], backend: DatabaseBackend) -> String {
result
.iter()
.map(|q| q.build(backend))
.collect::<Vec<_>>()
.join(";\n")
}
#[rstest]
#[case::postgres("postgres", DatabaseBackend::Postgres)]
#[case::mysql("mysql", DatabaseBackend::MySql)]
#[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
fn test_delete_column_with_unique_constraint(
#[case] title: &str,
#[case] backend: DatabaseBackend,
) {
let schema = vec![TableDef {
name: "users".into(),
description: None,
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
col("email", ColumnType::Simple(SimpleColumnType::Text)),
col("name", ColumnType::Simple(SimpleColumnType::Text)),
],
constraints: vec![TableConstraint::Unique {
name: None,
columns: vec!["email".into()],
}],
}];
let result = build_delete_column(&backend, "users", "email", None, &schema, &[]);
let sql = build_sql_snapshot(&result, backend);
with_settings!({ snapshot_suffix => format!("delete_column_with_unique_{}", title) }, {
assert_snapshot!(sql);
});
}
#[rstest]
#[case::postgres("postgres", DatabaseBackend::Postgres)]
#[case::mysql("mysql", DatabaseBackend::MySql)]
#[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
fn test_delete_column_with_index_constraint(
#[case] title: &str,
#[case] backend: DatabaseBackend,
) {
let schema = vec![TableDef {
name: "posts".into(),
description: None,
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
col(
"created_at",
ColumnType::Simple(SimpleColumnType::Timestamp),
),
col("title", ColumnType::Simple(SimpleColumnType::Text)),
],
constraints: vec![TableConstraint::Index {
name: None,
columns: vec!["created_at".into()],
}],
}];
let result = build_delete_column(&backend, "posts", "created_at", None, &schema, &[]);
let sql = build_sql_snapshot(&result, backend);
with_settings!({ snapshot_suffix => format!("delete_column_with_index_{}", title) }, {
assert_snapshot!(sql);
});
}
#[rstest]
#[case::postgres("postgres", DatabaseBackend::Postgres)]
#[case::mysql("mysql", DatabaseBackend::MySql)]
#[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
fn test_delete_column_with_fk_constraint(
#[case] title: &str,
#[case] backend: DatabaseBackend,
) {
let schema = vec![TableDef {
name: "orders".into(),
description: None,
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
col("user_id", ColumnType::Simple(SimpleColumnType::BigInt)),
col("total", ColumnType::Simple(SimpleColumnType::Integer)),
],
constraints: vec![TableConstraint::ForeignKey {
name: None,
columns: vec!["user_id".into()],
ref_table: "users".into(),
ref_columns: vec!["id".into()],
on_delete: None,
on_update: None,
}],
}];
let result = build_delete_column(&backend, "orders", "user_id", None, &schema, &[]);
let sql = build_sql_snapshot(&result, backend);
with_settings!({ snapshot_suffix => format!("delete_column_with_fk_{}", title) }, {
assert_snapshot!(sql);
});
}
#[rstest]
#[case::postgres("postgres", DatabaseBackend::Postgres)]
#[case::mysql("mysql", DatabaseBackend::MySql)]
#[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
fn test_delete_column_with_pk_constraint(
#[case] title: &str,
#[case] backend: DatabaseBackend,
) {
let schema = vec![TableDef {
name: "order_items".into(),
description: None,
columns: vec![
col("order_id", ColumnType::Simple(SimpleColumnType::Integer)),
col("product_id", ColumnType::Simple(SimpleColumnType::Integer)),
col("quantity", ColumnType::Simple(SimpleColumnType::Integer)),
],
constraints: vec![TableConstraint::PrimaryKey {
auto_increment: false,
columns: vec!["order_id".into(), "product_id".into()],
}],
}];
let result = build_delete_column(&backend, "order_items", "product_id", None, &schema, &[]);
let sql = build_sql_snapshot(&result, backend);
with_settings!({ snapshot_suffix => format!("delete_column_with_pk_{}", title) }, {
assert_snapshot!(sql);
});
}
#[rstest]
#[case::postgres("postgres", DatabaseBackend::Postgres)]
#[case::mysql("mysql", DatabaseBackend::MySql)]
#[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
fn test_delete_column_with_fk_and_index_constraints(
#[case] title: &str,
#[case] backend: DatabaseBackend,
) {
let schema = vec![TableDef {
name: "orders".into(),
description: None,
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
col("user_id", ColumnType::Simple(SimpleColumnType::BigInt)),
col(
"created_at",
ColumnType::Simple(SimpleColumnType::Timestamp),
),
col("total", ColumnType::Simple(SimpleColumnType::Integer)),
],
constraints: vec![
TableConstraint::ForeignKey {
name: None,
columns: vec!["user_id".into()],
ref_table: "users".into(),
ref_columns: vec!["id".into()],
on_delete: None,
on_update: None,
},
TableConstraint::Index {
name: None,
columns: vec!["created_at".into()],
},
],
}];
let result = build_delete_column(&backend, "orders", "user_id", None, &schema, &[]);
let sql = build_sql_snapshot(&result, backend);
with_settings!({ snapshot_suffix => format!("delete_column_with_fk_and_index_{}", title) }, {
assert_snapshot!(sql);
});
}
#[test]
fn test_delete_column_sqlite_temp_table_with_enum_column() {
use vespertide_core::EnumValues;
let enum_type = ColumnType::Complex(ComplexColumnType::Enum {
name: "order_status".into(),
values: EnumValues::String(vec![
"pending".into(),
"shipped".into(),
"delivered".into(),
]),
});
let schema = vec![TableDef {
name: "orders".into(),
description: None,
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
col("user_id", ColumnType::Simple(SimpleColumnType::BigInt)),
col("status", enum_type.clone()),
],
constraints: vec![TableConstraint::ForeignKey {
name: None,
columns: vec!["user_id".into()],
ref_table: "users".into(),
ref_columns: vec!["id".into()],
on_delete: None,
on_update: None,
}],
}];
let result = build_delete_column(
&DatabaseBackend::Sqlite,
"orders",
"user_id",
Some(&enum_type),
&schema,
&[],
);
assert!(
result.len() >= 4,
"Expected at least 4 statements for temp table approach, got: {}",
result.len()
);
let all_sql: Vec<String> = result
.iter()
.map(|q| q.build(DatabaseBackend::Sqlite))
.collect();
let combined_sql = all_sql.join("\n");
assert!(
combined_sql.contains("orders_temp"),
"Should use temp table approach"
);
let last_stmt = result.last().unwrap();
let last_sql = last_stmt.build(DatabaseBackend::Sqlite);
assert!(
last_sql.is_empty() || !last_sql.contains("DROP TYPE"),
"SQLite should not emit DROP TYPE"
);
let pg_last_sql = last_stmt.build(DatabaseBackend::Postgres);
assert!(
pg_last_sql.contains("DROP TYPE"),
"PostgreSQL should emit DROP TYPE, got: {}",
pg_last_sql
);
}
#[test]
fn test_delete_column_sqlite_with_check_constraint_referencing_column() {
let schema = vec![TableDef {
name: "orders".into(),
description: None,
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
col("amount", ColumnType::Simple(SimpleColumnType::Integer)),
],
constraints: vec![TableConstraint::Check {
name: "check_positive".into(),
expr: "amount > 0".into(),
}],
}];
let result = build_delete_column(
&DatabaseBackend::Sqlite,
"orders",
"amount",
None,
&schema,
&[],
);
assert!(
result.len() >= 4,
"Expected temp table approach (>=4 stmts), got: {} statements",
result.len()
);
let sql = result[0].build(DatabaseBackend::Sqlite);
assert!(
sql.contains("orders_temp"),
"Expected temp table creation, got: {}",
sql
);
assert!(
!sql.contains("check_positive"),
"CHECK referencing deleted column should be removed, got: {}",
sql
);
}
#[test]
fn test_delete_column_sqlite_with_check_constraint_not_referencing_column() {
let schema = vec![TableDef {
name: "orders".into(),
description: None,
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
col("amount", ColumnType::Simple(SimpleColumnType::Integer)),
col("note", ColumnType::Simple(SimpleColumnType::Text)),
],
constraints: vec![TableConstraint::Check {
name: "check_positive".into(),
expr: "amount > 0".into(),
}],
}];
let result = build_delete_column(
&DatabaseBackend::Sqlite,
"orders",
"note",
None,
&schema,
&[],
);
assert_eq!(
result.len(),
1,
"Unrelated CHECK should be skipped, got: {} statements",
result.len()
);
let sql = result[0].build(DatabaseBackend::Sqlite);
assert!(
sql.contains("DROP COLUMN"),
"Expected DROP COLUMN, got: {}",
sql
);
}
}