1use 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
19const 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
23const 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
27const CREATE_MAIN_TRIGGERS_SQL: &str = include_str!("../../sql/create_main_triggers.sql");
29
30lazy_static::lazy_static! {
31 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
41pub(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 ConnectionType::ReadOnly => {}
87
88 ConnectionType::ReadWrite => {
91 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 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
111fn 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 migration(db, from, 2, &[CREATE_SHARED_SCHEMA_SQL], || Ok(()))?;
149 migration(
150 db,
151 from,
152 3,
153 &[
154 "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(()))?; migration(db, from, 6, &[CREATE_SHARED_SCHEMA_SQL], || Ok(()))?; migration(
164 db,
165 from,
166 7,
167 &[
168 &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 &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 "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 "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 "CREATE INDEX IF NOT EXISTS moz_bookmarks_synced_keywords
218 ON moz_bookmarks_synced(keyword) WHERE keyword NOT NULL",
219 "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 "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 "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(()))?; migration(
257 db,
258 from,
259 14,
260 &[
261 "DROP TABLE moz_places_metadata",
263 CREATE_SHARED_SCHEMA_SQL,
264 ],
265 || Ok(()),
266 )?;
267
268 match from {
272 n if n < 15 => (),
274 15 => {
276 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 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 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 rusqlite::params![NEW_SQL],
327 )?;
328 }
329 _ => 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 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 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 {
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 {
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 {
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 }
581
582 {
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 {
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 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 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 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 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 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 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 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 conn.execute("DELETE FROM moz_places WHERE guid = 'place_guid__';", [])
775 .expect_err("should fail");
776
777 conn.execute("DELETE FROM moz_bookmarks WHERE guid = 'fake_guid___';", [])
779 .expect("should be able to delete the bookmark");
780
781 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 "INSERT INTO moz_bookmarks
793 (type, parent, position, dateAdded, lastModified, guid)
794 VALUES
795 (3, 1, 0, 1, 1, 'folder_guid_')",
796 "INSERT INTO moz_places
798 (guid, url, url_hash)
799 VALUES ('place_guid__', 'http://example.com/', hash('http://example.com/'))",
800 "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 conn.execute("DELETE FROM moz_bookmarks WHERE guid = 'folder_guid_';", [])
815 .expect("should work");
816
817 assert_eq!(
819 select_simple_int(
820 &conn,
821 "SELECT count(*) FROM moz_bookmarks WHERE guid = 'folder_guid_'"
822 ),
823 0
824 );
825 assert_eq!(
827 select_simple_int(
828 &conn,
829 "SELECT count(*) FROM moz_bookmarks WHERE guid = 'bookmarkguid';"
830 ),
831 0
832 );
833
834 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 "INSERT INTO moz_bookmarks
851 (type, parent, position, dateAdded, lastModified, guid)
852 VALUES
853 (3, 1, 0, 1, 1, 'folder_guid_')",
854 "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 "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 conn.execute("DELETE FROM moz_bookmarks WHERE guid = 'folder_guid_';", [])
873 .expect("should work");
874
875 assert_eq!(
877 select_simple_int(
878 &conn,
879 "SELECT count(*) FROM moz_bookmarks WHERE guid = 'folder_guid_'"
880 ),
881 0
882 );
883 assert_eq!(
885 select_simple_int(
886 &conn,
887 "SELECT count(*) FROM moz_bookmarks WHERE guid = 'bookmarkguid';"
888 ),
889 0
890 );
891
892 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 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 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 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 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 conn.execute(
979 "INSERT into moz_bookmarks_deleted VALUES ('bookmarkguid', 1)",
980 [],
981 )
982 .expect("should insert tombstone");
983
984 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 assert_eq!(
995 select_simple_int(&conn, "SELECT COUNT(*) from moz_bookmarks_deleted"),
996 1
997 );
998 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 crate::storage::delete_pending_temp_tables(&conn).expect("should work");
1023
1024 assert_eq!(
1026 select_simple_int(
1027 &conn,
1028 "SELECT count(*) FROM moz_origins WHERE host = 'example.com'"
1029 ),
1030 1
1031 );
1032
1033 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 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 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 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 #[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 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}