Skip to main content

drizzle_cli/commands/
check.rs

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