drizzle_cli/commands/
push.rs

1//! Push command implementation
2//!
3//! Pushes schema changes directly to the database without creating migration files.
4//! Note: This command requires database connectivity which depends on
5//! driver-specific features being enabled.
6
7use crate::config::{Casing, DrizzleConfig};
8use crate::error::CliError;
9use crate::output;
10use crate::snapshot::parse_result_to_snapshot;
11
12#[derive(Debug, Clone)]
13pub struct PushOptions {
14    pub cli_verbose: bool,
15    pub cli_strict: bool,
16    pub force: bool,
17    pub cli_explain: bool,
18    pub casing: Option<Casing>,
19    pub extensions_filters: Option<Vec<String>>,
20}
21
22/// Run the push command
23pub fn run(
24    config: &DrizzleConfig,
25    db_name: Option<&str>,
26    opts: PushOptions,
27) -> Result<(), CliError> {
28    use drizzle_migrations::parser::SchemaParser;
29
30    let db = config.database(db_name)?;
31
32    // CLI flags override config
33    let verbose = opts.cli_verbose || db.verbose;
34    let explain = opts.cli_explain;
35    let _effective_casing = opts.casing.unwrap_or_else(|| db.effective_casing());
36    // Note: extensions_filters would be used when introspecting the database
37    // to filter out extension-specific types (e.g., PostGIS geometry types)
38    let _extensions_filters = opts.extensions_filters;
39
40    if opts.cli_strict {
41        println!(
42            "{}",
43            output::warning("Deprecated: Do not use '--strict'. Use '--explain' instead.")
44        );
45        return Err(CliError::Other("strict flag is deprecated".into()));
46    }
47
48    if !config.is_single_database() {
49        let name = db_name.unwrap_or("(default)");
50        println!("{}: {}", output::label("Database"), name);
51    }
52
53    println!("{}", output::heading("Pushing schema to database..."));
54    println!();
55
56    // Get credentials
57    let credentials = db.credentials()?;
58    let credentials = match credentials {
59        Some(c) => c,
60        None => {
61            println!("{}", output::warning("No database credentials configured."));
62            println!();
63            println!("Add credentials to your drizzle.config.toml:");
64            println!();
65            println!("  {}", output::muted("[dbCredentials]"));
66            match db.dialect.to_base() {
67                drizzle_types::Dialect::SQLite => {
68                    println!("  {}", output::muted("url = \"./dev.db\""));
69                }
70                drizzle_types::Dialect::PostgreSQL => {
71                    println!(
72                        "  {}",
73                        output::muted("url = \"postgres://user:pass@localhost:5432/db\"")
74                    );
75                }
76                drizzle_types::Dialect::MySQL => {
77                    // drizzle-cli doesn't currently support MySQL end-to-end, but the base
78                    // dialect type includes it, so keep the match exhaustive.
79                    println!(
80                        "  {}",
81                        output::muted("url = \"mysql://user:pass@localhost:3306/db\"")
82                    );
83                }
84            }
85            println!();
86            println!("Or use an environment variable:");
87            println!();
88            println!("  {}", output::muted("[dbCredentials]"));
89            println!("  {}", output::muted("url = { env = \"DATABASE_URL\" }"));
90            return Ok(());
91        }
92    };
93
94    // Parse schema files
95    let schema_files = db.schema_files()?;
96    if schema_files.is_empty() {
97        return Err(CliError::NoSchemaFiles(db.schema_display()));
98    }
99
100    println!(
101        "  {} {} schema file(s)",
102        output::label("Parsing"),
103        schema_files.len()
104    );
105
106    let mut combined_code = String::new();
107    for path in &schema_files {
108        let code = std::fs::read_to_string(path)
109            .map_err(|e| CliError::IoError(format!("Failed to read {}: {}", path.display(), e)))?;
110        combined_code.push_str(&code);
111        combined_code.push('\n');
112    }
113
114    let parse_result = SchemaParser::parse(&combined_code);
115
116    if parse_result.tables.is_empty() && parse_result.indexes.is_empty() {
117        println!(
118            "{}",
119            output::warning("No tables or indexes found in schema files.")
120        );
121        return Ok(());
122    }
123
124    println!(
125        "  {} {} table(s), {} index(es)",
126        output::label("Found"),
127        parse_result.tables.len(),
128        parse_result.indexes.len()
129    );
130
131    // Build snapshot from parsed schema (use config dialect)
132    let dialect = db.dialect.to_base();
133    let desired_snapshot = parse_result_to_snapshot(&parse_result, dialect);
134
135    // Compute push plan (DB snapshot -> desired snapshot)
136    let plan = crate::db::plan_push(&credentials, db.dialect, &desired_snapshot, db.breakpoints)?;
137
138    if !plan.warnings.is_empty() {
139        println!("{}", output::warning("Warnings:"));
140        for w in &plan.warnings {
141            println!("  {} {}", output::warning("-"), w);
142        }
143        println!();
144    }
145
146    // Print SQL plan for explain/verbose
147    if explain || verbose {
148        if plan.sql_statements.is_empty() {
149            println!("{}", output::success("No schema changes detected."));
150            return Ok(());
151        }
152
153        println!("{}", output::muted("--- Planned SQL ---"));
154        println!();
155        for stmt in &plan.sql_statements {
156            println!("{stmt}\n");
157        }
158        println!("{}", output::muted("--- End SQL ---"));
159        println!();
160    }
161
162    // Provide explain/dry-run output when requested
163    if explain {
164        return Ok(());
165    }
166
167    if plan.sql_statements.is_empty() {
168        println!("{}", output::success("No schema changes detected."));
169        return Ok(());
170    }
171
172    // Apply plan
173    crate::db::apply_push(&credentials, db.dialect, &plan, opts.force)?;
174
175    println!("{}", output::success("Push complete!"));
176
177    Ok(())
178}