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