use clap::{Parser, Subcommand};
use std::path::PathBuf;
use std::process::ExitCode;
use drizzle_cli::config::{Casing, Config, IntrospectCasing};
use drizzle_cli::error::CliError;
use drizzle_cli::output;
const DEFAULT_CONFIG_FILE: &str = "drizzle.config.toml";
const SCHEMA_URL: &str =
"https://raw.githubusercontent.com/themixednuts/drizzle-rs/main/cli/schema.json";
#[derive(Parser, Debug)]
#[command(name = "drizzle")]
#[command(author, version, about = "Database migration CLI for drizzle-rs", long_about = None)]
struct Cli {
#[arg(short, long, global = true, value_name = "PATH")]
config: Option<PathBuf>,
#[arg(long, global = true, value_name = "NAME")]
db: Option<String>,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
Generate {
#[arg(short, long)]
name: Option<String>,
#[arg(long)]
custom: bool,
#[arg(long, value_parser = parse_casing)]
casing: Option<Casing>,
},
Migrate,
Up {
#[arg(long)]
dialect: Option<String>,
#[arg(long)]
out: Option<PathBuf>,
},
Push {
#[arg(long)]
verbose: bool,
#[arg(long, hide = true)]
strict: bool,
#[arg(long)]
force: bool,
#[arg(long)]
explain: bool,
#[arg(long, value_parser = parse_casing)]
casing: Option<Casing>,
#[arg(long = "extensionsFilters", value_delimiter = ',')]
extensions_filters: Option<Vec<String>>,
},
Introspect {
#[arg(long, name = "init")]
init_metadata: bool,
#[arg(long, value_parser = parse_introspect_casing)]
casing: Option<IntrospectCasing>,
#[arg(long)]
out: Option<PathBuf>,
#[arg(long)]
breakpoints: Option<bool>,
},
Pull {
#[arg(long, name = "init")]
init_metadata: bool,
#[arg(long, value_parser = parse_introspect_casing)]
casing: Option<IntrospectCasing>,
#[arg(long)]
out: Option<PathBuf>,
#[arg(long)]
breakpoints: Option<bool>,
},
Status,
Check {
#[arg(long)]
dialect: Option<String>,
#[arg(long)]
out: Option<PathBuf>,
},
Export {
#[arg(long)]
sql: Option<PathBuf>,
},
Init {
#[arg(short, long, default_value = "sqlite", value_parser = ["sqlite", "postgresql", "postgres", "turso"])]
dialect: String,
#[arg(short = 'r', long, value_parser = ["rusqlite", "libsql", "turso", "postgres-sync", "tokio-postgres"])]
driver: Option<String>,
},
}
fn parse_casing(s: &str) -> Result<Casing, String> {
s.parse()
}
fn parse_introspect_casing(s: &str) -> Result<IntrospectCasing, String> {
s.parse()
}
fn main() -> ExitCode {
let _ = dotenvy::dotenv();
let cli = Cli::parse();
let result = run(cli);
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
let msg = e.to_string();
eprintln!("{}", output::err_line(&msg));
ExitCode::FAILURE
}
}
}
fn run(cli: Cli) -> Result<(), CliError> {
let db_name = cli.db.as_deref();
match cli.command {
Command::Init { dialect, driver } => run_init(&dialect, driver.as_deref()),
Command::Generate {
name,
custom,
casing,
} => {
let config = load_config(cli.config.as_deref())?;
drizzle_cli::commands::generate::run(&config, db_name, name, custom, casing)
}
Command::Migrate => {
let config = load_config(cli.config.as_deref())?;
drizzle_cli::commands::migrate::run(&config, db_name)
}
Command::Up { dialect, out } => {
let config = load_config(cli.config.as_deref())?;
drizzle_cli::commands::upgrade::run(
&config,
db_name,
dialect.as_deref(),
out.as_deref(),
)
}
Command::Push {
verbose,
strict,
force,
explain,
casing,
extensions_filters,
} => {
let config = load_config(cli.config.as_deref())?;
drizzle_cli::commands::push::run(
&config,
db_name,
drizzle_cli::commands::push::PushOptions {
cli_verbose: verbose,
cli_strict: strict,
force,
cli_explain: explain,
casing,
extensions_filters,
},
)
}
Command::Introspect {
init_metadata,
casing,
out,
breakpoints,
}
| Command::Pull {
init_metadata,
casing,
out,
breakpoints,
} => {
let config = load_config(cli.config.as_deref())?;
drizzle_cli::commands::introspect::run(
&config,
db_name,
init_metadata,
casing,
out.as_deref(),
breakpoints,
)
}
Command::Status => {
let config = load_config(cli.config.as_deref())?;
drizzle_cli::commands::status::run(&config, db_name)
}
Command::Check { dialect, out } => {
let config = load_config(cli.config.as_deref())?;
drizzle_cli::commands::check::run(&config, db_name, dialect.as_deref(), out.as_deref())
}
Command::Export { sql } => {
let config = load_config(cli.config.as_deref())?;
drizzle_cli::commands::export::run(&config, db_name, sql)
}
}
}
fn load_config(custom_path: Option<&std::path::Path>) -> Result<Config, CliError> {
match custom_path {
Some(path) => {
if let Some(dir) = path.parent() {
let _ = dotenvy::from_path(dir.join(".env"));
}
Config::load_from(path).map_err(Into::into)
}
None => Config::load().map_err(Into::into),
}
}
fn run_init(dialect: &str, driver: Option<&str>) -> Result<(), CliError> {
let config_path = PathBuf::from(DEFAULT_CONFIG_FILE);
if config_path.exists() {
return Err(CliError::Other(format!(
"{} already exists. Delete it first to reinitialize.",
DEFAULT_CONFIG_FILE
)));
}
let config_content = generate_init_config(dialect, driver)?;
std::fs::write(&config_path, config_content).map_err(|e| CliError::IoError(e.to_string()))?;
println!(
"{}",
output::success(&format!("Created {}", DEFAULT_CONFIG_FILE))
);
println!();
println!("Next steps:");
println!(
" 1. Edit {} with your database credentials",
DEFAULT_CONFIG_FILE
);
println!(
" 2. Create your schema file at {}",
output::heading("src/schema.rs")
);
println!(
" 3. Run {} to generate your first migration",
output::heading("drizzle generate")
);
Ok(())
}
fn generate_init_config(dialect: &str, driver: Option<&str>) -> Result<String, CliError> {
let dialect = dialect.to_lowercase();
let driver = driver.map(|d| d.to_lowercase());
match dialect.as_str() {
"sqlite" => {
if let Some(ref d) = driver
&& d != "rusqlite"
{
return Err(CliError::Other(format!(
"Invalid driver for sqlite: {d}. Supported: rusqlite"
)));
}
Ok(format!(
r#"#:schema {}
# Drizzle Configuration (drizzle-rs)
#
# This file is parsed by `drizzle-cli` and should stay aligned with its config schema:
# - dialect: sqlite | turso | postgresql
# - drivers: Rust drivers only (optional)
dialect = "sqlite"
# driver = "rusqlite"
schema = "src/schema.rs"
out = "./drizzle"
# breakpoints = true
[dbCredentials]
url = "./dev.db"
"#,
SCHEMA_URL
))
}
"turso" => {
if let Some(ref d) = driver
&& d != "libsql"
&& d != "turso"
{
return Err(CliError::Other(format!(
"Invalid driver for turso: {d}. Supported: libsql, turso"
)));
}
Ok(format!(
r#"#:schema {}
# Drizzle Configuration (drizzle-rs)
dialect = "turso"
# driver = "libsql" # local libsql (embedded)
# driver = "turso" # remote Turso
schema = "src/schema.rs"
out = "./drizzle"
# breakpoints = true
[dbCredentials]
url = "libsql://your-db.turso.io"
authToken = "your-auth-token"
"#,
SCHEMA_URL
))
}
"postgresql" | "postgres" => {
if let Some(ref d) = driver
&& d != "postgres-sync"
&& d != "tokio-postgres"
{
return Err(CliError::Other(format!(
"Invalid driver for postgresql: {d}. Supported: postgres-sync, tokio-postgres"
)));
}
Ok(format!(
r#"#:schema {}
# Drizzle Configuration (drizzle-rs)
dialect = "postgresql"
# driver = "postgres-sync"
# driver = "tokio-postgres"
schema = "src/schema.rs"
out = "./drizzle"
# breakpoints = true
[dbCredentials]
url = "postgres://user:password@localhost:5432/mydb"
# Or use individual connection fields:
# [dbCredentials]
# host = "localhost"
# port = 5432
# user = "postgres"
# password = "password"
# database = "mydb"
# ssl = true
"#,
SCHEMA_URL
))
}
_ => Err(CliError::Other(format!(
"Unknown dialect: {dialect}. Supported: sqlite, turso, postgresql"
))),
}
}