Skip to content

Commit

Permalink
Bug 1935797 - Replace CHECK and parent FOREIGN KEY constraints on `mo…
Browse files Browse the repository at this point in the history
…z_bookmarks` with triggers.
  • Loading branch information
linabutler committed Dec 7, 2024
1 parent d3a5b91 commit c80a4b0
Show file tree
Hide file tree
Showing 5 changed files with 664 additions and 58 deletions.
8 changes: 1 addition & 7 deletions components/places/sql/create_shared_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,12 @@ CREATE TABLE IF NOT EXISTS moz_bookmarks (
title TEXT, -- a'la bug 1356159, NULL is special here - it means 'not edited'
dateAdded INTEGER NOT NULL DEFAULT 0,
lastModified INTEGER NOT NULL DEFAULT 0,
guid TEXT NOT NULL UNIQUE CHECK(length(guid) == 12),
guid TEXT NOT NULL UNIQUE,

syncStatus INTEGER NOT NULL DEFAULT 0,
syncChangeCounter INTEGER NOT NULL DEFAULT 1,

-- bookmarks must have a fk to a URL, other types must not.
CHECK((type == 1 AND fk IS NOT NULL) OR (type > 1 AND fk IS NULL))
-- only the root is allowed to have a non-null parent
CHECK(guid == "root________" OR parent IS NOT NULL)

FOREIGN KEY(fk) REFERENCES moz_places(id) ON DELETE RESTRICT
FOREIGN KEY(parent) REFERENCES moz_bookmarks(id) ON DELETE CASCADE
);

-- CREATE INDEX IF NOT EXISTS itemindex ON moz_bookmarks(fk, type);
Expand Down
59 changes: 59 additions & 0 deletions components/places/sql/create_shared_triggers.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,57 @@ BEGIN
DELETE FROM moz_places_tombstones WHERE guid = NEW.guid;
END;

CREATE TEMP TRIGGER moz_bookmarks_beforeinsert_trigger
BEFORE INSERT ON moz_bookmarks
BEGIN
-- SQLite < 3.47.0 only supports string literals for error messages,
-- so we use our own `throw(...)` function.

SELECT throw(format('insert: length(guid) = %d', length(NEW.guid)))
WHERE length(NEW.guid) <> 12;

SELECT throw('insert: type = 1; fk IS NULL')
WHERE NEW.type = 1
AND NEW.fk IS NULL;

SELECT throw(format('insert: type = %d; fk IS NOT NULL', NEW.type))
WHERE NEW.type <> 1
AND NEW.fk IS NOT NULL;

SELECT throw('insert: root with parent')
WHERE NEW.guid = 'root________'
AND NEW.parent IS NOT NULL;

SELECT throw('insert: item without parent')
WHERE NEW.guid <> 'root________'
AND NOT EXISTS(
SELECT 1 FROM moz_bookmarks WHERE id = NEW.parent);
END;

CREATE TEMP TRIGGER moz_bookmarks_beforeupdate_trigger
BEFORE UPDATE ON moz_bookmarks
BEGIN
SELECT throw(format('update: length(guid) = %d', length(NEW.guid)))
WHERE length(NEW.guid) <> 12;

SELECT throw('update: type = 1; fk IS NULL')
WHERE NEW.type = 1
AND NEW.fk IS NULL;

SELECT throw(format('update: type = %d; fk IS NOT NULL', NEW.type))
WHERE NEW.type <> 1
AND NEW.fk IS NOT NULL;

SELECT throw('update: root with parent')
WHERE NEW.guid = 'root________'
AND NEW.parent IS NOT NULL;

SELECT throw('update: item without parent')
WHERE NEW.guid <> 'root________'
AND NOT EXISTS(
SELECT 1 FROM moz_bookmarks WHERE id = NEW.parent);
END;

-- Triggers which update visit_count and last_visit_date based on historyvisits
-- table changes.
-- NOTE: the values "0, 4, 7, 8, 9" below are EXCLUDED_VISIT_TYPES, stolen
Expand Down Expand Up @@ -260,6 +311,14 @@ BEGIN
AND last_visit_date_remote = 0;
END;

-- Equivalent to a CASCADE foreign key action on `moz_bookmarks.parent`.
CREATE TEMP TRIGGER moz_bookmarks_folder_afterdelete_trigger
AFTER DELETE ON moz_bookmarks
BEGIN
DELETE FROM moz_bookmarks
WHERE parent = OLD.id;
END;

-- These triggers adjust the foreign count for tagged URLs, and bump the
-- tag's last modified time when a URL is tagged or untagged. These are
-- split out from the main connection's tag triggers because we also want
Expand Down
292 changes: 292 additions & 0 deletions components/places/sql/tests/create_v17_db.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
-- 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/.

