diff options
Diffstat (limited to 'third_party/rust/tabs/src/schema.rs')
-rw-r--r-- | third_party/rust/tabs/src/schema.rs | 150 |
1 files changed, 150 insertions, 0 deletions
diff --git a/third_party/rust/tabs/src/schema.rs b/third_party/rust/tabs/src/schema.rs new file mode 100644 index 0000000000..ad8713b487 --- /dev/null +++ b/third_party/rust/tabs/src/schema.rs @@ -0,0 +1,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"); + } +} |