Skip to main content

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