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: Box<SnapshotColumn>,
209 new: Box<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: Box::new((*old_col).clone()),
423 new: Box::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((c.column_name.as_str(), (c, rt, rc, c.fk_on_delete.clone())))
567 })
568 .collect();
569 let new_fks: HashMap<&str, (&SnapshotColumn, String, String, Option<String>)> = new_et
570 .columns
571 .iter()
572 .filter_map(|c| {
573 let (rt, rc) = fk_target(c)?;
574 Some((c.column_name.as_str(), (c, rt, rc, c.fk_on_delete.clone())))
575 })
576 .collect();
577
578 let old_names: HashSet<&str> = old_fks.keys().copied().collect();
579 let new_names: HashSet<&str> = new_fks.keys().copied().collect();
580
581 for col in new_names.difference(&old_names) {
582 let (_, rt, rc, od) = &new_fks[col];
583 changes.push(SchemaChange::AddForeignKey {
584 table: table.to_string(),
585 column: (*col).to_string(),
586 referenced_table: rt.clone(),
587 referenced_column: rc.clone(),
588 on_delete: od.clone(),
589 });
590 }
591
592 for col in old_names.difference(&new_names) {
593 let (_, rt, _, _) = &old_fks[col];
594 changes.push(SchemaChange::DropForeignKey {
595 table: table.to_string(),
596 column: (*col).to_string(),
597 referenced_table: rt.clone(),
598 });
599 }
600
601 for col in old_names.intersection(&new_names) {
602 let (_, old_rt, old_rc, old_od) = &old_fks[col];
603 let (_, new_rt, new_rc, new_od) = &new_fks[col];
604 if old_rt != new_rt || old_rc != new_rc || old_od != new_od {
605 changes.push(SchemaChange::DropForeignKey {
606 table: table.to_string(),
607 column: (*col).to_string(),
608 referenced_table: old_rt.clone(),
609 });
610 changes.push(SchemaChange::AddForeignKey {
611 table: table.to_string(),
612 column: (*col).to_string(),
613 referenced_table: new_rt.clone(),
614 referenced_column: new_rc.clone(),
615 on_delete: new_od.clone(),
616 });
617 }
618 }
619
620 changes
621}
622
623fn columns_structurally_equal(a: &SnapshotColumn, b: &SnapshotColumn) -> bool {
628 a.field_name == b.field_name
629 && a.column_name == b.column_name
630 && a.type_name == b.type_name
631 && a.is_primary_key == b.is_primary_key
632 && a.is_required == b.is_required
633 && a.is_foreign_key == b.is_foreign_key
634 && a.max_length == b.max_length
635 && a.is_auto_increment == b.is_auto_increment
636 && a.fk_referenced_table == b.fk_referenced_table
637 && a.fk_referenced_column == b.fk_referenced_column
638}
639
640fn diff_indexes(table: &str, old: &SnapshotColumn, new: &SnapshotColumn) -> Vec<SchemaChange> {
643 let old_kind = index_kind(old);
644 let new_kind = index_kind(new);
645 if old_kind == new_kind {
646 return Vec::new();
647 }
648 let mut changes = Vec::new();
649 if old_kind != IndexKind::None {
650 changes.push(SchemaChange::DropIndex {
651 table: table.to_string(),
652 column: new.column_name.clone(),
653 is_unique: old_kind == IndexKind::Unique,
654 });
655 }
656 if new_kind != IndexKind::None {
657 changes.push(SchemaChange::CreateIndex {
658 table: table.to_string(),
659 column: new.column_name.clone(),
660 is_unique: new_kind == IndexKind::Unique,
661 });
662 }
663 changes
664}
665
666#[derive(Clone, Copy, PartialEq, Eq)]
667enum IndexKind {
668 None,
669 NonUnique,
670 Unique,
671}
672
673fn index_kind(col: &SnapshotColumn) -> IndexKind {
674 if col.is_unique {
675 IndexKind::Unique
676 } else if col.has_index {
677 IndexKind::NonUnique
678 } else {
679 IndexKind::None
680 }
681}
682
683impl MigrationEngine {
684 fn initial_create_with_fks(&self, current: &ModelSnapshot) -> Vec<SchemaChange> {
685 let mut changes = self.initial_create(current);
686 for et in ¤t.entity_types {
687 Self::append_create_table_fks(&mut changes, &et.table_name, &et.columns);
688 Self::append_create_table_indexes(&mut changes, &et.table_name, &et.columns);
689 }
690 changes
691 }
692
693 pub fn foreign_key_name(table: &str, column: &str, referenced_table: &str) -> String {
699 format!("fk_{}_{}_{}", table, column, referenced_table)
700 }
701
702 pub fn index_name(table: &str, column: &str) -> String {
704 format!("ix_{}_{}", table, column)
705 }
706
707 pub fn generate_alter_column_sql(
708 &self,
709 table: &str,
710 column_name: &str,
711 new: &SnapshotColumn,
712 ) -> String {
713 let q = |s: &str| self.dialect.quote(s);
714 let col_type = self.dialect.map_column_type(new);
715 match self.dialect {
716 MigrationDialect::Postgres => {
717 let nullable = if new.is_required {
718 "SET NOT NULL"
719 } else {
720 "DROP NOT NULL"
721 };
722 format!(
723 "ALTER TABLE {} ALTER COLUMN {} TYPE {};\nALTER TABLE {} ALTER COLUMN {} {};\n",
724 q(table),
725 q(column_name),
726 col_type,
727 q(table),
728 q(column_name),
729 nullable
730 )
731 }
732 MigrationDialect::MySql => {
733 let nullable = if new.is_required { "NOT NULL" } else { "NULL" };
734 format!(
735 "ALTER TABLE {} MODIFY COLUMN {} {} {};\n",
736 q(table),
737 q(column_name),
738 col_type,
739 nullable
740 )
741 }
742 MigrationDialect::Sqlite => format!(
743 "-- SQLite does not support ALTER COLUMN type changes; \
744 rebuild table manually for {}.{}\n",
745 q(table),
746 q(column_name)
747 ),
748 }
749 }
750
751 fn generate_up_sql(&self, changes: &[SchemaChange]) -> String {
752 self.generate_up_sql_inner(changes, true)
753 }
754
755 fn generate_ddl_sql(&self, changes: &[SchemaChange]) -> String {
756 self.generate_up_sql_inner(changes, false)
757 }
758
759 fn generate_up_sql_inner(&self, changes: &[SchemaChange], record_history: bool) -> String {
760 let mut sql = String::from("-- Up Migration (auto-generated by rust-ef)\n\n");
761 let q = |s: &str| self.dialect.quote(s);
762 let create_kw = if record_history {
763 "CREATE TABLE"
764 } else {
765 "CREATE TABLE IF NOT EXISTS"
766 };
767
768 for change in changes {
769 match change {
770 SchemaChange::CreateTable { table, columns } => {
771 if self.dialect == MigrationDialect::Postgres {
773 for c in columns {
774 if c.is_sequence {
775 if let Some(seq_name) = &c.sequence_name {
776 sql.push_str(&format!(
777 "CREATE SEQUENCE IF NOT EXISTS {};\n",
778 q(seq_name)
779 ));
780 }
781 }
782 }
783 }
784
785 sql.push_str(&format!("{} {} (\n", create_kw, q(table)));
786
787 let pk_columns: Vec<&str> = columns
789 .iter()
790 .filter(|c| c.is_primary_key)
791 .map(|c| c.column_name.as_str())
792 .collect();
793
794 let col_defs: Vec<String> = columns
795 .iter()
796 .map(|c| {
797 let nullable = if c.is_required { "NOT NULL" } else { "NULL" };
798 let col_type = self.dialect.map_column_type(c);
799 if c.is_sequence && self.dialect == MigrationDialect::Postgres {
801 if let Some(seq_name) = &c.sequence_name {
802 return format!(
803 "{} {} DEFAULT nextval('{}') {}",
804 q(&c.column_name),
805 col_type,
806 seq_name,
807 nullable
808 );
809 }
810 }
811 [q(&c.column_name), col_type, nullable.to_string()]
813 .into_iter()
814 .filter(|s| !s.is_empty())
815 .collect::<Vec<_>>()
816 .join(" ")
817 })
818 .collect();
819 sql.push_str(&format!(" {}\n", col_defs.join(",\n ")));
820
821 if pk_columns.len() == 1 {
823 sql.push_str(&format!(" ,PRIMARY KEY ({})\n", q(pk_columns[0])));
824 } else if pk_columns.len() > 1 {
825 let pk_list: Vec<String> = pk_columns.iter().map(|c| q(c)).collect();
826 sql.push_str(&format!(" ,PRIMARY KEY ({})\n", pk_list.join(", ")));
827 }
828
829 sql.push_str(");\n\n");
830 }
831 SchemaChange::DropTable { table } => {
832 sql.push_str(&format!("DROP TABLE IF EXISTS {};\n", q(table)));
833 }
834 SchemaChange::AddColumn { table, column } => {
835 let col_type = self.dialect.map_column_type(column);
836 let nullable = if column.is_required {
837 "NOT NULL"
838 } else {
839 "NULL"
840 };
841 sql.push_str(&format!(
842 "ALTER TABLE {} ADD COLUMN {} {} {};\n",
843 q(table),
844 q(&column.column_name),
845 col_type,
846 nullable
847 ));
848 }
849 SchemaChange::DropColumn { table, column_name } => {
850 sql.push_str(&format!(
851 "ALTER TABLE {} DROP COLUMN IF EXISTS {};\n",
852 q(table),
853 q(column_name)
854 ));
855 }
856 SchemaChange::AlterColumn {
857 table,
858 column_name,
859 old: _,
860 new,
861 } => {
862 sql.push_str(&self.generate_alter_column_sql(table, column_name, new));
863 }
864 SchemaChange::AddForeignKey {
865 table,
866 column,
867 referenced_table,
868 referenced_column,
869 on_delete,
870 } => {
871 let fk_name = Self::foreign_key_name(table, column, referenced_table);
872 let on_delete_clause = on_delete
873 .as_deref()
874 .map(|c| format!(" ON DELETE {}", c))
875 .unwrap_or_default();
876 sql.push_str(&format!(
877 "ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}){};\n",
878 q(table),
879 q(&fk_name),
880 q(column),
881 q(referenced_table),
882 q(referenced_column),
883 on_delete_clause
884 ));
885 }
886 SchemaChange::DropForeignKey {
887 table,
888 column,
889 referenced_table,
890 } => {
891 let fk_name = Self::foreign_key_name(table, column, referenced_table);
892 match self.dialect {
893 MigrationDialect::MySql => sql.push_str(&format!(
894 "ALTER TABLE {} DROP FOREIGN KEY {};\n",
895 q(table),
896 q(&fk_name)
897 )),
898 _ => sql.push_str(&format!(
899 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {};\n",
900 q(table),
901 q(&fk_name)
902 )),
903 }
904 }
905 SchemaChange::CreateIndex {
906 table,
907 column,
908 is_unique,
909 } => {
910 let unique_kw = if *is_unique { "UNIQUE " } else { "" };
911 let idx_name = index_name(table, column);
912 sql.push_str(&format!(
913 "CREATE {unique_kw}INDEX {} ON {} ({});\n",
914 q(&idx_name),
915 q(table),
916 q(column)
917 ));
918 }
919 SchemaChange::DropIndex {
920 table,
921 column,
922 is_unique: _,
923 } => {
924 let idx_name = index_name(table, column);
925 match self.dialect {
926 MigrationDialect::MySql => {
927 sql.push_str(&format!("DROP INDEX {} ON {};\n", q(&idx_name), q(table)))
928 }
929 _ => sql.push_str(&format!("DROP INDEX IF EXISTS {};\n", q(&idx_name))),
930 }
931 }
932 }
933 }
934
935 if record_history {
936 sql.push_str(&format!(
937 "INSERT INTO {}(migration_id, product_version) VALUES ('{{migration_id}}', '{}');\n",
938 q(MIGRATION_HISTORY_TABLE),
939 PRODUCT_VERSION
940 ));
941 }
942
943 sql
944 }
945
946 fn generate_down_sql(&self, changes: &[SchemaChange]) -> String {
947 let mut sql = String::from("-- Down Migration (auto-generated by rust-ef)\n\n");
948 let q = |s: &str| self.dialect.quote(s);
949
950 for change in changes.iter().rev() {
952 match change {
953 SchemaChange::CreateTable { table, .. } => {
954 sql.push_str(&format!("DROP TABLE IF EXISTS {};\n", q(table)));
955 }
956 SchemaChange::DropTable { table } => {
957 sql.push_str(&format!(
959 "-- WARNING: Cannot restore table {} (original schema unknown)\n",
960 q(table)
961 ));
962 }
963 SchemaChange::AddColumn { table, column } => {
964 sql.push_str(&format!(
965 "ALTER TABLE {} DROP COLUMN IF EXISTS {};\n",
966 q(table),
967 q(&column.column_name)
968 ));
969 }
970 SchemaChange::DropColumn { table, column_name } => {
971 sql.push_str(&format!(
973 "-- WARNING: Cannot restore column {} on {} (original type unknown)\n",
974 q(column_name),
975 q(table)
976 ));
977 }
978 SchemaChange::AlterColumn {
979 table,
980 column_name,
981 old,
982 new: _,
983 } => {
984 sql.push_str(&self.generate_alter_column_sql(table, column_name, old));
985 }
986 SchemaChange::AddForeignKey {
987 table,
988 column,
989 referenced_table,
990 ..
991 } => {
992 let fk_name = Self::foreign_key_name(table, column, referenced_table);
993 match self.dialect {
994 MigrationDialect::MySql => sql.push_str(&format!(
995 "ALTER TABLE {} DROP FOREIGN KEY {};\n",
996 q(table),
997 q(&fk_name)
998 )),
999 _ => sql.push_str(&format!(
1000 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {};\n",
1001 q(table),
1002 q(&fk_name)
1003 )),
1004 }
1005 }
1006 SchemaChange::DropForeignKey {
1007 table,
1008 column,
1009 referenced_table,
1010 ..
1011 } => {
1012 sql.push_str(&format!(
1013 "-- WARNING: Cannot restore foreign key constraint {} on {}.{}\n",
1014 q(&Self::foreign_key_name(table, column, referenced_table)),
1015 q(table),
1016 q(column)
1017 ));
1018 }
1019 SchemaChange::CreateIndex {
1020 table,
1021 column,
1022 is_unique: _,
1023 } => {
1024 let idx_name = index_name(table, column);
1025 match self.dialect {
1026 MigrationDialect::MySql => {
1027 sql.push_str(&format!("DROP INDEX {} ON {};\n", q(&idx_name), q(table)))
1028 }
1029 _ => sql.push_str(&format!("DROP INDEX IF EXISTS {};\n", q(&idx_name))),
1030 }
1031 }
1032 SchemaChange::DropIndex {
1033 table,
1034 column,
1035 is_unique,
1036 } => {
1037 let unique_kw = if *is_unique { "UNIQUE " } else { "" };
1038 let idx_name = index_name(table, column);
1039 sql.push_str(&format!(
1040 "CREATE {unique_kw}INDEX {} ON {} ({});\n",
1041 q(&idx_name),
1042 q(table),
1043 q(column)
1044 ));
1045 }
1046 }
1047 }
1048
1049 sql.push_str(&format!(
1050 "DELETE FROM {} WHERE migration_id = '{{migration_id}}';\n",
1051 q(MIGRATION_HISTORY_TABLE)
1052 ));
1053
1054 sql
1055 }
1056
1057 pub async fn ensure_history_table(
1059 &self,
1060 provider: &dyn crate::provider::IDatabaseProvider,
1061 ) -> EFResult<()> {
1062 let sql = create_migration_history_table_sql(self.dialect);
1063 provider.execute_migration_command(&sql).await
1064 }
1065
1066 pub async fn apply(
1068 &self,
1069 provider: &dyn crate::provider::IDatabaseProvider,
1070 migration: &Migration,
1071 ) -> EFResult<()> {
1072 if self.is_applied(provider, &migration.id).await? {
1073 return Ok(());
1074 }
1075 self.ensure_history_table(provider).await?;
1076 let sql = migration.up_sql.replace("{migration_id}", &migration.id);
1077 for statement in split_sql_statements(&sql) {
1078 if !statement.is_empty() {
1079 provider.execute_migration_command(&statement).await?;
1080 }
1081 }
1082 Ok(())
1083 }
1084
1085 pub async fn revert(
1087 &self,
1088 provider: &dyn crate::provider::IDatabaseProvider,
1089 migration: &Migration,
1090 ) -> EFResult<()> {
1091 if !self.is_applied(provider, &migration.id).await? {
1092 return Err(crate::error::EFError::migration(format!(
1093 "migration '{}' is not applied",
1094 migration.id
1095 )));
1096 }
1097 let sql = migration.down_sql.replace("{migration_id}", &migration.id);
1098 for statement in split_sql_statements(&sql) {
1099 if !statement.is_empty() {
1100 provider.execute_migration_command(&statement).await?;
1101 }
1102 }
1103 Ok(())
1104 }
1105
1106 pub async fn get_applied_migrations(
1108 &self,
1109 provider: &dyn crate::provider::IDatabaseProvider,
1110 ) -> EFResult<Vec<MigrationHistoryEntry>> {
1111 self.ensure_history_table(provider).await?;
1112 let q = |s: &str| self.dialect.quote(s);
1113 let table = q(MIGRATION_HISTORY_TABLE);
1114 let sql = format!(
1115 "SELECT {}, {} FROM {} ORDER BY {}",
1116 q("migration_id"),
1117 q("product_version"),
1118 table,
1119 q("migration_id")
1120 );
1121 let mut conn = provider.get_connection().await?;
1122 let rows = conn.query(&sql, &[]).await?;
1123 Ok(rows
1124 .into_iter()
1125 .map(|row| MigrationHistoryEntry {
1126 migration_id: row
1127 .first()
1128 .and_then(|v| String::try_from(v.clone()).ok())
1129 .unwrap_or_default(),
1130 product_version: row
1131 .get(1)
1132 .and_then(|v| String::try_from(v.clone()).ok())
1133 .unwrap_or_default(),
1134 })
1135 .collect())
1136 }
1137
1138 pub async fn is_applied(
1140 &self,
1141 provider: &dyn crate::provider::IDatabaseProvider,
1142 migration_id: &str,
1143 ) -> EFResult<bool> {
1144 let applied = self.get_applied_migrations(provider).await?;
1145 Ok(applied.iter().any(|e| e.migration_id == migration_id))
1146 }
1147
1148 pub async fn apply_pending(
1150 &self,
1151 provider: &dyn crate::provider::IDatabaseProvider,
1152 migrations: &[Migration],
1153 ) -> EFResult<usize> {
1154 let applied: std::collections::HashSet<String> = self
1155 .get_applied_migrations(provider)
1156 .await?
1157 .into_iter()
1158 .map(|e| e.migration_id)
1159 .collect();
1160 let mut count = 0usize;
1161 for migration in migrations {
1162 if applied.contains(&migration.id) {
1163 continue;
1164 }
1165 self.apply(provider, migration).await?;
1166 count += 1;
1167 }
1168 Ok(count)
1169 }
1170
1171 pub async fn revert_last(
1173 &self,
1174 provider: &dyn crate::provider::IDatabaseProvider,
1175 migrations: &[Migration],
1176 ) -> EFResult<Option<String>> {
1177 let applied = self.get_applied_migrations(provider).await?;
1178 let last = applied.last().map(|e| e.migration_id.clone());
1179 let Some(last_id) = last else {
1180 return Ok(None);
1181 };
1182 let migration = migrations.iter().find(|m| m.id == last_id).ok_or_else(|| {
1183 crate::error::EFError::migration(format!(
1184 "applied migration '{}' not found in local migration set",
1185 last_id
1186 ))
1187 })?;
1188 self.revert(provider, migration).await?;
1189 Ok(Some(last_id))
1190 }
1191
1192 pub async fn revert_to_target(
1199 &self,
1200 provider: &dyn crate::provider::IDatabaseProvider,
1201 migrations: &[Migration],
1202 target: Option<&str>,
1203 ) -> EFResult<Vec<String>> {
1204 let applied = self.get_applied_migrations(provider).await?;
1205 let applied_ids: Vec<&str> = applied.iter().map(|e| e.migration_id.as_str()).collect();
1206
1207 let to_revert: Vec<String> = match target {
1208 Some(t) => {
1209 let idx = applied_ids.iter().position(|id| *id == t).ok_or_else(|| {
1210 crate::error::EFError::migration(format!(
1211 "target migration '{t}' is not applied"
1212 ))
1213 })?;
1214 applied_ids[idx + 1..]
1215 .iter()
1216 .map(|s| s.to_string())
1217 .collect()
1218 }
1219 None => applied_ids.iter().map(|s| s.to_string()).collect(),
1220 };
1221
1222 let mut reverted = Vec::with_capacity(to_revert.len());
1223 for id in to_revert.iter().rev() {
1224 let migration = migrations.iter().find(|m| m.id == *id).ok_or_else(|| {
1225 crate::error::EFError::migration(format!(
1226 "applied migration '{id}' not found in local migration set"
1227 ))
1228 })?;
1229 self.revert(provider, migration).await?;
1230 reverted.push(id.clone());
1231 }
1232 Ok(reverted)
1233 }
1234
1235 pub fn generate_script(
1247 migrations: &[Migration],
1248 from: Option<&str>,
1249 to: Option<&str>,
1250 ) -> EFResult<String> {
1251 let from_idx: i64 = match from {
1254 Some(f) => migrations.iter().position(|m| m.id == f).ok_or_else(|| {
1255 crate::error::EFError::migration(format!(
1256 "from migration '{f}' not found in local set"
1257 ))
1258 })? as i64,
1259 None => -1,
1260 };
1261 let to_idx: i64 = match to {
1262 Some(t) => migrations.iter().position(|m| m.id == t).ok_or_else(|| {
1263 crate::error::EFError::migration(format!(
1264 "to migration '{t}' not found in local set"
1265 ))
1266 })? as i64,
1267 None => migrations.len() as i64 - 1,
1268 };
1269
1270 let mut sql = String::new();
1271 if from_idx < to_idx {
1272 sql.push_str("-- Migration script (forward)\n\n");
1274 let start = (from_idx + 1) as usize;
1275 let end = (to_idx + 1) as usize; for m in &migrations[start..end] {
1277 sql.push_str(&format!("-- Up: {}\n", m.id));
1278 sql.push_str(&m.up_sql.replace("{migration_id}", &m.id));
1279 sql.push('\n');
1280 }
1281 } else if from_idx > to_idx {
1282 sql.push_str("-- Migration script (reverse)\n\n");
1284 let start = to_idx as usize + 1; let end = from_idx as usize + 1; for m in migrations[start..end].iter().rev() {
1287 sql.push_str(&format!("-- Down: {}\n", m.id));
1288 sql.push_str(&m.down_sql.replace("{migration_id}", &m.id));
1289 sql.push('\n');
1290 }
1291 } else {
1292 sql.push_str("-- Nothing to do (from == to)\n");
1293 }
1294 Ok(sql)
1295 }
1296
1297 pub async fn ensure_created(
1301 &self,
1302 provider: &dyn crate::provider::IDatabaseProvider,
1303 entity_types: &[EntityTypeMeta],
1304 ) -> EFResult<()> {
1305 let snapshot = self.create_snapshot("__ensure_created__", entity_types);
1306 let changes = self.initial_create(&snapshot);
1307 let sql = self.generate_ddl_sql(&changes);
1308 for statement in split_sql_statements(&sql) {
1309 if !statement.is_empty() {
1310 provider.execute_migration_command(&statement).await?;
1311 }
1312 }
1313 Ok(())
1314 }
1315
1316 pub async fn ensure_deleted(
1319 &self,
1320 provider: &dyn crate::provider::IDatabaseProvider,
1321 entity_types: &[EntityTypeMeta],
1322 ) -> EFResult<()> {
1323 let q = |s: &str| self.dialect.quote(s);
1324 for meta in entity_types {
1325 let sql = format!("DROP TABLE IF EXISTS {};", q(meta.table_name.as_ref()));
1326 provider.execute_migration_command(&sql).await?;
1327 }
1328 Ok(())
1329 }
1330
1331 pub async fn apply_seed_data(
1333 &self,
1334 provider: &dyn crate::provider::IDatabaseProvider,
1335 meta: &EntityTypeMeta,
1336 rows: &[std::collections::HashMap<String, crate::provider::DbValue>],
1337 ) -> EFResult<()> {
1338 if rows.is_empty() {
1339 return Ok(());
1340 }
1341
1342 let gen = provider.sql_generator();
1343 let scalar_props: Vec<_> = meta.mapped_scalar_properties().collect();
1344 if scalar_props.is_empty() {
1345 return Ok(());
1346 }
1347
1348 let col_names: Vec<&str> = scalar_props
1349 .iter()
1350 .map(|p| p.column_name.as_ref())
1351 .collect();
1352
1353 let mut conn = provider.get_connection().await?;
1354 for row in rows {
1355 let params: Vec<crate::provider::DbValue> = scalar_props
1356 .iter()
1357 .map(|p| {
1358 row.get(p.field_name.as_ref())
1359 .cloned()
1360 .unwrap_or(crate::provider::DbValue::Null)
1361 })
1362 .collect();
1363
1364 let sql = seed_insert_sql(self.dialect, meta.table_name.as_ref(), &col_names, gen);
1365 conn.execute(&sql, ¶ms).await?;
1366 }
1367 Ok(())
1368 }
1369}
1370
1371#[derive(Debug, Clone)]
1375pub struct MigrationHistoryEntry {
1376 pub migration_id: String,
1377 pub product_version: String,
1378}
1379
1380pub const MIGRATION_HISTORY_TABLE: &str = "__ef_migrations_history";
1382
1383pub const PRODUCT_VERSION: &str = env!("CARGO_PKG_VERSION");
1385
1386fn seed_insert_sql(
1387 dialect: MigrationDialect,
1388 table: &str,
1389 columns: &[&str],
1390 gen: &dyn crate::provider::ISqlGenerator,
1391) -> String {
1392 let quoted_table = dialect.quote(table);
1393 let quoted_cols: Vec<String> = columns.iter().map(|c| dialect.quote(c)).collect();
1394 let placeholders: Vec<String> = (0..columns.len())
1395 .map(|i| gen.parameter_placeholder(i + 1))
1396 .collect();
1397 let cols = quoted_cols.join(", ");
1398 let vals = placeholders.join(", ");
1399 match dialect {
1400 MigrationDialect::Sqlite => {
1401 format!("INSERT OR IGNORE INTO {quoted_table} ({cols}) VALUES ({vals})")
1402 }
1403 MigrationDialect::Postgres => {
1404 format!("INSERT INTO {quoted_table} ({cols}) VALUES ({vals}) ON CONFLICT DO NOTHING")
1405 }
1406 MigrationDialect::MySql => {
1407 format!("INSERT IGNORE INTO {quoted_table} ({cols}) VALUES ({vals})")
1408 }
1409 }
1410}
1411
1412fn split_sql_statements(sql: &str) -> Vec<String> {
1414 sql.lines()
1415 .filter(|l| {
1416 let trimmed = l.trim();
1417 !trimmed.is_empty() && !trimmed.starts_with("--")
1418 })
1419 .collect::<Vec<_>>()
1420 .join("\n")
1421 .split(';')
1422 .map(|s| s.trim().to_string())
1423 .filter(|s| !s.is_empty())
1424 .collect()
1425}
1426
1427pub fn create_migration_history_table_sql(dialect: MigrationDialect) -> String {
1429 match dialect {
1430 MigrationDialect::Postgres => format!(
1431 r#"CREATE TABLE IF NOT EXISTS "{table}" (
1432 "migration_id" VARCHAR(150) NOT NULL PRIMARY KEY,
1433 "product_version" VARCHAR(32) NOT NULL,
1434 "applied_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1435);"#,
1436 table = MIGRATION_HISTORY_TABLE
1437 ),
1438 MigrationDialect::MySql => format!(
1439 r#"CREATE TABLE IF NOT EXISTS `{table}` (
1440 `migration_id` VARCHAR(150) NOT NULL PRIMARY KEY,
1441 `product_version` VARCHAR(32) NOT NULL,
1442 `applied_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
1443) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"#,
1444 table = MIGRATION_HISTORY_TABLE
1445 ),
1446 MigrationDialect::Sqlite => format!(
1447 r#"CREATE TABLE IF NOT EXISTS "{table}" (
1448 "migration_id" TEXT NOT NULL PRIMARY KEY,
1449 "product_version" TEXT NOT NULL
1450);"#,
1451 table = MIGRATION_HISTORY_TABLE
1452 ),
1453 }
1454}
1455
1456use std::fs;
1461use std::path::{Path, PathBuf};
1462
1463#[derive(Debug, Clone)]
1473pub struct MigrationStore {
1474 root: PathBuf,
1475}
1476
1477impl MigrationStore {
1478 pub fn new(root: impl Into<PathBuf>) -> Self {
1479 Self { root: root.into() }
1480 }
1481
1482 pub fn root(&self) -> &Path {
1483 &self.root
1484 }
1485
1486 pub fn save(&self, migration: &Migration) -> EFResult<()> {
1488 fs::create_dir_all(&self.root).map_err(migration_io_err)?;
1489 let dir = self.root.join(&migration.id);
1490 fs::create_dir_all(&dir).map_err(migration_io_err)?;
1491 fs::write(dir.join("up.sql"), &migration.up_sql).map_err(migration_io_err)?;
1492 fs::write(dir.join("down.sql"), &migration.down_sql).map_err(migration_io_err)?;
1493 Ok(())
1494 }
1495
1496 pub fn load_all(&self) -> EFResult<Vec<Migration>> {
1498 if !self.root.exists() {
1499 return Ok(Vec::new());
1500 }
1501 let mut ids: Vec<String> = fs::read_dir(&self.root)
1502 .map_err(migration_io_err)?
1503 .filter_map(|e| e.ok())
1504 .filter(|e| e.path().is_dir())
1505 .filter_map(|e| e.file_name().into_string().ok())
1506 .collect();
1507 ids.sort();
1508 ids.into_iter().map(|id| self.load(&id)).collect()
1509 }
1510
1511 pub fn load(&self, id: &str) -> EFResult<Migration> {
1512 let dir = self.root.join(id);
1513 let up_sql = fs::read_to_string(dir.join("up.sql")).map_err(migration_io_err)?;
1514 let down_sql = fs::read_to_string(dir.join("down.sql")).map_err(migration_io_err)?;
1515 Ok(Migration {
1516 id: id.to_string(),
1517 description: id.to_string(),
1518 up_sql,
1519 down_sql,
1520 })
1521 }
1522
1523 pub fn save_snapshot(&self, snapshot: &ModelSnapshot) -> EFResult<()> {
1525 fs::create_dir_all(&self.root).map_err(migration_io_err)?;
1526 let json = snapshot_to_json(snapshot);
1527 fs::write(self.root.join("model_snapshot.json"), json).map_err(migration_io_err)
1528 }
1529
1530 pub fn load_snapshot(&self) -> EFResult<Option<ModelSnapshot>> {
1531 let path = self.root.join("model_snapshot.json");
1532 if !path.exists() {
1533 return Ok(None);
1534 }
1535 let text = fs::read_to_string(path).map_err(migration_io_err)?;
1536 parse_model_snapshot_json(&text)
1537 }
1538}
1539
1540pub fn parse_model_snapshot_json(text: &str) -> EFResult<Option<ModelSnapshot>> {
1542 snapshot_from_json(text)
1543}
1544
1545fn migration_io_err(e: std::io::Error) -> crate::error::EFError {
1546 crate::error::EFError::migration(e.to_string())
1547}
1548
1549fn snapshot_to_json(snapshot: &ModelSnapshot) -> String {
1550 let mut out = String::from("{\n \"migration_id\": \"");
1551 out.push_str(&snapshot.migration_id.replace('"', "\\\""));
1552 out.push_str("\",\n \"entity_types\": [\n");
1553 for (i, et) in snapshot.entity_types.iter().enumerate() {
1554 if i > 0 {
1555 out.push(',');
1556 }
1557 out.push_str(" {\n");
1558 out.push_str(&format!(
1559 " \"type_name\": \"{}\",\n",
1560 et.type_name.replace('"', "\\\"")
1561 ));
1562 out.push_str(&format!(
1563 " \"table_name\": \"{}\",\n",
1564 et.table_name.replace('"', "\\\"")
1565 ));
1566 out.push_str(" \"columns\": [\n");
1567 for (j, col) in et.columns.iter().enumerate() {
1568 if j > 0 {
1569 out.push(',');
1570 }
1571 out.push_str(&format!(
1572 " {{\"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",
1573 col.field_name.replace('"', "\\\""),
1574 col.column_name.replace('"', "\\\""),
1575 col.type_name.replace('"', "\\\""),
1576 col.is_primary_key,
1577 col.is_required,
1578 col.is_foreign_key,
1579 col.max_length.map(|n| n.to_string()).unwrap_or_else(|| "null".into()),
1580 col.is_auto_increment,
1581 col.is_sequence,
1582 col.sequence_name
1583 .as_ref()
1584 .map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
1585 .unwrap_or_else(|| "null".into()),
1586 col.fk_referenced_table
1587 .as_ref()
1588 .map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
1589 .unwrap_or_else(|| "null".into()),
1590 col.fk_referenced_column
1591 .as_ref()
1592 .map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
1593 .unwrap_or_else(|| "null".into()),
1594 col.has_index,
1595 col.is_unique,
1596 col.fk_on_delete
1597 .as_ref()
1598 .map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
1599 .unwrap_or_else(|| "null".into())
1600 ));
1601 }
1602 out.push_str(" ]\n }\n");
1603 }
1604 out.push_str(" ]\n}\n");
1605 out
1606}
1607
1608fn snapshot_from_json(text: &str) -> EFResult<Option<ModelSnapshot>> {
1609 let migration_id =
1611 extract_json_string(text, "migration_id").unwrap_or_else(|| "__snapshot__".to_string());
1612 let mut entity_types = Vec::new();
1613 if let Some(arr_start) = text.find("\"entity_types\"") {
1614 let slice = &text[arr_start..];
1615 for table_block in slice.split("\"table_name\"").skip(1) {
1616 let table_name = extract_quoted_after_colon(table_block).unwrap_or_default();
1617 let _type_name = entity_types.len().to_string(); let columns_start = table_block.find("\"columns\"");
1619 let mut columns = Vec::new();
1620 if let Some(cs) = columns_start {
1621 let col_slice = &table_block[cs..];
1622 for col_chunk in col_slice.split("\"field_name\"").skip(1) {
1623 if let Some(field_name) = extract_quoted_after_colon(col_chunk) {
1624 let column_name = extract_json_string(col_chunk, "column_name")
1625 .unwrap_or_else(|| field_name.clone());
1626 let type_name_col = extract_json_string(col_chunk, "type_name")
1627 .unwrap_or_else(|| "String".to_string());
1628 columns.push(SnapshotColumn {
1629 field_name,
1630 column_name,
1631 type_name: type_name_col,
1632 is_primary_key: col_chunk.contains("\"is_primary_key\":true"),
1633 is_required: col_chunk.contains("\"is_required\":true"),
1634 is_foreign_key: col_chunk.contains("\"is_foreign_key\":true"),
1635 max_length: None,
1636 is_auto_increment: col_chunk.contains("\"is_auto_increment\":true"),
1637 is_sequence: col_chunk.contains("\"is_sequence\":true"),
1638 sequence_name: extract_json_string(col_chunk, "sequence_name"),
1639 fk_referenced_table: extract_json_string(
1640 col_chunk,
1641 "fk_referenced_table",
1642 ),
1643 fk_referenced_column: extract_json_string(
1644 col_chunk,
1645 "fk_referenced_column",
1646 ),
1647 has_index: col_chunk.contains("\"has_index\":true"),
1648 is_unique: col_chunk.contains("\"is_unique\":true"),
1649 fk_on_delete: extract_json_string(col_chunk, "fk_on_delete"),
1650 });
1651 }
1652 }
1653 }
1654 if !table_name.is_empty() {
1655 entity_types.push(SnapshotEntityType {
1656 type_name: table_name.clone(),
1657 table_name,
1658 columns,
1659 });
1660 }
1661 }
1662 }
1663 if entity_types.is_empty() {
1664 return Ok(None);
1665 }
1666 Ok(Some(ModelSnapshot {
1667 migration_id,
1668 entity_types,
1669 }))
1670}
1671
1672fn extract_json_string(haystack: &str, key: &str) -> Option<String> {
1673 let needle = format!("\"{key}\"");
1674 let pos = haystack.find(&needle)?;
1675 extract_quoted_after_colon(&haystack[pos + needle.len()..])
1676}
1677
1678fn extract_quoted_after_colon(s: &str) -> Option<String> {
1679 let colon = s.find(':')?;
1680 let rest = s[colon + 1..].trim_start();
1681 if !rest.starts_with('"') {
1682 return None;
1683 }
1684 let inner = &rest[1..];
1685 let end = inner.find('"')?;
1686 Some(inner[..end].to_string())
1687}