fix(db): make Blogs.BlogId the only blog ID and drop BlogNames
Blogs.BlogId was a one-time copy of BlogNames and nothing kept it current: 12,238 blogs first seen after 2026-08-07 had a BlogNames ID but a NULL Blogs.BlogId, so GetBlogs' join on BlogId silently skipped them and their 23,148 notes. - retire-blognames.sql: stub Blogs rows for the 17 unregistered note participants, backfill IDs (none renumbered), make ix_Blogs_BlogId UNIQUE, drop BlogNames, and add triggers that stop a Blogs row with a BlogId from being deleted, renamed or renumbered - AddNote registers both blogs via RegisterBlog (Blogs row + MAX+1 ID) and every query resolves names through Blogs instead of BlogNames - verify-db-schema.sql reports a DB that still has BlogNames (1e) - Update TL.db.md, AGENTS.md and the DB Browser saved queries Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
-- retire-blognames.sql
|
||||
-- Makes Blogs.BlogId the only ID authority for Notes and retires the BlogNames table.
|
||||
--
|
||||
-- WHY: normalize-notes.sql (2026-08-07) put the IDs in BlogNames and copied them into
|
||||
-- Blogs.BlogId once. Nothing kept the copy current: by 2026-09-28, 12,238 blogs first seen
|
||||
-- in a note after the migration had a BlogNames ID but Blogs.BlogId = NULL, so every query
|
||||
-- joining Notes to Blogs on BlogId (GetBlogs and friends) silently skipped them -- 23,148
|
||||
-- notes. Two copies of one ID drift; this leaves one.
|
||||
--
|
||||
-- What it does:
|
||||
-- 1. Gives every BlogNames name a Blogs row (17 had none), carrying its ID over.
|
||||
-- 2. Copies the ID onto every Blogs row that is missing it. IDs are never renumbered --
|
||||
-- they are stored in 1.18M Notes rows.
|
||||
-- 3. Proves every Notes ID resolves through Blogs before anything is dropped.
|
||||
-- 4. Makes ix_Blogs_BlogId UNIQUE.
|
||||
-- 5. Drops BlogNames. No compatibility view: any other app that still names it gets
|
||||
-- "no such table: BlogNames" and must port to Blogs.BlogId (see TL.db.md).
|
||||
-- 6. Adds triggers that stop a Blogs row holding a BlogId from being deleted, renamed or
|
||||
-- renumbered -- the guarantees BlogNames gave by never being touched.
|
||||
--
|
||||
-- DateModified is NOT moved: assigning an ID is bookkeeping, not a content change. The 17
|
||||
-- new stub rows get DateAdded/DateModified/DateCreated = now, as AddBlog would give them.
|
||||
--
|
||||
-- Runs after normalize-notes.sql. A backup from before 2026-08-07 needs both, in order.
|
||||
--
|
||||
-- HOW TO RUN:
|
||||
-- 1. Stop every app that uses TL.db. Pause NextCloud sync.
|
||||
-- 2. Back up TL.db: sqlite3 TL.db ".backup 'TL pre-retire-blognames.db'"
|
||||
-- 3. sqlite3 -bail TL.db < retire-blognames.sql
|
||||
-- -bail matters: a failed check aborts before COMMIT and nothing is changed.
|
||||
-- In DB Browser, Execute SQL stops at the first error; then Revert Changes.
|
||||
-- 4. Run the build of URLNotesGrabberCORE that no longer uses BlogNames. An older
|
||||
-- build fails every AddNote with "no such table: BlogNames".
|
||||
|
||||
PRAGMA foreign_keys = off;
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Every check inserts one count here; the CHECK aborts the script on anything but 0.
|
||||
CREATE TEMP TABLE MustBeZero (Check_ TEXT, n INTEGER CHECK (n = 0));
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
-- STEP 0: the two copies must not disagree anywhere they are both set
|
||||
--------------------------------------------------------------------------
|
||||
INSERT INTO MustBeZero
|
||||
SELECT 'Blogs.BlogId differs from BlogNames', COUNT(*)
|
||||
FROM Blogs b JOIN BlogNames bn ON bn.BlogName = b.BlogName
|
||||
WHERE b.BlogId <> bn.BlogId;
|
||||
|
||||
INSERT INTO MustBeZero
|
||||
SELECT 'Blogs.BlogId unknown to BlogNames', COUNT(*)
|
||||
FROM Blogs b
|
||||
WHERE b.BlogId IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM BlogNames bn WHERE bn.BlogId = b.BlogId AND bn.BlogName = b.BlogName);
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
-- STEP 1: a Blogs row for every name Notes points at
|
||||
--------------------------------------------------------------------------
|
||||
-- No IsActive in the column list: it is not ours to write (defaults to live).
|
||||
INSERT INTO Blogs (BlogName, DateAdded, DateModified, DateCreated, BlogId)
|
||||
SELECT bn.BlogName,
|
||||
strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime'),
|
||||
strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime'),
|
||||
strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime'),
|
||||
bn.BlogId
|
||||
FROM BlogNames bn
|
||||
WHERE NOT EXISTS (SELECT 1 FROM Blogs b WHERE b.BlogName = bn.BlogName);
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
-- STEP 2: backfill the IDs Blogs never received
|
||||
--------------------------------------------------------------------------
|
||||
UPDATE Blogs
|
||||
SET BlogId = (SELECT bn.BlogId FROM BlogNames bn WHERE bn.BlogName = Blogs.BlogName)
|
||||
WHERE BlogId IS NULL
|
||||
AND BlogName IN (SELECT BlogName FROM BlogNames);
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
-- STEP 3: prove Blogs now holds exactly what BlogNames held
|
||||
--------------------------------------------------------------------------
|
||||
INSERT INTO MustBeZero
|
||||
SELECT 'BlogNames pair missing from Blogs', COUNT(*)
|
||||
FROM BlogNames bn
|
||||
WHERE NOT EXISTS (SELECT 1 FROM Blogs b WHERE b.BlogId = bn.BlogId AND b.BlogName = bn.BlogName);
|
||||
|
||||
INSERT INTO MustBeZero
|
||||
SELECT 'Blogs IDs vs BlogNames rows', (SELECT COUNT(*) FROM Blogs WHERE BlogId IS NOT NULL) - (SELECT COUNT(*) FROM BlogNames);
|
||||
|
||||
INSERT INTO MustBeZero
|
||||
SELECT 'Notes.RootBlogId unresolved', COUNT(*)
|
||||
FROM (SELECT DISTINCT RootBlogId AS Id FROM Notes) n
|
||||
WHERE NOT EXISTS (SELECT 1 FROM Blogs b WHERE b.BlogId = n.Id);
|
||||
|
||||
INSERT INTO MustBeZero
|
||||
SELECT 'Notes.NoteBlogId unresolved', COUNT(*)
|
||||
FROM (SELECT DISTINCT NoteBlogId AS Id FROM Notes) n
|
||||
WHERE NOT EXISTS (SELECT 1 FROM Blogs b WHERE b.BlogId = n.Id);
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
-- STEP 4: one row per ID
|
||||
--------------------------------------------------------------------------
|
||||
-- UNIQUE still allows the NULLs on the ~168k blogs that have never appeared in a note.
|
||||
DROP INDEX ix_Blogs_BlogId;
|
||||
CREATE UNIQUE INDEX ix_Blogs_BlogId ON Blogs (BlogId);
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
-- STEP 5: BlogNames goes
|
||||
--------------------------------------------------------------------------
|
||||
DROP TABLE BlogNames;
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
-- STEP 6: what BlogNames guaranteed by never being written
|
||||
--------------------------------------------------------------------------
|
||||
-- A deleted row would orphan its notes, and MAX(BlogId) + 1 in RegisterBlog could then
|
||||
-- hand the same ID to a different blog. Remove a blog with IsActive = 0 instead.
|
||||
CREATE TRIGGER trg_Blogs_BlogId_NoDelete
|
||||
BEFORE DELETE ON Blogs
|
||||
WHEN OLD.BlogId IS NOT NULL
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'Blogs row has a BlogId that Notes points at; set IsActive = 0 instead of deleting');
|
||||
END;
|
||||
|
||||
-- A blog renamed upstream is a new blog to Tumblr's API and gets a new row. Editing the name
|
||||
-- in place would re-attribute every note to it; changing the ID would orphan them.
|
||||
CREATE TRIGGER trg_Blogs_BlogId_Immutable
|
||||
BEFORE UPDATE OF BlogId, BlogName ON Blogs
|
||||
WHEN OLD.BlogId IS NOT NULL
|
||||
AND (NEW.BlogId IS NOT OLD.BlogId OR NEW.BlogName IS NOT OLD.BlogName)
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'BlogId and BlogName are fixed once a blog has a BlogId; Notes rows point at it');
|
||||
END;
|
||||
|
||||
DROP TABLE temp.MustBeZero;
|
||||
|
||||
COMMIT;
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
-- VERIFY
|
||||
--------------------------------------------------------------------------
|
||||
-- SELECT COUNT(*) FROM sqlite_master WHERE name = 'BlogNames'; -- expect: 0
|
||||
-- SELECT COUNT(*) FROM Blogs WHERE BlogId IS NOT NULL; -- expect: the old BlogNames row count
|
||||
-- SELECT sql FROM sqlite_master WHERE name = 'ix_Blogs_BlogId'; -- expect: CREATE UNIQUE INDEX
|
||||
-- SELECT name FROM sqlite_master WHERE type = 'trigger'; -- expect: both triggers
|
||||
-- PRAGMA integrity_check; -- expect: ok
|
||||
Reference in New Issue
Block a user