Skip to main content

drizzle_cli/commands/
check.rs

1//! `drizzle check`: validates the config and prints a summary.
2
3use std::path::{Path, PathBuf};
4
5use crate::config::{Config, Credentials, Dialect, MySQLCreds, PostgresCreds};
6use crate::error::CliError;
7use crate::output;
8
9#[derive(clap::Args, Debug, Clone, Default)]
10pub struct CheckOptions {
11    /// Override dialect from config
12    #[arg(long)]
13    pub dialect: Option<Dialect>,
14
15    /// Override output directory
16    #[arg(long)]
17    pub out: Option<PathBuf>,
18}
19
20/// Runs `drizzle check`: prints the resolved settings, then checks the schema
21/// files, the migrations folder, and the credentials.
22///
23/// Problems that do not block other commands (no migrations yet, a legacy
24/// journal, no credentials) are printed as warnings and still return `Ok`.
25/// Does not connect to the database.
26///
27/// # Errors
28///
29/// Returns a [`CliError`] if `db_name` does not match the config, the
30/// migrations folder cannot be read, or schema-file discovery or credential
31/// resolution fails (those two are printed first, then reported as
32/// [`CliError::Other`]).
33pub fn run(config: &Config, db_name: Option<&str>, opts: &CheckOptions) -> Result<(), CliError> {
34    let db = config.database(db_name)?;
35
36    // CLI flags override config
37    let effective_dialect = opts.dialect.unwrap_or(db.dialect);
38    let effective_out = opts
39        .out
40        .as_deref()
41        .map_or_else(|| db.out.clone(), Path::to_path_buf);
42
43    println!("{}", output::heading("Checking configuration..."));
44    println!();
45
46    crate::commands::harness::print_db_header(config, db_name);
47
48    let mut warnings = Vec::new();
49    let mut has_errors = false;
50
51    // Basic info
52    println!("  {}: {}", output::label("Dialect"), effective_dialect);
53    if let Some(driver) = db.driver {
54        println!("  {}: {}", output::label("Driver"), driver);
55    }
56    println!("  {}: {}", output::label("Schema"), db.schema_display());
57    println!("  {}: {}", output::label("Output"), effective_out.display());
58
59    // Schema files
60    println!();
61    print!("  {} Schema files... ", output::label("Checking"));
62    match db.schema_files() {
63        Ok(files) => {
64            println!("{}", output::status_ok());
65            for f in &files {
66                println!("    {}", f.display());
67            }
68        }
69        Err(e) => {
70            println!("{}", output::status_error());
71            println!("    {e}");
72            has_errors = true;
73        }
74    }
75
76    // Migrations dir
77    println!();
78    print!("  {} Migrations... ", output::label("Checking"));
79    let dir = &effective_out;
80    let journal_path = effective_out.join("meta").join("_journal.json");
81    if dir.exists() {
82        println!("{}", output::status_ok());
83        let migration_count = count_migration_dirs(dir)?;
84        if migration_count > 0 {
85            println!(
86                "    Folders: {}",
87                output::success(&migration_count.to_string())
88            );
89        } else {
90            println!(
91                "    Folders: {} (run generate first)",
92                output::warning("missing")
93            );
94            warnings.push("No migration folders");
95        }
96
97        if journal_path.exists() {
98            println!(
99                "    Legacy journal: {} (run upgrade)",
100                output::warning("found")
101            );
102            warnings.push("Legacy migration journal detected");
103        }
104    } else {
105        println!("{}", output::status_warning("NOT CREATED"));
106        warnings.push("Migrations directory doesn't exist yet");
107    }
108
109    // Credentials
110    println!();
111    print!("  {} Credentials... ", output::label("Checking"));
112    match db.credentials() {
113        Ok(Some(creds)) => {
114            println!("{}", output::status_ok());
115            print_credentials(&creds);
116        }
117        Ok(None) => {
118            println!("{}", output::status_warning("NOT SET"));
119            warnings.push("No credentials (needed for push/pull/migrate)");
120        }
121        Err(e) => {
122            println!("{}", output::status_error());
123            println!("    {e}");
124            has_errors = true;
125        }
126    }
127
128    // Summary
129    println!();
130    if has_errors {
131        println!("{}", output::error("Configuration has errors."));
132        Err(CliError::Other("config check failed".into()))
133    } else if warnings.is_empty() {
134        println!("{}", output::success("Configuration OK."));
135        Ok(())
136    } else {
137        println!(
138            "{}",
139            output::warning(&format!("{} warning(s):", warnings.len()))
140        );
141        for w in warnings {
142            println!("  - {w}");
143        }
144        Ok(())
145    }
146}
147
148fn count_migration_dirs(dir: &Path) -> Result<usize, CliError> {
149    let mut count = 0usize;
150    for entry in std::fs::read_dir(dir).map_err(|e| CliError::IoError(e.to_string()))? {
151        let entry = entry.map_err(|e| CliError::IoError(e.to_string()))?;
152        if !entry
153            .file_type()
154            .map_err(|e| CliError::IoError(e.to_string()))?
155            .is_dir()
156        {
157            continue;
158        }
159
160        let tag = entry.file_name().to_string_lossy().to_string();
161        if tag == "meta" {
162            continue;
163        }
164
165        if entry.path().join("migration.sql").exists() {
166            count += 1;
167        }
168    }
169
170    Ok(count)
171}
172
173fn print_credentials(creds: &Credentials) {
174    match creds {
175        Credentials::Sqlite { path } => {
176            println!("    {}: {path}", output::label("SQLite"));
177        }
178        Credentials::Turso { url, auth_token } => {
179            println!("    {}: {}", output::label("Turso"), output::mask_url(url));
180            if auth_token.is_some() {
181                println!("    Token: ****");
182            }
183        }
184        Credentials::Postgres(pg) => match pg {
185            PostgresCreds::Url(url) => {
186                println!(
187                    "    {}: {}",
188                    output::label("PostgreSQL"),
189                    output::mask_url(url)
190                );
191            }
192            PostgresCreds::Host {
193                host,
194                port,
195                database,
196                user,
197                ..
198            } => {
199                println!(
200                    "    {}: {host}:{port}/{database}",
201                    output::label("PostgreSQL")
202                );
203                if let Some(u) = user {
204                    println!("    User: {u}");
205                }
206            }
207        },
208        Credentials::MySQL(mysql) => match mysql {
209            MySQLCreds::Url(url) => {
210                println!("    {}: {}", output::label("MySQL"), output::mask_url(url));
211            }
212            MySQLCreds::Host {
213                host,
214                port,
215                database,
216                user,
217                ..
218            } => {
219                println!("    {}: {host}:{port}/{database}", output::label("MySQL"));
220                if let Some(user) = user {
221                    println!("    User: {user}");
222                }
223            }
224        },
225        Credentials::D1 {
226            account_id,
227            database_id,
228            ..
229        } => {
230            // Mask the token (never print it) and show just the IDs.
231            println!("    {}: {account_id}/{database_id}", output::label("D1"));
232            println!("    Token: ****");
233        }
234        Credentials::AwsDataApi { database, .. } => {
235            // Mask both ARNs — drizzle-kit flags both secretArn and resourceArn
236            // as secrets. The database name is fine to show.
237            println!("    {}: {database}", output::label("AWS Data API"));
238            println!("    SecretArn:   ****");
239            println!("    ResourceArn: ****");
240        }
241    }
242}