webext_storage/
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::error::{debug, Result};
7use rusqlite::{Connection, Transaction};
8use sql_support::open_database::{
9    ConnectionInitializer as MigrationLogic, Error as MigrationError, Result as MigrationResult,
10};
11
12const CREATE_SCHEMA_SQL: &str = include_str!("../sql/create_schema.sql");
13const CREATE_SYNC_TEMP_TABLES_SQL: &str = include_str!("../sql/create_sync_temp_tables.sql");
14
15pub struct WebExtMigrationLogin;
16
17impl MigrationLogic for WebExtMigrationLogin {
18    const NAME: &'static str = "webext storage db";
19    const END_VERSION: u32 = 2;
20
21    fn prepare(&self, conn: &Connection, _db_empty: bool) -> MigrationResult<()> {
22        let initial_pragmas = "
23            -- We don't care about temp tables being persisted to disk.
24            PRAGMA temp_store = 2;
25            -- we unconditionally want write-ahead-logging mode
26            PRAGMA journal_mode=WAL;
27            -- foreign keys seem worth enforcing!
28            PRAGMA foreign_keys = ON;
29        ";
30        conn.execute_batch(initial_pragmas)?;
31        define_functions(conn)?;
32        conn.set_prepared_statement_cache_capacity(128);
33        Ok(())
34    }
35
36    fn init(&self, db: &Transaction<'_>) -> MigrationResult<()> {
37        debug!("Creating schema");
38        db.execute_batch(CREATE_SCHEMA_SQL)?;
39        Ok(())
40    }
41
42    fn upgrade_from(&self, db: &Transaction<'_>, version: u32) -> MigrationResult<()> {
43        match version {
44            1 => upgrade_from_1(db),
45            _ => Err(MigrationError::IncompatibleVersion(version)),
46        }
47    }
48}
49
50fn define_functions(c: &Connection) -> MigrationResult<()> {
51    use rusqlite::functions::FunctionFlags;
52    c.create_scalar_function(
53        "generate_guid",
54        0,
55        FunctionFlags::SQLITE_UTF8,
56        sql_fns::generate_guid,
57    )?;
58    Ok(())
59}
60
61fn upgrade_from_1(db: &Connection) -> MigrationResult<()> {
62    // We changed a not null constraint
63    db.execute_batch("ALTER TABLE storage_sync_mirror RENAME TO old_mirror;")?;
64    // just re-run the full schema commands to recreate the able.
65    db.execute_batch(CREATE_SCHEMA_SQL)?;
66    db.execute_batch(
67        "INSERT OR IGNORE INTO storage_sync_mirror(guid, ext_id, data)
68         SELECT guid, ext_id, data FROM old_mirror;",
69    )?;
70    db.execute_batch("DROP TABLE old_mirror;")?;
71    db.execute_batch("PRAGMA user_version = 2;")?;
72    Ok(())
73}
74
75// Note that we expect this to be called before and after a sync - before to
76// ensure we are syncing with a clean state, after to be good memory citizens
77// given the temp tables are in memory.
78pub fn create_empty_sync_temp_tables(db: &Connection) -> Result<()> {
79    debug!("Initializing sync temp tables");
80    db.execute_batch(CREATE_SYNC_TEMP_TABLES_SQL)?;
81    Ok(())
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::db::test::new_mem_db;
88    use rusqlite::Error;
89    use sql_support::open_database::test_utils::MigratedDatabaseFile;
90    use sql_support::ConnExt;
91
92    const CREATE_SCHEMA_V1_SQL: &str = include_str!("../sql/tests/create_schema_v1.sql");
93
94    #[test]
95    fn test_create_schema_twice() {
96        let db = new_mem_db();
97        let conn = db.get_connection().expect("should retrieve connection");
98        conn.execute_batch(CREATE_SCHEMA_SQL)
99            .expect("should allow running twice");
100    }
101
102    #[test]
103    fn test_create_empty_sync_temp_tables_twice() {
104        let db = new_mem_db();
105        let conn = db.get_connection().expect("should retrieve connection");
106        create_empty_sync_temp_tables(conn).expect("should work first time");
107        // insert something into our new temp table and check it's there.
108        conn.execute_batch(
109            "INSERT INTO temp.storage_sync_staging
110                            (guid, ext_id) VALUES
111                            ('guid', 'ext_id');",
112        )
113        .expect("should work once");
114        let count = conn
115            .query_row_and_then(
116                "SELECT COUNT(*) FROM temp.storage_sync_staging;",
117                [],
118                |row| row.get::<_, u32>(0),
119            )
120            .expect("query should work");
121        assert_eq!(count, 1, "should be one row");
122
123        // re-execute
124        create_empty_sync_temp_tables(conn).expect("should second first time");
125        // and it should have deleted existing data.
126        let count = conn
127            .query_row_and_then(
128                "SELECT COUNT(*) FROM temp.storage_sync_staging;",
129                [],
130                |row| row.get::<_, u32>(0),
131            )
132            .expect("query should work");
133        assert_eq!(count, 0, "should be no rows");
134    }
135
136    #[test]
137    fn test_all_upgrades() -> Result<()> {
138        let db_file = MigratedDatabaseFile::new(WebExtMigrationLogin, CREATE_SCHEMA_V1_SQL);
139        db_file.run_all_upgrades();
140        let db = db_file.open();
141
142        let get_id_data = |guid: &str| -> Result<(Option<String>, Option<String>)> {
143            let (ext_id, data) = db
144                .try_query_row::<_, Error, _, _>(
145                    "SELECT ext_id, data FROM storage_sync_mirror WHERE guid = :guid",
146                    &[(":guid", &guid.to_string())],
147                    |row| Ok((row.get(0)?, row.get(1)?)),
148                    true,
149                )?
150                .expect("row should exist.");
151            Ok((ext_id, data))
152        };
153        assert_eq!(
154            get_id_data("guid-1")?,
155            (Some("ext-id-1".to_string()), Some("data-1".to_string()))
156        );
157        assert_eq!(
158            get_id_data("guid-2")?,
159            (Some("ext-id-2".to_string()), Some("data-2".to_string()))
160        );
161        Ok(())
162    }
163
164    #[test]
165    fn test_upgrade_2() -> Result<()> {
166        error_support::init_for_tests();
167
168        let db_file = MigratedDatabaseFile::new(WebExtMigrationLogin, CREATE_SCHEMA_V1_SQL);
169        db_file.upgrade_to(2);
170        let db = db_file.open();
171
172        // Should be able to insert a new with a NULL ext_id
173        db.execute_batch(
174            "INSERT INTO storage_sync_mirror(guid, ext_id, data)
175             VALUES ('guid-3', NULL, NULL);",
176        )?;
177        Ok(())
178    }
179}