use crate::commands::overrides::{self, ConnectionOverrides, FilterArgs};
use crate::config::{Casing, Config, Dialect};
use crate::error::CliError;
use crate::output;
use crate::snapshot::parse_result_to_snapshot;
#[derive(clap::Args, Debug, Clone)]
pub struct PushOptions {
#[arg(long)]
pub verbose: bool,
#[arg(long)]
pub force: bool,
#[arg(long)]
pub explain: bool,
#[arg(long)]
pub casing: Option<Casing>,
#[arg(long)]
pub dialect: Option<Dialect>,
#[arg(long, value_delimiter = ',')]
pub schema: Option<Vec<String>>,
#[command(flatten)]
pub filters: FilterArgs,
#[command(flatten)]
pub connection: ConnectionOverrides,
}
pub fn run(config: &Config, db_name: Option<&str>, opts: &PushOptions) -> Result<(), CliError> {
let db = config.database(db_name)?;
let verbose = opts.verbose || db.verbose;
let explain = opts.explain;
let effective_casing = opts.casing.or(db.casing);
let effective_dialect = overrides::resolve_dialect(db, opts.dialect);
warn_unsupported_pg_filters(effective_dialect, opts);
crate::commands::harness::print_db_header(config, db_name);
println!("{}", output::heading("Pushing schema to database..."));
println!();
println!(
" {}: {}",
output::label("Dialect"),
effective_dialect.as_str()
);
let credentials = overrides::resolve_credentials(db, effective_dialect, &opts.connection)?;
let Some(credentials) = credentials else {
print_missing_credentials_help(effective_dialect);
return Ok(());
};
let parse_result = parse_schema_files(db, opts.schema.as_deref())?;
if parse_result.tables.is_empty() && parse_result.indexes.is_empty() {
println!(
"{}",
output::warning("No tables or indexes found in schema files.")
);
return Ok(());
}
println!(
" {} {} table(s), {} index(es)",
output::label("Found"),
parse_result.tables.len(),
parse_result.indexes.len()
);
let dialect = effective_dialect.to_base();
let mut desired_snapshot = parse_result_to_snapshot(&parse_result, dialect, effective_casing);
let filters = crate::db::SnapshotFilters {
tables: overrides::resolve_filter_list(
opts.filters.tables_filter.as_deref(),
db.tables_filter.as_ref(),
),
schemas: overrides::resolve_schema_filters(
effective_dialect,
opts.filters.schema_filters.as_deref(),
db.schema_filter.as_ref(),
),
extensions: overrides::resolve_extensions_filter(
opts.filters.extensions_filters.as_deref(),
db.extensions_filters.as_deref(),
),
};
crate::db::apply_snapshot_filters(&mut desired_snapshot, effective_dialect, &filters)?;
let plan = crate::db::plan_push(
&credentials,
effective_dialect,
&desired_snapshot,
db.breakpoints,
&filters,
)?;
if !plan.warnings.is_empty() {
println!("{}", output::warning("Warnings:"));
for w in &plan.warnings {
println!(" {} {}", output::warning("-"), w);
}
println!();
}
if explain || verbose {
if plan.sql_statements.is_empty() {
println!("{}", output::success("No schema changes detected."));
return Ok(());
}
println!("{}", output::muted("--- Planned SQL ---"));
println!();
for stmt in &plan.sql_statements {
println!("{stmt}\n");
}
println!("{}", output::muted("--- End SQL ---"));
println!();
}
if explain {
return Ok(());
}
if plan.sql_statements.is_empty() {
println!("{}", output::success("No schema changes detected."));
return Ok(());
}
crate::db::apply_push(&credentials, effective_dialect, &plan, opts.force)?;
println!("{}", output::success("Push complete!"));
Ok(())
}
fn warn_unsupported_pg_filters(effective_dialect: Dialect, opts: &PushOptions) {
if effective_dialect == Dialect::Postgresql {
return;
}
if opts
.filters
.schema_filters
.as_ref()
.is_some_and(|v| !v.is_empty())
{
println!(
"{}",
output::warning("Ignoring --schemaFilters: only supported for postgresql")
);
}
if opts
.filters
.extensions_filters
.as_ref()
.is_some_and(|v| !v.is_empty())
{
println!(
"{}",
output::warning("Ignoring --extensionsFilters: only supported for postgresql")
);
}
}
fn print_missing_credentials_help(effective_dialect: Dialect) {
println!("{}", output::warning("No database credentials configured."));
println!();
println!("Add credentials to your drizzle.config.toml:");
println!();
println!(" {}", output::muted("[dbCredentials]"));
match effective_dialect.to_base() {
drizzle_types::Dialect::SQLite => {
println!(" {}", output::muted("url = \"./dev.db\""));
}
drizzle_types::Dialect::PostgreSQL => {
println!(
" {}",
output::muted("url = \"postgres://user:pass@localhost:5432/db\"")
);
}
drizzle_types::Dialect::MySQL => {
println!(
" {}",
output::muted("url = \"mysql://user:pass@localhost:3306/db\"")
);
}
}
println!();
println!("Or use an environment variable:");
println!();
println!(" {}", output::muted("[dbCredentials]"));
println!(" {}", output::muted("url = { env = \"DATABASE_URL\" }"));
}
fn parse_schema_files(
db: &crate::config::DatabaseConfig,
schema_override: Option<&[String]>,
) -> Result<drizzle_migrations::parser::ParseResult, CliError> {
use drizzle_migrations::parser::SchemaParser;
let schema_files = overrides::resolve_schema_files(db, schema_override)?;
if schema_files.is_empty() {
return Err(CliError::NoSchemaFiles(overrides::resolve_schema_display(
db,
schema_override,
)));
}
println!(
" {} {} schema file(s)",
output::label("Parsing"),
schema_files.len()
);
let mut combined_code = String::new();
for path in &schema_files {
let code = std::fs::read_to_string(path)
.map_err(|e| CliError::IoError(format!("Failed to read {}: {}", path.display(), e)))?;
combined_code.push_str(&code);
combined_code.push('\n');
}
Ok(SchemaParser::parse(&combined_code))
}