drizzle_cli/commands/
generate.rs1use std::path::Path;
6
7use crate::config::{Casing, DrizzleConfig};
8use crate::error::CliError;
9use crate::output;
10use crate::snapshot::parse_result_to_snapshot;
11
12pub 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 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 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 if custom {
44 return generate_custom_migration(db, name);
45 }
46
47 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 let dialect = db.dialect.to_base();
86
87 let current_snapshot = parse_result_to_snapshot(&parse_result, dialect);
89
90 let journal_path = db.journal_path();
92 let prev_snapshot = load_previous_snapshot(out_dir, &journal_path, dialect)?;
93
94 let sql_statements = generate_diff(&prev_snapshot, ¤t_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 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 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 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 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 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
157fn 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 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 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 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
222fn 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 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 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 Ok(Snapshot::empty(dialect))
247}
248
249fn 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 let prev_ddl = SQLiteDDL::from_entities(prev_snap.ddl.clone());
264 let cur_ddl = SQLiteDDL::from_entities(curr_snap.ddl.clone());
265
266 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}