Run on Postgres with today's schema #20
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent
#18
What to build
The backend runs on Postgres instead of SQLite, and nothing else changes. Every endpoint behaves identically, the wire format is untouched, and the whole existing test suite still passes. This is the foundation every later ticket sits on, and its value is precisely that it is boring: if anything observable changes, this ticket is wrong.
Chosen for future supportability, not throughput — SQLite was benchmarked and had ample headroom. See ADR-0001; do not reopen on performance grounds.
Acceptance criteria
favoritebecomes a real boolean; chapter numbers are double precision; timestamps stay unix-millisecond integers.updated_atordering rule usesIS DISTINCT FROM. A Progress change advancesupdated_at; a Latest Chapter change or a favourite toggle does not.Blocked by
None — can start immediately.
Implemented on
feat/db-change— not merged, so this stays open;249aacacarriesCloses #20and will fire when it lands onmain.249aaca7a0c190How each criterion was met
DATABASE_URL, required,log.Fatalwhen empty (main.go). Kept out of the startup log line since it carries a password.//go:embed migrations/*.sql,schema_migrationsversion table, one transaction per file viaapplyMigration, applied on every start.TestOpenIsIdempotentcovers the re-run.favorite boolean,last_chapter_num/latest_chapter_num double precision,updated_at/latest_checked_at bigint. Confirmed live with\d bookmarks.IS DISTINCT FROM— in both theupdated_atCASE and the finished-series filter. Verified against a live server: a favourite toggle leftupdated_atat…5958, a progress write moved it to…6013.?is now$N;bookmarkColumnsremains the only compile-time constant concatenated. The::textcasts on$14/$15are load-bearing: insideCOALESCE/NULLIFthere is no target column to infer the parameter type from and Postgres refuses to guess.migrateColumns,addedColumns,existingColumnsandmigrateAsuraKeysare gone, with the six tests that seeded legacy schemas. The Asura rewrite was confirmed to have no remaining work as the ticket predicted. Its regexp is not migration machinery, so it was rehomed as unexportedlatest.asuraBuildHash— the poller still needs it to scope chapter links for a series whose slug carries a rotating build hash.internal/pgtest: onepostgres:17-alpineper test binary (TestMain→pgtest.Main),CREATE DATABASE test_Nper test. Hand-rolled rather than testcontainers: onedocker run, onedocker portand a ping loop, against a module list that is otherwise stdlib.grep -c sqlite go.mod go.sum→ 0. The wholemodernc.org/*tree went with it.postgresservice with apg_isreadyhealthcheck on aninternal: truedbnetwork,postgres-datavolume, no published port.DB_PATHretired everywhere. The pre-migrationbookmarks-datavolume is deliberately left undeclared sodocker compose down -vcannot take it — that is user story 6 of #18 kept safe ahead of time.go test ./...requires Docker and explains the container-per-package model.go test -count=1 ./...all green in 7.3s.gofmt/go vetclean (poller.goandweb.gowere already unformatted before this change and are untouched).Beyond the checklist
Smoke-tested against a real Postgres, not just the suite: healthz, 401 on unauthenticated,
GET/PUT/DELETE, a 204 preflight with CORS headers, and the web UI. Restarting the binary on a populated database re-runs the migration silently.REDEPLOY.mdandDEPLOY.mdwere rewritten off SQLite —pg_dump -Fc/pg_restorereplaceVACUUM INTO, and the-wal/-shmandchown 65532advice is gone. A redeploy runbook that quietly backs up nothing would have been the worst outcome of this change.Two-axis review clean: 0 standards violations, 11/11 spec criteria, no scope creep. The one judgement call —
newTestStorerepeated in three test packages — was left alone:store_test.gois in packagestore, so a sharedpgtest.Store(t)helper would be an import cycle.Nothing here touches the reader/series/session split, Discord OAuth, per-reader tokens or the 29-row data migration. Those stay in their own tickets under #18, as scoped.