2022-11-01 20:32:46 +00:00
|
|
|
// Migrations are constructed by domain, and stored in a table in the connection db with domain name,
|
|
|
|
// effected tables, actual query text, and order.
|
|
|
|
// If a migration is run and any of the query texts don't match, the app panics on startup (maybe fallback
|
|
|
|
// to creating a new db?)
|
|
|
|
// Otherwise any missing migrations are run on the connection
|
|
|
|
|
|
|
|
use anyhow::{anyhow, Result};
|
|
|
|
use indoc::{formatdoc, indoc};
|
|
|
|
|
|
|
|
use crate::connection::Connection;
|
|
|
|
|
2022-11-17 00:35:56 +00:00
|
|
|
impl Connection {
|
|
|
|
pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> {
|
2022-11-01 20:32:46 +00:00
|
|
|
// Setup the migrations table unconditionally
|
2022-11-17 00:35:56 +00:00
|
|
|
self.exec(indoc! {"
|
|
|
|
CREATE TABLE IF NOT EXISTS migrations (
|
|
|
|
domain TEXT,
|
|
|
|
step INTEGER,
|
|
|
|
migration TEXT
|
|
|
|
)"})?()?;
|
2022-11-01 20:32:46 +00:00
|
|
|
|
2022-11-07 01:00:34 +00:00
|
|
|
let completed_migrations =
|
2022-11-17 00:35:56 +00:00
|
|
|
self.select_bound::<&str, (String, usize, String)>(indoc! {"
|
2022-11-07 01:00:34 +00:00
|
|
|
SELECT domain, step, migration FROM migrations
|
|
|
|
WHERE domain = ?
|
|
|
|
ORDER BY step
|
2022-11-17 00:35:56 +00:00
|
|
|
"})?(domain)?;
|
2022-11-01 20:32:46 +00:00
|
|
|
|
2022-11-17 00:35:56 +00:00
|
|
|
let mut store_completed_migration =
|
|
|
|
self.exec_bound("INSERT INTO migrations (domain, step, migration) VALUES (?, ?, ?)")?;
|
2022-11-01 20:32:46 +00:00
|
|
|
|
2022-11-17 00:35:56 +00:00
|
|
|
for (index, migration) in migrations.iter().enumerate() {
|
2022-11-01 20:32:46 +00:00
|
|
|
if let Some((_, _, completed_migration)) = completed_migrations.get(index) {
|
|
|
|
if completed_migration != migration {
|
|
|
|
return Err(anyhow!(formatdoc! {"
|
|
|
|
Migration changed for {} at step {}
|
|
|
|
|
|
|
|
Stored migration:
|
|
|
|
{}
|
|
|
|
|
|
|
|
Proposed migration:
|
2022-11-17 00:35:56 +00:00
|
|
|
{}", domain, index, completed_migration, migration}));
|
2022-11-01 20:32:46 +00:00
|
|
|
} else {
|
|
|
|
// Migration already run. Continue
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-17 00:35:56 +00:00
|
|
|
self.exec(migration)?()?;
|
|
|
|
store_completed_migration((domain, index, *migration))?;
|
2022-11-01 20:32:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use indoc::indoc;
|
|
|
|
|
2022-11-17 00:35:56 +00:00
|
|
|
use crate::connection::Connection;
|
2022-11-01 20:32:46 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_migrations_are_added_to_table() {
|
|
|
|
let connection = Connection::open_memory("migrations_are_added_to_table");
|
|
|
|
|
|
|
|
// Create first migration with a single step and run it
|
2022-11-17 00:35:56 +00:00
|
|
|
connection
|
|
|
|
.migrate(
|
|
|
|
"test",
|
|
|
|
&[indoc! {"
|
|
|
|
CREATE TABLE test1 (
|
|
|
|
a TEXT,
|
|
|
|
b TEXT
|
|
|
|
)"}],
|
|
|
|
)
|
|
|
|
.unwrap();
|
2022-11-01 20:32:46 +00:00
|
|
|
|
|
|
|
// Verify it got added to the migrations table
|
|
|
|
assert_eq!(
|
|
|
|
&connection
|
2022-11-07 01:00:34 +00:00
|
|
|
.select::<String>("SELECT (migration) FROM migrations")
|
|
|
|
.unwrap()()
|
|
|
|
.unwrap()[..],
|
2022-11-17 00:35:56 +00:00
|
|
|
&[indoc! {"
|
2022-11-01 20:32:46 +00:00
|
|
|
CREATE TABLE test1 (
|
|
|
|
a TEXT,
|
|
|
|
b TEXT
|
2022-11-17 00:35:56 +00:00
|
|
|
)"}],
|
|
|
|
);
|
|
|
|
|
|
|
|
// Add another step to the migration and run it again
|
|
|
|
connection
|
|
|
|
.migrate(
|
|
|
|
"test",
|
|
|
|
&[
|
|
|
|
indoc! {"
|
|
|
|
CREATE TABLE test1 (
|
|
|
|
a TEXT,
|
|
|
|
b TEXT
|
|
|
|
)"},
|
|
|
|
indoc! {"
|
|
|
|
CREATE TABLE test2 (
|
|
|
|
c TEXT,
|
|
|
|
d TEXT
|
|
|
|
)"},
|
|
|
|
],
|
|
|
|
)
|
|
|
|
.unwrap();
|
2022-11-01 20:32:46 +00:00
|
|
|
|
|
|
|
// Verify it is also added to the migrations table
|
|
|
|
assert_eq!(
|
|
|
|
&connection
|
2022-11-07 01:00:34 +00:00
|
|
|
.select::<String>("SELECT (migration) FROM migrations")
|
|
|
|
.unwrap()()
|
|
|
|
.unwrap()[..],
|
2022-11-17 00:35:56 +00:00
|
|
|
&[
|
|
|
|
indoc! {"
|
|
|
|
CREATE TABLE test1 (
|
|
|
|
a TEXT,
|
|
|
|
b TEXT
|
|
|
|
)"},
|
|
|
|
indoc! {"
|
|
|
|
CREATE TABLE test2 (
|
|
|
|
c TEXT,
|
|
|
|
d TEXT
|
|
|
|
)"},
|
|
|
|
],
|
2022-11-01 20:32:46 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_migration_setup_works() {
|
|
|
|
let connection = Connection::open_memory("migration_setup_works");
|
|
|
|
|
|
|
|
connection
|
2022-11-07 01:00:34 +00:00
|
|
|
.exec(indoc! {"
|
|
|
|
CREATE TABLE IF NOT EXISTS migrations (
|
2022-11-01 20:32:46 +00:00
|
|
|
domain TEXT,
|
|
|
|
step INTEGER,
|
|
|
|
migration TEXT
|
|
|
|
);"})
|
2022-11-07 01:00:34 +00:00
|
|
|
.unwrap()()
|
|
|
|
.unwrap();
|
2022-11-01 20:32:46 +00:00
|
|
|
|
|
|
|
let mut store_completed_migration = connection
|
2022-11-17 00:35:56 +00:00
|
|
|
.exec_bound::<(&str, usize, String)>(indoc! {"
|
2022-11-01 20:32:46 +00:00
|
|
|
INSERT INTO migrations (domain, step, migration)
|
|
|
|
VALUES (?, ?, ?)"})
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
let domain = "test_domain";
|
|
|
|
for i in 0..5 {
|
|
|
|
// Create a table forcing a schema change
|
|
|
|
connection
|
2022-11-07 01:00:34 +00:00
|
|
|
.exec(&format!("CREATE TABLE table{} ( test TEXT );", i))
|
|
|
|
.unwrap()()
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
store_completed_migration((domain, i, i.to_string())).unwrap();
|
2022-11-01 20:32:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn migrations_dont_rerun() {
|
|
|
|
let connection = Connection::open_memory("migrations_dont_rerun");
|
|
|
|
|
2022-11-17 00:35:56 +00:00
|
|
|
// Create migration which clears a tabl
|
2022-11-01 20:32:46 +00:00
|
|
|
|
|
|
|
// Manually create the table for that migration with a row
|
|
|
|
connection
|
|
|
|
.exec(indoc! {"
|
2022-11-07 01:00:34 +00:00
|
|
|
CREATE TABLE test_table (
|
|
|
|
test_column INTEGER
|
|
|
|
);"})
|
|
|
|
.unwrap()()
|
|
|
|
.unwrap();
|
|
|
|
connection
|
|
|
|
.exec(indoc! {"
|
|
|
|
INSERT INTO test_table (test_column) VALUES (1);"})
|
|
|
|
.unwrap()()
|
|
|
|
.unwrap();
|
2022-11-01 20:32:46 +00:00
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
connection
|
2022-11-07 01:00:34 +00:00
|
|
|
.select_row::<usize>("SELECT * FROM test_table")
|
|
|
|
.unwrap()()
|
|
|
|
.unwrap(),
|
|
|
|
Some(1)
|
2022-11-01 20:32:46 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
// Run the migration verifying that the row got dropped
|
2022-11-17 00:35:56 +00:00
|
|
|
connection
|
|
|
|
.migrate("test", &["DELETE FROM test_table"])
|
|
|
|
.unwrap();
|
2022-11-01 20:32:46 +00:00
|
|
|
assert_eq!(
|
|
|
|
connection
|
2022-11-07 01:00:34 +00:00
|
|
|
.select_row::<usize>("SELECT * FROM test_table")
|
|
|
|
.unwrap()()
|
|
|
|
.unwrap(),
|
|
|
|
None
|
2022-11-01 20:32:46 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
// Recreate the dropped row
|
|
|
|
connection
|
|
|
|
.exec("INSERT INTO test_table (test_column) VALUES (2)")
|
2022-11-07 01:00:34 +00:00
|
|
|
.unwrap()()
|
|
|
|
.unwrap();
|
2022-11-01 20:32:46 +00:00
|
|
|
|
|
|
|
// Run the same migration again and verify that the table was left unchanged
|
2022-11-17 00:35:56 +00:00
|
|
|
connection
|
|
|
|
.migrate("test", &["DELETE FROM test_table"])
|
|
|
|
.unwrap();
|
2022-11-01 20:32:46 +00:00
|
|
|
assert_eq!(
|
|
|
|
connection
|
2022-11-07 01:00:34 +00:00
|
|
|
.select_row::<usize>("SELECT * FROM test_table")
|
|
|
|
.unwrap()()
|
|
|
|
.unwrap(),
|
|
|
|
Some(2)
|
2022-11-01 20:32:46 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn changed_migration_fails() {
|
|
|
|
let connection = Connection::open_memory("changed_migration_fails");
|
|
|
|
|
|
|
|
// Create a migration with two steps and run it
|
2022-11-17 00:35:56 +00:00
|
|
|
connection
|
|
|
|
.migrate(
|
|
|
|
"test migration",
|
|
|
|
&[
|
|
|
|
indoc! {"
|
2022-11-01 20:32:46 +00:00
|
|
|
CREATE TABLE test (
|
|
|
|
col INTEGER
|
|
|
|
)"},
|
2022-11-17 00:35:56 +00:00
|
|
|
indoc! {"
|
|
|
|
INSERT INTO test (col) VALUES (1)"},
|
|
|
|
],
|
|
|
|
)
|
|
|
|
.unwrap();
|
2022-11-01 20:32:46 +00:00
|
|
|
|
|
|
|
// Create another migration with the same domain but different steps
|
2022-11-17 00:35:56 +00:00
|
|
|
let second_migration_result = connection.migrate(
|
2022-11-01 20:32:46 +00:00
|
|
|
"test migration",
|
|
|
|
&[
|
|
|
|
indoc! {"
|
|
|
|
CREATE TABLE test (
|
|
|
|
color INTEGER
|
|
|
|
)"},
|
|
|
|
indoc! {"
|
|
|
|
INSERT INTO test (color) VALUES (1)"},
|
|
|
|
],
|
2022-11-17 00:35:56 +00:00
|
|
|
);
|
2022-11-01 20:32:46 +00:00
|
|
|
|
|
|
|
// Verify new migration returns error when run
|
|
|
|
assert!(second_migration_result.is_err())
|
|
|
|
}
|
|
|
|
}
|