1use sea_query::{Alias, Index};
2
3use vespertide_core::{ColumnType, ComplexColumnType, TableConstraint, TableDef};
4
5use self::direct::build_direct_delete_column;
6use self::sqlite_rebuild::build_delete_column_sqlite_temp_table;
7use super::types::{BuiltQuery, DatabaseBackend};
8
9mod direct;
10mod sqlite_rebuild;
11
12pub fn build_delete_column(
20 backend: DatabaseBackend,
21 table: &str,
22 column: &str,
23 column_type: Option<&ColumnType>,
24 current_schema: &[TableDef],
25 pending_constraints: &[TableConstraint],
26) -> Vec<BuiltQuery> {
27 let mut stmts = Vec::new();
28
29 if backend == DatabaseBackend::Sqlite
31 && let Some(table_def) = current_schema.iter().find(|t| t.name == table)
32 && let Some(sqlite_queries) = sqlite_constraint_handling(
33 table,
34 column,
35 table_def,
36 column_type,
37 pending_constraints,
38 &mut stmts,
39 )
40 {
41 return sqlite_queries;
42 }
43
44 stmts.extend(build_direct_delete_column(table, column, column_type));
45 stmts
46}
47
48fn sqlite_constraint_handling(
49 table: &str,
50 column: &str,
51 table_def: &TableDef,
52 column_type: Option<&ColumnType>,
53 pending_constraints: &[TableConstraint],
54 stmts: &mut Vec<BuiltQuery>,
55) -> Option<Vec<BuiltQuery>> {
56 if let Some(col_def) = table_def.columns.iter().find(|c| c.name == column)
60 && let ColumnType::Complex(ComplexColumnType::Enum { .. }) = &col_def.r#type
61 {
62 return Some(build_delete_column_sqlite_temp_table(
63 table,
64 column,
65 table_def,
66 column_type,
67 pending_constraints,
68 ));
69 }
70
71 for constraint in &table_def.constraints {
73 match constraint {
74 TableConstraint::Check { expr, .. } => {
77 if expr.contains(&format!("\"{column}\"")) || expr.contains(column) {
79 return Some(build_delete_column_sqlite_temp_table(
80 table,
81 column,
82 table_def,
83 column_type,
84 pending_constraints,
85 ));
86 }
87 }
88 _ if !constraint.columns().iter().any(|c| c == column) => {}
90 TableConstraint::ForeignKey { .. } | TableConstraint::PrimaryKey { .. } => {
92 return Some(build_delete_column_sqlite_temp_table(
93 table,
94 column,
95 table_def,
96 column_type,
97 pending_constraints,
98 ));
99 }
100 TableConstraint::Unique { name, columns, .. } => {
102 let index_name = vespertide_naming::build_unique_constraint_name(
103 table,
104 columns,
105 name.as_deref(),
106 );
107 let drop_idx = Index::drop()
108 .name(&index_name)
109 .table(Alias::new(table))
110 .to_owned();
111 stmts.push(BuiltQuery::DropIndex(Box::new(drop_idx)));
112 }
113 TableConstraint::Index { name, columns } => {
114 let index_name =
115 vespertide_naming::build_index_name(table, columns, name.as_deref());
116 let drop_idx = Index::drop()
117 .name(&index_name)
118 .table(Alias::new(table))
119 .to_owned();
120 stmts.push(BuiltQuery::DropIndex(Box::new(drop_idx)));
121 }
122 _ => {
123 unreachable!("TableConstraint is #[non_exhaustive]; all variants are matched above")
124 }
125 }
126 }
127
128 None
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::sql::types::DatabaseBackend;
135 use crate::test_support::col;
136 use insta::{assert_snapshot, with_settings};
137 use rstest::rstest;
138 use vespertide_core::{ComplexColumnType, SimpleColumnType};
139
140 #[rstest]
141 #[case::delete_column_postgres(
142 "delete_column_postgres",
143 DatabaseBackend::Postgres,
144 &["ALTER TABLE \"users\" DROP COLUMN \"email\""]
145 )]
146 #[case::delete_column_mysql(
147 "delete_column_mysql",
148 DatabaseBackend::MySql,
149 &["ALTER TABLE `users` DROP COLUMN `email`"]
150 )]
151 #[case::delete_column_sqlite(
152 "delete_column_sqlite",
153 DatabaseBackend::Sqlite,
154 &["ALTER TABLE \"users\" DROP COLUMN \"email\""]
155 )]
156 fn test_delete_column(
157 #[case] title: &str,
158 #[case] backend: DatabaseBackend,
159 #[case] expected: &[&str],
160 ) {
161 let result = build_delete_column(backend, "users", "email", None, &[], &[]);
162 let sql = result[0].build(backend);
163 for exp in expected {
164 assert!(
165 sql.contains(exp),
166 "Expected SQL to contain '{exp}', got: {sql}"
167 );
168 }
169
170 with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("delete_column_{}", title) }, {
171 assert_snapshot!(sql);
172 });
173 }
174
175 #[test]
176 fn test_delete_enum_column_postgres() {
177 use vespertide_core::EnumValues;
178
179 let enum_type = ColumnType::Complex(ComplexColumnType::Enum {
180 name: "status".into(),
181 values: EnumValues::String(vec!["active".into(), "inactive".into()]),
182 });
183 let result = build_delete_column(
184 DatabaseBackend::Postgres,
185 "users",
186 "status",
187 Some(&enum_type),
188 &[],
189 &[],
190 );
191
192 assert_eq!(result.len(), 2);
194
195 let alter_sql = result[0].build(DatabaseBackend::Postgres);
196 assert!(alter_sql.contains("DROP COLUMN"));
197
198 let drop_type_sql = result[1].build(DatabaseBackend::Postgres);
199 assert!(drop_type_sql.contains("DROP TYPE \"users_status\""));
200
201 let drop_type_mysql = result[1].build(DatabaseBackend::MySql);
203 assert!(drop_type_mysql.is_empty());
204 }
205
206 #[test]
207 fn test_delete_non_enum_column_no_drop_type() {
208 let text_type = ColumnType::Simple(SimpleColumnType::Text);
209 let result = build_delete_column(
210 DatabaseBackend::Postgres,
211 "users",
212 "name",
213 Some(&text_type),
214 &[],
215 &[],
216 );
217
218 assert_eq!(result.len(), 1);
220 }
221
222 #[test]
223 fn test_delete_column_sqlite_drops_unique_constraint_first() {
224 let schema = vec![TableDef {
226 name: "gift".into(),
227 description: None,
228 columns: vec![
229 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
230 col("gift_code", ColumnType::Simple(SimpleColumnType::Text)),
231 ],
232 constraints: vec![TableConstraint::Unique {
233 name: None,
234 columns: vec!["gift_code".into()],
235 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
236 keep: vespertide_core::KeepPolicy::First,
237 },
238 }],
239 }];
240
241 let result = build_delete_column(
242 DatabaseBackend::Sqlite,
243 "gift",
244 "gift_code",
245 None,
246 &schema,
247 &[],
248 );
249
250 assert_eq!(result.len(), 2);
252
253 let drop_index_sql = result[0].build(DatabaseBackend::Sqlite);
254 assert!(
255 drop_index_sql.contains("DROP INDEX"),
256 "Expected DROP INDEX, got: {drop_index_sql}"
257 );
258 assert!(
259 drop_index_sql.contains("uq_gift__gift_code"),
260 "Expected index name uq_gift__gift_code, got: {drop_index_sql}"
261 );
262
263 let drop_column_sql = result[1].build(DatabaseBackend::Sqlite);
264 assert!(
265 drop_column_sql.contains("DROP COLUMN"),
266 "Expected DROP COLUMN, got: {drop_column_sql}"
267 );
268 }
269
270 #[test]
271 fn test_delete_column_sqlite_drops_index_constraint_first() {
272 let schema = vec![TableDef {
274 name: "users".into(),
275 description: None,
276 columns: vec![
277 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
278 col("email", ColumnType::Simple(SimpleColumnType::Text)),
279 ],
280 constraints: vec![TableConstraint::Index {
281 name: None,
282 columns: vec!["email".into()],
283 }],
284 }];
285
286 let result = build_delete_column(
287 DatabaseBackend::Sqlite,
288 "users",
289 "email",
290 None,
291 &schema,
292 &[],
293 );
294
295 assert_eq!(result.len(), 2);
297
298 let drop_index_sql = result[0].build(DatabaseBackend::Sqlite);
299 assert!(drop_index_sql.contains("DROP INDEX"));
300 assert!(drop_index_sql.contains("ix_users__email"));
301
302 let drop_column_sql = result[1].build(DatabaseBackend::Sqlite);
303 assert!(drop_column_sql.contains("DROP COLUMN"));
304 }
305
306 #[test]
307 fn test_delete_column_postgres_does_not_drop_constraints() {
308 let schema = vec![TableDef {
310 name: "gift".into(),
311 description: None,
312 columns: vec![
313 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
314 col("gift_code", ColumnType::Simple(SimpleColumnType::Text)),
315 ],
316 constraints: vec![TableConstraint::Unique {
317 name: None,
318 columns: vec!["gift_code".into()],
319 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
320 keep: vespertide_core::KeepPolicy::First,
321 },
322 }],
323 }];
324
325 let result = build_delete_column(
326 DatabaseBackend::Postgres,
327 "gift",
328 "gift_code",
329 None,
330 &schema,
331 &[],
332 );
333
334 assert_eq!(result.len(), 1);
336
337 let drop_column_sql = result[0].build(DatabaseBackend::Postgres);
338 assert!(drop_column_sql.contains("DROP COLUMN"));
339 }
340
341 #[test]
342 fn test_delete_column_sqlite_with_named_unique_constraint() {
343 let schema = vec![TableDef {
345 name: "gift".into(),
346 description: None,
347 columns: vec![
348 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
349 col("gift_code", ColumnType::Simple(SimpleColumnType::Text)),
350 ],
351 constraints: vec![TableConstraint::Unique {
352 name: Some("gift_code".into()),
353 columns: vec!["gift_code".into()],
354 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
355 keep: vespertide_core::KeepPolicy::First,
356 },
357 }],
358 }];
359
360 let result = build_delete_column(
361 DatabaseBackend::Sqlite,
362 "gift",
363 "gift_code",
364 None,
365 &schema,
366 &[],
367 );
368
369 assert_eq!(result.len(), 2);
370
371 let drop_index_sql = result[0].build(DatabaseBackend::Sqlite);
372 assert!(
374 drop_index_sql.contains("uq_gift__gift_code"),
375 "Expected uq_gift__gift_code, got: {drop_index_sql}"
376 );
377 }
378
379 #[test]
380 fn test_delete_column_sqlite_with_fk_uses_temp_table() {
381 let schema = vec![TableDef {
383 name: "gift".into(),
384 description: None,
385 columns: vec![
386 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
387 col("sender_id", ColumnType::Simple(SimpleColumnType::BigInt)),
388 col("message", ColumnType::Simple(SimpleColumnType::Text)),
389 ],
390 constraints: vec![TableConstraint::ForeignKey {
391 name: None,
392 columns: vec!["sender_id".into()],
393 ref_table: "user".into(),
394 ref_columns: vec!["id".into()],
395 on_delete: None,
396 on_update: None,
397 orphan_strategy: vespertide_core::ForeignKeyOrphanStrategy::default(),
398 }],
399 }];
400
401 let result = build_delete_column(
402 DatabaseBackend::Sqlite,
403 "gift",
404 "sender_id",
405 None,
406 &schema,
407 &[],
408 );
409
410 assert!(
416 result.len() >= 4,
417 "Expected at least 4 statements for temp table approach, got: {}",
418 result.len()
419 );
420
421 let all_sql: Vec<String> = result
422 .iter()
423 .map(|q| q.build(DatabaseBackend::Sqlite))
424 .collect();
425 let combined_sql = all_sql.join("\n");
426
427 assert!(
429 combined_sql.contains("CREATE TABLE") && combined_sql.contains("gift_temp"),
430 "Expected CREATE TABLE gift_temp, got: {combined_sql}"
431 );
432
433 assert!(
435 !combined_sql.contains("\"sender_id\"") || combined_sql.contains("DROP TABLE"),
436 "New table should not contain sender_id column"
437 );
438
439 assert!(
441 combined_sql.contains("INSERT INTO"),
442 "Expected INSERT INTO for data copy, got: {combined_sql}"
443 );
444
445 assert!(
447 combined_sql.contains("DROP TABLE") && combined_sql.contains("\"gift\""),
448 "Expected DROP TABLE gift, got: {combined_sql}"
449 );
450
451 assert!(
453 combined_sql.contains("RENAME"),
454 "Expected RENAME for temp table, got: {combined_sql}"
455 );
456 }
457
458 #[test]
459 fn test_delete_column_sqlite_with_fk_preserves_other_columns() {
460 let schema = vec![TableDef {
462 name: "gift".into(),
463 description: None,
464 columns: vec![
465 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
466 col("sender_id", ColumnType::Simple(SimpleColumnType::BigInt)),
467 col("receiver_id", ColumnType::Simple(SimpleColumnType::BigInt)),
468 col("message", ColumnType::Simple(SimpleColumnType::Text)),
469 ],
470 constraints: vec![
471 TableConstraint::ForeignKey {
472 name: None,
473 columns: vec!["sender_id".into()],
474 ref_table: "user".into(),
475 ref_columns: vec!["id".into()],
476 on_delete: None,
477 on_update: None,
478 orphan_strategy: vespertide_core::ForeignKeyOrphanStrategy::default(),
479 },
480 TableConstraint::Index {
481 name: None,
482 columns: vec!["receiver_id".into()],
483 },
484 ],
485 }];
486
487 let result = build_delete_column(
488 DatabaseBackend::Sqlite,
489 "gift",
490 "sender_id",
491 None,
492 &schema,
493 &[],
494 );
495
496 let all_sql: Vec<String> = result
497 .iter()
498 .map(|q| q.build(DatabaseBackend::Sqlite))
499 .collect();
500 let combined_sql = all_sql.join("\n");
501
502 assert!(combined_sql.contains("\"id\""), "Should preserve id column");
504 assert!(
505 combined_sql.contains("\"receiver_id\""),
506 "Should preserve receiver_id column"
507 );
508 assert!(
509 combined_sql.contains("\"message\""),
510 "Should preserve message column"
511 );
512
513 assert!(
515 combined_sql.contains("CREATE INDEX") && combined_sql.contains("ix_gift__receiver_id"),
516 "Should recreate index on receiver_id, got: {combined_sql}"
517 );
518 }
519
520 #[test]
521 fn test_delete_column_postgres_with_fk_does_not_use_temp_table() {
522 let schema = vec![TableDef {
524 name: "gift".into(),
525 description: None,
526 columns: vec![
527 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
528 col("sender_id", ColumnType::Simple(SimpleColumnType::BigInt)),
529 ],
530 constraints: vec![TableConstraint::ForeignKey {
531 name: None,
532 columns: vec!["sender_id".into()],
533 ref_table: "user".into(),
534 ref_columns: vec!["id".into()],
535 on_delete: None,
536 on_update: None,
537 orphan_strategy: vespertide_core::ForeignKeyOrphanStrategy::default(),
538 }],
539 }];
540
541 let result = build_delete_column(
542 DatabaseBackend::Postgres,
543 "gift",
544 "sender_id",
545 None,
546 &schema,
547 &[],
548 );
549
550 assert_eq!(
552 result.len(),
553 1,
554 "PostgreSQL should only have 1 statement, got: {}",
555 result.len()
556 );
557
558 let sql = result[0].build(DatabaseBackend::Postgres);
559 assert!(
560 sql.contains("DROP COLUMN"),
561 "Expected DROP COLUMN, got: {sql}"
562 );
563 assert!(
564 !sql.contains("gift_temp"),
565 "PostgreSQL should not use temp table"
566 );
567 }
568
569 #[test]
570 fn test_delete_column_sqlite_with_pk_uses_temp_table() {
571 let schema = vec![TableDef {
573 name: "order_items".into(),
574 description: None,
575 columns: vec![
576 col("order_id", ColumnType::Simple(SimpleColumnType::Integer)),
577 col("product_id", ColumnType::Simple(SimpleColumnType::Integer)),
578 col("quantity", ColumnType::Simple(SimpleColumnType::Integer)),
579 ],
580 constraints: vec![TableConstraint::PrimaryKey {
581 auto_increment: false,
582 columns: vec!["order_id".into(), "product_id".into()],
583 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
584 }],
585 }];
586
587 let result = build_delete_column(
588 DatabaseBackend::Sqlite,
589 "order_items",
590 "product_id",
591 None,
592 &schema,
593 &[],
594 );
595
596 assert!(
598 result.len() >= 4,
599 "Expected at least 4 statements for temp table approach, got: {}",
600 result.len()
601 );
602
603 let all_sql: Vec<String> = result
604 .iter()
605 .map(|q| q.build(DatabaseBackend::Sqlite))
606 .collect();
607 let combined_sql = all_sql.join("\n");
608
609 assert!(
610 combined_sql.contains("order_items_temp"),
611 "Should use temp table approach for PK column deletion"
612 );
613 }
614
615 #[test]
616 fn test_delete_column_sqlite_unique_on_different_column_not_dropped() {
617 let schema = vec![TableDef {
620 name: "users".into(),
621 description: None,
622 columns: vec![
623 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
624 col("email", ColumnType::Simple(SimpleColumnType::Text)),
625 col("nickname", ColumnType::Simple(SimpleColumnType::Text)),
626 ],
627 constraints: vec![
628 TableConstraint::Unique {
630 name: None,
631 columns: vec!["email".into()],
632 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
633 keep: vespertide_core::KeepPolicy::First,
634 },
635 },
636 ],
637 }];
638
639 let result = build_delete_column(
641 DatabaseBackend::Sqlite,
642 "users",
643 "nickname",
644 None,
645 &schema,
646 &[],
647 );
648
649 assert_eq!(
651 result.len(),
652 1,
653 "Should not drop UNIQUE on email when deleting nickname, got: {} statements",
654 result.len()
655 );
656
657 let sql = result[0].build(DatabaseBackend::Sqlite);
658 assert!(
659 sql.contains("DROP COLUMN"),
660 "Expected DROP COLUMN, got: {sql}"
661 );
662 assert!(
663 !sql.contains("DROP INDEX"),
664 "Should NOT drop the email UNIQUE constraint when deleting nickname"
665 );
666 }
667
668 #[test]
669 fn test_delete_column_sqlite_temp_table_filters_constraints_correctly() {
670 let schema = vec![TableDef {
674 name: "orders".into(),
675 description: None,
676 columns: vec![
677 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
678 col("user_id", ColumnType::Simple(SimpleColumnType::BigInt)),
679 col("status", ColumnType::Simple(SimpleColumnType::Text)),
680 col(
681 "created_at",
682 ColumnType::Simple(SimpleColumnType::Timestamp),
683 ),
684 ],
685 constraints: vec![
686 TableConstraint::ForeignKey {
688 name: None,
689 columns: vec!["user_id".into()],
690 ref_table: "users".into(),
691 ref_columns: vec!["id".into()],
692 on_delete: None,
693 on_update: None,
694 orphan_strategy: vespertide_core::ForeignKeyOrphanStrategy::default(),
695 },
696 TableConstraint::Index {
698 name: None,
699 columns: vec!["created_at".into()],
700 },
701 TableConstraint::ForeignKey {
703 name: None,
704 columns: vec!["status".into()],
705 ref_table: "statuses".into(),
706 ref_columns: vec!["code".into()],
707 on_delete: None,
708 on_update: None,
709 orphan_strategy: vespertide_core::ForeignKeyOrphanStrategy::default(),
710 },
711 ],
712 }];
713
714 let result = build_delete_column(
715 DatabaseBackend::Sqlite,
716 "orders",
717 "user_id",
718 None,
719 &schema,
720 &[],
721 );
722
723 let all_sql: Vec<String> = result
724 .iter()
725 .map(|q| q.build(DatabaseBackend::Sqlite))
726 .collect();
727 let combined_sql = all_sql.join("\n");
728
729 assert!(
731 combined_sql.contains("orders_temp"),
732 "Should use temp table approach for FK column deletion"
733 );
734
735 assert!(
737 combined_sql.contains("ix_orders__created_at"),
738 "Index on created_at should be recreated, got: {combined_sql}"
739 );
740
741 assert!(
744 combined_sql.contains("REFERENCES \"statuses\""),
745 "FK on status should be preserved, got: {combined_sql}"
746 );
747
748 let fk_patterns = combined_sql.matches("REFERENCES").count();
750 assert_eq!(
751 fk_patterns, 1,
752 "Only the FK on status should exist (not the one on user_id), got: {combined_sql}"
753 );
754 }
755
756 fn build_sql_snapshot(result: &[BuiltQuery], backend: DatabaseBackend) -> String {
759 result
760 .iter()
761 .map(|q| q.build(backend))
762 .collect::<Vec<_>>()
763 .join(";\n")
764 }
765
766 #[rstest]
767 #[case::postgres("postgres", DatabaseBackend::Postgres)]
768 #[case::mysql("mysql", DatabaseBackend::MySql)]
769 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
770 fn test_delete_column_with_unique_constraint(
771 #[case] title: &str,
772 #[case] backend: DatabaseBackend,
773 ) {
774 let schema = vec![TableDef {
775 name: "users".into(),
776 description: None,
777 columns: vec![
778 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
779 col("email", ColumnType::Simple(SimpleColumnType::Text)),
780 col("name", ColumnType::Simple(SimpleColumnType::Text)),
781 ],
782 constraints: vec![TableConstraint::Unique {
783 name: None,
784 columns: vec!["email".into()],
785 strategy: vespertide_core::UniqueConstraintStrategy::DeleteDuplicates {
786 keep: vespertide_core::KeepPolicy::First,
787 },
788 }],
789 }];
790
791 let result = build_delete_column(backend, "users", "email", None, &schema, &[]);
792 let sql = build_sql_snapshot(&result, backend);
793
794 with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("delete_column_with_unique_{}", title) }, {
795 assert_snapshot!(sql);
796 });
797 }
798
799 #[rstest]
800 #[case::postgres("postgres", DatabaseBackend::Postgres)]
801 #[case::mysql("mysql", DatabaseBackend::MySql)]
802 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
803 fn test_delete_column_with_index_constraint(
804 #[case] title: &str,
805 #[case] backend: DatabaseBackend,
806 ) {
807 let schema = vec![TableDef {
808 name: "posts".into(),
809 description: None,
810 columns: vec![
811 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
812 col(
813 "created_at",
814 ColumnType::Simple(SimpleColumnType::Timestamp),
815 ),
816 col("title", ColumnType::Simple(SimpleColumnType::Text)),
817 ],
818 constraints: vec![TableConstraint::Index {
819 name: None,
820 columns: vec!["created_at".into()],
821 }],
822 }];
823
824 let result = build_delete_column(backend, "posts", "created_at", None, &schema, &[]);
825 let sql = build_sql_snapshot(&result, backend);
826
827 with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("delete_column_with_index_{}", title) }, {
828 assert_snapshot!(sql);
829 });
830 }
831
832 #[rstest]
833 #[case::postgres("postgres", DatabaseBackend::Postgres)]
834 #[case::mysql("mysql", DatabaseBackend::MySql)]
835 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
836 fn test_delete_column_with_fk_constraint(
837 #[case] title: &str,
838 #[case] backend: DatabaseBackend,
839 ) {
840 let schema = vec![TableDef {
841 name: "orders".into(),
842 description: None,
843 columns: vec![
844 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
845 col("user_id", ColumnType::Simple(SimpleColumnType::BigInt)),
846 col("total", ColumnType::Simple(SimpleColumnType::Integer)),
847 ],
848 constraints: vec![TableConstraint::ForeignKey {
849 name: None,
850 columns: vec!["user_id".into()],
851 ref_table: "users".into(),
852 ref_columns: vec!["id".into()],
853 on_delete: None,
854 on_update: None,
855 orphan_strategy: vespertide_core::ForeignKeyOrphanStrategy::default(),
856 }],
857 }];
858
859 let result = build_delete_column(backend, "orders", "user_id", None, &schema, &[]);
860 let sql = build_sql_snapshot(&result, backend);
861
862 with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("delete_column_with_fk_{}", title) }, {
863 assert_snapshot!(sql);
864 });
865 }
866
867 #[rstest]
868 #[case::postgres("postgres", DatabaseBackend::Postgres)]
869 #[case::mysql("mysql", DatabaseBackend::MySql)]
870 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
871 fn test_delete_column_with_pk_constraint(
872 #[case] title: &str,
873 #[case] backend: DatabaseBackend,
874 ) {
875 let schema = vec![TableDef {
876 name: "order_items".into(),
877 description: None,
878 columns: vec![
879 col("order_id", ColumnType::Simple(SimpleColumnType::Integer)),
880 col("product_id", ColumnType::Simple(SimpleColumnType::Integer)),
881 col("quantity", ColumnType::Simple(SimpleColumnType::Integer)),
882 ],
883 constraints: vec![TableConstraint::PrimaryKey {
884 auto_increment: false,
885 columns: vec!["order_id".into(), "product_id".into()],
886 strategy: vespertide_core::PrimaryKeyAdditionStrategy::default(),
887 }],
888 }];
889
890 let result = build_delete_column(backend, "order_items", "product_id", None, &schema, &[]);
891 let sql = build_sql_snapshot(&result, backend);
892
893 with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("delete_column_with_pk_{}", title) }, {
894 assert_snapshot!(sql);
895 });
896 }
897
898 #[rstest]
899 #[case::postgres("postgres", DatabaseBackend::Postgres)]
900 #[case::mysql("mysql", DatabaseBackend::MySql)]
901 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
902 fn test_delete_column_with_fk_and_index_constraints(
903 #[case] title: &str,
904 #[case] backend: DatabaseBackend,
905 ) {
906 let schema = vec![TableDef {
908 name: "orders".into(),
909 description: None,
910 columns: vec![
911 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
912 col("user_id", ColumnType::Simple(SimpleColumnType::BigInt)),
913 col(
914 "created_at",
915 ColumnType::Simple(SimpleColumnType::Timestamp),
916 ),
917 col("total", ColumnType::Simple(SimpleColumnType::Integer)),
918 ],
919 constraints: vec![
920 TableConstraint::ForeignKey {
921 name: None,
922 columns: vec!["user_id".into()],
923 ref_table: "users".into(),
924 ref_columns: vec!["id".into()],
925 on_delete: None,
926 on_update: None,
927 orphan_strategy: vespertide_core::ForeignKeyOrphanStrategy::default(),
928 },
929 TableConstraint::Index {
930 name: None,
931 columns: vec!["created_at".into()],
932 },
933 ],
934 }];
935
936 let result = build_delete_column(backend, "orders", "user_id", None, &schema, &[]);
937 let sql = build_sql_snapshot(&result, backend);
938
939 with_settings!({ snapshot_path => "../snapshots", snapshot_suffix => format!("delete_column_with_fk_and_index_{}", title) }, {
940 assert_snapshot!(sql);
941 });
942 }
943
944 #[test]
945 fn test_delete_column_sqlite_temp_table_with_enum_column() {
946 use vespertide_core::EnumValues;
949
950 let enum_type = ColumnType::Complex(ComplexColumnType::Enum {
951 name: "order_status".into(),
952 values: EnumValues::String(vec![
953 "pending".into(),
954 "shipped".into(),
955 "delivered".into(),
956 ]),
957 });
958
959 let schema = vec![TableDef {
960 name: "orders".into(),
961 description: None,
962 columns: vec![
963 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
964 col("user_id", ColumnType::Simple(SimpleColumnType::BigInt)),
965 col("status", enum_type.clone()),
966 ],
967 constraints: vec![TableConstraint::ForeignKey {
968 name: None,
969 columns: vec!["user_id".into()],
970 ref_table: "users".into(),
971 ref_columns: vec!["id".into()],
972 on_delete: None,
973 on_update: None,
974 orphan_strategy: vespertide_core::ForeignKeyOrphanStrategy::default(),
975 }],
976 }];
977
978 let result = build_delete_column(
980 DatabaseBackend::Sqlite,
981 "orders",
982 "user_id",
983 Some(&enum_type),
984 &schema,
985 &[],
986 );
987
988 assert!(
990 result.len() >= 4,
991 "Expected at least 4 statements for temp table approach, got: {}",
992 result.len()
993 );
994
995 let all_sql: Vec<String> = result
996 .iter()
997 .map(|q| q.build(DatabaseBackend::Sqlite))
998 .collect();
999 let combined_sql = all_sql.join("\n");
1000
1001 assert!(
1003 combined_sql.contains("orders_temp"),
1004 "Should use temp table approach"
1005 );
1006
1007 let last_stmt = result.last().unwrap();
1010 let last_sql = last_stmt.build(DatabaseBackend::Sqlite);
1011 assert!(
1013 last_sql.is_empty() || !last_sql.contains("DROP TYPE"),
1014 "SQLite should not emit DROP TYPE"
1015 );
1016
1017 let pg_last_sql = last_stmt.build(DatabaseBackend::Postgres);
1019 assert!(
1020 pg_last_sql.contains("DROP TYPE"),
1021 "PostgreSQL should emit DROP TYPE, got: {pg_last_sql}"
1022 );
1023 }
1024
1025 #[test]
1026 fn test_delete_column_sqlite_with_check_constraint_referencing_column() {
1027 let schema = vec![TableDef {
1030 name: "orders".into(),
1031 description: None,
1032 columns: vec![
1033 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1034 col("amount", ColumnType::Simple(SimpleColumnType::Integer)),
1035 ],
1036 constraints: vec![TableConstraint::Check {
1037 name: "check_positive".into(),
1038 expr: "amount > 0".into(),
1039 strategy: vespertide_core::CheckViolationStrategy::default(),
1040 }],
1041 }];
1042
1043 let result = build_delete_column(
1045 DatabaseBackend::Sqlite,
1046 "orders",
1047 "amount",
1048 None,
1049 &schema,
1050 &[],
1051 );
1052
1053 assert!(
1055 result.len() >= 4,
1056 "Expected temp table approach (>=4 stmts), got: {} statements",
1057 result.len()
1058 );
1059
1060 let sql = result[0].build(DatabaseBackend::Sqlite);
1061 assert!(
1062 sql.contains("orders_temp"),
1063 "Expected temp table creation, got: {sql}"
1064 );
1065 assert!(
1067 !sql.contains("check_positive"),
1068 "CHECK referencing deleted column should be removed, got: {sql}"
1069 );
1070 }
1071
1072 #[rstest]
1077 #[case::sqlite_unquoted(DatabaseBackend::Sqlite, "age > 0")]
1078 #[case::sqlite_quoted(DatabaseBackend::Sqlite, r#""age" > 0"#)]
1079 #[case::postgres_unquoted(DatabaseBackend::Postgres, "age > 0")]
1080 #[case::postgres_quoted(DatabaseBackend::Postgres, r#""age" > 0"#)]
1081 #[case::mysql_unquoted(DatabaseBackend::MySql, "age > 0")]
1082 #[case::mysql_quoted(DatabaseBackend::MySql, r#""age" > 0"#)]
1083 fn delete_column_removes_check_constraint_referencing_it(
1084 #[case] backend: DatabaseBackend,
1085 #[case] check_expr: &str,
1086 ) {
1087 let schema = vec![TableDef {
1088 name: "people".into(),
1089 description: None,
1090 columns: vec![
1091 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1092 col("age", ColumnType::Simple(SimpleColumnType::Integer)),
1093 ],
1094 constraints: vec![TableConstraint::Check {
1095 name: "chk_age_positive".into(),
1096 expr: check_expr.into(),
1097 strategy: vespertide_core::CheckViolationStrategy::default(),
1098 }],
1099 }];
1100
1101 let queries = build_delete_column(backend, "people", "age", None, &schema, &[]);
1102 let sql = queries
1103 .iter()
1104 .map(|q| q.build(backend))
1105 .collect::<Vec<_>>()
1106 .join("\n");
1107
1108 if backend == DatabaseBackend::Sqlite {
1109 assert!(
1110 !sql.contains("chk_age_positive"),
1111 "SQLite rebuild must omit the CHECK '{check_expr}' that \
1112 references the deleted column; got: {sql}"
1113 );
1114 assert!(
1115 !sql.contains(check_expr),
1116 "SQLite rebuild must not echo the CHECK expression \
1117 '{check_expr}' (it references the deleted column); got: {sql}"
1118 );
1119 assert!(
1120 sql.contains("people_temp"),
1121 "expected SQLite temp-table rebuild for 'people'; got: {sql}"
1122 );
1123 } else {
1124 assert!(
1125 !queries.is_empty(),
1126 "{backend:?}: expected DROP COLUMN output, got empty"
1127 );
1128 assert!(
1129 sql.contains("DROP COLUMN"),
1130 "{backend:?}: expected DROP COLUMN, got: {sql}"
1131 );
1132 }
1133 }
1134
1135 #[test]
1136 fn test_delete_column_sqlite_with_check_constraint_not_referencing_column() {
1137 let schema = vec![TableDef {
1140 name: "orders".into(),
1141 description: None,
1142 columns: vec![
1143 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1144 col("amount", ColumnType::Simple(SimpleColumnType::Integer)),
1145 col("note", ColumnType::Simple(SimpleColumnType::Text)),
1146 ],
1147 constraints: vec![TableConstraint::Check {
1148 name: "check_positive".into(),
1149 expr: "amount > 0".into(),
1150 strategy: vespertide_core::CheckViolationStrategy::default(),
1151 }],
1152 }];
1153
1154 let result = build_delete_column(
1156 DatabaseBackend::Sqlite,
1157 "orders",
1158 "note",
1159 None,
1160 &schema,
1161 &[],
1162 );
1163
1164 assert_eq!(
1165 result.len(),
1166 1,
1167 "Unrelated CHECK should be skipped, got: {} statements",
1168 result.len()
1169 );
1170
1171 let sql = result[0].build(DatabaseBackend::Sqlite);
1172 assert!(
1173 sql.contains("DROP COLUMN"),
1174 "Expected DROP COLUMN, got: {sql}"
1175 );
1176 }
1177}