1use crate::error::EFResult;
10use crate::metadata::{EntityTypeMeta, NavigationKind, NavigationMeta};
11use std::collections::{HashMap, HashSet};
12
13#[derive(Debug, Clone)]
15pub struct Migration {
16 pub id: String,
17 pub description: String,
18 pub up_sql: String,
19 pub down_sql: String,
20}
21
22#[derive(Debug, Clone)]
25pub struct ModelSnapshot {
26 pub migration_id: String,
27 pub entity_types: Vec<SnapshotEntityType>,
28}
29
30#[derive(Debug, Clone)]
32pub struct SnapshotEntityType {
33 pub type_name: String,
34 pub table_name: String,
35 pub columns: Vec<SnapshotColumn>,
36}
37
38#[derive(Debug, Clone, PartialEq, Default)]
40pub struct SnapshotColumn {
41 pub field_name: String,
42 pub column_name: String,
43 pub type_name: String,
44 pub is_primary_key: bool,
45 pub is_required: bool,
46 pub is_foreign_key: bool,
47 pub max_length: Option<usize>,
48 pub is_auto_increment: bool,
49 pub is_sequence: bool,
51 pub sequence_name: Option<String>,
53 pub fk_referenced_table: Option<String>,
55 pub fk_referenced_column: Option<String>,
57 pub has_index: bool,
59 pub is_unique: bool,
61 pub fk_on_delete: Option<String>,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum MigrationDialect {
69 Postgres,
70 MySql,
71 Sqlite,
72}
73
74impl MigrationDialect {
75 pub fn quote(&self, ident: &str) -> String {
77 match self {
78 MigrationDialect::Postgres | MigrationDialect::Sqlite => format!("\"{}\"", ident),
79 MigrationDialect::MySql => format!("`{}`", ident),
80 }
81 }
82
83 pub fn map_column_type(&self, col: &SnapshotColumn) -> String {
85 let tn = col.type_name.as_str();
89
90 if col.is_auto_increment {
92 if tn.ends_with("i32") {
93 return match self {
94 MigrationDialect::Postgres => "SERIAL".into(),
95 MigrationDialect::MySql => "INT AUTO_INCREMENT".into(),
96 MigrationDialect::Sqlite => "INTEGER".into(),
97 };
98 }
99 if tn.ends_with("i64") {
100 return match self {
101 MigrationDialect::Postgres => "BIGSERIAL".into(),
102 MigrationDialect::MySql => "BIGINT AUTO_INCREMENT".into(),
103 MigrationDialect::Sqlite => "INTEGER".into(),
104 };
105 }
106 }
107
108 if col.is_sequence {
111 if tn.ends_with("i32") {
112 return match self {
113 MigrationDialect::Postgres => "INTEGER".into(),
114 MigrationDialect::MySql => "INT AUTO_INCREMENT".into(),
115 MigrationDialect::Sqlite => "INTEGER".into(),
116 };
117 }
118 if tn.ends_with("i64") {
119 return match self {
120 MigrationDialect::Postgres => "BIGINT".into(),
121 MigrationDialect::MySql => "BIGINT AUTO_INCREMENT".into(),
122 MigrationDialect::Sqlite => "INTEGER".into(),
123 };
124 }
125 }
126
127 let base: &str = if tn.ends_with("i16") {
128 "SMALLINT"
129 } else if tn.ends_with("i32") {
130 "INTEGER"
131 } else if tn.ends_with("i64") {
132 "BIGINT"
133 } else if tn.ends_with("f32") {
134 "REAL"
135 } else if tn.ends_with("f64") {
136 "DOUBLE PRECISION"
137 } else if tn.ends_with("bool") {
138 "BOOLEAN"
139 } else if tn.ends_with("String") {
140 return match col.max_length {
141 Some(n) => format!("VARCHAR({})", n),
142 None => "TEXT".into(),
143 };
144 } else if tn.ends_with("Vec<u8>") {
145 return match self {
146 MigrationDialect::Postgres => "BYTEA".into(),
147 MigrationDialect::MySql | MigrationDialect::Sqlite => "BLOB".into(),
148 };
149 } else if tn.contains("NaiveDateTime") {
150 return match self {
151 MigrationDialect::Postgres => "TIMESTAMP".into(),
152 MigrationDialect::MySql => "DATETIME".into(),
153 MigrationDialect::Sqlite => "TEXT".into(),
154 };
155 } else if tn.contains("NaiveDate") {
156 return match self {
157 MigrationDialect::Postgres => "DATE".into(),
158 MigrationDialect::MySql => "DATE".into(),
159 MigrationDialect::Sqlite => "TEXT".into(),
160 };
161 } else if tn.contains("DateTime") {
162 return match self {
164 MigrationDialect::Postgres => "TIMESTAMPTZ".into(),
165 MigrationDialect::MySql => "DATETIME".into(),
166 MigrationDialect::Sqlite => "TEXT".into(),
167 };
168 } else if tn.contains("Uuid") {
169 return match self {
170 MigrationDialect::Postgres => "UUID".into(),
171 MigrationDialect::MySql => "CHAR(36)".into(),
172 MigrationDialect::Sqlite => "TEXT".into(),
173 };
174 } else if tn.contains("Decimal") {
175 return match self {
176 MigrationDialect::Postgres => "NUMERIC".into(),
177 MigrationDialect::MySql => "DECIMAL(38,18)".into(),
178 MigrationDialect::Sqlite => "TEXT".into(),
179 };
180 } else {
181 "TEXT"
182 };
183 base.to_string()
184 }
185}
186
187#[derive(Debug, Clone)]
189pub(crate) enum SchemaChange {
190 CreateTable {
191 table: String,
192 columns: Vec<SnapshotColumn>,
193 },
194 DropTable {
195 table: String,
196 },
197 AddColumn {
198 table: String,
199 column: SnapshotColumn,
200 },
201 DropColumn {
202 table: String,
203 column_name: String,
204 },
205 AlterColumn {
206 table: String,
207 column_name: String,
208 old: SnapshotColumn,
209 new: SnapshotColumn,
210 },
211 AddForeignKey {
212 table: String,
213 column: String,
214 referenced_table: String,
215 referenced_column: String,
216 on_delete: Option<String>,
217 },
218 DropForeignKey {
219 table: String,
220 column: String,
221 referenced_table: String,
222 },
223 CreateIndex {
224 table: String,
225 column: String,
226 is_unique: bool,
227 },
228 DropIndex {
229 table: String,
230 column: String,
231 is_unique: bool,
232 },
233}
234
235pub struct MigrationEngine {
238 dialect: MigrationDialect,
239}
240
241impl MigrationEngine {
242 pub fn new(dialect: MigrationDialect) -> Self {
243 Self { dialect }
244 }
245
246 pub fn generate(
248 &self,
249 name: &str,
250 current: &[EntityTypeMeta],
251 previous_snapshot: &Option<ModelSnapshot>,
252 ) -> EFResult<Migration> {
253 let current_snapshot = self.create_snapshot("__current__", current);
254
255 let changes = match previous_snapshot {
256 Some(prev) => self.diff(prev, ¤t_snapshot),
257 None => self.initial_create_with_fks(¤t_snapshot),
258 };
259
260 let up_sql = self.generate_up_sql(&changes);
261 let down_sql = self.generate_down_sql(&changes);
262
263 Ok(Migration {
264 id: name.to_string(),
265 description: name.to_string(),
266 up_sql,
267 down_sql,
268 })
269 }
270
271 pub fn create_snapshot(
273 &self,
274 migration_id: &str,
275 entity_types: &[EntityTypeMeta],
276 ) -> ModelSnapshot {
277 let types = entity_types
278 .iter()
279 .map(|et| SnapshotEntityType {
280 type_name: et.type_name.to_string(),
281 table_name: et.table_name.to_string(),
282 columns: et
283 .properties
284 .iter()
285 .filter(|p| !p.is_not_mapped)
286 .map(|p| {
287 let (fk_table, fk_col, fk_on_delete) =
288 fk_reference_for_property(et, p.column_name.as_ref());
289 SnapshotColumn {
290 field_name: p.field_name.to_string(),
291 column_name: p.column_name.to_string(),
292 type_name: p.type_name.to_string(),
293 is_primary_key: p.is_primary_key,
294 is_required: p.is_required,
295 is_foreign_key: p.is_foreign_key,
296 max_length: p.max_length,
297 is_auto_increment: p.is_auto_increment,
298 is_sequence: p.is_sequence,
299 sequence_name: p.sequence_name.as_ref().map(|s| s.to_string()),
300 fk_referenced_table: fk_table,
301 fk_referenced_column: fk_col,
302 has_index: p.has_index,
303 is_unique: p.is_unique,
304 fk_on_delete,
305 }
306 })
307 .collect(),
308 })
309 .collect();
310
311 ModelSnapshot {
312 migration_id: migration_id.to_string(),
313 entity_types: types,
314 }
315 }
316
317 fn initial_create(&self, current: &ModelSnapshot) -> Vec<SchemaChange> {
322 current
323 .entity_types
324 .iter()
325 .map(|et| SchemaChange::CreateTable {
326 table: et.table_name.clone(),
327 columns: et.columns.clone(),
328 })
329 .collect()
330 }
331
332 fn diff(&self, old: &ModelSnapshot, new: &ModelSnapshot) -> Vec<SchemaChange> {
333 let mut changes = Vec::new();
334
335 let old_tables: HashMap<&str, &SnapshotEntityType> = old
337 .entity_types
338 .iter()
339 .map(|e| (e.table_name.as_str(), e))
340 .collect();
341 let new_tables: HashMap<&str, &SnapshotEntityType> = new
342 .entity_types
343 .iter()
344 .map(|e| (e.table_name.as_str(), e))
345 .collect();
346
347 let old_names: HashSet<&str> = old_tables.keys().copied().collect();
348 let new_names: HashSet<&str> = new_tables.keys().copied().collect();
349
350 for name in old_names.difference(&new_names) {
352 changes.push(SchemaChange::DropTable {
353 table: name.to_string(),
354 });
355 }
356
357 for name in new_names.difference(&old_names) {
359 let et = new_tables[name];
360 changes.push(SchemaChange::CreateTable {
361 table: et.table_name.clone(),
362 columns: et.columns.clone(),
363 });
364 Self::append_create_table_fks(&mut changes, &et.table_name, &et.columns);
365 Self::append_create_table_indexes(&mut changes, &et.table_name, &et.columns);
366 }
367
368 for name in old_names.intersection(&new_names) {
370 let old_et = old_tables[name];
371 let new_et = new_tables[name];
372 let table = &old_et.table_name;
373
374 let old_cols: HashMap<&str, &SnapshotColumn> = old_et
375 .columns
376 .iter()
377 .map(|c| (c.column_name.as_str(), c))
378 .collect();
379 let new_cols: HashMap<&str, &SnapshotColumn> = new_et
380 .columns
381 .iter()
382 .map(|c| (c.column_name.as_str(), c))
383 .collect();
384
385 let old_col_names: HashSet<&str> = old_cols.keys().copied().collect();
386 let new_col_names: HashSet<&str> = new_cols.keys().copied().collect();
387
388 for col_name in new_col_names.difference(&old_col_names) {
390 let col = new_cols[col_name];
391 changes.push(SchemaChange::AddColumn {
392 table: table.clone(),
393 column: (*col).clone(),
394 });
395 changes.extend(diff_indexes(table, &SnapshotColumn::default(), col));
396 }
397
398 changes.extend(diff_foreign_keys(table, old_et, new_et));
400
401 for col_name in old_col_names.difference(&new_col_names) {
403 changes.push(SchemaChange::DropColumn {
404 table: table.clone(),
405 column_name: col_name.to_string(),
406 });
407 }
408
409 for col_name in old_col_names.intersection(&new_col_names) {
411 let old_col = old_cols[col_name];
412 let new_col = new_cols[col_name];
413
414 changes.extend(diff_indexes(table, old_col, new_col));
416
417 if !columns_structurally_equal(old_col, new_col) {
419 changes.push(SchemaChange::AlterColumn {
420 table: table.clone(),
421 column_name: col_name.to_string(),
422 old: (*old_col).clone(),
423 new: (*new_col).clone(),
424 });
425 }
426 }
427 }
428
429 changes
430 }
431
432 fn append_create_table_fks(
433 changes: &mut Vec<SchemaChange>,
434 table: &str,
435 columns: &[SnapshotColumn],
436 ) {
437 for col in columns {
438 if let Some((ref_table, ref_col)) = fk_target(col) {
439 changes.push(SchemaChange::AddForeignKey {
440 table: table.to_string(),
441 column: col.column_name.clone(),
442 referenced_table: ref_table,
443 referenced_column: ref_col,
444 on_delete: col.fk_on_delete.clone(),
445 });
446 }
447 }
448 }
449
450 fn append_create_table_indexes(
452 changes: &mut Vec<SchemaChange>,
453 table: &str,
454 columns: &[SnapshotColumn],
455 ) {
456 for col in columns {
457 if col.is_unique {
458 changes.push(SchemaChange::CreateIndex {
459 table: table.to_string(),
460 column: col.column_name.clone(),
461 is_unique: true,
462 });
463 } else if col.has_index {
464 changes.push(SchemaChange::CreateIndex {
465 table: table.to_string(),
466 column: col.column_name.clone(),
467 is_unique: false,
468 });
469 }
470 }
471 }
472}
473
474fn fk_target(col: &SnapshotColumn) -> Option<(String, String)> {
475 if !col.is_foreign_key {
476 return None;
477 }
478 Some((
479 col.fk_referenced_table.clone()?,
480 col.fk_referenced_column.clone()?,
481 ))
482}
483
484fn index_name(table: &str, column: &str) -> String {
486 format!("ix_{}_{}", table, column)
487}
488
489fn fk_reference_for_property(
490 meta: &EntityTypeMeta,
491 column_name: &str,
492) -> (Option<String>, Option<String>, Option<String>) {
493 for nav in &meta.navigations {
494 if nav.kind != NavigationKind::BelongsTo {
495 continue;
496 }
497 let matches = nav
498 .fk_column
499 .as_ref()
500 .is_some_and(|fk| fk.as_ref() == column_name);
501 if matches {
502 let on_delete = resolve_fk_on_delete_clause(nav, meta);
503 return (
504 nav.related_table.as_ref().map(|s| s.to_string()),
505 nav.referenced_key_column.as_ref().map(|s| s.to_string()),
506 on_delete,
507 );
508 }
509 }
510 (None, None, None)
511}
512
513fn resolve_fk_on_delete_clause(
518 belongs_to_nav: &NavigationMeta,
519 child_meta: &EntityTypeMeta,
520) -> Option<String> {
521 use crate::relations::DeleteBehavior;
522
523 if let Some(b) = belongs_to_nav.delete_behavior {
525 return Some(b.to_sql_clause().to_string());
526 }
527
528 if let Some(meta_fn) = belongs_to_nav.related_entity_meta {
530 let principal_meta = meta_fn();
531 for principal_nav in &principal_meta.navigations {
532 if principal_nav.kind == NavigationKind::HasMany
533 && principal_nav.related_type_id == child_meta.type_id
534 {
535 let behavior = crate::db_context::resolve_delete_behavior(principal_nav);
536 return Some(behavior.to_sql_clause().to_string());
537 }
538 }
539 }
540
541 if let Some(fk_prop) = child_meta.properties.iter().find(|p| p.is_foreign_key) {
543 let is_nullable = fk_prop.type_name.contains("Option");
544 let behavior = if is_nullable {
545 DeleteBehavior::Restrict
546 } else {
547 DeleteBehavior::Cascade
548 };
549 return Some(behavior.to_sql_clause().to_string());
550 }
551
552 None
553}
554
555fn diff_foreign_keys(
556 table: &str,
557 old_et: &SnapshotEntityType,
558 new_et: &SnapshotEntityType,
559) -> Vec<SchemaChange> {
560 let mut changes = Vec::new();
561 let old_fks: HashMap<&str, (&SnapshotColumn, String, String, Option<String>)> = old_et
562 .columns
563 .iter()
564 .filter_map(|c| {
565 let (rt, rc) = fk_target(c)?;
566 Some((
567 c.column_name.as_str(),
568 (c, rt, rc, c.fk_on_delete.clone()),
569 ))
570 })
571 .collect();
572 let new_fks: HashMap<&str, (&SnapshotColumn, String, String, Option<String>)> = new_et
573 .columns
574 .iter()
575 .filter_map(|c| {
576 let (rt, rc) = fk_target(c)?;
577 Some((
578 c.column_name.as_str(),
579 (c, rt, rc, c.fk_on_delete.clone()),
580 ))
581 })
582 .collect();
583
584 let old_names: HashSet<&str> = old_fks.keys().copied().collect();
585 let new_names: HashSet<&str> = new_fks.keys().copied().collect();
586
587 for col in new_names.difference(&old_names) {
588 let (_, rt, rc, od) = &new_fks[col];
589 changes.push(SchemaChange::AddForeignKey {
590 table: table.to_string(),
591 column: (*col).to_string(),
592 referenced_table: rt.clone(),
593 referenced_column: rc.clone(),
594 on_delete: od.clone(),
595 });
596 }
597
598 for col in old_names.difference(&new_names) {
599 let (_, rt, _, _) = &old_fks[col];
600 changes.push(SchemaChange::DropForeignKey {
601 table: table.to_string(),
602 column: (*col).to_string(),
603 referenced_table: rt.clone(),
604 });
605 }
606
607 for col in old_names.intersection(&new_names) {
608 let (_, old_rt, old_rc, old_od) = &old_fks[col];
609 let (_, new_rt, new_rc, new_od) = &new_fks[col];
610 if old_rt != new_rt || old_rc != new_rc || old_od != new_od {
611 changes.push(SchemaChange::DropForeignKey {
612 table: table.to_string(),
613 column: (*col).to_string(),
614 referenced_table: old_rt.clone(),
615 });
616 changes.push(SchemaChange::AddForeignKey {
617 table: table.to_string(),
618 column: (*col).to_string(),
619 referenced_table: new_rt.clone(),
620 referenced_column: new_rc.clone(),
621 on_delete: new_od.clone(),
622 });
623 }
624 }
625
626 changes
627}
628
629fn columns_structurally_equal(a: &SnapshotColumn, b: &SnapshotColumn) -> bool {
634 a.field_name == b.field_name
635 && a.column_name == b.column_name
636 && a.type_name == b.type_name
637 && a.is_primary_key == b.is_primary_key
638 && a.is_required == b.is_required
639 && a.is_foreign_key == b.is_foreign_key
640 && a.max_length == b.max_length
641 && a.is_auto_increment == b.is_auto_increment
642 && a.fk_referenced_table == b.fk_referenced_table
643 && a.fk_referenced_column == b.fk_referenced_column
644}
645
646fn diff_indexes(table: &str, old: &SnapshotColumn, new: &SnapshotColumn) -> Vec<SchemaChange> {
649 let old_kind = index_kind(old);
650 let new_kind = index_kind(new);
651 if old_kind == new_kind {
652 return Vec::new();
653 }
654 let mut changes = Vec::new();
655 if old_kind != IndexKind::None {
656 changes.push(SchemaChange::DropIndex {
657 table: table.to_string(),
658 column: new.column_name.clone(),
659 is_unique: old_kind == IndexKind::Unique,
660 });
661 }
662 if new_kind != IndexKind::None {
663 changes.push(SchemaChange::CreateIndex {
664 table: table.to_string(),
665 column: new.column_name.clone(),
666 is_unique: new_kind == IndexKind::Unique,
667 });
668 }
669 changes
670}
671
672#[derive(Clone, Copy, PartialEq, Eq)]
673enum IndexKind {
674 None,
675 NonUnique,
676 Unique,
677}
678
679fn index_kind(col: &SnapshotColumn) -> IndexKind {
680 if col.is_unique {
681 IndexKind::Unique
682 } else if col.has_index {
683 IndexKind::NonUnique
684 } else {
685 IndexKind::None
686 }
687}
688
689impl MigrationEngine {
690 fn initial_create_with_fks(&self, current: &ModelSnapshot) -> Vec<SchemaChange> {
691 let mut changes = self.initial_create(current);
692 for et in ¤t.entity_types {
693 Self::append_create_table_fks(&mut changes, &et.table_name, &et.columns);
694 Self::append_create_table_indexes(&mut changes, &et.table_name, &et.columns);
695 }
696 changes
697 }
698
699 pub fn foreign_key_name(table: &str, column: &str, referenced_table: &str) -> String {
705 format!("fk_{}_{}_{}", table, column, referenced_table)
706 }
707
708 pub fn index_name(table: &str, column: &str) -> String {
710 format!("ix_{}_{}", table, column)
711 }
712
713 pub fn generate_alter_column_sql(
714 &self,
715 table: &str,
716 column_name: &str,
717 new: &SnapshotColumn,
718 ) -> String {
719 let q = |s: &str| self.dialect.quote(s);
720 let col_type = self.dialect.map_column_type(new);
721 match self.dialect {
722 MigrationDialect::Postgres => {
723 let nullable = if new.is_required {
724 "SET NOT NULL"
725 } else {
726 "DROP NOT NULL"
727 };
728 format!(
729 "ALTER TABLE {} ALTER COLUMN {} TYPE {};\nALTER TABLE {} ALTER COLUMN {} {};\n",
730 q(table),
731 q(column_name),
732 col_type,
733 q(table),
734 q(column_name),
735 nullable
736 )
737 }
738 MigrationDialect::MySql => {
739 let nullable = if new.is_required { "NOT NULL" } else { "NULL" };
740 format!(
741 "ALTER TABLE {} MODIFY COLUMN {} {} {};\n",
742 q(table),
743 q(column_name),
744 col_type,
745 nullable
746 )
747 }
748 MigrationDialect::Sqlite => format!(
749 "-- SQLite does not support ALTER COLUMN type changes; \
750 rebuild table manually for {}.{}\n",
751 q(table),
752 q(column_name)
753 ),
754 }
755 }
756
757 fn generate_up_sql(&self, changes: &[SchemaChange]) -> String {
758 self.generate_up_sql_inner(changes, true)
759 }
760
761 fn generate_ddl_sql(&self, changes: &[SchemaChange]) -> String {
762 self.generate_up_sql_inner(changes, false)
763 }
764
765 fn generate_up_sql_inner(&self, changes: &[SchemaChange], record_history: bool) -> String {
766 let mut sql = String::from("-- Up Migration (auto-generated by rust-ef)\n\n");
767 let q = |s: &str| self.dialect.quote(s);
768 let create_kw = if record_history {
769 "CREATE TABLE"
770 } else {
771 "CREATE TABLE IF NOT EXISTS"
772 };
773
774 for change in changes {
775 match change {
776 SchemaChange::CreateTable { table, columns } => {
777 if self.dialect == MigrationDialect::Postgres {
779 for c in columns {
780 if c.is_sequence {
781 if let Some(seq_name) = &c.sequence_name {
782 sql.push_str(&format!(
783 "CREATE SEQUENCE IF NOT EXISTS {};\n",
784 q(seq_name)
785 ));
786 }
787 }
788 }
789 }
790
791 sql.push_str(&format!("{} {} (\n", create_kw, q(table)));
792
793 let pk_columns: Vec<&str> = columns
795 .iter()
796 .filter(|c| c.is_primary_key)
797 .map(|c| c.column_name.as_str())
798 .collect();
799
800 let col_defs: Vec<String> = columns
801 .iter()
802 .map(|c| {
803 let nullable = if c.is_required { "NOT NULL" } else { "NULL" };
804 let col_type = self.dialect.map_column_type(c);
805 if c.is_sequence && self.dialect == MigrationDialect::Postgres {
807 if let Some(seq_name) = &c.sequence_name {
808 return format!(
809 "{} {} DEFAULT nextval('{}') {}",
810 q(&c.column_name),
811 col_type,
812 seq_name,
813 nullable
814 );
815 }
816 }
817 [q(&c.column_name), col_type, nullable.to_string()]
819 .into_iter()
820 .filter(|s| !s.is_empty())
821 .collect::<Vec<_>>()
822 .join(" ")
823 })
824 .collect();
825 sql.push_str(&format!(" {}\n", col_defs.join(",\n ")));
826
827 if pk_columns.len() == 1 {
829 sql.push_str(&format!(" ,PRIMARY KEY ({})\n", q(pk_columns[0])));
830 } else if pk_columns.len() > 1 {
831 let pk_list: Vec<String> = pk_columns.iter().map(|c| q(c)).collect();
832 sql.push_str(&format!(" ,PRIMARY KEY ({})\n", pk_list.join(", ")));
833 }
834
835 sql.push_str(");\n\n");
836 }
837 SchemaChange::DropTable { table } => {
838 sql.push_str(&format!("DROP TABLE IF EXISTS {};\n", q(table)));
839 }
840 SchemaChange::AddColumn { table, column } => {
841 let col_type = self.dialect.map_column_type(column);
842 let nullable = if column.is_required {
843 "NOT NULL"
844 } else {
845 "NULL"
846 };
847 sql.push_str(&format!(
848 "ALTER TABLE {} ADD COLUMN {} {} {};\n",
849 q(table),
850 q(&column.column_name),
851 col_type,
852 nullable
853 ));
854 }
855 SchemaChange::DropColumn { table, column_name } => {
856 sql.push_str(&format!(
857 "ALTER TABLE {} DROP COLUMN IF EXISTS {};\n",
858 q(table),
859 q(column_name)
860 ));
861 }
862 SchemaChange::AlterColumn {
863 table,
864 column_name,
865 old: _,
866 new,
867 } => {
868 sql.push_str(&self.generate_alter_column_sql(table, column_name, new));
869 }
870 SchemaChange::AddForeignKey {
871 table,
872 column,
873 referenced_table,
874 referenced_column,
875 on_delete,
876 } => {
877 let fk_name = Self::foreign_key_name(table, column, referenced_table);
878 let on_delete_clause = on_delete
879 .as_deref()
880 .map(|c| format!(" ON DELETE {}", c))
881 .unwrap_or_default();
882 sql.push_str(&format!(
883 "ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}){};\n",
884 q(table),
885 q(&fk_name),
886 q(column),
887 q(referenced_table),
888 q(referenced_column),
889 on_delete_clause
890 ));
891 }
892 SchemaChange::DropForeignKey {
893 table,
894 column,
895 referenced_table,
896 } => {
897 let fk_name = Self::foreign_key_name(table, column, referenced_table);
898 match self.dialect {
899 MigrationDialect::MySql => sql.push_str(&format!(
900 "ALTER TABLE {} DROP FOREIGN KEY {};\n",
901 q(table),
902 q(&fk_name)
903 )),
904 _ => sql.push_str(&format!(
905 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {};\n",
906 q(table),
907 q(&fk_name)
908 )),
909 }
910 }
911 SchemaChange::CreateIndex {
912 table,
913 column,
914 is_unique,
915 } => {
916 let unique_kw = if *is_unique { "UNIQUE " } else { "" };
917 let idx_name = index_name(table, column);
918 sql.push_str(&format!(
919 "CREATE {unique_kw}INDEX {} ON {} ({});\n",
920 q(&idx_name),
921 q(table),
922 q(column)
923 ));
924 }
925 SchemaChange::DropIndex {
926 table,
927 column,
928 is_unique: _,
929 } => {
930 let idx_name = index_name(table, column);
931 match self.dialect {
932 MigrationDialect::MySql => {
933 sql.push_str(&format!("DROP INDEX {} ON {};\n", q(&idx_name), q(table)))
934 }
935 _ => sql.push_str(&format!("DROP INDEX IF EXISTS {};\n", q(&idx_name))),
936 }
937 }
938 }
939 }
940
941 if record_history {
942 sql.push_str(&format!(
943 "INSERT INTO {}(migration_id, product_version) VALUES ('{{migration_id}}', '{}');\n",
944 q(MIGRATION_HISTORY_TABLE),
945 PRODUCT_VERSION
946 ));
947 }
948
949 sql
950 }
951
952 fn generate_down_sql(&self, changes: &[SchemaChange]) -> String {
953 let mut sql = String::from("-- Down Migration (auto-generated by rust-ef)\n\n");
954 let q = |s: &str| self.dialect.quote(s);
955
956 for change in changes.iter().rev() {
958 match change {
959 SchemaChange::CreateTable { table, .. } => {
960 sql.push_str(&format!("DROP TABLE IF EXISTS {};\n", q(table)));
961 }
962 SchemaChange::DropTable { table } => {
963 sql.push_str(&format!(
965 "-- WARNING: Cannot restore table {} (original schema unknown)\n",
966 q(table)
967 ));
968 }
969 SchemaChange::AddColumn { table, column } => {
970 sql.push_str(&format!(
971 "ALTER TABLE {} DROP COLUMN IF EXISTS {};\n",
972 q(table),
973 q(&column.column_name)
974 ));
975 }
976 SchemaChange::DropColumn { table, column_name } => {
977 sql.push_str(&format!(
979 "-- WARNING: Cannot restore column {} on {} (original type unknown)\n",
980 q(column_name),
981 q(table)
982 ));
983 }
984 SchemaChange::AlterColumn {
985 table,
986 column_name,
987 old,
988 new: _,
989 } => {
990 sql.push_str(&self.generate_alter_column_sql(table, column_name, old));
991 }
992 SchemaChange::AddForeignKey {
993 table,
994 column,
995 referenced_table,
996 ..
997 } => {
998 let fk_name = Self::foreign_key_name(table, column, referenced_table);
999 match self.dialect {
1000 MigrationDialect::MySql => sql.push_str(&format!(
1001 "ALTER TABLE {} DROP FOREIGN KEY {};\n",
1002 q(table),
1003 q(&fk_name)
1004 )),
1005 _ => sql.push_str(&format!(
1006 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {};\n",
1007 q(table),
1008 q(&fk_name)
1009 )),
1010 }
1011 }
1012 SchemaChange::DropForeignKey {
1013 table,
1014 column,
1015 referenced_table,
1016 ..
1017 } => {
1018 sql.push_str(&format!(
1019 "-- WARNING: Cannot restore foreign key constraint {} on {}.{}\n",
1020 q(&Self::foreign_key_name(table, column, referenced_table)),
1021 q(table),
1022 q(column)
1023 ));
1024 }
1025 SchemaChange::CreateIndex {
1026 table,
1027 column,
1028 is_unique: _,
1029 } => {
1030 let idx_name = index_name(table, column);
1031 match self.dialect {
1032 MigrationDialect::MySql => {
1033 sql.push_str(&format!("DROP INDEX {} ON {};\n", q(&idx_name), q(table)))
1034 }
1035 _ => sql.push_str(&format!("DROP INDEX IF EXISTS {};\n", q(&idx_name))),
1036 }
1037 }
1038 SchemaChange::DropIndex {
1039 table,
1040 column,
1041 is_unique,
1042 } => {
1043 let unique_kw = if *is_unique { "UNIQUE " } else { "" };
1044 let idx_name = index_name(table, column);
1045 sql.push_str(&format!(
1046 "CREATE {unique_kw}INDEX {} ON {} ({});\n",
1047 q(&idx_name),
1048 q(table),
1049 q(column)
1050 ));
1051 }
1052 }
1053 }
1054
1055 sql.push_str(&format!(
1056 "DELETE FROM {} WHERE migration_id = '{{migration_id}}';\n",
1057 q(MIGRATION_HISTORY_TABLE)
1058 ));
1059
1060 sql
1061 }
1062
1063 pub async fn ensure_history_table(
1065 &self,
1066 provider: &dyn crate::provider::IDatabaseProvider,
1067 ) -> EFResult<()> {
1068 let sql = create_migration_history_table_sql(self.dialect);
1069 provider.execute_migration_command(&sql).await
1070 }
1071
1072 pub async fn apply(
1074 &self,
1075 provider: &dyn crate::provider::IDatabaseProvider,
1076 migration: &Migration,
1077 ) -> EFResult<()> {
1078 if self.is_applied(provider, &migration.id).await? {
1079 return Ok(());
1080 }
1081 self.ensure_history_table(provider).await?;
1082 let sql = migration.up_sql.replace("{migration_id}", &migration.id);
1083 for statement in split_sql_statements(&sql) {
1084 if !statement.is_empty() {
1085 provider.execute_migration_command(&statement).await?;
1086 }
1087 }
1088 Ok(())
1089 }
1090
1091 pub async fn revert(
1093 &self,
1094 provider: &dyn crate::provider::IDatabaseProvider,
1095 migration: &Migration,
1096 ) -> EFResult<()> {
1097 if !self.is_applied(provider, &migration.id).await? {
1098 return Err(crate::error::EFError::migration(format!(
1099 "migration '{}' is not applied",
1100 migration.id
1101 )));
1102 }
1103 let sql = migration.down_sql.replace("{migration_id}", &migration.id);
1104 for statement in split_sql_statements(&sql) {
1105 if !statement.is_empty() {
1106 provider.execute_migration_command(&statement).await?;
1107 }
1108 }
1109 Ok(())
1110 }
1111
1112 pub async fn get_applied_migrations(
1114 &self,
1115 provider: &dyn crate::provider::IDatabaseProvider,
1116 ) -> EFResult<Vec<MigrationHistoryEntry>> {
1117 self.ensure_history_table(provider).await?;
1118 let q = |s: &str| self.dialect.quote(s);
1119 let table = q(MIGRATION_HISTORY_TABLE);
1120 let sql = format!(
1121 "SELECT {}, {} FROM {} ORDER BY {}",
1122 q("migration_id"),
1123 q("product_version"),
1124 table,
1125 q("migration_id")
1126 );
1127 let mut conn = provider.get_connection().await?;
1128 let rows = conn.query(&sql, &[]).await?;
1129 Ok(rows
1130 .into_iter()
1131 .map(|row| MigrationHistoryEntry {
1132 migration_id: row
1133 .first()
1134 .and_then(|v| String::try_from(v.clone()).ok())
1135 .unwrap_or_default(),
1136 product_version: row
1137 .get(1)
1138 .and_then(|v| String::try_from(v.clone()).ok())
1139 .unwrap_or_default(),
1140 })
1141 .collect())
1142 }
1143
1144 pub async fn is_applied(
1146 &self,
1147 provider: &dyn crate::provider::IDatabaseProvider,
1148 migration_id: &str,
1149 ) -> EFResult<bool> {
1150 let applied = self.get_applied_migrations(provider).await?;
1151 Ok(applied.iter().any(|e| e.migration_id == migration_id))
1152 }
1153
1154 pub async fn apply_pending(
1156 &self,
1157 provider: &dyn crate::provider::IDatabaseProvider,
1158 migrations: &[Migration],
1159 ) -> EFResult<usize> {
1160 let applied: std::collections::HashSet<String> = self
1161 .get_applied_migrations(provider)
1162 .await?
1163 .into_iter()
1164 .map(|e| e.migration_id)
1165 .collect();
1166 let mut count = 0usize;
1167 for migration in migrations {
1168 if applied.contains(&migration.id) {
1169 continue;
1170 }
1171 self.apply(provider, migration).await?;
1172 count += 1;
1173 }
1174 Ok(count)
1175 }
1176
1177 pub async fn revert_last(
1179 &self,
1180 provider: &dyn crate::provider::IDatabaseProvider,
1181 migrations: &[Migration],
1182 ) -> EFResult<Option<String>> {
1183 let applied = self.get_applied_migrations(provider).await?;
1184 let last = applied.last().map(|e| e.migration_id.clone());
1185 let Some(last_id) = last else {
1186 return Ok(None);
1187 };
1188 let migration = migrations.iter().find(|m| m.id == last_id).ok_or_else(|| {
1189 crate::error::EFError::migration(format!(
1190 "applied migration '{}' not found in local migration set",
1191 last_id
1192 ))
1193 })?;
1194 self.revert(provider, migration).await?;
1195 Ok(Some(last_id))
1196 }
1197
1198 pub async fn revert_to_target(
1205 &self,
1206 provider: &dyn crate::provider::IDatabaseProvider,
1207 migrations: &[Migration],
1208 target: Option<&str>,
1209 ) -> EFResult<Vec<String>> {
1210 let applied = self.get_applied_migrations(provider).await?;
1211 let applied_ids: Vec<&str> = applied.iter().map(|e| e.migration_id.as_str()).collect();
1212
1213 let to_revert: Vec<String> = match target {
1214 Some(t) => {
1215 let idx = applied_ids.iter().position(|id| *id == t).ok_or_else(|| {
1216 crate::error::EFError::migration(format!(
1217 "target migration '{t}' is not applied"
1218 ))
1219 })?;
1220 applied_ids[idx + 1..]
1221 .iter()
1222 .map(|s| s.to_string())
1223 .collect()
1224 }
1225 None => applied_ids.iter().map(|s| s.to_string()).collect(),
1226 };
1227
1228 let mut reverted = Vec::with_capacity(to_revert.len());
1229 for id in to_revert.iter().rev() {
1230 let migration = migrations.iter().find(|m| m.id == *id).ok_or_else(|| {
1231 crate::error::EFError::migration(format!(
1232 "applied migration '{id}' not found in local migration set"
1233 ))
1234 })?;
1235 self.revert(provider, migration).await?;
1236 reverted.push(id.clone());
1237 }
1238 Ok(reverted)
1239 }
1240
1241 pub fn generate_script(
1253 migrations: &[Migration],
1254 from: Option<&str>,
1255 to: Option<&str>,
1256 ) -> EFResult<String> {
1257 let from_idx: i64 = match from {
1260 Some(f) => migrations.iter().position(|m| m.id == f).ok_or_else(|| {
1261 crate::error::EFError::migration(format!(
1262 "from migration '{f}' not found in local set"
1263 ))
1264 })? as i64,
1265 None => -1,
1266 };
1267 let to_idx: i64 = match to {
1268 Some(t) => migrations.iter().position(|m| m.id == t).ok_or_else(|| {
1269 crate::error::EFError::migration(format!(
1270 "to migration '{t}' not found in local set"
1271 ))
1272 })? as i64,
1273 None => migrations.len() as i64 - 1,
1274 };
1275
1276 let mut sql = String::new();
1277 if from_idx < to_idx {
1278 sql.push_str("-- Migration script (forward)\n\n");
1280 let start = (from_idx + 1) as usize;
1281 let end = (to_idx + 1) as usize; for m in &migrations[start..end] {
1283 sql.push_str(&format!("-- Up: {}\n", m.id));
1284 sql.push_str(&m.up_sql.replace("{migration_id}", &m.id));
1285 sql.push('\n');
1286 }
1287 } else if from_idx > to_idx {
1288 sql.push_str("-- Migration script (reverse)\n\n");
1290 let start = to_idx as usize + 1; let end = from_idx as usize + 1; for m in migrations[start..end].iter().rev() {
1293 sql.push_str(&format!("-- Down: {}\n", m.id));
1294 sql.push_str(&m.down_sql.replace("{migration_id}", &m.id));
1295 sql.push('\n');
1296 }
1297 } else {
1298 sql.push_str("-- Nothing to do (from == to)\n");
1299 }
1300 Ok(sql)
1301 }
1302
1303 pub async fn ensure_created(
1307 &self,
1308 provider: &dyn crate::provider::IDatabaseProvider,
1309 entity_types: &[EntityTypeMeta],
1310 ) -> EFResult<()> {
1311 let snapshot = self.create_snapshot("__ensure_created__", entity_types);
1312 let changes = self.initial_create(&snapshot);
1313 let sql = self.generate_ddl_sql(&changes);
1314 for statement in split_sql_statements(&sql) {
1315 if !statement.is_empty() {
1316 provider.execute_migration_command(&statement).await?;
1317 }
1318 }
1319 Ok(())
1320 }
1321
1322 pub async fn ensure_deleted(
1325 &self,
1326 provider: &dyn crate::provider::IDatabaseProvider,
1327 entity_types: &[EntityTypeMeta],
1328 ) -> EFResult<()> {
1329 let q = |s: &str| self.dialect.quote(s);
1330 for meta in entity_types {
1331 let sql = format!("DROP TABLE IF EXISTS {};", q(meta.table_name.as_ref()));
1332 provider.execute_migration_command(&sql).await?;
1333 }
1334 Ok(())
1335 }
1336
1337 pub async fn apply_seed_data(
1339 &self,
1340 provider: &dyn crate::provider::IDatabaseProvider,
1341 meta: &EntityTypeMeta,
1342 rows: &[std::collections::HashMap<String, crate::provider::DbValue>],
1343 ) -> EFResult<()> {
1344 if rows.is_empty() {
1345 return Ok(());
1346 }
1347
1348 let gen = provider.sql_generator();
1349 let scalar_props: Vec<_> = meta.mapped_scalar_properties().collect();
1350 if scalar_props.is_empty() {
1351 return Ok(());
1352 }
1353
1354 let col_names: Vec<&str> = scalar_props
1355 .iter()
1356 .map(|p| p.column_name.as_ref())
1357 .collect();
1358
1359 let mut conn = provider.get_connection().await?;
1360 for row in rows {
1361 let params: Vec<crate::provider::DbValue> = scalar_props
1362 .iter()
1363 .map(|p| {
1364 row.get(p.field_name.as_ref())
1365 .cloned()
1366 .unwrap_or(crate::provider::DbValue::Null)
1367 })
1368 .collect();
1369
1370 let sql = seed_insert_sql(self.dialect, meta.table_name.as_ref(), &col_names, gen);
1371 conn.execute(&sql, ¶ms).await?;
1372 }
1373 Ok(())
1374 }
1375}
1376
1377#[derive(Debug, Clone)]
1381pub struct MigrationHistoryEntry {
1382 pub migration_id: String,
1383 pub product_version: String,
1384}
1385
1386pub const MIGRATION_HISTORY_TABLE: &str = "__ef_migrations_history";
1388
1389pub const PRODUCT_VERSION: &str = env!("CARGO_PKG_VERSION");
1391
1392fn seed_insert_sql(
1393 dialect: MigrationDialect,
1394 table: &str,
1395 columns: &[&str],
1396 gen: &dyn crate::provider::ISqlGenerator,
1397) -> String {
1398 let quoted_table = dialect.quote(table);
1399 let quoted_cols: Vec<String> = columns.iter().map(|c| dialect.quote(c)).collect();
1400 let placeholders: Vec<String> = (0..columns.len())
1401 .map(|i| gen.parameter_placeholder(i + 1))
1402 .collect();
1403 let cols = quoted_cols.join(", ");
1404 let vals = placeholders.join(", ");
1405 match dialect {
1406 MigrationDialect::Sqlite => {
1407 format!("INSERT OR IGNORE INTO {quoted_table} ({cols}) VALUES ({vals})")
1408 }
1409 MigrationDialect::Postgres => {
1410 format!("INSERT INTO {quoted_table} ({cols}) VALUES ({vals}) ON CONFLICT DO NOTHING")
1411 }
1412 MigrationDialect::MySql => {
1413 format!("INSERT IGNORE INTO {quoted_table} ({cols}) VALUES ({vals})")
1414 }
1415 }
1416}
1417
1418fn split_sql_statements(sql: &str) -> Vec<String> {
1420 sql.lines()
1421 .filter(|l| {
1422 let trimmed = l.trim();
1423 !trimmed.is_empty() && !trimmed.starts_with("--")
1424 })
1425 .collect::<Vec<_>>()
1426 .join("\n")
1427 .split(';')
1428 .map(|s| s.trim().to_string())
1429 .filter(|s| !s.is_empty())
1430 .collect()
1431}
1432
1433pub fn create_migration_history_table_sql(dialect: MigrationDialect) -> String {
1435 match dialect {
1436 MigrationDialect::Postgres => format!(
1437 r#"CREATE TABLE IF NOT EXISTS "{table}" (
1438 "migration_id" VARCHAR(150) NOT NULL PRIMARY KEY,
1439 "product_version" VARCHAR(32) NOT NULL,
1440 "applied_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1441);"#,
1442 table = MIGRATION_HISTORY_TABLE
1443 ),
1444 MigrationDialect::MySql => format!(
1445 r#"CREATE TABLE IF NOT EXISTS `{table}` (
1446 `migration_id` VARCHAR(150) NOT NULL PRIMARY KEY,
1447 `product_version` VARCHAR(32) NOT NULL,
1448 `applied_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
1449) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"#,
1450 table = MIGRATION_HISTORY_TABLE
1451 ),
1452 MigrationDialect::Sqlite => format!(
1453 r#"CREATE TABLE IF NOT EXISTS "{table}" (
1454 "migration_id" TEXT NOT NULL PRIMARY KEY,
1455 "product_version" TEXT NOT NULL
1456);"#,
1457 table = MIGRATION_HISTORY_TABLE
1458 ),
1459 }
1460}
1461
1462use std::fs;
1467use std::path::{Path, PathBuf};
1468
1469#[derive(Debug, Clone)]
1479pub struct MigrationStore {
1480 root: PathBuf,
1481}
1482
1483impl MigrationStore {
1484 pub fn new(root: impl Into<PathBuf>) -> Self {
1485 Self { root: root.into() }
1486 }
1487
1488 pub fn root(&self) -> &Path {
1489 &self.root
1490 }
1491
1492 pub fn save(&self, migration: &Migration) -> EFResult<()> {
1494 fs::create_dir_all(&self.root).map_err(migration_io_err)?;
1495 let dir = self.root.join(&migration.id);
1496 fs::create_dir_all(&dir).map_err(migration_io_err)?;
1497 fs::write(dir.join("up.sql"), &migration.up_sql).map_err(migration_io_err)?;
1498 fs::write(dir.join("down.sql"), &migration.down_sql).map_err(migration_io_err)?;
1499 Ok(())
1500 }
1501
1502 pub fn load_all(&self) -> EFResult<Vec<Migration>> {
1504 if !self.root.exists() {
1505 return Ok(Vec::new());
1506 }
1507 let mut ids: Vec<String> = fs::read_dir(&self.root)
1508 .map_err(migration_io_err)?
1509 .filter_map(|e| e.ok())
1510 .filter(|e| e.path().is_dir())
1511 .filter_map(|e| e.file_name().into_string().ok())
1512 .collect();
1513 ids.sort();
1514 ids.into_iter().map(|id| self.load(&id)).collect()
1515 }
1516
1517 pub fn load(&self, id: &str) -> EFResult<Migration> {
1518 let dir = self.root.join(id);
1519 let up_sql = fs::read_to_string(dir.join("up.sql")).map_err(migration_io_err)?;
1520 let down_sql = fs::read_to_string(dir.join("down.sql")).map_err(migration_io_err)?;
1521 Ok(Migration {
1522 id: id.to_string(),
1523 description: id.to_string(),
1524 up_sql,
1525 down_sql,
1526 })
1527 }
1528
1529 pub fn save_snapshot(&self, snapshot: &ModelSnapshot) -> EFResult<()> {
1531 fs::create_dir_all(&self.root).map_err(migration_io_err)?;
1532 let json = snapshot_to_json(snapshot);
1533 fs::write(self.root.join("model_snapshot.json"), json).map_err(migration_io_err)
1534 }
1535
1536 pub fn load_snapshot(&self) -> EFResult<Option<ModelSnapshot>> {
1537 let path = self.root.join("model_snapshot.json");
1538 if !path.exists() {
1539 return Ok(None);
1540 }
1541 let text = fs::read_to_string(path).map_err(migration_io_err)?;
1542 parse_model_snapshot_json(&text)
1543 }
1544}
1545
1546pub fn parse_model_snapshot_json(text: &str) -> EFResult<Option<ModelSnapshot>> {
1548 snapshot_from_json(text)
1549}
1550
1551fn migration_io_err(e: std::io::Error) -> crate::error::EFError {
1552 crate::error::EFError::migration(e.to_string())
1553}
1554
1555fn snapshot_to_json(snapshot: &ModelSnapshot) -> String {
1556 let mut out = String::from("{\n \"migration_id\": \"");
1557 out.push_str(&snapshot.migration_id.replace('"', "\\\""));
1558 out.push_str("\",\n \"entity_types\": [\n");
1559 for (i, et) in snapshot.entity_types.iter().enumerate() {
1560 if i > 0 {
1561 out.push(',');
1562 }
1563 out.push_str(" {\n");
1564 out.push_str(&format!(
1565 " \"type_name\": \"{}\",\n",
1566 et.type_name.replace('"', "\\\"")
1567 ));
1568 out.push_str(&format!(
1569 " \"table_name\": \"{}\",\n",
1570 et.table_name.replace('"', "\\\"")
1571 ));
1572 out.push_str(" \"columns\": [\n");
1573 for (j, col) in et.columns.iter().enumerate() {
1574 if j > 0 {
1575 out.push(',');
1576 }
1577 out.push_str(&format!(
1578 " {{\"field_name\":\"{}\",\"column_name\":\"{}\",\"type_name\":\"{}\",\"is_primary_key\":{},\"is_required\":{},\"is_foreign_key\":{},\"max_length\":{},\"is_auto_increment\":{},\"is_sequence\":{},\"sequence_name\":{},\"fk_referenced_table\":{},\"fk_referenced_column\":{},\"has_index\":{},\"is_unique\":{},\"fk_on_delete\":{}}}\n",
1579 col.field_name.replace('"', "\\\""),
1580 col.column_name.replace('"', "\\\""),
1581 col.type_name.replace('"', "\\\""),
1582 col.is_primary_key,
1583 col.is_required,
1584 col.is_foreign_key,
1585 col.max_length.map(|n| n.to_string()).unwrap_or_else(|| "null".into()),
1586 col.is_auto_increment,
1587 col.is_sequence,
1588 col.sequence_name
1589 .as_ref()
1590 .map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
1591 .unwrap_or_else(|| "null".into()),
1592 col.fk_referenced_table
1593 .as_ref()
1594 .map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
1595 .unwrap_or_else(|| "null".into()),
1596 col.fk_referenced_column
1597 .as_ref()
1598 .map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
1599 .unwrap_or_else(|| "null".into()),
1600 col.has_index,
1601 col.is_unique,
1602 col.fk_on_delete
1603 .as_ref()
1604 .map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
1605 .unwrap_or_else(|| "null".into())
1606 ));
1607 }
1608 out.push_str(" ]\n }\n");
1609 }
1610 out.push_str(" ]\n}\n");
1611 out
1612}
1613
1614fn snapshot_from_json(text: &str) -> EFResult<Option<ModelSnapshot>> {
1615 let migration_id =
1617 extract_json_string(text, "migration_id").unwrap_or_else(|| "__snapshot__".to_string());
1618 let mut entity_types = Vec::new();
1619 if let Some(arr_start) = text.find("\"entity_types\"") {
1620 let slice = &text[arr_start..];
1621 for table_block in slice.split("\"table_name\"").skip(1) {
1622 let table_name = extract_quoted_after_colon(table_block).unwrap_or_default();
1623 let _type_name = entity_types.len().to_string(); let columns_start = table_block.find("\"columns\"");
1625 let mut columns = Vec::new();
1626 if let Some(cs) = columns_start {
1627 let col_slice = &table_block[cs..];
1628 for col_chunk in col_slice.split("\"field_name\"").skip(1) {
1629 if let Some(field_name) = extract_quoted_after_colon(col_chunk) {
1630 let column_name = extract_json_string(col_chunk, "column_name")
1631 .unwrap_or_else(|| field_name.clone());
1632 let type_name_col = extract_json_string(col_chunk, "type_name")
1633 .unwrap_or_else(|| "String".to_string());
1634 columns.push(SnapshotColumn {
1635 field_name,
1636 column_name,
1637 type_name: type_name_col,
1638 is_primary_key: col_chunk.contains("\"is_primary_key\":true"),
1639 is_required: col_chunk.contains("\"is_required\":true"),
1640 is_foreign_key: col_chunk.contains("\"is_foreign_key\":true"),
1641 max_length: None,
1642 is_auto_increment: col_chunk.contains("\"is_auto_increment\":true"),
1643 is_sequence: col_chunk.contains("\"is_sequence\":true"),
1644 sequence_name: extract_json_string(col_chunk, "sequence_name"),
1645 fk_referenced_table: extract_json_string(
1646 col_chunk,
1647 "fk_referenced_table",
1648 ),
1649 fk_referenced_column: extract_json_string(
1650 col_chunk,
1651 "fk_referenced_column",
1652 ),
1653 has_index: col_chunk.contains("\"has_index\":true"),
1654 is_unique: col_chunk.contains("\"is_unique\":true"),
1655 fk_on_delete: extract_json_string(col_chunk, "fk_on_delete"),
1656 });
1657 }
1658 }
1659 }
1660 if !table_name.is_empty() {
1661 entity_types.push(SnapshotEntityType {
1662 type_name: table_name.clone(),
1663 table_name,
1664 columns,
1665 });
1666 }
1667 }
1668 }
1669 if entity_types.is_empty() {
1670 return Ok(None);
1671 }
1672 Ok(Some(ModelSnapshot {
1673 migration_id,
1674 entity_types,
1675 }))
1676}
1677
1678fn extract_json_string(haystack: &str, key: &str) -> Option<String> {
1679 let needle = format!("\"{key}\"");
1680 let pos = haystack.find(&needle)?;
1681 extract_quoted_after_colon(&haystack[pos + needle.len()..])
1682}
1683
1684fn extract_quoted_after_colon(s: &str) -> Option<String> {
1685 let colon = s.find(':')?;
1686 let rest = s[colon + 1..].trim_start();
1687 if !rest.starts_with('"') {
1688 return None;
1689 }
1690 let inner = &rest[1..];
1691 let end = inner.find('"')?;
1692 Some(inner[..end].to_string())
1693}