toasty_sql/
migration.rs

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