rorm_cli/make_migrations/
mod.rs1use 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#[derive(Debug)]
20pub struct MakeMigrationsOptions {
21 pub models_file: String,
23 pub migration_dir: String,
25 pub name: Option<String>,
27 pub non_interactive: bool,
29 pub warnings_disabled: bool,
31}
32
33pub 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
61pub 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
75pub 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 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 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 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 let mut altered_fields: HashMap<String, Vec<(&Field, &Field)>> = HashMap::new();
135
136 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 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 internal_models
153 .models
154 .iter()
155 .filter(|x| old_lookup.contains_key(x.name.as_str()))
156 .for_each(|x| {
157 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 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 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 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 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 for (old, new) in &renamed_models {
213 new_models.retain(|x| x != new);
214 deleted_models.retain(|x| x != old);
215
216 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 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 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 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 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 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 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 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 altered_fields.iter().for_each(|(model, af)| {
341 af.iter().for_each(|(old, new)| {
342 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 (_, _) => {
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 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 internal_models.models.is_empty() {
390 println!("No models found.");
391 } 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 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}