docs: record the domain model and the Postgres/OAuth ADRs
Written while scoping #18. CONTEXT.md pins the ubiquitous language (Series, Reader, Bookmark, Progress, Latest Chapter, Poll) that the schema split and the ordering rule are argued in; the four ADRs record the decisions that follow from it, starting with Postgres over SQLite. Refs #18
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
# Postgres replaces SQLite as the primary datastore
|
||||
|
||||
Status: accepted
|
||||
|
||||
The project is moving from a single-reader tracker to a service published to a community,
|
||||
so we replaced `modernc.org/sqlite` with Postgres (`jackc/pgx/v5`, still pure Go, so
|
||||
`CGO_ENABLED=0` and the distroless image are unaffected). The deciding reason is future
|
||||
supportability — managed hosting, a datastore that survives the app outgrowing one box —
|
||||
**not** concurrency, which was measured and found to be a non-issue.
|
||||
|
||||
## Considered options
|
||||
|
||||
**Stay on SQLite.** Benchmarked against the real store at 1,500 rows (≈50 readers × 30
|
||||
series): ~9,700 upserts/sec single-writer, plateauing at ~780/sec under 8–50 concurrent
|
||||
writers, with `List()` holding 90–103 calls/sec under continuous write load. Projected
|
||||
real load at 50 readers is ~0.02 writes/sec — roughly four and a half orders of magnitude
|
||||
of headroom. `SetMaxOpenConns(1)` serialises writes but was shown not to starve reads;
|
||||
an apparent read collapse traced to row count and per-row scanning, not lock contention.
|
||||
SQLite would have worked. It was rejected for where the project is going, not for what
|
||||
it does today.
|
||||
|
||||
**Postgres.** Chosen. Migrating is cheapest now — 29 rows in one table — and gets
|
||||
materially harder once there are live readers and a multi-tenant schema.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Every statement in `internal/store` is rewritten: `?` → `$N`, `IS NOT` →
|
||||
`IS DISTINCT FROM` (this one is load-bearing; it implements the `updated_at`
|
||||
ordering rule), `pragma_table_info` → `information_schema.columns`,
|
||||
`INTEGER`/`REAL` → `bigint`/`double precision`, `favorite` int-as-bool → `boolean`.
|
||||
- ~75 tests currently get a free isolated database from `t.TempDir()`. They now need a
|
||||
live server, which makes Docker a hard prerequisite for `go test ./...`. This is the
|
||||
permanent cost of the decision and the main reason it was close.
|
||||
- Backups get worse, not better: `VACUUM INTO` produced one self-contained file;
|
||||
restoring now means `pg_dump`/`pg_restore`, a role, and a password.
|
||||
- A second stateful container joins the VPS alongside the existing headless-shell.
|
||||
- **Postgres does not address the real scaling limit.** At batch 14 per 10-minute tick
|
||||
the poller checks at most 84 series/hour; 50 readers × 30 series is 1,500 bookmarks,
|
||||
an 18-hour sweep against a configured 1-hour cooldown. That ceiling is an outbound
|
||||
fetch budget and is fixed by deduplicating polls per Series, not by the datastore.
|
||||
Reference in New Issue
Block a user