Skip to main content

toasty_sql/
migration.rs

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