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};
10
11use crate::linter;
12use crate::utils::migrations::{
13    convert_migration_to_file, convert_migrations_to_internal_models, get_existing_migrations,
14};
15use crate::utils::question;
16use crate::utils::re::RE;
17
18/// Options struct for [run_make_migrations]
19#[derive(Debug)]
20pub struct MakeMigrationsOptions {
21    /// Path to internal model file
22    pub models_file: String,
23    /// Path to the migration directory
24    pub migration_dir: String,
25    /// Alternative name of the migration
26    pub name: Option<String>,
27    /// If set, no questions are gonna be asked
28    pub non_interactive: bool,
29    /// If set, all warnings are suppressed
30    pub warnings_disabled: bool,
31}
32
33/**
34Checks the options
35*/
36pub fn check_options(options: &MakeMigrationsOptions) -> anyhow::Result<()> {
37    let models_file = Path::new(options.models_file.as_str());
38    if !models_file.exists() || !models_file.is_file() {
39        return Err(anyhow!("Models file does not exist"));
40    }
41
42    let migration_dir = Path::new(options.migration_dir.as_str());
43    if migration_dir.is_file() {
44        return Err(anyhow!("Migration directory cannot be created, is a file"));
45    }
46    if !migration_dir.exists() {
47        create_dir_all(migration_dir).with_context(|| "Couldn't create migration directory")?;
48    }
49
50    if let Some(name) = &options.name {
51        if !RE.migration_allowed_comment.is_match(name.as_str()) {
52            return Err(anyhow!(
53                "Custom migration name contains illegal characters!"
54            ));
55        }
56    }
57
58    Ok(())
59}
60
61/**
62A helper function to retrieve the internal models from a given location.
63
64`models_file`: [&str]: The path to the models file.
65*/
66pub fn get_internal_models(models_file: &str) -> anyhow::Result<InternalModelFormat> {
67    let internal_str = read_to_string(Path::new(&models_file))
68        .with_context(|| "Couldn't read internal models file")?;
69    let internal: InternalModelFormat = serde_json::from_str(internal_str.as_str())
70        .with_context(|| "Error deserializing internal models file")?;
71
72    Ok(internal)
73}
74
75/**
76Runs the make-migrations tool
77*/
78pub fn run_make_migrations(options: MakeMigrationsOptions) -> anyhow::Result<()> {
79    check_options(&options).with_context(|| "Error while checking options")?;
80
81    let internal_models = get_internal_models(&options.models_file)
82        .with_context(|| "Couldn't retrieve internal model files.")?;
83
84    linter::check_internal_models(&internal_models).with_context(|| "Model checks failed.")?;
85
86    let existing_migrations = get_existing_migrations(&options.migration_dir)
87        .with_context(|| "An error occurred while deserializing migrations")?;
88
89    let mut hasher = DefaultHasher::new();
90    internal_models.hash(&mut hasher);
91    let h = hasher.finish();
92
93    let mut new_migration = None;
94
95    if !existing_migrations.is_empty() {
96        let last_migration = &existing_migrations[existing_migrations.len() - 1];
97
98        // If hash matches with the one of the current models, exiting
99        if last_migration.hash == h.to_string() {
100            println!("No changes - nothing to do.");
101            return Ok(());
102        }
103
104        let constructed = convert_migrations_to_internal_models(&existing_migrations)
105            .with_context(|| "Error while parsing existing migration files")?;
106
107        let last_id: u16 = last_migration.id + 1;
108        let name = options.name.as_ref().map_or("placeholder", |x| x.as_str());
109
110        let mut op: Vec<Operation> = vec![];
111
112        let old_lookup: HashMap<String, &Model> = constructed
113            .models
114            .iter()
115            .map(|x| (x.name.clone(), x))
116            .collect();
117
118        let new_lookup: HashMap<String, &Model> = internal_models
119            .models
120            .iter()
121            .map(|x| (x.name.clone(), x))
122            .collect();
123
124        // Old -> New
125        let mut renamed_models: Vec<(&Model, &Model)> = vec![];
126        let mut new_models: Vec<&Model> = vec![];
127        let mut deleted_models: Vec<&Model> = vec![];
128
129        // Mapping: Model name -> (Old field name, New field name)
130        let mut renamed_fields: HashMap<String, Vec<(&Field, &Field)>> = HashMap::new();
131        let mut new_fields: HashMap<String, Vec<&Field>> = HashMap::new();
132        let mut deleted_fields: HashMap<String, Vec<&Field>> = HashMap::new();
133        // Mapping: Model name -> (Old field, new field)
134        let mut altered_fields: HashMap<String, Vec<(&Field, &Field)>> = HashMap::new();
135
136        // Check if any new models exist
137        internal_models.models.iter().for_each(|x| {
138            if !old_lookup.iter().any(|(a, _)| x.name == *a) {
139                new_models.push(x);
140            }
141        });
142
143        // Check if any old model got deleted
144        constructed.models.iter().for_each(|x| {
145            if !new_lookup.iter().any(|(a, _)| x.name == *a) {
146                deleted_models.push(x);
147            }
148        });
149
150        // Iterate over all models, that are in the constructed
151        // as well as in the new internal models
152        internal_models
153            .models
154            .iter()
155            .filter(|x| old_lookup.contains_key(x.name.as_str()))
156            .for_each(|x| {
157                // Check if a new field has been added
158                x.fields.iter().for_each(|y| {
159                    if !old_lookup[x.name.as_str()]
160                        .fields
161                        .iter()
162                        .any(|z| z.name == y.name)
163                    {
164                        if !new_fields.contains_key(x.name.as_str()) {
165                            new_fields.insert(x.name.clone(), vec![]);
166                        }
167                        new_fields.get_mut(x.name.as_str()).unwrap().push(y);
168                    }
169                });
170
171                // Check if a existing field got deleted
172                old_lookup[x.name.as_str()].fields.iter().for_each(|y| {
173                    if !x.fields.iter().any(|z| z.name == y.name) {
174                        if !deleted_fields.contains_key(x.name.as_str()) {
175                            deleted_fields.insert(x.name.clone(), vec![]);
176                        }
177                        deleted_fields.get_mut(x.name.as_str()).unwrap().push(y);
178                    }
179                });
180
181                // Check if a existing field got altered
182                old_lookup[x.name.as_str()].fields.iter().for_each(|y| {
183                    x.fields.iter().filter(|z| y.name == z.name).for_each(|z| {
184                        // Check for differences
185                        if y.db_type != z.db_type || y.annotations != z.annotations {
186                            if !altered_fields.contains_key(x.name.as_str()) {
187                                altered_fields.insert(x.name.clone(), vec![]);
188                            }
189                            altered_fields.get_mut(&x.name).unwrap().push((y, z));
190                        }
191                    });
192                });
193            });
194
195        // Check if a model was renamed
196        if !new_models.is_empty() && !deleted_models.is_empty() {
197            for x in &new_models {
198                for y in &deleted_models {
199                    if x.fields == y.fields
200                        && question(
201                            format!("Did you rename the model {} to {}?", &y.name, &x.name)
202                                .as_str(),
203                        )
204                    {
205                        println!("Renamed model {} to {}.", &y.name, &x.name);
206                        renamed_models.push((y, x));
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        new_models.iter().for_each(|x| {
227            let mut normal_fields = vec![];
228
229            for y in &x.fields {
230                if y.annotations
231                    .iter()
232                    .any(|z| z.eq_shallow(&Annotation::ForeignKey(Default::default())))
233                {
234                    references
235                        .entry(x.name.clone())
236                        .or_default()
237                        .push(y.clone());
238                } else {
239                    normal_fields.push(y.clone());
240                }
241            }
242
243            op.push(Operation::CreateModel {
244                name: x.name.clone(),
245                fields: normal_fields,
246            });
247            println!("Created model {}", x.name);
248        });
249
250        // Create referencing fields for new models
251        for (model, fields) in references {
252            for field in fields {
253                op.push(Operation::CreateField {
254                    model: model.clone(),
255                    field,
256                });
257            }
258        }
259
260        // Create migration operations for deleted models
261        deleted_models.iter().for_each(|x| {
262            op.push(Operation::DeleteModel {
263                name: x.name.clone(),
264            });
265            println!("Deleted model {}", x.name);
266        });
267
268        for (x, new_fields) in &new_fields {
269            if let Some(old_fields) = deleted_fields.get(x) {
270                for new_field in new_fields {
271                    for old_field in old_fields {
272                        if new_field.db_type == old_field.db_type
273                            && new_field.annotations == old_field.annotations
274                            && question(
275                                format!(
276                                    "Did you rename the field {} of model {} to {}?",
277                                    &old_field.name, &x, &new_field.name
278                                )
279                                .as_str(),
280                            )
281                        {
282                            if !renamed_fields.contains_key(x) {
283                                renamed_fields.insert(x.clone(), vec![]);
284                            }
285                            let f = renamed_fields.get_mut(x).unwrap();
286                            f.push((old_field, new_field));
287                            println!(
288                                "Renamed field {} of model {} to {}.",
289                                &new_field.name, &x, &old_field.name
290                            );
291                        }
292                    }
293                }
294            }
295        }
296        // Remove renamed fields in existing models from new and deleted lists
297        renamed_fields.iter().for_each(|(model_name, 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        new_fields.iter().for_each(|(x, y)| {
319            y.iter().for_each(|z| {
320                op.push(Operation::CreateField {
321                    model: x.clone(),
322                    field: (*z).clone(),
323                });
324                println!("Added field {} to model {}", z.name, x);
325            })
326        });
327
328        // Create migration operations for deleted fields in existing models
329        deleted_fields.iter().for_each(|(x, y)| {
330            y.iter().for_each(|z| {
331                op.push(Operation::DeleteField {
332                    model: x.clone(),
333                    name: z.name.clone(),
334                });
335                println!("Deleted field {} from model {}", z.name, x);
336            })
337        });
338
339        // Create migration operations for altered fields in existing models
340        altered_fields.iter().for_each(|(model, af)| {
341            af.iter().for_each(|(old, new)| {
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                            println!("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                    println!("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            println!("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(|x| {
397                let mut normal_fields = vec![];
398
399                for y in &x.fields {
400                    if y.annotations
401                        .iter()
402                        .any(|z| z.eq_shallow(&Annotation::ForeignKey(Default::default())))
403                    {
404                        references
405                            .entry(x.name.clone())
406                            .or_default()
407                            .push(y.clone());
408                    } else {
409                        normal_fields.push(y.clone());
410                    }
411                }
412
413                let o = Operation::CreateModel {
414                    name: x.name.clone(),
415                    fields: normal_fields,
416                };
417                println!("Created model {}", x.name);
418                o
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    println!("Done.");
455
456    Ok(())
457}