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 = 3;
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 _ => Err(Error::IncompatibleVersion(version)),
110 }
111 }
112
113 fn finish(&self, db: &Connection) -> Result<()> {
114 Ok(db.execute_batch(CREATE_SHARED_TRIGGERS_SQL)?)
115 }
116}
117
118fn define_functions(c: &Connection) -> Result<()> {
119 c.create_scalar_function(
120 "generate_guid",
121 0,
122 FunctionFlags::SQLITE_UTF8,
123 sql_fns::generate_guid,
124 )?;
125 c.create_scalar_function("now", 0, FunctionFlags::SQLITE_UTF8, sql_fns::now)?;
126
127 Ok(())
128}
129
130fn upgrade_from_v0(db: &Connection) -> Result<()> {
131 db.execute_batch(
140 "
141 DROP TABLE IF EXISTS credit_cards_data;
142 CREATE TABLE credit_cards_data (
143 guid TEXT NOT NULL PRIMARY KEY CHECK(length(guid) != 0),
144 cc_name TEXT NOT NULL,
145 cc_number_enc TEXT NOT NULL CHECK(length(cc_number_enc) > 20),
146 cc_number_last_4 TEXT NOT NULL CHECK(length(cc_number_last_4) <= 4),
147 cc_exp_month INTEGER,
148 cc_exp_year INTEGER,
149 cc_type TEXT NOT NULL,
150 time_created INTEGER NOT NULL,
151 time_last_used INTEGER,
152 time_last_modified INTEGER NOT NULL,
153 times_used INTEGER NOT NULL,
154 sync_change_counter INTEGER NOT NULL
155 );
156 ",
157 )?;
158 Ok(())
159}
160
161fn upgrade_from_v1(db: &Connection) -> Result<()> {
162 db.execute_batch(
168 "
169 CREATE TABLE new_credit_cards_data (
170 guid TEXT NOT NULL PRIMARY KEY CHECK(length(guid) != 0),
171 cc_name TEXT NOT NULL,
172 cc_number_enc TEXT NOT NULL CHECK(length(cc_number_enc) > 20 OR cc_number_enc == ''),
173 cc_number_last_4 TEXT NOT NULL CHECK(length(cc_number_last_4) <= 4),
174 cc_exp_month INTEGER,
175 cc_exp_year INTEGER,
176 cc_type TEXT NOT NULL,
177 time_created INTEGER NOT NULL,
178 time_last_used INTEGER,
179 time_last_modified INTEGER NOT NULL,
180 times_used INTEGER NOT NULL,
181 sync_change_counter INTEGER NOT NULL
182 );
183 INSERT INTO new_credit_cards_data(guid, cc_name, cc_number_enc, cc_number_last_4, cc_exp_month,
184 cc_exp_year, cc_type, time_created, time_last_used, time_last_modified, times_used,
185 sync_change_counter)
186 SELECT guid, cc_name, cc_number_enc, cc_number_last_4, cc_exp_month, cc_exp_year, cc_type,
187 time_created, time_last_used, time_last_modified, times_used, sync_change_counter
188 FROM credit_cards_data;
189 DROP TABLE credit_cards_data;
190 ALTER TABLE new_credit_cards_data RENAME to credit_cards_data;
191 ")?;
192 Ok(())
193}
194
195fn upgrade_from_v2(db: &Connection) -> Result<()> {
196 db.execute_batch("ALTER TABLE addresses_data ADD COLUMN name TEXT NOT NULL DEFAULT ''")?;
197
198 let mut stmt =
199 db.prepare("SELECT guid, given_name, additional_name, family_name FROM addresses_data")?;
200 let rows = stmt.query_map([], |row| {
201 Ok((
202 row.get::<_, String>("guid")?,
203 row.get::<_, String>("given_name")?,
204 row.get::<_, String>("additional_name")?,
205 row.get::<_, String>("family_name")?,
206 ))
207 })?;
208
209 for row in rows {
210 let (guid, given, middle, family) = row?;
211 let full_name = join_name_parts(&NameParts {
212 given,
213 middle,
214 family,
215 });
216
217 db.execute(
218 "UPDATE addresses_data SET name = (:name) WHERE guid = (:guid)",
219 rusqlite::named_params! { ":name": full_name, ":guid": guid},
220 )?;
221 }
222
223 db.execute_batch(
224 "
225 ALTER TABLE addresses_data DROP COLUMN given_name;
226 ALTER TABLE addresses_data DROP COLUMN additional_name;
227 ALTER TABLE addresses_data DROP COLUMN family_name;
228 ",
229 )?;
230
231 Ok(())
232}
233
234pub fn create_empty_sync_temp_tables(db: &Connection) -> Result<()> {
235 debug!("Initializing sync temp tables");
236 db.execute_batch(CREATE_SYNC_TEMP_TABLES_SQL)?;
237 Ok(())
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::db::addresses::get_address;
244 use crate::db::credit_cards::get_credit_card;
245 use crate::db::test::new_mem_db;
246 use sql_support::open_database::test_utils::MigratedDatabaseFile;
247 use sync_guid::Guid;
248 use types::Timestamp;
249
250 const CREATE_V0_DB: &str = include_str!("../../sql/tests/create_v0_db.sql");
251 const CREATE_V1_DB: &str = include_str!("../../sql/tests/create_v1_db.sql");
252 const CREATE_V2_DB: &str = include_str!("../../sql/tests/create_v2_db.sql");
253
254 #[test]
255 fn test_create_schema_twice() {
256 let db = new_mem_db();
257 db.execute_batch(CREATE_SHARED_SCHEMA_SQL)
258 .expect("should allow running main schema creation twice");
259 db.execute_batch(CREATE_SYNC_TEMP_TABLES_SQL)
261 .expect("should allow running sync temp tables first time");
262 db.execute_batch(CREATE_SYNC_TEMP_TABLES_SQL)
263 .expect("should allow running sync temp tables second time");
264 }
265
266 #[test]
267 fn test_all_upgrades() {
268 let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V1_DB);
270 db_file.run_all_upgrades();
271 let conn = db_file.open();
272
273 let cc = get_credit_card(&conn, &Guid::new("A")).unwrap();
275 assert_eq!(cc.guid, "A");
276 assert_eq!(cc.cc_name, "Jane Doe");
277 assert_eq!(cc.cc_number_enc, "012345678901234567890");
278 assert_eq!(cc.cc_number_last_4, "1234");
279 assert_eq!(cc.cc_exp_month, 1);
280 assert_eq!(cc.cc_exp_year, 2020);
281 assert_eq!(cc.cc_type, "visa");
282 assert_eq!(cc.metadata.time_created, Timestamp(0));
283 assert_eq!(cc.metadata.time_last_used, Timestamp(1));
284 assert_eq!(cc.metadata.time_last_modified, Timestamp(2));
285 assert_eq!(cc.metadata.times_used, 3);
286 assert_eq!(cc.metadata.sync_change_counter, 0);
287
288 let address = get_address(&conn, &Guid::new("A")).unwrap();
289 assert_eq!(address.guid, "A");
290 assert_eq!(address.name, "Jane JaneDoe2 Doe");
291 assert_eq!(address.organization, "Mozilla");
292 assert_eq!(address.street_address, "123 Maple lane");
293 assert_eq!(address.address_level3, "Shelbyville");
294 assert_eq!(address.address_level2, "Springfield");
295 assert_eq!(address.address_level1, "MA");
296 assert_eq!(address.postal_code, "12345");
297 assert_eq!(address.country, "US");
298 assert_eq!(address.tel, "01-234-567-8000");
299 assert_eq!(address.email, "jane@hotmail.com");
300 assert_eq!(address.metadata.time_created, Timestamp(0));
301 assert_eq!(address.metadata.time_last_used, Timestamp(1));
302 assert_eq!(address.metadata.time_last_modified, Timestamp(2));
303 assert_eq!(address.metadata.times_used, 3);
304 assert_eq!(address.metadata.sync_change_counter, 0);
305 }
306
307 #[test]
308 fn test_upgrade_version_0() {
309 let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V0_DB);
310 let select_cc_number_enc = "SELECT cc_number_enc from credit_cards_data";
313 db_file
314 .open()
315 .execute_batch(select_cc_number_enc)
316 .expect_err("select should fail due to bad field name");
317
318 db_file.upgrade_to(1);
319
320 db_file
321 .open()
322 .execute_batch(select_cc_number_enc)
323 .expect("select should now work");
324 }
325
326 #[test]
327 fn test_upgrade_version_1() {
328 let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V1_DB);
329
330 db_file.upgrade_to(2);
331 let db = db_file.open();
332
333 db.execute("UPDATE credit_cards_data SET cc_number_enc=''", [])
335 .expect("blank cc_number_enc should be valid");
336 db.execute("UPDATE credit_cards_data SET cc_number_enc='x'", [])
337 .expect_err("cc_number_enc should be invalid");
338 }
339
340 #[test]
341 fn test_upgrade_version_2() {
342 let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V2_DB);
343 let db = db_file.open();
344
345 db.execute_batch("SELECT name from addresses_data")
346 .expect_err("select should fail");
347 db.execute_batch("SELECT street_address from addresses_data")
348 .expect("street_address should work");
349 db.execute_batch("SELECT additional_name from addresses_data")
350 .expect("additional_name should work");
351 db.execute_batch("SELECT family_name from addresses_data")
352 .expect("family_name should work");
353
354 db_file.upgrade_to(3);
355
356 db.execute_batch("SELECT name from addresses_data")
357 .expect("select name should now work");
358 db.execute_batch("SELECT given_name from addresses_data")
359 .expect_err("given_name should fail");
360 db.execute_batch("SELECT additional_name from addresses_data")
361 .expect_err("additional_name should fail");
362 db.execute_batch("SELECT family_name from addresses_data")
363 .expect_err("family_name should fail");
364
365 let mut address = get_address(&db, &Guid::new("A")).unwrap();
366 assert_eq!(address.guid, "A");
367 assert_eq!(address.name, "Jane John Doe");
368
369 address = get_address(&db, &Guid::new("B")).unwrap();
370 assert_eq!(address.guid, "B");
371
372 assert_eq!(address.name, "");
374 }
375}