rorm_cli/make_migrations/
mod.rs1use 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#[derive(Debug)]
23pub struct MakeMigrationsOptions {
24 pub models_file: String,
26 pub migration_dir: String,
28 pub name: Option<String>,
30 pub non_interactive: bool,
32 pub warnings_disabled: bool,
34}
35
36pub 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
62pub 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
74pub 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 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 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 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 let mut altered_fields: HashMap<String, Vec<(&Field, &Field)>> = HashMap::new();
132
133 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 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 for new_model in &internal_models.models {
150 let Some(old_model) = old_lookup.get(&new_model.name) else {
151 continue;
152 };
153
154 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 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 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 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 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 for (old, new) in &renamed_models {
216 new_models.retain(|x| x != new);
217 deleted_models.retain(|x| x != old);
218
219 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 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 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 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 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 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 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 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 for (model, af) in &altered_fields {
347 for (old, new) in af {
348 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 (_, _) => {
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 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 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 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 internal_models.models.is_empty() {
446 info!("No models found.");
447 } 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 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 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}