summaryrefslogtreecommitdiffstats
path: root/third_party/rust/tabs/src/schema.rs
blob: ad8713b487fb20700fc9a91a0530e46c63588a92 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// Tabs is a bit special - it's a trivial SQL schema and is only used as a persistent
// cache, and the semantics of the "tabs" collection means there's no need for
// syncChangeCounter/syncStatus nor a mirror etc.

use rusqlite::{Connection, Transaction};
use sql_support::{
    open_database::{
        ConnectionInitializer as MigrationLogic, Error as MigrationError, Result as MigrationResult,
    },
    ConnExt,
};

// The record is the TabsRecord struct in json and this module doesn't need to deserialize, so we just
// store each client as its own row.
const CREATE_SCHEMA_SQL: &str = "
    CREATE TABLE IF NOT EXISTS tabs (
        guid            TEXT NOT NULL PRIMARY KEY,
        record          TEXT NOT NULL,
        last_modified   INTEGER NOT NULL
    );
";

const CREATE_META_TABLE_SQL: &str = "
    CREATE TABLE IF NOT EXISTS moz_meta (
        key    TEXT PRIMARY KEY,
        value  NOT NULL
    )
";

pub(crate) static LAST_SYNC_META_KEY: &str = "last_sync_time";
pub(crate) static GLOBAL_SYNCID_META_KEY: &str = "global_sync_id";
pub(crate) static COLLECTION_SYNCID_META_KEY: &str = "tabs_sync_id";
// Tabs stores this in the meta table due to a unique requirement that we only know the list
// of connected clients when syncing, however getting the list of tabs could be called at anytime
// so we store it so we can translate from the tabs sync record ID to the FxA device id for the client
pub(crate) static REMOTE_CLIENTS_KEY: &str = "remote_clients";

pub struct TabsMigrationLogic;

impl MigrationLogic for TabsMigrationLogic {
    const NAME: &'static str = "tabs storage db";
    const END_VERSION: u32 = 2;

    fn prepare(&self, conn: &Connection, _db_empty: bool) -> MigrationResult<()> {
        let initial_pragmas = "
            -- We don't care about temp tables being persisted to disk.
            PRAGMA temp_store = 2;
            -- we unconditionally want write-ahead-logging mode.
            PRAGMA journal_mode=WAL;
            -- foreign keys seem worth enforcing (and again, we don't care in practice)
            PRAGMA foreign_keys = ON;
        ";
        conn.execute_batch(initial_pragmas)?;
        // This is where we'd define our sql functions if we had any!
        conn.set_prepared_statement_cache_capacity(128);
        Ok(())
    }

    fn init(&self, db: &Transaction<'_>) -> MigrationResult<()> {
        log::debug!("Creating schemas");
        db.execute_all(&[CREATE_SCHEMA_SQL, CREATE_META_TABLE_SQL])?;
        Ok(())
    }

    fn upgrade_from(&self, db: &Transaction<'_>, version: u32) -> MigrationResult<()> {
        match version {
            1 => upgrade_from_v1(db),
            _ => Err(MigrationError::IncompatibleVersion(version)),
        }
    }
}

fn upgrade_from_v1(db: &Connection) -> MigrationResult<()> {
    // The previous version stored the entire payload in one row
    // and cleared on each sync -- it's fine to just drop it
    db.execute_batch("DROP TABLE tabs;")?;
    // Recreate the world
    db.execute_all(&[CREATE_SCHEMA_SQL, CREATE_META_TABLE_SQL])?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::storage::TabsStorage;
    use rusqlite::OptionalExtension;
    use serde_json::json;
    use sql_support::open_database::test_utils::MigratedDatabaseFile;

    const CREATE_V1_SCHEMA_SQL: &str = "
        CREATE TABLE IF NOT EXISTS tabs (
            payload TEXT NOT NULL
        );
        PRAGMA user_version=1;
    ";

    #[test]
    fn test_create_schema_twice() {
        let mut db = TabsStorage::new_with_mem_path("test");
        let conn = db.open_or_create().unwrap();
        conn.execute_batch(CREATE_SCHEMA_SQL)
            .expect("should allow running twice");
    }

    #[test]
    fn test_tabs_db_upgrade_from_v1() {
        let db_file = MigratedDatabaseFile::new(TabsMigrationLogic, CREATE_V1_SCHEMA_SQL);
        db_file.run_all_upgrades();
        // Verify we can open the DB just fine, since migration is essentially a drop
        // we don't need to check any data integrity
        let mut storage = TabsStorage::new(db_file.path);
        storage.open_or_create().unwrap();
        assert!(storage.open_if_exists().unwrap().is_some());

        let test_payload = json!({
            "id": "device-with-a-tab",
            "clientName": "device with a tab",
            "tabs": [{
                "title": "the title",
                "urlHistory": [
                    "https://mozilla.org/"
                ],
                "icon": "https://mozilla.org/icon",
                "lastUsed": 1643764207,
            }]
        });
        let db = storage.open_if_exists().unwrap().unwrap();
        // We should be able to insert without a SQL error after upgrade
        db.execute(
            "INSERT INTO tabs (guid, record, last_modified) VALUES (:guid, :record, :last_modified);",
            rusqlite::named_params! {
                ":guid": "my-device",
                ":record": serde_json::to_string(&test_payload).unwrap(),
                ":last_modified": "1643764207"
            },
        )
        .unwrap();

        let row: Option<String> = db
            .query_row("SELECT guid FROM tabs;", [], |row| row.get(0))
            .optional()
            .unwrap();
        // Verify we can query for a valid guid now
        assert_eq!(row.unwrap(), "my-device");
    }
}