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}