1use crate::theme::Theme;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub enum MigrationState {
33 Applied,
35 #[default]
37 Pending,
38 Failed,
40 Skipped,
42}
43
44impl MigrationState {
45 #[must_use]
47 pub fn as_str(&self) -> &'static str {
48 match self {
49 Self::Applied => "APPLIED",
50 Self::Pending => "PENDING",
51 Self::Failed => "FAILED",
52 Self::Skipped => "SKIPPED",
53 }
54 }
55
56 #[must_use]
58 pub fn indicator(&self) -> &'static str {
59 match self {
60 Self::Applied => "[OK]",
61 Self::Pending => "[PENDING]",
62 Self::Failed => "[FAILED]",
63 Self::Skipped => "[SKIPPED]",
64 }
65 }
66
67 #[must_use]
69 pub fn icon(&self) -> &'static str {
70 match self {
71 Self::Applied => "✓",
72 Self::Pending => "○",
73 Self::Failed => "✗",
74 Self::Skipped => "⊘",
75 }
76 }
77
78 #[must_use]
80 pub fn color_code(&self) -> &'static str {
81 match self {
82 Self::Applied => "\x1b[32m", Self::Pending => "\x1b[33m", Self::Failed => "\x1b[31m", Self::Skipped => "\x1b[90m", }
87 }
88
89 #[must_use]
91 pub fn reset_code() -> &'static str {
92 "\x1b[0m"
93 }
94}
95
96impl std::fmt::Display for MigrationState {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 write!(f, "{}", self.as_str())
99 }
100}
101
102#[derive(Debug, Clone, Default)]
104pub struct MigrationRecord {
105 pub version: String,
107 pub name: String,
109 pub state: MigrationState,
111 pub applied_at: Option<String>,
113 pub checksum: Option<String>,
115 pub duration_ms: Option<u64>,
117 pub error_message: Option<String>,
119 pub up_sql: Option<String>,
121 pub down_sql: Option<String>,
123}
124
125impl MigrationRecord {
126 #[must_use]
136 pub fn new(version: impl Into<String>, name: impl Into<String>) -> Self {
137 Self {
138 version: version.into(),
139 name: name.into(),
140 state: MigrationState::default(),
141 applied_at: None,
142 checksum: None,
143 duration_ms: None,
144 error_message: None,
145 up_sql: None,
146 down_sql: None,
147 }
148 }
149
150 #[must_use]
152 pub fn state(mut self, state: MigrationState) -> Self {
153 self.state = state;
154 self
155 }
156
157 #[must_use]
159 pub fn applied_at(mut self, timestamp: Option<String>) -> Self {
160 self.applied_at = timestamp;
161 self
162 }
163
164 #[must_use]
166 pub fn checksum(mut self, checksum: Option<String>) -> Self {
167 self.checksum = checksum;
168 self
169 }
170
171 #[must_use]
173 pub fn duration_ms(mut self, duration: Option<u64>) -> Self {
174 self.duration_ms = duration;
175 self
176 }
177
178 #[must_use]
180 pub fn error_message(mut self, message: Option<String>) -> Self {
181 self.error_message = message;
182 self
183 }
184
185 #[must_use]
187 pub fn up_sql(mut self, sql: Option<String>) -> Self {
188 self.up_sql = sql;
189 self
190 }
191
192 #[must_use]
194 pub fn down_sql(mut self, sql: Option<String>) -> Self {
195 self.down_sql = sql;
196 self
197 }
198
199 fn format_duration(&self) -> Option<String> {
201 self.duration_ms.map(|ms| {
202 if ms < 1000 {
203 format!("{}ms", ms)
204 } else if ms < 60_000 {
205 let secs = ms as f64 / 1000.0;
206 format!("{:.1}s", secs)
207 } else {
208 let mins = ms / 60_000;
209 let secs = (ms % 60_000) / 1000;
210 format!("{}m {}s", mins, secs)
211 }
212 })
213 }
214
215 fn format_timestamp(&self) -> Option<String> {
217 self.applied_at.as_ref().map(|ts| {
218 ts.replace('T', " ")
222 .trim_end_matches('Z')
223 .trim_end_matches("+00:00")
224 .to_string()
225 })
226 }
227}
228
229#[derive(Debug, Clone)]
233pub struct MigrationStatus {
234 records: Vec<MigrationRecord>,
236 theme: Theme,
238 show_checksums: bool,
240 show_duration: bool,
242 show_sql: bool,
244 width: Option<usize>,
246 title: Option<String>,
248}
249
250impl MigrationStatus {
251 #[must_use]
264 pub fn new(records: Vec<MigrationRecord>) -> Self {
265 Self {
266 records,
267 theme: Theme::default(),
268 show_checksums: false,
269 show_duration: true,
270 show_sql: false,
271 width: None,
272 title: None,
273 }
274 }
275
276 #[must_use]
278 pub fn theme(mut self, theme: Theme) -> Self {
279 self.theme = theme;
280 self
281 }
282
283 #[must_use]
285 pub fn show_checksums(mut self, show: bool) -> Self {
286 self.show_checksums = show;
287 self
288 }
289
290 #[must_use]
292 pub fn show_duration(mut self, show: bool) -> Self {
293 self.show_duration = show;
294 self
295 }
296
297 #[must_use]
299 pub fn show_sql(mut self, show: bool) -> Self {
300 self.show_sql = show;
301 self
302 }
303
304 #[must_use]
306 pub fn width(mut self, width: usize) -> Self {
307 self.width = Some(width);
308 self
309 }
310
311 #[must_use]
313 pub fn title(mut self, title: impl Into<String>) -> Self {
314 self.title = Some(title.into());
315 self
316 }
317
318 #[must_use]
320 pub fn applied_count(&self) -> usize {
321 self.records
322 .iter()
323 .filter(|r| r.state == MigrationState::Applied)
324 .count()
325 }
326
327 #[must_use]
329 pub fn pending_count(&self) -> usize {
330 self.records
331 .iter()
332 .filter(|r| r.state == MigrationState::Pending)
333 .count()
334 }
335
336 #[must_use]
338 pub fn failed_count(&self) -> usize {
339 self.records
340 .iter()
341 .filter(|r| r.state == MigrationState::Failed)
342 .count()
343 }
344
345 #[must_use]
347 pub fn skipped_count(&self) -> usize {
348 self.records
349 .iter()
350 .filter(|r| r.state == MigrationState::Skipped)
351 .count()
352 }
353
354 #[must_use]
356 pub fn total_count(&self) -> usize {
357 self.records.len()
358 }
359
360 #[must_use]
362 pub fn is_up_to_date(&self) -> bool {
363 self.pending_count() == 0 && self.failed_count() == 0
364 }
365
366 #[must_use]
371 pub fn render_plain(&self) -> String {
372 let mut lines = Vec::new();
373
374 let title = self.title.as_deref().unwrap_or("MIGRATION STATUS");
376 lines.push(title.to_string());
377 lines.push("=".repeat(title.len()));
378
379 lines.push(format!(
381 "Applied: {}, Pending: {}, Failed: {}, Total: {}",
382 self.applied_count(),
383 self.pending_count(),
384 self.failed_count(),
385 self.total_count()
386 ));
387 lines.push(String::new());
388
389 if self.records.is_empty() {
390 lines.push("No migrations found.".to_string());
391 return lines.join("\n");
392 }
393
394 for record in &self.records {
396 let mut parts = vec![
397 record.state.indicator().to_string(),
398 format!("{}_{}", record.version, record.name),
399 ];
400
401 if let Some(ts) = record.format_timestamp() {
403 parts.push(format!("- Applied {}", ts));
404 }
405
406 if self.show_duration {
408 if let Some(dur) = record.format_duration() {
409 parts.push(format!("({})", dur));
410 }
411 }
412
413 lines.push(parts.join(" "));
414
415 if record.state == MigrationState::Failed {
417 if let Some(ref err) = record.error_message {
418 lines.push(format!(" Error: {}", err));
419 }
420 }
421
422 if self.show_checksums {
424 if let Some(ref checksum) = record.checksum {
425 lines.push(format!(" Checksum: {}", checksum));
426 }
427 }
428
429 if self.show_sql {
431 if record.state == MigrationState::Pending {
432 if let Some(ref sql) = record.up_sql {
433 lines.push(" Up SQL:".to_string());
434 for sql_line in sql.lines().take(3) {
435 lines.push(format!(" {}", sql_line));
436 }
437 }
438 } else if record.state == MigrationState::Applied {
439 if let Some(ref sql) = record.down_sql {
440 lines.push(" Down SQL:".to_string());
441 for sql_line in sql.lines().take(3) {
442 lines.push(format!(" {}", sql_line));
443 }
444 }
445 }
446 }
447 }
448
449 lines.join("\n")
450 }
451
452 #[must_use]
457 pub fn render_styled(&self) -> String {
458 let width = self.width.unwrap_or(80);
459 let reset = MigrationState::reset_code();
460 let dim = "\x1b[2m";
461
462 let mut lines = Vec::new();
463
464 let title = self.title.as_deref().unwrap_or("Migration Status");
466
467 let title_display = format!(" {} ", title);
469 let title_len = title_display.chars().count();
470 let left_pad = (width - 2 - title_len) / 2;
471 let right_pad = width - 2 - title_len - left_pad;
472
473 lines.push(format!(
474 "{}╭{}{}{}╮{}",
475 self.border_color(),
476 "─".repeat(left_pad),
477 title_display,
478 "─".repeat(right_pad),
479 reset
480 ));
481
482 let summary = format!(
484 " Applied: {}{}{} Pending: {}{}{} Failed: {}{}{}",
485 self.theme.success.color_code(),
486 self.applied_count(),
487 reset,
488 self.theme.warning.color_code(),
489 self.pending_count(),
490 reset,
491 self.theme.error.color_code(),
492 self.failed_count(),
493 reset,
494 );
495 lines.push(self.wrap_line(&summary, width));
496
497 lines.push(format!(
499 "{}├{}┤{}",
500 self.border_color(),
501 "─".repeat(width - 2),
502 reset
503 ));
504
505 if self.records.is_empty() {
506 let empty_msg = format!(" {}No migrations found.{}", dim, reset);
507 lines.push(self.wrap_line(&empty_msg, width));
508 } else {
509 let header = format!(
511 " {dim}Status Version Name{:width$}Applied At Duration{reset}",
512 "",
513 width = width.saturating_sub(70),
514 dim = dim,
515 reset = reset
516 );
517 lines.push(self.wrap_line(&header, width));
518 lines.push(format!(
519 "{}│{}{}│{}",
520 self.border_color(),
521 dim,
522 "─".repeat(width - 2),
523 reset
524 ));
525
526 for record in &self.records {
528 let state_color = record.state.color_code();
529 let icon = record.state.icon();
530
531 let version_name = format!("{}_{}", record.version, record.name);
533 let version_name_display = if version_name.len() > 30 {
534 format!("{}...", &version_name[..27])
535 } else {
536 version_name
537 };
538
539 let timestamp = record.format_timestamp().unwrap_or_else(|| "-".to_string());
541
542 let duration = if self.show_duration {
544 record.format_duration().unwrap_or_else(|| "-".to_string())
545 } else {
546 String::new()
547 };
548
549 let row = format!(
550 " {}{} {:7}{} {:30} {:19} {:>8}",
551 state_color,
552 icon,
553 record.state.as_str(),
554 reset,
555 version_name_display,
556 timestamp,
557 duration,
558 );
559 lines.push(self.wrap_line(&row, width));
560
561 if record.state == MigrationState::Failed {
563 if let Some(ref err) = record.error_message {
564 let err_line = format!(
565 " {}Error: {}{}",
566 self.theme.error.color_code(),
567 err,
568 reset
569 );
570 lines.push(self.wrap_line(&err_line, width));
571 }
572 }
573
574 if self.show_checksums {
576 if let Some(ref checksum) = record.checksum {
577 let checksum_line = format!(" {}Checksum: {}{}", dim, checksum, reset);
578 lines.push(self.wrap_line(&checksum_line, width));
579 }
580 }
581 }
582 }
583
584 lines.push(format!(
586 "{}╰{}╯{}",
587 self.border_color(),
588 "─".repeat(width - 2),
589 reset
590 ));
591
592 lines.join("\n")
593 }
594
595 #[must_use]
599 pub fn to_json(&self) -> serde_json::Value {
600 let records: Vec<serde_json::Value> = self
601 .records
602 .iter()
603 .map(|r| {
604 serde_json::json!({
605 "version": r.version,
606 "name": r.name,
607 "state": r.state.as_str(),
608 "applied_at": r.applied_at,
609 "checksum": r.checksum,
610 "duration_ms": r.duration_ms,
611 "error_message": r.error_message,
612 })
613 })
614 .collect();
615
616 serde_json::json!({
617 "title": self.title,
618 "summary": {
619 "applied": self.applied_count(),
620 "pending": self.pending_count(),
621 "failed": self.failed_count(),
622 "skipped": self.skipped_count(),
623 "total": self.total_count(),
624 "up_to_date": self.is_up_to_date(),
625 },
626 "migrations": records,
627 })
628 }
629
630 fn border_color(&self) -> String {
632 self.theme.border.color_code()
633 }
634
635 fn wrap_line(&self, content: &str, width: usize) -> String {
637 let visible_len = self.visible_length(content);
638 let padding = (width - 2).saturating_sub(visible_len);
639 let reset = MigrationState::reset_code();
640
641 format!(
642 "{}│{}{content}{:padding$}{}│{}",
643 self.border_color(),
644 reset,
645 "",
646 self.border_color(),
647 reset,
648 padding = padding
649 )
650 }
651
652 fn visible_length(&self, s: &str) -> usize {
654 let mut len = 0;
655 let mut in_escape = false;
656
657 for c in s.chars() {
658 if c == '\x1b' {
659 in_escape = true;
660 } else if in_escape {
661 if c == 'm' {
662 in_escape = false;
663 }
664 } else {
665 len += 1;
666 }
667 }
668 len
669 }
670}
671
672impl Default for MigrationStatus {
673 fn default() -> Self {
674 Self::new(Vec::new())
675 }
676}
677
678#[cfg(test)]
679mod tests {
680 use super::*;
681
682 #[test]
684 fn test_migration_status_creation() {
685 let status = MigrationStatus::new(vec![
686 MigrationRecord::new("001", "create_users"),
687 MigrationRecord::new("002", "add_posts"),
688 ]);
689
690 assert_eq!(status.total_count(), 2);
691 assert_eq!(status.records.len(), 2);
692 }
693
694 #[test]
696 fn test_migration_state_applied() {
697 let state = MigrationState::Applied;
698
699 assert_eq!(state.as_str(), "APPLIED");
700 assert_eq!(state.indicator(), "[OK]");
701 assert_eq!(state.icon(), "✓");
702 assert!(state.color_code().contains("32")); }
704
705 #[test]
707 fn test_migration_state_pending() {
708 let state = MigrationState::Pending;
709
710 assert_eq!(state.as_str(), "PENDING");
711 assert_eq!(state.indicator(), "[PENDING]");
712 assert_eq!(state.icon(), "○");
713 assert!(state.color_code().contains("33")); }
715
716 #[test]
718 fn test_migration_state_failed() {
719 let state = MigrationState::Failed;
720
721 assert_eq!(state.as_str(), "FAILED");
722 assert_eq!(state.indicator(), "[FAILED]");
723 assert_eq!(state.icon(), "✗");
724 assert!(state.color_code().contains("31")); }
726
727 #[test]
729 fn test_migration_render_plain() {
730 let status = MigrationStatus::new(vec![
731 MigrationRecord::new("001", "create_users")
732 .state(MigrationState::Applied)
733 .applied_at(Some("2024-01-15T10:30:00Z".to_string()))
734 .duration_ms(Some(45)),
735 MigrationRecord::new("002", "add_posts").state(MigrationState::Pending),
736 ]);
737
738 let plain = status.render_plain();
739
740 assert!(plain.contains("MIGRATION STATUS"));
742
743 assert!(plain.contains("Applied: 1"));
745 assert!(plain.contains("Pending: 1"));
746
747 assert!(plain.contains("[OK] 001_create_users"));
749 assert!(plain.contains("[PENDING] 002_add_posts"));
750
751 assert!(plain.contains("2024-01-15"));
753
754 assert!(plain.contains("45ms"));
756 }
757
758 #[test]
760 fn test_migration_render_rich() {
761 let status = MigrationStatus::new(vec![
762 MigrationRecord::new("001", "create_users").state(MigrationState::Applied),
763 ])
764 .width(80);
765
766 let styled = status.render_styled();
767
768 assert!(styled.contains("╭"));
770 assert!(styled.contains("╯"));
771 assert!(styled.contains("│"));
772
773 assert!(styled.contains("✓"));
775 }
776
777 #[test]
779 fn test_migration_timestamps() {
780 let record = MigrationRecord::new("001", "test")
781 .applied_at(Some("2024-01-15T10:30:00Z".to_string()));
782
783 let formatted = record.format_timestamp();
784
785 assert!(formatted.is_some());
786 let ts = formatted.unwrap();
787 assert!(ts.contains("2024-01-15"));
788 assert!(ts.contains("10:30:00"));
789 assert!(!ts.contains('T')); assert!(!ts.contains('Z')); }
792
793 #[test]
795 fn test_migration_checksums() {
796 let status = MigrationStatus::new(vec![
797 MigrationRecord::new("001", "test")
798 .state(MigrationState::Applied)
799 .checksum(Some("abc123def456".to_string())),
800 ])
801 .show_checksums(true);
802
803 let plain = status.render_plain();
804
805 assert!(plain.contains("Checksum: abc123def456"));
806 }
807
808 #[test]
810 fn test_migration_duration() {
811 let record_ms = MigrationRecord::new("001", "test").duration_ms(Some(45));
813 assert_eq!(record_ms.format_duration(), Some("45ms".to_string()));
814
815 let record_sec = MigrationRecord::new("002", "test").duration_ms(Some(2500));
817 assert_eq!(record_sec.format_duration(), Some("2.5s".to_string()));
818
819 let record_m = MigrationRecord::new("003", "test").duration_ms(Some(125_000));
821 assert_eq!(record_m.format_duration(), Some("2m 5s".to_string()));
822 }
823
824 #[test]
826 fn test_migration_empty_list() {
827 let status = MigrationStatus::new(vec![]);
828
829 assert_eq!(status.total_count(), 0);
830 assert_eq!(status.applied_count(), 0);
831 assert_eq!(status.pending_count(), 0);
832 assert!(status.is_up_to_date());
833
834 let plain = status.render_plain();
835 assert!(plain.contains("No migrations found"));
836 }
837
838 #[test]
841 fn test_migration_state_display() {
842 assert_eq!(format!("{}", MigrationState::Applied), "APPLIED");
843 assert_eq!(format!("{}", MigrationState::Pending), "PENDING");
844 assert_eq!(format!("{}", MigrationState::Failed), "FAILED");
845 assert_eq!(format!("{}", MigrationState::Skipped), "SKIPPED");
846 }
847
848 #[test]
849 fn test_migration_state_skipped() {
850 let state = MigrationState::Skipped;
851
852 assert_eq!(state.as_str(), "SKIPPED");
853 assert_eq!(state.indicator(), "[SKIPPED]");
854 assert_eq!(state.icon(), "⊘");
855 assert!(state.color_code().contains("90")); }
857
858 #[test]
859 fn test_migration_record_builder() {
860 let record = MigrationRecord::new("001", "create_users")
861 .state(MigrationState::Applied)
862 .applied_at(Some("2024-01-15T10:30:00Z".to_string()))
863 .checksum(Some("abc123".to_string()))
864 .duration_ms(Some(100))
865 .error_message(None)
866 .up_sql(Some("CREATE TABLE users".to_string()))
867 .down_sql(Some("DROP TABLE users".to_string()));
868
869 assert_eq!(record.version, "001");
870 assert_eq!(record.name, "create_users");
871 assert_eq!(record.state, MigrationState::Applied);
872 assert!(record.applied_at.is_some());
873 assert!(record.checksum.is_some());
874 assert_eq!(record.duration_ms, Some(100));
875 assert!(record.up_sql.is_some());
876 assert!(record.down_sql.is_some());
877 }
878
879 #[test]
880 fn test_migration_status_counts() {
881 let status = MigrationStatus::new(vec![
882 MigrationRecord::new("001", "a").state(MigrationState::Applied),
883 MigrationRecord::new("002", "b").state(MigrationState::Applied),
884 MigrationRecord::new("003", "c").state(MigrationState::Pending),
885 MigrationRecord::new("004", "d").state(MigrationState::Failed),
886 MigrationRecord::new("005", "e").state(MigrationState::Skipped),
887 ]);
888
889 assert_eq!(status.applied_count(), 2);
890 assert_eq!(status.pending_count(), 1);
891 assert_eq!(status.failed_count(), 1);
892 assert_eq!(status.skipped_count(), 1);
893 assert_eq!(status.total_count(), 5);
894 assert!(!status.is_up_to_date()); }
896
897 #[test]
898 fn test_migration_is_up_to_date() {
899 let status1 = MigrationStatus::new(vec![
901 MigrationRecord::new("001", "a").state(MigrationState::Applied),
902 MigrationRecord::new("002", "b").state(MigrationState::Applied),
903 ]);
904 assert!(status1.is_up_to_date());
905
906 let status2 = MigrationStatus::new(vec![
908 MigrationRecord::new("001", "a").state(MigrationState::Applied),
909 MigrationRecord::new("002", "b").state(MigrationState::Pending),
910 ]);
911 assert!(!status2.is_up_to_date());
912
913 let status3 = MigrationStatus::new(vec![
915 MigrationRecord::new("001", "a").state(MigrationState::Failed),
916 ]);
917 assert!(!status3.is_up_to_date());
918 }
919
920 #[test]
921 fn test_migration_status_builder_pattern() {
922 let status = MigrationStatus::new(vec![])
923 .theme(Theme::light())
924 .show_checksums(true)
925 .show_duration(false)
926 .show_sql(true)
927 .width(100)
928 .title("Custom Title");
929
930 assert!(status.show_checksums);
931 assert!(!status.show_duration);
932 assert!(status.show_sql);
933 assert_eq!(status.width, Some(100));
934 assert_eq!(status.title, Some("Custom Title".to_string()));
935 }
936
937 #[test]
938 fn test_migration_to_json() {
939 let status = MigrationStatus::new(vec![
940 MigrationRecord::new("001", "create_users")
941 .state(MigrationState::Applied)
942 .applied_at(Some("2024-01-15T10:30:00Z".to_string()))
943 .duration_ms(Some(45)),
944 MigrationRecord::new("002", "add_posts").state(MigrationState::Pending),
945 ]);
946
947 let json = status.to_json();
948
949 assert_eq!(json["summary"]["applied"], 1);
951 assert_eq!(json["summary"]["pending"], 1);
952 assert_eq!(json["summary"]["total"], 2);
953 assert!(!json["summary"]["up_to_date"].as_bool().unwrap());
954
955 let migrations = json["migrations"].as_array().unwrap();
957 assert_eq!(migrations.len(), 2);
958 assert_eq!(migrations[0]["state"], "APPLIED");
959 assert_eq!(migrations[1]["state"], "PENDING");
960 }
961
962 #[test]
963 fn test_migration_failed_with_error() {
964 let status = MigrationStatus::new(vec![
965 MigrationRecord::new("001", "broken")
966 .state(MigrationState::Failed)
967 .error_message(Some("Duplicate column 'id'".to_string())),
968 ]);
969
970 let plain = status.render_plain();
971
972 assert!(plain.contains("[FAILED]"));
973 assert!(plain.contains("Error: Duplicate column 'id'"));
974 }
975
976 #[test]
977 fn test_migration_render_plain_with_sql() {
978 let status = MigrationStatus::new(vec![
979 MigrationRecord::new("001", "create_users")
980 .state(MigrationState::Pending)
981 .up_sql(Some(
982 "CREATE TABLE users (\n id SERIAL,\n name TEXT\n);".to_string(),
983 )),
984 ])
985 .show_sql(true);
986
987 let plain = status.render_plain();
988
989 assert!(plain.contains("Up SQL:"));
990 assert!(plain.contains("CREATE TABLE users"));
991 }
992
993 #[test]
994 fn test_migration_default() {
995 let status = MigrationStatus::default();
996 assert_eq!(status.total_count(), 0);
997 assert!(status.records.is_empty());
998 }
999
1000 #[test]
1001 fn test_migration_record_default() {
1002 let record = MigrationRecord::default();
1003 assert_eq!(record.version, "");
1004 assert_eq!(record.name, "");
1005 assert_eq!(record.state, MigrationState::Pending);
1006 }
1007
1008 #[test]
1009 fn test_migration_state_default() {
1010 let state = MigrationState::default();
1011 assert_eq!(state, MigrationState::Pending);
1012 }
1013}