Skip to main content

drizzle_cli/commands/
status.rs

1//! `drizzle status`: lists local migrations.
2//!
3//! Lists the local migration folders and whether each has a snapshot. It
4//! does not connect to the database; `drizzle migrate --plan` shows what the
5//! database has applied.
6
7use crate::config::Config;
8use crate::error::CliError;
9use crate::output;
10
11/// Run the status command.
12///
13/// # Errors
14///
15/// Returns [`CliError`] if the database entry cannot be resolved from the
16/// config or the migrations directory cannot be read.
17pub fn run(config: &Config, db_name: Option<&str>) -> Result<(), CliError> {
18    let db = config.database(db_name)?;
19
20    println!("{}", output::heading("Migration Status"));
21    println!();
22
23    if !config.is_single_database() {
24        crate::commands::harness::print_db_header(config, db_name);
25        println!();
26    }
27
28    let out_dir = db.migrations_dir();
29    let journal_path = db.journal_path();
30
31    // Check if migrations directory exists
32    if !out_dir.exists() {
33        println!("  {}", output::warning("No migrations directory found."));
34        println!("  Run 'drizzle generate' to create your first migration.");
35        return Ok(());
36    }
37
38    if journal_path.exists() {
39        println!(
40            "  {}",
41            output::warning("Legacy migration journal detected. Run 'drizzle up' first.")
42        );
43        println!();
44    }
45
46    let entries = discover_migration_dirs(out_dir)?;
47    if entries.is_empty() {
48        println!("  {}", output::warning("No migrations found."));
49        return Ok(());
50    }
51
52    // Display migration entries
53    println!("  {} migration folder(s):\n", entries.len());
54
55    for (i, (tag, migration_path, snapshot_path)) in entries.iter().enumerate() {
56        // Migration is in {out}/{tag}/migration.sql
57        let sql_exists = migration_path.exists();
58        let snapshot_exists = snapshot_path.exists();
59
60        let status_icon = if sql_exists && snapshot_exists {
61            output::success("✓")
62        } else if sql_exists {
63            output::warning("○")
64        } else {
65            output::error("✗")
66        };
67        let idx_display = output::muted(&format!("{:3}.", i + 1));
68
69        println!("  {idx_display} {status_icon} {tag}");
70
71        if !sql_exists {
72            println!("      {}", output::error("Migration file missing!"));
73        }
74        if !snapshot_exists && sql_exists {
75            println!("      {}", output::warning("Snapshot file missing"));
76        }
77    }
78
79    println!();
80    println!(
81        "  {}: {}",
82        output::muted("Migrations directory"),
83        out_dir.display()
84    );
85    println!(
86        "  {}: {}",
87        output::muted("Schema files"),
88        db.schema_display()
89    );
90
91    Ok(())
92}
93
94fn discover_migration_dirs(
95    out_dir: &std::path::Path,
96) -> Result<Vec<(String, std::path::PathBuf, std::path::PathBuf)>, CliError> {
97    let mut entries = Vec::new();
98
99    for entry in std::fs::read_dir(out_dir).map_err(|e| CliError::IoError(e.to_string()))? {
100        let entry = entry.map_err(|e| CliError::IoError(e.to_string()))?;
101        if !entry
102            .file_type()
103            .map_err(|e| CliError::IoError(e.to_string()))?
104            .is_dir()
105        {
106            continue;
107        }
108
109        let tag = entry.file_name().to_string_lossy().to_string();
110        if tag == "meta" {
111            continue;
112        }
113
114        let path = entry.path();
115        let migration_path = path.join("migration.sql");
116        if !migration_path.exists() {
117            continue;
118        }
119
120        let snapshot_path = path.join("snapshot.json");
121        entries.push((tag, migration_path, snapshot_path));
122    }
123
124    entries.sort_by(|a, b| a.0.cmp(&b.0));
125    Ok(entries)
126}