Skip to main content

sova_db/
migrate_cli.rs

1//! App CLI: `migrate [up|down|status] [N]`.
2
3use sova_core::Error;
4use sea_orm::DatabaseConnection;
5use sea_orm_migration::MigratorTrait;
6
7const USAGE: &str = "usage: migrate [up|down|status] [N]";
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum MigrateCmd {
11    /// Apply pending migrations (`None` = all).
12    Up(Option<u32>),
13    /// Roll back `n` applied migrations (default 1 at the CLI layer).
14    Down(u32),
15    Status,
16}
17
18/// Parse argv after the `migrate` command name.
19pub fn parse_migrate_args(args: &[String]) -> Result<MigrateCmd, String> {
20    match args {
21        [] => Ok(MigrateCmd::Up(None)),
22        [s] if s == "up" => Ok(MigrateCmd::Up(None)),
23        [s] if s == "down" => Ok(MigrateCmd::Down(1)),
24        [s] if s == "status" => Ok(MigrateCmd::Status),
25        [s, n] if s == "up" => Ok(MigrateCmd::Up(Some(parse_steps(n)?))),
26        [s, n] if s == "down" => Ok(MigrateCmd::Down(parse_steps(n)?)),
27        _ => Err(USAGE.into()),
28    }
29}
30
31fn parse_steps(raw: &str) -> Result<u32, String> {
32    let n: u32 = raw
33        .parse()
34        .map_err(|_| format!("invalid step count `{raw}`; {USAGE}"))?;
35    if n == 0 {
36        return Err(format!("step count must be >= 1; {USAGE}"));
37    }
38    Ok(n)
39}
40
41pub(crate) async fn run_migrate<M: MigratorTrait>(
42    conn: DatabaseConnection,
43    args: &[String],
44) -> Result<(), Error> {
45    let cmd = parse_migrate_args(args).map_err(Error::Internal)?;
46    match cmd {
47        MigrateCmd::Up(steps) => M::up(&conn, steps)
48            .await
49            .map_err(|e| Error::Internal(format!("migrate up: {e}")))?,
50        MigrateCmd::Down(n) => M::down(&conn, Some(n))
51            .await
52            .map_err(|e| Error::Internal(format!("migrate down: {e}")))?,
53        MigrateCmd::Status => print_status::<M>(&conn).await?,
54    }
55    Ok(())
56}
57
58async fn print_status<M: MigratorTrait>(conn: &DatabaseConnection) -> Result<(), Error> {
59    let rows = M::get_migration_with_status(conn)
60        .await
61        .map_err(|e| Error::Internal(format!("migrate status: {e}")))?;
62    let mut applied = 0usize;
63    let mut pending = 0usize;
64    for m in &rows {
65        let status = m.status();
66        match status {
67            sea_orm_migration::MigrationStatus::Applied => applied += 1,
68            sea_orm_migration::MigrationStatus::Pending => pending += 1,
69        }
70        println!("{status:<9} {}", m.name());
71    }
72    println!("---");
73    println!("{applied} applied, {pending} pending");
74    Ok(())
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    fn s(args: &[&str]) -> Vec<String> {
82        args.iter().map(|a| (*a).to_string()).collect()
83    }
84
85    #[test]
86    fn parse_defaults_and_steps() {
87        assert_eq!(parse_migrate_args(&s(&[])).unwrap(), MigrateCmd::Up(None));
88        assert_eq!(parse_migrate_args(&s(&["up"])).unwrap(), MigrateCmd::Up(None));
89        assert_eq!(
90            parse_migrate_args(&s(&["up", "3"])).unwrap(),
91            MigrateCmd::Up(Some(3))
92        );
93        assert_eq!(parse_migrate_args(&s(&["down"])).unwrap(), MigrateCmd::Down(1));
94        assert_eq!(
95            parse_migrate_args(&s(&["down", "2"])).unwrap(),
96            MigrateCmd::Down(2)
97        );
98        assert_eq!(
99            parse_migrate_args(&s(&["status"])).unwrap(),
100            MigrateCmd::Status
101        );
102    }
103
104    #[test]
105    fn parse_rejects_bad() {
106        assert!(parse_migrate_args(&s(&["up", "0"])).is_err());
107        assert!(parse_migrate_args(&s(&["up", "x"])).is_err());
108        assert!(parse_migrate_args(&s(&["fresh"])).is_err());
109        assert!(parse_migrate_args(&s(&["status", "1"])).is_err());
110    }
111}