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