Prove the import against a copy of the real library #25

Closed
opened 2026-08-08 06:06:29 +07:00 by sulthan · 1 comment
Owner

Parent

#18

What to build

Retire the biggest risk in this whole migration — losing the owner's reading history — as early as possible, on a copy, while the rest of the work is still in flight.

Build the throwaway generator, run it against a copy of the real backup into a scratch database, and verify the result exactly. Production is not touched by this ticket.

Acceptance criteria

[x] A throwaway generator reads a SQLite snapshot and emits plain SQL: the distinct Series first, then the Bookmarks referencing them, all owned by the seeded Reader.
[x] It is run against a copy of the real backup into a scratch Postgres database. Production is untouched.
[x] Verified: 29 Bookmarks total; 18 reading and 11 archived; Series count equal to the number of distinct site and series-ID pairs; every Bookmark owned by the seeded Reader.
[x] Spot-checked against the source for a sample of rows: read position, favourite flag, and latest-chapter values all match.
[x] Neither the generator nor any of the owner's reading history is committed to the repository.
[x] The procedure is written down well enough to be repeated at cutover against a fresh export.
[x] No SQLite dependency is reintroduced into the backend module — the generator lives outside it.

Blocked by

#22

## Parent #18 ## What to build Retire the biggest risk in this whole migration — losing the owner's reading history — as early as possible, on a copy, while the rest of the work is still in flight. Build the throwaway generator, run it against a copy of the real backup into a scratch database, and verify the result exactly. Production is not touched by this ticket. ## Acceptance criteria [x] A throwaway generator reads a SQLite snapshot and emits plain SQL: the distinct Series first, then the Bookmarks referencing them, all owned by the seeded Reader. [x] It is run against a copy of the real backup into a scratch Postgres database. Production is untouched. [x] Verified: 29 Bookmarks total; 18 reading and 11 archived; Series count equal to the number of distinct site and series-ID pairs; every Bookmark owned by the seeded Reader. [x] Spot-checked against the source for a sample of rows: read position, favourite flag, and latest-chapter values all match. [x] Neither the generator nor any of the owner's reading history is committed to the repository. [x] The procedure is written down well enough to be repeated at cutover against a fresh export. [x] No SQLite dependency is reintroduced into the backend module — the generator lives outside it. ## Blocked by #22
sulthan added the ready-for-agent label 2026-08-08 06:06:29 +07:00
Author
Owner

Done — PR #33 (feat/prove-import-real-library).

Run on 2026-08-08. A throwaway generator (python3 stdlib sqlite3, ~20 lines, never committed) read a copy of bookmarks-20260807-213515.db and emitted 29 INSERT INTO series followed by 29 INSERT INTO bookmarks ... SELECT id FROM owner, wrapped in one transaction. The target was a scratch Postgres container whose schema and owner Reader were built by the real binary rather than hand-written DDL, so the import was proven against exactly the schema the migration runner produces. Production untouched.

Verified

  • 29 Bookmarks; 18 reading, 11 archived, 0 anything else.
  • 29 Series, equal to the distinct (site, series_id) count in the source.
  • 1 Reader; 0 Bookmarks owned by anyone else, checked by resolving the Reader through OWNER_DISCORD_ID.
  • Field-by-field diff of all 29 rows across all 15 migrated columns: 0 differences. The ticket asked for a sample; at 29 rows the full comparison was cheaper than choosing one.
  • GET /bookmarks over the real read path returned the same 29 with matching values — catches a correct import sitting behind a broken join, which the table counts would not.
  • TRUNCATE bookmarks, series; then re-apply: clean, 29 again. That is the abort path in the runbook, so it is exercised rather than asserted.

Committed: CUTOVER.md and two cross-links in REDEPLOY.md. Nothing else. The generator stays out of the repo — its output is the owner's reading history, and it reads SQLite, which ADR-0001 removed from the backend module. backend/go.mod is unchanged and has no SQLite dependency.

Found while reviewing, now in the runbook. Six SQLite columns (title, series_url, cover, last_chapter, last_chapter_url, last_chapter_num) are nullable in the old schema but NOT NULL in Postgres, so one NULL aborts the whole import — the exact opposite of latest_chapter_num, where NULL is meaningful and must survive. The 2026-08-07 export happens to have none, so this would only have surfaced at cutover against a fresh export. Also fixed: the temp table must be created inside the transaction or psql's autocommit drops it immediately, and the ownership check now resolves the Reader by Discord id instead of ORDER BY id LIMIT 1, which was true by construction and could not have failed.

Unblocks the cutover step of #18.

Done — PR #33 (`feat/prove-import-real-library`). **Run on 2026-08-08.** A throwaway generator (python3 stdlib `sqlite3`, ~20 lines, never committed) read a copy of `bookmarks-20260807-213515.db` and emitted 29 `INSERT INTO series` followed by 29 `INSERT INTO bookmarks ... SELECT id FROM owner`, wrapped in one transaction. The target was a scratch Postgres container whose schema and owner Reader were built by the real binary rather than hand-written DDL, so the import was proven against exactly the schema the migration runner produces. Production untouched. **Verified** - 29 Bookmarks; 18 `reading`, 11 `archived`, 0 anything else. - 29 Series, equal to the distinct `(site, series_id)` count in the source. - 1 Reader; 0 Bookmarks owned by anyone else, checked by resolving the Reader through `OWNER_DISCORD_ID`. - Field-by-field diff of all 29 rows across all 15 migrated columns: **0 differences**. The ticket asked for a sample; at 29 rows the full comparison was cheaper than choosing one. - `GET /bookmarks` over the real read path returned the same 29 with matching values — catches a correct import sitting behind a broken join, which the table counts would not. - `TRUNCATE bookmarks, series;` then re-apply: clean, 29 again. That is the abort path in the runbook, so it is exercised rather than asserted. **Committed**: `CUTOVER.md` and two cross-links in `REDEPLOY.md`. Nothing else. The generator stays out of the repo — its output is the owner's reading history, and it reads SQLite, which ADR-0001 removed from the backend module. `backend/go.mod` is unchanged and has no SQLite dependency. **Found while reviewing, now in the runbook.** Six SQLite columns (`title`, `series_url`, `cover`, `last_chapter`, `last_chapter_url`, `last_chapter_num`) are nullable in the old schema but `NOT NULL` in Postgres, so one `NULL` aborts the whole import — the exact opposite of `latest_chapter_num`, where `NULL` is meaningful and must survive. The 2026-08-07 export happens to have none, so this would only have surfaced at cutover against a fresh export. Also fixed: the temp table must be created inside the transaction or psql's autocommit drops it immediately, and the ownership check now resolves the Reader by Discord id instead of `ORDER BY id LIMIT 1`, which was true by construction and could not have failed. Unblocks the cutover step of #18.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sulthan/mangaBookmark#25