Skip to main content

drizzle_cli/commands/
status.rs

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