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