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