1use sea_query::{Alias, Index, Query, Table};
2
3use vespertide_core::{ColumnType, TableConstraint, TableDef};
4
5use super::create_table::build_create_table_for_backend;
6use super::helpers::build_drop_enum_type_sql;
7use super::rename_table::build_rename_table;
8use super::types::{BuiltQuery, DatabaseBackend};
9
10pub fn build_delete_column(
18 backend: &DatabaseBackend,
19 table: &str,
20 column: &str,
21 column_type: Option<&ColumnType>,
22 current_schema: &[TableDef],
23) -> Vec<BuiltQuery> {
24 let mut stmts = Vec::new();
25
26 if *backend == DatabaseBackend::Sqlite
28 && let Some(table_def) = current_schema.iter().find(|t| t.name == table)
29 {
30 for constraint in &table_def.constraints {
32 match constraint {
33 TableConstraint::Check { .. } => continue,
35 _ if !constraint.columns().iter().any(|c| c == column) => continue,
37 TableConstraint::ForeignKey { .. } | TableConstraint::PrimaryKey { .. } => {
39 return build_delete_column_sqlite_temp_table(
40 table,
41 column,
42 table_def,
43 column_type,
44 );
45 }
46 TableConstraint::Unique { name, columns } => {
48 let index_name = vespertide_naming::build_unique_constraint_name(
49 table,
50 columns,
51 name.as_deref(),
52 );
53 let drop_idx = Index::drop()
54 .name(&index_name)
55 .table(Alias::new(table))
56 .to_owned();
57 stmts.push(BuiltQuery::DropIndex(Box::new(drop_idx)));
58 }
59 TableConstraint::Index { name, columns } => {
60 let index_name =
61 vespertide_naming::build_index_name(table, columns, name.as_deref());
62 let drop_idx = Index::drop()
63 .name(&index_name)
64 .table(Alias::new(table))
65 .to_owned();
66 stmts.push(BuiltQuery::DropIndex(Box::new(drop_idx)));
67 }
68 }
69 }
70 }
71
72 let stmt = Table::alter()
74 .table(Alias::new(table))
75 .drop_column(Alias::new(column))
76 .to_owned();
77 stmts.push(BuiltQuery::AlterTable(Box::new(stmt)));
78
79 if let Some(col_type) = column_type
82 && let Some(drop_type_sql) = build_drop_enum_type_sql(table, col_type)
83 {
84 stmts.push(BuiltQuery::Raw(drop_type_sql));
85 }
86
87 stmts
88}
89
90fn build_delete_column_sqlite_temp_table(
99 table: &str,
100 column: &str,
101 table_def: &TableDef,
102 column_type: Option<&ColumnType>,
103) -> Vec<BuiltQuery> {
104 let mut stmts = Vec::new();
105 let temp_table = format!("{}_temp", table);
106
107 let new_columns: Vec<_> = table_def
109 .columns
110 .iter()
111 .filter(|c| c.name != column)
112 .cloned()
113 .collect();
114
115 let new_constraints: Vec<_> = table_def
117 .constraints
118 .iter()
119 .filter(|c| !c.columns().iter().any(|col| col == column))
120 .cloned()
121 .collect();
122
123 let create_temp_table = build_create_table_for_backend(
125 &DatabaseBackend::Sqlite,
126 &temp_table,
127 &new_columns,
128 &new_constraints,
129 );
130 stmts.push(BuiltQuery::CreateTable(Box::new(create_temp_table)));
131
132 let column_aliases: Vec<Alias> = new_columns.iter().map(|c| Alias::new(&c.name)).collect();
134 let mut select_query = Query::select();
135 for col_alias in &column_aliases {
136 select_query = select_query.column(col_alias.clone()).to_owned();
137 }
138 select_query = select_query.from(Alias::new(table)).to_owned();
139
140 let insert_stmt = Query::insert()
141 .into_table(Alias::new(&temp_table))
142 .columns(column_aliases.clone())
143 .select_from(select_query)
144 .unwrap()
145 .to_owned();
146 stmts.push(BuiltQuery::Insert(Box::new(insert_stmt)));
147
148 let drop_table = Table::drop().table(Alias::new(table)).to_owned();
150 stmts.push(BuiltQuery::DropTable(Box::new(drop_table)));
151
152 stmts.push(build_rename_table(&temp_table, table));
154
155 for constraint in &new_constraints {
157 if let TableConstraint::Index { name, columns } = constraint {
158 let index_name = vespertide_naming::build_index_name(table, columns, name.as_deref());
159 let mut idx_stmt = Index::create();
160 idx_stmt = idx_stmt
161 .name(&index_name)
162 .table(Alias::new(table))
163 .to_owned();
164 for col_name in columns {
165 idx_stmt = idx_stmt.col(Alias::new(col_name)).to_owned();
166 }
167 stmts.push(BuiltQuery::CreateIndex(Box::new(idx_stmt)));
168 }
169 }
170
171 if let Some(col_type) = column_type
173 && let Some(drop_type_sql) = build_drop_enum_type_sql(table, col_type)
174 {
175 stmts.push(BuiltQuery::Raw(drop_type_sql));
176 }
177
178 stmts
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::sql::types::DatabaseBackend;
185 use insta::{assert_snapshot, with_settings};
186 use rstest::rstest;
187 use vespertide_core::{ColumnDef, ComplexColumnType, SimpleColumnType};
188
189 fn col(name: &str, ty: ColumnType) -> ColumnDef {
190 ColumnDef {
191 name: name.to_string(),
192 r#type: ty,
193 nullable: true,
194 default: None,
195 comment: None,
196 primary_key: None,
197 unique: None,
198 index: None,
199 foreign_key: None,
200 }
201 }
202
203 #[rstest]
204 #[case::delete_column_postgres(
205 "delete_column_postgres",
206 DatabaseBackend::Postgres,
207 &["ALTER TABLE \"users\" DROP COLUMN \"email\""]
208 )]
209 #[case::delete_column_mysql(
210 "delete_column_mysql",
211 DatabaseBackend::MySql,
212 &["ALTER TABLE `users` DROP COLUMN `email`"]
213 )]
214 #[case::delete_column_sqlite(
215 "delete_column_sqlite",
216 DatabaseBackend::Sqlite,
217 &["ALTER TABLE \"users\" DROP COLUMN \"email\""]
218 )]
219 fn test_delete_column(
220 #[case] title: &str,
221 #[case] backend: DatabaseBackend,
222 #[case] expected: &[&str],
223 ) {
224 let result = build_delete_column(&backend, "users", "email", None, &[]);
225 let sql = result[0].build(backend);
226 for exp in expected {
227 assert!(
228 sql.contains(exp),
229 "Expected SQL to contain '{}', got: {}",
230 exp,
231 sql
232 );
233 }
234
235 with_settings!({ snapshot_suffix => format!("delete_column_{}", title) }, {
236 assert_snapshot!(sql);
237 });
238 }
239
240 #[test]
241 fn test_delete_enum_column_postgres() {
242 use vespertide_core::EnumValues;
243
244 let enum_type = ColumnType::Complex(ComplexColumnType::Enum {
245 name: "status".into(),
246 values: EnumValues::String(vec!["active".into(), "inactive".into()]),
247 });
248 let result = build_delete_column(
249 &DatabaseBackend::Postgres,
250 "users",
251 "status",
252 Some(&enum_type),
253 &[],
254 );
255
256 assert_eq!(result.len(), 2);
258
259 let alter_sql = result[0].build(DatabaseBackend::Postgres);
260 assert!(alter_sql.contains("DROP COLUMN"));
261
262 let drop_type_sql = result[1].build(DatabaseBackend::Postgres);
263 assert!(drop_type_sql.contains("DROP TYPE IF EXISTS \"users_status\""));
264
265 let drop_type_mysql = result[1].build(DatabaseBackend::MySql);
267 assert!(drop_type_mysql.is_empty());
268 }
269
270 #[test]
271 fn test_delete_non_enum_column_no_drop_type() {
272 let text_type = ColumnType::Simple(SimpleColumnType::Text);
273 let result = build_delete_column(
274 &DatabaseBackend::Postgres,
275 "users",
276 "name",
277 Some(&text_type),
278 &[],
279 );
280
281 assert_eq!(result.len(), 1);
283 }
284
285 #[test]
286 fn test_delete_column_sqlite_drops_unique_constraint_first() {
287 let schema = vec![TableDef {
289 name: "gift".into(),
290 description: None,
291 columns: vec![
292 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
293 col("gift_code", ColumnType::Simple(SimpleColumnType::Text)),
294 ],
295 constraints: vec![TableConstraint::Unique {
296 name: None,
297 columns: vec!["gift_code".into()],
298 }],
299 }];
300
301 let result =
302 build_delete_column(&DatabaseBackend::Sqlite, "gift", "gift_code", None, &schema);
303
304 assert_eq!(result.len(), 2);
306
307 let drop_index_sql = result[0].build(DatabaseBackend::Sqlite);
308 assert!(
309 drop_index_sql.contains("DROP INDEX"),
310 "Expected DROP INDEX, got: {}",
311 drop_index_sql
312 );
313 assert!(
314 drop_index_sql.contains("uq_gift__gift_code"),
315 "Expected index name uq_gift__gift_code, got: {}",
316 drop_index_sql
317 );
318
319 let drop_column_sql = result[1].build(DatabaseBackend::Sqlite);
320 assert!(
321 drop_column_sql.contains("DROP COLUMN"),
322 "Expected DROP COLUMN, got: {}",
323 drop_column_sql
324 );
325 }
326
327 #[test]
328 fn test_delete_column_sqlite_drops_index_constraint_first() {
329 let schema = vec![TableDef {
331 name: "users".into(),
332 description: None,
333 columns: vec![
334 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
335 col("email", ColumnType::Simple(SimpleColumnType::Text)),
336 ],
337 constraints: vec![TableConstraint::Index {
338 name: None,
339 columns: vec!["email".into()],
340 }],
341 }];
342
343 let result = build_delete_column(&DatabaseBackend::Sqlite, "users", "email", None, &schema);
344
345 assert_eq!(result.len(), 2);
347
348 let drop_index_sql = result[0].build(DatabaseBackend::Sqlite);
349 assert!(drop_index_sql.contains("DROP INDEX"));
350 assert!(drop_index_sql.contains("ix_users__email"));
351
352 let drop_column_sql = result[1].build(DatabaseBackend::Sqlite);
353 assert!(drop_column_sql.contains("DROP COLUMN"));
354 }
355
356 #[test]
357 fn test_delete_column_postgres_does_not_drop_constraints() {
358 let schema = vec![TableDef {
360 name: "gift".into(),
361 description: None,
362 columns: vec![
363 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
364 col("gift_code", ColumnType::Simple(SimpleColumnType::Text)),
365 ],
366 constraints: vec![TableConstraint::Unique {
367 name: None,
368 columns: vec!["gift_code".into()],
369 }],
370 }];
371
372 let result = build_delete_column(
373 &DatabaseBackend::Postgres,
374 "gift",
375 "gift_code",
376 None,
377 &schema,
378 );
379
380 assert_eq!(result.len(), 1);
382
383 let drop_column_sql = result[0].build(DatabaseBackend::Postgres);
384 assert!(drop_column_sql.contains("DROP COLUMN"));
385 }
386
387 #[test]
388 fn test_delete_column_sqlite_with_named_unique_constraint() {
389 let schema = vec![TableDef {
391 name: "gift".into(),
392 description: None,
393 columns: vec![
394 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
395 col("gift_code", ColumnType::Simple(SimpleColumnType::Text)),
396 ],
397 constraints: vec![TableConstraint::Unique {
398 name: Some("gift_code".into()),
399 columns: vec!["gift_code".into()],
400 }],
401 }];
402
403 let result =
404 build_delete_column(&DatabaseBackend::Sqlite, "gift", "gift_code", None, &schema);
405
406 assert_eq!(result.len(), 2);
407
408 let drop_index_sql = result[0].build(DatabaseBackend::Sqlite);
409 assert!(
411 drop_index_sql.contains("uq_gift__gift_code"),
412 "Expected uq_gift__gift_code, got: {}",
413 drop_index_sql
414 );
415 }
416
417 #[test]
418 fn test_delete_column_sqlite_with_fk_uses_temp_table() {
419 let schema = vec![TableDef {
421 name: "gift".into(),
422 description: None,
423 columns: vec![
424 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
425 col("sender_id", ColumnType::Simple(SimpleColumnType::BigInt)),
426 col("message", ColumnType::Simple(SimpleColumnType::Text)),
427 ],
428 constraints: vec![TableConstraint::ForeignKey {
429 name: None,
430 columns: vec!["sender_id".into()],
431 ref_table: "user".into(),
432 ref_columns: vec!["id".into()],
433 on_delete: None,
434 on_update: None,
435 }],
436 }];
437
438 let result =
439 build_delete_column(&DatabaseBackend::Sqlite, "gift", "sender_id", None, &schema);
440
441 assert!(
447 result.len() >= 4,
448 "Expected at least 4 statements for temp table approach, got: {}",
449 result.len()
450 );
451
452 let all_sql: Vec<String> = result
453 .iter()
454 .map(|q| q.build(DatabaseBackend::Sqlite))
455 .collect();
456 let combined_sql = all_sql.join("\n");
457
458 assert!(
460 combined_sql.contains("CREATE TABLE") && combined_sql.contains("gift_temp"),
461 "Expected CREATE TABLE gift_temp, got: {}",
462 combined_sql
463 );
464
465 assert!(
467 !combined_sql.contains("\"sender_id\"") || combined_sql.contains("DROP TABLE"),
468 "New table should not contain sender_id column"
469 );
470
471 assert!(
473 combined_sql.contains("INSERT INTO"),
474 "Expected INSERT INTO for data copy, got: {}",
475 combined_sql
476 );
477
478 assert!(
480 combined_sql.contains("DROP TABLE") && combined_sql.contains("\"gift\""),
481 "Expected DROP TABLE gift, got: {}",
482 combined_sql
483 );
484
485 assert!(
487 combined_sql.contains("RENAME"),
488 "Expected RENAME for temp table, got: {}",
489 combined_sql
490 );
491 }
492
493 #[test]
494 fn test_delete_column_sqlite_with_fk_preserves_other_columns() {
495 let schema = vec![TableDef {
497 name: "gift".into(),
498 description: None,
499 columns: vec![
500 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
501 col("sender_id", ColumnType::Simple(SimpleColumnType::BigInt)),
502 col("receiver_id", ColumnType::Simple(SimpleColumnType::BigInt)),
503 col("message", ColumnType::Simple(SimpleColumnType::Text)),
504 ],
505 constraints: vec![
506 TableConstraint::ForeignKey {
507 name: None,
508 columns: vec!["sender_id".into()],
509 ref_table: "user".into(),
510 ref_columns: vec!["id".into()],
511 on_delete: None,
512 on_update: None,
513 },
514 TableConstraint::Index {
515 name: None,
516 columns: vec!["receiver_id".into()],
517 },
518 ],
519 }];
520
521 let result =
522 build_delete_column(&DatabaseBackend::Sqlite, "gift", "sender_id", None, &schema);
523
524 let all_sql: Vec<String> = result
525 .iter()
526 .map(|q| q.build(DatabaseBackend::Sqlite))
527 .collect();
528 let combined_sql = all_sql.join("\n");
529
530 assert!(combined_sql.contains("\"id\""), "Should preserve id column");
532 assert!(
533 combined_sql.contains("\"receiver_id\""),
534 "Should preserve receiver_id column"
535 );
536 assert!(
537 combined_sql.contains("\"message\""),
538 "Should preserve message column"
539 );
540
541 assert!(
543 combined_sql.contains("CREATE INDEX") && combined_sql.contains("ix_gift__receiver_id"),
544 "Should recreate index on receiver_id, got: {}",
545 combined_sql
546 );
547 }
548
549 #[test]
550 fn test_delete_column_postgres_with_fk_does_not_use_temp_table() {
551 let schema = vec![TableDef {
553 name: "gift".into(),
554 description: None,
555 columns: vec![
556 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
557 col("sender_id", ColumnType::Simple(SimpleColumnType::BigInt)),
558 ],
559 constraints: vec![TableConstraint::ForeignKey {
560 name: None,
561 columns: vec!["sender_id".into()],
562 ref_table: "user".into(),
563 ref_columns: vec!["id".into()],
564 on_delete: None,
565 on_update: None,
566 }],
567 }];
568
569 let result = build_delete_column(
570 &DatabaseBackend::Postgres,
571 "gift",
572 "sender_id",
573 None,
574 &schema,
575 );
576
577 assert_eq!(
579 result.len(),
580 1,
581 "PostgreSQL should only have 1 statement, got: {}",
582 result.len()
583 );
584
585 let sql = result[0].build(DatabaseBackend::Postgres);
586 assert!(
587 sql.contains("DROP COLUMN"),
588 "Expected DROP COLUMN, got: {}",
589 sql
590 );
591 assert!(
592 !sql.contains("gift_temp"),
593 "PostgreSQL should not use temp table"
594 );
595 }
596
597 #[test]
598 fn test_delete_column_sqlite_with_pk_uses_temp_table() {
599 let schema = vec![TableDef {
601 name: "order_items".into(),
602 description: None,
603 columns: vec![
604 col("order_id", ColumnType::Simple(SimpleColumnType::Integer)),
605 col("product_id", ColumnType::Simple(SimpleColumnType::Integer)),
606 col("quantity", ColumnType::Simple(SimpleColumnType::Integer)),
607 ],
608 constraints: vec![TableConstraint::PrimaryKey {
609 auto_increment: false,
610 columns: vec!["order_id".into(), "product_id".into()],
611 }],
612 }];
613
614 let result = build_delete_column(
615 &DatabaseBackend::Sqlite,
616 "order_items",
617 "product_id",
618 None,
619 &schema,
620 );
621
622 assert!(
624 result.len() >= 4,
625 "Expected at least 4 statements for temp table approach, got: {}",
626 result.len()
627 );
628
629 let all_sql: Vec<String> = result
630 .iter()
631 .map(|q| q.build(DatabaseBackend::Sqlite))
632 .collect();
633 let combined_sql = all_sql.join("\n");
634
635 assert!(
636 combined_sql.contains("order_items_temp"),
637 "Should use temp table approach for PK column deletion"
638 );
639 }
640
641 #[test]
642 fn test_delete_column_sqlite_unique_on_different_column_not_dropped() {
643 let schema = vec![TableDef {
646 name: "users".into(),
647 description: None,
648 columns: vec![
649 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
650 col("email", ColumnType::Simple(SimpleColumnType::Text)),
651 col("nickname", ColumnType::Simple(SimpleColumnType::Text)),
652 ],
653 constraints: vec![
654 TableConstraint::Unique {
656 name: None,
657 columns: vec!["email".into()],
658 },
659 ],
660 }];
661
662 let result =
664 build_delete_column(&DatabaseBackend::Sqlite, "users", "nickname", None, &schema);
665
666 assert_eq!(
668 result.len(),
669 1,
670 "Should not drop UNIQUE on email when deleting nickname, got: {} statements",
671 result.len()
672 );
673
674 let sql = result[0].build(DatabaseBackend::Sqlite);
675 assert!(
676 sql.contains("DROP COLUMN"),
677 "Expected DROP COLUMN, got: {}",
678 sql
679 );
680 assert!(
681 !sql.contains("DROP INDEX"),
682 "Should NOT drop the email UNIQUE constraint when deleting nickname"
683 );
684 }
685
686 #[test]
687 fn test_delete_column_sqlite_temp_table_filters_constraints_correctly() {
688 let schema = vec![TableDef {
692 name: "orders".into(),
693 description: None,
694 columns: vec![
695 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
696 col("user_id", ColumnType::Simple(SimpleColumnType::BigInt)),
697 col("status", ColumnType::Simple(SimpleColumnType::Text)),
698 col(
699 "created_at",
700 ColumnType::Simple(SimpleColumnType::Timestamp),
701 ),
702 ],
703 constraints: vec![
704 TableConstraint::ForeignKey {
706 name: None,
707 columns: vec!["user_id".into()],
708 ref_table: "users".into(),
709 ref_columns: vec!["id".into()],
710 on_delete: None,
711 on_update: None,
712 },
713 TableConstraint::Index {
715 name: None,
716 columns: vec!["created_at".into()],
717 },
718 TableConstraint::ForeignKey {
720 name: None,
721 columns: vec!["status".into()],
722 ref_table: "statuses".into(),
723 ref_columns: vec!["code".into()],
724 on_delete: None,
725 on_update: None,
726 },
727 ],
728 }];
729
730 let result =
731 build_delete_column(&DatabaseBackend::Sqlite, "orders", "user_id", None, &schema);
732
733 let all_sql: Vec<String> = result
734 .iter()
735 .map(|q| q.build(DatabaseBackend::Sqlite))
736 .collect();
737 let combined_sql = all_sql.join("\n");
738
739 assert!(
741 combined_sql.contains("orders_temp"),
742 "Should use temp table approach for FK column deletion"
743 );
744
745 assert!(
747 combined_sql.contains("ix_orders__created_at"),
748 "Index on created_at should be recreated, got: {}",
749 combined_sql
750 );
751
752 assert!(
755 combined_sql.contains("REFERENCES \"statuses\""),
756 "FK on status should be preserved, got: {}",
757 combined_sql
758 );
759
760 let fk_patterns = combined_sql.matches("REFERENCES").count();
762 assert_eq!(
763 fk_patterns, 1,
764 "Only the FK on status should exist (not the one on user_id), got: {}",
765 combined_sql
766 );
767 }
768
769 fn build_sql_snapshot(result: &[BuiltQuery], backend: DatabaseBackend) -> String {
772 result
773 .iter()
774 .map(|q| q.build(backend))
775 .collect::<Vec<_>>()
776 .join(";\n")
777 }
778
779 #[rstest]
780 #[case::postgres("postgres", DatabaseBackend::Postgres)]
781 #[case::mysql("mysql", DatabaseBackend::MySql)]
782 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
783 fn test_delete_column_with_unique_constraint(
784 #[case] title: &str,
785 #[case] backend: DatabaseBackend,
786 ) {
787 let schema = vec![TableDef {
788 name: "users".into(),
789 description: None,
790 columns: vec![
791 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
792 col("email", ColumnType::Simple(SimpleColumnType::Text)),
793 col("name", ColumnType::Simple(SimpleColumnType::Text)),
794 ],
795 constraints: vec![TableConstraint::Unique {
796 name: None,
797 columns: vec!["email".into()],
798 }],
799 }];
800
801 let result = build_delete_column(&backend, "users", "email", None, &schema);
802 let sql = build_sql_snapshot(&result, backend);
803
804 with_settings!({ snapshot_suffix => format!("delete_column_with_unique_{}", title) }, {
805 assert_snapshot!(sql);
806 });
807 }
808
809 #[rstest]
810 #[case::postgres("postgres", DatabaseBackend::Postgres)]
811 #[case::mysql("mysql", DatabaseBackend::MySql)]
812 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
813 fn test_delete_column_with_index_constraint(
814 #[case] title: &str,
815 #[case] backend: DatabaseBackend,
816 ) {
817 let schema = vec![TableDef {
818 name: "posts".into(),
819 description: None,
820 columns: vec![
821 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
822 col(
823 "created_at",
824 ColumnType::Simple(SimpleColumnType::Timestamp),
825 ),
826 col("title", ColumnType::Simple(SimpleColumnType::Text)),
827 ],
828 constraints: vec![TableConstraint::Index {
829 name: None,
830 columns: vec!["created_at".into()],
831 }],
832 }];
833
834 let result = build_delete_column(&backend, "posts", "created_at", None, &schema);
835 let sql = build_sql_snapshot(&result, backend);
836
837 with_settings!({ snapshot_suffix => format!("delete_column_with_index_{}", title) }, {
838 assert_snapshot!(sql);
839 });
840 }
841
842 #[rstest]
843 #[case::postgres("postgres", DatabaseBackend::Postgres)]
844 #[case::mysql("mysql", DatabaseBackend::MySql)]
845 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
846 fn test_delete_column_with_fk_constraint(
847 #[case] title: &str,
848 #[case] backend: DatabaseBackend,
849 ) {
850 let schema = vec![TableDef {
851 name: "orders".into(),
852 description: None,
853 columns: vec![
854 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
855 col("user_id", ColumnType::Simple(SimpleColumnType::BigInt)),
856 col("total", ColumnType::Simple(SimpleColumnType::Integer)),
857 ],
858 constraints: vec![TableConstraint::ForeignKey {
859 name: None,
860 columns: vec!["user_id".into()],
861 ref_table: "users".into(),
862 ref_columns: vec!["id".into()],
863 on_delete: None,
864 on_update: None,
865 }],
866 }];
867
868 let result = build_delete_column(&backend, "orders", "user_id", None, &schema);
869 let sql = build_sql_snapshot(&result, backend);
870
871 with_settings!({ snapshot_suffix => format!("delete_column_with_fk_{}", title) }, {
872 assert_snapshot!(sql);
873 });
874 }
875
876 #[rstest]
877 #[case::postgres("postgres", DatabaseBackend::Postgres)]
878 #[case::mysql("mysql", DatabaseBackend::MySql)]
879 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
880 fn test_delete_column_with_pk_constraint(
881 #[case] title: &str,
882 #[case] backend: DatabaseBackend,
883 ) {
884 let schema = vec![TableDef {
885 name: "order_items".into(),
886 description: None,
887 columns: vec![
888 col("order_id", ColumnType::Simple(SimpleColumnType::Integer)),
889 col("product_id", ColumnType::Simple(SimpleColumnType::Integer)),
890 col("quantity", ColumnType::Simple(SimpleColumnType::Integer)),
891 ],
892 constraints: vec![TableConstraint::PrimaryKey {
893 auto_increment: false,
894 columns: vec!["order_id".into(), "product_id".into()],
895 }],
896 }];
897
898 let result = build_delete_column(&backend, "order_items", "product_id", None, &schema);
899 let sql = build_sql_snapshot(&result, backend);
900
901 with_settings!({ snapshot_suffix => format!("delete_column_with_pk_{}", title) }, {
902 assert_snapshot!(sql);
903 });
904 }
905
906 #[rstest]
907 #[case::postgres("postgres", DatabaseBackend::Postgres)]
908 #[case::mysql("mysql", DatabaseBackend::MySql)]
909 #[case::sqlite("sqlite", DatabaseBackend::Sqlite)]
910 fn test_delete_column_with_fk_and_index_constraints(
911 #[case] title: &str,
912 #[case] backend: DatabaseBackend,
913 ) {
914 let schema = vec![TableDef {
916 name: "orders".into(),
917 description: None,
918 columns: vec![
919 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
920 col("user_id", ColumnType::Simple(SimpleColumnType::BigInt)),
921 col(
922 "created_at",
923 ColumnType::Simple(SimpleColumnType::Timestamp),
924 ),
925 col("total", ColumnType::Simple(SimpleColumnType::Integer)),
926 ],
927 constraints: vec![
928 TableConstraint::ForeignKey {
929 name: None,
930 columns: vec!["user_id".into()],
931 ref_table: "users".into(),
932 ref_columns: vec!["id".into()],
933 on_delete: None,
934 on_update: None,
935 },
936 TableConstraint::Index {
937 name: None,
938 columns: vec!["created_at".into()],
939 },
940 ],
941 }];
942
943 let result = build_delete_column(&backend, "orders", "user_id", None, &schema);
944 let sql = build_sql_snapshot(&result, backend);
945
946 with_settings!({ snapshot_suffix => format!("delete_column_with_fk_and_index_{}", title) }, {
947 assert_snapshot!(sql);
948 });
949 }
950
951 #[test]
952 fn test_delete_column_sqlite_temp_table_with_enum_column() {
953 use vespertide_core::EnumValues;
956
957 let enum_type = ColumnType::Complex(ComplexColumnType::Enum {
958 name: "order_status".into(),
959 values: EnumValues::String(vec![
960 "pending".into(),
961 "shipped".into(),
962 "delivered".into(),
963 ]),
964 });
965
966 let schema = vec![TableDef {
967 name: "orders".into(),
968 description: None,
969 columns: vec![
970 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
971 col("user_id", ColumnType::Simple(SimpleColumnType::BigInt)),
972 col("status", enum_type.clone()),
973 ],
974 constraints: vec![TableConstraint::ForeignKey {
975 name: None,
976 columns: vec!["user_id".into()],
977 ref_table: "users".into(),
978 ref_columns: vec!["id".into()],
979 on_delete: None,
980 on_update: None,
981 }],
982 }];
983
984 let result = build_delete_column(
986 &DatabaseBackend::Sqlite,
987 "orders",
988 "user_id",
989 Some(&enum_type),
990 &schema,
991 );
992
993 assert!(
995 result.len() >= 4,
996 "Expected at least 4 statements for temp table approach, got: {}",
997 result.len()
998 );
999
1000 let all_sql: Vec<String> = result
1001 .iter()
1002 .map(|q| q.build(DatabaseBackend::Sqlite))
1003 .collect();
1004 let combined_sql = all_sql.join("\n");
1005
1006 assert!(
1008 combined_sql.contains("orders_temp"),
1009 "Should use temp table approach"
1010 );
1011
1012 let last_stmt = result.last().unwrap();
1015 let last_sql = last_stmt.build(DatabaseBackend::Sqlite);
1016 assert!(
1018 last_sql.is_empty() || !last_sql.contains("DROP TYPE"),
1019 "SQLite should not emit DROP TYPE"
1020 );
1021
1022 let pg_last_sql = last_stmt.build(DatabaseBackend::Postgres);
1024 assert!(
1025 pg_last_sql.contains("DROP TYPE"),
1026 "PostgreSQL should emit DROP TYPE, got: {}",
1027 pg_last_sql
1028 );
1029 }
1030
1031 #[test]
1032 fn test_delete_column_sqlite_with_check_constraint_skipped() {
1033 let schema = vec![TableDef {
1036 name: "orders".into(),
1037 description: None,
1038 columns: vec![
1039 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1040 col("amount", ColumnType::Simple(SimpleColumnType::Integer)),
1041 ],
1042 constraints: vec![TableConstraint::Check {
1043 name: "check_positive".into(),
1044 expr: "amount > 0".into(),
1045 }],
1046 }];
1047
1048 let result =
1050 build_delete_column(&DatabaseBackend::Sqlite, "orders", "amount", None, &schema);
1051
1052 assert_eq!(
1055 result.len(),
1056 1,
1057 "Check constraint should be skipped, got: {} statements",
1058 result.len()
1059 );
1060
1061 let sql = result[0].build(DatabaseBackend::Sqlite);
1062 assert!(
1063 sql.contains("DROP COLUMN"),
1064 "Expected DROP COLUMN, got: {}",
1065 sql
1066 );
1067 }
1068}