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).max(6);
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 max_title_chars = width.saturating_sub(4);
469 let title_text = self.truncate_plain_to_width(title, max_title_chars);
470 let title_display = format!(" {title_text} ");
471 let title_len = title_display.chars().count();
472 let border_space = width.saturating_sub(2);
473 let total_pad = border_space.saturating_sub(title_len);
474 let left_pad = total_pad / 2;
475 let right_pad = total_pad.saturating_sub(left_pad);
476
477 lines.push(format!(
478 "{}╭{}{}{}╮{}",
479 self.border_color(),
480 "─".repeat(left_pad),
481 title_display,
482 "─".repeat(right_pad),
483 reset
484 ));
485
486 let summary = format!(
488 " Applied: {}{}{} Pending: {}{}{} Failed: {}{}{}",
489 self.theme.success.color_code(),
490 self.applied_count(),
491 reset,
492 self.theme.warning.color_code(),
493 self.pending_count(),
494 reset,
495 self.theme.error.color_code(),
496 self.failed_count(),
497 reset,
498 );
499 lines.push(self.wrap_line(&summary, width));
500
501 lines.push(format!(
503 "{}├{}┤{}",
504 self.border_color(),
505 "─".repeat(width.saturating_sub(2)),
506 reset
507 ));
508
509 if self.records.is_empty() {
510 let empty_msg = format!(" {}No migrations found.{}", dim, reset);
511 lines.push(self.wrap_line(&empty_msg, width));
512 } else {
513 let header = format!(
515 " {dim}Status Version Name{:width$}Applied At Duration{reset}",
516 "",
517 width = width.saturating_sub(70),
518 dim = dim,
519 reset = reset
520 );
521 lines.push(self.wrap_line(&header, width));
522 lines.push(format!(
523 "{}│{}{}│{}",
524 self.border_color(),
525 dim,
526 "─".repeat(width.saturating_sub(2)),
527 reset
528 ));
529
530 for record in &self.records {
532 let state_color = record.state.color_code();
533 let icon = record.state.icon();
534
535 let version_name = format!("{}_{}", record.version, record.name);
537 let version_name_display = self.truncate_plain_to_width(&version_name, 30);
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.saturating_sub(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.saturating_sub(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 truncate_plain_to_width(&self, s: &str, max_visible: usize) -> String {
653 if max_visible == 0 {
654 return String::new();
655 }
656
657 let char_count = s.chars().count();
658 if char_count <= max_visible {
659 return s.to_string();
660 }
661
662 if max_visible <= 3 {
663 return ".".repeat(max_visible);
664 }
665
666 let truncated: String = s.chars().take(max_visible - 3).collect();
667 format!("{truncated}...")
668 }
669
670 fn visible_length(&self, s: &str) -> usize {
672 let mut len = 0;
673 let mut in_escape = false;
674
675 for c in s.chars() {
676 if c == '\x1b' {
677 in_escape = true;
678 } else if in_escape {
679 if c == 'm' {
680 in_escape = false;
681 }
682 } else {
683 len += 1;
684 }
685 }
686 len
687 }
688}
689
690impl Default for MigrationStatus {
691 fn default() -> Self {
692 Self::new(Vec::new())
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699
700 #[test]
702 fn test_migration_status_creation() {
703 let status = MigrationStatus::new(vec![
704 MigrationRecord::new("001", "create_users"),
705 MigrationRecord::new("002", "add_posts"),
706 ]);
707
708 assert_eq!(status.total_count(), 2);
709 assert_eq!(status.records.len(), 2);
710 }
711
712 #[test]
714 fn test_migration_state_applied() {
715 let state = MigrationState::Applied;
716
717 assert_eq!(state.as_str(), "APPLIED");
718 assert_eq!(state.indicator(), "[OK]");
719 assert_eq!(state.icon(), "✓");
720 assert!(state.color_code().contains("32")); }
722
723 #[test]
725 fn test_migration_state_pending() {
726 let state = MigrationState::Pending;
727
728 assert_eq!(state.as_str(), "PENDING");
729 assert_eq!(state.indicator(), "[PENDING]");
730 assert_eq!(state.icon(), "○");
731 assert!(state.color_code().contains("33")); }
733
734 #[test]
736 fn test_migration_state_failed() {
737 let state = MigrationState::Failed;
738
739 assert_eq!(state.as_str(), "FAILED");
740 assert_eq!(state.indicator(), "[FAILED]");
741 assert_eq!(state.icon(), "✗");
742 assert!(state.color_code().contains("31")); }
744
745 #[test]
747 fn test_migration_render_plain() {
748 let status = MigrationStatus::new(vec![
749 MigrationRecord::new("001", "create_users")
750 .state(MigrationState::Applied)
751 .applied_at(Some("2024-01-15T10:30:00Z".to_string()))
752 .duration_ms(Some(45)),
753 MigrationRecord::new("002", "add_posts").state(MigrationState::Pending),
754 ]);
755
756 let plain = status.render_plain();
757
758 assert!(plain.contains("MIGRATION STATUS"));
760
761 assert!(plain.contains("Applied: 1"));
763 assert!(plain.contains("Pending: 1"));
764
765 assert!(plain.contains("[OK] 001_create_users"));
767 assert!(plain.contains("[PENDING] 002_add_posts"));
768
769 assert!(plain.contains("2024-01-15"));
771
772 assert!(plain.contains("45ms"));
774 }
775
776 #[test]
778 fn test_migration_render_rich() {
779 let status = MigrationStatus::new(vec![
780 MigrationRecord::new("001", "create_users").state(MigrationState::Applied),
781 ])
782 .width(80);
783
784 let styled = status.render_styled();
785
786 assert!(styled.contains("╭"));
788 assert!(styled.contains("╯"));
789 assert!(styled.contains("│"));
790
791 assert!(styled.contains("✓"));
793 }
794
795 #[test]
796 fn test_migration_render_styled_tiny_width_does_not_panic() {
797 let status = MigrationStatus::new(vec![
798 MigrationRecord::new("001", "create_users").state(MigrationState::Applied),
799 ])
800 .width(1);
801
802 let styled = status.render_styled();
803
804 assert!(!styled.is_empty());
805 assert!(styled.contains('╭'));
806 assert!(styled.contains('╯'));
807 }
808
809 #[test]
810 fn test_migration_render_styled_unicode_name_truncation() {
811 let status = MigrationStatus::new(vec![
812 MigrationRecord::new(
813 "001",
814 "🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥",
815 )
816 .state(MigrationState::Applied),
817 ])
818 .width(80);
819
820 let styled = status.render_styled();
821
822 assert!(styled.contains("..."));
823 assert!(styled.contains("001_"));
824 }
825
826 #[test]
828 fn test_migration_timestamps() {
829 let record = MigrationRecord::new("001", "test")
830 .applied_at(Some("2024-01-15T10:30:00Z".to_string()));
831
832 let formatted = record.format_timestamp();
833
834 assert!(formatted.is_some());
835 let ts = formatted.unwrap();
836 assert!(ts.contains("2024-01-15"));
837 assert!(ts.contains("10:30:00"));
838 assert!(!ts.contains('T')); assert!(!ts.contains('Z')); }
841
842 #[test]
844 fn test_migration_checksums() {
845 let status = MigrationStatus::new(vec![
846 MigrationRecord::new("001", "test")
847 .state(MigrationState::Applied)
848 .checksum(Some("abc123def456".to_string())),
849 ])
850 .show_checksums(true);
851
852 let plain = status.render_plain();
853
854 assert!(plain.contains("Checksum: abc123def456"));
855 }
856
857 #[test]
859 fn test_migration_duration() {
860 let record_ms = MigrationRecord::new("001", "test").duration_ms(Some(45));
862 assert_eq!(record_ms.format_duration(), Some("45ms".to_string()));
863
864 let record_sec = MigrationRecord::new("002", "test").duration_ms(Some(2500));
866 assert_eq!(record_sec.format_duration(), Some("2.5s".to_string()));
867
868 let record_m = MigrationRecord::new("003", "test").duration_ms(Some(125_000));
870 assert_eq!(record_m.format_duration(), Some("2m 5s".to_string()));
871 }
872
873 #[test]
875 fn test_migration_empty_list() {
876 let status = MigrationStatus::new(vec![]);
877
878 assert_eq!(status.total_count(), 0);
879 assert_eq!(status.applied_count(), 0);
880 assert_eq!(status.pending_count(), 0);
881 assert!(status.is_up_to_date());
882
883 let plain = status.render_plain();
884 assert!(plain.contains("No migrations found"));
885 }
886
887 #[test]
890 fn test_migration_state_display() {
891 assert_eq!(format!("{}", MigrationState::Applied), "APPLIED");
892 assert_eq!(format!("{}", MigrationState::Pending), "PENDING");
893 assert_eq!(format!("{}", MigrationState::Failed), "FAILED");
894 assert_eq!(format!("{}", MigrationState::Skipped), "SKIPPED");
895 }
896
897 #[test]
898 fn test_migration_state_skipped() {
899 let state = MigrationState::Skipped;
900
901 assert_eq!(state.as_str(), "SKIPPED");
902 assert_eq!(state.indicator(), "[SKIPPED]");
903 assert_eq!(state.icon(), "⊘");
904 assert!(state.color_code().contains("90")); }
906
907 #[test]
908 fn test_migration_record_builder() {
909 let record = MigrationRecord::new("001", "create_users")
910 .state(MigrationState::Applied)
911 .applied_at(Some("2024-01-15T10:30:00Z".to_string()))
912 .checksum(Some("abc123".to_string()))
913 .duration_ms(Some(100))
914 .error_message(None)
915 .up_sql(Some("CREATE TABLE users".to_string()))
916 .down_sql(Some("DROP TABLE users".to_string()));
917
918 assert_eq!(record.version, "001");
919 assert_eq!(record.name, "create_users");
920 assert_eq!(record.state, MigrationState::Applied);
921 assert!(record.applied_at.is_some());
922 assert!(record.checksum.is_some());
923 assert_eq!(record.duration_ms, Some(100));
924 assert!(record.up_sql.is_some());
925 assert!(record.down_sql.is_some());
926 }
927
928 #[test]
929 fn test_migration_status_counts() {
930 let status = MigrationStatus::new(vec![
931 MigrationRecord::new("001", "a").state(MigrationState::Applied),
932 MigrationRecord::new("002", "b").state(MigrationState::Applied),
933 MigrationRecord::new("003", "c").state(MigrationState::Pending),
934 MigrationRecord::new("004", "d").state(MigrationState::Failed),
935 MigrationRecord::new("005", "e").state(MigrationState::Skipped),
936 ]);
937
938 assert_eq!(status.applied_count(), 2);
939 assert_eq!(status.pending_count(), 1);
940 assert_eq!(status.failed_count(), 1);
941 assert_eq!(status.skipped_count(), 1);
942 assert_eq!(status.total_count(), 5);
943 assert!(!status.is_up_to_date()); }
945
946 #[test]
947 fn test_migration_is_up_to_date() {
948 let status1 = MigrationStatus::new(vec![
950 MigrationRecord::new("001", "a").state(MigrationState::Applied),
951 MigrationRecord::new("002", "b").state(MigrationState::Applied),
952 ]);
953 assert!(status1.is_up_to_date());
954
955 let status2 = MigrationStatus::new(vec![
957 MigrationRecord::new("001", "a").state(MigrationState::Applied),
958 MigrationRecord::new("002", "b").state(MigrationState::Pending),
959 ]);
960 assert!(!status2.is_up_to_date());
961
962 let status3 = MigrationStatus::new(vec![
964 MigrationRecord::new("001", "a").state(MigrationState::Failed),
965 ]);
966 assert!(!status3.is_up_to_date());
967 }
968
969 #[test]
970 fn test_migration_status_builder_pattern() {
971 let status = MigrationStatus::new(vec![])
972 .theme(Theme::light())
973 .show_checksums(true)
974 .show_duration(false)
975 .show_sql(true)
976 .width(100)
977 .title("Custom Title");
978
979 assert!(status.show_checksums);
980 assert!(!status.show_duration);
981 assert!(status.show_sql);
982 assert_eq!(status.width, Some(100));
983 assert_eq!(status.title, Some("Custom Title".to_string()));
984 }
985
986 #[test]
987 fn test_migration_to_json() {
988 let status = MigrationStatus::new(vec![
989 MigrationRecord::new("001", "create_users")
990 .state(MigrationState::Applied)
991 .applied_at(Some("2024-01-15T10:30:00Z".to_string()))
992 .duration_ms(Some(45)),
993 MigrationRecord::new("002", "add_posts").state(MigrationState::Pending),
994 ]);
995
996 let json = status.to_json();
997
998 assert_eq!(json["summary"]["applied"], 1);
1000 assert_eq!(json["summary"]["pending"], 1);
1001 assert_eq!(json["summary"]["total"], 2);
1002 assert!(!json["summary"]["up_to_date"].as_bool().unwrap());
1003
1004 let migrations = json["migrations"].as_array().unwrap();
1006 assert_eq!(migrations.len(), 2);
1007 assert_eq!(migrations[0]["state"], "APPLIED");
1008 assert_eq!(migrations[1]["state"], "PENDING");
1009 }
1010
1011 #[test]
1012 fn test_migration_failed_with_error() {
1013 let status = MigrationStatus::new(vec![
1014 MigrationRecord::new("001", "broken")
1015 .state(MigrationState::Failed)
1016 .error_message(Some("Duplicate column 'id'".to_string())),
1017 ]);
1018
1019 let plain = status.render_plain();
1020
1021 assert!(plain.contains("[FAILED]"));
1022 assert!(plain.contains("Error: Duplicate column 'id'"));
1023 }
1024
1025 #[test]
1026 fn test_migration_render_plain_with_sql() {
1027 let status = MigrationStatus::new(vec![
1028 MigrationRecord::new("001", "create_users")
1029 .state(MigrationState::Pending)
1030 .up_sql(Some(
1031 "CREATE TABLE users (\n id SERIAL,\n name TEXT\n);".to_string(),
1032 )),
1033 ])
1034 .show_sql(true);
1035
1036 let plain = status.render_plain();
1037
1038 assert!(plain.contains("Up SQL:"));
1039 assert!(plain.contains("CREATE TABLE users"));
1040 }
1041
1042 #[test]
1043 fn test_migration_default() {
1044 let status = MigrationStatus::default();
1045 assert_eq!(status.total_count(), 0);
1046 assert!(status.records.is_empty());
1047 }
1048
1049 #[test]
1050 fn test_migration_record_default() {
1051 let record = MigrationRecord::default();
1052 assert_eq!(record.version, "");
1053 assert_eq!(record.name, "");
1054 assert_eq!(record.state, MigrationState::Pending);
1055 }
1056
1057 #[test]
1058 fn test_migration_state_default() {
1059 let state = MigrationState::default();
1060 assert_eq!(state, MigrationState::Pending);
1061 }
1062}