@@ -286,14 +286,18 @@ pub fn diff_schemas(from: &[TableDef], to: &[TableDef]) -> Result<MigrationPlan,
286286 . map ( |c| ( c. name . as_str ( ) , c) )
287287 . collect ( ) ;
288288
289- // Deleted columns
290- for col in from_cols. keys ( ) {
291- if !to_cols. contains_key ( col) {
292- actions. push ( MigrationAction :: DeleteColumn {
293- table : name. to_string ( ) ,
294- column : col. to_string ( ) ,
295- } ) ;
296- }
289+ // Deleted columns - collect the set of columns being deleted for this table
290+ let deleted_columns: BTreeSet < & str > = from_cols
291+ . keys ( )
292+ . filter ( |col| !to_cols. contains_key ( * col) )
293+ . copied ( )
294+ . collect ( ) ;
295+
296+ for col in & deleted_columns {
297+ actions. push ( MigrationAction :: DeleteColumn {
298+ table : name. to_string ( ) ,
299+ column : col. to_string ( ) ,
300+ } ) ;
297301 }
298302
299303 // Modified columns - type changes
@@ -365,12 +369,25 @@ pub fn diff_schemas(from: &[TableDef], to: &[TableDef]) -> Result<MigrationPlan,
365369 }
366370
367371 // Constraints - compare and detect additions/removals (includes indexes)
372+ // Skip RemoveConstraint for constraints where ALL columns are being deleted
373+ // (the constraint will be automatically dropped when the column is dropped)
368374 for from_constraint in & from_tbl. constraints {
369375 if !to_tbl. constraints . contains ( from_constraint) {
370- actions. push ( MigrationAction :: RemoveConstraint {
371- table : name. to_string ( ) ,
372- constraint : from_constraint. clone ( ) ,
373- } ) ;
376+ // Get the columns referenced by this constraint
377+ let constraint_columns = from_constraint. columns ( ) ;
378+
379+ // Skip if ALL columns of the constraint are being deleted
380+ let all_columns_deleted = !constraint_columns. is_empty ( )
381+ && constraint_columns
382+ . iter ( )
383+ . all ( |col| deleted_columns. contains ( col. as_str ( ) ) ) ;
384+
385+ if !all_columns_deleted {
386+ actions. push ( MigrationAction :: RemoveConstraint {
387+ table : name. to_string ( ) ,
388+ constraint : from_constraint. clone ( ) ,
389+ } ) ;
390+ }
374391 }
375392 }
376393 for to_constraint in & to_tbl. constraints {
@@ -3212,4 +3229,191 @@ mod tests {
32123229 ) ) ;
32133230 }
32143231 }
3232+
3233+ mod constraint_removal_on_deleted_columns {
3234+ use super :: * ;
3235+
3236+ fn fk ( columns : Vec < & str > , ref_table : & str , ref_columns : Vec < & str > ) -> TableConstraint {
3237+ TableConstraint :: ForeignKey {
3238+ name : None ,
3239+ columns : columns. into_iter ( ) . map ( |s| s. to_string ( ) ) . collect ( ) ,
3240+ ref_table : ref_table. to_string ( ) ,
3241+ ref_columns : ref_columns. into_iter ( ) . map ( |s| s. to_string ( ) ) . collect ( ) ,
3242+ on_delete : None ,
3243+ on_update : None ,
3244+ }
3245+ }
3246+
3247+ #[ test]
3248+ fn skip_remove_constraint_when_all_columns_deleted ( ) {
3249+ // When a column with FK and index is deleted, the constraints should NOT
3250+ // generate separate RemoveConstraint actions (they are dropped with the column)
3251+ let from = vec ! [ table(
3252+ "project" ,
3253+ vec![
3254+ col( "id" , ColumnType :: Simple ( SimpleColumnType :: Integer ) ) ,
3255+ col( "template_id" , ColumnType :: Simple ( SimpleColumnType :: Integer ) ) ,
3256+ ] ,
3257+ vec![
3258+ fk( vec![ "template_id" ] , "book_template" , vec![ "id" ] ) ,
3259+ idx( "ix_project__template_id" , vec![ "template_id" ] ) ,
3260+ ] ,
3261+ ) ] ;
3262+
3263+ let to = vec ! [ table(
3264+ "project" ,
3265+ vec![ col( "id" , ColumnType :: Simple ( SimpleColumnType :: Integer ) ) ] ,
3266+ vec![ ] ,
3267+ ) ] ;
3268+
3269+ let plan = diff_schemas ( & from, & to) . unwrap ( ) ;
3270+
3271+ // Should only have DeleteColumn, NO RemoveConstraint actions
3272+ assert_eq ! ( plan. actions. len( ) , 1 ) ;
3273+ assert ! ( matches!(
3274+ & plan. actions[ 0 ] ,
3275+ MigrationAction :: DeleteColumn { table, column }
3276+ if table == "project" && column == "template_id"
3277+ ) ) ;
3278+
3279+ // Explicitly verify no RemoveConstraint
3280+ let has_remove_constraint = plan
3281+ . actions
3282+ . iter ( )
3283+ . any ( |a| matches ! ( a, MigrationAction :: RemoveConstraint { .. } ) ) ;
3284+ assert ! (
3285+ !has_remove_constraint,
3286+ "Should NOT have RemoveConstraint when column is deleted"
3287+ ) ;
3288+ }
3289+
3290+ #[ test]
3291+ fn keep_remove_constraint_when_only_some_columns_deleted ( ) {
3292+ // If a composite constraint has some columns remaining, RemoveConstraint is needed
3293+ let from = vec ! [ table(
3294+ "orders" ,
3295+ vec![
3296+ col( "id" , ColumnType :: Simple ( SimpleColumnType :: Integer ) ) ,
3297+ col( "user_id" , ColumnType :: Simple ( SimpleColumnType :: Integer ) ) ,
3298+ col( "product_id" , ColumnType :: Simple ( SimpleColumnType :: Integer ) ) ,
3299+ ] ,
3300+ vec![ idx(
3301+ "ix_orders__user_product" ,
3302+ vec![ "user_id" , "product_id" ] ,
3303+ ) ] ,
3304+ ) ] ;
3305+
3306+ let to = vec ! [ table(
3307+ "orders" ,
3308+ vec![
3309+ col( "id" , ColumnType :: Simple ( SimpleColumnType :: Integer ) ) ,
3310+ col( "user_id" , ColumnType :: Simple ( SimpleColumnType :: Integer ) ) ,
3311+ // product_id is deleted, but user_id remains
3312+ ] ,
3313+ vec![ ] ,
3314+ ) ] ;
3315+
3316+ let plan = diff_schemas ( & from, & to) . unwrap ( ) ;
3317+
3318+ // Should have both DeleteColumn AND RemoveConstraint
3319+ // (because user_id is still there, the composite index needs explicit removal)
3320+ let has_delete_column = plan. actions . iter ( ) . any ( |a| {
3321+ matches ! (
3322+ a,
3323+ MigrationAction :: DeleteColumn { table, column }
3324+ if table == "orders" && column == "product_id"
3325+ )
3326+ } ) ;
3327+ assert ! ( has_delete_column, "Should have DeleteColumn for product_id" ) ;
3328+
3329+ let has_remove_constraint = plan. actions . iter ( ) . any ( |a| {
3330+ matches ! (
3331+ a,
3332+ MigrationAction :: RemoveConstraint { table, .. }
3333+ if table == "orders"
3334+ )
3335+ } ) ;
3336+ assert ! (
3337+ has_remove_constraint,
3338+ "Should have RemoveConstraint for composite index when only some columns deleted"
3339+ ) ;
3340+ }
3341+
3342+ #[ test]
3343+ fn skip_remove_constraint_when_all_composite_columns_deleted ( ) {
3344+ // If ALL columns of a composite constraint are deleted, skip RemoveConstraint
3345+ let from = vec ! [ table(
3346+ "orders" ,
3347+ vec![
3348+ col( "id" , ColumnType :: Simple ( SimpleColumnType :: Integer ) ) ,
3349+ col( "user_id" , ColumnType :: Simple ( SimpleColumnType :: Integer ) ) ,
3350+ col( "product_id" , ColumnType :: Simple ( SimpleColumnType :: Integer ) ) ,
3351+ ] ,
3352+ vec![ idx(
3353+ "ix_orders__user_product" ,
3354+ vec![ "user_id" , "product_id" ] ,
3355+ ) ] ,
3356+ ) ] ;
3357+
3358+ let to = vec ! [ table(
3359+ "orders" ,
3360+ vec![ col( "id" , ColumnType :: Simple ( SimpleColumnType :: Integer ) ) ] ,
3361+ vec![ ] ,
3362+ ) ] ;
3363+
3364+ let plan = diff_schemas ( & from, & to) . unwrap ( ) ;
3365+
3366+ // Should only have DeleteColumn actions, no RemoveConstraint
3367+ let delete_columns: Vec < _ > = plan
3368+ . actions
3369+ . iter ( )
3370+ . filter ( |a| matches ! ( a, MigrationAction :: DeleteColumn { .. } ) )
3371+ . collect ( ) ;
3372+ assert_eq ! (
3373+ delete_columns. len( ) ,
3374+ 2 ,
3375+ "Should have 2 DeleteColumn actions"
3376+ ) ;
3377+
3378+ let has_remove_constraint = plan
3379+ . actions
3380+ . iter ( )
3381+ . any ( |a| matches ! ( a, MigrationAction :: RemoveConstraint { .. } ) ) ;
3382+ assert ! (
3383+ !has_remove_constraint,
3384+ "Should NOT have RemoveConstraint when all composite columns deleted"
3385+ ) ;
3386+ }
3387+
3388+ #[ test]
3389+ fn keep_remove_constraint_when_no_columns_deleted ( ) {
3390+ // Normal case: constraint removed but columns remain
3391+ let from = vec ! [ table(
3392+ "users" ,
3393+ vec![
3394+ col( "id" , ColumnType :: Simple ( SimpleColumnType :: Integer ) ) ,
3395+ col( "email" , ColumnType :: Simple ( SimpleColumnType :: Text ) ) ,
3396+ ] ,
3397+ vec![ idx( "ix_users__email" , vec![ "email" ] ) ] ,
3398+ ) ] ;
3399+
3400+ let to = vec ! [ table(
3401+ "users" ,
3402+ vec![
3403+ col( "id" , ColumnType :: Simple ( SimpleColumnType :: Integer ) ) ,
3404+ col( "email" , ColumnType :: Simple ( SimpleColumnType :: Text ) ) ,
3405+ ] ,
3406+ vec![ ] , // Index removed but column remains
3407+ ) ] ;
3408+
3409+ let plan = diff_schemas ( & from, & to) . unwrap ( ) ;
3410+
3411+ assert_eq ! ( plan. actions. len( ) , 1 ) ;
3412+ assert ! ( matches!(
3413+ & plan. actions[ 0 ] ,
3414+ MigrationAction :: RemoveConstraint { table, .. }
3415+ if table == "users"
3416+ ) ) ;
3417+ }
3418+ }
32153419}
0 commit comments