Skip to main content

ferro_cli/doctor/checks/
migrations.rs

1//! Migrations check (D-04): pending vs applied count via `cargo run -- db:status`.
2//!
3//! Mirrors the subprocess pattern from `commands::db_status` to avoid pulling
4//! SeaORM into doctor's compile graph.
5
6use crate::doctor::check::{CheckResult, DoctorCheck};
7use crate::doctor::checks::run_cargo_subcommand;
8use std::path::Path;
9
10pub struct MigrationsCheck;
11
12const NAME: &str = "migrations_pending";
13
14impl DoctorCheck for MigrationsCheck {
15    fn name(&self) -> &'static str {
16        NAME
17    }
18    fn run(&self, root: &Path) -> CheckResult {
19        check_impl(root)
20    }
21}
22
23pub(crate) fn check_impl(root: &Path) -> CheckResult {
24    if !root.join("src/migrations").exists() {
25        return CheckResult::warn(NAME, "no migrations directory");
26    }
27
28    let output = match run_cargo_subcommand(root, &["db:status"]) {
29        Ok(o) => o,
30        Err(e) => return CheckResult::error(NAME, format!("could not invoke cargo: {e}")),
31    };
32
33    if !output.status.success() {
34        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
35        return CheckResult::error(NAME, "db:status failed").with_details(stderr);
36    }
37
38    let stdout = String::from_utf8_lossy(&output.stdout);
39    let pending = count_lines_containing(&stdout, "pending");
40    let applied = count_lines_containing(&stdout, "applied");
41
42    if pending == 0 {
43        CheckResult::ok(NAME, format!("{applied} applied, 0 pending"))
44    } else {
45        CheckResult::warn(NAME, format!("{applied} applied, {pending} pending"))
46    }
47}
48
49fn count_lines_containing(text: &str, needle: &str) -> usize {
50    text.lines()
51        .filter(|l| l.to_ascii_lowercase().contains(needle))
52        .count()
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn name_is_migrations() {
61        assert_eq!(MigrationsCheck.name(), "migrations_pending");
62    }
63
64    #[test]
65    fn count_lines_containing_is_case_insensitive() {
66        let text = "Pending: m1\nApplied: m0\nPending: m2";
67        assert_eq!(count_lines_containing(text, "pending"), 2);
68        assert_eq!(count_lines_containing(text, "applied"), 1);
69    }
70}