use std::path::{Path, PathBuf};
use crate::config::{Config, Credentials, Dialect, PostgresCreds};
use crate::error::CliError;
use crate::output;
#[derive(clap::Args, Debug, Clone, Default)]
pub struct CheckOptions {
#[arg(long)]
pub dialect: Option<Dialect>,
#[arg(long)]
pub out: Option<PathBuf>,
}
pub fn run(config: &Config, db_name: Option<&str>, opts: &CheckOptions) -> Result<(), CliError> {
let db = config.database(db_name)?;
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;
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());
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;
}
}
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");
}
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;
}
}
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"), mask_url(url));
if auth_token.is_some() {
println!(" Token: ****");
}
}
Credentials::Postgres(pg) => match pg {
PostgresCreds::Url(url) => {
println!(" {}: {}", output::label("PostgreSQL"), 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::D1 {
account_id,
database_id,
..
} => {
println!(" {}: {account_id}/{database_id}", output::label("D1"));
println!(" Token: ****");
}
Credentials::AwsDataApi { database, .. } => {
println!(" {}: {database}", output::label("AWS Data API"));
println!(" SecretArn: ****");
println!(" ResourceArn: ****");
}
}
}
fn mask_url(url: &str) -> String {
if let Some(at) = url.find('@')
&& let Some(colon) = url[..at].rfind(':')
{
let scheme_end = url.find("://").map_or(0, |p| p + 3);
if colon > scheme_end {
return format!("{}****{}", &url[..=colon], &url[at..]);
}
}
url.to_string()
}