autofill/db/
schema.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use 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
71pub const PASSPORT_COMMON_COLS: &str = "
72    guid,
73    name,
74    country,
75    passport_number,
76    issue_date_month,
77    issue_date_day,
78    issue_date_year,
79    expiry_date_month,
80    expiry_date_day,
81    expiry_date_year,
82    time_created,
83    time_last_used,
84    time_last_modified,
85    times_used";
86
87pub const PASSPORT_COMMON_VALS: &str = "
88    :guid,
89    :name,
90    :country,
91    :passport_number,
92    :issue_date_month,
93    :issue_date_day,
94    :issue_date_year,
95    :expiry_date_month,
96    :expiry_date_day,
97    :expiry_date_year,
98    :time_created,
99    :time_last_used,
100    :time_last_modified,
101    :times_used";
102
103const CREATE_SHARED_SCHEMA_SQL: &str = include_str!("../../sql/create_shared_schema.sql");
104const CREATE_SHARED_TRIGGERS_SQL: &str = include_str!("../../sql/create_shared_triggers.sql");
105const CREATE_SYNC_TEMP_TABLES_SQL: &str = include_str!("../../sql/create_sync_temp_tables.sql");
106
107pub struct AutofillConnectionInitializer;
108
109impl ConnectionInitializer for AutofillConnectionInitializer {
110    const NAME: &'static str = "autofill db";
111    const END_VERSION: u32 = 5;
112
113    fn prepare(&self, conn: &Connection, _db_empty: bool) -> Result<()> {
114        define_functions(conn)?;
115
116        // Set up default SQLite pragmas to truncate the -wal file.
117        sql_support::setup_sqlite_defaults(conn)?;
118
119        let initial_pragmas = "
120            -- autofill does not use foreign keys at present but this is probably a good pragma to set
121            PRAGMA foreign_keys = ON;
122        ";
123        conn.execute_batch(initial_pragmas)?;
124
125        conn.set_prepared_statement_cache_capacity(128);
126        Ok(())
127    }
128
129    fn init(&self, db: &Transaction<'_>) -> Result<()> {
130        Ok(db.execute_batch(CREATE_SHARED_SCHEMA_SQL)?)
131    }
132
133    fn upgrade_from(&self, db: &Transaction<'_>, version: u32) -> Result<()> {
134        match version {
135            // AutofillDB has a slightly strange version history, so we start on v0.  See
136            // upgrade_from_v0() for more details.
137            0 => upgrade_from_v0(db),
138            1 => upgrade_from_v1(db),
139            2 => upgrade_from_v2(db),
140            3 => upgrade_from_v3(db),
141            4 => upgrade_from_v4(db),
142            _ => Err(Error::IncompatibleVersion(version)),
143        }
144    }
145
146    fn finish(&self, db: &Connection) -> Result<()> {
147        Ok(db.execute_batch(CREATE_SHARED_TRIGGERS_SQL)?)
148    }
149}
150
151fn define_functions(c: &Connection) -> Result<()> {
152    c.create_scalar_function(
153        "generate_guid",
154        0,
155        FunctionFlags::SQLITE_UTF8,
156        sql_fns::generate_guid,
157    )?;
158    c.create_scalar_function("now", 0, FunctionFlags::SQLITE_UTF8, sql_fns::now)?;
159
160    Ok(())
161}
162
163fn upgrade_from_v0(db: &Connection) -> Result<()> {
164    // This is a bit painful - there are (probably 3) databases out there
165    // that have a schema of 0.
166    // These databases have a `cc_number` but we need them to have a
167    // `cc_number_enc` and `cc_number_last_4`.
168    // This was so very early in the Fenix nightly cycle, and before any
169    // real UI existed to create cards, so we don't bother trying to
170    // migrate them, we just drop the table and re-create it with the
171    // correct schema.
172    db.execute_batch(
173        "
174        DROP TABLE IF EXISTS credit_cards_data;
175        CREATE TABLE credit_cards_data (
176            guid                TEXT NOT NULL PRIMARY KEY CHECK(length(guid) != 0),
177            cc_name             TEXT NOT NULL,
178            cc_number_enc       TEXT NOT NULL CHECK(length(cc_number_enc) > 20),
179            cc_number_last_4    TEXT NOT NULL CHECK(length(cc_number_last_4) <= 4),
180            cc_exp_month        INTEGER,
181            cc_exp_year         INTEGER,
182            cc_type             TEXT NOT NULL,
183            time_created        INTEGER NOT NULL,
184            time_last_used      INTEGER,
185            time_last_modified  INTEGER NOT NULL,
186            times_used          INTEGER NOT NULL,
187            sync_change_counter INTEGER NOT NULL
188        );
189        ",
190    )?;
191    Ok(())
192}
193
194fn upgrade_from_v1(db: &Connection) -> Result<()> {
195    // Alter cc_number_enc using the 12-step generalized procedure described here:
196    // https://sqlite.org/lang_altertable.html
197    // Note that all our triggers are TEMP triggers so do not exist when
198    // this is called (except possibly by tests which do things like
199    // downgrade the version after they are created etc.)
200    db.execute_batch(
201        "
202        CREATE TABLE new_credit_cards_data (
203            guid                TEXT NOT NULL PRIMARY KEY CHECK(length(guid) != 0),
204            cc_name             TEXT NOT NULL,
205            cc_number_enc       TEXT NOT NULL CHECK(length(cc_number_enc) > 20 OR cc_number_enc == ''),
206            cc_number_last_4    TEXT NOT NULL CHECK(length(cc_number_last_4) <= 4),
207            cc_exp_month        INTEGER,
208            cc_exp_year         INTEGER,
209            cc_type             TEXT NOT NULL,
210            time_created        INTEGER NOT NULL,
211            time_last_used      INTEGER,
212            time_last_modified  INTEGER NOT NULL,
213            times_used          INTEGER NOT NULL,
214            sync_change_counter INTEGER NOT NULL
215        );
216        INSERT INTO new_credit_cards_data(guid, cc_name, cc_number_enc, cc_number_last_4, cc_exp_month,
217        cc_exp_year, cc_type, time_created, time_last_used, time_last_modified, times_used,
218        sync_change_counter)
219        SELECT guid, cc_name, cc_number_enc, cc_number_last_4, cc_exp_month, cc_exp_year, cc_type,
220            time_created, time_last_used, time_last_modified, times_used, sync_change_counter
221        FROM credit_cards_data;
222        DROP TABLE credit_cards_data;
223        ALTER TABLE new_credit_cards_data RENAME to credit_cards_data;
224        ")?;
225    Ok(())
226}
227
228fn upgrade_from_v2(db: &Connection) -> Result<()> {
229    db.execute_batch("ALTER TABLE addresses_data ADD COLUMN name TEXT NOT NULL DEFAULT ''")?;
230
231    let mut stmt =
232        db.prepare("SELECT guid, given_name, additional_name, family_name FROM addresses_data")?;
233    let rows = stmt.query_map([], |row| {
234        Ok((
235            row.get::<_, String>("guid")?,
236            row.get::<_, String>("given_name")?,
237            row.get::<_, String>("additional_name")?,
238            row.get::<_, String>("family_name")?,
239        ))
240    })?;
241
242    for row in rows {
243        let (guid, given, middle, family) = row?;
244        let full_name = join_name_parts(&NameParts {
245            given,
246            middle,
247            family,
248        });
249
250        db.execute(
251            "UPDATE addresses_data SET name = (:name) WHERE guid = (:guid)",
252            rusqlite::named_params! { ":name": full_name, ":guid": guid},
253        )?;
254    }
255
256    db.execute_batch(
257        "
258        ALTER TABLE addresses_data DROP COLUMN given_name;
259        ALTER TABLE addresses_data DROP COLUMN additional_name;
260        ALTER TABLE addresses_data DROP COLUMN family_name;
261        ",
262    )?;
263
264    Ok(())
265}
266
267fn upgrade_from_v3(db: &Connection) -> Result<()> {
268    let migration_string: &str = include_str!("../../sql/migrations/v4_migration.sql");
269    db.execute_batch(migration_string)?;
270    Ok(())
271}
272
273fn upgrade_from_v4(db: &Connection) -> Result<()> {
274    // v4 -> v5 only adds new tables (the passports_* tables), so we can just
275    // re-run the shared schema, which uses `CREATE TABLE IF NOT EXISTS`. This
276    // is the approach used by most other components and avoids duplicating the
277    // table definitions in a separate migration file.
278    db.execute_batch(CREATE_SHARED_SCHEMA_SQL)?;
279    Ok(())
280}
281
282pub fn create_empty_sync_temp_tables(db: &Connection) -> Result<()> {
283    debug!("Initializing sync temp tables");
284    db.execute_batch(CREATE_SYNC_TEMP_TABLES_SQL)?;
285    Ok(())
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use crate::db::addresses::get_address;
292    use crate::db::credit_cards::get_credit_card;
293    use crate::db::test::new_mem_db;
294    use crate::db::AutofillDb;
295    use sql_support::open_database::test_utils::MigratedDatabaseFile;
296    use sync_guid::Guid;
297    use types::Timestamp;
298
299    const CREATE_V0_DB: &str = include_str!("../../sql/tests/create_v0_db.sql");
300    const CREATE_V1_DB: &str = include_str!("../../sql/tests/create_v1_db.sql");
301    const CREATE_V2_DB: &str = include_str!("../../sql/tests/create_v2_db.sql");
302    const CREATE_V3_DB: &str = include_str!("../../sql/tests/create_v3_db.sql");
303    const CREATE_V4_DB: &str = include_str!("../../sql/tests/create_v4_db.sql");
304
305    #[test]
306    fn test_wal_size_is_bounded() {
307        // A memory database has no -wal file, so open a real one.
308        let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, "");
309        let db = AutofillDb::new(&db_file.path).expect("should open the database");
310
311        let journal_mode: String = db
312            .query_row("PRAGMA journal_mode", [], |row| row.get(0))
313            .unwrap();
314        assert_eq!(journal_mode, "wal");
315
316        let autocheckpoint: i64 = db
317            .query_row("PRAGMA wal_autocheckpoint", [], |row| row.get(0))
318            .unwrap();
319        assert!(autocheckpoint > 0, "auto-checkpointing is disabled");
320
321        let journal_size_limit: i64 = db
322            .query_row("PRAGMA journal_size_limit", [], |row| row.get(0))
323            .unwrap();
324        assert!(
325            journal_size_limit > 0,
326            "journal_size_limit is {journal_size_limit}, so the -wal file is never truncated"
327        );
328    }
329
330    #[test]
331    fn test_create_schema_twice() {
332        let db = new_mem_db();
333        db.execute_batch(CREATE_SHARED_SCHEMA_SQL)
334            .expect("should allow running main schema creation twice");
335        // sync tables aren't created by default, so do it twice here.
336        db.execute_batch(CREATE_SYNC_TEMP_TABLES_SQL)
337            .expect("should allow running sync temp tables first time");
338        db.execute_batch(CREATE_SYNC_TEMP_TABLES_SQL)
339            .expect("should allow running sync temp tables second time");
340    }
341
342    #[test]
343    fn test_all_upgrades() {
344        // Let's start with v1, since the v0 upgrade deletes data
345        let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V1_DB);
346        db_file.run_all_upgrades();
347        let conn = db_file.open();
348
349        // Test that the data made it through
350        let cc = get_credit_card(&conn, &Guid::new("A")).unwrap();
351        assert_eq!(cc.guid, "A");
352        assert_eq!(cc.cc_name, "Jane Doe");
353        assert_eq!(cc.cc_number_enc, "012345678901234567890");
354        assert_eq!(cc.cc_number_last_4, "1234");
355        assert_eq!(cc.cc_exp_month, 1);
356        assert_eq!(cc.cc_exp_year, 2020);
357        assert_eq!(cc.cc_type, "visa");
358        assert_eq!(cc.metadata.time_created, Timestamp(0));
359        assert_eq!(cc.metadata.time_last_used, Timestamp(1));
360        assert_eq!(cc.metadata.time_last_modified, Timestamp(2));
361        assert_eq!(cc.metadata.times_used, 3);
362        assert_eq!(cc.metadata.sync_change_counter, 0);
363
364        let address = get_address(&conn, &Guid::new("A")).unwrap();
365        assert_eq!(address.guid, "A");
366        assert_eq!(address.name, "Jane JaneDoe2 Doe");
367        assert_eq!(address.organization, "Mozilla");
368        assert_eq!(address.street_address, "123 Maple lane");
369        assert_eq!(address.address_level3, "Shelbyville");
370        assert_eq!(address.address_level2, "Springfield");
371        assert_eq!(address.address_level1, "MA");
372        assert_eq!(address.postal_code, "12345");
373        assert_eq!(address.country, "US");
374        assert_eq!(address.tel, "01-234-567-8000");
375        assert_eq!(address.email, "jane@hotmail.com");
376        assert_eq!(address.metadata.time_created, Timestamp(0));
377        assert_eq!(address.metadata.time_last_used, Timestamp(1));
378        assert_eq!(address.metadata.time_last_modified, Timestamp(2));
379        assert_eq!(address.metadata.times_used, 3);
380        assert_eq!(address.metadata.sync_change_counter, 0);
381    }
382
383    #[test]
384    fn test_upgrade_version_0() {
385        let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V0_DB);
386        // Just to test what we think we are testing, select a field that
387        // doesn't exist now but will after we recreate the table.
388        let select_cc_number_enc = "SELECT cc_number_enc from credit_cards_data";
389        db_file
390            .open()
391            .execute_batch(select_cc_number_enc)
392            .expect_err("select should fail due to bad field name");
393
394        db_file.upgrade_to(1);
395
396        db_file
397            .open()
398            .execute_batch(select_cc_number_enc)
399            .expect("select should now work");
400    }
401
402    #[test]
403    fn test_upgrade_version_1() {
404        let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V1_DB);
405
406        db_file.upgrade_to(2);
407        let db = db_file.open();
408
409        // Test the upgraded check constraint
410        db.execute("UPDATE credit_cards_data SET cc_number_enc=''", [])
411            .expect("blank cc_number_enc should be valid");
412        db.execute("UPDATE credit_cards_data SET cc_number_enc='x'", [])
413            .expect_err("cc_number_enc should be invalid");
414    }
415
416    #[test]
417    fn test_upgrade_version_2() {
418        let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V2_DB);
419        let db = db_file.open();
420
421        db.execute_batch("SELECT name from addresses_data")
422            .expect_err("select should fail");
423        db.execute_batch("SELECT street_address from addresses_data")
424            .expect("street_address should work");
425        db.execute_batch("SELECT additional_name from addresses_data")
426            .expect("additional_name should work");
427        db.execute_batch("SELECT family_name from addresses_data")
428            .expect("family_name should work");
429
430        db_file.upgrade_to(3);
431
432        db.execute_batch("SELECT name from addresses_data")
433            .expect("select name should now work");
434        db.execute_batch("SELECT given_name from addresses_data")
435            .expect_err("given_name should fail");
436        db.execute_batch("SELECT additional_name from addresses_data")
437            .expect_err("additional_name should fail");
438        db.execute_batch("SELECT family_name from addresses_data")
439            .expect_err("family_name should fail");
440
441        let mut address = get_address(&db, &Guid::new("A")).unwrap();
442        assert_eq!(address.guid, "A");
443        assert_eq!(address.name, "Jane John Doe");
444
445        address = get_address(&db, &Guid::new("B")).unwrap();
446        assert_eq!(address.guid, "B");
447
448        // Record B has no given_name, additional_name or family_name, so name should also be empty.
449        assert_eq!(address.name, "");
450    }
451
452    #[test]
453    fn test_upgrade_version_3() {
454        let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V3_DB);
455        let db = db_file.open();
456
457        // Assert that the existing addresses have the fully qualified address_level1 name.
458        let mut address = get_address(&db, &Guid::new("A")).unwrap();
459        assert_eq!(address.guid, "A");
460        assert_eq!(address.name, "Jane John Doe");
461        assert_eq!(address.address_level1, "Massachusetts");
462
463        address = get_address(&db, &Guid::new("B")).unwrap();
464        assert_eq!(address.guid, "B");
465        assert_eq!(address.address_level1, "Ontario");
466
467        db_file.upgrade_to(4);
468
469        // Assert that the addresses have been migrated to use the subregion keys.
470        address = get_address(&db, &Guid::new("A")).unwrap();
471        assert_eq!(address.guid, "A");
472        assert_eq!(address.name, "Jane John Doe");
473        assert_eq!(address.address_level1, "MA");
474
475        address = get_address(&db, &Guid::new("B")).unwrap();
476        assert_eq!(address.guid, "B");
477        assert_eq!(address.address_level1, "ON");
478    }
479
480    #[test]
481    fn test_upgrade_version_4() {
482        let db_file = MigratedDatabaseFile::new(AutofillConnectionInitializer, CREATE_V4_DB);
483        let db = db_file.open();
484
485        // passports_data must not exist yet at v4.
486        db.execute_batch("SELECT guid FROM passports_data")
487            .expect_err("passports_data should not exist at v4");
488
489        db_file.upgrade_to(5);
490
491        // After upgrading to v5 the table exists.
492        db.execute_batch("SELECT guid FROM passports_data")
493            .expect("passports_data should exist at v5");
494    }
495}