-- shrink-db.sql
-- Reduces TL.db from ~267 MB to ~207 MB (-22%) with no application changes,
-- and no visible change in any of the three apps that touch this file.
--
-- The three consumers, and what each one uses:
--   URLNotesGrabberCORE  System.Data.SQLite 1.0.119  writes Notes, Posts, Blogs
--   Rolodex (web)        Microsoft.Data.Sqlite 10.0  reads all three; soft-deletes via IsActive
--   TumblThree           System.Data.SQLite.Core 1.0.119
--                        one statement only, ManagerController.cs:972 --
--                        "UPDATE Blogs SET IsActive = 0, DateModified = @DateModified
--                         WHERE BlogName = @BlogName"
--                        Nothing below touches the Blogs table, so TumblThree is
--                        unaffected. (Its GlobalDatabaseService talks to TumblThree's
--                        own separate FileEntries/BlogFiles database, not this file.)
--
-- WITHOUT ROWID needs SQLite >= 3.8.2 (Dec 2013). All three providers above are
-- 2024-25 builds, an order of magnitude newer, so STEP 2 is readable by all of them.
--
-- Every figure below was measured on a copy of the live 267 MB file, and the
-- result was checked against all three apps' access patterns:
--   PRAGMA integrity_check ....... ok
--   row counts ................... Notes 1182333, Posts 22468, Blogs 188620 (unchanged)
--   Rolodex soft-delete UPDATE ... works
--   Rolodex NoteBlogName filter .. still uses ix_NoteBlogName01
--   crawler INSERT OR IGNORE ..... still dedupes (0 dupes admitted)
--   TumblThree's UPDATE Blogs ..... untouched -- Blogs is not modified by this script
--
-- HOW TO RUN (DB Browser for SQLite):
--   1. Stop ALL THREE apps: the crawler, the Rolodex web app, and TumblThree.
--      Rolodex holds the file open and checkpoints the WAL, so it must be down,
--      not just idle. TumblThree only opens the file for an instant when you
--      delete a blog, but it can also launch the crawler on its own
--      (UrlNotesGrabberService) -- so close it rather than merely avoiding it.
--   2. Back up TL.db (copy the 267 MB file somewhere safe).
--   3. Open TL.db, go to Execute SQL, paste STEP 1-3, run.
--   4. Click "Write Changes".
--   5. Run Tools > Compact Database. This is VACUUM; it will not run from the
--      Execute SQL tab because DB Browser keeps a transaction open there.
--      NOTHING SHRINKS ON DISK UNTIL THIS FINISHES.
--
-- Expected: steps 1-3 a couple of minutes, Compact a couple more.
-- Free disk needed during Compact: ~270 MB for the temp copy.

--------------------------------------------------------------------------
-- STEP 1: drop the TimeStamp index          (required by STEP 2, not optional)
--------------------------------------------------------------------------
-- Measured cost/benefit:
--
--   * Rolodex's DEFAULT Notes view does not use it. Its sort carries the
--     tiebreaker "RootBlogName, PostID, NoteBlogName", which forces a full sort
--     regardless -- the query plan is byte-identical with and without the index.
--     Sorting.cs:138 already assumes as much, and is right in practice.
--   * The crawler's collect query (DataAccess.cs:1183) filters
--     TimeStamp >= 1535778000, which excludes 786 of 1,182,333 rows (0.07%).
--     A full index scan wearing a disguise. Same measured time without it.
--   * The reply-matching UPDATE uses ABS(TimeStamp - ?) <= 5, which can never
--     use an index on TimeStamp.
--   * It DOES help exactly one path: Rolodex's Notes page with a date-range
--     filter applied. 60 ms -> 164 ms. That is the whole of what is lost.
--
-- And it must go, because after STEP 2 it stops being cheap. A secondary index
-- on a WITHOUT ROWID table carries the full 5-column primary key instead of a
-- compact rowid, so this index grows 14 MB -> 58 MB. Keeping it lands the file
-- at 265 MB instead of 207 MB -- i.e. it cancels the entire exercise to save
-- 100 ms on one filtered view.

DROP INDEX IF EXISTS Notes_idx_06e01ae3;

