Skip to content

Commit b95351a

Browse files
committed
Add testcase
1 parent 2729faa commit b95351a

2 files changed

Lines changed: 216 additions & 22 deletions

File tree

crates/vespertide-cli/src/commands/revision.rs

Lines changed: 215 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,42 @@ fn rewrite_plan_for_recreation(
526526
}
527527
}
528528

529+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
530+
enum RecreateHandling {
531+
NotNeeded,
532+
Rewritten,
533+
PlanEmptied,
534+
}
535+
536+
fn handle_recreate_requirements<F>(
537+
plan: &mut MigrationPlan,
538+
current_models: &[TableDef],
539+
prompt_fn: F,
540+
) -> Result<RecreateHandling>
541+
where
542+
F: Fn(&[RecreateTableRequired]) -> Result<bool>,
543+
{
544+
let recreate_tables = find_non_nullable_fk_add_columns(plan, current_models);
545+
if recreate_tables.is_empty() {
546+
return Ok(RecreateHandling::NotNeeded);
547+
}
548+
549+
if !prompt_fn(&recreate_tables)? {
550+
anyhow::bail!(
551+
"Migration cancelled. To proceed without recreation, make the column nullable \
552+
or add it with a default value that references an existing row."
553+
);
554+
}
555+
556+
rewrite_plan_for_recreation(plan, &recreate_tables, current_models);
557+
558+
if plan.actions.is_empty() {
559+
return Ok(RecreateHandling::PlanEmptied);
560+
}
561+
562+
Ok(RecreateHandling::Rewritten)
563+
}
564+
529565
pub async fn cmd_revision(message: String, fill_with_args: Vec<String>) -> Result<()> {
530566
let config = load_config()?;
531567
let current_models = load_models(&config)?;
@@ -543,20 +579,10 @@ pub async fn cmd_revision(message: String, fill_with_args: Vec<String>) -> Resul
543579
return Ok(());
544580
}
545581

