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