Skip to main content

rorm_cli/make_migrations/
mod.rs

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