1mod display;
2mod narrowing_strategy;
3mod prefix;
4mod remap_mapping_serde;
5
6use crate::schema::{ColumnDef, ColumnName, ColumnType, TableConstraint, TableName};
7pub use narrowing_strategy::NarrowingStrategy;
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21#[serde(rename_all = "snake_case")]
22pub struct MigrationPlan {
23 #[serde(default)]
26 pub id: String,
27 pub comment: Option<String>,
29 #[serde(default)]
31 pub created_at: Option<String>,
32 pub version: u32,
34 pub actions: Vec<MigrationAction>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
51#[serde(tag = "type", rename_all = "snake_case")]
52#[non_exhaustive]
53pub enum MigrationAction {
54 CreateTable {
56 table: TableName,
57 columns: Vec<ColumnDef>,
58 constraints: Vec<TableConstraint>,
59 },
60 DeleteTable { table: TableName },
62 AddColumn {
64 table: TableName,
65 column: Box<ColumnDef>,
66 fill_with: Option<String>,
68 },
69 RenameColumn {
71 table: TableName,
72 from: ColumnName,
73 to: ColumnName,
74 },
75 DeleteColumn {
77 table: TableName,
78 column: ColumnName,
79 },
80 ModifyColumnType {
82 table: TableName,
83 column: ColumnName,
84 new_type: ColumnType,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
88 fill_with: Option<BTreeMap<String, String>>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
95 narrowing_strategy: Option<NarrowingStrategy>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
103 timezone: Option<String>,
104 },
105 ModifyColumnNullable {
107 table: TableName,
108 column: ColumnName,
109 nullable: bool,
110 fill_with: Option<String>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
116 delete_null_rows: Option<bool>,
117 },
118 ModifyColumnDefault {
120 table: TableName,
121 column: ColumnName,
122 new_default: Option<String>,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
137 backfill: Option<String>,
138 },
139 ModifyColumnComment {
141 table: TableName,
142 column: ColumnName,
143 new_comment: Option<String>,
145 },
146 AddConstraint {
154 table: TableName,
155 constraint: TableConstraint,
156 },
157 RemoveConstraint {
159 table: TableName,
160 constraint: TableConstraint,
161 },
162 ReplaceConstraint {
164 table: TableName,
165 from: TableConstraint,
166 to: TableConstraint,
167 },
168 RemapEnumValues {
191 table: TableName,
192 column: ColumnName,
193 #[serde(with = "remap_mapping_serde")]
194 #[cfg_attr(feature = "schema", schemars(with = "BTreeMap<String, i64>"))]
195 mapping: BTreeMap<i64, i64>,
196 },
197 RenameTable { from: TableName, to: TableName },
199 RawSql { sql: String },
205}
206
207impl MigrationAction {
208 #[must_use]
211 pub fn table_name(&self) -> Option<&str> {
212 match self {
213 Self::CreateTable { table, .. }
214 | Self::DeleteTable { table }
215 | Self::AddColumn { table, .. }
216 | Self::DeleteColumn { table, .. }
217 | Self::RenameColumn { table, .. }
218 | Self::ModifyColumnType { table, .. }
219 | Self::ModifyColumnNullable { table, .. }
220 | Self::ModifyColumnDefault { table, .. }
221 | Self::ModifyColumnComment { table, .. }
222 | Self::AddConstraint { table, .. }
223 | Self::RemoveConstraint { table, .. }
224 | Self::ReplaceConstraint { table, .. }
225 | Self::RemapEnumValues { table, .. } => Some(table.as_str()),
226 Self::RenameTable { from, .. } => Some(from.as_str()),
227 Self::RawSql { .. } => None,
228 }
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use crate::schema::{ReferenceAction, SimpleColumnType};
236 use rstest::rstest;
237
238 fn default_column() -> ColumnDef {
239 ColumnDef::new("email", ColumnType::Simple(SimpleColumnType::Text), true)
240 }
241
242 fn idx(name: Option<&str>, cols: &[&str]) -> TableConstraint {
243 TableConstraint::Index {
244 name: name.map(Into::into),
245 columns: cols.iter().map(|c| (*c).into()).collect(),
246 }
247 }
248 fn pk_id() -> TableConstraint {
249 TableConstraint::PrimaryKey {
250 auto_increment: false,
251 columns: vec!["id".into()],
252 strategy: crate::PrimaryKeyAdditionStrategy::default(),
253 }
254 }
255 fn pk_id_auto() -> TableConstraint {
256 TableConstraint::PrimaryKey {
257 auto_increment: true,
258 columns: vec!["id".into()],
259 strategy: crate::PrimaryKeyAdditionStrategy::default(),
260 }
261 }
262 fn uq_email(name: Option<&str>) -> TableConstraint {
263 TableConstraint::Unique {
264 name: name.map(Into::into),
265 columns: vec!["email".into()],
266 strategy: crate::schema::UniqueConstraintStrategy::DeleteDuplicates {
267 keep: crate::schema::KeepPolicy::First,
268 },
269 }
270 }
271 fn fk_user(name: Option<&str>, on_delete: Option<ReferenceAction>) -> TableConstraint {
272 TableConstraint::ForeignKey {
273 name: name.map(Into::into),
274 columns: vec!["user_id".into()],
275 ref_table: "users".into(),
276 ref_columns: vec!["id".into()],
277 on_delete,
278 on_update: None,
279 orphan_strategy: crate::ForeignKeyOrphanStrategy::default(),
280 }
281 }
282 fn chk(name: &str, expr: &str) -> TableConstraint {
283 TableConstraint::Check {
284 name: name.into(),
285 expr: expr.into(),
286 strategy: crate::CheckViolationStrategy::default(),
287 }
288 }
289 fn idx_email(name: Option<&str>) -> TableConstraint {
290 idx(name, &["email"])
291 }
292
293 #[test]
294 fn migration_action_wire_format_round_trip() {
295 let canonical = r#"{"type":"create_table","table":"user","columns":[],"constraints":[]}"#;
296 let parsed: MigrationAction = serde_json::from_str(canonical).expect("parse");
297 let reserialized = serde_json::to_string(&parsed).expect("serialize");
298
299 assert_eq!(
300 reserialized, canonical,
301 "wire format MUST be byte-identical"
302 );
303 }
304
305 #[test]
306 fn migration_action_rename_column_wire_format() {
307 let canonical = r#"{"type":"rename_column","table":"orders","from":"old","to":"new"}"#;
308 let parsed: MigrationAction = serde_json::from_str(canonical).expect("parse");
309 let reserialized = serde_json::to_string(&parsed).expect("serialize");
310
311 assert_eq!(reserialized, canonical);
312 }
313
314 #[test]
315 fn migration_plan_real_example_round_trip() {
316 let plan_json =
317 include_str!("../../../../examples/app/migrations/0001_init.vespertide.json");
318 let parsed: MigrationPlan =
319 serde_json::from_str(plan_json).expect("real migration plan parses");
320 let reserialized = serde_json::to_string(&parsed).expect("serialize");
321 let reparsed: MigrationPlan = serde_json::from_str(&reserialized).expect("round-trip");
322
323 assert_eq!(
324 parsed, reparsed,
325 "semantic content preserved across round-trip"
326 );
327 }
328
329 #[rstest]
330 #[case::create_table(
331 MigrationAction::CreateTable {
332 table: "users".into(),
333 columns: vec![],
334 constraints: vec![],
335 },
336 "CreateTable: users"
337 )]
338 #[case::delete_table(
339 MigrationAction::DeleteTable {
340 table: "users".into(),
341 },
342 "DeleteTable: users"
343 )]
344 #[case::add_column(
345 MigrationAction::AddColumn {
346 table: "users".into(),
347 column: Box::new(default_column()),
348 fill_with: None,
349 },
350 "AddColumn: users.email"
351 )]
352 #[case::rename_column(
353 MigrationAction::RenameColumn {
354 table: "users".into(),
355 from: "old_name".into(),
356 to: "new_name".into(),
357 },
358 "RenameColumn: users.old_name -> new_name"
359 )]
360 #[case::delete_column(
361 MigrationAction::DeleteColumn {
362 table: "users".into(),
363 column: "email".into(),
364 },
365 "DeleteColumn: users.email"
366 )]
367 #[case::modify_column_type(
368 MigrationAction::ModifyColumnType {
369 table: "users".into(),
370 column: "age".into(),
371 new_type: ColumnType::Simple(SimpleColumnType::Integer),
372 fill_with: None,
373 narrowing_strategy: None,
374 timezone: None,
375 },
376 "ModifyColumnType: users.age"
377 )]
378 #[case::add_constraint_index_with_name(
379 MigrationAction::AddConstraint { table: "users".into(), constraint: idx_email(Some("ix_users__email")) },
380 "AddConstraint: users.ix_users__email (INDEX)"
381 )]
382 #[case::add_constraint_index_without_name(
383 MigrationAction::AddConstraint { table: "users".into(), constraint: idx_email(None) },
384 "AddConstraint: users.INDEX"
385 )]
386 #[case::remove_constraint_index_with_name(
387 MigrationAction::RemoveConstraint { table: "users".into(), constraint: idx_email(Some("ix_users__email")) },
388 "RemoveConstraint: users.ix_users__email (INDEX)"
389 )]
390 #[case::remove_constraint_index_without_name(
391 MigrationAction::RemoveConstraint { table: "users".into(), constraint: idx_email(None) },
392 "RemoveConstraint: users.INDEX"
393 )]
394 #[case::rename_table(
395 MigrationAction::RenameTable {
396 from: "old_table".into(),
397 to: "new_table".into(),
398 },
399 "RenameTable: old_table -> new_table"
400 )]
401 fn test_display_basic_actions(#[case] action: MigrationAction, #[case] expected: &str) {
402 assert_eq!(action.to_string(), expected);
403 }
404
405 #[test]
406 fn test_display_raw_sql_truncates_unicode_without_panicking() {
407 let sql = "COMMENT ON COLUMN 한국어테이블.이름 IS '日本語 café 📊';".repeat(3);
408 let action = MigrationAction::RawSql { sql };
409
410 let display = action.to_string();
411
412 assert!(display.starts_with("RawSql: COMMENT ON COLUMN 한국어테이블"));
413 assert!(display.ends_with("..."));
414 }
415
416 #[rstest]
417 #[case::create_table(
418 MigrationAction::CreateTable {
419 table: "users".into(),
420 columns: vec![],
421 constraints: vec![],
422 },
423 Some("users")
424 )]
425 #[case::rename_table(
426 MigrationAction::RenameTable {
427 from: "old_users".into(),
428 to: "users".into(),
429 },
430 Some("old_users")
431 )]
432 #[case::raw_sql(MigrationAction::RawSql { sql: "SELECT 1".into() }, None)]
433 fn test_table_name(#[case] action: MigrationAction, #[case] expected: Option<&str>) {
434 assert_eq!(action.table_name(), expected);
435 }
436
437 #[rstest]
438 #[case::add_constraint_primary_key(
439 MigrationAction::AddConstraint { table: "users".into(), constraint: pk_id() },
440 "AddConstraint: users.PRIMARY KEY"
441 )]
442 #[case::add_constraint_unique_with_name(
443 MigrationAction::AddConstraint { table: "users".into(), constraint: uq_email(Some("uq_email")) },
444 "AddConstraint: users.uq_email (UNIQUE)"
445 )]
446 #[case::add_constraint_unique_without_name(
447 MigrationAction::AddConstraint { table: "users".into(), constraint: uq_email(None) },
448 "AddConstraint: users.UNIQUE"
449 )]
450 #[case::add_constraint_foreign_key_with_name(
451 MigrationAction::AddConstraint { table: "posts".into(), constraint: fk_user(Some("fk_user"), Some(ReferenceAction::Cascade)) },
452 "AddConstraint: posts.fk_user (FOREIGN KEY)"
453 )]
454 #[case::add_constraint_foreign_key_without_name(
455 MigrationAction::AddConstraint { table: "posts".into(), constraint: fk_user(None, None) },
456 "AddConstraint: posts.FOREIGN KEY"
457 )]
458 #[case::add_constraint_check(
459 MigrationAction::AddConstraint { table: "users".into(), constraint: chk("chk_age", "age > 0") },
460 "AddConstraint: users.chk_age (CHECK)"
461 )]
462 fn test_display_add_constraint(#[case] action: MigrationAction, #[case] expected: &str) {
463 assert_eq!(action.to_string(), expected);
464 }
465
466 #[rstest]
467 #[case::remove_constraint_primary_key(
468 MigrationAction::RemoveConstraint { table: "users".into(), constraint: pk_id() },
469 "RemoveConstraint: users.PRIMARY KEY"
470 )]
471 #[case::remove_constraint_unique_with_name(
472 MigrationAction::RemoveConstraint { table: "users".into(), constraint: uq_email(Some("uq_email")) },
473 "RemoveConstraint: users.uq_email (UNIQUE)"
474 )]
475 #[case::remove_constraint_unique_without_name(
476 MigrationAction::RemoveConstraint { table: "users".into(), constraint: uq_email(None) },
477 "RemoveConstraint: users.UNIQUE"
478 )]
479 #[case::remove_constraint_foreign_key_with_name(
480 MigrationAction::RemoveConstraint { table: "posts".into(), constraint: fk_user(Some("fk_user"), None) },
481 "RemoveConstraint: posts.fk_user (FOREIGN KEY)"
482 )]
483 #[case::remove_constraint_foreign_key_without_name(
484 MigrationAction::RemoveConstraint { table: "posts".into(), constraint: fk_user(None, None) },
485 "RemoveConstraint: posts.FOREIGN KEY"
486 )]
487 #[case::remove_constraint_check(
488 MigrationAction::RemoveConstraint { table: "users".into(), constraint: chk("chk_age", "age > 0") },
489 "RemoveConstraint: users.chk_age (CHECK)"
490 )]
491 fn test_display_remove_constraint(#[case] action: MigrationAction, #[case] expected: &str) {
492 assert_eq!(action.to_string(), expected);
493 }
494
495 #[rstest]
496 #[case::raw_sql_short(
497 MigrationAction::RawSql {
498 sql: "SELECT 1".into(),
499 },
500 "RawSql: SELECT 1"
501 )]
502 fn test_display_raw_sql_short(#[case] action: MigrationAction, #[case] expected: &str) {
503 assert_eq!(action.to_string(), expected);
504 }
505
506 #[test]
507 fn test_display_raw_sql_long() {
508 let action = MigrationAction::RawSql {
509 sql:
510 "SELECT * FROM users WHERE id = 1 AND name = 'test' AND email = 'test@example.com'"
511 .into(),
512 };
513 let result = action.to_string();
514 assert!(result.starts_with("RawSql: "));
515 assert!(result.ends_with("..."));
516 assert!(result.len() > 10);
517 }
518
519 #[rstest]
520 #[case::modify_column_nullable_to_not_null(
521 MigrationAction::ModifyColumnNullable {
522 table: "users".into(),
523 column: "email".into(),
524 nullable: false,
525 fill_with: None,
526 delete_null_rows: None,
527 },
528 "ModifyColumnNullable: users.email -> NOT NULL"
529 )]
530 #[case::modify_column_nullable_to_null(
531 MigrationAction::ModifyColumnNullable {
532 table: "users".into(),
533 column: "email".into(),
534 nullable: true,
535 fill_with: None,
536 delete_null_rows: None,
537 },
538 "ModifyColumnNullable: users.email -> NULL"
539 )]
540 fn test_display_modify_column_nullable(
541 #[case] action: MigrationAction,
542 #[case] expected: &str,
543 ) {
544 assert_eq!(action.to_string(), expected);
545 }
546
547 #[rstest]
548 #[case::modify_column_default_set(
549 MigrationAction::ModifyColumnDefault {
550 table: "users".into(),
551 column: "status".into(),
552 new_default: Some("'active'".into()),
553 backfill: None,
554 },
555 "ModifyColumnDefault: users.status -> 'active'"
556 )]
557 #[case::modify_column_default_drop(
558 MigrationAction::ModifyColumnDefault {
559 table: "users".into(),
560 column: "status".into(),
561 new_default: None,
562 backfill: None,
563 },
564 "ModifyColumnDefault: users.status -> (none)"
565 )]
566 fn test_display_modify_column_default(#[case] action: MigrationAction, #[case] expected: &str) {
567 assert_eq!(action.to_string(), expected);
568 }
569
570 #[rstest]
571 #[case::modify_column_comment_set(
572 MigrationAction::ModifyColumnComment {
573 table: "users".into(),
574 column: "email".into(),
575 new_comment: Some("User email address".into()),
576 },
577 "ModifyColumnComment: users.email -> 'User email address'"
578 )]
579 #[case::modify_column_comment_drop(
580 MigrationAction::ModifyColumnComment {
581 table: "users".into(),
582 column: "email".into(),
583 new_comment: None,
584 },
585 "ModifyColumnComment: users.email -> (none)"
586 )]
587 fn test_display_modify_column_comment(#[case] action: MigrationAction, #[case] expected: &str) {
588 assert_eq!(action.to_string(), expected);
589 }
590
591 #[test]
592 fn test_display_modify_column_comment_long() {
593 let action = MigrationAction::ModifyColumnComment {
595 table: "users".into(),
596 column: "email".into(),
597 new_comment: Some(
598 "This is a very long comment that should be truncated in display".into(),
599 ),
600 };
601 let result = action.to_string();
602 assert!(result.contains("..."));
603 assert!(result.contains("This is a very long comment"));
604 assert!(!result.contains("truncated in display"));
606 }
607
608 #[test]
610 fn test_action_with_prefix_create_table() {
611 let action = MigrationAction::CreateTable {
612 table: "users".into(),
613 columns: vec![default_column()],
614 constraints: vec![TableConstraint::ForeignKey {
615 name: Some("fk_org".into()),
616 columns: vec!["org_id".into()],
617 ref_table: "organizations".into(),
618 ref_columns: vec!["id".into()],
619 on_delete: None,
620 on_update: None,
621 orphan_strategy: crate::ForeignKeyOrphanStrategy::default(),
622 }],
623 };
624 let prefixed = action.with_prefix("myapp_");
625 if let MigrationAction::CreateTable {
626 table, constraints, ..
627 } = prefixed
628 {
629 assert_eq!(table.as_str(), "myapp_users");
630 if let TableConstraint::ForeignKey { ref_table, .. } = &constraints[0] {
631 assert_eq!(ref_table.as_str(), "myapp_organizations");
632 }
633 } else {
634 panic!("Expected CreateTable");
635 }
636 }
637
638 #[test]
639 fn test_action_with_prefix_delete_table() {
640 let action = MigrationAction::DeleteTable {
641 table: "users".into(),
642 };
643 let prefixed = action.with_prefix("myapp_");
644 if let MigrationAction::DeleteTable { table } = prefixed {
645 assert_eq!(table.as_str(), "myapp_users");
646 } else {
647 panic!("Expected DeleteTable");
648 }
649 }
650
651 #[test]
652 fn test_action_with_prefix_add_column() {
653 let action = MigrationAction::AddColumn {
654 table: "users".into(),
655 column: Box::new(default_column()),
656 fill_with: None,
657 };
658 let prefixed = action.with_prefix("myapp_");
659 if let MigrationAction::AddColumn { table, .. } = prefixed {
660 assert_eq!(table.as_str(), "myapp_users");
661 } else {
662 panic!("Expected AddColumn");
663 }
664 }
665
666 #[test]
667 fn test_action_with_prefix_rename_table() {
668 let action = MigrationAction::RenameTable {
669 from: "old_table".into(),
670 to: "new_table".into(),
671 };
672 let prefixed = action.with_prefix("myapp_");
673 if let MigrationAction::RenameTable { from, to } = prefixed {
674 assert_eq!(from.as_str(), "myapp_old_table");
675 assert_eq!(to.as_str(), "myapp_new_table");
676 } else {
677 panic!("Expected RenameTable");
678 }
679 }
680
681 #[test]
682 fn test_action_with_prefix_raw_sql_unchanged() {
683 let action = MigrationAction::RawSql {
684 sql: "SELECT * FROM users".into(),
685 };
686 let prefixed = action.with_prefix("myapp_");
687 if let MigrationAction::RawSql { sql } = prefixed {
688 assert_eq!(sql, "SELECT * FROM users");
690 } else {
691 panic!("Expected RawSql");
692 }
693 }
694
695 #[test]
696 fn test_action_with_prefix_empty_prefix() {
697 let action = MigrationAction::CreateTable {
698 table: "users".into(),
699 columns: vec![],
700 constraints: vec![],
701 };
702 let prefixed = action.clone().with_prefix("");
703 if let MigrationAction::CreateTable { table, .. } = prefixed {
704 assert_eq!(table.as_str(), "users");
705 }
706 }
707
708 #[test]
709 fn test_migration_plan_with_prefix() {
710 let plan = MigrationPlan {
711 id: String::new(),
712 comment: Some("test".into()),
713 created_at: None,
714 version: 1,
715 actions: vec![
716 MigrationAction::CreateTable {
717 table: "users".into(),
718 columns: vec![],
719 constraints: vec![],
720 },
721 MigrationAction::CreateTable {
722 table: "posts".into(),
723 columns: vec![],
724 constraints: vec![fk_user(Some("fk_user"), None)],
725 },
726 ],
727 };
728 let prefixed = plan.with_prefix("myapp_");
729 assert_eq!(prefixed.actions.len(), 2);
730
731 if let MigrationAction::CreateTable { table, .. } = &prefixed.actions[0] {
732 assert_eq!(table.as_str(), "myapp_users");
733 }
734 if let MigrationAction::CreateTable {
735 table, constraints, ..
736 } = &prefixed.actions[1]
737 {
738 assert_eq!(table.as_str(), "myapp_posts");
739 if let TableConstraint::ForeignKey { ref_table, .. } = &constraints[0] {
740 assert_eq!(ref_table.as_str(), "myapp_users");
741 }
742 }
743 }
744
745 #[test]
746 fn test_action_with_prefix_rename_column() {
747 let action = MigrationAction::RenameColumn {
748 table: "users".into(),
749 from: "name".into(),
750 to: "full_name".into(),
751 };
752 let prefixed = action.with_prefix("myapp_");
753 if let MigrationAction::RenameColumn { table, from, to } = prefixed {
754 assert_eq!(table.as_str(), "myapp_users");
755 assert_eq!(from.as_str(), "name");
756 assert_eq!(to.as_str(), "full_name");
757 } else {
758 panic!("Expected RenameColumn");
759 }
760 }
761
762 #[test]
763 fn test_action_with_prefix_delete_column() {
764 let action = MigrationAction::DeleteColumn {
765 table: "users".into(),
766 column: "old_field".into(),
767 };
768 let prefixed = action.with_prefix("myapp_");
769 if let MigrationAction::DeleteColumn { table, column } = prefixed {
770 assert_eq!(table.as_str(), "myapp_users");
771 assert_eq!(column.as_str(), "old_field");
772 } else {
773 panic!("Expected DeleteColumn");
774 }
775 }
776
777 #[test]
778 fn test_action_with_prefix_modify_column_type() {
779 let action = MigrationAction::ModifyColumnType {
780 table: "users".into(),
781 column: "age".into(),
782 new_type: ColumnType::Simple(SimpleColumnType::BigInt),
783 fill_with: None,
784 narrowing_strategy: None,
785 timezone: None,
786 };
787 let prefixed = action.with_prefix("myapp_");
788 if let MigrationAction::ModifyColumnType {
789 table,
790 column,
791 new_type,
792 fill_with,
793 ..
794 } = prefixed
795 {
796 assert_eq!(table.as_str(), "myapp_users");
797 assert_eq!(column.as_str(), "age");
798 assert!(matches!(
799 new_type,
800 ColumnType::Simple(SimpleColumnType::BigInt)
801 ));
802 assert_eq!(fill_with, None);
803 } else {
804 panic!("Expected ModifyColumnType");
805 }
806 }
807
808 #[test]
809 fn test_action_with_prefix_modify_column_nullable() {
810 let action = MigrationAction::ModifyColumnNullable {
811 table: "users".into(),
812 column: "email".into(),
813 nullable: false,
814 fill_with: Some("default@example.com".into()),
815 delete_null_rows: None,
816 };
817 let prefixed = action.with_prefix("myapp_");
818 if let MigrationAction::ModifyColumnNullable {
819 table,
820 column,
821 nullable,
822 fill_with,
823 delete_null_rows,
824 } = prefixed
825 {
826 assert_eq!(table.as_str(), "myapp_users");
827 assert_eq!(column.as_str(), "email");
828 assert!(!nullable);
829 assert_eq!(fill_with, Some("default@example.com".into()));
830 assert_eq!(delete_null_rows, None);
831 } else {
832 panic!("Expected ModifyColumnNullable");
833 }
834 }
835
836 #[test]
837 fn test_action_with_prefix_modify_column_default() {
838 let action = MigrationAction::ModifyColumnDefault {
839 table: "users".into(),
840 column: "status".into(),
841 new_default: Some("active".into()),
842 backfill: None,
843 };
844 let prefixed = action.with_prefix("myapp_");
845 if let MigrationAction::ModifyColumnDefault {
846 table,
847 column,
848 new_default,
849 ..
850 } = prefixed
851 {
852 assert_eq!(table.as_str(), "myapp_users");
853 assert_eq!(column.as_str(), "status");
854 assert_eq!(new_default, Some("active".into()));
855 } else {
856 panic!("Expected ModifyColumnDefault");
857 }
858 }
859
860 #[test]
861 fn test_action_with_prefix_modify_column_comment() {
862 let action = MigrationAction::ModifyColumnComment {
863 table: "users".into(),
864 column: "bio".into(),
865 new_comment: Some("User biography".into()),
866 };
867 let prefixed = action.with_prefix("myapp_");
868 if let MigrationAction::ModifyColumnComment {
869 table,
870 column,
871 new_comment,
872 } = prefixed
873 {
874 assert_eq!(table.as_str(), "myapp_users");
875 assert_eq!(column.as_str(), "bio");
876 assert_eq!(new_comment, Some("User biography".into()));
877 } else {
878 panic!("Expected ModifyColumnComment");
879 }
880 }
881
882 #[test]
883 fn test_action_with_prefix_add_constraint() {
884 let action = MigrationAction::AddConstraint {
885 table: "posts".into(),
886 constraint: fk_user(Some("fk_user"), None),
887 };
888 let prefixed = action.with_prefix("myapp_");
889 if let MigrationAction::AddConstraint { table, constraint } = prefixed {
890 assert_eq!(table.as_str(), "myapp_posts");
891 if let TableConstraint::ForeignKey { ref_table, .. } = constraint {
892 assert_eq!(ref_table.as_str(), "myapp_users");
893 } else {
894 panic!("Expected ForeignKey constraint");
895 }
896 } else {
897 panic!("Expected AddConstraint");
898 }
899 }
900
901 #[test]
902 fn test_action_with_prefix_remove_constraint() {
903 let action = MigrationAction::RemoveConstraint {
904 table: "posts".into(),
905 constraint: fk_user(Some("fk_user"), None),
906 };
907 let prefixed = action.with_prefix("myapp_");
908 if let MigrationAction::RemoveConstraint { table, constraint } = prefixed {
909 assert_eq!(table.as_str(), "myapp_posts");
910 if let TableConstraint::ForeignKey { ref_table, .. } = constraint {
911 assert_eq!(ref_table.as_str(), "myapp_users");
912 } else {
913 panic!("Expected ForeignKey constraint");
914 }
915 } else {
916 panic!("Expected RemoveConstraint");
917 }
918 }
919
920 #[rstest]
921 #[case::replace_constraint_primary_key(
922 MigrationAction::ReplaceConstraint { table: "users".into(), from: pk_id(), to: pk_id_auto() },
923 "ReplaceConstraint: users.PRIMARY KEY"
924 )]
925 #[case::replace_constraint_unique_with_name(
926 MigrationAction::ReplaceConstraint { table: "users".into(), from: uq_email(None), to: uq_email(Some("uq_email")) },
927 "ReplaceConstraint: users.uq_email (UNIQUE)"
928 )]
929 #[case::replace_constraint_unique_without_name(
930 MigrationAction::ReplaceConstraint { table: "users".into(), from: uq_email(Some("uq_email")), to: uq_email(None) },
931 "ReplaceConstraint: users.UNIQUE"
932 )]
933 #[case::replace_constraint_foreign_key_with_name(
934 MigrationAction::ReplaceConstraint { table: "posts".into(), from: fk_user(None, None), to: fk_user(Some("fk_user"), None) },
935 "ReplaceConstraint: posts.fk_user (FOREIGN KEY)"
936 )]
937 #[case::replace_constraint_foreign_key_without_name(
938 MigrationAction::ReplaceConstraint { table: "posts".into(), from: fk_user(Some("fk_user"), None), to: fk_user(None, None) },
939 "ReplaceConstraint: posts.FOREIGN KEY"
940 )]
941 #[case::replace_constraint_check(
942 MigrationAction::ReplaceConstraint { table: "users".into(), from: chk("chk_age", "age > 0"), to: chk("chk_age", "age >= 0") },
943 "ReplaceConstraint: users.chk_age (CHECK)"
944 )]
945 #[case::replace_constraint_index_with_name(
946 MigrationAction::ReplaceConstraint { table: "users".into(), from: idx_email(None), to: idx_email(Some("ix_users__email")) },
947 "ReplaceConstraint: users.ix_users__email (INDEX)"
948 )]
949 #[case::replace_constraint_index_without_name(
950 MigrationAction::ReplaceConstraint { table: "users".into(), from: idx_email(Some("ix_users__email")), to: idx_email(None) },
951 "ReplaceConstraint: users.INDEX"
952 )]
953 fn test_display_replace_constraint(#[case] action: MigrationAction, #[case] expected: &str) {
954 assert_eq!(action.to_string(), expected);
955 }
956
957 #[test]
958 fn test_action_with_prefix_replace_constraint() {
959 let action = MigrationAction::ReplaceConstraint {
960 table: "posts".into(),
961 from: fk_user(Some("fk_user"), Some(ReferenceAction::Cascade)),
962 to: fk_user(Some("fk_user"), Some(ReferenceAction::SetNull)),
963 };
964 let prefixed = action.with_prefix("myapp_");
965 if let MigrationAction::ReplaceConstraint { table, from, to } = prefixed {
966 assert_eq!(table.as_str(), "myapp_posts");
967 if let TableConstraint::ForeignKey { ref_table, .. } = from {
968 assert_eq!(ref_table.as_str(), "myapp_users");
969 } else {
970 panic!("Expected ForeignKey constraint in from");
971 }
972 if let TableConstraint::ForeignKey { ref_table, .. } = to {
973 assert_eq!(ref_table.as_str(), "myapp_users");
974 } else {
975 panic!("Expected ForeignKey constraint in to");
976 }
977 } else {
978 panic!("Expected ReplaceConstraint");
979 }
980 }
981}