drizzle_cli/commands/
generate.rs

1//! Generate command implementation
2//!
3//! Generates migration files from schema changes.
4
5use std::path::Path;
6
7use crate::config::{Casing, DrizzleConfig};
8use crate::error::CliError;
9use crate::output;
10use crate::snapshot::parse_result_to_snapshot;
11
12/// Run the generate command
13pub fn run(
14    config: &DrizzleConfig,
15    db_name: Option<&str>,
16    name: Option<String>,
17    custom: bool,
18    casing: Option<Casing>,
19) -> Result<(), CliError> {
20    use drizzle_migrations::journal::Journal;
21    use drizzle_migrations::parser::SchemaParser;
22    use drizzle_migrations::words::{PrefixMode, generate_migration_tag_with_mode};
23
24    let db = config.database(db_name)?;
25
26    // CLI flag overrides config, config default is camelCase
27    let _effective_casing = casing.unwrap_or_else(|| db.effective_casing());
28
29    if !config.is_single_database() {
30        let name = db_name.unwrap_or("(default)");
31        println!("{}: {}", output::label("Database"), name);
32    }
33
34    println!("{}", output::heading("Generating migration..."));
35
36    // Create output directories if they don't exist
37    let out_dir = db.migrations_dir();
38    let meta_dir = db.meta_dir();
39    std::fs::create_dir_all(out_dir).map_err(|e| CliError::IoError(e.to_string()))?;
40    std::fs::create_dir_all(&meta_dir).map_err(|e| CliError::IoError(e.to_string()))?;
41
42    // Handle custom migration (empty migration file for manual SQL)
43    if custom {
44        return generate_custom_migration(db, name);
45    }
46
47    // Parse schema files
48    let schema_files = db.schema_files()?;
49    if schema_files.is_empty() {
50        return Err(CliError::NoSchemaFiles(db.schema_display()));
51    }
52
53    println!(
54        "  {} {} schema file(s)",
55        output::label("Parsing"),
56        schema_files.len()
57    );
58
59    let mut combined_code = String::new();
60    for path in &schema_files {
61        let code = std::fs::read_to_string(path)
62            .map_err(|e| CliError::IoError(format!("Failed to read {}: {}", path.display(), e)))?;
63        combined_code.push_str(&code);
64        combined_code.push('\n');
65    }
66
67    let parse_result = SchemaParser::parse(&combined_code);
68
69    if parse_result.tables.is_empty() && parse_result.indexes.is_empty() {
70        println!(
71            "{}",
72            output::warning("No tables or indexes found in schema files.")
73        );
74        return Ok(());
75    }
76
77    println!(
78        "  {} {} table(s), {} index(es)",
79        output::label("Found"),
80        parse_result.tables.len(),
81        parse_result.indexes.len()
82    );
83
84    // Get dialect from config
85    let dialect = db.dialect.to_base();
86
87    // Build current snapshot from parsed schema (use config dialect, not parser-detected)
88    let current_snapshot = parse_result_to_snapshot(&parse_result, dialect);
89
90    // Load previous snapshot if exists
91    let journal_path = db.journal_path();
92    let prev_snapshot = load_previous_snapshot(out_dir, &journal_path, dialect)?;
93
94    // Generate diff
95    let sql_statements = generate_diff(&prev_snapshot, &current_snapshot, db.breakpoints)?;
96
97    if sql_statements.is_empty() {
98        println!("{}", output::warning("No schema changes detected 😴"));
99        return Ok(());
100    }
101
102    println!(
103        "  {} {} SQL statement(s)",
104        output::label("Generated"),
105        sql_statements.len()
106    );
107
108    // Load or create journal (needed for index-based prefixes)
109    let mut journal = Journal::load_or_create(&journal_path, dialect)
110        .map_err(|e| CliError::IoError(e.to_string()))?;
111
112    let prefix_mode = db
113        .migrations
114        .as_ref()
115        .and_then(|m| m.prefix)
116        .map(map_prefix_mode)
117        .unwrap_or(PrefixMode::Timestamp);
118
119    let migration_tag =
120        generate_migration_tag_with_mode(prefix_mode, journal.next_idx(), name.as_deref());
121
122    // Create migration subdirectory: {out}/{tag}/
123    let migration_dir = out_dir.join(&migration_tag);
124    std::fs::create_dir_all(&migration_dir).map_err(|e| CliError::IoError(e.to_string()))?;
125
126    // Write {tag}/migration.sql
127    let migration_sql_path = migration_dir.join("migration.sql");
128    let sql_content = if db.breakpoints {
129        sql_statements.join("\n--> statement-breakpoint\n")
130    } else {
131        sql_statements.join("\n\n")
132    };
133    std::fs::write(&migration_sql_path, &sql_content)
134        .map_err(|e| CliError::IoError(e.to_string()))?;
135
136    // Write {tag}/snapshot.json
137    let snapshot_path = migration_dir.join("snapshot.json");
138    current_snapshot
139        .save(&snapshot_path)
140        .map_err(|e| CliError::IoError(e.to_string()))?;
141
142    // Update journal
143    journal.add_entry(migration_tag.clone(), db.breakpoints);
144    journal
145        .save(&journal_path)
146        .map_err(|e| CliError::IoError(e.to_string()))?;
147
148    println!(
149        "{}",
150        output::success(&format!("Migration generated: {}", migration_tag))
151    );
152    println!("   {}", migration_dir.display());
153
154    Ok(())
155}
156
157/// Generate an empty custom migration for manual SQL
158fn generate_custom_migration(
159    db: &crate::config::DatabaseConfig,
160    name: Option<String>,
161) -> Result<(), CliError> {
162    use drizzle_migrations::journal::Journal;
163    use drizzle_migrations::words::{PrefixMode, generate_migration_tag_with_mode};
164
165    let out_dir = db.migrations_dir();
166    let journal_path = db.journal_path();
167    let dialect = db.dialect.to_base();
168
169    let custom_name = name.unwrap_or_else(|| "custom".to_string());
170    let mut journal = Journal::load_or_create(&journal_path, dialect)
171        .map_err(|e| CliError::IoError(e.to_string()))?;
172
173    let prefix_mode = db
174        .migrations
175        .as_ref()
176        .and_then(|m| m.prefix)
177        .map(map_prefix_mode)
178        .unwrap_or(PrefixMode::Timestamp);
179
180    let migration_tag =
181        generate_migration_tag_with_mode(prefix_mode, journal.next_idx(), Some(&custom_name));
182
183    // Create migration subdirectory: {out}/{tag}/
184    let migration_dir = out_dir.join(&migration_tag);
185    std::fs::create_dir_all(&migration_dir).map_err(|e| CliError::IoError(e.to_string()))?;
186
187    // Write {tag}/migration.sql with comment
188    let migration_sql_path = migration_dir.join("migration.sql");
189    let sql_content = "-- Custom SQL migration file, put your code below! --\n\n";
190    std::fs::write(&migration_sql_path, sql_content)
191        .map_err(|e| CliError::IoError(e.to_string()))?;
192
193    // Update journal
194    journal.add_entry(migration_tag.clone(), db.breakpoints);
195    journal
196        .save(&journal_path)
197        .map_err(|e| CliError::IoError(e.to_string()))?;
198
199    println!(
200        "{}",
201        output::success(&format!("Custom migration created: {}", migration_tag))
202    );
203    println!("   {}", migration_dir.display());
204    println!(
205        "{}",
206        output::label("   Edit the migration file to add your SQL statements.")
207    );
208
209    Ok(())
210}
211
212fn map_prefix_mode(p: crate::config::MigrationPrefix) -> drizzle_migrations::PrefixMode {
213    match p {
214        crate::config::MigrationPrefix::Index => drizzle_migrations::PrefixMode::Index,
215        crate::config::MigrationPrefix::Timestamp => drizzle_migrations::PrefixMode::Timestamp,
216        crate::config::MigrationPrefix::Supabase => drizzle_migrations::PrefixMode::Supabase,
217        crate::config::MigrationPrefix::Unix => drizzle_migrations::PrefixMode::Unix,
218        crate::config::MigrationPrefix::None => drizzle_migrations::PrefixMode::None,
219    }
220}
221
222/// Load the previous snapshot from the migration directory
223fn load_previous_snapshot(
224    out_dir: &Path,
225    journal_path: &Path,
226    dialect: drizzle_types::Dialect,
227) -> Result<drizzle_migrations::schema::Snapshot, CliError> {
228    use drizzle_migrations::journal::Journal;
229    use drizzle_migrations::schema::Snapshot;
230
231    // If a journal exists, it must be readable. Silently ignoring parse errors can
232    // lead to generating incorrect diffs and destructive migrations.
233    if journal_path.exists() {
234        let journal = Journal::load(journal_path).map_err(|e| CliError::IoError(e.to_string()))?;
235        if let Some(latest) = journal.entries.last() {
236            // Snapshot is in {out}/{tag}/snapshot.json
237            let snapshot_path = out_dir.join(&latest.tag).join("snapshot.json");
238            if snapshot_path.exists() {
239                return Snapshot::load(&snapshot_path, dialect)
240                    .map_err(|e| CliError::IoError(e.to_string()));
241            }
242        }
243    }
244
245    // No previous snapshot, return empty
246    Ok(Snapshot::empty(dialect))
247}
248
249/// Generate diff between two snapshots
250fn generate_diff(
251    prev: &drizzle_migrations::schema::Snapshot,
252    current: &drizzle_migrations::schema::Snapshot,
253    _breakpoints: bool,
254) -> Result<Vec<String>, CliError> {
255    use drizzle_migrations::schema::Snapshot;
256
257    match (prev, current) {
258        (Snapshot::Sqlite(prev_snap), Snapshot::Sqlite(curr_snap)) => {
259            use drizzle_migrations::sqlite::collection::SQLiteDDL;
260            use drizzle_migrations::sqlite::diff::compute_migration;
261
262            // Convert snapshots to DDL collections
263            let prev_ddl = SQLiteDDL::from_entities(prev_snap.ddl.clone());
264            let cur_ddl = SQLiteDDL::from_entities(curr_snap.ddl.clone());
265
266            // Use compute_migration which properly handles column alterations
267            // via table recreation (SQLite doesn't support ALTER COLUMN)
268            let migration = compute_migration(&prev_ddl, &cur_ddl);
269            Ok(migration.sql_statements)
270        }
271        (Snapshot::Postgres(prev_snap), Snapshot::Postgres(curr_snap)) => {
272            use drizzle_migrations::postgres::diff_full_snapshots;
273            use drizzle_migrations::postgres::statements::PostgresGenerator;
274
275            let diff = diff_full_snapshots(prev_snap, curr_snap);
276            let generator = PostgresGenerator::new().with_breakpoints(_breakpoints);
277            Ok(generator.generate(&diff.diffs))
278        }
279        _ => Err(CliError::DialectMismatch),
280    }
281}