-- XXX - TODO - moz_annos
-- XXX - TODO - moz_anno_attributes
-- XXX - TODO - moz_items_annos

CREATE TABLE IF NOT EXISTS moz_places (
id INTEGER PRIMARY KEY,
url LONGVARCHAR NOT NULL,
title LONGVARCHAR,
-- note - desktop has rev_host here - that's now in moz_origin.
visit_count_local INTEGER NOT NULL DEFAULT 0,
visit_count_remote INTEGER NOT NULL DEFAULT 0,
hidden INTEGER DEFAULT 0 NOT NULL,
typed INTEGER DEFAULT 0 NOT NULL, -- XXX - is 'typed' ok? Note also we want this as a *count*, not a bool.
frecency INTEGER DEFAULT -1 NOT NULL,
-- XXX - splitting last visit into local and remote correct?
last_visit_date_local INTEGER NOT NULL DEFAULT 0,
last_visit_date_remote INTEGER NOT NULL DEFAULT 0,
guid TEXT NOT NULL UNIQUE,
foreign_count INTEGER DEFAULT 0 NOT NULL,
url_hash INTEGER DEFAULT 0 NOT NULL,
description TEXT, -- XXXX - title above?
preview_image_url TEXT,
-- origin_id would ideally be NOT NULL, but we use a trigger to keep
-- it up to date, so do perform the initial insert with a null.
origin_id INTEGER,
-- a couple of sync-related fields.
sync_status TINYINT NOT NULL DEFAULT 1, -- 1 is SyncStatus::New
sync_change_counter INTEGER NOT NULL DEFAULT 0, -- adding visits will increment this
unknown_fields TEXT,

FOREIGN KEY(origin_id) REFERENCES moz_origins(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS url_hashindex ON moz_places(url_hash);
CREATE INDEX IF NOT EXISTS visitcountlocal ON moz_places(visit_count_local);
CREATE INDEX IF NOT EXISTS visitcountremote ON moz_places(visit_count_remote);
CREATE INDEX IF NOT EXISTS frecencyindex ON moz_places(frecency);
CREATE INDEX IF NOT EXISTS lastvisitdatelocalindex ON moz_places(last_visit_date_local);
CREATE INDEX IF NOT EXISTS lastvisitdateremoteindex ON moz_places(last_visit_date_remote);
CREATE UNIQUE INDEX IF NOT EXISTS guid_uniqueindex ON moz_places(guid);
CREATE INDEX IF NOT EXISTS originidindex ON moz_places(origin_id);


CREATE TABLE IF NOT EXISTS moz_places_tombstones (
guid TEXT PRIMARY KEY
) WITHOUT ROWID;


-- This table stores Place IDs with stale frecencies, along with the time they
-- were marked as stale. Maintenance and Sync periodically recalculate
-- frecencies for Place IDs in this table.
CREATE TABLE IF NOT EXISTS moz_places_stale_frecencies (
place_id INTEGER PRIMARY KEY NOT NULL REFERENCES moz_places(id)
ON DELETE CASCADE,
stale_at INTEGER NOT NULL -- In milliseconds.
);


CREATE TABLE IF NOT EXISTS moz_historyvisits (
id INTEGER PRIMARY KEY,
is_local INTEGER NOT NULL, -- XXX - not in desktop - will always be true for visits added locally, always false visits added by sync.
from_visit INTEGER, -- XXX - self-reference?
place_id INTEGER NOT NULL,
visit_date INTEGER NOT NULL,
visit_type INTEGER NOT NULL,
-- session INTEGER, -- XXX - what is 'session'? Appears unused.
unknown_fields TEXT,

FOREIGN KEY(place_id) REFERENCES moz_places(id) ON DELETE CASCADE,
FOREIGN KEY(from_visit) REFERENCES moz_historyvisits(id)
);

CREATE INDEX IF NOT EXISTS placedateindex ON moz_historyvisits(place_id, visit_date);
CREATE INDEX IF NOT EXISTS fromindex ON moz_historyvisits(from_visit);
CREATE INDEX IF NOT EXISTS dateindex ON moz_historyvisits(visit_date);
CREATE INDEX IF NOT EXISTS islocalindex ON moz_historyvisits(is_local);

-- Greatly helps the multi-join query in frecency.
CREATE INDEX IF NOT EXISTS visits_from_type_idx ON moz_historyvisits(from_visit, visit_type);

CREATE TABLE IF NOT EXISTS moz_historyvisit_tombstones (
place_id INTEGER NOT NULL,
visit_date INTEGER NOT NULL,
FOREIGN KEY(place_id) REFERENCES moz_places(id) ON DELETE CASCADE,
PRIMARY KEY(place_id, visit_date)
);


CREATE TABLE IF NOT EXISTS moz_inputhistory (
place_id INTEGER NOT NULL,
input LONGVARCHAR NOT NULL,
use_count INTEGER,

PRIMARY KEY (place_id, input),
FOREIGN KEY(place_id) REFERENCES moz_places(id) ON DELETE CASCADE
);


CREATE TABLE IF NOT EXISTS moz_bookmarks (
id INTEGER PRIMARY KEY,
fk INTEGER DEFAULT NULL, -- place_id
type INTEGER NOT NULL,
parent INTEGER,
position INTEGER NOT NULL,
title TEXT, -- a'la bug 1356159, NULL is special here - it means 'not edited'
dateAdded INTEGER NOT NULL DEFAULT 0,
lastModified INTEGER NOT NULL DEFAULT 0,
guid TEXT NOT NULL UNIQUE CHECK(length(guid) == 12),

syncStatus INTEGER NOT NULL DEFAULT 0,
syncChangeCounter INTEGER NOT NULL DEFAULT 1,

-- bookmarks must have a fk to a URL, other types must not.
CHECK((type == 1 AND fk IS NOT NULL) OR (type > 1 AND fk IS NULL))
-- only the root is allowed to have a non-null parent
CHECK(guid == "root________" OR parent IS NOT NULL)

FOREIGN KEY(fk) REFERENCES moz_places(id) ON DELETE RESTRICT
FOREIGN KEY(parent) REFERENCES moz_bookmarks(id) ON DELETE CASCADE
);

-- CREATE INDEX IF NOT EXISTS itemindex ON moz_bookmarks(fk, type);
-- CREATE INDEX IF NOT EXISTS parentindex ON moz_bookmarks(parent, position);
CREATE INDEX IF NOT EXISTS itemlastmodifiedindex ON moz_bookmarks(fk, lastModified);
-- CREATE INDEX IF NOT EXISTS dateaddedindex ON moz_bookmarks(dateAdded);
CREATE UNIQUE INDEX IF NOT EXISTS guid_uniqueindex ON moz_bookmarks(guid);


CREATE TABLE IF NOT EXISTS moz_bookmarks_deleted (
guid TEXT PRIMARY KEY,
dateRemoved INTEGER NOT NULL
) WITHOUT ROWID;

-- Note: desktop has/had a 'keywords' table, but we intentionally do not.


CREATE TABLE IF NOT EXISTS moz_origins (
id INTEGER PRIMARY KEY,
prefix TEXT NOT NULL,
host TEXT NOT NULL,
rev_host TEXT NOT NULL,
frecency INTEGER NOT NULL, -- XXX - why not default of -1 like in moz_places?
UNIQUE (prefix, host)
);

CREATE INDEX IF NOT EXISTS hostindex ON moz_origins(rev_host);


-- This table holds key-value metadata for Places and its consumers. Sync stores
-- the sync IDs for the bookmarks and history collections in this table, and the
-- last sync time for history.
CREATE TABLE IF NOT EXISTS moz_meta (
key TEXT PRIMARY KEY,
value NOT NULL
) WITHOUT ROWID;

-- Support for tags.
CREATE TABLE IF NOT EXISTS moz_tags(
id INTEGER PRIMARY KEY,
tag TEXT UNIQUE NOT NULL,
lastModified INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS moz_tags_relation(
tag_id INTEGER NOT NULL REFERENCES moz_tags(id) ON DELETE CASCADE,
place_id INTEGER NOT NULL REFERENCES moz_places(id) ON DELETE CASCADE,
PRIMARY KEY(tag_id, place_id)
) WITHOUT ROWID;

-- This table holds synced items, including tombstones. It's unused if Sync
-- isn't configured. At the end of a sync, this table's contents should match
-- both what's on the server, and the local tree in `moz_bookmarks`.
CREATE TABLE IF NOT EXISTS moz_bookmarks_synced(
id INTEGER PRIMARY KEY,
-- We intentionally don't validate GUIDs, as we allow and fix up invalid
-- ones.
guid TEXT UNIQUE NOT NULL,
-- The `parentid` from the record.
parentGuid TEXT,
-- The server modified time, in milliseconds. This is *not* a
-- ServerTimestamp, which is in fractional seconds.
serverModified INTEGER NOT NULL DEFAULT 0,
needsMerge BOOLEAN NOT NULL DEFAULT 0,
validity INTEGER NOT NULL DEFAULT 1, -- SyncValidity::Valid
isDeleted BOOLEAN NOT NULL DEFAULT 0,
kind INTEGER NOT NULL DEFAULT -1,
-- The creation date, in milliseconds.
dateAdded INTEGER NOT NULL DEFAULT 0,
title TEXT,
placeId INTEGER REFERENCES moz_places(id)
ON DELETE SET NULL,
keyword TEXT,
description TEXT,
loadInSidebar BOOLEAN,
smartBookmarkName TEXT,
feedURL TEXT,
siteURL TEXT,
-- All unknown fields from the server record, encoded as a JSON object.
unknownFields TEXT
);

CREATE INDEX IF NOT EXISTS moz_bookmarks_synced_urls ON moz_bookmarks_synced(placeId);
CREATE INDEX IF NOT EXISTS moz_bookmarks_synced_keywords ON moz_bookmarks_synced(keyword)
WHERE keyword NOT NULL;

-- This table holds parent-child relationships and positions for synced items,
-- from each folder's `children`. Unlike `moz_bookmarks`, this is stored
-- separately because we might see an incoming folder before its children. This
-- also lets us catch disagreements between a folder's `children` and its
-- childrens' `parentid`.
CREATE TABLE IF NOT EXISTS moz_bookmarks_synced_structure(
guid TEXT,
parentGuid TEXT REFERENCES moz_bookmarks_synced(guid)
ON DELETE CASCADE,
position INTEGER NOT NULL,
PRIMARY KEY(parentGuid, guid)
) WITHOUT ROWID;

-- This table holds tags for synced items.
CREATE TABLE IF NOT EXISTS moz_bookmarks_synced_tag_relation(
itemId INTEGER NOT NULL REFERENCES moz_bookmarks_synced(id)
ON DELETE CASCADE,
tagId INTEGER NOT NULL REFERENCES moz_tags(id)
ON DELETE CASCADE,
PRIMARY KEY(itemId, tagId)
) WITHOUT ROWID;

-- This table holds search keywords for URLs. Desktop would like to replace
-- these with custom search engines eventually (bug 648398); however, we
-- must still round-trip keywords imported via Sync or migrated from Fennec.
-- Since none of the `moz_bookmarks_synced_*` tables are durable, we store
-- keywords for URLs in a separate table. Unlike Desktop, we don't support
-- custom POST data, since we don't sync it (bug 1345417), and Fennec
-- doesn't write it.
CREATE TABLE IF NOT EXISTS moz_keywords(
place_id INTEGER PRIMARY KEY REFERENCES moz_places(id)
ON DELETE RESTRICT,
keyword TEXT NOT NULL UNIQUE
);

----------------------------------------------------------------------
--------------------History Metadata----------------------------------
----------------------------------------------------------------------

-- These tables store metadata information related to moz_places.
-- None of this data is synced for now.
CREATE TABLE IF NOT EXISTS moz_places_metadata (
id INTEGER PRIMARY KEY,
created_at INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL DEFAULT 0,

place_id INTEGER NOT NULL,

total_view_time INTEGER NOT NULL DEFAULT 0, -- a rolling aggregate
search_query_id INTEGER,
referrer_place_id INTEGER,
document_type INTEGER NOT NULL DEFAULT 0, -- 0=generic, 1=media
typing_time INTEGER NOT NULL DEFAULT 0,
key_presses INTEGER NOT NULL DEFAULT 0,

FOREIGN KEY(place_id) REFERENCES moz_places(id) ON DELETE CASCADE,
FOREIGN KEY(search_query_id) REFERENCES moz_places_metadata_search_queries(id) ON DELETE CASCADE,
FOREIGN KEY(referrer_place_id) REFERENCES moz_places(id) ON DELETE CASCADE

CHECK(place_id != referrer_place_id)
);

CREATE TABLE IF NOT EXISTS moz_places_metadata_search_queries (
id INTEGER PRIMARY KEY,
term TEXT NOT NULL UNIQUE
);

INSERT INTO moz_places(id, guid, url, frecency)
VALUES(1, 'page_guid___', 'https://example.com', -1);

INSERT INTO moz_bookmarks(id, type, parent, position, dateAdded, lastModified, guid)
VALUES(1, 2, NULL, 0, 1, 1, 'root________');

INSERT INTO moz_bookmarks(id, type, parent, position, dateAdded, lastModified, guid)
VALUES(2, 2, 1, 0, 1, 1, 'folder_guid_');

INSERT INTO moz_bookmarks(id, type, parent, position, dateAdded, lastModified, fk, guid)
VALUES(3, 1, 2, 0, 1, 1, 1, 'bmk_guid____');

INSERT INTO moz_bookmarks(id, type, parent, position, dateAdded, lastModified, guid)
VALUES(4, 3, 2, 1, 1, 1, 'sep_guid____');

PRAGMA user_version = 17;
Loading

0 comments on commit c80a4b0

Please sign in to comment.