1use crate::schema::{ColumnDef, ColumnName, ColumnType, TableConstraint, TableName};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
7#[serde(rename_all = "snake_case")]
8pub struct MigrationPlan {
9 #[serde(default)]
12 pub id: String,
13 pub comment: Option<String>,
14 #[serde(default)]
15 pub created_at: Option<String>,
16 pub version: u32,
17 pub actions: Vec<MigrationAction>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
21#[serde(tag = "type", rename_all = "snake_case")]
22pub enum MigrationAction {
23 CreateTable {
24 table: TableName,
25 columns: Vec<ColumnDef>,
26 constraints: Vec<TableConstraint>,
27 },
28 DeleteTable {
29 table: TableName,
30 },
31 AddColumn {
32 table: TableName,
33 column: Box<ColumnDef>,
34 fill_with: Option<String>,
36 },
37 RenameColumn {
38 table: TableName,
39 from: ColumnName,
40 to: ColumnName,
41 },
42 DeleteColumn {
43 table: TableName,
44 column: ColumnName,
45 },
46 ModifyColumnType {
47 table: TableName,
48 column: ColumnName,
49 new_type: ColumnType,
50 },
51 ModifyColumnNullable {
52 table: TableName,
53 column: ColumnName,
54 nullable: bool,
55 fill_with: Option<String>,
57 },
58 ModifyColumnDefault {
59 table: TableName,
60 column: ColumnName,
61 new_default: Option<String>,
63 },
64 ModifyColumnComment {
65 table: TableName,
66 column: ColumnName,
67 new_comment: Option<String>,
69 },
70 AddConstraint {
71 table: TableName,
72 constraint: TableConstraint,
73 },
74 RemoveConstraint {
75 table: TableName,
76 constraint: TableConstraint,
77 },
78 RenameTable {
79 from: TableName,
80 to: TableName,
81 },
82 RawSql {
83 sql: String,
84 },
85}
86
87impl MigrationPlan {
88 pub fn with_prefix(self, prefix: &str) -> Self {
91 if prefix.is_empty() {
92 return self;
93 }
94 Self {
95 actions: self
96 .actions
97 .into_iter()
98 .map(|action| action.with_prefix(prefix))
99 .collect(),
100 ..self
101 }
102 }
103}
104
105impl MigrationAction {
106 pub fn with_prefix(self, prefix: &str) -> Self {
108 if prefix.is_empty() {
109 return self;
110 }
111 match self {
112 MigrationAction::CreateTable {
113 table,
114 columns,
115 constraints,
116 } => MigrationAction::CreateTable {
117 table: format!("{}{}", prefix, table),
118 columns,
119 constraints: constraints
120 .into_iter()
121 .map(|c| c.with_prefix(prefix))
122 .collect(),
123 },
124 MigrationAction::DeleteTable { table } => MigrationAction::DeleteTable {
125 table: format!("{}{}", prefix, table),
126 },
127 MigrationAction::AddColumn {
128 table,
129 column,
130 fill_with,
131 } => MigrationAction::AddColumn {
132 table: format!("{}{}", prefix, table),
133 column,
134 fill_with,
135 },
136 MigrationAction::RenameColumn { table, from, to } => MigrationAction::RenameColumn {
137 table: format!("{}{}", prefix, table),
138 from,
139 to,
140 },
141 MigrationAction::DeleteColumn { table, column } => MigrationAction::DeleteColumn {
142 table: format!("{}{}", prefix, table),
143 column,
144 },
145 MigrationAction::ModifyColumnType {
146 table,
147 column,
148 new_type,
149 } => MigrationAction::ModifyColumnType {
150 table: format!("{}{}", prefix, table),
151 column,
152 new_type,
153 },
154 MigrationAction::ModifyColumnNullable {
155 table,
156 column,
157 nullable,
158 fill_with,
159 } => MigrationAction::ModifyColumnNullable {
160 table: format!("{}{}", prefix, table),
161 column,
162 nullable,
163 fill_with,
164 },
165 MigrationAction::ModifyColumnDefault {
166 table,
167 column,
168 new_default,
169 } => MigrationAction::ModifyColumnDefault {
170 table: format!("{}{}", prefix, table),
171 column,
172 new_default,
173 },
174 MigrationAction::ModifyColumnComment {
175 table,
176 column,
177 new_comment,
178 } => MigrationAction::ModifyColumnComment {
179 table: format!("{}{}", prefix, table),
180 column,
181 new_comment,
182 },
183 MigrationAction::AddConstraint { table, constraint } => {
184 MigrationAction::AddConstraint {
185 table: format!("{}{}", prefix, table),
186 constraint: constraint.with_prefix(prefix),
187 }
188 }
189 MigrationAction::RemoveConstraint { table, constraint } => {
190 MigrationAction::RemoveConstraint {
191 table: format!("{}{}", prefix, table),
192 constraint: constraint.with_prefix(prefix),
193 }
194 }
195 MigrationAction::RenameTable { from, to } => MigrationAction::RenameTable {
196 from: format!("{}{}", prefix, from),
197 to: format!("{}{}", prefix, to),
198 },
199 MigrationAction::RawSql { sql } => MigrationAction::RawSql { sql },
200 }
201 }
202}
203
204impl fmt::Display for MigrationAction {
205 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206 match self {
207 MigrationAction::CreateTable { table, .. } => {
208 write!(f, "CreateTable: {}", table)
209 }
210 MigrationAction::DeleteTable { table } => {
211 write!(f, "DeleteTable: {}", table)
212 }
213 MigrationAction::AddColumn { table, column, .. } => {
214 write!(f, "AddColumn: {}.{}", table, column.name)
215 }
216 MigrationAction::RenameColumn { table, from, to } => {
217 write!(f, "RenameColumn: {}.{} -> {}", table, from, to)
218 }
219 MigrationAction::DeleteColumn { table, column } => {
220 write!(f, "DeleteColumn: {}.{}", table, column)
221 }
222 MigrationAction::ModifyColumnType { table, column, .. } => {
223 write!(f, "ModifyColumnType: {}.{}", table, column)
224 }
225 MigrationAction::ModifyColumnNullable {
226 table,
227 column,
228 nullable,
229 ..
230 } => {
231 let nullability = if *nullable { "NULL" } else { "NOT NULL" };
232 write!(
233 f,
234 "ModifyColumnNullable: {}.{} -> {}",
235 table, column, nullability
236 )
237 }
238 MigrationAction::ModifyColumnDefault {
239 table,
240 column,
241 new_default,
242 } => {
243 if let Some(default) = new_default {
244 write!(
245 f,
246 "ModifyColumnDefault: {}.{} -> {}",
247 table, column, default
248 )
249 } else {
250 write!(f, "ModifyColumnDefault: {}.{} -> (none)", table, column)
251 }
252 }
253 MigrationAction::ModifyColumnComment {
254 table,
255 column,
256 new_comment,
257 } => {
258 if let Some(comment) = new_comment {
259 let display = if comment.chars().count() > 30 {
260 format!("{}...", comment.chars().take(27).collect::<String>())
261 } else {
262 comment.clone()
263 };
264 write!(
265 f,
266 "ModifyColumnComment: {}.{} -> '{}'",
267 table, column, display
268 )
269 } else {
270 write!(f, "ModifyColumnComment: {}.{} -> (none)", table, column)
271 }
272 }
273 MigrationAction::AddConstraint { table, constraint } => {
274 let constraint_name = match constraint {
275 TableConstraint::PrimaryKey { .. } => "PRIMARY KEY",
276 TableConstraint::Unique { name, .. } => {
277 if let Some(n) = name {
278 return write!(f, "AddConstraint: {}.{} (UNIQUE)", table, n);
279 }
280 "UNIQUE"
281 }
282 TableConstraint::ForeignKey { name, .. } => {
283 if let Some(n) = name {
284 return write!(f, "AddConstraint: {}.{} (FOREIGN KEY)", table, n);
285 }
286 "FOREIGN KEY"
287 }
288 TableConstraint::Check { name, .. } => {
289 return write!(f, "AddConstraint: {}.{} (CHECK)", table, name);
290 }
291 TableConstraint::Index { name, .. } => {
292 if let Some(n) = name {
293 return write!(f, "AddConstraint: {}.{} (INDEX)", table, n);
294 }
295 "INDEX"
296 }
297 };
298 write!(f, "AddConstraint: {}.{}", table, constraint_name)
299 }
300 MigrationAction::RemoveConstraint { table, constraint } => {
301 let constraint_name = match constraint {
302 TableConstraint::PrimaryKey { .. } => "PRIMARY KEY",
303 TableConstraint::Unique { name, .. } => {
304 if let Some(n) = name {
305 return write!(f, "RemoveConstraint: {}.{} (UNIQUE)", table, n);
306 }
307 "UNIQUE"
308 }
309 TableConstraint::ForeignKey { name, .. } => {
310 if let Some(n) = name {
311 return write!(f, "RemoveConstraint: {}.{} (FOREIGN KEY)", table, n);
312 }
313 "FOREIGN KEY"
314 }
315 TableConstraint::Check { name, .. } => {
316 return write!(f, "RemoveConstraint: {}.{} (CHECK)", table, name);
317 }
318 TableConstraint::Index { name, .. } => {
319 if let Some(n) = name {
320 return write!(f, "RemoveConstraint: {}.{} (INDEX)", table, n);
321 }
322 "INDEX"
323 }
324 };
325 write!(f, "RemoveConstraint: {}.{}", table, constraint_name)
326 }
327 MigrationAction::RenameTable { from, to } => {
328 write!(f, "RenameTable: {} -> {}", from, to)
329 }
330 MigrationAction::RawSql { sql } => {
331 let display_sql = if sql.len() > 50 {
333 format!("{}...", &sql[..47])
334 } else {
335 sql.clone()
336 };
337 write!(f, "RawSql: {}", display_sql)
338 }
339 }
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use crate::schema::{ReferenceAction, SimpleColumnType};
347 use rstest::rstest;
348
349 fn default_column() -> ColumnDef {
350 ColumnDef {
351 name: "email".into(),
352 r#type: ColumnType::Simple(SimpleColumnType::Text),
353 nullable: true,
354 default: None,
355 comment: None,
356 primary_key: None,
357 unique: None,
358 index: None,
359 foreign_key: None,
360 }
361 }
362
363 #[rstest]
364 #[case::create_table(
365 MigrationAction::CreateTable {
366 table: "users".into(),
367 columns: vec![],
368 constraints: vec![],
369 },
370 "CreateTable: users"
371 )]
372 #[case::delete_table(
373 MigrationAction::DeleteTable {
374 table: "users".into(),
375 },
376 "DeleteTable: users"
377 )]
378 #[case::add_column(
379 MigrationAction::AddColumn {
380 table: "users".into(),
381 column: Box::new(default_column()),
382 fill_with: None,
383 },
384 "AddColumn: users.email"
385 )]
386 #[case::rename_column(
387 MigrationAction::RenameColumn {
388 table: "users".into(),
389 from: "old_name".into(),
390 to: "new_name".into(),
391 },
392 "RenameColumn: users.old_name -> new_name"
393 )]
394 #[case::delete_column(
395 MigrationAction::DeleteColumn {
396 table: "users".into(),
397 column: "email".into(),
398 },
399 "DeleteColumn: users.email"
400 )]
401 #[case::modify_column_type(
402 MigrationAction::ModifyColumnType {
403 table: "users".into(),
404 column: "age".into(),
405 new_type: ColumnType::Simple(SimpleColumnType::Integer),
406 },
407 "ModifyColumnType: users.age"
408 )]
409 #[case::add_constraint_index_with_name(
410 MigrationAction::AddConstraint {
411 table: "users".into(),
412 constraint: TableConstraint::Index {
413 name: Some("ix_users__email".into()),
414 columns: vec!["email".into()],
415 },
416 },
417 "AddConstraint: users.ix_users__email (INDEX)"
418 )]
419 #[case::add_constraint_index_without_name(
420 MigrationAction::AddConstraint {
421 table: "users".into(),
422 constraint: TableConstraint::Index {
423 name: None,
424 columns: vec!["email".into()],
425 },
426 },
427 "AddConstraint: users.INDEX"
428 )]
429 #[case::remove_constraint_index_with_name(
430 MigrationAction::RemoveConstraint {
431 table: "users".into(),
432 constraint: TableConstraint::Index {
433 name: Some("ix_users__email".into()),
434 columns: vec!["email".into()],
435 },
436 },
437 "RemoveConstraint: users.ix_users__email (INDEX)"
438 )]
439 #[case::remove_constraint_index_without_name(
440 MigrationAction::RemoveConstraint {
441 table: "users".into(),
442 constraint: TableConstraint::Index {
443 name: None,
444 columns: vec!["email".into()],
445 },
446 },
447 "RemoveConstraint: users.INDEX"
448 )]
449 #[case::rename_table(
450 MigrationAction::RenameTable {
451 from: "old_table".into(),
452 to: "new_table".into(),
453 },
454 "RenameTable: old_table -> new_table"
455 )]
456 fn test_display_basic_actions(#[case] action: MigrationAction, #[case] expected: &str) {
457 assert_eq!(action.to_string(), expected);
458 }
459
460 #[rstest]
461 #[case::add_constraint_primary_key(
462 MigrationAction::AddConstraint {
463 table: "users".into(),
464 constraint: TableConstraint::PrimaryKey {
465 auto_increment: false,
466 columns: vec!["id".into()],
467 },
468 },
469 "AddConstraint: users.PRIMARY KEY"
470 )]
471 #[case::add_constraint_unique_with_name(
472 MigrationAction::AddConstraint {
473 table: "users".into(),
474 constraint: TableConstraint::Unique {
475 name: Some("uq_email".into()),
476 columns: vec!["email".into()],
477 },
478 },
479 "AddConstraint: users.uq_email (UNIQUE)"
480 )]
481 #[case::add_constraint_unique_without_name(
482 MigrationAction::AddConstraint {
483 table: "users".into(),
484 constraint: TableConstraint::Unique {
485 name: None,
486 columns: vec!["email".into()],
487 },
488 },
489 "AddConstraint: users.UNIQUE"
490 )]
491 #[case::add_constraint_foreign_key_with_name(
492 MigrationAction::AddConstraint {
493 table: "posts".into(),
494 constraint: TableConstraint::ForeignKey {
495 name: Some("fk_user".into()),
496 columns: vec!["user_id".into()],
497 ref_table: "users".into(),
498 ref_columns: vec!["id".into()],
499 on_delete: Some(ReferenceAction::Cascade),
500 on_update: None,
501 },
502 },
503 "AddConstraint: posts.fk_user (FOREIGN KEY)"
504 )]
505 #[case::add_constraint_foreign_key_without_name(
506 MigrationAction::AddConstraint {
507 table: "posts".into(),
508 constraint: TableConstraint::ForeignKey {
509 name: None,
510 columns: vec!["user_id".into()],
511 ref_table: "users".into(),
512 ref_columns: vec!["id".into()],
513 on_delete: None,
514 on_update: None,
515 },
516 },
517 "AddConstraint: posts.FOREIGN KEY"
518 )]
519 #[case::add_constraint_check(
520 MigrationAction::AddConstraint {
521 table: "users".into(),
522 constraint: TableConstraint::Check {
523 name: "chk_age".into(),
524 expr: "age > 0".into(),
525 },
526 },
527 "AddConstraint: users.chk_age (CHECK)"
528 )]
529 fn test_display_add_constraint(#[case] action: MigrationAction, #[case] expected: &str) {
530 assert_eq!(action.to_string(), expected);
531 }
532
533 #[rstest]
534 #[case::remove_constraint_primary_key(
535 MigrationAction::RemoveConstraint {
536 table: "users".into(),
537 constraint: TableConstraint::PrimaryKey {
538 auto_increment: false,
539 columns: vec!["id".into()],
540 },
541 },
542 "RemoveConstraint: users.PRIMARY KEY"
543 )]
544 #[case::remove_constraint_unique_with_name(
545 MigrationAction::RemoveConstraint {
546 table: "users".into(),
547 constraint: TableConstraint::Unique {
548 name: Some("uq_email".into()),
549 columns: vec!["email".into()],
550 },
551 },
552 "RemoveConstraint: users.uq_email (UNIQUE)"
553 )]
554 #[case::remove_constraint_unique_without_name(
555 MigrationAction::RemoveConstraint {
556 table: "users".into(),
557 constraint: TableConstraint::Unique {
558 name: None,
559 columns: vec!["email".into()],
560 },
561 },
562 "RemoveConstraint: users.UNIQUE"
563 )]
564 #[case::remove_constraint_foreign_key_with_name(
565 MigrationAction::RemoveConstraint {
566 table: "posts".into(),
567 constraint: TableConstraint::ForeignKey {
568 name: Some("fk_user".into()),
569 columns: vec!["user_id".into()],
570 ref_table: "users".into(),
571 ref_columns: vec!["id".into()],
572 on_delete: None,
573 on_update: None,
574 },
575 },
576 "RemoveConstraint: posts.fk_user (FOREIGN KEY)"
577 )]
578 #[case::remove_constraint_foreign_key_without_name(
579 MigrationAction::RemoveConstraint {
580 table: "posts".into(),
581 constraint: TableConstraint::ForeignKey {
582 name: None,
583 columns: vec!["user_id".into()],
584 ref_table: "users".into(),
585 ref_columns: vec!["id".into()],
586 on_delete: None,
587 on_update: None,
588 },
589 },
590 "RemoveConstraint: posts.FOREIGN KEY"
591 )]
592 #[case::remove_constraint_check(
593 MigrationAction::RemoveConstraint {
594 table: "users".into(),
595 constraint: TableConstraint::Check {
596 name: "chk_age".into(),
597 expr: "age > 0".into(),
598 },
599 },
600 "RemoveConstraint: users.chk_age (CHECK)"
601 )]
602 fn test_display_remove_constraint(#[case] action: MigrationAction, #[case] expected: &str) {
603 assert_eq!(action.to_string(), expected);
604 }
605
606 #[rstest]
607 #[case::raw_sql_short(
608 MigrationAction::RawSql {
609 sql: "SELECT 1".into(),
610 },
611 "RawSql: SELECT 1"
612 )]
613 fn test_display_raw_sql_short(#[case] action: MigrationAction, #[case] expected: &str) {
614 assert_eq!(action.to_string(), expected);
615 }
616
617 #[test]
618 fn test_display_raw_sql_long() {
619 let action = MigrationAction::RawSql {
620 sql:
621 "SELECT * FROM users WHERE id = 1 AND name = 'test' AND email = 'test@example.com'"
622 .into(),
623 };
624 let result = action.to_string();
625 assert!(result.starts_with("RawSql: "));
626 assert!(result.ends_with("..."));
627 assert!(result.len() > 10);
628 }
629
630 #[rstest]
631 #[case::modify_column_nullable_to_not_null(
632 MigrationAction::ModifyColumnNullable {
633 table: "users".into(),
634 column: "email".into(),
635 nullable: false,
636 fill_with: None,
637 },
638 "ModifyColumnNullable: users.email -> NOT NULL"
639 )]
640 #[case::modify_column_nullable_to_null(
641 MigrationAction::ModifyColumnNullable {
642 table: "users".into(),
643 column: "email".into(),
644 nullable: true,
645 fill_with: None,
646 },
647 "ModifyColumnNullable: users.email -> NULL"
648 )]
649 fn test_display_modify_column_nullable(
650 #[case] action: MigrationAction,
651 #[case] expected: &str,
652 ) {
653 assert_eq!(action.to_string(), expected);
654 }
655
656 #[rstest]
657 #[case::modify_column_default_set(
658 MigrationAction::ModifyColumnDefault {
659 table: "users".into(),
660 column: "status".into(),
661 new_default: Some("'active'".into()),
662 },
663 "ModifyColumnDefault: users.status -> 'active'"
664 )]
665 #[case::modify_column_default_drop(
666 MigrationAction::ModifyColumnDefault {
667 table: "users".into(),
668 column: "status".into(),
669 new_default: None,
670 },
671 "ModifyColumnDefault: users.status -> (none)"
672 )]
673 fn test_display_modify_column_default(#[case] action: MigrationAction, #[case] expected: &str) {
674 assert_eq!(action.to_string(), expected);
675 }
676
677 #[rstest]
678 #[case::modify_column_comment_set(
679 MigrationAction::ModifyColumnComment {
680 table: "users".into(),
681 column: "email".into(),
682 new_comment: Some("User email address".into()),
683 },
684 "ModifyColumnComment: users.email -> 'User email address'"
685 )]
686 #[case::modify_column_comment_drop(
687 MigrationAction::ModifyColumnComment {
688 table: "users".into(),
689 column: "email".into(),
690 new_comment: None,
691 },
692 "ModifyColumnComment: users.email -> (none)"
693 )]
694 fn test_display_modify_column_comment(#[case] action: MigrationAction, #[case] expected: &str) {
695 assert_eq!(action.to_string(), expected);
696 }
697
698 #[test]
699 fn test_display_modify_column_comment_long() {
700 let action = MigrationAction::ModifyColumnComment {
702 table: "users".into(),
703 column: "email".into(),
704 new_comment: Some(
705 "This is a very long comment that should be truncated in display".into(),
706 ),
707 };
708 let result = action.to_string();
709 assert!(result.contains("..."));
710 assert!(result.contains("This is a very long comment"));
711 assert!(!result.contains("truncated in display"));
713 }
714
715 #[test]
717 fn test_action_with_prefix_create_table() {
718 let action = MigrationAction::CreateTable {
719 table: "users".into(),
720 columns: vec![default_column()],
721 constraints: vec![TableConstraint::ForeignKey {
722 name: Some("fk_org".into()),
723 columns: vec!["org_id".into()],
724 ref_table: "organizations".into(),
725 ref_columns: vec!["id".into()],
726 on_delete: None,
727 on_update: None,
728 }],
729 };
730 let prefixed = action.with_prefix("myapp_");
731 if let MigrationAction::CreateTable {
732 table, constraints, ..
733 } = prefixed
734 {
735 assert_eq!(table.as_str(), "myapp_users");
736 if let TableConstraint::ForeignKey { ref_table, .. } = &constraints[0] {
737 assert_eq!(ref_table.as_str(), "myapp_organizations");
738 }
739 } else {
740 panic!("Expected CreateTable");
741 }
742 }
743
744 #[test]
745 fn test_action_with_prefix_delete_table() {
746 let action = MigrationAction::DeleteTable {
747 table: "users".into(),
748 };
749 let prefixed = action.with_prefix("myapp_");
750 if let MigrationAction::DeleteTable { table } = prefixed {
751 assert_eq!(table.as_str(), "myapp_users");
752 } else {
753 panic!("Expected DeleteTable");
754 }
755 }
756
757 #[test]
758 fn test_action_with_prefix_add_column() {
759 let action = MigrationAction::AddColumn {
760 table: "users".into(),
761 column: Box::new(default_column()),
762 fill_with: None,
763 };
764 let prefixed = action.with_prefix("myapp_");
765 if let MigrationAction::AddColumn { table, .. } = prefixed {
766 assert_eq!(table.as_str(), "myapp_users");
767 } else {
768 panic!("Expected AddColumn");
769 }
770 }
771
772 #[test]
773 fn test_action_with_prefix_rename_table() {
774 let action = MigrationAction::RenameTable {
775 from: "old_table".into(),
776 to: "new_table".into(),
777 };
778 let prefixed = action.with_prefix("myapp_");
779 if let MigrationAction::RenameTable { from, to } = prefixed {
780 assert_eq!(from.as_str(), "myapp_old_table");
781 assert_eq!(to.as_str(), "myapp_new_table");
782 } else {
783 panic!("Expected RenameTable");
784 }
785 }
786
787 #[test]
788 fn test_action_with_prefix_raw_sql_unchanged() {
789 let action = MigrationAction::RawSql {
790 sql: "SELECT * FROM users".into(),
791 };
792 let prefixed = action.with_prefix("myapp_");
793 if let MigrationAction::RawSql { sql } = prefixed {
794 assert_eq!(sql, "SELECT * FROM users");
796 } else {
797 panic!("Expected RawSql");
798 }
799 }
800
801 #[test]
802 fn test_action_with_prefix_empty_prefix() {
803 let action = MigrationAction::CreateTable {
804 table: "users".into(),
805 columns: vec![],
806 constraints: vec![],
807 };
808 let prefixed = action.clone().with_prefix("");
809 if let MigrationAction::CreateTable { table, .. } = prefixed {
810 assert_eq!(table.as_str(), "users");
811 }
812 }
813
814 #[test]
815 fn test_migration_plan_with_prefix() {
816 let plan = MigrationPlan {
817 id: String::new(),
818 comment: Some("test".into()),
819 created_at: None,
820 version: 1,
821 actions: vec![
822 MigrationAction::CreateTable {
823 table: "users".into(),
824 columns: vec![],
825 constraints: vec![],
826 },
827 MigrationAction::CreateTable {
828 table: "posts".into(),
829 columns: vec![],
830 constraints: vec![TableConstraint::ForeignKey {
831 name: Some("fk_user".into()),
832 columns: vec!["user_id".into()],
833 ref_table: "users".into(),
834 ref_columns: vec!["id".into()],
835 on_delete: None,
836 on_update: None,
837 }],
838 },
839 ],
840 };
841 let prefixed = plan.with_prefix("myapp_");
842 assert_eq!(prefixed.actions.len(), 2);
843
844 if let MigrationAction::CreateTable { table, .. } = &prefixed.actions[0] {
845 assert_eq!(table.as_str(), "myapp_users");
846 }
847 if let MigrationAction::CreateTable {
848 table, constraints, ..
849 } = &prefixed.actions[1]
850 {
851 assert_eq!(table.as_str(), "myapp_posts");
852 if let TableConstraint::ForeignKey { ref_table, .. } = &constraints[0] {
853 assert_eq!(ref_table.as_str(), "myapp_users");
854 }
855 }
856 }
857
858 #[test]
859 fn test_action_with_prefix_rename_column() {
860 let action = MigrationAction::RenameColumn {
861 table: "users".into(),
862 from: "name".into(),
863 to: "full_name".into(),
864 };
865 let prefixed = action.with_prefix("myapp_");
866 if let MigrationAction::RenameColumn { table, from, to } = prefixed {
867 assert_eq!(table.as_str(), "myapp_users");
868 assert_eq!(from.as_str(), "name");
869 assert_eq!(to.as_str(), "full_name");
870 } else {
871 panic!("Expected RenameColumn");
872 }
873 }
874
875 #[test]
876 fn test_action_with_prefix_delete_column() {
877 let action = MigrationAction::DeleteColumn {
878 table: "users".into(),
879 column: "old_field".into(),
880 };
881 let prefixed = action.with_prefix("myapp_");
882 if let MigrationAction::DeleteColumn { table, column } = prefixed {
883 assert_eq!(table.as_str(), "myapp_users");
884 assert_eq!(column.as_str(), "old_field");
885 } else {
886 panic!("Expected DeleteColumn");
887 }
888 }
889
890 #[test]
891 fn test_action_with_prefix_modify_column_type() {
892 let action = MigrationAction::ModifyColumnType {
893 table: "users".into(),
894 column: "age".into(),
895 new_type: ColumnType::Simple(SimpleColumnType::BigInt),
896 };
897 let prefixed = action.with_prefix("myapp_");
898 if let MigrationAction::ModifyColumnType {
899 table,
900 column,
901 new_type,
902 } = prefixed
903 {
904 assert_eq!(table.as_str(), "myapp_users");
905 assert_eq!(column.as_str(), "age");
906 assert!(matches!(
907 new_type,
908 ColumnType::Simple(SimpleColumnType::BigInt)
909 ));
910 } else {
911 panic!("Expected ModifyColumnType");
912 }
913 }
914
915 #[test]
916 fn test_action_with_prefix_modify_column_nullable() {
917 let action = MigrationAction::ModifyColumnNullable {
918 table: "users".into(),
919 column: "email".into(),
920 nullable: false,
921 fill_with: Some("default@example.com".into()),
922 };
923 let prefixed = action.with_prefix("myapp_");
924 if let MigrationAction::ModifyColumnNullable {
925 table,
926 column,
927 nullable,
928 fill_with,
929 } = prefixed
930 {
931 assert_eq!(table.as_str(), "myapp_users");
932 assert_eq!(column.as_str(), "email");
933 assert!(!nullable);
934 assert_eq!(fill_with, Some("default@example.com".into()));
935 } else {
936 panic!("Expected ModifyColumnNullable");
937 }
938 }
939
940 #[test]
941 fn test_action_with_prefix_modify_column_default() {
942 let action = MigrationAction::ModifyColumnDefault {
943 table: "users".into(),
944 column: "status".into(),
945 new_default: Some("active".into()),
946 };
947 let prefixed = action.with_prefix("myapp_");
948 if let MigrationAction::ModifyColumnDefault {
949 table,
950 column,
951 new_default,
952 } = prefixed
953 {
954 assert_eq!(table.as_str(), "myapp_users");
955 assert_eq!(column.as_str(), "status");
956 assert_eq!(new_default, Some("active".into()));
957 } else {
958 panic!("Expected ModifyColumnDefault");
959 }
960 }
961
962 #[test]
963 fn test_action_with_prefix_modify_column_comment() {
964 let action = MigrationAction::ModifyColumnComment {
965 table: "users".into(),
966 column: "bio".into(),
967 new_comment: Some("User biography".into()),
968 };
969 let prefixed = action.with_prefix("myapp_");
970 if let MigrationAction::ModifyColumnComment {
971 table,
972 column,
973 new_comment,
974 } = prefixed
975 {
976 assert_eq!(table.as_str(), "myapp_users");
977 assert_eq!(column.as_str(), "bio");
978 assert_eq!(new_comment, Some("User biography".into()));
979 } else {
980 panic!("Expected ModifyColumnComment");
981 }
982 }
983
984 #[test]
985 fn test_action_with_prefix_add_constraint() {
986 let action = MigrationAction::AddConstraint {
987 table: "posts".into(),
988 constraint: TableConstraint::ForeignKey {
989 name: Some("fk_user".into()),
990 columns: vec!["user_id".into()],
991 ref_table: "users".into(),
992 ref_columns: vec!["id".into()],
993 on_delete: None,
994 on_update: None,
995 },
996 };
997 let prefixed = action.with_prefix("myapp_");
998 if let MigrationAction::AddConstraint { table, constraint } = prefixed {
999 assert_eq!(table.as_str(), "myapp_posts");
1000 if let TableConstraint::ForeignKey { ref_table, .. } = constraint {
1001 assert_eq!(ref_table.as_str(), "myapp_users");
1002 } else {
1003 panic!("Expected ForeignKey constraint");
1004 }
1005 } else {
1006 panic!("Expected AddConstraint");
1007 }
1008 }
1009
1010 #[test]
1011 fn test_action_with_prefix_remove_constraint() {
1012 let action = MigrationAction::RemoveConstraint {
1013 table: "posts".into(),
1014 constraint: TableConstraint::ForeignKey {
1015 name: Some("fk_user".into()),
1016 columns: vec!["user_id".into()],
1017 ref_table: "users".into(),
1018 ref_columns: vec!["id".into()],
1019 on_delete: None,
1020 on_update: None,
1021 },
1022 };
1023 let prefixed = action.with_prefix("myapp_");
1024 if let MigrationAction::RemoveConstraint { table, constraint } = prefixed {
1025 assert_eq!(table.as_str(), "myapp_posts");
1026 if let TableConstraint::ForeignKey { ref_table, .. } = constraint {
1027 assert_eq!(ref_table.as_str(), "myapp_users");
1028 } else {
1029 panic!("Expected ForeignKey constraint");
1030 }
1031 } else {
1032 panic!("Expected RemoveConstraint");
1033 }
1034 }
1035}