-- ============================================================================ -- verify-db-schema.sql -- -- Purpose: Verify that a TL.db (e.g. a restored backup) has every column the -- current URLNotesGrabberCORE code expects. The app has NO startup -- migration: missing columns only get added when specific modes run, -- and a referenced-but-missing column causes a "no such column" crash. -- -- How to use (DB Browser for SQLite): -- 1. File > Open Database -> pick the restored backup. -- 2. Execute SQL tab. Run SECTION 1 (it is read-only). -- * Zero rows from every query = schema is fully aligned, you're done. -- * Rows in "MISSING COLUMNS" = copy the run_this_to_fix text. -- 3. If columns are missing: KEEP A COPY OF THE BACKUP FIRST, then go to -- SECTION 2, uncomment ONLY the ALTER lines that match the report, and run. -- 4. Re-run SECTION 1 to confirm zero rows. -- -- This script never UPDATEs/DELETEs/DROPs. In particular it deliberately does -- NOT replicate the likes-reset that the app's -likes migration performs -- (DataAccess.cs:375), so existing likes high-water marks are preserved. -- ============================================================================ -- ============================================================================ -- SECTION 1 -- VERIFICATION (read-only) -- ============================================================================ -- Expected schema for the current code version. -- alter_stmt is a runnable ALTER for additively-fixable columns; for base -- columns it is a 'MANUAL REVIEW' note (a missing base column means the backup -- predates the table's creation or is damaged -- do not blindly auto-add). WITH expected(tbl, col, alter_stmt) AS ( VALUES -- Posts (base columns: manual review if missing) ('Posts','BlogName', 'MANUAL REVIEW - base/PK column missing'), ('Posts','PostID', 'MANUAL REVIEW - base/PK column missing'), ('Posts','HasNotesGathered', 'MANUAL REVIEW - base column missing'), ('Posts','reblogURL', 'MANUAL REVIEW - base column missing'), ('Posts','NotFound', 'MANUAL REVIEW - base column missing'), ('Posts','PostDate', 'MANUAL REVIEW - base column missing'), ('Posts','NotesGatheredDateTime', 'MANUAL REVIEW - base column missing'), ('Posts','HasImage', 'MANUAL REVIEW - base column missing'), ('Posts','PostURL', 'MANUAL REVIEW - base column missing'), ('Posts','Slug', 'MANUAL REVIEW - base column missing'), ('Posts','ReblogKey', 'MANUAL REVIEW - base column missing'), ('Posts','ReblogName', 'MANUAL REVIEW - base column missing'), ('Posts','Summary', 'MANUAL REVIEW - base column missing'), ('Posts','Quote', 'MANUAL REVIEW - base column missing'), ('Posts','Body', 'MANUAL REVIEW - base column missing'), ('Posts','Tags', 'MANUAL REVIEW - base column missing'), ('Posts','Link', 'MANUAL REVIEW - base column missing'), ('Posts','PhotoURL', 'MANUAL REVIEW - base column missing'), ('Posts','PhotoCaption', 'MANUAL REVIEW - base column missing'), ('Posts','DownloadedFiles', 'MANUAL REVIEW - base column missing'), ('Posts','AudioCaption', 'MANUAL REVIEW - base column missing'), ('Posts','Question', 'MANUAL REVIEW - base column missing'), ('Posts','Answer', 'MANUAL REVIEW - base column missing'), ('Posts','Title', 'MANUAL REVIEW - base column missing'), ('Posts','ByLikes', 'MANUAL REVIEW - base column missing'), ('Posts','RootBlogName', 'MANUAL REVIEW - base column missing'), ('Posts','RootURL', 'MANUAL REVIEW - base column missing'), ('Posts','DateModified', 'MANUAL REVIEW - base column missing'), ('Posts','DateCreated', 'MANUAL REVIEW - base column missing'), -- Posts (additive migration column, auto-fixable) ('Posts','PostType', 'ALTER TABLE Posts ADD COLUMN PostType TEXT;'), -- Blogs (base columns: manual review if missing) ('Blogs','BlogName', 'MANUAL REVIEW - base/PK column missing'), ('Blogs','HasBeenOutput', 'MANUAL REVIEW - base column missing'), ('Blogs','IsActive', 'MANUAL REVIEW - base column missing'), ('Blogs','DateAdded', 'MANUAL REVIEW - base column missing'), ('Blogs','ByLikes', 'MANUAL REVIEW - base column missing'), ('Blogs','DateModified', 'MANUAL REVIEW - base column missing'), ('Blogs','DateCreated', 'MANUAL REVIEW - base column missing'), -- Blogs (additive migration columns, auto-fixable) ('Blogs','LikesPulled', 'ALTER TABLE Blogs ADD COLUMN LikesPulled INTEGER DEFAULT 0;'), ('Blogs','LikesCursor', 'ALTER TABLE Blogs ADD COLUMN LikesCursor INTEGER DEFAULT 0;'), ('Blogs','LikesNewestTimestamp', 'ALTER TABLE Blogs ADD COLUMN LikesNewestTimestamp INTEGER DEFAULT 0;'), ('Blogs','LikesLastRefreshed', 'ALTER TABLE Blogs ADD COLUMN LikesLastRefreshed INTEGER DEFAULT 0;'), ('Blogs','LikesLastNewCount', 'ALTER TABLE Blogs ADD COLUMN LikesLastNewCount INTEGER DEFAULT 0;'), ('Blogs','TTFolderPath', 'ALTER TABLE Blogs ADD COLUMN TTFolderPath TEXT;'), -- Notes (base columns: manual review if missing) ('Notes','RootBlogName', 'MANUAL REVIEW - base/PK column missing'), ('Notes','PostID', 'MANUAL REVIEW - base/PK column missing'), ('Notes','NoteBlogName', 'MANUAL REVIEW - base/PK column missing'), ('Notes','TimeStamp', 'MANUAL REVIEW - base/PK column missing'), ('Notes','Type', 'MANUAL REVIEW - base/PK column missing'), ('Notes','DatetimeCrawled', 'MANUAL REVIEW - base column missing'), ('Notes','DateModified', 'MANUAL REVIEW - base column missing'), ('Notes','DateCreated', 'MANUAL REVIEW - base column missing'), -- Notes (additive migration column, auto-fixable) ('Notes','replyText', 'ALTER TABLE Notes ADD COLUMN replyText TEXT DEFAULT ''.'';'), -- DailyAPICount (base columns) ('DailyAPICount','Date', 'MANUAL REVIEW - base/PK column missing'), ('DailyAPICount','APICount', 'MANUAL REVIEW - base column missing'), -- ApiKeyPoolState (created at runtime by EnsureApiKeyPoolTables; auto-fixable by re-running app, but safe to add) ('ApiKeyPoolState','KeyName', 'MANUAL REVIEW - run app once to auto-create ApiKeyPool tables'), ('ApiKeyPoolState','RetryUntil', 'MANUAL REVIEW - run app once to auto-create ApiKeyPool tables'), ('ApiKeyPoolMeta','Id', 'MANUAL REVIEW - run app once to auto-create ApiKeyPool tables'), ('ApiKeyPoolMeta','LastIndex', 'MANUAL REVIEW - run app once to auto-create ApiKeyPool tables') ), actual(tbl, col) AS ( SELECT 'Posts', name FROM pragma_table_info('Posts') UNION ALL SELECT 'Blogs', name FROM pragma_table_info('Blogs') UNION ALL SELECT 'Notes', name FROM pragma_table_info('Notes') UNION ALL SELECT 'DailyAPICount', name FROM pragma_table_info('DailyAPICount') UNION ALL SELECT 'ApiKeyPoolState', name FROM pragma_table_info('ApiKeyPoolState') UNION ALL SELECT 'ApiKeyPoolMeta', name FROM pragma_table_info('ApiKeyPoolMeta') ) -- 1a. MISSING COLUMNS: columns the code needs that the DB does not have. -- Zero rows = good. Otherwise copy run_this_to_fix into SECTION 2. SELECT e.tbl AS table_name, e.col AS missing_column, e.alter_stmt AS run_this_to_fix FROM expected e LEFT JOIN actual a ON a.tbl = e.tbl AND lower(a.col) = lower(e.col) WHERE a.col IS NULL ORDER BY (e.alter_stmt LIKE 'ALTER%') DESC, e.tbl, e.col; -- 1b. MISSING TABLES: expected tables that don't exist at all in this DB. -- Zero rows = good. WITH expected_tables(tbl) AS ( VALUES ('Posts'),('Blogs'),('Notes'),('DailyAPICount'), ('ApiKeyPoolState'),('ApiKeyPoolMeta') ) SELECT et.tbl AS missing_table FROM expected_tables et WHERE NOT EXISTS ( SELECT 1 FROM sqlite_master WHERE type = 'table' AND lower(name) = lower(et.tbl) ) ORDER BY et.tbl; -- 1c. EXTRA / UNEXPECTED COLUMNS: present in the DB but not in the expected -- list above. Informational only -- e.g. a NEWER backup, or a column this -- script's expected-list hasn't been updated for. Not an error by itself. WITH expected(tbl, col) AS ( VALUES ('Posts','BlogName'),('Posts','PostID'),('Posts','HasNotesGathered'),('Posts','reblogURL'), ('Posts','NotFound'),('Posts','PostDate'),('Posts','NotesGatheredDateTime'),('Posts','HasImage'), ('Posts','PostURL'),('Posts','Slug'),('Posts','ReblogKey'),('Posts','ReblogName'),('Posts','Summary'), ('Posts','Quote'),('Posts','Body'),('Posts','Tags'),('Posts','Link'),('Posts','PhotoURL'), ('Posts','PhotoCaption'),('Posts','DownloadedFiles'),('Posts','AudioCaption'),('Posts','Question'), ('Posts','Answer'),('Posts','Title'),('Posts','ByLikes'),('Posts','RootBlogName'),('Posts','RootURL'), ('Posts','DateModified'),('Posts','DateCreated'),('Posts','PostType'), ('Blogs','BlogName'),('Blogs','HasBeenOutput'),('Blogs','IsActive'),('Blogs','DateAdded'), ('Blogs','ByLikes'),('Blogs','DateModified'),('Blogs','DateCreated'),('Blogs','LikesPulled'), ('Blogs','LikesCursor'),('Blogs','LikesNewestTimestamp'),('Blogs','LikesLastRefreshed'), ('Blogs','LikesLastNewCount'),('Blogs','TTFolderPath'), ('Notes','RootBlogName'),('Notes','PostID'),('Notes','NoteBlogName'),('Notes','TimeStamp'), ('Notes','Type'),('Notes','DatetimeCrawled'),('Notes','DateModified'),('Notes','DateCreated'), ('Notes','replyText'), ('DailyAPICount','Date'),('DailyAPICount','APICount'), ('ApiKeyPoolState','KeyName'),('ApiKeyPoolState','RetryUntil'), ('ApiKeyPoolMeta','Id'),('ApiKeyPoolMeta','LastIndex') ), actual(tbl, col) AS ( SELECT 'Posts', name FROM pragma_table_info('Posts') UNION ALL SELECT 'Blogs', name FROM pragma_table_info('Blogs') UNION ALL SELECT 'Notes', name FROM pragma_table_info('Notes') UNION ALL SELECT 'DailyAPICount', name FROM pragma_table_info('DailyAPICount') UNION ALL SELECT 'ApiKeyPoolState', name FROM pragma_table_info('ApiKeyPoolState') UNION ALL SELECT 'ApiKeyPoolMeta', name FROM pragma_table_info('ApiKeyPoolMeta') ) SELECT a.tbl AS table_name, a.col AS unexpected_column FROM actual a LEFT JOIN expected e ON e.tbl = a.tbl AND lower(e.col) = lower(a.col) WHERE e.col IS NULL ORDER BY a.tbl, a.col; -- ============================================================================ -- SECTION 2 -- FIX (opt-in, additive only) -- -- Run ONLY the lines that query 1a flagged with an ALTER statement. -- KEEP A COPY OF THE BACKUP FIRST. SQLite has no "ADD COLUMN IF NOT EXISTS", -- so running an ALTER for a column that already exists throws a harmless -- "duplicate column name" error and changes nothing -- just run the flagged -- subset. These are the 8 additive migration columns and nothing else; the -- likes high-water-mark reset is intentionally NOT included. -- ============================================================================ -- ALTER TABLE Posts ADD COLUMN PostType TEXT; -- ALTER TABLE Blogs ADD COLUMN LikesPulled INTEGER DEFAULT 0; -- ALTER TABLE Blogs ADD COLUMN LikesCursor INTEGER DEFAULT 0; -- ALTER TABLE Blogs ADD COLUMN LikesNewestTimestamp INTEGER DEFAULT 0; -- ALTER TABLE Blogs ADD COLUMN LikesLastRefreshed INTEGER DEFAULT 0; -- ALTER TABLE Blogs ADD COLUMN LikesLastNewCount INTEGER DEFAULT 0; -- ALTER TABLE Blogs ADD COLUMN TTFolderPath TEXT; -- ALTER TABLE Notes ADD COLUMN replyText TEXT DEFAULT '.';