Skip to main content

rorm_cli/make_migrations/
mod.rs

1use std::collections::hash_map::DefaultHasher;
2use std::collections::HashMap;
3use std::fs::{create_dir_all, read_to_string};
4use std::hash::{Hash, Hasher};
5use std::path::Path;
6
7use anyhow::{anyhow, Context};
8use rorm_declaration::imr::{Annotation, Field, InternalModelFormat, Model};
9use rorm_declaration::migration::{Migration, Operation};
10use tracing::info;
11
12use crate::linter;
13use crate::utils::migrations::{
14    convert_migration_to_file, convert_migrations_to_internal_models, get_existing_migrations,
15};
16use crate::utils::question;
17use crate::utils::re::RE;
18
19/// Options struct for [run_make_migrations]
20#[derive(Debug)]
21pub struct MakeMigrationsOptions {
22    /// Path to internal model file
23    pub models_file: String,
24    /// Path to the migration directory
25    pub migration_dir: String,
26    /// Alternative name of the migration
27    pub name: Option<String>,
28    /// If set, no questions are gonna be asked
29    pub non_interactive: bool,
30    /// If set, all warnings are suppressed
31    pub warnings_disabled: bool,
32}
33
34/// Checks the options
35pub fn check_options(options: &MakeMigrationsOptions) -> anyhow::Result<()> {
36    let models_file = Path::new(options.models_file.as_str());
37    if !models_file.exists() || !models_file.is_file() {
38        return Err(anyhow!("Models file does not exist"));
39    }
40
41    let migration_dir = Path::new(options.migration_dir.as_str());
42    if migration_dir.is_file() {
43        return Err(anyhow!("Migration directory cannot be created, is a file"));
44    }
45    if !migration_dir.exists() {
46        create_dir_all(migration_dir).with_context(|| "Couldn't create migration directory")?;
47    }
48
49    if let Some(name) = &options.name {
50        if !RE.migration_allowed_comment.is_match(name.as_str()) {
51            return Err(anyhow!(
52                "Custom migration name contains illegal characters!"
53            ));
54        }
55    }
56
57    Ok(())
58}
59
60/// A helper function to retrieve the internal models from a given location.
61///
62/// `models_file`: [&str]: The path to the models file.
63pub fn get_internal_models(models_file: &str) -> anyhow::Result<InternalModelFormat> {
64    let internal_str = read_to_string(Path::new(&models_file))
65        .with_context(|| "Couldn't read internal models file")?;
66    let internal: InternalModelFormat = serde_json::from_str(internal_str.as_str())
67        .with_context(|| "Error deserializing internal models file")?;
68
69    Ok(internal)
70}
71
72/// Runs the make-migrations tool
73pub fn run_make_migrations(options: MakeMigrationsOptions) -> anyhow::Result<()> {
74    check_options(&options).with_context(|| "Error while checking options")?;
75
76    let internal_models = get_internal_models(&options.models_file)
77        .with_context(|| "Couldn't retrieve internal model files.")?;
78
79    linter::check_internal_models(&internal_models).with_context(|| "Model checks failed.")?;
80
81    let existing_migrations = get_existing_migrations(&options.migration_dir)
82        .with_context(|| "An error occurred while deserializing migrations")?;
83
84    let mut hasher = DefaultHasher::new();
85    internal_models.hash(&mut hasher);
86    let h = hasher.finish();
87
88    let mut new_migration = None;
89
90    if !existing_migrations.is_empty() {
91        let last_migration = &existing_migrations[existing_migrations.len() - 1];
92
93        // If hash matches with the one of the current models, exiting
94        if last_migration.hash == h.to_string() {
95            info!("No changes - nothing to do.");
96            return Ok(());
97        }
98
99        let constructed = convert_migrations_to_internal_models(&existing_migrations)
100            .with_context(|| "Error while parsing existing migration files")?;
101
102        let last_id: u16 = last_migration.id + 1;
103        let name = options.name.as_deref().unwrap_or("placeholder");
104
105        let mut op: Vec<Operation> = vec![];
106
107        let old_lookup: HashMap<String, &Model> = constructed
108            .models
109            .iter()
110            .map(|x| (x.name.clone(), x))
111            .collect();
112
113        let new_lookup: HashMap<String, &Model> = internal_models
114            .models
115            .iter()
116            .map(|x| (x.name.clone(), x))
117            .collect();
118
119        // Old -> New
120        let mut renamed_models: Vec<(&Model, &Model)> = vec![];
121        let mut new_models: Vec<&Model> = vec![];
122        let mut deleted_models: Vec<&Model> = vec![];
123
124        // Mapping: Model name -> (Old field name, New field name)
125        let mut renamed_fields: HashMap<String, Vec<(&Field, &Field)>> = HashMap::new();
126        let mut new_fields: HashMap<String, Vec<&Field>> = HashMap::new();
127        let mut deleted_fields: HashMap<String, Vec<&Field>> = HashMap::new();
128        // Mapping: Model name -> (Old field, new field)
129        let mut altered_fields: HashMap<String, Vec<(&Field, &Field)>> = HashMap::new();
130
131        // Check if any new models exist
132        for new_model in &internal_models.models {
133            if !old_lookup.iter().any(|(a, _)| new_model.name == *a) {
134                new_models.push(new_model);
135            }
136        }
137
138        // Check if any old model got deleted
139        for old_model in &constructed.models {
140            if !new_lookup.iter().any(|(a, _)| old_model.name == *a) {
141                deleted_models.push(old_model);
142            }
143        }
144
145        // Iterate over all models, that are in the constructed
146        // as well as in the new internal models
147        for new_model in &internal_models.models {
148            let Some(old_model) = old_lookup.get(&new_model.name) else {
149                continue;
150            };
151
152            // Check if a new field has been added
153            for new_field in &new_model.fields {
154                if !old_model.fields.iter().any(|z| z.name == new_field.name) {
155                    new_fields
156                        .entry(new_model.name.clone())
157                        .or_default()
158                        .push(new_field);
159                }
160            }
161
162            // Check if a existing field got deleted
163            for old_field in &old_model.fields {
164                if !new_model.fields.iter().any(|z| z.name == old_field.name) {
165                    deleted_fields
166                        .entry(new_model.name.clone())
167                        .or_default()
168                        .push(old_field);
169                }
170            }
171
172            // Check if a existing field got altered
173            for old_field in &old_model.fields {
174                for new_field in &new_model.fields {
175                    if old_field.name != new_field.name {
176                        continue;
177                    }
178
179                    // Check for differences
180                    if old_field.db_type != new_field.db_type
181                        || old_field.annotations != new_field.annotations
182                    {
183                        altered_fields
184                            .entry(new_model.name.clone())
185                            .or_default()
186                            .push((old_field, new_field));
187                    }
188                }
189            }
190        }
191
192        // Check if a model was renamed
193        if !new_models.is_empty() && !deleted_models.is_empty() {
194            for new_model in &new_models {
195                for old_model in &deleted_models {
196                    if new_model.fields == old_model.fields
197                        && question(
198                            format!(
199                                "Did you rename the model {} to {}?",
200                                old_model.name, new_model.name
201                            )
202                            .as_str(),
203                        )
204                    {
205                        info!("Renamed model {} to {}.", old_model.name, new_model.name);
206                        renamed_models.push((old_model, new_model));
207                    }
208                }
209            }
210        }
211        // Remove renamed models from new and deleted lists
212        for (old, new) in &renamed_models {
213            new_models.retain(|x| x != new);
214            deleted_models.retain(|x| x != old);
215
216            // Create migration operations for renamed models
217            op.push(Operation::RenameModel {
218                old: old.name.clone(),
219                new: new.name.clone(),
220            })
221        }
222
223        let mut references: HashMap<String, Vec<Field>> = HashMap::new();
224
225        // Create migration operations for new models
226        for new_model in &new_models {
227            let mut normal_fields = vec![];
228
229            for new_field in &new_model.fields {
230                if new_field
231                    .annotations
232                    .iter()
233                    .any(|x| matches!(x, Annotation::ForeignKey(_)))
234                {
235                    references
236                        .entry(new_model.name.clone())
237                        .or_default()
238                        .push(new_field.clone());
239                } else {
240                    normal_fields.push(new_field.clone());
241                }
242            }
243
244            op.push(Operation::CreateModel {
245                name: new_model.name.clone(),
246                fields: normal_fields,
247            });
248            info!("Created model {}", new_model.name);
249        }
250
251        // Create referencing fields for new models
252        for (model, fields) in references {
253            for field in fields {
254                op.push(Operation::CreateField {
255                    model: model.clone(),
256                    field,
257                });
258            }
259        }
260
261        // Create migration operations for deleted models
262        for deleted_model in &deleted_models {
263            op.push(Operation::DeleteModel {
264                name: deleted_model.name.clone(),
265            });
266            info!("Deleted model {}", deleted_model.name);
267        }
268
269        for (model_name, new_fields) in &new_fields {
270            if let Some(old_fields) = deleted_fields.get(model_name) {
271                for new_field in new_fields {
272                    for old_field in old_fields {
273                        if new_field.db_type == old_field.db_type
274                            && new_field.annotations == old_field.annotations
275                            && question(
276                                format!(
277                                    "Did you rename the field {} of model {model_name} to {}?",
278                                    old_field.name, new_field.name
279                                )
280                                .as_str(),
281                            )
282                        {
283                            renamed_fields
284                                .entry(model_name.clone())
285                                .or_default()
286                                .push((old_field, new_field));
287                            info!(
288                                "Renamed field {} of model {model_name} to {}.",
289                                old_field.name, new_field.name
290                            );
291                        }
292                    }
293                }
294            }
295        }
296        // Remove renamed fields in existing models from new and deleted lists
297        for (model_name, fields) in &renamed_fields {
298            for (old_field, new_field) in fields {
299                new_fields
300                    .get_mut(model_name)
301                    .unwrap()
302                    .retain(|x| x.name != new_field.name);
303                deleted_fields
304                    .get_mut(model_name)
305                    .unwrap()
306                    .retain(|x| x.name != old_field.name);
307
308                // Create migration operation for renamed fields on existing models
309                op.push(Operation::RenameField {
310                    table_name: model_name.clone(),
311                    old: old_field.name.clone(),
312                    new: new_field.name.clone(),
313                })
314            }
315        }
316
317        // Create migration operations for new fields in existing models
318        for (model_name, fields) in &new_fields {
319            for field in fields {
320                op.push(Operation::CreateField {
321                    model: model_name.clone(),
322                    field: (*field).clone(),
323                });
324                info!("Added field {} to model {}", field.name, model_name);
325            }
326        }
327
328        // Create migration operations for deleted fields in existing models
329        for (model_name, fields) in &deleted_fields {
330            for field in fields {
331                op.push(Operation::DeleteField {
332                    model: model_name.clone(),
333                    name: field.name.clone(),
334                });
335                info!("Deleted field {} from model {}", field.name, model_name);
336            }
337        }
338
339        // Create migration operations for altered fields in existing models
340        for (model, af) in &altered_fields {
341            for (old, new) in af {
342                // Check datatype
343                if old.db_type != new.db_type {
344                    #[expect(clippy::match_single_binding, reason = "It will be extended™")]
345                    match (old.db_type, new.db_type) {
346                        // TODO:
347                        // There are cases where columns can be altered
348                        // e.g. i8 -> i16 or float -> double
349
350                        // Default case
351                        (_, _) => {
352                            op.push(Operation::DeleteField {
353                                model: model.clone(),
354                                name: old.name.clone(),
355                            });
356                            op.push(Operation::CreateField {
357                                model: model.clone(),
358                                field: (*new).clone(),
359                            });
360                            info!("Recreated field {} on model {}", &new.name, &model);
361                        }
362                    }
363                } else {
364                    // As the datatypes match, there must be a change in the annotations
365                    op.push(Operation::DeleteField {
366                        model: model.clone(),
367                        name: old.name.clone(),
368                    });
369                    op.push(Operation::CreateField {
370                        model: model.clone(),
371                        field: (*new).clone(),
372                    });
373                    info!("Recreated field {} on model {}", &new.name, &model);
374                }
375            }
376        }
377
378        new_migration = Some(Migration {
379            hash: h.to_string(),
380            initial: false,
381            id: last_id,
382            name: name.to_string(),
383            dependency: Some(last_migration.id),
384            replaces: vec![],
385            operations: op,
386        });
387    } else {
388        // If there are no models yet, no migrations must be created
389        if internal_models.models.is_empty() {
390            info!("No models found.");
391        // New migration must be generated as no migration exists
392        } else {
393            let mut operations = vec![];
394            let mut references: HashMap<String, Vec<Field>> = HashMap::new();
395
396            operations.extend(internal_models.models.iter().map(|model| {
397                let mut normal_fields = vec![];
398
399                for field in &model.fields {
400                    if field
401                        .annotations
402                        .iter()
403                        .any(|x| matches!(x, Annotation::ForeignKey(_)))
404                    {
405                        references
406                            .entry(model.name.clone())
407                            .or_default()
408                            .push(field.clone());
409                    } else {
410                        normal_fields.push(field.clone());
411                    }
412                }
413
414                info!("Created model {}", model.name);
415                Operation::CreateModel {
416                    name: model.name.clone(),
417                    fields: normal_fields,
418                }
419            }));
420
421            operations.extend(references.into_iter().flat_map(|(model, fields)| {
422                fields
423                    .iter()
424                    .map(|field| Operation::CreateField {
425                        model: model.clone(),
426                        field: field.clone(),
427                    })
428                    .collect::<Vec<Operation>>()
429            }));
430
431            new_migration = Some(Migration {
432                hash: h.to_string(),
433                initial: true,
434                id: 1,
435                name: match &options.name {
436                    None => "initial".to_string(),
437                    Some(n) => n.clone(),
438                },
439                dependency: None,
440                replaces: vec![],
441                operations,
442            });
443        }
444    }
445
446    if let Some(migration) = new_migration {
447        // Write migration to disk
448        let path = Path::new(options.migration_dir.as_str())
449            .join(format!("{:04}_{}.toml", migration.id, &migration.name));
450        convert_migration_to_file(migration, &path)
451            .with_context(|| "Error occurred while converting migration to file")?;
452    }
453
454    info!("Done.");
455
456    Ok(())
457}