Skip to main content

toasty_sql/
migration.rs

1use std::{borrow::Cow, collections::HashSet};
2
3use toasty_core::{
4    driver::Capability,
5    schema::{
6        db::{Column, Schema, Table, TableId, Type, TypeEnum},
7        diff,
8    },
9};
10
11use crate::stmt::{AlterColumnChanges, AlterTable, AlterTableAction, DropTable, Name, Statement};
12
13/// Returns `true` if a named enum change is handled at the type level and
14/// should not produce column-level DDL.
15fn is_named_enum_type_only_change(
16    previous: &Column,
17    next: &Column,
18    renamed_types: &HashSet<(&str, &str)>,
19) -> bool {
20    if previous.name != next.name
21        || previous.nullable != next.nullable
22        || previous.primary_key != next.primary_key
23        || previous.auto_increment != next.auto_increment
24    {
25        return false;
26    }
27
28    // A scalar ↔ array change is a real column type change, so both sides
29    // must have the same shape.
30    let same_shape = matches!(
31        (&previous.storage_ty, &next.storage_ty),
32        (Type::Enum(_), Type::Enum(_)) | (Type::List(_), Type::List(_))
33    );
34
35    if !same_shape {
36        return false;
37    }
38
39    matches!(
40        (
41            previous.storage_ty.named_enum(),
42            next.storage_ty.named_enum(),
43        ),
44        (
45            Some(TypeEnum { name: Some(a), .. }),
46            Some(TypeEnum { name: Some(b), .. }),
47        ) if a == b || renamed_types.contains(&(a.as_str(), b.as_str()))
48    )
49}
50
51/// A migration step pairing a DDL [`Statement`] with the [`Schema`] it applies against.
52///
53/// Each `MigrationStatement` carries a snapshot of the schema at the point where
54/// the statement should be serialized. This is necessary because rename and
55/// recreation operations modify the schema as they go.
56pub struct MigrationStatement<'a> {
57    statement: Statement,
58    schema: Cow<'a, Schema>,
59}
60
61impl<'a> MigrationStatement<'a> {
62    fn new(statement: Statement, schema: Cow<'a, Schema>) -> Self {
63        MigrationStatement { statement, schema }
64    }
65
66    /// Generates migration statements from a [`diff::Schema`].
67    ///
68    /// Walks the diff's type, table, column, and index changes and produces
69    /// the corresponding DDL statements. Type changes (CREATE TYPE, ALTER
70    /// TYPE) are emitted before table changes. On databases that lack
71    /// `ALTER COLUMN` support (e.g. SQLite), column type changes trigger a
72    /// full table recreation sequence.
73    pub fn from_diff(schema_diff: &'a diff::Schema<'a>, capability: &Capability) -> Vec<Self> {
74        let mut result = Vec::new();
75        let types_diff = schema_diff.types();
76        let renamed_types: HashSet<(&str, &str)> = types_diff
77            .iter()
78            .filter_map(|item| match item {
79                diff::Type::Rename { previous, next } => Some((
80                    previous.name.as_deref().expect("named enum type"),
81                    next.name.as_deref().expect("named enum type"),
82                )),
83                _ => None,
84            })
85            .collect();
86
87        // Emit enum type changes before table changes (tables may reference
88        // newly created types).
89        if capability.named_enum_types {
90            for item in types_diff.iter() {
91                match item {
92                    diff::Type::Create(ty) => {
93                        result.push(Self::new(
94                            Statement::create_enum_type(ty),
95                            Cow::Borrowed(schema_diff.next()),
96                        ));
97                    }
98                    diff::Type::Rename { previous, next } => {
99                        result.push(Self::new(
100                            Statement::rename_type(
101                                previous.name.as_deref().expect("named enum type"),
102                                next.name.as_deref().expect("named enum type"),
103                            ),
104                            Cow::Borrowed(schema_diff.previous()),
105                        ));
106                    }
107                    diff::Type::AddVariants { ty, added } => {
108                        let type_name = ty.name.as_deref().expect("named enum type");
109                        for variant in added {
110                            result.push(Self::new(
111                                Statement::alter_type_add_value(type_name, variant),
112                                Cow::Borrowed(schema_diff.next()),
113                            ));
114                        }
115                    }
116                }
117            }
118        }
119
120        for table in schema_diff.tables().iter() {
121            match table {
122                diff::Table::Create(table) => {
123                    result.push(Self::new(
124                        Statement::create_table(table, capability),
125                        Cow::Borrowed(schema_diff.next()),
126                    ));
127                    for index in &table.indices {
128                        if index.primary_key {
129                            continue; // PK indices are created as part of CREATE TABLE
130                        }
131                        result.push(Self::new(
132                            Statement::create_index(index),
133                            Cow::Borrowed(schema_diff.next()),
134                        ));
135                    }
136                }
137                diff::Table::Drop(table) => result.push(Self::new(
138                    Statement::drop_table(table),
139                    Cow::Borrowed(schema_diff.previous()),
140                )),
141                diff::Table::Alter {
142                    previous,
143                    next,
144                    columns,
145                    indices,
146                    ..
147                } => {
148                    let mut schema = Cow::Borrowed(schema_diff.previous());
149                    if previous.name != next.name {
150                        result.push(Self::new(
151                            Statement::alter_table_rename_to(previous, &next.name),
152                            schema.clone(),
153                        ));
154                        schema.to_mut().table_mut(previous.id).name = next.name.clone();
155                    }
156
157                    // Check if any column alteration requires table recreation
158                    // (e.g. SQLite can't alter column type/nullability/auto_increment)
159                    let needs_recreation = !capability.schema_mutations.alter_column_type
160                        && columns.iter().any(|item| {
161                            matches!(
162                                item,
163                                diff::Column::Alter {
164                                    previous: prev_col,
165                                    next: next_col
166                                } if AlterColumnChanges::from_diff(prev_col, next_col).has_type_change()
167                                    && !(capability.named_enum_types
168                                        && is_named_enum_type_only_change(
169                                            prev_col,
170                                            next_col,
171                                            &renamed_types,
172                                        ))
173                            )
174                        });
175
176                    // Drop existing indices before changing columns because they may
177                    // reference a column that is being removed or altered.
178                    for item in indices.iter() {
179                        match item {
180                            diff::Index::Drop(index) => {
181                                result.push(Self::new(
182                                    Statement::drop_index(index),
183                                    Cow::Borrowed(schema_diff.previous()),
184                                ));
185                            }
186                            diff::Index::Alter { previous, .. } => {
187                                result.push(Self::new(
188                                    Statement::drop_index(previous),
189                                    Cow::Borrowed(schema_diff.previous()),
190                                ));
191                            }
192                            diff::Index::Create(_) => {}
193                        }
194                    }
195
196                    if needs_recreation {
197                        Self::emit_table_recreation(
198                            &mut result,
199                            schema,
200                            previous,
201                            next,
202                            columns,
203                            capability,
204                        );
205                    } else {
206                        Self::emit_column_changes(
207                            &mut result,
208                            schema,
209                            previous.id,
210                            columns,
211                            capability,
212                            &renamed_types,
213                        );
214                    }
215
216                    // Create new indices after the column changes so they reference
217                    // the next schema's column definitions.
218                    for item in indices.iter() {
219                        match item {
220                            diff::Index::Create(index) => {
221                                result.push(Self::new(
222                                    Statement::create_index(index),
223                                    Cow::Borrowed(schema_diff.next()),
224                                ));
225                            }
226                            diff::Index::Alter { next, .. } => {
227                                result.push(Self::new(
228                                    Statement::create_index(next),
229                                    Cow::Borrowed(schema_diff.next()),
230                                ));
231                            }
232                            diff::Index::Drop(_) => {}
233                        }
234                    }
235                }
236            }
237        }
238        result
239    }
240
241    fn emit_table_recreation(
242        result: &mut Vec<Self>,
243        schema: Cow<'a, Schema>,
244        previous: &Table,
245        next: &Table,
246        columns: &[diff::Column<'_>],
247        capability: &Capability,
248    ) {
249        let current_name = schema.table(previous.id).name.clone();
250        let temp_name = format!("_toasty_new_{}", current_name);
251
252        // 1. PRAGMA foreign_keys = OFF
253        result.push(Self::new(
254            Statement::pragma_disable_foreign_keys(),
255            schema.clone(),
256        ));
257
258        // 2. CREATE TABLE temp with new schema
259        let temp_schema = {
260            let mut s = schema.as_ref().clone();
261            let t = s.table_mut(next.id);
262            t.name = temp_name.clone();
263            t.columns = next.columns.clone();
264            t.primary_key = next.primary_key.clone();
265            s
266        };
267        result.push(Self::new(
268            Statement::create_table(next, capability),
269            Cow::Owned(temp_schema),
270        ));
271
272        // 3. INSERT INTO temp SELECT ... FROM current
273        let column_mappings: Vec<(Name, Name)> = next
274            .columns
275            .iter()
276            .filter(|col| {
277                // Skip added columns (no source data)
278                !columns
279                    .iter()
280                    .any(|item| matches!(item, diff::Column::Add(c) if c.id == col.id))
281            })
282            .map(|col| {
283                let target_name = Name::from(&col.name[..]);
284                // Check if this column was renamed
285                let source_name = columns
286                    .iter()
287                    .find_map(|item| match item {
288                        diff::Column::Alter {
289                            previous: prev_col,
290                            next: next_col,
291                        } if next_col.id == col.id && prev_col.name != next_col.name => {
292                            Some(Name::from(&prev_col.name[..]))
293                        }
294                        _ => None,
295                    })
296                    .unwrap_or_else(|| Name::from(&col.name[..]));
297                (target_name, source_name)
298            })
299            .collect();
300
301        result.push(Self::new(
302            Statement::copy_table(
303                Name::from(current_name.as_str()),
304                Name::from(temp_name.as_str()),
305                column_mappings,
306            ),
307            schema.clone(),
308        ));
309
310        // 4. DROP TABLE current
311        result.push(Self::new(
312            DropTable {
313                name: Name::from(current_name.as_str()),
314                if_exists: false,
315            }
316            .into(),
317            schema.clone(),
318        ));
319
320        // 5. ALTER TABLE temp RENAME TO current
321        result.push(Self::new(
322            AlterTable {
323                name: Name::from(temp_name.as_str()),
324                action: AlterTableAction::RenameTo(Name::from(current_name.as_str())),
325            }
326            .into(),
327            schema.clone(),
328        ));
329
330        // 6. PRAGMA foreign_keys = ON
331        result.push(Self::new(
332            Statement::pragma_enable_foreign_keys(),
333            schema.clone(),
334        ));
335    }
336
337    fn emit_column_changes(
338        result: &mut Vec<Self>,
339        schema: Cow<'a, Schema>,
340        table: TableId,
341        columns: &[diff::Column<'_>],
342        capability: &Capability,
343        renamed_types: &HashSet<(&str, &str)>,
344    ) {
345        for item in columns.iter() {
346            match item {
347                diff::Column::Add(column) => {
348                    result.push(Self::new(
349                        Statement::add_column(table, column, capability),
350                        schema.clone(),
351                    ));
352                }
353                diff::Column::Drop(column) => {
354                    result.push(Self::new(Statement::drop_column(column), schema.clone()));
355                }
356                diff::Column::Alter {
357                    previous,
358                    next: col_next,
359                } => {
360                    // Skip column-level DDL for named enum changes handled by
361                    // ALTER TYPE.
362                    if capability.named_enum_types
363                        && is_named_enum_type_only_change(previous, col_next, renamed_types)
364                    {
365                        continue;
366                    }
367
368                    let changes = AlterColumnChanges::from_diff(previous, col_next);
369                    let changes = if capability.schema_mutations.alter_column_properties_atomic {
370                        vec![changes]
371                    } else {
372                        changes.split()
373                    };
374
375                    for changes in changes {
376                        result.push(Self::new(
377                            Statement::alter_column(previous, changes, capability),
378                            schema.clone(),
379                        ));
380                    }
381                }
382            }
383        }
384    }
385
386    /// Returns the DDL statement for this migration step.
387    pub fn statement(&self) -> &Statement {
388        &self.statement
389    }
390
391    /// Returns the schema snapshot this statement should be serialized against.
392    pub fn schema(&self) -> &Schema {
393        &self.schema
394    }
395}