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};
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#[derive(Debug)]
21pub struct MakeMigrationsOptions {
22 pub models_file: String,
24 pub migration_dir: String,
26 pub name: Option<String>,
28 pub non_interactive: bool,
30 pub warnings_disabled: bool,
32}
33
34pub 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
60pub 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
72pub 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 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 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 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 let mut altered_fields: HashMap<String, Vec<(&Field, &Field)>> = HashMap::new();
130
131 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 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 for new_model in &internal_models.models {
148 let Some(old_model) = old_lookup.get(&new_model.name) else {
149 continue;
150 };
151
152 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 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 for old_field in &old_model.fields {
174 for new_field in &new_model.fields {
175 if old_field.name != new_field.name {
176 continue;
177 }
178
179 if old_field.db_type != new_field.db_type
181 || old_field.annotations != new_field.annotations
182 {
183 altered_fields
184 .entry(new_model.name.clone())
185 .or_default()
186 .push((old_field, new_field));
187 }
188 }
189 }
190 }
191
192 if !new_models.is_empty() && !deleted_models.is_empty() {
194 for new_model in &new_models {
195 for old_model in &deleted_models {
196 if new_model.fields == old_model.fields
197 && question(
198 format!(
199 "Did you rename the model {} to {}?",
200 old_model.name, new_model.name
201 )
202 .as_str(),
203 )
204 {
205 info!("Renamed model {} to {}.", old_model.name, new_model.name);
206 renamed_models.push((old_model, new_model));
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 for new_model in &new_models {
227 let mut normal_fields = vec![];
228
229 for new_field in &new_model.fields {
230 if new_field
231 .annotations
232 .iter()
233 .any(|x| matches!(x, Annotation::ForeignKey(_)))
234 {
235 references
236 .entry(new_model.name.clone())
237 .or_default()
238 .push(new_field.clone());
239 } else {
240 normal_fields.push(new_field.clone());
241 }
242 }
243
244 op.push(Operation::CreateModel {
245 name: new_model.name.clone(),
246 fields: normal_fields,
247 });
248 info!("Created model {}", new_model.name);
249 }
250
251 for (model, fields) in references {
253 for field in fields {
254 op.push(Operation::CreateField {
255 model: model.clone(),
256 field,
257 });
258 }
259 }
260
261 for deleted_model in &deleted_models {
263 op.push(Operation::DeleteModel {
264 name: deleted_model.name.clone(),
265 });
266 info!("Deleted model {}", deleted_model.name);
267 }
268
269 for (model_name, new_fields) in &new_fields {
270 if let Some(old_fields) = deleted_fields.get(model_name) {
271 for new_field in new_fields {
272 for old_field in old_fields {
273 if new_field.db_type == old_field.db_type
274 && new_field.annotations == old_field.annotations
275 && question(
276 format!(
277 "Did you rename the field {} of model {model_name} to {}?",
278 old_field.name, new_field.name
279 )
280 .as_str(),
281 )
282 {
283 renamed_fields
284 .entry(model_name.clone())
285 .or_default()
286 .push((old_field, new_field));
287 info!(
288 "Renamed field {} of model {model_name} to {}.",
289 old_field.name, new_field.name
290 );
291 }
292 }
293 }
294 }
295 }
296 for (model_name, fields) in &renamed_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 for (model_name, fields) in &new_fields {
319 for field in fields {
320 op.push(Operation::CreateField {
321 model: model_name.clone(),
322 field: (*field).clone(),
323 });
324 info!("Added field {} to model {}", field.name, model_name);
325 }
326 }
327
328 for (model_name, fields) in &deleted_fields {
330 for field in fields {
331 op.push(Operation::DeleteField {
332 model: model_name.clone(),
333 name: field.name.clone(),
334 });
335 info!("Deleted field {} from model {}", field.name, model_name);
336 }
337 }
338
339 for (model, af) in &altered_fields {
341 for (old, new) in af {
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 info!("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 info!("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 info!("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(|model| {
397 let mut normal_fields = vec![];
398
399 for field in &model.fields {
400 if field
401 .annotations
402 .iter()
403 .any(|x| matches!(x, Annotation::ForeignKey(_)))
404 {
405 references
406 .entry(model.name.clone())
407 .or_default()
408 .push(field.clone());
409 } else {
410 normal_fields.push(field.clone());
411 }
412 }
413
414 info!("Created model {}", model.name);
415 Operation::CreateModel {
416 name: model.name.clone(),
417 fields: normal_fields,
418 }
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 info!("Done.");
455
456 Ok(())
457}