1use crate::error::MigrationError;
10use crate::operation::Operation;
11use crate::state::{FieldState, ModelState, ProjectState, auto_index_name};
12use std::collections::{BTreeMap, BTreeSet, HashMap};
13
14#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct RenameHints {
24 pub fields: BTreeMap<(String, String), String>,
26 pub models: BTreeMap<String, String>,
28 pub allow_model_drops: BTreeSet<String>,
31 pub allow_field_drops: BTreeSet<(String, String)>,
34}
35
36impl RenameHints {
37 pub fn new() -> Self {
39 Self::default()
40 }
41
42 #[must_use]
44 pub fn rename_field(
45 mut self,
46 model: impl Into<String>,
47 old_name: impl Into<String>,
48 new_name: impl Into<String>,
49 ) -> Self {
50 self.fields
51 .insert((model.into(), old_name.into()), new_name.into());
52 self
53 }
54
55 #[must_use]
57 pub fn rename_model(
58 mut self,
59 old_name: impl Into<String>,
60 new_name: impl Into<String>,
61 ) -> Self {
62 self.models.insert(old_name.into(), new_name.into());
63 self
64 }
65
66 #[must_use]
71 pub fn allow_drop_model(mut self, name: impl Into<String>) -> Self {
72 self.allow_model_drops.insert(name.into());
73 self
74 }
75
76 #[must_use]
79 pub fn allow_drop_field(mut self, model: impl Into<String>, field: impl Into<String>) -> Self {
80 self.allow_field_drops.insert((model.into(), field.into()));
81 self
82 }
83}
84
85pub fn diff(from: &ProjectState, to: &ProjectState) -> Result<Vec<Operation>, MigrationError> {
95 diff_with(from, to, &RenameHints::default())
96}
97
98pub fn diff_with(
104 from: &ProjectState,
105 to: &ProjectState,
106 hints: &RenameHints,
107) -> Result<Vec<Operation>, MigrationError> {
108 let mut ops = Vec::new();
109
110 let from_names: BTreeSet<&str> = from.models.keys().map(String::as_str).collect();
111 let to_names: BTreeSet<&str> = to.models.keys().map(String::as_str).collect();
112
113 let renamed_models: BTreeMap<&str, &str> = hints
114 .models
115 .iter()
116 .filter(|(old, new)| from.models.contains_key(*old) && to.models.contains_key(*new))
117 .map(|(old, new)| (old.as_str(), new.as_str()))
118 .collect();
119 let renamed_from: BTreeSet<&str> = renamed_models.keys().copied().collect();
120 let renamed_to: BTreeSet<&str> = renamed_models.values().copied().collect();
121
122 let deleted: Vec<&str> = from_names
123 .difference(&to_names)
124 .copied()
125 .filter(|n| !renamed_from.contains(n))
126 .collect();
127 let created: Vec<&str> = to_names
128 .difference(&from_names)
129 .copied()
130 .filter(|n| !renamed_to.contains(n))
131 .collect();
132 let common: Vec<&str> = from_names.intersection(&to_names).copied().collect();
133
134 refuse_unhinted_model_renames(from, to, &deleted, &created, hints)?;
135
136 let pairs: Vec<(&str, &str)> = common
137 .iter()
138 .map(|n| (*n, *n))
139 .chain(renamed_models.iter().map(|(o, n)| (*o, *n)))
140 .collect();
141
142 for (old_name, new_name) in &pairs {
144 let old = &from.models[*old_name];
145 let new = &to.models[*new_name];
146 let renamed = hints_for(hints, old_name);
147 refuse_unhinted_field_renames(old, new, &renamed, hints)?;
148 push_deletes(&mut ops, old, new, &renamed);
149 }
150 for name in reverse_topo(&deleted, from) {
151 ops.push(Operation::DeleteModel {
152 name: name.to_owned(),
153 });
154 }
155
156 for (old_name, new_name) in &renamed_models {
157 ops.push(Operation::RenameModel {
158 old_name: (*old_name).to_owned(),
159 new_name: (*new_name).to_owned(),
160 table: to.models[*new_name].table.clone(),
161 });
162 }
163
164 for (old_name, new_name) in &pairs {
166 let renamed = hints_for(hints, old_name);
167 for (from_field, to_field) in &renamed {
168 if from.models[*old_name].field(from_field).is_some()
169 && to.models[*new_name].field(to_field).is_some()
170 {
171 ops.push(Operation::RenameField {
172 model: (*new_name).to_owned(),
173 old_name: from_field.clone(),
174 new_name: to_field.clone(),
175 });
176 }
177 }
178 }
179
180 let created_states: Vec<&ModelState> = created.iter().map(|n| &to.models[*n]).collect();
182 let (ordered, deferred) = order_creates(&created_states, to);
183 for model in ordered {
184 let mut create = model.clone();
185 let extra = deferred
186 .get(model.name.as_str())
187 .cloned()
188 .unwrap_or_default();
189 if !extra.is_empty() {
190 let extra_names: BTreeSet<&str> = extra.iter().map(|f| f.name.as_str()).collect();
191 create
192 .fields
193 .retain(|f| !extra_names.contains(f.name.as_str()));
194 create.indexes.retain(|i| {
195 i.columns
196 .iter()
197 .all(|c| create.fields.iter().any(|f| f.column == *c))
198 });
199 create.constraints.retain(|c| match c {
200 crate::state::ConstraintState::Unique { columns, .. } => columns
201 .iter()
202 .all(|col| create.fields.iter().any(|f| f.column == *col)),
203 crate::state::ConstraintState::Check { .. } => extra.is_empty(),
204 });
205 }
206 ops.push(Operation::CreateModel { model: create });
207 if let Some(fields) = deferred.get(model.name.as_str()) {
208 for field in fields {
209 ops.push(Operation::AddField {
210 model: model.name.clone(),
211 field: field.clone(),
212 });
213 }
214 }
215 }
216
217 for (old_name, new_name) in &pairs {
219 let old = &from.models[*old_name];
220 let new = &to.models[*new_name];
221 let renamed = hints_for(hints, old_name);
222 push_adds_and_alters(&mut ops, old, new, &renamed);
223 }
224
225 Ok(ops)
226}
227
228fn refuse_unhinted_model_renames(
229 from: &ProjectState,
230 to: &ProjectState,
231 deleted: &[&str],
232 created: &[&str],
233 hints: &RenameHints,
234) -> Result<(), MigrationError> {
235 for old_name in deleted {
236 if hints.allow_model_drops.contains(*old_name) {
237 continue;
238 }
239 let old = &from.models[*old_name];
240 for new_name in created {
241 let new = &to.models[*new_name];
242 if same_model_shape(old, new) {
243 return Err(MigrationError::state(format!(
244 "model `{old_name}` was deleted and `{new_name}` was created with the same columns; \
245 pass RenameHints::rename_model(\"{old_name}\", \"{new_name}\") to preserve data, \
246 RenameHints::allow_drop_model(\"{old_name}\") when the drop is intentional, \
247 or split the drop and the create into two migrations"
248 )));
249 }
250 }
251 }
252 Ok(())
253}
254
255fn refuse_unhinted_field_renames(
256 old: &ModelState,
257 new: &ModelState,
258 renamed: &BTreeMap<String, String>,
259 hints: &RenameHints,
260) -> Result<(), MigrationError> {
261 let renamed_from: BTreeSet<&str> = renamed.keys().map(String::as_str).collect();
262 let renamed_to: BTreeSet<&str> = renamed.values().map(String::as_str).collect();
263 let removed: Vec<&FieldState> = old
264 .fields
265 .iter()
266 .filter(|f| !renamed_from.contains(f.name.as_str()) && new.field(&f.name).is_none())
267 .collect();
268 let added: Vec<&FieldState> = new
269 .fields
270 .iter()
271 .filter(|f| !renamed_to.contains(f.name.as_str()) && old.field(&f.name).is_none())
272 .collect();
273 for lost in &removed {
274 if hints
275 .allow_field_drops
276 .contains(&(old.name.clone(), lost.name.clone()))
277 {
278 continue;
279 }
280 for gained in &added {
281 if same_column_shape(lost, gained) {
282 return Err(MigrationError::state(format!(
283 "field `{}.{}` was removed and `{}.{}` was added with the same type; \
284 pass RenameHints::rename_field(\"{}\", \"{}\", \"{}\") to preserve data, \
285 RenameHints::allow_drop_field(\"{}\", \"{}\") when the drop is intentional, \
286 or split the drop and the add into two migrations",
287 old.name,
288 lost.name,
289 new.name,
290 gained.name,
291 old.name,
292 lost.name,
293 gained.name,
294 old.name,
295 lost.name
296 )));
297 }
298 }
299 }
300 Ok(())
301}
302
303fn same_model_shape(a: &ModelState, b: &ModelState) -> bool {
308 if a.fields.is_empty() {
309 return false;
310 }
311 let cols = |m: &ModelState| {
312 let mut v: Vec<_> = m
313 .fields
314 .iter()
315 .map(|f| (f.column.clone(), f.sql_type, f.nullable, f.max_length))
316 .collect();
317 v.sort_by(|x, y| x.0.cmp(&y.0));
318 v
319 };
320 cols(a) == cols(b)
321}
322
323fn same_column_shape(a: &FieldState, b: &FieldState) -> bool {
324 a.sql_type == b.sql_type
325 && a.nullable == b.nullable
326 && a.max_length == b.max_length
327 && a.primary_key == b.primary_key
328 && a.unique == b.unique
329}
330
331fn hints_for(hints: &RenameHints, model: &str) -> BTreeMap<String, String> {
332 hints
333 .fields
334 .iter()
335 .filter_map(|((m, old), new)| {
336 if m == model {
337 Some((old.clone(), new.clone()))
338 } else {
339 None
340 }
341 })
342 .collect()
343}
344
345fn push_deletes(
346 ops: &mut Vec<Operation>,
347 old: &ModelState,
348 new: &ModelState,
349 renamed: &BTreeMap<String, String>,
350) {
351 let renamed_from: BTreeSet<&str> = renamed.keys().map(String::as_str).collect();
352
353 for constraint in &old.constraints {
354 if new.constraint(constraint.name()).is_none() {
355 ops.push(Operation::DeleteConstraint {
356 model: old.name.clone(),
357 name: constraint.name().to_owned(),
358 });
359 }
360 }
361
362 let old_auto_indexes = auto_index_names(old);
363 let new_auto_indexes = auto_index_names(new);
364 for index in &old.indexes {
365 if new.index(&index.name).is_none() {
366 ops.push(Operation::DeleteIndex {
367 model: old.name.clone(),
368 name: index.name.clone(),
369 });
370 }
371 }
372 for name in &old_auto_indexes {
373 if !new_auto_indexes.contains(name) && old.index(name).is_none() {
374 ops.push(Operation::DeleteIndex {
375 model: old.name.clone(),
376 name: name.clone(),
377 });
378 }
379 }
380
381 for field in &old.fields {
382 if renamed_from.contains(field.name.as_str()) {
383 continue;
384 }
385 if new.field(&field.name).is_none() {
386 ops.push(Operation::RemoveField {
387 model: old.name.clone(),
388 name: field.name.clone(),
389 });
390 }
391 }
392}
393
394fn push_adds_and_alters(
395 ops: &mut Vec<Operation>,
396 old: &ModelState,
397 new: &ModelState,
398 renamed: &BTreeMap<String, String>,
399) {
400 let old_by_new_name: HashMap<&str, &FieldState> = {
401 let mut m = HashMap::new();
402 for field in &old.fields {
403 let key = renamed
404 .get(&field.name)
405 .map(String::as_str)
406 .unwrap_or(field.name.as_str());
407 m.insert(key, field);
408 }
409 m
410 };
411 let renamed_from: BTreeSet<&str> = renamed.keys().map(String::as_str).collect();
412
413 for field in &new.fields {
414 if renamed.values().any(|n| n == &field.name) {
415 if let Some(old_field) = old_by_new_name.get(field.name.as_str()) {
416 let mut comparable = (*old_field).clone();
417 comparable.name = field.name.clone();
418 if comparable.column
419 == renamed
420 .iter()
421 .find_map(|(o, n)| (n == &field.name).then_some(o.as_str()))
422 .unwrap_or("")
423 {
424 comparable.column = field.column.clone();
425 }
426 if &comparable != field {
427 ops.push(Operation::AlterField {
428 model: new.name.clone(),
429 name: field.name.clone(),
430 field: field.clone(),
431 });
432 }
433 }
434 continue;
435 }
436 if old.field(&field.name).is_none() && !renamed_from.contains(field.name.as_str()) {
437 ops.push(Operation::AddField {
438 model: new.name.clone(),
439 field: field.clone(),
440 });
441 } else if let Some(old_field) = old.field(&field.name)
442 && old_field != field
443 {
444 ops.push(Operation::AlterField {
445 model: new.name.clone(),
446 name: field.name.clone(),
447 field: field.clone(),
448 });
449 }
450 }
451
452 let old_auto = auto_index_names(old);
453 let new_auto = auto_index_names(new);
454 for name in &new_auto {
455 if !old_auto.contains(name)
456 && new.index(name).is_none()
457 && let Some(field) = new
458 .fields
459 .iter()
460 .find(|f| &auto_index_name(&new.table, &f.column) == name)
461 {
462 ops.push(Operation::CreateIndex {
463 model: new.name.clone(),
464 index: crate::state::IndexState {
465 name: name.clone(),
466 columns: vec![field.column.clone()],
467 unique: false,
468 },
469 });
470 }
471 }
472 for index in &new.indexes {
473 if old.index(&index.name).is_none() {
474 ops.push(Operation::CreateIndex {
475 model: new.name.clone(),
476 index: index.clone(),
477 });
478 }
479 }
480 for constraint in &new.constraints {
481 if old.constraint(constraint.name()).is_none() {
482 ops.push(Operation::AddConstraint {
483 model: new.name.clone(),
484 constraint: constraint.clone(),
485 });
486 }
487 }
488}
489
490fn auto_index_names(model: &ModelState) -> BTreeSet<String> {
491 model
492 .fields
493 .iter()
494 .filter(|f| f.index && !f.unique && !f.primary_key)
495 .map(|f| auto_index_name(&model.table, &f.column))
496 .collect()
497}
498
499fn order_creates<'a>(
501 created: &[&'a ModelState],
502 project: &ProjectState,
503) -> (Vec<&'a ModelState>, BTreeMap<String, Vec<FieldState>>) {
504 let names: BTreeSet<&str> = created.iter().map(|m| m.name.as_str()).collect();
505 let by_name: BTreeMap<&str, &ModelState> =
506 created.iter().map(|m| (m.name.as_str(), *m)).collect();
507
508 let mut deps: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
509 for model in created {
510 let mut d = BTreeSet::new();
511 for field in &model.fields {
512 if let Some(fk) = &field.fk
513 && let Some(target) = project.model_for_table(&fk.target_table)
514 && names.contains(target.name.as_str())
515 && target.name != model.name
516 {
517 d.insert(target.name.as_str());
518 }
519 }
520 deps.insert(model.name.as_str(), d);
521 }
522
523 let mut deferred: BTreeMap<String, Vec<FieldState>> = BTreeMap::new();
524 let mut ordered_names = match kahn(&names, &deps) {
525 Ok(order) => order,
526 Err(rest) => {
527 let leftover: BTreeSet<&str> = rest.iter().copied().collect();
529 for name in &leftover {
530 let model = by_name[name];
531 let extra: Vec<FieldState> = model
532 .fields
533 .iter()
534 .filter(|f| {
535 f.fk.as_ref().is_some_and(|fk| {
536 project.model_for_table(&fk.target_table).is_some_and(|t| {
537 leftover.contains(t.name.as_str()) && t.name != model.name
538 })
539 })
540 })
541 .cloned()
542 .collect();
543 if !extra.is_empty() {
544 deferred.insert((*name).to_owned(), extra);
545 if let Some(d) = deps.get_mut(name) {
546 d.retain(|dep| !leftover.contains(dep));
547 }
548 }
549 }
550 match kahn(&names, &deps) {
551 Ok(order) => order,
552 Err(rest) => {
553 let mut all: Vec<&str> = names.iter().copied().collect();
554 all.sort_unstable();
555 let mut rest_sorted = rest;
556 rest_sorted.sort_unstable();
557 let mut order: Vec<&str> = all
558 .iter()
559 .copied()
560 .filter(|n| !rest_sorted.contains(n))
561 .collect();
562 order.extend(rest_sorted);
563 order
564 }
565 }
566 }
567 };
568
569 let ordered: Vec<&ModelState> = ordered_names.drain(..).map(|n| by_name[n]).collect();
570 (ordered, deferred)
571}
572
573fn kahn<'a>(
574 names: &BTreeSet<&'a str>,
575 deps: &BTreeMap<&'a str, BTreeSet<&'a str>>,
576) -> Result<Vec<&'a str>, Vec<&'a str>> {
577 let mut indeg: BTreeMap<&str, usize> = BTreeMap::new();
578 for n in names {
579 indeg.insert(*n, deps.get(n).map(BTreeSet::len).unwrap_or(0));
580 }
581 let mut ready_set: BTreeSet<&str> = indeg
582 .iter()
583 .filter(|(_, d)| **d == 0)
584 .map(|(n, _)| *n)
585 .collect();
586 let mut order = Vec::new();
587 let mut remaining = indeg;
588 while let Some(n) = ready_set.iter().next().copied() {
589 ready_set.remove(n);
590 order.push(n);
591 remaining.remove(n);
592 for (node, ds) in deps {
593 if ds.contains(n)
594 && let Some(deg) = remaining.get_mut(node)
595 {
596 *deg = deg.saturating_sub(1);
597 if *deg == 0 {
598 ready_set.insert(node);
599 }
600 }
601 }
602 }
603 if remaining.is_empty() {
604 Ok(order)
605 } else {
606 Err(remaining.keys().copied().collect())
607 }
608}
609
610fn reverse_topo<'a>(deleted: &[&'a str], from: &'a ProjectState) -> Vec<&'a str> {
611 let names: BTreeSet<&str> = deleted.iter().copied().collect();
612 let mut deps: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
613 for name in &names {
614 let mut d = BTreeSet::new();
615 if let Some(model) = from.model(name) {
616 for field in &model.fields {
617 if let Some(fk) = &field.fk
618 && let Some(target) = from.model_for_table(&fk.target_table)
619 && names.contains(target.name.as_str())
620 && target.name != model.name
621 {
622 d.insert(target.name.as_str());
623 }
624 }
625 }
626 deps.insert(*name, d);
627 }
628 match kahn(&names, &deps) {
629 Ok(mut order) => {
630 order.reverse();
631 order
632 }
633 Err(_) => {
634 let mut all: Vec<&str> = names.iter().copied().collect();
635 all.sort_unstable();
636 all.reverse();
637 all
638 }
639 }
640}