546-
// Check for non-nullable FK columns being added to existing tables.
547-
// These require table recreation because existing rows can't satisfy the FK constraint.
548-
let recreate_tables = find_non_nullable_fk_add_columns(&plan, &current_models);
549-
if !recreate_tables.is_empty() {
550-
if !prompt_recreate_tables(&recreate_tables)? {
551-
anyhow::bail!(
552-
"Migration cancelled. To proceed without recreation, make the column nullable \
553-
or add it with a default value that references an existing row."
554-
);
555-
}
556-
rewrite_plan_for_recreation(&mut plan, &recreate_tables, &current_models);
557-
558-
// Re-check: if plan is now empty after recreation rewrite, nothing to do
559-
if plan.actions.is_empty() {
582+
// Check for non-nullable FK changes that require table recreation.
583+
match handle_recreate_requirements(&mut plan, &current_models, prompt_recreate_tables)? {
584+
RecreateHandling::NotNeeded | RecreateHandling::Rewritten => {}
585+
RecreateHandling::PlanEmptied => {
560586
println!(
561587
"{} {}",
562588
"No changes detected.".bright_yellow(),
@@ -1098,6 +1124,181 @@ mod tests {
10981124
);
10991125
}
11001126

1127+
#[test]
1128+
fn rewrite_plan_keeps_non_table_actions() {
1129+
use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType};
1130+
1131+
let mut plan = MigrationPlan {
1132+
id: String::new(),
1133+
comment: None,
1134+
created_at: None,
1135+
version: 2,
1136+
actions: vec![
1137+
MigrationAction::RawSql {
1138+
sql: "select 1".into(),
1139+
},
1140+
MigrationAction::AddColumn {
1141+
table: "post".into(),
1142+
column: Box::new(ColumnDef {
1143+
name: "user_id".into(),
1144+
r#type: ColumnType::Simple(SimpleColumnType::Uuid),
1145+
nullable: false,
1146+
default: None,
1147+
comment: None,
1148+
primary_key: None,
1149+
unique: None,
1150+
index: None,
1151+
foreign_key: None,
1152+
}),
1153+
fill_with: None,
1154+
},
1155+
],
1156+
};
1157+
1158+
let recreate = vec![RecreateTableRequired {
1159+
table: "post".into(),
1160+
column: "user_id".into(),
1161+
reason: RecreateReason::AddColumnWithFk,
1162+
}];
1163+
1164+
let models = vec![TableDef {
1165+
name: "post".into(),
1166+
description: None,
1167+
columns: vec![ColumnDef {
1168+
name: "user_id".into(),
1169+
r#type: ColumnType::Simple(SimpleColumnType::Uuid),
1170+
nullable: false,
1171+
default: None,
1172+
comment: None,
1173+
primary_key: None,
1174+
unique: None,
1175+
index: None,
1176+
foreign_key: None,
1177+
}],
1178+
constraints: vec![],
1179+
}];
1180+
1181+
rewrite_plan_for_recreation(&mut plan, &recreate, &models);
1182+
1183+
assert!(matches!(&plan.actions[0], MigrationAction::RawSql { sql } if sql == "select 1"));
1184+
assert!(
1185+
matches!(&plan.actions[1], MigrationAction::DeleteTable { table } if table == "post")
1186+
);
1187+
assert!(
1188+
matches!(&plan.actions[2], MigrationAction::CreateTable { table, .. } if table == "post")
1189+
);
1190+
}
1191+
1192+
#[test]
1193+
fn handle_recreate_requirements_returns_not_needed() {
1194+
let mut plan = MigrationPlan {
1195+
id: String::new(),
1196+
comment: None,
1197+
created_at: None,
1198+
version: 1,
1199+
actions: vec![MigrationAction::RawSql {
1200+
sql: "select 1".into(),
1201+
}],
1202+
};
1203+
1204+
let result = handle_recreate_requirements(&mut plan, &[], |_| Ok(true)).unwrap();
1205+
1206+
assert_eq!(result, RecreateHandling::NotNeeded);
1207+
assert_eq!(plan.actions.len(), 1);
1208+
}
1209+
1210+
#[test]
1211+
fn handle_recreate_requirements_bails_when_prompt_rejected() {
1212+
use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType};
1213+
1214+
let mut plan = MigrationPlan {
1215+
id: String::new(),
1216+
comment: None,
1217+
created_at: None,
1218+
version: 1,
1219+
actions: vec![
1220+
MigrationAction::AddColumn {
1221+
table: "post".into(),
1222+
column: Box::new(ColumnDef {
1223+
name: "user_id".into(),
1224+
r#type: ColumnType::Simple(SimpleColumnType::Uuid),
1225+
nullable: false,
1226+
default: None,
1227+
comment: None,
1228+
primary_key: None,
1229+
unique: None,
1230+
index: None,
1231+
foreign_key: None,
1232+
}),
1233+
fill_with: None,
1234+
},
1235+
MigrationAction::AddConstraint {
1236+
table: "post".into(),
1237+
constraint: TableConstraint::ForeignKey {
1238+
name: None,
1239+
columns: vec!["user_id".into()],
1240+
ref_table: "user".into(),
1241+
ref_columns: vec!["id".into()],
1242+
on_delete: None,
1243+
on_update: None,
1244+
},
1245+
},
1246+
],
1247+
};
1248+
1249+
let err = handle_recreate_requirements(&mut plan, &[], |_| Ok(false)).unwrap_err();
1250+
1251+
assert!(
1252+
err.to_string()
1253+
.contains("Migration cancelled. To proceed without recreation")
1254+
);
1255+
}
1256+
1257+
#[test]
1258+
fn handle_recreate_requirements_returns_plan_emptied_when_model_missing() {
1259+
use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType};
1260+
1261+
let mut plan = MigrationPlan {
1262+
id: String::new(),
1263+
comment: None,
1264+
created_at: None,
1265+
version: 1,
1266+
actions: vec![
1267+
MigrationAction::AddColumn {
1268+
table: "post".into(),
1269+
column: Box::new(ColumnDef {
1270+
name: "user_id".into(),
1271+
r#type: ColumnType::Simple(SimpleColumnType::Uuid),
1272+
nullable: false,
1273+
default: None,
1274+
comment: None,
1275+
primary_key: None,
1276+
unique: None,
1277+
index: None,
1278+
foreign_key: None,
1279+
}),
1280+
fill_with: None,
1281+
},
1282+
MigrationAction::AddConstraint {
1283+
table: "post".into(),
1284+
constraint: TableConstraint::ForeignKey {
1285+
name: None,
1286+
columns: vec!["user_id".into()],
1287+
ref_table: "user".into(),
1288+
ref_columns: vec!["id".into()],
1289+
on_delete: None,
1290+
on_update: None,
1291+
},
1292+
},
1293+
],
1294+
};
1295+
1296+
let result = handle_recreate_requirements(&mut plan, &[], |_| Ok(true)).unwrap();
1297+
1298+
assert_eq!(result, RecreateHandling::PlanEmptied);
1299+
assert!(plan.actions.is_empty());
1300+
}
1301+
11011302
#[test]
11021303
fn test_parse_fill_with_args() {
11031304
let args = vec![

crates/vespertide-query/src/sql/modify_column_type.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -193,14 +193,7 @@ pub fn build_modify_column_type(
193193
}
194194

195195
// 3. ALTER TABLE ... ALTER COLUMN ... TYPE target_type USING col::text::target_type
196-
queries.push(BuiltQuery::Raw(super::types::RawSql::per_backend(
197-
format!(
198-
"ALTER TABLE \"{}\" ALTER COLUMN \"{}\" TYPE \"{}\" USING \"{}\"::text::\"{}\"",
199-
table, column, target_type_name, column, target_type_name
200-
),
201-
String::new(),
202-
String::new(),
203-
)));
196+
queries.push(BuiltQuery::Raw(super::types::RawSql::per_backend(format!("ALTER TABLE \"{}\" ALTER COLUMN \"{}\" TYPE \"{}\" USING \"{}\"::text::\"{}\"", table, column, target_type_name, column, target_type_name), String::new(), String::new())));
204197

205198
// 4. DROP old enum type
206199
queries.push(BuiltQuery::Raw(super::types::RawSql::per_backend(

0 commit comments

Comments
 (0)