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