docs(spec): D19 Postgres switch design + pg driver dependency
Locked decisions: pg driver behind an async Db interface with dual engines (sqlite for fast tests, pg for prod), bigint paise, TEXT ISO dates, jsonb payloads, numbered migrations, ?->$n in the adapter, DATABASE_URL selects the engine. Build order: async refactor first (zero behavior change), then schema, adapter, dual-engine suite, smoke. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>feat/client-detail-redesign
parent
83f12685c4
commit
08be48817e
@ -0,0 +1,56 @@
|
||||
# HQ Console — Postgres Switch (D15 executed → D19)
|
||||
|
||||
> **STATUS: IN BUILD, started 2026-07-17.** The dedicated slice queued after the D18
|
||||
> go-live cluster and **before** the real APEX cutover — so production data is born in
|
||||
> Postgres and nothing is ever migrated.
|
||||
|
||||
## 1. Goal & why now
|
||||
|
||||
D15 locked Postgres as the production engine; the repo has run on `better-sqlite3`
|
||||
throughout development, with the portable-repo pattern maintained specifically so this
|
||||
switch stays a contained task. Doing it before the one-shot 300-client import means:
|
||||
**empty Postgres → seed → import APEX CSVs straight in. No SQLite→Postgres data
|
||||
migration, ever.**
|
||||
|
||||
## 2. Locked decisions
|
||||
|
||||
| # | Decision | Why |
|
||||
|---|---|---|
|
||||
| D19.1 | Driver: **`pg`** (node-postgres) with a `Pool` | Boring, ubiquitous, first-class TS types |
|
||||
| D19.2 | **Async `Db` interface** (`query`/`get`/`all`/`run`/`transaction`) with TWO implementations: better-sqlite3 (dev/tests, wraps sync in async) and pg | There is no sync Postgres client for Node; the whole repo layer goes `async/await` once, mechanically. SQLite stays for fast `:memory:` tests — the dual-engine suite *proves* the portable-SQL rule |
|
||||
| D19.3 | Paise columns: **`bigint`** in Postgres (`INTEGER` unchanged in SQLite) | int4 overflows at ₹2.15 crore in paise; aggregate SUMs cross that immediately. `pg` returns bigint as string → parse at the adapter edge, still integer paise in the domain |
|
||||
| D19.4 | Dates stay **`TEXT` ISO** (`YYYY-MM-DD` / ISO timestamps) | Already the house convention; portable, index-friendly, zero timezone surprises |
|
||||
| D19.5 | JSON payloads (`document.payload`, `client.contacts`, …): **`jsonb`** in Postgres, TEXT in SQLite; repos keep JSON.stringify/parse at the edge | jsonb gets us future indexing for free without changing repo code |
|
||||
| D19.6 | Migrations: numbered SQL files + a tiny **`schema_migrations`** runner (per engine schema file) | Replaces the PRAGMA-guard `migrate()` + `rebuildTable` gymnastics; Postgres does real `ALTER TABLE` |
|
||||
| D19.7 | Placeholders: repos keep `?`; the pg adapter rewrites `?` → `$n` | One SQL dialect in the codebase; the adapter owns the difference |
|
||||
| D19.8 | Engine differences behind the adapter: `INSERT OR IGNORE` → `ON CONFLICT DO NOTHING`, nested `db.transaction` → savepoints, `sqlite_master` → `information_schema` | Repo code stays engine-blind |
|
||||
| D19.9 | Config: **`DATABASE_URL`** env selects Postgres; absent → SQLite as today | Dev machines keep working with zero setup; prod sets one variable |
|
||||
|
||||
## 3. Build order (each phase lands with the full suite green)
|
||||
|
||||
1. **Async refactor, SQLite still underneath** — introduce the `Db` interface, convert
|
||||
every `repos-*.ts`, `api.ts`, scheduler, server and test to `async/await` with the
|
||||
sqlite implementation behind it. **Zero behavior change**; this is ~70% of the diff
|
||||
and is deliberately not mixed with any engine change.
|
||||
2. **Postgres schema + migration runner** — `schema.pg.sql` (bigint paise, boolean,
|
||||
jsonb, real CHECKs) + `schema_migrations`.
|
||||
3. **pg adapter** — Pool, `?`→`$n`, ON CONFLICT mapping, savepoint-nested transactions,
|
||||
bigint parsing.
|
||||
4. **Dual-engine test run** — full suite on `:memory:` SQLite (default, fast) and on a
|
||||
real local Postgres via `TEST_PG=1` + `DATABASE_URL`. Both green before landing.
|
||||
5. **Boot + smoke on local Postgres** — server on `DATABASE_URL`, real flows exercised
|
||||
(login → quote → issue → payment → PDF → share).
|
||||
6. **Prod later**: RDS (Mumbai) when go-live infra is provisioned; same `DATABASE_URL`
|
||||
mechanism, then cutover per DEPLOY-HQ.
|
||||
|
||||
## 4. Environment (this machine)
|
||||
|
||||
- PostgreSQL 17 installed locally as a Windows service (EDB installer, unattended);
|
||||
superuser `postgres`. App role `hq`; databases `hq` (dev runtime) and `hq_test`
|
||||
(suite). DBeaver connects to `localhost:5432` for inspection.
|
||||
- `DATABASE_URL=postgres://hq:hq_dev@localhost:5432/hq`
|
||||
|
||||
## 5. Non-goals
|
||||
|
||||
RDS provisioning (separate infra step at go-live), pgbouncer/replicas, ORM adoption
|
||||
(the portable-repo pattern is the point), rewriting any business logic.
|
||||
Loading…
Reference in New Issue