Skip to main content

rust_ef/migration/
engine.rs

1//! `MigrationEngine` — model snapshot diffing and migration generation.
2
3use super::dialect::MigrationDialect;
4use super::diff::{
5    columns_structurally_equal, diff_foreign_keys, diff_indexes, fk_reference_for_property,
6};
7use super::schema_change::SchemaChange;
8use super::types::{Migration, ModelSnapshot, SnapshotColumn, SnapshotEntityType};
9use crate::error::EFResult;
10use crate::metadata::EntityTypeMeta;
11use std::collections::{HashMap, HashSet};
12
13/// The migration engine — compares old and new model snapshots to generate
14/// migration SQL.
15pub struct MigrationEngine {
16    pub(crate) dialect: MigrationDialect,
17}
18
19impl MigrationEngine {
20    pub fn new(dialect: MigrationDialect) -> Self {
21        Self { dialect }
22    }
23
24    /// Generates a migration by diffing the current model against a snapshot.
25    pub fn generate(
26        &self,
27        name: &str,
28        current: &[EntityTypeMeta],
29        previous_snapshot: &Option<ModelSnapshot>,
30    ) -> EFResult<Migration> {
31        let current_snapshot = self.create_snapshot("__current__", current);
32
33        let changes = match previous_snapshot {
34            Some(prev) => self.diff(prev, &current_snapshot),
35            None => self.initial_create_with_fks(&current_snapshot),
36        };
37
38        let up_sql = self.generate_up_sql(&changes);
39        let down_sql = self.generate_down_sql(&changes);
40
41        Ok(Migration {
42            id: name.to_string(),
43            description: name.to_string(),
44            up_sql,
45            down_sql,
46        })
47    }
48
49    /// Creates a snapshot from current entity type metadata.
50    pub fn create_snapshot(
51        &self,
52        migration_id: &str,
53        entity_types: &[EntityTypeMeta],
54    ) -> ModelSnapshot {
55        let types = entity_types
56            .iter()
57            .map(|et| SnapshotEntityType {
58                type_name: et.type_name.to_string(),
59                table_name: et.table_name.to_string(),
60                columns: et
61                    .properties
62                    .iter()
63                    .filter(|p| !p.is_not_mapped)
64                    .map(|p| {
65                        let (fk_table, fk_col, fk_on_delete) =
66                            fk_reference_for_property(et, p.column_name.as_ref());
67                        SnapshotColumn {
68                            field_name: p.field_name.to_string(),
69                            column_name: p.column_name.to_string(),
70                            type_name: p.type_name.to_string(),
71                            is_primary_key: p.is_primary_key,
72                            is_required: p.is_required,
73                            is_foreign_key: p.is_foreign_key,
74                            max_length: p.max_length,
75                            is_auto_increment: p.is_auto_increment,
76                            is_sequence: p.is_sequence,
77                            sequence_name: p.sequence_name.as_ref().map(|s| s.to_string()),
78                            fk_referenced_table: fk_table,
79                            fk_referenced_column: fk_col,
80                            has_index: p.has_index,
81                            is_unique: p.is_unique,
82                            fk_on_delete,
83                        }
84                    })
85                    .collect(),
86            })
87            .collect();
88
89        ModelSnapshot {
90            migration_id: migration_id.to_string(),
91            entity_types: types,
92        }
93    }
94
95    // -----------------------------------------------------------------------
96    // Diffing
97    // -----------------------------------------------------------------------
98
99    pub(crate) fn initial_create(&self, current: &ModelSnapshot) -> Vec<SchemaChange> {
100        current
101            .entity_types
102            .iter()
103            .map(|et| SchemaChange::CreateTable {
104                table: et.table_name.clone(),
105                columns: et.columns.clone(),
106            })
107            .collect()
108    }
109
110    fn diff(&self, old: &ModelSnapshot, new: &ModelSnapshot) -> Vec<SchemaChange> {
111        let mut changes = Vec::new();
112
113        // Build lookup maps by table name
114        let old_tables: HashMap<&str, &SnapshotEntityType> = old
115            .entity_types
116            .iter()
117            .map(|e| (e.table_name.as_str(), e))
118            .collect();
119        let new_tables: HashMap<&str, &SnapshotEntityType> = new
120            .entity_types
121            .iter()
122            .map(|e| (e.table_name.as_str(), e))
123            .collect();
124
125        let old_names: HashSet<&str> = old_tables.keys().copied().collect();
126        let new_names: HashSet<&str> = new_tables.keys().copied().collect();
127
128        // Dropped tables
129        for name in old_names.difference(&new_names) {
130            changes.push(SchemaChange::DropTable {
131                table: name.to_string(),
132            });
133        }
134
135        // New tables
136        for name in new_names.difference(&old_names) {
137            let et = new_tables[name];
138            changes.push(SchemaChange::CreateTable {
139                table: et.table_name.clone(),
140                columns: et.columns.clone(),
141            });
142            Self::append_create_table_fks(&mut changes, &et.table_name, &et.columns);
143            Self::append_create_table_indexes(&mut changes, &et.table_name, &et.columns);
144        }
145
146        // Tables present in both — compare columns
147        for name in old_names.intersection(&new_names) {
148            let old_et = old_tables[name];
149            let new_et = new_tables[name];
150            let table = &old_et.table_name;
151
152            let old_cols: HashMap<&str, &SnapshotColumn> = old_et
153                .columns
154                .iter()
155                .map(|c| (c.column_name.as_str(), c))
156                .collect();
157            let new_cols: HashMap<&str, &SnapshotColumn> = new_et
158                .columns
159                .iter()
160                .map(|c| (c.column_name.as_str(), c))
161                .collect();
162
163            let old_col_names: HashSet<&str> = old_cols.keys().copied().collect();
164            let new_col_names: HashSet<&str> = new_cols.keys().copied().collect();
165
166            // Added columns (with indexes if configured)
167            for col_name in new_col_names.difference(&old_col_names) {
168                let col = new_cols[col_name];
169                changes.push(SchemaChange::AddColumn {
170                    table: table.clone(),
171                    column: (*col).clone(),
172                });
173                changes.extend(diff_indexes(table, &SnapshotColumn::default(), col));
174            }
175
176            // Foreign keys (after add-column, before drop-column)
177            changes.extend(diff_foreign_keys(table, old_et, new_et));
178
179            // Dropped columns
180            for col_name in old_col_names.difference(&new_col_names) {
181                changes.push(SchemaChange::DropColumn {
182                    table: table.clone(),
183                    column_name: col_name.to_string(),
184                });
185            }
186
187            // Altered columns and index changes (present in both)
188            for col_name in old_col_names.intersection(&new_col_names) {
189                let old_col = old_cols[col_name];
190                let new_col = new_cols[col_name];
191
192                // Index state changes → CreateIndex/DropIndex
193                changes.extend(diff_indexes(table, old_col, new_col));
194
195                // Structural changes (excluding index fields) → AlterColumn
196                if !columns_structurally_equal(old_col, new_col) {
197                    changes.push(SchemaChange::AlterColumn {
198                        table: table.clone(),
199                        column_name: col_name.to_string(),
200                        old: Box::new((*old_col).clone()),
201                        new: Box::new((*new_col).clone()),
202                    });
203                }
204            }
205        }
206
207        changes
208    }
209
210    pub(super) fn append_create_table_fks(
211        changes: &mut Vec<SchemaChange>,
212        table: &str,
213        columns: &[SnapshotColumn],
214    ) {
215        for col in columns {
216            if let Some((ref_table, ref_col)) = super::diff::fk_target(col) {
217                changes.push(SchemaChange::AddForeignKey {
218                    table: table.to_string(),
219                    column: col.column_name.clone(),
220                    referenced_table: ref_table,
221                    referenced_column: ref_col,
222                    on_delete: col.fk_on_delete.clone(),
223                });
224            }
225        }
226    }
227
228    pub(super) fn append_create_table_indexes(
229        changes: &mut Vec<SchemaChange>,
230        table: &str,
231        columns: &[SnapshotColumn],
232    ) {
233        for col in columns {
234            if col.is_unique {
235                changes.push(SchemaChange::CreateIndex {
236                    table: table.to_string(),
237                    column: col.column_name.clone(),
238                    is_unique: true,
239                });
240            } else if col.has_index {
241                changes.push(SchemaChange::CreateIndex {
242                    table: table.to_string(),
243                    column: col.column_name.clone(),
244                    is_unique: false,
245                });
246            }
247        }
248    }
249}