1use crate::db::sql_fns;
6use crate::sync::address::name_utils::{join_name_parts, NameParts};
7use error_support::debug;
8use rusqlite::{functions::FunctionFlags, Connection, Transaction};
9use sql_support::open_database::{ConnectionInitializer, Error, Result};
10
11pub const ADDRESS_COMMON_COLS: &str = "
12 guid,
13 name,
14 organization,
15 street_address,
16 address_level3,
17 address_level2,
18 address_level1,
19 postal_code,
20 country,
21 tel,
22 email,
23 time_created,
24 time_last_used,
25 time_last_modified,
26 times_used";
27
28pub const ADDRESS_COMMON_VALS: &str = "
29 :guid,
30 :name,
31 :organization,
32 :street_address,
33 :address_level3,
34 :address_level2,
35 :address_level1,
36 :postal_code,
37 :country,
38 :tel,
39 :email,
40 :time_created,
41 :time_last_used,
42 :time_last_modified,
43 :times_used";
44
45pub const CREDIT_CARD_COMMON_COLS: &str = "
46 guid,
47 cc_name,
48 cc_number_enc,
49 cc_number_last_4,
50 cc_exp_month,
51 cc_exp_year,
52 cc_type,
53 time_created,
54 time_last_used,
55 time_last_modified,
56 times_used";
57
58pub const CREDIT_CARD_COMMON_VALS: &str = "
59 :guid,
60 :cc_name,
61 :cc_number_enc,
62 :cc_number_last_4,
63 :cc_exp_month,
64 :cc_exp_year,
65 :cc_type,
66 :time_created,
67 :time_last_used,
68 :time_last_modified,
69 :times_used";
70
71const CREATE_SHARED_SCHEMA_SQL: &str = include_str!("../../sql/create_shared_schema.sql");
72const CREATE_SHARED_TRIGGERS_SQL: &str = include_str!("../../sql/create_shared_triggers.sql");
73const CREATE_SYNC_TEMP_TABLES_SQL: &str = include_str!("../../sql/create_sync_temp_tables.sql");
74
75pub struct AutofillConnectionInitializer;
76
77impl ConnectionInitializer for AutofillConnectionInitializer {
78 const NAME: &'static str = "autofill db";
79 const END_VERSION: u32 = 4;
80
81 fn prepare(&self, conn: &Connection, _db_empty: bool) -> Result<()> {
82 define_functions(conn)?;
83
84 let initial_pragmas = "
85 -- use in-memory storage
86 PRAGMA temp_store = 2;
87 -- use write-ahead logging
88 PRAGMA journal_mode = WAL;
89 -- autofill does not use foreign keys at present but this is probably a good pragma to set
90 PRAGMA foreign_keys = ON;
91 ";
92 conn.execute_batch(initial_pragmas)?;
93
94 conn.set_prepared_statement_cache_capacity(128);
95 Ok(())
96 }
97
98 fn init(&self, db: &Transaction<'_>) -> Result<()> {
99 Ok(db.execute_batch(CREATE_SHARED_SCHEMA_SQL)?)
100 }
101
102 fn upgrade_from(&self, db: &Transaction<'_>, version: u32) -> Result<()> {
103 match version {
104 0 => upgrade_from_v0(db),
107 1 => upgrade_from_v1(db),
108 2 => upgrade_from_v2(db),
109 3 => upgrade_from_v3(db),
110 _ => Err(Error::IncompatibleVersion(version)),
111 }
112 }
113
114 fn finish(&self, db: &Connection) -> Result<()> {
115 Ok(db.execute_batch(CREATE_SHARED_TRIGGERS_SQL)?)
116 }
117}
118
119fn define_functions(c: &Connection) -> Result<()> {
120 c.create_scalar_function(
121 "generate_guid",
122 0,
123 FunctionFlags::SQLITE_UTF8,
124 sql_fns::generate_guid,
125 )?;
126 c.create_scalar_function("now", 0, FunctionFlags::SQLITE_UTF8, sql_fns::now)?;
127
128 Ok(())
129}
130
131fn upgrade_from_v0(db: &Connection) -> Result<()> {
132 db.execute_batch(
141 "
142 DROP TABLE IF EXISTS credit_cards_data;
143 CREATE TABLE credit_cards_data (
144 guid TEXT NOT NULL PRIMARY KEY CHECK(length(guid) != 0),
145 cc_name TEXT NOT NULL,
146 cc_number_enc TEXT NOT NULL CHECK(length(cc_number_enc) > 20),
147 cc_number_last_4 TEXT NOT NULL CHECK(length(cc_number_last_4) <= 4),
148 cc_exp_month INTEGER,
149 cc_exp_year INTEGER,
150 cc_type TEXT NOT NULL,
151 time_created INTEGER NOT NULL,
152 time_last_used INTEGER,
153 time_last_modified INTEGER NOT NULL,
154 times_used INTEGER NOT NULL,
155 sync_change_counter INTEGER NOT NULL
156 );
157 ",
158 )?;
159 Ok(())
160}
161
162fn upgrade_from_v1(db: &Connection) -> Result<()> {
163 db.execute_batch(
169 "
170 CREATE TABLE new_credit_cards_data (
171 guid TEXT NOT NULL PRIMARY KEY CHECK(length(guid) != 0),
172 cc_name TEXT NOT NULL,
173 cc_number_enc TEXT NOT NULL CHECK(length(cc_number_enc) > 20 OR cc_number_enc == ''),
174 cc_number_last_4 TEXT NOT NULL CHECK(length(cc_number_last_4) <= 4),
175 cc_exp_month INTEGER,
176 cc_exp_year INTEGER,
177 cc_type TEXT NOT NULL,
178 time_created INTEGER NOT NULL,
179 time_last_used INTEGER,
180 time_last_modified INTEGER NOT NULL,
181 times_used INTEGER NOT NULL,
182 sync_change_counter INTEGER NOT NULL
183 );
184 INSERT INTO new_credit_cards_data(guid, cc_name, cc_number_enc, cc_number_last_4, cc_exp_month,
185 cc_exp_year, cc_type, time_created, time_last_used, time_last_modified, times_used,
186 sync_change_counter)
187 SELECT guid, cc_name, cc_number_enc, cc_number_last_4, cc_exp_month, cc_exp_year, cc_type,
188 time_created, time_last_used, time_last_modified, times_used, sync_change_counter
189 FROM credit_cards_data;
190 DROP TABLE credit_cards_data;
191 ALTER TABLE new_credit_cards_data RENAME to credit_cards_data;
192 ")?;
193 Ok(())
194}
195
196fn upgrade_from_v2(db: &Connection) -> Result<()> {
197 db.execute_batch("ALTER TABLE addresses_data ADD COLUMN name TEXT NOT NULL DEFAULT ''")?;
198
199 let mut stmt =
200 db.prepare("SELECT guid, given_name, additional_name, family_name FROM addresses_data")?;
201 let rows = stmt.query_map([], |row| {
202 Ok((
203 row.get::<_, String>("guid")?,
204 row.get::<_, String>("given_name")?,
205 row.get::<_, String>("additional_name")?,
206 row.get::<_, String>("family_name")?,
207 ))
208 })?;
209
210 for row in rows {
211 let (guid, given, middle, family) = row?;
212 let full_name = join_name_parts(&NameParts {
213 given,
214 middle,
215 family,
216 });
217
218 db.execute(
219 "UPDATE addresses_data SET name = (:name) WHERE guid = (:guid)",
220 rusqlite::named_params! { ":name": full_name, ":guid": guid},
221 )?;
222 }
223
224 db.execute_batch(
225 "
226 ALTER TABLE addresses_data DROP COLUMN given_name;
227 ALTER TABLE addresses_data DROP COLUMN additional_name;
228 ALTER TABLE addresses_data DROP COLUMN family_name;
229 ",
230 )?;
231
232 Ok(())
233}
234
235fn upgrade_from_v3(db: &Connection) -> Result<()> {
236 let migration_string: &str = include_str!("../../sql/migrations/v4_migration.sql");
237 db.execute_batch(migration_string)?;
238 Ok(())
239}
240
241pub fn create_empty_sync_temp_tables(db: &Connection) -> Result<()> {
242 debug!("Initializing sync temp tables");
243 db.execute_batch(CREATE_SYNC_TEMP_TABLES_SQL)?;
244 Ok(())
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use crate::db::addresses::get_address;
251 use crate::db::credit_cards::get_credit_card;
252 use crate::db::test::new_mem_db;
253 use sql_support::open_database::test_utils::MigratedDatabaseFile;
254 use sync_guid::Guid;
255 use types::Timestamp;
256
257 const CREATE_V0_DB: &str = include_str!("../../sql/tests/create_v0_db.sql");
258 const CREATE_V1_DB: &str = include_str!("../../sql/tests/create_v1_db.sql");
259 const CREATE_V2_DB: &str = include_str!("../../sql/tests/create_v2_db.sql");
260 const CREATE_V3_DB: &str = include_str!("../../sql/tests/create_v3_db.sql");
261
262 #[test]
263 fn test_create_schema_twice() {
264 let db = new_mem_db();
265 db.execute_batch(CREATE_SHARED_SCHEMA_SQL)
266 .expect("should allow running main schema creation twice");
267 db.execute_batch(CREATE_SYNC_TEMP_TABLES_SQL)
269 .expect("should allow running sync temp tables first time");
270 db.execute_batch(CREATE_SYNC_TEMP_TABLES_SQL)
271 .expect("should allow running sync temp tables second time");
272 }
273
274 #[test]
275 fn test_all_upgrades() {
276 let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V1_DB);
278 db_file.run_all_upgrades();
279 let conn = db_file.open();
280
281 let cc = get_credit_card(&conn, &Guid::new("A")).unwrap();
283 assert_eq!(cc.guid, "A");
284 assert_eq!(cc.cc_name, "Jane Doe");
285 assert_eq!(cc.cc_number_enc, "012345678901234567890");
286 assert_eq!(cc.cc_number_last_4, "1234");
287 assert_eq!(cc.cc_exp_month, 1);
288 assert_eq!(cc.cc_exp_year, 2020);
289 assert_eq!(cc.cc_type, "visa");
290 assert_eq!(cc.metadata.time_created, Timestamp(0));
291 assert_eq!(cc.metadata.time_last_used, Timestamp(1));
292 assert_eq!(cc.metadata.time_last_modified, Timestamp(2));
293 assert_eq!(cc.metadata.times_used, 3);
294 assert_eq!(cc.metadata.sync_change_counter, 0);
295
296 let address = get_address(&conn, &Guid::new("A")).unwrap();
297 assert_eq!(address.guid, "A");
298 assert_eq!(address.name, "Jane JaneDoe2 Doe");
299 assert_eq!(address.organization, "Mozilla");
300 assert_eq!(address.street_address, "123 Maple lane");
301 assert_eq!(address.address_level3, "Shelbyville");
302 assert_eq!(address.address_level2, "Springfield");
303 assert_eq!(address.address_level1, "MA");
304 assert_eq!(address.postal_code, "12345");
305 assert_eq!(address.country, "US");
306 assert_eq!(address.tel, "01-234-567-8000");
307 assert_eq!(address.email, "jane@hotmail.com");
308 assert_eq!(address.metadata.time_created, Timestamp(0));
309 assert_eq!(address.metadata.time_last_used, Timestamp(1));
310 assert_eq!(address.metadata.time_last_modified, Timestamp(2));
311 assert_eq!(address.metadata.times_used, 3);
312 assert_eq!(address.metadata.sync_change_counter, 0);
313 }
314
315 #[test]
316 fn test_upgrade_version_0() {
317 let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V0_DB);
318 let select_cc_number_enc = "SELECT cc_number_enc from credit_cards_data";
321 db_file
322 .open()
323 .execute_batch(select_cc_number_enc)
324 .expect_err("select should fail due to bad field name");
325
326 db_file.upgrade_to(1);
327
328 db_file
329 .open()
330 .execute_batch(select_cc_number_enc)
331 .expect("select should now work");
332 }
333
334 #[test]
335 fn test_upgrade_version_1() {
336 let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V1_DB);
337
338 db_file.upgrade_to(2);
339 let db = db_file.open();
340
341 db.execute("UPDATE credit_cards_data SET cc_number_enc=''", [])
343 .expect("blank cc_number_enc should be valid");
344 db.execute("UPDATE credit_cards_data SET cc_number_enc='x'", [])
345 .expect_err("cc_number_enc should be invalid");
346 }
347
348 #[test]
349 fn test_upgrade_version_2() {
350 let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V2_DB);
351 let db = db_file.open();
352
353 db.execute_batch("SELECT name from addresses_data")
354 .expect_err("select should fail");
355 db.execute_batch("SELECT street_address from addresses_data")
356 .expect("street_address should work");
357 db.execute_batch("SELECT additional_name from addresses_data")
358 .expect("additional_name should work");
359 db.execute_batch("SELECT family_name from addresses_data")
360 .expect("family_name should work");
361
362 db_file.upgrade_to(3);
363
364 db.execute_batch("SELECT name from addresses_data")
365 .expect("select name should now work");
366 db.execute_batch("SELECT given_name from addresses_data")
367 .expect_err("given_name should fail");
368 db.execute_batch("SELECT additional_name from addresses_data")
369 .expect_err("additional_name should fail");
370 db.execute_batch("SELECT family_name from addresses_data")
371 .expect_err("family_name should fail");
372
373 let mut address = get_address(&db, &Guid::new("A")).unwrap();
374 assert_eq!(address.guid, "A");
375 assert_eq!(address.name, "Jane John Doe");
376
377 address = get_address(&db, &Guid::new("B")).unwrap();
378 assert_eq!(address.guid, "B");
379
380 assert_eq!(address.name, "");
382 }
383
384 #[test]
385 fn test_upgrade_version_3() {
386 let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V3_DB);
387 let db = db_file.open();
388
389 let mut address = get_address(&db, &Guid::new("A")).unwrap();
391 assert_eq!(address.guid, "A");
392 assert_eq!(address.name, "Jane John Doe");
393 assert_eq!(address.address_level1, "Massachusetts");
394
395 address = get_address(&db, &Guid::new("B")).unwrap();
396 assert_eq!(address.guid, "B");
397 assert_eq!(address.address_level1, "Ontario");
398
399 db_file.upgrade_to(4);
400
401 address = get_address(&db, &Guid::new("A")).unwrap();
403 assert_eq!(address.guid, "A");
404 assert_eq!(address.name, "Jane John Doe");
405 assert_eq!(address.address_level1, "MA");
406
407 address = get_address(&db, &Guid::new("B")).unwrap();
408 assert_eq!(address.guid, "B");
409 assert_eq!(address.address_level1, "ON");
410 }
411}