Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changepacks/changepack_log_eaCfY8efty1cg1T-dikgX.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"changes":{"crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-naming/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch"},"note":"Fix change enum with default","date":"2026-01-08T08:13:13.219036100Z"}
153 changes: 149 additions & 4 deletions crates/vespertide-query/src/sql/modify_column_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use sea_query::{Alias, ColumnDef as SeaColumnDef, Query, Table};
use vespertide_core::{ColumnType, ComplexColumnType, TableDef};

use super::create_table::build_create_table_for_backend;
use super::helpers::{apply_column_type_with_table, build_create_enum_type_sql};
use super::helpers::{
apply_column_type_with_table, build_create_enum_type_sql, convert_default_for_backend,
};
use super::rename_table::build_rename_table;
use super::types::{BuiltQuery, DatabaseBackend};
use crate::error::QueryError;
Expand Down Expand Up @@ -132,6 +134,13 @@ pub fn build_modify_column_type(
let type_name = super::helpers::build_enum_type_name(table, enum_name);
let temp_type_name = format!("{}_new", type_name);

// Check if column has a DEFAULT value that needs to be handled
let column_default = current_schema
.iter()
.find(|t| t.name == table)
.and_then(|t| t.columns.iter().find(|c| c.name == column))
.and_then(|c| c.default.clone());

// 1. CREATE TYPE {table}_{enum}_new AS ENUM (new values)
let create_temp_values = new_values.to_sql_values().join(", ");
queries.push(BuiltQuery::Raw(super::types::RawSql::per_backend(
Expand All @@ -143,17 +152,29 @@ pub fn build_modify_column_type(
String::new(),
)));

// 2. ALTER TABLE ... ALTER COLUMN ... TYPE {table}_{enum}_new USING {column}::text::{table}_{enum}_new
// 2. DROP DEFAULT if exists (must be done before type change)
if column_default.is_some() {
queries.push(BuiltQuery::Raw(super::types::RawSql::per_backend(
format!(
"ALTER TABLE \"{}\" ALTER COLUMN \"{}\" DROP DEFAULT",
table, column
),
String::new(),
String::new(),
)));
}

// 3. ALTER TABLE ... ALTER COLUMN ... TYPE {table}_{enum}_new USING {column}::text::{table}_{enum}_new
queries.push(BuiltQuery::Raw(super::types::RawSql::per_backend(format!("ALTER TABLE \"{}\" ALTER COLUMN \"{}\" TYPE \"{}\" USING \"{}\"::text::\"{}\"", table, column, temp_type_name, column, temp_type_name), String::new(), String::new())));

// 3. DROP TYPE {table}_{enum}
// 4. DROP TYPE {table}_{enum}
queries.push(BuiltQuery::Raw(super::types::RawSql::per_backend(
format!("DROP TYPE \"{}\"", type_name),
String::new(),
String::new(),
)));

// 4. ALTER TYPE {table}_{enum}_new RENAME TO {table}_{enum}
// 5. ALTER TYPE {table}_{enum}_new RENAME TO {table}_{enum}
queries.push(BuiltQuery::Raw(super::types::RawSql::per_backend(
format!(
"ALTER TYPE \"{}\" RENAME TO \"{}\"",
Expand All @@ -162,6 +183,20 @@ pub fn build_modify_column_type(
String::new(),
String::new(),
)));

// 6. Restore DEFAULT if it existed
if let Some(default_value) = column_default {
queries.push(BuiltQuery::Raw(super::types::RawSql::per_backend(
format!(
"ALTER TABLE \"{}\" ALTER COLUMN \"{}\" SET DEFAULT {}",
table,
column,
default_value.to_sql()
),
String::new(),
String::new(),
)));
}
}
} else {
// Standard column type modification
Expand Down Expand Up @@ -192,6 +227,24 @@ pub fn build_modify_column_type(
let mut col = SeaColumnDef::new(Alias::new(column));
apply_column_type_with_table(&mut col, new_type, table);

// MySQL MODIFY COLUMN redefines the entire column, so we must preserve
// existing NOT NULL and DEFAULT attributes
if *backend == DatabaseBackend::MySql
&& let Some(column_def) = current_schema
.iter()
.find(|t| t.name == table)
.and_then(|t| t.columns.iter().find(|c| c.name == column))
{
if !column_def.nullable {
col.not_null();
}
if let Some(default) = &column_def.default {
let default_str = default.to_sql();
let converted = convert_default_for_backend(&default_str, backend);
col.default(sea_query::Expr::cust(converted));
}
}

let stmt = Table::alter()
.table(Alias::new(table))
.modify_column(col)
Expand Down Expand Up @@ -724,6 +777,98 @@ mod tests {
});
}

#[rstest]
#[case::modify_enum_with_default_postgres(
"modify_enum_with_default_postgres",
DatabaseBackend::Postgres
)]
#[case::modify_enum_with_default_mysql(
"modify_enum_with_default_mysql",
DatabaseBackend::MySql
)]
#[case::modify_enum_with_default_sqlite(
"modify_enum_with_default_sqlite",
DatabaseBackend::Sqlite
)]
fn test_modify_enum_with_default_value(#[case] title: &str, #[case] backend: DatabaseBackend) {
// Test that enum type change handles DEFAULT values correctly
// PostgreSQL requires: DROP DEFAULT -> change type -> SET DEFAULT
let current_schema = vec![TableDef {
name: "reservation_session".into(),
description: None,
columns: vec![ColumnDef {
name: "status".into(),
r#type: ColumnType::Complex(ComplexColumnType::Enum {
name: "session_status".into(),
values: EnumValues::String(vec!["pending".into(), "confirmed".into()]),
}),
nullable: false,
default: Some("'pending'".into()),
comment: None,
primary_key: None,
unique: None,
index: None,
foreign_key: None,
}],
constraints: vec![],
}];

