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.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 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 for (old, new) in &renamed_models {
209 new_models.retain(|x| x != new);
210 deleted_models.retain(|x| x != old);
211
212 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 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 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 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 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 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 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 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 for (model, af) in &altered_fields {
337 for (old, new) in af {
338 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 (_, _) => {
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 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 internal_models.models.is_empty() {
386 info!("No models found.");
387 } 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 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}