1use super::dialect::MigrationDialect;
4use super::diff::index_name;
5use super::engine::MigrationEngine;
6use super::history::{MIGRATION_HISTORY_TABLE, PRODUCT_VERSION};
7use super::schema_change::SchemaChange;
8use super::types::{ModelSnapshot, SnapshotColumn};
9
10impl MigrationEngine {
11 pub(crate) fn initial_create_with_fks(&self, current: &ModelSnapshot) -> Vec<SchemaChange> {
12 let mut changes = self.initial_create(current);
13 for et in ¤t.entity_types {
14 Self::append_create_table_fks(&mut changes, &et.table_name, &et.columns);
15 Self::append_create_table_indexes(&mut changes, &et.table_name, &et.columns);
16 }
17 changes
18 }
19
20 pub fn foreign_key_name(table: &str, column: &str, referenced_table: &str) -> String {
26 format!("fk_{}_{}_{}", table, column, referenced_table)
27 }
28
29 pub fn index_name(table: &str, column: &str) -> String {
31 format!("ix_{}_{}", table, column)
32 }
33
34 fn column_type(&self, col: &SnapshotColumn) -> (String, bool) {
41 let col_type = self.dialect.map_column_type(col);
42 if self.dialect == MigrationDialect::MySql && (col.is_auto_increment || col.is_sequence) {
43 let stripped = col_type
44 .strip_suffix(" AUTO_INCREMENT")
45 .unwrap_or(&col_type)
46 .to_string();
47 (stripped, true)
48 } else {
49 (col_type, false)
50 }
51 }
52
53 pub fn generate_alter_column_sql(
54 &self,
55 table: &str,
56 column_name: &str,
57 new: &SnapshotColumn,
58 ) -> String {
59 let q = |s: &str| self.dialect.quote(s);
60 let (col_type, mysql_auto_increment) = self.column_type(new);
61 match self.dialect {
62 MigrationDialect::Postgres => {
63 let nullable = if new.is_required || new.is_primary_key {
64 "SET NOT NULL"
65 } else {
66 "DROP NOT NULL"
67 };
68 format!(
69 "ALTER TABLE {} ALTER COLUMN {} TYPE {};\nALTER TABLE {} ALTER COLUMN {} {};\n",
70 q(table),
71 q(column_name),
72 col_type,
73 q(table),
74 q(column_name),
75 nullable
76 )
77 }
78 MigrationDialect::MySql => {
79 let nullable = if new.is_required || new.is_primary_key {
80 "NOT NULL"
81 } else {
82 "NULL"
83 };
84 let auto_increment = if mysql_auto_increment {
85 " AUTO_INCREMENT"
86 } else {
87 ""
88 };
89 format!(
90 "ALTER TABLE {} MODIFY COLUMN {} {} {}{};\n",
91 q(table),
92 q(column_name),
93 col_type,
94 nullable,
95 auto_increment
96 )
97 }
98 MigrationDialect::Sqlite => format!(
99 "-- SQLite does not support ALTER COLUMN type changes; \
100 rebuild table manually for {}.{}\n",
101 q(table),
102 q(column_name)
103 ),
104 }
105 }
106
107 pub(crate) fn generate_up_sql(&self, changes: &[SchemaChange]) -> String {
108 self.generate_up_sql_inner(changes, true)
109 }
110
111 pub(crate) fn generate_ddl_sql(&self, changes: &[SchemaChange]) -> String {
112 self.generate_up_sql_inner(changes, false)
113 }
114
115 fn generate_up_sql_inner(&self, changes: &[SchemaChange], record_history: bool) -> String {
116 let mut sql = String::from("-- Up Migration (auto-generated by rust-ef)\n\n");
117 let q = |s: &str| self.dialect.quote(s);
118 let create_kw = if record_history {
119 "CREATE TABLE"
120 } else {
121 "CREATE TABLE IF NOT EXISTS"
122 };
123
124 for change in changes {
125 match change {
126 SchemaChange::CreateTable { table, columns } => {
127 if self.dialect == MigrationDialect::Postgres {
129 for c in columns {
130 if c.is_sequence {
131 if let Some(seq_name) = &c.sequence_name {
132 sql.push_str(&format!(
133 "CREATE SEQUENCE IF NOT EXISTS {};\n",
134 q(seq_name)
135 ));
136 }
137 }
138 }
139 }
140
141 sql.push_str(&format!("{} {} (\n", create_kw, q(table)));
142
143 let pk_columns: Vec<&str> = columns
145 .iter()
146 .filter(|c| c.is_primary_key)
147 .map(|c| c.column_name.as_str())
148 .collect();
149
150 let col_defs: Vec<String> = columns
151 .iter()
152 .map(|c| {
153 let nullable = if c.is_required || c.is_primary_key {
154 "NOT NULL"
155 } else {
156 "NULL"
157 };
158 let (col_type, mysql_auto_increment) = self.column_type(c);
159 if c.is_sequence && self.dialect == MigrationDialect::Postgres {
161 if let Some(seq_name) = &c.sequence_name {
162 return format!(
163 "{} {} DEFAULT nextval('{}') {}",
164 q(&c.column_name),
165 col_type,
166 seq_name,
167 nullable
168 );
169 }
170 }
171 let mut parts = vec![q(&c.column_name), col_type, nullable.to_string()];
173 if mysql_auto_increment {
174 parts.push("AUTO_INCREMENT".to_string());
175 }
176 parts.join(" ")
177 })
178 .collect();
179 sql.push_str(&format!(" {}\n", col_defs.join(",\n ")));
180
181 if pk_columns.len() == 1 {
183 sql.push_str(&format!(" ,PRIMARY KEY ({})\n", q(pk_columns[0])));
184 } else if pk_columns.len() > 1 {
185 let pk_list: Vec<String> = pk_columns.iter().map(|c| q(c)).collect();
186 sql.push_str(&format!(" ,PRIMARY KEY ({})\n", pk_list.join(", ")));
187 }
188
189 sql.push_str(");\n\n");
190 }
191 SchemaChange::DropTable { table } => {
192 sql.push_str(&format!("DROP TABLE IF EXISTS {};\n", q(table)));
193 }
194 SchemaChange::AddColumn { table, column } => {
195 let (col_type, mysql_auto_increment) = self.column_type(column);
196 let nullable = if column.is_required || column.is_primary_key {
197 "NOT NULL"
198 } else {
199 "NULL"
200 };
201 let auto_increment = if mysql_auto_increment {
202 " AUTO_INCREMENT"
203 } else {
204 ""
205 };
206 sql.push_str(&format!(
207 "ALTER TABLE {} ADD COLUMN {} {} {}{};\n",
208 q(table),
209 q(&column.column_name),
210 col_type,
211 nullable,
212 auto_increment
213 ));
214 }
215 SchemaChange::DropColumn { table, column_name } => {
216 sql.push_str(&format!(
217 "ALTER TABLE {} DROP COLUMN IF EXISTS {};\n",
218 q(table),
219 q(column_name)
220 ));
221 }
222 SchemaChange::AlterColumn {
223 table,
224 column_name,
225 old: _,
226 new,
227 } => {
228 sql.push_str(&self.generate_alter_column_sql(table, column_name, new));
229 }
230 SchemaChange::AddForeignKey {
231 table,
232 column,
233 referenced_table,
234 referenced_column,
235 on_delete,
236 } => {
237 let fk_name = Self::foreign_key_name(table, column, referenced_table);
238 let on_delete_clause = on_delete
239 .as_deref()
240 .map(|c| format!(" ON DELETE {}", c))
241 .unwrap_or_default();
242 sql.push_str(&format!(
243 "ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}){};\n",
244 q(table),
245 q(&fk_name),
246 q(column),
247 q(referenced_table),
248 q(referenced_column),
249 on_delete_clause
250 ));
251 }
252 SchemaChange::DropForeignKey {
253 table,
254 column,
255 referenced_table,
256 } => {
257 let fk_name = Self::foreign_key_name(table, column, referenced_table);
258 match self.dialect {
259 MigrationDialect::MySql => sql.push_str(&format!(
260 "ALTER TABLE {} DROP FOREIGN KEY {};\n",
261 q(table),
262 q(&fk_name)
263 )),
264 _ => sql.push_str(&format!(
265 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {};\n",
266 q(table),
267 q(&fk_name)
268 )),
269 }
270 }
271 SchemaChange::CreateIndex {
272 table,
273 column,
274 is_unique,
275 } => {
276 let unique_kw = if *is_unique { "UNIQUE " } else { "" };
277 let idx_name = index_name(table, column);
278 sql.push_str(&format!(
279 "CREATE {unique_kw}INDEX {} ON {} ({});\n",
280 q(&idx_name),
281 q(table),
282 q(column)
283 ));
284 }
285 SchemaChange::DropIndex {
286 table,
287 column,
288 is_unique: _,
289 } => {
290 let idx_name = index_name(table, column);
291 match self.dialect {
292 MigrationDialect::MySql => {
293 sql.push_str(&format!("DROP INDEX {} ON {};\n", q(&idx_name), q(table)))
294 }
295 _ => sql.push_str(&format!("DROP INDEX IF EXISTS {};\n", q(&idx_name))),
296 }
297 }
298 }
299 }
300
301 if record_history {
302 sql.push_str(&format!(
303 "INSERT INTO {}(migration_id, product_version) VALUES ('{{migration_id}}', '{}');\n",
304 q(MIGRATION_HISTORY_TABLE),
305 PRODUCT_VERSION
306 ));
307 }
308
309 sql
310 }
311
312 pub(super) fn generate_down_sql(&self, changes: &[SchemaChange]) -> String {
313 let mut sql = String::from("-- Down Migration (auto-generated by rust-ef)\n\n");
314 let q = |s: &str| self.dialect.quote(s);
315
316 for change in changes.iter().rev() {
318 match change {
319 SchemaChange::CreateTable { table, .. } => {
320 sql.push_str(&format!("DROP TABLE IF EXISTS {};\n", q(table)));
321 }
322 SchemaChange::DropTable { table } => {
323 sql.push_str(&format!(
324 "-- WARNING: Cannot restore table {} (original schema unknown)\n",
325 q(table)
326 ));
327 }
328 SchemaChange::AddColumn { table, column } => {
329 sql.push_str(&format!(
330 "ALTER TABLE {} DROP COLUMN IF EXISTS {};\n",
331 q(table),
332 q(&column.column_name)
333 ));
334 }
335 SchemaChange::DropColumn { table, column_name } => {
336 sql.push_str(&format!(
337 "-- WARNING: Cannot restore column {} on {} (original type unknown)\n",
338 q(column_name),
339 q(table)
340 ));
341 }
342 SchemaChange::AlterColumn {
343 table,
344 column_name,
345 old,
346 new: _,
347 } => {
348 sql.push_str(&self.generate_alter_column_sql(table, column_name, old));
349 }
350 SchemaChange::AddForeignKey {
351 table,
352 column,
353 referenced_table,
354 ..
355 } => {
356 let fk_name = Self::foreign_key_name(table, column, referenced_table);
357 match self.dialect {
358 MigrationDialect::MySql => sql.push_str(&format!(
359 "ALTER TABLE {} DROP FOREIGN KEY {};\n",
360 q(table),
361 q(&fk_name)
362 )),
363 _ => sql.push_str(&format!(
364 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {};\n",
365 q(table),
366 q(&fk_name)
367 )),
368 }
369 }
370 SchemaChange::DropForeignKey {
371 table,
372 column,
373 referenced_table,
374 ..
375 } => {
376 sql.push_str(&format!(
377 "-- WARNING: Cannot restore foreign key constraint {} on {}.{}\n",
378 q(&Self::foreign_key_name(table, column, referenced_table)),
379 q(table),
380 q(column)
381 ));
382 }
383 SchemaChange::CreateIndex {
384 table,
385 column,
386 is_unique: _,
387 } => {
388 let idx_name = index_name(table, column);
389 match self.dialect {
390 MigrationDialect::MySql => {
391 sql.push_str(&format!("DROP INDEX {} ON {};\n", q(&idx_name), q(table)))
392 }
393 _ => sql.push_str(&format!("DROP INDEX IF EXISTS {};\n", q(&idx_name))),
394 }
395 }
396 SchemaChange::DropIndex {
397 table,
398 column,
399 is_unique,
400 } => {
401 let unique_kw = if *is_unique { "UNIQUE " } else { "" };
402 let idx_name = index_name(table, column);
403 sql.push_str(&format!(
404 "CREATE {unique_kw}INDEX {} ON {} ({});\n",
405 q(&idx_name),
406 q(table),
407 q(column)
408 ));
409 }
410 }
411 }
412
413 sql.push_str(&format!(
414 "DELETE FROM {} WHERE migration_id = '{{migration_id}}';\n",
415 q(MIGRATION_HISTORY_TABLE)
416 ));
417
418 sql
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use crate::migration::schema_change::SchemaChange;
426 use crate::migration::types::SnapshotColumn;
427
428 #[test]
429 fn mysql_auto_increment_follows_nullability() {
430 let engine = MigrationEngine::new(MigrationDialect::MySql);
431 let column = SnapshotColumn {
432 column_name: "id".into(),
433 type_name: "i32".into(),
434 is_primary_key: true,
435 is_required: true,
436 is_auto_increment: true,
437 ..Default::default()
438 };
439 let sql = engine.generate_ddl_sql(&[SchemaChange::CreateTable {
440 table: "items".into(),
441 columns: vec![column],
442 }]);
443 assert!(
444 sql.contains("`id` INT NOT NULL AUTO_INCREMENT"),
445 "unexpected DDL: {sql}"
446 );
447 assert!(
448 !sql.contains("INT AUTO_INCREMENT NOT NULL"),
449 "unexpected DDL: {sql}"
450 );
451 }
452
453 #[test]
454 fn primary_key_columns_are_not_null_even_without_required() {
455 let engine = MigrationEngine::new(MigrationDialect::Postgres);
456 let column = SnapshotColumn {
457 column_name: "id".into(),
458 type_name: "i32".into(),
459 is_primary_key: true,
460 is_required: false,
461 is_auto_increment: true,
462 ..Default::default()
463 };
464 let sql = engine.generate_ddl_sql(&[SchemaChange::CreateTable {
465 table: "items".into(),
466 columns: vec![column],
467 }]);
468 assert!(
469 sql.contains("\"id\" SERIAL NOT NULL"),
470 "unexpected DDL: {sql}"
471 );
472 }
473}