Skip to main content

siderite_migrations/
operation.rs

1//! Schema operations recorded in a migration file.
2
3use crate::error::MigrationError;
4use crate::state::{ConstraintState, FieldState, IndexState, ModelState, ProjectState};
5use serde::{Deserialize, Serialize};
6
7/// One atomic schema change (or a data operation).
8///
9/// [`RunSQL`](Self::RunSQL) without `reverse_sql` and [`RunRust`](Self::RunRust)
10/// without `backwards` are irreversible. [`DeleteModel`](Self::DeleteModel) and
11/// [`RemoveField`](Self::RemoveField) reverse by recreating the object from
12/// `state_before`.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[serde(tag = "op", rename_all = "snake_case")]
15pub enum Operation {
16    /// Create a table (and its indexes / table constraints).
17    CreateModel {
18        /// Full model snapshot after the create.
19        model: ModelState,
20    },
21    /// Drop a table.
22    DeleteModel {
23        /// Model name.
24        name: String,
25    },
26    /// Rename a model (and its table when `table` differs from the current
27    /// table). Detected only when listed in [`crate::autodetector::RenameHints`].
28    RenameModel {
29        /// Current model name.
30        old_name: String,
31        /// New model name.
32        new_name: String,
33        /// Table name after the rename.
34        table: String,
35    },
36    /// Add a column.
37    AddField {
38        /// Model name.
39        model: String,
40        /// New field.
41        field: FieldState,
42    },
43    /// Drop a column.
44    RemoveField {
45        /// Model name.
46        model: String,
47        /// Field name.
48        name: String,
49    },
50    /// Replace a field's schema (type, nullability, default, …).
51    AlterField {
52        /// Model name.
53        model: String,
54        /// Field name (after any rename in the same migration).
55        name: String,
56        /// New field snapshot.
57        field: FieldState,
58    },
59    /// Rename a field. Detected only when listed in
60    /// [`crate::autodetector::RenameHints`]; otherwise a same-shape
61    /// remove+add is refused.
62    RenameField {
63        /// Model name.
64        model: String,
65        /// Current field name.
66        old_name: String,
67        /// New field name. The column is renamed when it currently equals
68        /// `old_name`.
69        new_name: String,
70    },
71    /// Create a named index.
72    CreateIndex {
73        /// Model name.
74        model: String,
75        /// Index snapshot.
76        index: IndexState,
77    },
78    /// Drop a named index.
79    DeleteIndex {
80        /// Model name.
81        model: String,
82        /// Index name.
83        name: String,
84    },
85    /// Add a table-level constraint.
86    AddConstraint {
87        /// Model name.
88        model: String,
89        /// Constraint snapshot.
90        constraint: ConstraintState,
91    },
92    /// Drop a table-level constraint.
93    DeleteConstraint {
94        /// Model name.
95        model: String,
96        /// Constraint name.
97        name: String,
98    },
99    /// Developer-authored SQL. Irreversible unless `reverse_sql` is set.
100    RunSQL {
101        /// Forward SQL (may contain several statements).
102        sql: String,
103        /// SQL that undoes `sql`.
104        #[serde(default, skip_serializing_if = "Option::is_none")]
105        reverse_sql: Option<String>,
106    },
107    /// A function registered at runtime in a
108    /// [`crate::registry::MigrationRegistry`]. The function itself cannot be
109    /// serialized; only the name is stored.
110    RunRust {
111        /// Registered forward name.
112        name: String,
113        /// Registered backwards name, if reversible.
114        #[serde(default, skip_serializing_if = "Option::is_none")]
115        backwards: Option<String>,
116    },
117}
118
119impl Operation {
120    /// Apply this operation to `state`.
121    ///
122    /// # Errors
123    /// [`MigrationError::State`] when a referenced model or field is missing,
124    /// or when creating a duplicate.
125    pub fn apply_to_state(&self, state: &mut ProjectState) -> Result<(), MigrationError> {
126        match self {
127            Self::CreateModel { model } => {
128                if state.models.contains_key(&model.name) {
129                    return Err(MigrationError::state(format!(
130                        "model `{}` already exists",
131                        model.name
132                    )));
133                }
134                state.models.insert(model.name.clone(), model.clone());
135            }
136            Self::DeleteModel { name } => {
137                state.models.remove(name).ok_or_else(|| {
138                    MigrationError::state(format!("model `{name}` is not in the project state"))
139                })?;
140            }
141            Self::RenameModel {
142                old_name,
143                new_name,
144                table,
145            } => {
146                let mut model = state.models.remove(old_name).ok_or_else(|| {
147                    MigrationError::state(format!("model `{old_name}` is not in the project state"))
148                })?;
149                if old_name != new_name && state.models.contains_key(new_name) {
150                    return Err(MigrationError::state(format!(
151                        "model `{new_name}` already exists"
152                    )));
153                }
154                let old_table = model.table.clone();
155                model.name = new_name.clone();
156                model.table = table.clone();
157                state.models.insert(new_name.clone(), model);
158                // Foreign keys point at tables: retarget every model's FKs
159                // that referenced the old table name, including this one's
160                // (a self-referencing `parent_id` keeps pointing at the old
161                // table otherwise).
162                if old_table != *table {
163                    for other in state.models.values_mut() {
164                        for field in &mut other.fields {
165                            if let Some(fk) = field.fk.as_mut()
166                                && fk.target_table == old_table
167                            {
168                                fk.target_table = table.clone();
169                            }
170                        }
171                    }
172                }
173            }
174            Self::AddField { model, field } => {
175                let m = state.require_mut(model)?;
176                if m.field(&field.name).is_some() {
177                    return Err(MigrationError::state(format!(
178                        "field `{}` already exists on `{model}`",
179                        field.name
180                    )));
181                }
182                m.fields.push(field.clone());
183            }
184            Self::RemoveField { model, name } => {
185                let m = state.require_mut(model)?;
186                let before = m.fields.len();
187                m.fields.retain(|f| f.name != *name);
188                if m.fields.len() == before {
189                    return Err(MigrationError::state(format!(
190                        "field `{name}` not on model `{model}`"
191                    )));
192                }
193            }
194            Self::AlterField { model, name, field } => {
195                let m = state.require_mut(model)?;
196                let existing = m.field_mut(name).ok_or_else(|| {
197                    MigrationError::state(format!("field `{name}` not on model `{model}`"))
198                })?;
199                *existing = field.clone();
200            }
201            Self::RenameField {
202                model,
203                old_name,
204                new_name,
205            } => rename_field(state.require_mut(model)?, old_name, new_name)?,
206            Self::CreateIndex { model, index } => {
207                let m = state.require_mut(model)?;
208                if m.index(&index.name).is_some() {
209                    return Err(MigrationError::state(format!(
210                        "index `{}` already exists on `{model}`",
211                        index.name
212                    )));
213                }
214                m.indexes.push(index.clone());
215            }
216            Self::DeleteIndex { model, name } => {
217                let m = state.require_mut(model)?;
218                let before = m.indexes.len();
219                m.indexes.retain(|i| i.name != *name);
220                if m.indexes.len() == before {
221                    return Err(MigrationError::state(format!(
222                        "index `{name}` not on model `{model}`"
223                    )));
224                }
225            }
226            Self::AddConstraint { model, constraint } => {
227                let m = state.require_mut(model)?;
228                if m.constraint(constraint.name()).is_some() {
229                    return Err(MigrationError::state(format!(
230                        "constraint `{}` already exists on `{model}`",
231                        constraint.name()
232                    )));
233                }
234                m.constraints.push(constraint.clone());
235            }
236            Self::DeleteConstraint { model, name } => {
237                let m = state.require_mut(model)?;
238                let before = m.constraints.len();
239                m.constraints.retain(|c| c.name() != *name);
240                if m.constraints.len() == before {
241                    return Err(MigrationError::state(format!(
242                        "constraint `{name}` not on model `{model}`"
243                    )));
244                }
245            }
246            Self::RunSQL { .. } | Self::RunRust { .. } => {}
247        }
248        Ok(())
249    }
250
251    /// Inverse operation, or `None` if this step is irreversible.
252    ///
253    /// `state_before` is the project state immediately before this operation
254    /// was applied. [`DeleteModel`](Self::DeleteModel) and
255    /// [`RemoveField`](Self::RemoveField) recreate the dropped object from it.
256    pub fn reverse(&self, state_before: &ProjectState) -> Option<Operation> {
257        match self {
258            Self::CreateModel { model } => Some(Self::DeleteModel {
259                name: model.name.clone(),
260            }),
261            Self::DeleteModel { name } => {
262                let model = state_before.model(name)?.clone();
263                Some(Self::CreateModel { model })
264            }
265            Self::RenameModel {
266                old_name, new_name, ..
267            } => {
268                let table = state_before.model(old_name)?.table.clone();
269                Some(Self::RenameModel {
270                    old_name: new_name.clone(),
271                    new_name: old_name.clone(),
272                    table,
273                })
274            }
275            Self::AddField { model, field } => Some(Self::RemoveField {
276                model: model.clone(),
277                name: field.name.clone(),
278            }),
279            Self::RemoveField { model, name } => {
280                let field = state_before.model(model)?.field(name)?.clone();
281                Some(Self::AddField {
282                    model: model.clone(),
283                    field,
284                })
285            }
286            Self::AlterField { model, name, .. } => {
287                let field = state_before.model(model)?.field(name)?.clone();
288                Some(Self::AlterField {
289                    model: model.clone(),
290                    name: field.name.clone(),
291                    field,
292                })
293            }
294            Self::RenameField {
295                model,
296                old_name,
297                new_name,
298            } => Some(Self::RenameField {
299                model: model.clone(),
300                old_name: new_name.clone(),
301                new_name: old_name.clone(),
302            }),
303            Self::CreateIndex { model, index } => Some(Self::DeleteIndex {
304                model: model.clone(),
305                name: index.name.clone(),
306            }),
307            Self::DeleteIndex { model, name } => {
308                let index = state_before.model(model)?.index(name)?.clone();
309                Some(Self::CreateIndex {
310                    model: model.clone(),
311                    index,
312                })
313            }
314            Self::AddConstraint { model, constraint } => Some(Self::DeleteConstraint {
315                model: model.clone(),
316                name: constraint.name().to_owned(),
317            }),
318            Self::DeleteConstraint { model, name } => {
319                let constraint = state_before.model(model)?.constraint(name)?.clone();
320                Some(Self::AddConstraint {
321                    model: model.clone(),
322                    constraint,
323                })
324            }
325            Self::RunSQL { reverse_sql, .. } => reverse_sql.as_ref().map(|sql| Self::RunSQL {
326                sql: sql.clone(),
327                reverse_sql: None,
328            }),
329            Self::RunRust { name, backwards } => backwards.as_ref().map(|back| Self::RunRust {
330                name: back.clone(),
331                backwards: Some(name.clone()),
332            }),
333        }
334    }
335
336    /// Whether reversal is impossible regardless of project state
337    /// (`RunSQL` without `reverse_sql`, `RunRust` without `backwards`).
338    pub fn is_unconditionally_irreversible(&self) -> bool {
339        match self {
340            Self::RunSQL { reverse_sql, .. } => reverse_sql.is_none(),
341            Self::RunRust { backwards, .. } => backwards.is_none(),
342            _ => false,
343        }
344    }
345
346    /// Short label for CLI output (`CreateModel Author`).
347    pub fn summary(&self) -> String {
348        match self {
349            Self::CreateModel { model } => format!("CreateModel {}", model.name),
350            Self::DeleteModel { name } => format!("DeleteModel {name}"),
351            Self::RenameModel {
352                old_name, new_name, ..
353            } => format!("RenameModel {old_name} -> {new_name}"),
354            Self::AddField { model, field } => format!("AddField {model}.{}", field.name),
355            Self::RemoveField { model, name } => format!("RemoveField {model}.{name}"),
356            Self::AlterField { model, name, .. } => format!("AlterField {model}.{name}"),
357            Self::RenameField {
358                model,
359                old_name,
360                new_name,
361            } => format!("RenameField {model}.{old_name} -> {new_name}"),
362            Self::CreateIndex { model, index } => format!("CreateIndex {model}.{}", index.name),
363            Self::DeleteIndex { model, name } => format!("DeleteIndex {model}.{name}"),
364            Self::AddConstraint { model, constraint } => {
365                format!("AddConstraint {model}.{}", constraint.name())
366            }
367            Self::DeleteConstraint { model, name } => format!("DeleteConstraint {model}.{name}"),
368            Self::RunSQL { .. } => "RunSQL".to_owned(),
369            Self::RunRust { name, .. } => format!("RunRust {name}"),
370        }
371    }
372}
373
374fn rename_field(
375    model: &mut ModelState,
376    old_name: &str,
377    new_name: &str,
378) -> Result<(), MigrationError> {
379    if model.field(new_name).is_some() && old_name != new_name {
380        return Err(MigrationError::state(format!(
381            "field `{new_name}` already exists on `{}`",
382            model.name
383        )));
384    }
385    let rename_column = {
386        let field = model.field(old_name).ok_or_else(|| {
387            MigrationError::state(format!("field `{old_name}` not on model `{}`", model.name))
388        })?;
389        field.column == old_name
390    };
391    let old_column = model
392        .field(old_name)
393        .map(|f| f.column.clone())
394        .unwrap_or_default();
395    let model_name = model.name.clone();
396    let field = model.field_mut(old_name).ok_or_else(|| {
397        MigrationError::state(format!("field `{old_name}` not on model `{model_name}`"))
398    })?;
399    field.name = new_name.to_owned();
400    if rename_column {
401        field.column = new_name.to_owned();
402    }
403    if rename_column {
404        let new_column = new_name.to_owned();
405        for index in &mut model.indexes {
406            for col in &mut index.columns {
407                if *col == old_column {
408                    *col = new_column.clone();
409                }
410            }
411        }
412        for constraint in &mut model.constraints {
413            if let crate::state::ConstraintState::Unique { columns, .. } = constraint {
414                for col in columns {
415                    if *col == old_column {
416                        *col = new_column.clone();
417                    }
418                }
419            }
420        }
421    }
422    Ok(())
423}