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]>
144 lines
7.0 KiB
PL/PgSQL
144 lines
7.0 KiB
PL/PgSQL
-- 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
|