drizzle-cli 0.3.0

Command-line interface for drizzle-rs migrations
Documentation
//! `drizzle check`: validates the config and prints a summary.

use std::path::{Path, PathBuf};

use crate::config::{Config, Credentials, Dialect, MySQLCreds, PostgresCreds};
use crate::error::CliError;
use crate::output;

#[derive(clap::Args, Debug, Clone, Default)]
pub struct CheckOptions {
    /// Override dialect from config
    #[arg(long)]
    pub dialect: Option<Dialect>,

    /// Override output directory
    #[arg(long)]
    pub out: Option<PathBuf>,
}

/// Runs `drizzle check`: prints the resolved settings, then checks the schema
/// files, the migrations folder, and the credentials.
///
/// Problems that do not block other commands (no migrations yet, a legacy
/// journal, no credentials) are printed as warnings and still return `Ok`.
/// Does not connect to the database.
///
/// # Errors
///
/// Returns a [`CliError`] if `db_name` does not match the config, the
/// migrations folder cannot be read, or schema-file discovery or credential
/// resolution fails (those two are printed first, then reported as
/// [`CliError::Other`]).
pub fn run(config: &Config, db_name: Option<&str>, opts: &CheckOptions) -> Result<(), CliError> {
    let db = config.database(db_name)?;

    // CLI flags override config
    let effective_dialect = opts.dialect.unwrap_or(db.dialect);
    let effective_out = opts
        .out
        .as_deref()
        .map_or_else(|| db.out.clone(), Path::to_path_buf);

    println!("{}", output::heading("Checking configuration..."));
    println!();

    crate::commands::harness::print_db_header(config, db_name);

    let mut warnings = Vec::new();
    let mut has_errors = false;

    // Basic info
    println!("  {}: {}", output::label("Dialect"), effective_dialect);
    if let Some(driver) = db.driver {
        println!("  {}: {}", output::label("Driver"), driver);
    }
    println!("  {}: {}", output::label("Schema"), db.schema_display());
    println!("  {}: {}", output::label("Output"), effective_out.display());

    // Schema files
    println!();
    print!("  {} Schema files... ", output::label("Checking"));
    match db.schema_files() {
        Ok(files) => {
            println!("{}", output::status_ok());
            for f in &files {
                println!("    {}", f.display());
            }
        }
        Err(e) => {
            println!("{}", output::status_error());
            println!("    {e}");
            has_errors = true;
        }
    }

    // Migrations dir
    println!();
    print!("  {} Migrations... ", output::label("Checking"));
    let dir = &effective_out;
    let journal_path = effective_out.join("meta").join("_journal.json");
    if dir.exists() {
        println!("{}", output::status_ok());
        let migration_count = count_migration_dirs(dir)?;
        if migration_count > 0 {
            println!(
                "    Folders: {}",
                output::success(&migration_count.to_string())
            );
        } else {
            println!(
                "    Folders: {} (run generate first)",
                output::warning("missing")
            );
            warnings.push("No migration folders");
        }

        if journal_path.exists() {
            println!(
                "    Legacy journal: {} (run upgrade)",
                output::warning("found")
            );
            warnings.push("Legacy migration journal detected");
        }
    } else {
        println!("{}", output::status_warning("NOT CREATED"));
        warnings.push("Migrations directory doesn't exist yet");
    }

    // Credentials
    println!();
    print!("  {} Credentials... ", output::label("Checking"));
    match db.credentials() {
        Ok(Some(creds)) => {
            println!("{}", output::status_ok());
            print_credentials(&creds);
        }
        Ok(None) => {
            println!("{}", output::status_warning("NOT SET"));
            warnings.push("No credentials (needed for push/pull/migrate)");
        }
        Err(e) => {
            println!("{}", output::status_error());
            println!("    {e}");
            has_errors = true;
        }
    }

    // Summary
    println!();
    if has_errors {
        println!("{}", output::error("Configuration has errors."));
        Err(CliError::Other("config check failed".into()))
    } else if warnings.is_empty() {
        println!("{}", output::success("Configuration OK."));
        Ok(())
    } else {
        println!(
            "{}",
            output::warning(&format!("{} warning(s):", warnings.len()))
        );
        for w in warnings {
            println!("  - {w}");
        }
        Ok(())
    }
}

fn count_migration_dirs(dir: &Path) -> Result<usize, CliError> {
    let mut count = 0usize;
    for entry in std::fs::read_dir(dir).map_err(|e| CliError::IoError(e.to_string()))? {
        let entry = entry.map_err(|e| CliError::IoError(e.to_string()))?;
        if !entry
            .file_type()
            .map_err(|e| CliError::IoError(e.to_string()))?
            .is_dir()
        {
            continue;
        }

        let tag = entry.file_name().to_string_lossy().to_string();
        if tag == "meta" {
            continue;
        }

        if entry.path().join("migration.sql").exists() {
            count += 1;
        }
    }

    Ok(count)
}

fn print_credentials(creds: &Credentials) {
    match creds {
        Credentials::Sqlite { path } => {
            println!("    {}: {path}", output::label("SQLite"));
        }
        Credentials::Turso { url, auth_token } => {
            println!("    {}: {}", output::label("Turso"), output::mask_url(url));
            if auth_token.is_some() {
                println!("    Token: ****");
            }
        }
        Credentials::Postgres(pg) => match pg {
            PostgresCreds::Url(url) => {
                println!(
                    "    {}: {}",
                    output::label("PostgreSQL"),
                    output::mask_url(url)
                );
            }
            PostgresCreds::Host {
                host,
                port,
                database,
                user,
                ..
            } => {
                println!(
                    "    {}: {host}:{port}/{database}",
                    output::label("PostgreSQL")
                );
                if let Some(u) = user {
                    println!("    User: {u}");
                }
            }
        },
        Credentials::MySQL(mysql) => match mysql {
            MySQLCreds::Url(url) => {
                println!("    {}: {}", output::label("MySQL"), output::mask_url(url));
            }
            MySQLCreds::Host {
                host,
                port,
                database,
                user,
                ..
            } => {
                println!("    {}: {host}:{port}/{database}", output::label("MySQL"));
                if let Some(user) = user {
                    println!("    User: {user}");
                }
            }
        },
        Credentials::D1 {
            account_id,
            database_id,
            ..
        } => {
            // Mask the token (never print it) and show just the IDs.
            println!("    {}: {account_id}/{database_id}", output::label("D1"));
            println!("    Token: ****");
        }
        Credentials::AwsDataApi { database, .. } => {
            // Mask both ARNs — drizzle-kit flags both secretArn and resourceArn
            // as secrets. The database name is fine to show.
            println!("    {}: {database}", output::label("AWS Data API"));
            println!("    SecretArn:   ****");
            println!("    ResourceArn: ****");
        }
    }
}