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                    // Check for differences
176                    if old_field.db_type != new_field.db_type
177                        || old_field.annotations != new_field.annotations
178                    {
179                        altered_fields
180                            .entry(new_model.name.clone())
181                            .or_default()
182                            .push((old_field, new_field));
183                    }
184                }
185            }
186        }
187
188        // Check if a model was renamed
189        if !new_models.is_empty() && !deleted_models.is_empty() {
190            for new_model in &new_models {
191                for old_model in &deleted_models {
192                    if new_model.fields == old_model.fields
193                        && question(
194                            format!(
195                                "Did you rename the model {} to {}?",
196                                old_model.name, new_model.name
197                            )
198                            .as_str(),
199                        )
200                    {
201                        info!("Renamed model {} to {}.", old_model.name, new_model.name);
202                        renamed_models.push((old_model, new_model));
203                    }
204                }
205            }
206        }
207        // Remove renamed models from new and deleted lists
208        for (old, new) in &renamed_models {
209            new_models.retain(|x| x != new);
210            deleted_models.retain(|x| x != old);
211
212            // Create migration operations for renamed models
213            op.push(Operation::RenameModel {
214                old: old.name.clone(),
215                new: new.name.clone(),
216            })
217        }
218
219        let mut references: HashMap<String, Vec<Field>> = HashMap::new();
220
221        // Create migration operations for new models
222        for new_model in &new_models {
223            let mut normal_fields = vec![];
224
225            for new_field in &new_model.fields {
226                if new_field
227                    .annotations
228                    .iter()
229                    .any(|x| matches!(x, Annotation::ForeignKey(_)))
230                {
231                    references
232                        .entry(new_model.name.clone())
233                        .or_default()
234                        .push(new_field.clone());
235                } else {
236                    normal_fields.push(new_field.clone());
237                }
238            }
239
240            op.push(Operation::CreateModel {
241                name: new_model.name.clone(),
242                fields: normal_fields,
243            });
244            info!("Created model {}", new_model.name);
245        }
246
247        // Create referencing fields for new models
248        for (model, fields) in references {
249            for field in fields {
250                op.push(Operation::CreateField {
251                    model: model.clone(),
252                    field,
253                });
254            }
255        }
256
257        // Create migration operations for deleted models
258        for deleted_model in &deleted_models {
259            op.push(Operation::DeleteModel {
260                name: deleted_model.name.clone(),
261            });
262            info!("Deleted model {}", deleted_model.name);
263        }
264
265        for (model_name, new_fields) in &new_fields {
266            if let Some(old_fields) = deleted_fields.get(model_name) {
267                for new_field in new_fields {
268                    for old_field in old_fields {
269                        if new_field.db_type == old_field.db_type
270                            && new_field.annotations == old_field.annotations
271                            && question(
272                                format!(
273                                    "Did you rename the field {} of model {model_name} to {}?",
274                                    old_field.name, new_field.name
275                                )
276                                .as_str(),
277                            )
278                        {
279                            renamed_fields
280                                .entry(model_name.clone())
281                                .or_default()
282                                .push((old_field, new_field));
283                            info!(
284                                "Renamed field {} of model {model_name} to {}.",
285                                old_field.name, new_field.name
286                            );
287                        }
288                    }
289                }
290            }
291        }
292        // Remove renamed fields in existing models from new and deleted lists
293        for (model_name, fields) in &renamed_fields {
294            for (old_field, new_field) in fields {
295                new_fields
296                    .get_mut(model_name)
297                    .unwrap()
298                    .retain(|x| x.name != new_field.name);
299                deleted_fields
300                    .get_mut(model_name)
301                    .unwrap()
302                    .retain(|x| x.name != old_field.name);
303
304                // Create migration operation for renamed fields on existing models
305                op.push(Operation::RenameField {
306                    table_name: model_name.clone(),
307                    old: old_field.name.clone(),
308                    new: new_field.name.clone(),
309                })
310            }
311        }
312
313        // Create migration operations for new fields in existing models
314        for (model_name, fields) in &new_fields {
315            for field in fields {
316                op.push(Operation::CreateField {
317                    model: model_name.clone(),
318                    field: (*field).clone(),
319                });
320                info!("Added field {} to model {}", field.name, model_name);
321            }
322        }
323
324        // Create migration operations for deleted fields in existing models
325        for (model_name, fields) in &deleted_fields {
326            for field in fields {
327                op.push(Operation::DeleteField {
328                    model: model_name.clone(),
329                    name: field.name.clone(),
330                });
331                info!("Deleted field {} from model {}", field.name, model_name);
332            }
333        }
334
335        // Create migration operations for altered fields in existing models
336        for (model, af) in &altered_fields {
337            for (old, new) in af {
338                // Check datatype
339                if old.db_type != new.db_type {
340                    #[expect(clippy::match_single_binding, reason = "It will be extended™")]
341                    match (old.db_type, new.db_type) {
342                        // TODO:
343                        // There are cases where columns can be altered
344                        // e.g. i8 -> i16 or float -> double
345
346                        // Default case
347                        (_, _) => {
348                            op.push(Operation::DeleteField {
349                                model: model.clone(),
350                                name: old.name.clone(),
351                            });
352                            op.push(Operation::CreateField {
353                                model: model.clone(),
354                                field: (*new).clone(),
355                            });
356                            info!("Recreated field {} on model {}", &new.name, &model);
357                        }
358                    }
359                } else {
360                    // As the datatypes match, there must be a change in the annotations
361                    op.push(Operation::DeleteField {
362                        model: model.clone(),
363                        name: old.name.clone(),
364                    });
365                    op.push(Operation::CreateField {
366                        model: model.clone(),
367                        field: (*new).clone(),
368                    });
369                    info!("Recreated field {} on model {}", &new.name, &model);
370                }
371            }
372        }
373
374        new_migration = Some(Migration {
375            hash: h.to_string(),
376            initial: false,
377            id: last_id,
378            name: name.to_string(),
379            dependency: Some(last_migration.id),
380            replaces: vec![],
381            operations: op,
382        });
383    } else {
384        // If there are no models yet, no migrations must be created
385        if internal_models.models.is_empty() {
386            info!("No models found.");
387        // New migration must be generated as no migration exists
388        } else {
389            let mut operations = vec![];
390            let mut references: HashMap<String, Vec<Field>> = HashMap::new();
391
392            operations.extend(internal_models.models.iter().map(|model| {
393                let mut normal_fields = vec![];
394
395                for field in &model.fields {
396                    if field
397                        .annotations
398                        .iter()
399                        .any(|x| matches!(x, Annotation::ForeignKey(_)))
400                    {
401                        references
402                            .entry(model.name.clone())
403                            .or_default()
404                            .push(field.clone());
405                    } else {
406                        normal_fields.push(field.clone());
407                    }
408                }
409
410                info!("Created model {}", model.name);
411                Operation::CreateModel {
412                    name: model.name.clone(),
413                    fields: normal_fields,
414                }
415            }));
416
417            operations.extend(references.into_iter().flat_map(|(model, fields)| {
418                fields
419                    .iter()
420                    .map(|field| Operation::CreateField {
421                        model: model.clone(),
422                        field: field.clone(),
423                    })
424                    .collect::<Vec<Operation>>()
425            }));
426
427            new_migration = Some(Migration {
428                hash: h.to_string(),
429                initial: true,
430                id: 1,
431                name: match &options.name {
432                    None => "initial".to_string(),
433                    Some(n) => n.clone(),
434                },
435                dependency: None,
436                replaces: vec![],
437                operations,
438            });
439        }
440    }
441
442    if let Some(migration) = new_migration {
443        // Write migration to disk
444        let path = Path::new(options.migration_dir.as_str())
445            .join(format!("{:04}_{}.toml", migration.id, &migration.name));
446        convert_migration_to_file(migration, &path)
447            .with_context(|| "Error occurred while converting migration to file")?;
448    }
449
450    info!("Done.");
451
452    Ok(())
453}