migrations.rs

  1// Migrations are constructed by domain, and stored in a table in the connection db with domain name,
  2// effected tables, actual query text, and order.
  3// If a migration is run and any of the query texts don't match, the app panics on startup (maybe fallback
  4// to creating a new db?)
  5// Otherwise any missing migrations are run on the connection
  6
  7use anyhow::{anyhow, Result};
  8use indoc::{formatdoc, indoc};
  9
 10use crate::connection::Connection;
 11
 12impl Connection {
 13    pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> {
 14        self.with_savepoint("migrating", || {
 15            // Setup the migrations table unconditionally
 16            self.exec(indoc! {"
 17                CREATE TABLE IF NOT EXISTS migrations (
 18                    domain TEXT,
 19                    step INTEGER,
 20                    migration TEXT
 21                )"})?()?;
 22
 23            let completed_migrations =
 24                self.select_bound::<&str, (String, usize, String)>(indoc! {"
 25                    SELECT domain, step, migration FROM migrations
 26                    WHERE domain = ?
 27                    ORDER BY step
 28                    "})?(domain)?;
 29
 30            let mut store_completed_migration = self
 31                .exec_bound("INSERT INTO migrations (domain, step, migration) VALUES (?, ?, ?)")?;
 32
 33            for (index, migration) in migrations.iter().enumerate() {
 34                if let Some((_, _, completed_migration)) = completed_migrations.get(index) {
 35                    if completed_migration != migration {
 36                        return Err(anyhow!(formatdoc! {"
 37                            Migration changed for {} at step {}
 38                            
 39                            Stored migration:
 40                            {}
 41                            
 42                            Proposed migration:
 43                            {}", domain, index, completed_migration, migration}));
 44                    } else {
 45                        // Migration already run. Continue
 46                        continue;
 47                    }
 48                }
 49
 50                self.exec(migration)?()?;
 51                store_completed_migration((domain, index, *migration))?;
 52            }
 53
 54            Ok(())
 55        })
 56    }
 57}
 58
 59#[cfg(test)]
 60mod test {
 61    use indoc::indoc;
 62
 63    use crate::connection::Connection;
 64
 65    #[test]
 66    fn test_migrations_are_added_to_table() {
 67        let connection = Connection::open_memory(Some("migrations_are_added_to_table"));
 68
 69        // Create first migration with a single step and run it
 70        connection
 71            .migrate(
 72                "test",
 73                &[indoc! {"
 74                CREATE TABLE test1 (
 75                    a TEXT,
 76                    b TEXT
 77                )"}],
 78            )
 79            .unwrap();
 80
 81        // Verify it got added to the migrations table
 82        assert_eq!(
 83            &connection
 84                .select::<String>("SELECT (migration) FROM migrations")
 85                .unwrap()()
 86            .unwrap()[..],
 87            &[indoc! {"
 88                CREATE TABLE test1 (
 89                    a TEXT,
 90                    b TEXT
 91                )"}],
 92        );
 93
 94        // Add another step to the migration and run it again
 95        connection
 96            .migrate(
 97                "test",
 98                &[
 99                    indoc! {"
100                    CREATE TABLE test1 (
101                        a TEXT,
102                        b TEXT
103                    )"},
104                    indoc! {"
105                    CREATE TABLE test2 (
106                        c TEXT,
107                        d TEXT
108                    )"},
109                ],
110            )
111            .unwrap();
112
113        // Verify it is also added to the migrations table
114        assert_eq!(
115            &connection
116                .select::<String>("SELECT (migration) FROM migrations")
117                .unwrap()()
118            .unwrap()[..],
119            &[
120                indoc! {"
121                    CREATE TABLE test1 (
122                        a TEXT,
123                        b TEXT
124                    )"},
125                indoc! {"
126                    CREATE TABLE test2 (
127                        c TEXT,
128                        d TEXT
129                    )"},
130            ],
131        );
132    }
133
134    #[test]
135    fn test_migration_setup_works() {
136        let connection = Connection::open_memory(Some("migration_setup_works"));
137
138        connection
139            .exec(indoc! {"
140                CREATE TABLE IF NOT EXISTS migrations (
141                    domain TEXT,
142                    step INTEGER,
143                    migration TEXT
144                );"})
145            .unwrap()()
146        .unwrap();
147
148        let mut store_completed_migration = connection
149            .exec_bound::<(&str, usize, String)>(indoc! {"
150                INSERT INTO migrations (domain, step, migration)
151                VALUES (?, ?, ?)"})
152            .unwrap();
153
154        let domain = "test_domain";
155        for i in 0..5 {
156            // Create a table forcing a schema change
157            connection
158                .exec(&format!("CREATE TABLE table{} ( test TEXT );", i))
159                .unwrap()()
160            .unwrap();
161
162            store_completed_migration((domain, i, i.to_string())).unwrap();
163        }
164    }
165
166    #[test]
167    fn migrations_dont_rerun() {
168        let connection = Connection::open_memory(Some("migrations_dont_rerun"));
169
170        // Create migration which clears a tabl
171
172        // Manually create the table for that migration with a row
173        connection
174            .exec(indoc! {"
175                CREATE TABLE test_table (
176                    test_column INTEGER
177                );"})
178            .unwrap()()
179        .unwrap();
180        connection
181            .exec(indoc! {"
182            INSERT INTO test_table (test_column) VALUES (1);"})
183            .unwrap()()
184        .unwrap();
185
186        assert_eq!(
187            connection
188                .select_row::<usize>("SELECT * FROM test_table")
189                .unwrap()()
190            .unwrap(),
191            Some(1)
192        );
193
194        // Run the migration verifying that the row got dropped
195        connection
196            .migrate("test", &["DELETE FROM test_table"])
197            .unwrap();
198        assert_eq!(
199            connection
200                .select_row::<usize>("SELECT * FROM test_table")
201                .unwrap()()
202            .unwrap(),
203            None
204        );
205
206        // Recreate the dropped row
207        connection
208            .exec("INSERT INTO test_table (test_column) VALUES (2)")
209            .unwrap()()
210        .unwrap();
211
212        // Run the same migration again and verify that the table was left unchanged
213        connection
214            .migrate("test", &["DELETE FROM test_table"])
215            .unwrap();
216        assert_eq!(
217            connection
218                .select_row::<usize>("SELECT * FROM test_table")
219                .unwrap()()
220            .unwrap(),
221            Some(2)
222        );
223    }
224
225    #[test]
226    fn changed_migration_fails() {
227        let connection = Connection::open_memory(Some("changed_migration_fails"));
228
229        // Create a migration with two steps and run it
230        connection
231            .migrate(
232                "test migration",
233                &[
234                    indoc! {"
235                CREATE TABLE test (
236                    col INTEGER
237                )"},
238                    indoc! {"
239                    INSERT INTO test (col) VALUES (1)"},
240                ],
241            )
242            .unwrap();
243
244        // Create another migration with the same domain but different steps
245        let second_migration_result = connection.migrate(
246            "test migration",
247            &[
248                indoc! {"
249                CREATE TABLE test (
250                    color INTEGER
251                )"},
252                indoc! {"
253                INSERT INTO test (color) VALUES (1)"},
254            ],
255        );
256
257        // Verify new migration returns error when run
258        assert!(second_migration_result.is_err())
259    }
260}