Skip to main content

siderite_migrations/
squash.rs

1//! Collapse a contiguous range of migrations into one.
2
3use crate::error::MigrationError;
4use crate::loader::MigrationGraph;
5use crate::migration::{Migration, next_id, sanitize_slug};
6use crate::operation::Operation;
7use crate::state::ProjectState;
8
9/// Squash the inclusive range `from_id..=to_id` (in graph order) into one
10/// migration.
11///
12/// Concatenates operations and applies:
13/// * `CreateModel` followed by `AddField` on the same model merges the field
14///   into the create
15/// * `CreateModel` + later `DeleteModel` of the same model (and ops on it in
16///   between) cancel
17///
18/// The result has `replaces` set to the squashed ids. Dependencies are those
19/// of the first migration in the range that are not themselves replaced.
20///
21/// # Errors
22/// Unknown ids, a non-contiguous range, or checksum encoding.
23pub 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
78/// Reduce `ops` with the documented local optimizations.
79pub fn optimize(mut ops: Vec<Operation>) -> Vec<Operation> {
80    let mut changed = true;
81    while changed {
82        changed = false;
83        // Merge CreateModel + later AddField on the same model.
84        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        // Cancel CreateModel + DeleteModel of the same model, dropping ops on it.
114        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
167/// Apply `operations` to an empty state (used by tests).
168pub 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}