1use super::ddl_expr::quote_string;
4use super::dialect::MigrationDialect;
5use super::diff::index_name;
6use super::engine::MigrationEngine;
7use super::history::{MIGRATION_HISTORY_TABLE, PRODUCT_VERSION};
8use super::schema_change::SchemaChange;
9use super::types::{
10 ModelSnapshot, SnapshotCheck, SnapshotColumn, SnapshotForeignKey, SnapshotIndex,
11 SnapshotIndexColumn, SnapshotTableOptions,
12};
13
14impl MigrationEngine {
15 pub(crate) fn initial_create_with_fks(&self, current: &ModelSnapshot) -> Vec<SchemaChange> {
16 let mut changes = self.initial_create(current);
17 for et in current.entity_types.iter().filter(|et| !et.is_view) {
18 if self.dialect != MigrationDialect::Sqlite {
21 Self::append_create_table_fks(&mut changes, &et.table_name, &et.columns);
22 Self::append_composite_fks(&mut changes, et);
23 }
24 Self::append_create_table_indexes(&mut changes, &et.table_name, &et.columns);
25 Self::append_declared_indexes(&mut changes, et);
26 }
27 changes
28 }
29
30 fn default_clause(&self, col: &SnapshotColumn) -> String {
32 match &col.default_value {
33 Some(v) if !v.trim().is_empty() => format!(" DEFAULT {}", v),
34 _ => String::new(),
35 }
36 }
37
38 fn collation_clause(&self, col: &SnapshotColumn) -> String {
39 match &col.collation {
40 Some(c) if !c.trim().is_empty() => format!(" COLLATE {}", c.trim()),
41 _ => String::new(),
42 }
43 }
44
45 fn render_column_definition(&self, col: &SnapshotColumn) -> String {
49 let q = |s: &str| self.dialect.quote(s);
50 let (col_type, mysql_auto_increment) = self.column_type(col);
51
52 if let Some(expr) = &col.computed_sql {
54 if expr.trim().is_empty() {
55 } else {
57 let stored = match self.dialect {
58 MigrationDialect::Postgres => true,
60 _ => col.computed_stored,
61 };
62 let storage = if stored { " STORED" } else { " VIRTUAL" };
63 let comment = if self.dialect == MigrationDialect::MySql {
64 self.comment_clause(col)
65 } else {
66 String::new()
67 };
68 return format!(
69 "{} {} GENERATED ALWAYS AS ({}){}{}",
70 q(&col.column_name),
71 col_type,
72 expr,
73 storage,
74 comment
75 );
76 }
77 }
78
79 let collation = self.collation_clause(col);
82 let col_type = if collation.is_empty() {
83 col_type
84 } else {
85 format!("{}{}", col_type, collation)
86 };
87
88 let nullable = if col.is_required || col.is_primary_key {
89 "NOT NULL"
90 } else {
91 "NULL"
92 };
93
94 if col.is_sequence && self.dialect == MigrationDialect::Postgres {
96 if let Some(seq_name) = &col.sequence_name {
97 return format!(
98 "{} {} DEFAULT nextval('{}') {}",
99 q(&col.column_name),
100 col_type,
101 seq_name,
102 nullable
103 );
104 }
105 }
106
107 let mut parts = vec![q(&col.column_name), col_type, nullable.to_string()];
108 if !mysql_auto_increment {
109 let default = self.default_clause(col);
110 if !default.is_empty() {
111 parts.push(default.trim_start().to_string());
112 }
113 }
114 if mysql_auto_increment {
115 parts.push("AUTO_INCREMENT".to_string());
116 }
117 if self.dialect == MigrationDialect::MySql {
118 let comment = self.comment_clause(col);
119 if !comment.is_empty() {
120 parts.push(comment.trim_start().to_string());
121 }
122 }
123 parts.join(" ")
124 }
125
126 fn comment_clause(&self, col: &SnapshotColumn) -> String {
128 match &col.comment {
129 Some(c) if !c.is_empty() => format!(" COMMENT {}", quote_string(c, self.dialect)),
130 _ => String::new(),
131 }
132 }
133
134 fn column_comment_sql(&self, table: &str, col: &SnapshotColumn) -> Option<String> {
136 if self.dialect != MigrationDialect::Postgres {
137 return None;
138 }
139 let comment = col.comment.as_deref()?;
140 let q = |s: &str| self.dialect.quote(s);
141 Some(format!(
142 "COMMENT ON COLUMN {}.{} IS {};\n",
143 q(table),
144 q(&col.column_name),
145 quote_string(comment, self.dialect)
146 ))
147 }
148
149 fn alter_table_comment_sql(&self, table: &str, comment: Option<&str>) -> String {
150 let q = |s: &str| self.dialect.quote(s);
151 match self.dialect {
152 MigrationDialect::Sqlite => {
153 "-- WARNING: SQLite does not support table comments\n".to_string()
154 }
155 MigrationDialect::Postgres => match comment {
156 Some(c) => format!(
157 "COMMENT ON TABLE {} IS {};\n",
158 q(table),
159 quote_string(c, self.dialect)
160 ),
161 None => format!("COMMENT ON TABLE {} IS NULL;\n", q(table)),
162 },
163 MigrationDialect::MySql => match comment {
164 Some(c) => format!(
165 "ALTER TABLE {} COMMENT = {};\n",
166 q(table),
167 quote_string(c, self.dialect)
168 ),
169 None => format!("ALTER TABLE {} COMMENT = '';\n", q(table)),
170 },
171 }
172 }
173
174 fn table_comment_sql(&self, table: &str, comment: Option<&str>) -> Option<String> {
175 let comment = comment?;
176 let q = |s: &str| self.dialect.quote(s);
177 match self.dialect {
178 MigrationDialect::Postgres => Some(format!(
179 "COMMENT ON TABLE {} IS {};\n",
180 q(table),
181 quote_string(comment, self.dialect)
182 )),
183 MigrationDialect::MySql => Some(format!(
184 "ALTER TABLE {} COMMENT = {};\n",
185 q(table),
186 quote_string(comment, self.dialect)
187 )),
188 MigrationDialect::Sqlite => None,
189 }
190 }
191
192 fn table_options_suffix(&self, options: &SnapshotTableOptions) -> String {
198 if self.dialect != MigrationDialect::MySql {
199 return String::new();
200 }
201 let engine = options.engine.as_deref().unwrap_or("InnoDB");
202 let charset = options.charset.as_deref().unwrap_or("utf8mb4");
203 let mut suffix = format!(" ENGINE={engine} DEFAULT CHARSET={charset}");
204 if let Some(collate) = &options.collate {
205 suffix.push_str(&format!(" COLLATE={collate}"));
206 }
207 suffix
208 }
209
210 fn render_index_key_part(&self, part: &SnapshotIndexColumn) -> String {
212 if part.is_expression {
213 format!("({})", part.value)
214 } else {
215 self.dialect.quote(&part.value)
216 }
217 }
218
219 pub(super) fn create_declared_index_sql(&self, table: &str, index: &SnapshotIndex) -> String {
227 let q = |s: &str| self.dialect.quote(s);
228 let unique_kw = if index.is_unique { "UNIQUE " } else { "" };
229 let name = q(&index.name);
230
231 let (include_warning, include_suffix) = if index.include.is_empty() {
233 (String::new(), String::new())
234 } else {
235 match self.dialect {
236 MigrationDialect::Postgres => {
237 let cols: Vec<String> = index.include.iter().map(|c| q(c)).collect();
238 (String::new(), format!(" INCLUDE ({})", cols.join(", ")))
239 }
240 _ => (
241 "-- WARNING: index INCLUDE is only supported by PostgreSQL; ignored\n"
242 .to_string(),
243 String::new(),
244 ),
245 }
246 };
247
248 match (&index.filter, self.dialect) {
249 (Some(filter), MigrationDialect::MySql) if index.is_unique => {
250 let parts: Vec<String> = index
251 .columns
252 .iter()
253 .map(|part| {
254 let value = if part.is_expression {
255 part.value.clone()
256 } else {
257 q(&part.value)
258 };
259 format!("(CASE WHEN {filter} THEN {value} END)")
260 })
261 .collect();
262 format!(
263 "{include_warning}-- MySQL has no partial indexes; emulating filtered UNIQUE index with a functional index\nCREATE UNIQUE INDEX {name} ON {} ({});\n",
264 q(table),
265 parts.join(", ")
266 )
267 }
268 (Some(_), MigrationDialect::MySql) => format!(
269 "-- WARNING: MySQL does not support partial non-unique indexes; {name} on {} skipped\n",
270 q(table)
271 ),
272 (Some(filter), _) => {
273 let cols: Vec<String> = index
274 .columns
275 .iter()
276 .map(|part| self.render_index_key_part(part))
277 .collect();
278 format!(
279 "{include_warning}CREATE {unique_kw}INDEX {name} ON {} ({}){include_suffix} WHERE {filter};\n",
280 q(table),
281 cols.join(", ")
282 )
283 }
284 (None, _) => {
285 let cols: Vec<String> = index
286 .columns
287 .iter()
288 .map(|part| self.render_index_key_part(part))
289 .collect();
290 format!(
291 "{include_warning}CREATE {unique_kw}INDEX {name} ON {} ({}){include_suffix};\n",
292 q(table),
293 cols.join(", ")
294 )
295 }
296 }
297 }
298
299 fn composite_fk_clause(&self, fk: &SnapshotForeignKey) -> String {
300 let q = |s: &str| self.dialect.quote(s);
301 let cols: Vec<String> = fk.columns.iter().map(|c| q(c)).collect();
302 let ref_cols: Vec<String> = fk.referenced_columns.iter().map(|c| q(c)).collect();
303 let on_delete = fk
304 .on_delete
305 .as_deref()
306 .map(|c| format!(" ON DELETE {}", c))
307 .unwrap_or_default();
308 format!(
309 "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}){}",
310 q(&fk.name),
311 cols.join(", "),
312 q(&fk.referenced_table),
313 ref_cols.join(", "),
314 on_delete
315 )
316 }
317
318 fn create_composite_fk_sql(&self, table: &str, fk: &SnapshotForeignKey) -> String {
319 if self.dialect == MigrationDialect::Sqlite {
320 return format!(
321 "-- WARNING: SQLite cannot add composite foreign key {} to an existing table; rebuild {} to apply it\n",
322 self.dialect.quote(&fk.name),
323 self.dialect.quote(table)
324 );
325 }
326 format!(
327 "ALTER TABLE {} ADD {};\n",
328 self.dialect.quote(table),
329 self.composite_fk_clause(fk)
330 )
331 }
332
333 fn add_check_sql(&self, table: &str, check: &SnapshotCheck) -> String {
334 let q = |s: &str| self.dialect.quote(s);
335 match self.dialect {
336 MigrationDialect::Sqlite => format!(
337 "-- WARNING: SQLite cannot add CHECK constraint {} to an existing table; rebuild the table to apply it\n",
338 q(&check.name)
339 ),
340 _ => format!(
341 "ALTER TABLE {} ADD CONSTRAINT {} CHECK ({});\n",
342 q(table),
343 q(&check.name),
344 check.expression
345 ),
346 }
347 }
348
349 fn drop_check_sql(&self, table: &str, check: &SnapshotCheck) -> String {
350 let q = |s: &str| self.dialect.quote(s);
351 match self.dialect {
352 MigrationDialect::Sqlite => format!(
353 "-- WARNING: SQLite cannot drop CHECK constraint {}; rebuild the table to remove it\n",
354 q(&check.name)
355 ),
356 MigrationDialect::MySql => format!(
357 "ALTER TABLE {} DROP CHECK {};\n",
358 q(table),
359 q(&check.name)
360 ),
361 MigrationDialect::Postgres => format!(
362 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {};\n",
363 q(table),
364 q(&check.name)
365 ),
366 }
367 }
368
369 pub fn foreign_key_name(table: &str, column: &str, referenced_table: &str) -> String {
378 format!(
379 "fk_{}_{}_{}",
380 table.replace('.', "_"),
381 column,
382 referenced_table.replace('.', "_")
383 )
384 }
385
386 pub fn index_name(table: &str, column: &str) -> String {
388 format!("ix_{}_{}", table.replace('.', "_"), column)
389 }
390
391 fn column_type(&self, col: &SnapshotColumn) -> (String, bool) {
398 let col_type = self.dialect.map_column_type(col);
399 if self.dialect == MigrationDialect::MySql && (col.is_auto_increment || col.is_sequence) {
400 let stripped = col_type
401 .strip_suffix(" AUTO_INCREMENT")
402 .unwrap_or(&col_type)
403 .to_string();
404 (stripped, true)
405 } else {
406 (col_type, false)
407 }
408 }
409
410 pub fn generate_alter_column_sql(
411 &self,
412 table: &str,
413 column_name: &str,
414 new: &SnapshotColumn,
415 ) -> String {
416 let q = |s: &str| self.dialect.quote(s);
417 let col_type = self.dialect.map_column_type(new);
418 match self.dialect {
419 MigrationDialect::Postgres => {
420 if new.computed_sql.is_some() {
421 return format!(
422 "-- WARNING: PostgreSQL cannot alter a generated column expression; drop and re-add {}.{} manually\n",
423 q(table),
424 q(column_name)
425 );
426 }
427 let nullable = if new.is_required || new.is_primary_key {
428 "SET NOT NULL"
429 } else {
430 "DROP NOT NULL"
431 };
432 let default = match &new.default_value {
433 Some(v) if !v.trim().is_empty() => format!(
434 "ALTER TABLE {} ALTER COLUMN {} SET DEFAULT {};\n",
435 q(table),
436 q(column_name),
437 v
438 ),
439 _ => format!(
440 "ALTER TABLE {} ALTER COLUMN {} DROP DEFAULT;\n",
441 q(table),
442 q(column_name)
443 ),
444 };
445 let collation = match &new.collation {
446 Some(c) if !c.trim().is_empty() => format!(" COLLATE {}", c.trim()),
447 _ => String::new(),
448 };
449 let mut sql = format!(
450 "ALTER TABLE {} ALTER COLUMN {} TYPE {}{};\nALTER TABLE {} ALTER COLUMN {} {};\n{}",
451 q(table),
452 q(column_name),
453 col_type,
454 collation,
455 q(table),
456 q(column_name),
457 nullable,
458 default
459 );
460 if let Some(stmt) = self.column_comment_sql(table, new) {
461 sql.push_str(&stmt);
462 }
463 sql
464 }
465 MigrationDialect::MySql => format!(
466 "ALTER TABLE {} MODIFY COLUMN {};\n",
467 q(table),
468 self.render_column_definition(new)
469 ),
470 MigrationDialect::Sqlite => format!(
471 "-- SQLite does not support ALTER COLUMN type changes; \
472 rebuild table manually for {}.{}\n",
473 q(table),
474 q(column_name)
475 ),
476 }
477 }
478
479 pub(crate) fn generate_up_sql(&self, changes: &[SchemaChange]) -> String {
480 self.generate_up_sql_inner(changes, true)
481 }
482
483 pub(crate) fn generate_ddl_sql(&self, changes: &[SchemaChange]) -> String {
484 self.generate_up_sql_inner(changes, false)
485 }
486
487 fn generate_up_sql_inner(&self, changes: &[SchemaChange], record_history: bool) -> String {
488 let mut sql = String::from("-- Up Migration (auto-generated by rust-ef)\n\n");
489 let q = |s: &str| self.dialect.quote(s);
490 let create_kw = if record_history {
491 "CREATE TABLE"
492 } else {
493 "CREATE TABLE IF NOT EXISTS"
494 };
495
496 for change in changes {
497 match change {
498 SchemaChange::CreateTable {
499 table,
500 columns,
501 checks,
502 table_options,
503 foreign_keys,
504 comment,
505 } => {
506 if self.dialect == MigrationDialect::Postgres {
508 for c in columns {
509 if c.is_sequence {
510 if let Some(seq_name) = &c.sequence_name {
511 sql.push_str(&format!(
512 "CREATE SEQUENCE IF NOT EXISTS {};\n",
513 q(seq_name)
514 ));
515 }
516 }
517 }
518 }
519
520 sql.push_str(&format!("{} {} (\n", create_kw, q(table)));
521
522 let pk_columns: Vec<&str> = columns
524 .iter()
525 .filter(|c| c.is_primary_key)
526 .map(|c| c.column_name.as_str())
527 .collect();
528
529 let col_defs: Vec<String> = columns
530 .iter()
531 .map(|c| self.render_column_definition(c))
532 .collect();
533 sql.push_str(&format!(" {}\n", col_defs.join(",\n ")));
534
535 if pk_columns.len() == 1 {
537 sql.push_str(&format!(" ,PRIMARY KEY ({})\n", q(pk_columns[0])));
538 } else if pk_columns.len() > 1 {
539 let pk_list: Vec<String> = pk_columns.iter().map(|c| q(c)).collect();
540 sql.push_str(&format!(" ,PRIMARY KEY ({})\n", pk_list.join(", ")));
541 }
542
543 for check in checks {
545 sql.push_str(&format!(
546 " ,CONSTRAINT {} CHECK ({})\n",
547 q(&check.name),
548 check.expression
549 ));
550 }
551
552 if record_history && self.dialect == MigrationDialect::Sqlite {
560 for (col, ref_table, ref_col) in columns
561 .iter()
562 .filter_map(|c| super::diff::fk_target(c).map(|(t, r)| (c, t, r)))
563 {
564 let fk_name =
565 Self::foreign_key_name(table, &col.column_name, &ref_table);
566 let on_delete = col
567 .fk_on_delete
568 .as_deref()
569 .map(|c| format!(" ON DELETE {}", c))
570 .unwrap_or_default();
571 sql.push_str(&format!(
572 " ,CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}){}\n",
573 q(&fk_name),
574 q(&col.column_name),
575 q(&ref_table),
576 q(&ref_col),
577 on_delete
578 ));
579 }
580 for fk in foreign_keys {
581 sql.push_str(&format!(" ,{}\n", self.composite_fk_clause(fk)));
582 }
583 }
584
585 sql.push(')');
586 sql.push_str(&self.table_options_suffix(table_options));
587 sql.push_str(";\n\n");
588
589 if let Some(stmt) = self.table_comment_sql(table, comment.as_deref()) {
593 sql.push_str(&stmt);
594 }
595 for c in columns {
596 if let Some(stmt) = self.column_comment_sql(table, c) {
597 sql.push_str(&stmt);
598 }
599 }
600 }
601 SchemaChange::DropTable { table } => {
602 sql.push_str(&format!(
603 "-- WARNING: dropping table {} is irreversible\nDROP TABLE IF EXISTS {};\n",
604 q(table),
605 q(table)
606 ));
607 }
608 SchemaChange::AddColumn { table, column } => {
609 if column.is_required
610 && !column.is_primary_key
611 && column.default_value.is_none()
612 && column.computed_sql.is_none()
613 {
614 sql.push_str(&format!(
615 "-- WARNING: adding NOT NULL column {} without a default fails on non-empty tables; provide has_default(...) or backfill first\n",
616 q(&column.column_name)
617 ));
618 }
619 sql.push_str(&format!(
620 "ALTER TABLE {} ADD COLUMN {};\n",
621 q(table),
622 self.render_column_definition(column)
623 ));
624 if let Some(stmt) = self.column_comment_sql(table, column) {
625 sql.push_str(&stmt);
626 }
627 }
628 SchemaChange::DropColumn { table, column_name } => {
629 sql.push_str(&format!(
630 "-- WARNING: dropping column {}.{} is irreversible; column renames are not detected (data loss)\n",
631 q(table),
632 q(column_name)
633 ));
634 sql.push_str(&format!(
635 "ALTER TABLE {} DROP COLUMN IF EXISTS {};\n",
636 q(table),
637 q(column_name)
638 ));
639 }
640 SchemaChange::AlterColumn {
641 table,
642 column_name,
643 old: _,
644 new,
645 } => {
646 sql.push_str(&self.generate_alter_column_sql(table, column_name, new));
647 }
648 SchemaChange::AddForeignKey {
649 table,
650 column,
651 referenced_table,
652 referenced_column,
653 on_delete,
654 } => {
655 if self.dialect == MigrationDialect::Sqlite {
656 sql.push_str(&format!(
657 "-- WARNING: SQLite cannot add a foreign key to an existing table; rebuild {} to add ({}) -> {}({})\n",
658 q(table),
659 q(column),
660 q(referenced_table),
661 q(referenced_column)
662 ));
663 } else {
664 let fk_name = Self::foreign_key_name(table, column, referenced_table);
665 let on_delete_clause = on_delete
666 .as_deref()
667 .map(|c| format!(" ON DELETE {}", c))
668 .unwrap_or_default();
669 sql.push_str(&format!(
670 "ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}){};\n",
671 q(table),
672 q(&fk_name),
673 q(column),
674 q(referenced_table),
675 q(referenced_column),
676 on_delete_clause
677 ));
678 }
679 }
680 SchemaChange::DropForeignKey {
681 table,
682 column,
683 referenced_table,
684 } => {
685 let fk_name = Self::foreign_key_name(table, column, referenced_table);
686 match self.dialect {
687 MigrationDialect::Sqlite => sql.push_str(&format!(
688 "-- WARNING: SQLite cannot drop a foreign key from an existing table; rebuild {} to remove {}\n",
689 q(table),
690 q(&fk_name)
691 )),
692 MigrationDialect::MySql => sql.push_str(&format!(
693 "ALTER TABLE {} DROP FOREIGN KEY {};\n",
694 q(table),
695 q(&fk_name)
696 )),
697 _ => sql.push_str(&format!(
698 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {};\n",
699 q(table),
700 q(&fk_name)
701 )),
702 }
703 }
704 SchemaChange::CreateIndex {
705 table,
706 column,
707 is_unique,
708 } => {
709 let unique_kw = if *is_unique { "UNIQUE " } else { "" };
710 let idx_name = index_name(table, column);
711 sql.push_str(&format!(
712 "CREATE {unique_kw}INDEX {} ON {} ({});\n",
713 q(&idx_name),
714 q(table),
715 q(column)
716 ));
717 }
718 SchemaChange::DropIndex {
719 table,
720 column,
721 is_unique: _,
722 } => {
723 let idx_name = index_name(table, column);
724 match self.dialect {
725 MigrationDialect::MySql => {
726 sql.push_str(&format!("DROP INDEX {} ON {};\n", q(&idx_name), q(table)))
727 }
728 _ => sql.push_str(&format!("DROP INDEX IF EXISTS {};\n", q(&idx_name))),
729 }
730 }
731 SchemaChange::CreateDeclaredIndex { table, index } => {
732 sql.push_str(&self.create_declared_index_sql(table, index));
733 }
734 SchemaChange::DropDeclaredIndex { table, index } => match self.dialect {
735 MigrationDialect::MySql => {
736 sql.push_str(&format!("DROP INDEX {} ON {};\n", q(&index.name), q(table)))
737 }
738 _ => sql.push_str(&format!("DROP INDEX IF EXISTS {};\n", q(&index.name))),
739 },
740 SchemaChange::AddCompositeForeignKey { table, foreign_key } => {
741 sql.push_str(&self.create_composite_fk_sql(table, foreign_key));
742 }
743 SchemaChange::DropCompositeForeignKey { table, foreign_key } => {
744 match self.dialect {
745 MigrationDialect::Sqlite => sql.push_str(&format!(
746 "-- WARNING: SQLite cannot drop composite foreign key {} from an existing table; rebuild {} to remove it\n",
747 q(&foreign_key.name),
748 q(table)
749 )),
750 MigrationDialect::MySql => sql.push_str(&format!(
751 "ALTER TABLE {} DROP FOREIGN KEY {};\n",
752 q(table),
753 q(&foreign_key.name)
754 )),
755 _ => sql.push_str(&format!(
756 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {};\n",
757 q(table),
758 q(&foreign_key.name)
759 )),
760 }
761 }
762 SchemaChange::AddCheck { table, check } => {
763 sql.push_str(&self.add_check_sql(table, check));
764 }
765 SchemaChange::DropCheck { table, check } => {
766 sql.push_str(&self.drop_check_sql(table, check));
767 }
768 SchemaChange::AlterTableOptions { table, options } => {
769 if self.dialect == MigrationDialect::MySql {
770 let engine = options.engine.as_deref().unwrap_or("InnoDB");
771 let charset = options.charset.as_deref().unwrap_or("utf8mb4");
772 sql.push_str(&format!(
773 "ALTER TABLE {} ENGINE={} DEFAULT CHARSET={}",
774 q(table),
775 engine,
776 charset
777 ));
778 if let Some(collate) = &options.collate {
779 sql.push_str(&format!(" COLLATE={}", collate));
780 }
781 sql.push_str(";\n");
782 } else {
783 sql.push_str(&format!(
784 "-- Table options on {} are MySQL-only; ignored for this dialect\n",
785 q(table)
786 ));
787 }
788 }
789 SchemaChange::AlterTableComment {
790 table,
791 old_comment: _,
792 new_comment,
793 } => {
794 sql.push_str(&self.alter_table_comment_sql(table, new_comment.as_deref()));
795 }
796 }
797 }
798
799 if record_history {
800 sql.push_str(&format!(
801 "INSERT INTO {}(migration_id, product_version) VALUES ('{{migration_id}}', '{}');\n",
802 q(MIGRATION_HISTORY_TABLE),
803 PRODUCT_VERSION
804 ));
805 }
806
807 sql
808 }
809
810 pub(super) fn generate_down_sql(&self, changes: &[SchemaChange]) -> String {
811 let mut sql = String::from("-- Down Migration (auto-generated by rust-ef)\n\n");
812 let q = |s: &str| self.dialect.quote(s);
813
814 for change in changes.iter().rev() {
816 match change {
817 SchemaChange::CreateTable { table, .. } => {
818 sql.push_str(&format!("DROP TABLE IF EXISTS {};\n", q(table)));
819 }
820 SchemaChange::DropTable { table } => {
821 sql.push_str(&format!(
822 "-- WARNING: Cannot restore table {} (original schema unknown)\n",
823 q(table)
824 ));
825 }
826 SchemaChange::AddColumn { table, column } => {
827 sql.push_str(&format!(
828 "ALTER TABLE {} DROP COLUMN IF EXISTS {};\n",
829 q(table),
830 q(&column.column_name)
831 ));
832 }
833 SchemaChange::DropColumn { table, column_name } => {
834 sql.push_str(&format!(
835 "-- WARNING: Cannot restore column {} on {} (original type unknown)\n",
836 q(column_name),
837 q(table)
838 ));
839 }
840 SchemaChange::AlterColumn {
841 table,
842 column_name,
843 old,
844 new: _,
845 } => {
846 sql.push_str(&self.generate_alter_column_sql(table, column_name, old));
847 }
848 SchemaChange::AddForeignKey {
849 table,
850 column,
851 referenced_table,
852 ..
853 } => {
854 let fk_name = Self::foreign_key_name(table, column, referenced_table);
855 match self.dialect {
856 MigrationDialect::Sqlite => sql.push_str(&format!(
857 "-- WARNING: SQLite cannot drop a foreign key from an existing table; rebuild {} to remove {}\n",
858 q(table),
859 q(&fk_name)
860 )),
861 MigrationDialect::MySql => sql.push_str(&format!(
862 "ALTER TABLE {} DROP FOREIGN KEY {};\n",
863 q(table),
864 q(&fk_name)
865 )),
866 _ => sql.push_str(&format!(
867 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {};\n",
868 q(table),
869 q(&fk_name)
870 )),
871 }
872 }
873 SchemaChange::DropForeignKey {
874 table,
875 column,
876 referenced_table,
877 ..
878 } => {
879 sql.push_str(&format!(
880 "-- WARNING: Cannot restore foreign key constraint {} on {}.{}\n",
881 q(&Self::foreign_key_name(table, column, referenced_table)),
882 q(table),
883 q(column)
884 ));
885 }
886 SchemaChange::CreateIndex {
887 table,
888 column,
889 is_unique: _,
890 } => {
891 let idx_name = index_name(table, column);
892 match self.dialect {
893 MigrationDialect::MySql => {
894 sql.push_str(&format!("DROP INDEX {} ON {};\n", q(&idx_name), q(table)))
895 }
896 _ => sql.push_str(&format!("DROP INDEX IF EXISTS {};\n", q(&idx_name))),
897 }
898 }
899 SchemaChange::DropIndex {
900 table,
901 column,
902 is_unique,
903 } => {
904 let unique_kw = if *is_unique { "UNIQUE " } else { "" };
905 let idx_name = index_name(table, column);
906 sql.push_str(&format!(
907 "CREATE {unique_kw}INDEX {} ON {} ({});\n",
908 q(&idx_name),
909 q(table),
910 q(column)
911 ));
912 }
913 SchemaChange::CreateDeclaredIndex { table, index } => match self.dialect {
914 MigrationDialect::MySql => {
915 sql.push_str(&format!("DROP INDEX {} ON {};\n", q(&index.name), q(table)))
916 }
917 _ => sql.push_str(&format!("DROP INDEX IF EXISTS {};\n", q(&index.name))),
918 },
919 SchemaChange::DropDeclaredIndex { table, index } => {
920 sql.push_str(&self.create_declared_index_sql(table, index));
921 }
922 SchemaChange::AddCompositeForeignKey { table, foreign_key } => match self.dialect {
923 MigrationDialect::Sqlite => sql.push_str(&format!(
924 "-- WARNING: SQLite cannot drop composite foreign key {} from an existing table; rebuild {} to remove it\n",
925 q(&foreign_key.name),
926 q(table)
927 )),
928 MigrationDialect::MySql => sql.push_str(&format!(
929 "ALTER TABLE {} DROP FOREIGN KEY {};\n",
930 q(table),
931 q(&foreign_key.name)
932 )),
933 _ => sql.push_str(&format!(
934 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {};\n",
935 q(table),
936 q(&foreign_key.name)
937 )),
938 },
939 SchemaChange::DropCompositeForeignKey { table, foreign_key } => {
940 sql.push_str(&self.create_composite_fk_sql(table, foreign_key));
941 }
942 SchemaChange::AddCheck { table, check } => {
943 sql.push_str(&self.drop_check_sql(table, check));
944 }
945 SchemaChange::DropCheck { table, check } => {
946 sql.push_str(&self.add_check_sql(table, check));
947 }
948 SchemaChange::AlterTableOptions { .. } => {
949 sql.push_str(
950 "-- WARNING: Table option changes cannot be reverted automatically\n",
951 );
952 }
953 SchemaChange::AlterTableComment {
954 table,
955 old_comment,
956 new_comment: _,
957 } => {
958 sql.push_str(&self.alter_table_comment_sql(table, old_comment.as_deref()));
959 }
960 }
961 }
962
963 sql.push_str(&format!(
964 "DELETE FROM {} WHERE migration_id = '{{migration_id}}';\n",
965 q(MIGRATION_HISTORY_TABLE)
966 ));
967
968 sql
969 }
970}
971
972#[cfg(test)]
974mod tests {
975 use super::*;
976 use crate::migration::schema_change::SchemaChange;
977 use crate::migration::types::SnapshotColumn;
978
979 #[test]
980 fn mysql_auto_increment_follows_nullability() {
981 let engine = MigrationEngine::new(MigrationDialect::MySql);
982 let column = SnapshotColumn {
983 column_name: "id".into(),
984 type_name: "i32".into(),
985 is_primary_key: true,
986 is_required: true,
987 is_auto_increment: true,
988 ..Default::default()
989 };
990 let sql = engine.generate_ddl_sql(&[SchemaChange::CreateTable {
991 table: "items".into(),
992 columns: vec![column],
993 checks: Vec::new(),
994 table_options: Default::default(),
995 foreign_keys: Vec::new(),
996 comment: None,
997 }]);
998 assert!(
999 sql.contains("`id` INT NOT NULL AUTO_INCREMENT"),
1000 "unexpected DDL: {sql}"
1001 );
1002 assert!(
1003 !sql.contains("INT AUTO_INCREMENT NOT NULL"),
1004 "unexpected DDL: {sql}"
1005 );
1006 }
1007
1008 #[test]
1009 fn primary_key_columns_are_not_null_even_without_required() {
1010 let engine = MigrationEngine::new(MigrationDialect::Postgres);
1011 let column = SnapshotColumn {
1012 column_name: "id".into(),
1013 type_name: "i32".into(),
1014 is_primary_key: true,
1015 is_required: false,
1016 is_auto_increment: true,
1017 ..Default::default()
1018 };
1019 let sql = engine.generate_ddl_sql(&[SchemaChange::CreateTable {
1020 table: "items".into(),
1021 columns: vec![column],
1022 checks: Vec::new(),
1023 table_options: Default::default(),
1024 foreign_keys: Vec::new(),
1025 comment: None,
1026 }]);
1027 assert!(
1028 sql.contains("\"id\" SERIAL NOT NULL"),
1029 "unexpected DDL: {sql}"
1030 );
1031 }
1032}