push/internal/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/. */
4use crate::error::warn;
5use rusqlite::Transaction;
6use sql_support::open_database;
7
8const CREATE_TABLE_PUSH_SQL: &str = include_str!("schema.sql");
9
10pub struct PushConnectionInitializer;
11
12impl open_database::ConnectionInitializer for PushConnectionInitializer {
13    const NAME: &'static str = "push db";
14    const END_VERSION: u32 = 3;
15
16    // This is such a simple database that we do almost nothing!
17    // * We have no foreign keys, so `PRAGMA foreign_keys = ON;` is pointless.
18    // * We have no temp tables, so `PRAGMA temp_store = 2;` is pointless.
19    // * We don't even use transactions, so `PRAGMA journal_mode=WAL;` is pointless.
20    // * We have a tiny number of different SQL statements, so
21    //   set_prepared_statement_cache_capacity is pointless.
22    // * We have no SQL functions.
23    // Kinda makes you wonder why we use a sql db at all :)
24    // So - no "prepare" and no "finish" methods.
25    fn init(&self, db: &Transaction<'_>) -> open_database::Result<()> {
26        db.execute_batch(CREATE_TABLE_PUSH_SQL)?;
27        Ok(())
28    }
29
30    fn upgrade_from(&self, db: &Transaction<'_>, version: u32) -> open_database::Result<()> {
31        match version {
32            0 => db.execute_batch(CREATE_TABLE_PUSH_SQL)?,
33            1 => db.execute_batch(CREATE_TABLE_PUSH_SQL)?,
34            2 => {
35                // We dropped the `uaid` and `native_id` columns and added a constraint that scope
36                // must not be an empty string and must be unique.
37                let sql = format!(
38                    "
39                    -- rename the old table.
40                    ALTER TABLE push_record RENAME TO push_record_old;
41                    -- create the new table with the new schema.
42                    {CREATE_TABLE_PUSH_SQL};
43                    -- move the data across.
44                    INSERT OR IGNORE INTO push_record ({COMMON_COLS})
45                    SELECT {COMMON_COLS} FROM push_record_old WHERE length(scope) > 0;
46                    -- drop the old table
47                    DROP TABLE push_record_old;",
48                    CREATE_TABLE_PUSH_SQL = CREATE_TABLE_PUSH_SQL,
49                    COMMON_COLS = COMMON_COLS,
50                );
51                db.execute_batch(&sql)?;
52            }
53            other => {
54                warn!(
55                    "Loaded future schema version {} (we only understand version {}). \
56                    Optimistically ",
57                    other,
58                    Self::END_VERSION
59                )
60            }
61        };
62        Ok(())
63    }
64}
65
66pub const COMMON_COLS: &str = "
67    channel_id,
68    endpoint,
69    scope,
70    key,
71    ctime,
72    app_server_key
73";
74
75#[cfg(test)]
76mod test {
77    use crate::internal::storage::db::{PushDb, Storage};
78    use rusqlite::{Connection, OpenFlags};
79    use sql_support::ConnExt;
80
81    const CREATE_V2_SCHEMA: &str = include_str!("test/schema_v2.sql");
82
83    #[test]
84    fn test_migrate_v2_v3() {
85        error_support::init_for_tests();
86        let tempdir = tempfile::tempdir().unwrap();
87        let path = tempdir.path().join("push_v2.sql");
88
89        let conn = Connection::open_with_flags(path.clone(), OpenFlags::default()).unwrap();
90        conn.execute_batch(CREATE_V2_SCHEMA).unwrap();
91
92        // insert some stuff
93        conn.execute_batch(
94            r#"
95            INSERT INTO push_record (
96                uaid,    channel_id, endpoint, scope,  key,     ctime, app_server_key, native_id
97            ) VALUES
98                ("id-1", "cid1",     "ep-1",   "sc-1", x'1234', 1,    "ask-1",         "nid-1"),
99                -- duplicate scope, which isn't allowed in the new schema
100                ("id-2", "cid2",     "ep-2",   "sc-1", x'5678', 2,    "ask-2",         "nid-2"),
101                -- empty scope, which isn't allowed in the new schema
102                ("id-3", "cid3",     "ep-3",   "",     x'0000', 3,    "ask-3",         "nid-3")
103            ;
104            INSERT into meta_data (
105                key, value
106            ) VALUES
107                ("key-1", "value-1"),
108                ("key-2", "value-2")
109            "#,
110        )
111        .unwrap();
112
113        // reopen the database.
114        drop(conn);
115        let db = PushDb::open(path).expect("should open");
116
117        // Should only have 1 row in push_record
118        assert_eq!(
119            db.query_one::<u32>("SELECT COUNT(*) FROM push_record")
120                .unwrap(),
121            1
122        );
123        let record = db
124            .get_record("cid1")
125            .expect("should work")
126            .expect("should get a record");
127        assert_eq!(record.channel_id, "cid1");
128        assert_eq!(record.endpoint, "ep-1");
129        assert_eq!(record.scope, "sc-1");
130        assert_eq!(record.key, [0x12, 0x34]);
131        assert_eq!(record.ctime.0, 1);
132        assert_eq!(record.app_server_key.unwrap(), "ask-1");
133
134        // But both metadata ones.
135        assert_eq!(
136            db.db
137                .query_one::<u32>("SELECT COUNT(*) FROM meta_data")
138                .unwrap(),
139            2
140        );
141        assert_eq!(db.get_meta("key-1").unwrap().unwrap(), "value-1");
142        assert_eq!(db.get_meta("key-2").unwrap().unwrap(), "value-2");
143    }
144}