let new_type = ColumnType::Complex(ComplexColumnType::Enum {
name: "session_status".into(),
values: EnumValues::String(vec![
"pending".into(),
"confirmed".into(),
"cancelled".into(),
]),
});

let result = build_modify_column_type(
&backend,
"reservation_session",
"status",
&new_type,
&current_schema,
)
.unwrap();

let sql = result
.iter()
.map(|q| q.build(backend))
.collect::<Vec<_>>()
.join(";\n");

// PostgreSQL-specific: verify DROP DEFAULT -> TYPE change -> SET DEFAULT order
if matches!(backend, DatabaseBackend::Postgres) {
assert!(
sql.contains("DROP DEFAULT"),
"Should drop default before type change. SQL: {}",
sql
);
assert!(
sql.contains("SET DEFAULT"),
"Should restore default after type change. SQL: {}",
sql
);

let drop_default_pos = sql.find("DROP DEFAULT").unwrap();
let type_change_pos = sql.find("USING").unwrap();
let set_default_pos = sql.find("SET DEFAULT").unwrap();

assert!(
drop_default_pos < type_change_pos,
"DROP DEFAULT should come before TYPE change"
);
assert!(
type_change_pos < set_default_pos,
"SET DEFAULT should come after TYPE change"
);
}

with_settings!({ snapshot_suffix => format!("modify_enum_with_default_{}", title) }, {
assert_snapshot!(sql);
});
}

#[test]
fn test_modify_column_type_to_enum_with_empty_schema() {
// Test the None branch in line 195-200
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: crates/vespertide-query/src/sql/modify_column_type.rs
expression: sql
---
ALTER TABLE `reservation_session` MODIFY COLUMN `status` ENUM('pending', 'confirmed', 'cancelled') NOT NULL DEFAULT 'pending'
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
source: crates/vespertide-query/src/sql/modify_column_type.rs
expression: sql
---
CREATE TYPE "reservation_session_session_status_new" AS ENUM ('pending', 'confirmed', 'cancelled');
ALTER TABLE "reservation_session" ALTER COLUMN "status" DROP DEFAULT;
ALTER TABLE "reservation_session" ALTER COLUMN "status" TYPE "reservation_session_session_status_new" USING "status"::text::"reservation_session_session_status_new";
DROP TYPE "reservation_session_session_status";
ALTER TYPE "reservation_session_session_status_new" RENAME TO "reservation_session_session_status";
ALTER TABLE "reservation_session" ALTER COLUMN "status" SET DEFAULT 'pending'
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
source: crates/vespertide-query/src/sql/modify_column_type.rs
expression: sql
---
CREATE TABLE "reservation_session_temp" ( "status" enum_text NOT NULL DEFAULT 'pending' );
INSERT INTO "reservation_session_temp" ("status") SELECT "status" FROM "reservation_session";
DROP TABLE "reservation_session";
ALTER TABLE "reservation_session_temp" RENAME TO "reservation_session"