siderite_migrations/
squash.rs1use crate::error::MigrationError;
4use crate::loader::MigrationGraph;
5use crate::migration::{Migration, next_id, sanitize_slug};
6use crate::operation::Operation;
7use crate::state::ProjectState;
8
9pub fn squash(
24 graph: &MigrationGraph,
25 from_id: &str,
26 to_id: &str,
27 name: Option<&str>,
28) -> Result<Migration, MigrationError> {
29 let order = &graph.order;
30 let start = order
31 .iter()
32 .position(|id| id == from_id)
33 .ok_or_else(|| MigrationError::usage(format!("unknown migration `{from_id}`")))?;
34 let end = order
35 .iter()
36 .position(|id| id == to_id)
37 .ok_or_else(|| MigrationError::usage(format!("unknown migration `{to_id}`")))?;
38 if start > end {
39 return Err(MigrationError::usage(format!(
40 "cannot squash `{from_id}`..=`{to_id}`: `{from_id}` is after `{to_id}`"
41 )));
42 }
43 let range: Vec<&Migration> = order[start..=end]
44 .iter()
45 .filter_map(|id| graph.get(id))
46 .collect();
47 if range.is_empty() {
48 return Err(MigrationError::usage("nothing to squash"));
49 }
50 let replaced: Vec<String> = range.iter().map(|m| m.id.clone()).collect();
51 let mut dependencies: Vec<String> = Vec::new();
52 let replaced_set: std::collections::HashSet<&str> =
53 replaced.iter().map(String::as_str).collect();
54 for dep in &range[0].dependencies {
55 if !replaced_set.contains(dep.as_str()) {
56 dependencies.push(dep.clone());
57 }
58 }
59 let mut operations = Vec::new();
60 let mut atomic = true;
61 for migration in &range {
62 operations.extend(migration.operations.iter().cloned());
63 atomic = atomic && migration.atomic;
64 }
65 operations = optimize(operations);
66 let slug = match name {
67 Some(n) => sanitize_slug(n),
68 None => format!(
69 "squashed_{}_{}",
70 sanitize_slug(from_id),
71 sanitize_slug(to_id)
72 ),
73 };
74 let id = next_id(&graph.all, &slug);
75 Migration::new(id, dependencies, operations, atomic, replaced)
76}
77
78pub fn optimize(mut ops: Vec<Operation>) -> Vec<Operation> {
80 let mut changed = true;
81 while changed {
82 changed = false;
83 let mut i = 0;
85 while i < ops.len() {
86 let model_name = match &ops[i] {
87 Operation::CreateModel { model } => Some(model.name.clone()),
88 _ => None,
89 };
90 if let Some(name) = model_name {
91 let mut j = i + 1;
92 while j < ops.len() {
93 let merge_field = match &ops[j] {
94 Operation::AddField { model, field } if model == &name => {
95 Some(field.clone())
96 }
97 Operation::DeleteModel { name: n } if n == &name => break,
98 _ => None,
99 };
100 if let Some(field) = merge_field {
101 if let Operation::CreateModel { model } = &mut ops[i] {
102 model.fields.push(field);
103 }
104 ops.remove(j);
105 changed = true;
106 continue;
107 }
108 j += 1;
109 }
110 }
111 i += 1;
112 }
113 if let Some((c, d, name)) = find_create_delete(&ops) {
115 let mut remove = Vec::new();
116 for (idx, op) in ops.iter().enumerate() {
117 if idx >= c && idx <= d && targets_model(op, &name) {
118 remove.push(idx);
119 }
120 }
121 for idx in remove.into_iter().rev() {
122 ops.remove(idx);
123 }
124 changed = true;
125 }
126 }
127 ops
128}
129
130fn find_create_delete(ops: &[Operation]) -> Option<(usize, usize, String)> {
131 for (c, op) in ops.iter().enumerate() {
132 if let Operation::CreateModel { model } = op {
133 let name = &model.name;
134 if let Some(d) = ops.iter().enumerate().skip(c + 1).find_map(|(i, o)| {
135 if let Operation::DeleteModel { name: n } = o {
136 (n == name).then_some(i)
137 } else {
138 None
139 }
140 }) {
141 return Some((c, d, name.clone()));
142 }
143 }
144 }
145 None
146}
147
148fn targets_model(op: &Operation, name: &str) -> bool {
149 match op {
150 Operation::CreateModel { model } => model.name == name,
151 Operation::DeleteModel { name: n } => n == name,
152 Operation::RenameModel {
153 old_name, new_name, ..
154 } => old_name == name || new_name == name,
155 Operation::AddField { model, .. }
156 | Operation::RemoveField { model, .. }
157 | Operation::AlterField { model, .. }
158 | Operation::RenameField { model, .. }
159 | Operation::CreateIndex { model, .. }
160 | Operation::DeleteIndex { model, .. }
161 | Operation::AddConstraint { model, .. }
162 | Operation::DeleteConstraint { model, .. } => model == name,
163 Operation::RunSQL { .. } | Operation::RunRust { .. } => false,
164 }
165}
166
167pub fn final_state(operations: &[Operation]) -> Result<ProjectState, MigrationError> {
169 let mut state = ProjectState::new();
170 for op in operations {
171 op.apply_to_state(&mut state)?;
172 }
173 Ok(state)
174}