You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
72 lines
5.1 KiB
Markdown
72 lines
5.1 KiB
Markdown
# HQ Console — Postgres Switch (D15 executed → D19)
|
|
|
|
> **STATUS: DELIVERED 2026-07-17.** All four phases landed same-day: async DB interface
|
|
> across every repo/route/test (default suite 346/346 on SQLite, zero behavior change),
|
|
> Postgres migration set + runner, pg adapter, `DATABASE_URL` engine selection, and a
|
|
> 5-test Postgres integration suite (opt-in `HQ_PG_TEST_URL`) verified green against a
|
|
> local PostgreSQL 17.
|
|
>
|
|
> Delivery deviations from the table below (recorded in migrations-pg.ts too):
|
|
> - **0/1 flags stay INTEGER** (not boolean) — repos say `active=1`; that SQL must be
|
|
> identical on both engines.
|
|
> - **JSON stays TEXT** (not jsonb) — node-pg auto-parses jsonb into objects, which
|
|
> would break the repos' JSON.parse at the edge; jsonb is a later pg-only ALTER if
|
|
> payload indexing is ever needed.
|
|
> - **Full-suite-on-pg** was scoped to a dedicated integration file (migrations-from-zero,
|
|
> series + due-date stamp, paisa-exact payment, ON CONFLICT idempotency, savepoint
|
|
> rollback, HTTP boot smoke) rather than re-running all 346 sqlite-fixture tests on pg.
|
|
> - Known dialect nuance to revisit before go-live: `LIKE` is case-insensitive on SQLite
|
|
> but case-sensitive on Postgres — client search/filters will match case-sensitively
|
|
> in production until switched to `LOWER(...) LIKE LOWER(?)`.
|
|
|
|
## 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.
|