sqlmo/
migrate.rs

1use std::collections::HashMap;
2
3use crate::query::{AlterTable, Update};
4use anyhow::Result;
5
6use crate::query::AlterAction;
7use crate::query::CreateIndex;
8use crate::query::CreateTable;
9use crate::query::DropTable;
10use crate::schema::Schema;
11use crate::{Dialect, ToSql};
12
13#[derive(Debug, Clone, Default)]
14pub struct MigrationOptions {
15    pub debug: bool,
16    pub allow_destructive: bool,
17}
18
19pub fn migrate(current: Schema, desired: Schema, options: &MigrationOptions) -> Result<Migration> {
20    let current_tables = current
21        .tables
22        .iter()
23        .map(|t| (&t.name, t))
24        .collect::<HashMap<_, _>>();
25    let desired_tables = desired
26        .tables
27        .iter()
28        .map(|t| (&t.name, t))
29        .collect::<HashMap<_, _>>();
30
31    let mut debug_results = vec![];
32    let mut statements = Vec::new();
33    // new tables
34    for (_name, table) in desired_tables
35        .iter()
36        .filter(|(name, _)| !current_tables.contains_key(*name))
37    {
38        let statement = Statement::CreateTable(CreateTable::from_table(table));
39        statements.push(statement);
40    }
41
42    // alter existing tables
43    for (name, desired_table) in desired_tables
44        .iter()
45        .filter(|(name, _)| current_tables.contains_key(*name))
46    {
47        let current_table = current_tables[name];
48        let current_columns = current_table
49            .columns
50            .iter()
51            .map(|c| (&c.name, c))
52            .collect::<HashMap<_, _>>();
53        // add columns
54        let mut actions = vec![];
55        for desired_column in desired_table.columns.iter() {
56            if let Some(current) = current_columns.get(&desired_column.name) {
57                if current.nullable != desired_column.nullable {
58                    actions.push(AlterAction::set_nullable(
59                        desired_column.name.clone(),
60                        desired_column.nullable,
61                    ));
62                }
63                if !desired_column.typ.lossy_eq(&current.typ) {
64                    actions.push(AlterAction::set_type(
65                        desired_column.name.clone(),
66                        desired_column.typ.clone(),
67                    ));
68                };
69                if desired_column.constraint != current.constraint {
70                    if let Some(c) = &desired_column.constraint {
71                        let name = desired_column.name.clone();
72                        actions.push(AlterAction::add_constraint(&desired_table.name, name, c.clone()));
73                    }
74                }
75            } else {
76                // add the column can be in 1 step if the column is nullable
77                if desired_column.nullable {
78                    actions.push(AlterAction::AddColumn {
79                        column: desired_column.clone(),
80                    });
81                } else {
82                    let mut nullable = desired_column.clone();
83                    nullable.nullable = true;
84                    statements.push(Statement::AlterTable(AlterTable {
85                        schema: desired_table.schema.clone(),
86                        name: desired_table.name.clone(),
87                        actions: vec![AlterAction::AddColumn { column: nullable }],
88                    }));
89                    statements.push(Statement::Update(
90                        Update::new(name)
91                            .set(
92                                &desired_column.name,
93                                "/* TODO set a value before setting the column to null */",
94                            )
95                            .where_(crate::query::Where::raw("true")),
96                    ));
97                    statements.push(Statement::AlterTable(AlterTable {
98                        schema: desired_table.schema.clone(),
99                        name: desired_table.name.clone(),
100                        actions: vec![AlterAction::AlterColumn {
101                            name: desired_column.name.clone(),
102                            action: crate::query::AlterColumnAction::SetNullable(false),
103                        }],
104                    }));
105                }
106            }
107        }
108        if actions.is_empty() {
109            debug_results.push(DebugResults::TablesIdentical(name.to_string()));
110        } else {
111            statements.push(Statement::AlterTable(AlterTable {
112                schema: desired_table.schema.clone(),
113                name: desired_table.name.clone(),
114                actions,
115            }));
116        }
117    }
118
119    for (_name, current_table) in current_tables
120        .iter()
121        .filter(|(name, _)| !desired_tables.contains_key(*name))
122    {
123        if options.allow_destructive {
124            statements.push(Statement::DropTable(DropTable {
125                schema: current_table.schema.clone(),
126                name: current_table.name.clone(),
127            }));
128        } else {
129            debug_results.push(DebugResults::SkippedDropTable(current_table.name.clone()));
130        }
131    }
132
133    Ok(Migration {
134        statements,
135        debug_results,
136    })
137}
138
139#[derive(Debug)]
140pub struct Migration {
141    pub statements: Vec<Statement>,
142    pub debug_results: Vec<DebugResults>,
143}
144
145impl Migration {
146    pub fn is_empty(&self) -> bool {
147        self.statements.is_empty()
148    }
149
150    pub fn set_schema(&mut self, schema_name: &str) {
151        for statement in &mut self.statements {
152            statement.set_schema(schema_name);
153        }
154    }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub enum Statement {
159    CreateTable(CreateTable),
160    CreateIndex(CreateIndex),
161    AlterTable(AlterTable),
162    DropTable(DropTable),
163    Update(Update),
164}
165
166impl Statement {
167    pub fn set_schema(&mut self, schema_name: &str) {
168        match self {
169            Statement::CreateTable(s) => {
170                s.schema = Some(schema_name.to_string());
171            }
172            Statement::AlterTable(s) => {
173                s.schema = Some(schema_name.to_string());
174            }
175            Statement::DropTable(s) => {
176                s.schema = Some(schema_name.to_string());
177            }
178            Statement::CreateIndex(s) => {
179                s.schema = Some(schema_name.to_string());
180            }
181            Statement::Update(s) => {
182                s.schema = Some(schema_name.to_string());
183            }
184        }
185    }
186
187    pub fn table_name(&self) -> &str {
188        match self {
189            Statement::CreateTable(s) => &s.name,
190            Statement::AlterTable(s) => &s.name,
191            Statement::DropTable(s) => &s.name,
192            Statement::CreateIndex(s) => &s.table,
193            Statement::Update(s) => &s.table,
194        }
195    }
196}
197
198impl ToSql for Statement {
199    fn write_sql(&self, buf: &mut String, dialect: Dialect) {
200        use Statement::*;
201        match self {
202            CreateTable(c) => c.write_sql(buf, dialect),
203            CreateIndex(c) => c.write_sql(buf, dialect),
204            AlterTable(a) => a.write_sql(buf, dialect),
205            DropTable(d) => d.write_sql(buf, dialect),
206            Update(u) => u.write_sql(buf, dialect),
207        }
208    }
209}
210
211#[derive(Debug)]
212pub enum DebugResults {
213    TablesIdentical(String),
214    SkippedDropTable(String),
215}
216
217impl DebugResults {
218    pub fn table_name(&self) -> &str {
219        match self {
220            DebugResults::TablesIdentical(name) => name,
221            DebugResults::SkippedDropTable(name) => name,
222        }
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    use crate::Table;
231
232    #[test]
233    fn test_drop_table() {
234        let empty_schema = Schema::default();
235        let mut single_table_schema = Schema::default();
236        let t = Table::new("new_table");
237        single_table_schema.tables.push(t.clone());
238        let mut allow_destructive_options = MigrationOptions::default();
239        allow_destructive_options.allow_destructive = true;
240
241        let mut migrations = migrate(
242            single_table_schema,
243            empty_schema,
244            &allow_destructive_options,
245        )
246            .unwrap();
247
248        let statement = migrations.statements.pop().unwrap();
249        let expected_statement = Statement::DropTable(DropTable {
250            schema: t.schema,
251            name: t.name,
252        });
253
254        assert_eq!(statement, expected_statement);
255    }
256
257    #[test]
258    fn test_drop_table_without_destructive_operations() {
259        let empty_schema = Schema::default();
260        let mut single_table_schema = Schema::default();
261        let t = Table::new("new_table");
262        single_table_schema.tables.push(t.clone());
263        let options = MigrationOptions::default();
264
265        let migrations = migrate(single_table_schema, empty_schema, &options).unwrap();
266        assert!(migrations.statements.is_empty());
267    }
268}