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