08749df050
Swap modernc.org/sqlite for jackc/pgx/v5 with no observable change: same endpoints, same wire format, same updated_at ordering rule. The schema now comes from numbered SQL embedded in the binary and applied on startup, one transaction each, recorded in schema_migrations. That replaces two pieces of SQLite-era machinery, both deleted rather than ported: the column probing (Postgres has ADD COLUMN IF NOT EXISTS, and there is no legacy database left to probe) and the Asura key rewrite, which has run clean on every start for months now that the userscripts strip build hashes before writing. Its regexp survives as latest.asuraBuildHash, where the poller still needs it to scope chapter links to a series whose slug carries a rotating hash. Types get real: favorite is a boolean, chapter numbers double precision, timestamps stay unix-ms bigint. SQLite's null-safe IS NOT becomes IS DISTINCT FROM, which is what implements the rule that only reading progress reorders a list. Inside COALESCE/NULLIF the status and kind parameters need an explicit ::text -- there is no target column to infer from and Postgres refuses to guess. Tests lose their free t.TempDir() database, so Docker is now a hard prerequisite for `go test ./...`: internal/pgtest starts one postgres:17-alpine per test binary and hands each test a database of its own. Also lands CONTEXT.md and the four ADRs written while scoping #18. BREAKING CHANGE: DB_PATH is retired for DATABASE_URL, which is required and has no default. Compose gains a postgres service on an internal network with its own volume; POSTGRES_PASSWORD joins .env. The old bookmarks-data volume is deliberately left undeclared so `docker compose down -v` cannot take the pre-migration database with it. main is not deployable until #25 and #26 land. Closes #20 Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com> Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
33 lines
1.7 KiB
Markdown
33 lines
1.7 KiB
Markdown
# The wire format stays flat and deliberately does not mirror the schema
|
|
|
|
Status: accepted
|
|
|
|
Storage splits a tracked series across two tables (ADR-0003), but `GET /bookmarks` and
|
|
`PUT /bookmarks/{key}` keep emitting and accepting one **flat** JSON object with `title`,
|
|
`cover`, `last_chapter` and `latest_chapter` as siblings — exactly the shape they had
|
|
when there was one table. The server joins on the way out and decomposes on the way in.
|
|
|
|
## Why a future reader will find this surprising
|
|
|
|
The obvious move after splitting a table is to nest the JSON to match. Don't "fix" this.
|
|
|
|
**A nested payload would have broken every installed userscript instantly.** Scripts read
|
|
`b.title` directly; moving it to `b.series.title` yields `undefined` — no error, just
|
|
blank rows and a New Chapter signal that silently reports nothing forever. Because
|
|
Violentmonkey updates roughly once a day per device, the migration relies on old scripts
|
|
continuing to work during a 14-day grace window. A nested format and that grace window
|
|
are mutually exclusive.
|
|
|
|
**It is also the better contract independently of compatibility.** A client rendering one
|
|
row needs the title and the reading position together; nesting exports the re-stitching
|
|
to every browser to mirror a decision about disk layout it should not know about. Keeping
|
|
them separate lets storage change again later without a client release — which is the
|
|
whole reason this ADR is worth the paragraph.
|
|
|
|
## Consequence
|
|
|
|
The flat shape is a contract, not an implementation detail. Changing the storage schema
|
|
must not change it. It follows the rule already in force for `updated_at`: the server
|
|
owns the truth and returns the row **as stored**, and clients adopt the response rather
|
|
than their own payload.
|