--------------------------------------------------------------------------
-- STEP 2: rebuild Notes as WITHOUT ROWID                       (-32 MB)
--------------------------------------------------------------------------
-- Notes has a 5-column composite primary key. In a rowid table SQLite stores
-- that key twice: once in the table, once in sqlite_autoindex_Notes_1 (62 MB).
-- WITHOUT ROWID stores the rows *in* the key's b-tree, so the copy disappears.
--
-- ix_NoteBlogName01 grows 25 -> 58 MB for the reason described above. Net -32 MB.
-- It is kept because Rolodex filters on NoteBlogName and the crawler joins on it.
--
-- Safe: neither codebase references rowid on Notes (grep across both trees,
-- zero matches). IsActive keeps its exact current declaration, which is what
-- Rolodex's ActiveFlag predicate reads.

PRAGMA foreign_keys = off;

CREATE TABLE Notes_new (
    "RootBlogName"      TEXT,
    "PostID"            INTEGER,
    "NoteBlogName"      TEXT,
    "TimeStamp"         INTEGER,
    "Type"              TEXT,
    "replyText"         TEXT DEFAULT '.',
    "DatetimeCrawled"   TEXT DEFAULT '2/12/26 12am',
    "DateModified"      TEXT,
    "DateCreated"       TEXT,
    IsActive            INTEGER NOT NULL DEFAULT 1,
    PRIMARY KEY("RootBlogName","PostID","TimeStamp","Type","NoteBlogName")
) WITHOUT ROWID;

INSERT INTO Notes_new
SELECT RootBlogName, PostID, NoteBlogName, TimeStamp, Type,
       replyText, DatetimeCrawled, DateModified, DateCreated, IsActive
  FROM Notes;

DROP TABLE Notes;
ALTER TABLE Notes_new RENAME TO Notes;

CREATE INDEX "ix_NoteBlogName01" ON "Notes" ("NoteBlogName");

--------------------------------------------------------------------------
-- STEP 3: clear the DatetimeCrawled placeholder                (-13 MB)
--------------------------------------------------------------------------
-- 1,148,077 of 1,182,333 rows hold the literal DDL default '2/12/26 12am' --
-- a backfill placeholder, not a crawl time. SQLite stores all 12 bytes of it
-- on every one of those rows.
--
-- This is UI-NEUTRAL in Rolodex, which is why it is safe despite Rolodex
-- displaying the column. Rolodex reads and sorts it through DateSql.Sortable
-- (DateRange.cs:67), whose CASE matches '____-__-__%' or the 8-character
-- '__/__/__'. The 12-character '2/12/26 12am' matches neither, so Sortable
-- already returns NULL for these rows and the page already renders an em dash
-- and sorts them to the bottom. RolodexRepository.cs:957-963 documents exactly
-- this. Writing a real NULL changes the bytes on disk, not the screen.
--
-- The crawler never reads the column back -- it only writes it on INSERT
-- (DataAccess.cs:713, 721).

UPDATE Notes SET DatetimeCrawled = NULL WHERE DatetimeCrawled = '2/12/26 12am';

--------------------------------------------------------------------------
-- DELIBERATELY NOT DONE: nulling Notes.DateCreated
--------------------------------------------------------------------------
-- An earlier draft of this script also cleared DateCreated = '2026-04-13'
-- (a further -12 MB). Do not. Unlike DatetimeCrawled, that value DOES match
-- Sortable's '____-__-__%' branch, so Rolodex renders it as a real date in the
-- "Created" column on the Notes page and Post detail, and sorts by it. Nulling
-- it would turn visible dates into em dashes and move rows in the sort order.

--------------------------------------------------------------------------
-- STEP 4: Write Changes, then Tools > Compact Database
--------------------------------------------------------------------------
-- Nothing above reclaims disk until VACUUM runs. From the sqlite3 CLI instead:
--     sqlite3 TL.db "VACUUM;"

--------------------------------------------------------------------------
-- VERIFY (run after compacting; file should be ~207 MB)
--------------------------------------------------------------------------
-- PRAGMA integrity_check;
--
-- SELECT 'Notes' t, COUNT(*) n FROM Notes
-- UNION ALL SELECT 'Posts', COUNT(*) FROM Posts
-- UNION ALL SELECT 'Blogs', COUNT(*) FROM Blogs;
--   -- expect 1182333 / 22468 / 188620, unchanged
--
-- SELECT name, SUM(pgsize)/1024/1024 AS mb
--   FROM dbstat GROUP BY name ORDER BY SUM(pgsize) DESC;
--   -- expect Notes 78, ix_NoteBlogName01 58, Posts 50, Blogs 14
--
-- APPLIED 2026-08-07. Actual result: 267.32 MB -> 207.17 MB, integrity_check ok,
-- row counts unchanged, journal_mode still wal. VACUUM took 6 seconds.
