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