places/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::api::places_api::ConnectionType;
6use crate::bookmark_sync::engine::LAST_SYNC_META_KEY;
7use crate::error::debug;
8use crate::storage::bookmarks::{
9    bookmark_sync::create_synced_bookmark_roots, create_bookmark_roots,
10};
11use crate::types::SyncStatus;
12use rusqlite::Connection;
13use sql_support::ConnExt;
14
15use super::db::{Pragma, PragmaGuard};
16
17pub const VERSION: u32 = 18;
18
19// Shared schema and temp tables for the read-write and Sync connections.
20const CREATE_SHARED_SCHEMA_SQL: &str = include_str!("../../sql/create_shared_schema.sql");
21const CREATE_SHARED_TEMP_TABLES_SQL: &str = include_str!("../../sql/create_shared_temp_tables.sql");
22
23// Sync-specific temp tables and triggers.
24const CREATE_SYNC_TEMP_TABLES_SQL: &str = include_str!("../../sql/create_sync_temp_tables.sql");
25const CREATE_SYNC_TRIGGERS_SQL: &str = include_str!("../../sql/create_sync_triggers.sql");
26
27// Triggers for the main read-write connection only.
28const CREATE_MAIN_TRIGGERS_SQL: &str = include_str!("../../sql/create_main_triggers.sql");
29
30lazy_static::lazy_static! {
31    // Triggers for the read-write and Sync connections.
32    static ref CREATE_SHARED_TRIGGERS_SQL: String = {
33        format!(
34            include_str!("../../sql/create_shared_triggers.sql"),
35            increase_frecency_stats = update_origin_frecency_stats("+"),
36            decrease_frecency_stats = update_origin_frecency_stats("-"),
37        )
38    };
39}
40
41// Keys in the moz_meta table.
42pub(crate) static MOZ_META_KEY_ORIGIN_FRECENCY_COUNT: &str = "origin_frecency_count";
43pub(crate) static MOZ_META_KEY_ORIGIN_FRECENCY_SUM: &str = "origin_frecency_sum";
44pub(crate) static MOZ_META_KEY_ORIGIN_FRECENCY_SUM_OF_SQUARES: &str =
45    "origin_frecency_sum_of_squares";
46
47fn update_origin_frecency_stats(op: &str) -> String {
48    format!(
49        "
50        INSERT OR REPLACE INTO moz_meta(key, value)
51        SELECT
52            '{frecency_count}',
53            IFNULL((SELECT value FROM moz_meta WHERE key = '{frecency_count}'), 0)
54                {op} CAST (frecency > 0 AS INT)
55        FROM moz_origins WHERE prefix = OLD.prefix AND host = OLD.host
56        UNION
57        SELECT
58            '{frecency_sum}',
59            IFNULL((SELECT value FROM moz_meta WHERE key = '{frecency_sum}'), 0)
60                {op} MAX(frecency, 0)
61        FROM moz_origins WHERE prefix = OLD.prefix AND host = OLD.host
62        UNION
63        SELECT
64            '{frecency_sum_of_squares}',
65            IFNULL((SELECT value FROM moz_meta WHERE key = '{frecency_sum_of_squares}'), 0)
66                {op} (MAX(frecency, 0) * MAX(frecency, 0))
67        FROM moz_origins WHERE prefix = OLD.prefix AND host = OLD.host",
68        op = op,
69        frecency_count = MOZ_META_KEY_ORIGIN_FRECENCY_COUNT,
70        frecency_sum = MOZ_META_KEY_ORIGIN_FRECENCY_SUM,
71        frecency_sum_of_squares = MOZ_META_KEY_ORIGIN_FRECENCY_SUM_OF_SQUARES,
72    )
73}
74
75pub fn init(conn: &Connection) -> rusqlite::Result<()> {
76    debug!("Initializing schema");
77    conn.execute_batch(CREATE_SHARED_SCHEMA_SQL)?;
78    create_bookmark_roots(conn)?;
79    Ok(())
80}
81
82pub fn finish(db: &Connection, conn_type: ConnectionType) -> rusqlite::Result<()> {
83    match conn_type {
84        // Read-only connections don't need temp tables or triggers, as they
85        // can't write anything.
86        ConnectionType::ReadOnly => {}
87
88        // The main read-write connection needs shared and main-specific
89        // temp tables and triggers (for example, for writing tombstones).
90        ConnectionType::ReadWrite => {
91            // Every writable connection needs these...
92            db.execute_batch(CREATE_SHARED_TEMP_TABLES_SQL)?;
93            db.execute_batch(&CREATE_SHARED_TRIGGERS_SQL)?;
94            db.execute_batch(CREATE_MAIN_TRIGGERS_SQL)?;
95        }
96
97        // The Sync connection needs shared and its own temp tables and
98        // triggers, for merging. It also bypasses some of the main
99        // triggers, so that we don't write tombstones for synced deletions.
100        ConnectionType::Sync => {
101            db.execute_batch(CREATE_SHARED_TEMP_TABLES_SQL)?;
102            db.execute_batch(&CREATE_SHARED_TRIGGERS_SQL)?;
103            db.execute_batch(CREATE_SYNC_TEMP_TABLES_SQL)?;
104            db.execute_batch(CREATE_SYNC_TRIGGERS_SQL)?;
105            create_synced_bookmark_roots(db)?;
106        }
107    }
108    Ok(())
109}
110
111/// Helper for migration - pre-dates MigrationLogic, hence it has slightly strange wiring.
112/// Intended use:
113///
114/// ```rust,ignore
115/// migration(db, cur_ver, 2, &[stuff, to, migrate, version2, to, version3], || Ok(()))?;
116/// migration(db, cur_ver, 3, &[stuff, to, migrate, version3, to, version4], || Ok(()))?;
117/// migration(db, cur_ver, 4, &[stuff, to, migrate, version4, to, version5], || Ok(()))?;
118/// ```
119///
120/// The callback parameter is if any extra logic is needed for the migration
121/// (for example, creating bookmark roots). In an ideal world, this would be an
122/// Option, but sadly, that can't typecheck.
123fn migration<F>(
124    db: &Connection,
125    cur_version: u32,
126    ours: u32,
127    stmts: &[&str],
128    extra_logic: F,
129) -> rusqlite::Result<()>
130where
131    F: FnOnce() -> rusqlite::Result<()>,
132{
133    if cur_version == ours {
134        debug!("Upgrading schema from {} to {}", cur_version, ours);
135        for stmt in stmts {
136            db.execute_batch(stmt)?;
137        }
138        extra_logic()?;
139    }
140    Ok(())
141}
142
143pub fn upgrade_from(db: &Connection, from: u32) -> rusqlite::Result<()> {
144    debug!("Upgrading schema from {} to {}", from, VERSION);
145
146    // Old-style migrations
147
148    migration(db, from, 2, &[CREATE_SHARED_SCHEMA_SQL], || Ok(()))?;
149    migration(
150        db,
151        from,
152        3,
153        &[
154            // Previous versions had an incomplete version of moz_bookmarks.
155            "DROP TABLE moz_bookmarks",
156            CREATE_SHARED_SCHEMA_SQL,
157        ],
158        || create_bookmark_roots(db.conn()),
159    )?;
160    migration(db, from, 4, &[CREATE_SHARED_SCHEMA_SQL], || Ok(()))?;
161    migration(db, from, 5, &[CREATE_SHARED_SCHEMA_SQL], || Ok(()))?; // new tags tables.
162    migration(db, from, 6, &[CREATE_SHARED_SCHEMA_SQL], || Ok(()))?; // bookmark syncing.
163    migration(
164        db,
165        from,
166        7,
167        &[
168            // Changing `moz_bookmarks_synced_structure` to store multiple
169            // parents, so we need to re-download all synced bookmarks.
170            &format!("DELETE FROM moz_meta WHERE key = '{}'", LAST_SYNC_META_KEY),
171            "DROP TABLE moz_bookmarks_synced",
172            "DROP TABLE moz_bookmarks_synced_structure",
173            CREATE_SHARED_SCHEMA_SQL,
174        ],
175        || Ok(()),
176    )?;
177    migration(
178        db,
179        from,
180        8,
181        &[
182            // Bump change counter of New() items due to bookmarks `reset`
183            // setting the counter to 0 instead of 1 (#1145)
184            &format!(
185                "UPDATE moz_bookmarks
186                 SET syncChangeCounter = syncChangeCounter + 1
187                 WHERE syncStatus = {}",
188                SyncStatus::New as u8
189            ),
190        ],
191        || Ok(()),
192    )?;
193    migration(
194        db,
195        from,
196        9,
197        &[
198            // Add an index for synced bookmark URLs.
199            "CREATE INDEX IF NOT EXISTS moz_bookmarks_synced_urls
200             ON moz_bookmarks_synced(placeId)",
201        ],
202        || Ok(()),
203    )?;
204    migration(
205        db,
206        from,
207        10,
208        &[
209            // Add a new table to hold synced and migrated search keywords.
210            "CREATE TABLE IF NOT EXISTS moz_keywords(
211                 place_id INTEGER PRIMARY KEY REFERENCES moz_places(id)
212                                  ON DELETE RESTRICT,
213                 keyword TEXT NOT NULL UNIQUE
214             )",
215            // Add an index on synced keywords, so that we can search for
216            // mismatched keywords without a table scan.
217            "CREATE INDEX IF NOT EXISTS moz_bookmarks_synced_keywords
218             ON moz_bookmarks_synced(keyword) WHERE keyword NOT NULL",
219            // Migrate synced keywords into their own table, so that they're
220            // available via `bookmarks_get_url_for_keyword` before the next
221            // sync.
222            "INSERT OR IGNORE INTO moz_keywords(keyword, place_id)
223             SELECT keyword, placeId
224             FROM moz_bookmarks_synced
225             WHERE placeId NOT NULL AND
226                   keyword NOT NULL",
227        ],
228        || Ok(()),
229    )?;
230    migration(
231        db,
232        from,
233        11,
234        &[
235            // Greatly helps the multi-join query in frecency.
236            "CREATE INDEX IF NOT EXISTS visits_from_type_idx
237             ON moz_historyvisits(from_visit, visit_type)",
238        ],
239        || Ok(()),
240    )?;
241    migration(
242        db,
243        from,
244        12,
245        &[
246            // Reconciled items didn't end up with the correct syncStatus.
247            // See #3504
248            "UPDATE moz_bookmarks AS b
249             SET syncStatus = 2 -- SyncStatus::Normal
250             WHERE EXISTS (SELECT 1 FROM moz_bookmarks_synced
251                                    WHERE guid = b.guid)",
252        ],
253        || Ok(()),
254    )?;
255    migration(db, from, 13, &[CREATE_SHARED_SCHEMA_SQL], || Ok(()))?; // moz_places_metadata.
256    migration(
257        db,
258        from,
259        14,
260        &[
261            // Changing `moz_places_metadata` structure, drop and recreate it.
262            "DROP TABLE moz_places_metadata",
263            CREATE_SHARED_SCHEMA_SQL,
264        ],
265        || Ok(()),
266    )?;
267
268    // End of old style migrations, starting with the 15 -> 16 migration, we just use match
269    // statements
270
271    match from {
272        // Skip the old style migrations
273        n if n < 15 => (),
274        // New-style migrations start here
275        15 => {
276            // Add the `unknownFields` column
277            //
278            // This migration was rolled out incorrectly and we need to check if it was already
279            // applied (https://github.com/mozilla/application-services/issues/5464)
280            let exists_sql = "SELECT 1 FROM pragma_table_info('moz_bookmarks_synced') WHERE name = 'unknownFields'";
281            let add_column_sql = "ALTER TABLE moz_bookmarks_synced ADD COLUMN unknownFields TEXT";
282            if !db.exists(exists_sql, [])? {
283                db.execute(add_column_sql, [])?;
284            }
285        }
286        16 => {
287            // Add the `unknownFields` column for history
288            db.execute("ALTER TABLE moz_places ADD COLUMN unknown_fields TEXT", ())?;
289            db.execute(
290                "ALTER TABLE moz_historyvisits ADD COLUMN unknown_fields TEXT",
291                (),
292            )?;
293        }
294        17 => {
295            // Drop the CHECK and `FOREIGN KEY(parent)` constraints on
296            // `moz_bookmarks`; schemas >= 18 enforce constraints using
297            // TEMP triggers with more informative error messages.
298
299            // SQLite doesn't support `ALTER TABLE DROP CONSTRAINT`, so
300            // we rewrite the schema.
301
302            const NEW_SQL: &str = "CREATE TABLE moz_bookmarks ( \
303                id INTEGER PRIMARY KEY, \
304                fk INTEGER DEFAULT NULL, \
305                type INTEGER NOT NULL, \
306                parent INTEGER, \
307                position INTEGER NOT NULL, \
308                title TEXT, \
309                dateAdded INTEGER NOT NULL DEFAULT 0, \
310                lastModified INTEGER NOT NULL DEFAULT 0, \
311                guid TEXT NOT NULL UNIQUE, \
312                syncStatus INTEGER NOT NULL DEFAULT 0, \
313                syncChangeCounter INTEGER NOT NULL DEFAULT 1, \
314                FOREIGN KEY(fk) REFERENCES moz_places(id) ON DELETE RESTRICT)";
315
316            let _c = PragmaGuard::new(db, Pragma::IgnoreCheckConstraints, true)?;
317            let _f = PragmaGuard::new(db, Pragma::ForeignKeys, false)?;
318            let _w = PragmaGuard::new(db, Pragma::WritableSchema, true)?;
319
320            db.execute(
321                "UPDATE sqlite_schema SET
322                   sql = ?
323                 WHERE type = 'table' AND name = 'moz_bookmarks'",
324                // _Must_ be valid SQL; updating `sqlite_schema.sql` with
325                // invalid SQL will corrupt the database.
326                rusqlite::params![NEW_SQL],
327            )?;
328        }
329        // Add more migrations here...
330
331        // Any other from value indicates that something very wrong happened
332        _ => panic!(
333            "Places does not have a v{} -> v{} migration",
334            from,
335            from + 1
336        ),
337    }
338
339    Ok(())
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use crate::db::{db::PlacesInitializer, PlacesDb};
346    use crate::error::Result;
347    use sql_support::open_database::test_utils::MigratedDatabaseFile;
348    use std::collections::BTreeSet;
349    use sync_guid::Guid as SyncGuid;
350    use url::Url;
351
352    #[test]
353    fn test_create_schema_twice() {
354        let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("no memory db");
355        conn.execute_batch(CREATE_SHARED_SCHEMA_SQL)
356            .expect("should allow running twice");
357    }
358
359    fn has_tombstone(conn: &PlacesDb, guid: &SyncGuid) -> bool {
360        let count: rusqlite::Result<Option<u32>> = conn.try_query_row(
361            "SELECT COUNT(*) from moz_places_tombstones
362                     WHERE guid = :guid",
363            &[(":guid", guid)],
364            |row| row.get::<_, u32>(0),
365            true,
366        );
367        count.unwrap().unwrap() == 1
368    }
369
370    #[test]
371    fn test_places_no_tombstone() {
372        let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("no memory db");
373        let guid = SyncGuid::random();
374
375        conn.execute_cached(
376            "INSERT INTO moz_places (guid, url, url_hash) VALUES (:guid, :url, hash(:url))",
377            &[
378                (":guid", &guid as &dyn rusqlite::ToSql),
379                (
380                    ":url",
381                    &String::from(Url::parse("http://example.com").expect("valid url")),
382                ),
383            ],
384        )
385        .expect("should work");
386
387        let place_id = conn.last_insert_rowid();
388        conn.execute_cached(
389            "DELETE FROM moz_places WHERE id = :id",
390            &[(":id", &place_id)],
391        )
392        .expect("should work");
393
394        // should not have a tombstone.
395        assert!(!has_tombstone(&conn, &guid));
396    }
397
398    #[test]
399    fn test_places_tombstone_removal() {
400        let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("no memory db");
401        let guid = SyncGuid::random();
402
403        conn.execute_cached(
404            "INSERT INTO moz_places_tombstones VALUES (:guid)",
405            &[(":guid", &guid)],
406        )
407        .expect("should work");
408
409        // insert into moz_places - the tombstone should be removed.
410        conn.execute_cached(
411            "INSERT INTO moz_places (guid, url, url_hash, sync_status)
412             VALUES (:guid, :url, hash(:url), :sync_status)",
413            &[
414                (":guid", &guid as &dyn rusqlite::ToSql),
415                (
416                    ":url",
417                    &String::from(Url::parse("http://example.com").expect("valid url")),
418                ),
419                (":sync_status", &SyncStatus::Normal),
420            ],
421        )
422        .expect("should work");
423        assert!(!has_tombstone(&conn, &guid));
424    }
425
426    #[test]
427    fn test_bookmark_check_constraints() {
428        let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("no memory db");
429
430        conn.execute_batch(
431            "INSERT INTO moz_places(id, guid, url, frecency)
432             VALUES(1, 'page_guid___', 'https://example.com', -1);",
433        )
434        .expect("should insert page");
435
436        // type==BOOKMARK but null fk
437        {
438            let e = conn
439                .execute_cached(
440                    "INSERT INTO moz_bookmarks
441                        (fk, type, parent, position, dateAdded, lastModified, guid)
442                     VALUES
443                        (NULL, 1, 0, 0, 1, 1, 'fake_guid___')",
444                    [],
445                )
446                .expect_err("should fail to insert bookmark with NULL fk");
447            assert!(
448                e.to_string().contains("insert: type=1; fk NULL"),
449                "Expected error, got: {:?}",
450                e,
451            );
452
453            conn.execute_batch(
454                "INSERT INTO moz_bookmarks
455                    (fk, type, parent, position, dateAdded, lastModified,
456                       guid)
457                 VALUES
458                    (1, 1, (SELECT id FROM moz_bookmarks WHERE guid = 'root________'), 0, 1, 1,
459                       'bmk_guid____')",
460            )
461            .expect("should insert bookmark");
462            let e = conn
463                .execute(
464                    "UPDATE moz_bookmarks SET
465                        fk = NULL
466                     WHERE guid = 'bmk_guid____'",
467                    [],
468                )
469                .expect_err("should fail to update bookmark with NULL fk");
470            assert!(
471                e.to_string().contains("update: type=1; fk NULL"),
472                "Expected error, got: {:?}",
473                e,
474            );
475        }
476
477        // type!=BOOKMARK and non-null fk
478        {
479            let e = conn
480                .execute_cached(
481                    "INSERT INTO moz_bookmarks
482                        (fk, type, parent, position, dateAdded, lastModified, guid)
483                     VALUES
484                        (1, 2, 0, 0, 1, 1, 'fake_guid___')",
485                    [],
486                )
487                .expect_err("should fail to insert folder with non-NULL fk");
488            assert!(
489                e.to_string().contains("insert: type=2; fk NOT NULL"),
490                "Expected error, got: {:?}",
491                e,
492            );
493
494            conn.execute_batch(
495                "INSERT INTO moz_bookmarks
496                    (fk, type, parent, position, dateAdded, lastModified,
497                       guid)
498                 VALUES
499                    (NULL, 2, (SELECT id FROM moz_bookmarks WHERE guid = 'root________'), 1, 1, 1,
500                       'folder_guid_')",
501            )
502            .expect("should insert folder");
503            let e = conn
504                .execute(
505                    "UPDATE moz_bookmarks SET
506                        fk = 1
507                     WHERE guid = 'folder_guid_'",
508                    [],
509                )
510                .expect_err("should fail to update folder with non-NULL fk");
511            assert!(
512                e.to_string().contains("update: type=2; fk NOT NULL"),
513                "Expected error, got: {:?}",
514                e,
515            );
516        }
517
518        // null parent for item other than the root
519        {
520            let e = conn
521                .execute_cached(
522                    "INSERT INTO moz_bookmarks
523                        (fk, type, parent, position, dateAdded, lastModified, guid)
524                     VALUES
525                        (NULL, 2, NULL, 0, 1, 1, 'fake_guid___')",
526                    [],
527                )
528                .expect_err("should fail to insert item with NULL parent");
529            assert!(
530                e.to_string().contains("insert: item without parent"),
531                "Expected error, got: {:?}",
532                e,
533            );
534
535            let e = conn
536                .execute_cached(
537                    "INSERT INTO moz_bookmarks
538                        (fk, type, parent, position, dateAdded, lastModified, guid)
539                     VALUES
540                        (NULL, 2, -1, 0, 1, 1, 'fake_guid___')",
541                    [],
542                )
543                .expect_err("should fail to insert item with nonexistent parent");
544            assert!(
545                e.to_string().contains("insert: item without parent"),
546                "Expected error, got: {:?}",
547                e,
548            );
549
550            let e = conn
551                .execute(
552                    "UPDATE moz_bookmarks SET
553                        parent = NULL
554                     WHERE guid = 'folder_guid_'",
555                    [],
556                )
557                .expect_err("should fail to update folder with NULL parent");
558            assert!(
559                e.to_string().contains("update: item without parent"),
560                "Expected error, got: {:?}",
561                e,
562            );
563
564            // Bug 1941655 - we only guard against NULL parents, not missing ones.
565            /*
566            let e = conn
567                .execute(
568                    "UPDATE moz_bookmarks SET
569                        parent = -1
570                     WHERE guid = 'folder_guid_'",
571                    [],
572                )
573                .expect_err("should fail to update folder with nonexistent parent");
574            assert!(
575                e.to_string().contains("update: item without parent"),
576                "Expected error, got: {:?}",
577                e,
578            );
579            */
580        }
581
582        // Invalid length guid
583        {
584            let e = conn
585                .execute_cached(
586                    "INSERT INTO moz_bookmarks
587                        (fk, type, parent, position, dateAdded, lastModified, guid)
588                     VALUES
589                        (NULL, 2, 0, 0, 1, 1, 'fake_guid')",
590                    [],
591                )
592                .expect_err("should fail");
593            assert!(
594                e.to_string().contains("insert: len(guid)=9"),
595                "Expected error, got: {:?}",
596                e,
597            );
598
599            let e = conn
600                .execute(
601                    "UPDATE moz_bookmarks SET
602                        guid = 'fake_guid'
603                     WHERE guid = 'bmk_guid____'",
604                    [],
605                )
606                .expect_err("should fail to update bookmark with invalid guid");
607            assert!(
608                e.to_string().contains("update: len(guid)=9"),
609                "Expected error, got: {:?}",
610                e,
611            );
612        }
613
614        // Changing the type of an existing item.
615        {
616            let e = conn
617                .execute(
618                    "UPDATE moz_bookmarks SET
619                        type = 3
620                     WHERE guid = 'folder_guid_'",
621                    [],
622                )
623                .expect_err("should fail to update type of bookmark");
624            assert!(
625                e.to_string().contains("update: old type=2; new=3"),
626                "Expected error, got: {:?}",
627                e,
628            );
629        }
630    }
631
632    fn select_simple_int(conn: &PlacesDb, stmt: &str) -> u32 {
633        let count: Result<Option<u32>> =
634            conn.try_query_row(stmt, [], |row| Ok(row.get::<_, u32>(0)?), false);
635        count.unwrap().unwrap()
636    }
637
638    fn get_foreign_count(conn: &PlacesDb, guid: &SyncGuid) -> u32 {
639        let count: Result<Option<u32>> = conn.try_query_row(
640            "SELECT foreign_count from moz_places
641                     WHERE guid = :guid",
642            &[(":guid", guid)],
643            |row| Ok(row.get::<_, u32>(0)?),
644            true,
645        );
646        count.unwrap().unwrap()
647    }
648
649    #[test]
650    fn test_bookmark_foreign_count_triggers() {
651        // create the place.
652        let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("no memory db");
653        let guid1 = SyncGuid::random();
654        let url1 = Url::parse("http://example.com").expect("valid url");
655        let guid2 = SyncGuid::random();
656        let url2 = Url::parse("http://example2.com").expect("valid url");
657
658        conn.execute_cached(
659            "INSERT INTO moz_places (guid, url, url_hash) VALUES (:guid, :url, hash(:url))",
660            &[
661                (":guid", &guid1 as &dyn rusqlite::ToSql),
662                (":url", &String::from(url1)),
663            ],
664        )
665        .expect("should work");
666        let place_id1 = conn.last_insert_rowid();
667
668        conn.execute_cached(
669            "INSERT INTO moz_places (guid, url, url_hash) VALUES (:guid, :url, hash(:url))",
670            &[
671                (":guid", &guid2 as &dyn rusqlite::ToSql),
672                (":url", &String::from(url2)),
673            ],
674        )
675        .expect("should work");
676        let place_id2 = conn.last_insert_rowid();
677
678        assert_eq!(get_foreign_count(&conn, &guid1), 0);
679        assert_eq!(get_foreign_count(&conn, &guid2), 0);
680
681        // record visits for both URLs, otherwise the place itself will be removed with the bookmark
682        conn.execute_cached(
683            "INSERT INTO moz_historyvisits (place_id, visit_date, visit_type, is_local)
684             VALUES (:place, 10000000, 1, 0);",
685            &[(":place", &place_id1)],
686        )
687        .expect("should work");
688        conn.execute_cached(
689            "INSERT INTO moz_historyvisits (place_id, visit_date, visit_type, is_local)
690             VALUES (:place, 10000000, 1, 1);",
691            &[(":place", &place_id2)],
692        )
693        .expect("should work");
694
695        // create a bookmark pointing at it.
696        conn.execute_cached(
697            "INSERT INTO moz_bookmarks
698                (fk, type, parent, position, dateAdded, lastModified, guid)
699            VALUES
700                (:place_id, 1, 1, 0, 1, 1, 'fake_guid___')",
701            &[(":place_id", &place_id1)],
702        )
703        .expect("should work");
704        assert_eq!(get_foreign_count(&conn, &guid1), 1);
705        assert_eq!(get_foreign_count(&conn, &guid2), 0);
706
707        // change the bookmark to point at a different place.
708        conn.execute_cached(
709            "UPDATE moz_bookmarks SET fk = :new_place WHERE guid = 'fake_guid___';",
710            &[(":new_place", &place_id2)],
711        )
712        .expect("should work");
713        assert_eq!(get_foreign_count(&conn, &guid1), 0);
714        assert_eq!(get_foreign_count(&conn, &guid2), 1);
715
716        conn.execute("DELETE FROM moz_bookmarks WHERE guid = 'fake_guid___';", [])
717            .expect("should work");
718        assert_eq!(get_foreign_count(&conn, &guid1), 0);
719        assert_eq!(get_foreign_count(&conn, &guid2), 0);
720    }
721
722    #[test]
723    fn test_bookmark_synced_foreign_count_triggers() {
724        // create the place.
725        let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("no memory db");
726
727        let url = Url::parse("http://example.com").expect("valid url");
728
729        conn.execute_cached(
730            "INSERT INTO moz_places (guid, url, url_hash) VALUES ('fake_guid___', :url, hash(:url))",
731            &[(":url", &String::from(url))],
732        )
733        .expect("should work");
734        let place_id = conn.last_insert_rowid();
735
736        assert_eq!(get_foreign_count(&conn, &"fake_guid___".into()), 0);
737
738        // create a bookmark pointing at it.
739        conn.execute_cached(
740            "INSERT INTO moz_bookmarks_synced
741                (placeId, guid)
742            VALUES
743                (:place_id, 'fake_guid___')",
744            &[(":place_id", &place_id)],
745        )
746        .expect("should work");
747        assert_eq!(get_foreign_count(&conn, &"fake_guid___".into()), 1);
748
749        // delete it.
750        conn.execute_cached(
751            "DELETE FROM moz_bookmarks_synced WHERE guid = 'fake_guid___';",
752            [],
753        )
754        .expect("should work");
755        assert_eq!(get_foreign_count(&conn, &"fake_guid___".into()), 0);
756    }
757
758    #[test]
759    fn test_bookmark_delete_restrict() {
760        let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("no memory db");
761        conn.execute_all(&[
762            "INSERT INTO moz_places
763                (guid, url, url_hash)
764             VALUES
765                ('place_guid__', 'http://example.com/', hash('http://example.com/'))",
766            "INSERT INTO moz_bookmarks
767                (type, parent, position, dateAdded, lastModified, guid, fk)
768            VALUES
769                (1, 1, 0, 1, 1, 'fake_guid___', last_insert_rowid())",
770        ])
771        .expect("should be able to do the inserts");
772
773        // Should be impossible to delete the place.
774        conn.execute("DELETE FROM moz_places WHERE guid = 'place_guid__';", [])
775            .expect_err("should fail");
776
777        // delete the bookmark.
778        conn.execute("DELETE FROM moz_bookmarks WHERE guid = 'fake_guid___';", [])
779            .expect("should be able to delete the bookmark");
780
781        // now we should be able to delete the place.
782        conn.execute("DELETE FROM moz_places WHERE guid = 'place_guid__';", [])
783            .expect("should now be able to delete the place");
784    }
785
786    #[test]
787    fn test_bookmark_auto_deletes() {
788        let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("no memory db");
789
790        conn.execute_all(&[
791            // A folder to hold a bookmark.
792            "INSERT INTO moz_bookmarks
793                (type, parent, position, dateAdded, lastModified, guid)
794            VALUES
795                (3, 1, 0, 1, 1, 'folder_guid_')",
796            // A place for the bookmark.
797            "INSERT INTO moz_places
798                (guid, url, url_hash)
799            VALUES ('place_guid__', 'http://example.com/', hash('http://example.com/'))",
800            // The bookmark.
801            "INSERT INTO moz_bookmarks
802                (fk, type, parent, position, dateAdded, lastModified, guid)
803            VALUES
804                --fk,                  type
805                (last_insert_rowid(), 1,
806                -- parent
807                 (SELECT id FROM moz_bookmarks WHERE guid = 'folder_guid_'),
808                -- positon, dateAdded, lastModified, guid
809                   0,       1,         1,           'bookmarkguid')",
810        ])
811        .expect("inserts should work");
812
813        // Delete the folder - the bookmark should cascade delete.
814        conn.execute("DELETE FROM moz_bookmarks WHERE guid = 'folder_guid_';", [])
815            .expect("should work");
816
817        // folder should be gone.
818        assert_eq!(
819            select_simple_int(
820                &conn,
821                "SELECT count(*) FROM moz_bookmarks WHERE guid = 'folder_guid_'"
822            ),
823            0
824        );
825        // bookmark should be gone.
826        assert_eq!(
827            select_simple_int(
828                &conn,
829                "SELECT count(*) FROM moz_bookmarks WHERE guid = 'bookmarkguid';"
830            ),
831            0
832        );
833
834        // Place should also be gone as bookmark url had no visits.
835        assert_eq!(
836            select_simple_int(
837                &conn,
838                "SELECT COUNT(*) from moz_places WHERE guid = 'place_guid__';"
839            ),
840            0
841        );
842    }
843
844    #[test]
845    fn test_bookmark_auto_deletes_place_remains() {
846        let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("no memory db");
847
848        conn.execute_all(&[
849            // A folder to hold a bookmark.
850            "INSERT INTO moz_bookmarks
851                (type, parent, position, dateAdded, lastModified, guid)
852            VALUES
853                (3, 1, 0, 1, 1, 'folder_guid_')",
854            // A place for the bookmark.
855            "INSERT INTO moz_places
856                (guid, url, url_hash, foreign_count) -- here we pretend it has a foreign count.
857            VALUES ('place_guid__', 'http://example.com/', hash('http://example.com/'), 1)",
858            // The bookmark.
859            "INSERT INTO moz_bookmarks
860                (fk, type, parent, position, dateAdded, lastModified, guid)
861            VALUES
862                --fk,                  type
863                (last_insert_rowid(), 1,
864                -- parent
865                 (SELECT id FROM moz_bookmarks WHERE guid = 'folder_guid_'),
866                -- positon, dateAdded, lastModified, guid
867                   0,       1,         1,           'bookmarkguid')",
868        ])
869        .expect("inserts should work");
870
871        // Delete the folder - the bookmark should cascade delete.
872        conn.execute("DELETE FROM moz_bookmarks WHERE guid = 'folder_guid_';", [])
873            .expect("should work");
874
875        // folder should be gone.
876        assert_eq!(
877            select_simple_int(
878                &conn,
879                "SELECT count(*) FROM moz_bookmarks WHERE guid = 'folder_guid_'"
880            ),
881            0
882        );
883        // bookmark should be gone.
884        assert_eq!(
885            select_simple_int(
886                &conn,
887                "SELECT count(*) FROM moz_bookmarks WHERE guid = 'bookmarkguid';"
888            ),
889            0
890        );
891
892        // Place should remain as we pretended it has a foreign reference.
893        assert_eq!(
894            select_simple_int(
895                &conn,
896                "SELECT COUNT(*) from moz_places WHERE guid = 'place_guid__';"
897            ),
898            1
899        );
900    }
901
902    #[test]
903    fn test_bookmark_tombstone_auto_created() {
904        let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("no memory db");
905        conn.execute(
906            &format!(
907                "INSERT INTO moz_bookmarks
908                        (syncStatus, type, parent, position, dateAdded, lastModified, guid)
909                     VALUES
910                        ({}, 3, 1, 0, 1, 1, 'bookmarkguid')",
911                SyncStatus::Normal as u8
912            ),
913            [],
914        )
915        .expect("should insert regular bookmark folder");
916        conn.execute("DELETE FROM moz_bookmarks WHERE guid = 'bookmarkguid'", [])
917            .expect("should delete");
918        // should have a tombstone.
919        assert_eq!(
920            select_simple_int(&conn, "SELECT COUNT(*) from moz_bookmarks_deleted"),
921            1
922        );
923        conn.execute("DELETE from moz_bookmarks_deleted", [])
924            .expect("should delete");
925        conn.execute(
926            &format!(
927                "INSERT INTO moz_bookmarks
928                        (syncStatus, type, parent, position, dateAdded, lastModified, guid)
929                     VALUES
930                        ({}, 3, 1, 0, 1, 1, 'bookmarkguid')",
931                SyncStatus::New as u8
932            ),
933            [],
934        )
935        .expect("should insert regular bookmark folder");
936        conn.execute("DELETE FROM moz_bookmarks WHERE guid = 'bookmarkguid'", [])
937            .expect("should delete");
938        // should not have a tombstone as syncStatus is new.
939        assert_eq!(
940            select_simple_int(&conn, "SELECT COUNT(*) from moz_bookmarks_deleted"),
941            0
942        );
943    }
944
945    #[test]
946    fn test_bookmark_tombstone_auto_deletes() {
947        let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("no memory db");
948        conn.execute(
949            "INSERT into moz_bookmarks_deleted VALUES ('bookmarkguid', 1)",
950            [],
951        )
952        .expect("should insert tombstone");
953        assert_eq!(
954            select_simple_int(&conn, "SELECT COUNT(*) from moz_bookmarks_deleted"),
955            1
956        );
957        // create a bookmark with the same guid.
958        conn.execute(
959            "INSERT INTO moz_bookmarks
960                        (type, parent, position, dateAdded, lastModified, guid)
961                     VALUES
962                        (3, 1, 0, 1, 1, 'bookmarkguid')",
963            [],
964        )
965        .expect("should insert regular bookmark folder");
966        // tombstone should have vanished.
967        assert_eq!(
968            select_simple_int(&conn, "SELECT COUNT(*) from moz_bookmarks_deleted"),
969            0
970        );
971    }
972
973    #[test]
974    fn test_bookmark_tombstone_auto_deletes_on_update() {
975        let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("no memory db");
976
977        // check updates do the right thing.
978        conn.execute(
979            "INSERT into moz_bookmarks_deleted VALUES ('bookmarkguid', 1)",
980            [],
981        )
982        .expect("should insert tombstone");
983
984        // create a bookmark with a different guid.
985        conn.execute(
986            "INSERT INTO moz_bookmarks
987                        (type, parent, position, dateAdded, lastModified, guid)
988                     VALUES
989                        (3, 1, 0, 1, 1, 'fake_guid___')",
990            [],
991        )
992        .expect("should insert regular bookmark folder");
993        // tombstone should remain.
994        assert_eq!(
995            select_simple_int(&conn, "SELECT COUNT(*) from moz_bookmarks_deleted"),
996            1
997        );
998        // update guid - should fail as we have a trigger with RAISEs.
999        conn.execute(
1000            "UPDATE moz_bookmarks SET guid = 'bookmarkguid'
1001             WHERE guid = 'fake_guid___'",
1002            [],
1003        )
1004        .expect_err("changing the guid should fail");
1005    }
1006
1007    #[test]
1008    fn test_origin_triggers_simple_removal() {
1009        let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("no memory db");
1010        let guid = SyncGuid::random();
1011        let url = String::from(Url::parse("http://example.com").expect("valid url"));
1012
1013        conn.execute(
1014            "INSERT INTO moz_places (guid, url, url_hash) VALUES (:guid, :url, hash(:url))",
1015            rusqlite::named_params! {
1016                ":guid": &guid,
1017                ":url": &url,
1018            },
1019        )
1020        .expect("should work");
1021        // origins are maintained via triggers, so make sure they are done.
1022        crate::storage::delete_pending_temp_tables(&conn).expect("should work");
1023
1024        // We should have inserted the origin.
1025        assert_eq!(
1026            select_simple_int(
1027                &conn,
1028                "SELECT count(*) FROM moz_origins WHERE host = 'example.com'"
1029            ),
1030            1
1031        );
1032
1033        // delete the place, ensure triggers have run, and check origin has gone.
1034        conn.execute("DELETE FROM moz_places", [])
1035            .expect("should work");
1036        crate::storage::delete_pending_temp_tables(&conn).expect("should work");
1037        assert_eq!(
1038            select_simple_int(&conn, "SELECT count(*) FROM moz_origins"),
1039            0
1040        );
1041    }
1042
1043    const CREATE_V15_DB: &str = include_str!("../../sql/tests/create_v15_db.sql");
1044
1045    #[test]
1046    fn test_upgrade_schema_15_16() {
1047        let db_file = MigratedDatabaseFile::new(PlacesInitializer::new_for_test(), CREATE_V15_DB);
1048
1049        db_file.upgrade_to(16);
1050        let db = db_file.open();
1051
1052        // Test the unknownFields column was added
1053        assert_eq!(
1054            db.query_one::<String>("SELECT type FROM pragma_table_info('moz_bookmarks_synced') WHERE name = 'unknownFields'").unwrap(),
1055            "TEXT"
1056        );
1057    }
1058
1059    #[test]
1060    fn test_gh5464() {
1061        // Test the gh-5464 error case: A user with the `v16` schema, but with `user_version` set
1062        // to 15
1063        let db_file = MigratedDatabaseFile::new(PlacesInitializer::new_for_test(), CREATE_V15_DB);
1064        db_file.upgrade_to(16);
1065        let db = db_file.open();
1066        db.execute("PRAGMA user_version=15", []).unwrap();
1067        drop(db);
1068        db_file.upgrade_to(16);
1069    }
1070
1071    const CREATE_V17_DB: &str = include_str!("../../sql/tests/create_v17_db.sql");
1072
1073    #[test]
1074    fn test_upgrade_schema_17_18() {
1075        let db_file = MigratedDatabaseFile::new(PlacesInitializer::new_for_test(), CREATE_V17_DB);
1076
1077        db_file.upgrade_to(18);
1078        let db = db_file.open();
1079
1080        let sql = db
1081            .query_one::<String>(
1082                "SELECT sql FROM sqlite_schema WHERE type = 'table' AND name = 'moz_bookmarks'",
1083            )
1084            .expect("should retrieve CREATE TABLE statement");
1085        assert!(!sql.contains("CHECK"));
1086        assert!(!sql.contains("FOREIGN KEY(parent)"));
1087
1088        // Make sure that we didn't corrupt the database.
1089        let integrity_ok = db
1090            .query_row("PRAGMA integrity_check", [], |row| {
1091                Ok(row.get::<_, String>(0)? == "ok")
1092            })
1093            .expect("should perform integrity check");
1094        assert!(integrity_ok);
1095
1096        let foreign_keys_ok = db
1097            .prepare("PRAGMA foreign_key_check")
1098            .and_then(|mut statement| Ok(statement.query([])?.next()?.is_none()))
1099            .expect("should perform foreign key check");
1100        assert!(foreign_keys_ok);
1101
1102        // ...And that we can read everything we inserted.
1103        #[derive(Eq, PartialEq, Debug)]
1104        struct BookmarkRow {
1105            id: i64,
1106            type_: i64,
1107            parent: Option<i64>,
1108            fk: Option<i64>,
1109            guid: String,
1110        }
1111        let rows = db
1112            .query_rows_and_then(
1113                "SELECT id, type, parent, fk, guid FROM moz_bookmarks ORDER BY id",
1114                [],
1115                |row| -> rusqlite::Result<_> {
1116                    Ok(BookmarkRow {
1117                        id: row.get("id")?,
1118                        type_: row.get("type")?,
1119                        parent: row.get("parent")?,
1120                        fk: row.get("fk")?,
1121                        guid: row.get("guid")?,
1122                    })
1123                },
1124            )
1125            .expect("should query all bookmark rows");
1126        assert_eq!(
1127            rows,
1128            &[
1129                BookmarkRow {
1130                    id: 1,
1131                    type_: 2,
1132                    parent: None,
1133                    fk: None,
1134                    guid: "root________".into()
1135                },
1136                BookmarkRow {
1137                    id: 2,
1138                    type_: 2,
1139                    parent: Some(1),
1140                    fk: None,
1141                    guid: "folder_guid_".into()
1142                },
1143                BookmarkRow {
1144                    id: 3,
1145                    type_: 1,
1146                    parent: Some(2),
1147                    fk: Some(1),
1148                    guid: "bmk_guid____".into()
1149                },
1150                BookmarkRow {
1151                    id: 4,
1152                    type_: 3,
1153                    parent: Some(2),
1154                    fk: None,
1155                    guid: "sep_guid____".into()
1156                }
1157            ]
1158        );
1159    }
1160
1161    #[test]
1162    fn test_all_upgrades() {
1163        // Test the migration process in general: open a fresh DB and a DB that's gone through the migration
1164        // process.  Check that the schemas match.
1165        let fresh_db = PlacesDb::open_in_memory(ConnectionType::ReadWrite).unwrap();
1166
1167        let db_file = MigratedDatabaseFile::new(PlacesInitializer::new_for_test(), CREATE_V15_DB);
1168        db_file.run_all_upgrades();
1169        let upgraded_db = db_file.open();
1170
1171        assert_eq!(
1172            fresh_db.query_one::<u32>("PRAGMA user_version").unwrap(),
1173            upgraded_db.query_one::<u32>("PRAGMA user_version").unwrap(),
1174        );
1175        let all_tables = [
1176            "moz_places",
1177            "moz_places_tombstones",
1178            "moz_places_stale_frecencies",
1179            "moz_historyvisits",
1180            "moz_historyvisit_tombstones",
1181            "moz_inputhistory",
1182            "moz_bookmarks",
1183            "moz_bookmarks_deleted",
1184            "moz_origins",
1185            "moz_meta",
1186            "moz_tags",
1187            "moz_tags_relation",
1188            "moz_bookmarks_synced",
1189            "moz_bookmarks_synced_structure",
1190            "moz_bookmarks_synced_tag_relation",
1191            "moz_keywords",
1192            "moz_places_metadata",
1193            "moz_places_metadata_search_queries",
1194        ];
1195        #[derive(Debug, Ord, PartialOrd, Eq, PartialEq)]
1196        struct ColumnInfo {
1197            name: String,
1198            type_: String,
1199            not_null: bool,
1200            default_value: Option<String>,
1201            pk: bool,
1202        }
1203
1204        fn get_table_column_info(conn: &Connection, table_name: &str) -> BTreeSet<ColumnInfo> {
1205            let mut stmt = conn
1206                .prepare("SELECT name, type, `notnull`, dflt_value, pk FROM pragma_table_info(?)")
1207                .unwrap();
1208            stmt.query_map((table_name,), |row| {
1209                Ok(ColumnInfo {
1210                    name: row.get(0)?,
1211                    type_: row.get(1)?,
1212                    not_null: row.get(2)?,
1213                    default_value: row.get(3)?,
1214                    pk: row.get(4)?,
1215                })
1216            })
1217            .unwrap()
1218            .collect::<rusqlite::Result<BTreeSet<_>>>()
1219            .unwrap()
1220        }
1221        for table_name in all_tables {
1222            assert_eq!(
1223                get_table_column_info(&upgraded_db, table_name),
1224                get_table_column_info(&fresh_db, table_name),
1225            );
1226        }
1227    }
1228}