@crvouga/postgres-mem 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/AGENTS.md +159 -0
  2. package/COMPATIBILITY-AUDIT.md +155 -0
  3. package/COMPATIBILITY.md +87 -0
  4. package/LICENSE +21 -0
  5. package/README.md +325 -0
  6. package/compat/coverage.json +1081 -0
  7. package/compat/divergences.json +191 -0
  8. package/compat/gate-report.json +80 -0
  9. package/compat/requirements.json +1105 -0
  10. package/compat/requirements.raw.html +484 -0
  11. package/compat/scenario-types.ts +98 -0
  12. package/compat/scenarios.ts +98 -0
  13. package/compat/sections/agg.ts +45 -0
  14. package/compat/sections/api.ts +28 -0
  15. package/compat/sections/arr.ts +45 -0
  16. package/compat/sections/cat.ts +47 -0
  17. package/compat/sections/con.ts +32 -0
  18. package/compat/sections/cpy.ts +24 -0
  19. package/compat/sections/cte.ts +29 -0
  20. package/compat/sections/dat.ts +60 -0
  21. package/compat/sections/ddl.ts +55 -0
  22. package/compat/sections/det.ts +20 -0
  23. package/compat/sections/dml.ts +37 -0
  24. package/compat/sections/eco.ts +16 -0
  25. package/compat/sections/err.ts +16 -0
  26. package/compat/sections/exp.ts +53 -0
  27. package/compat/sections/fun.ts +58 -0
  28. package/compat/sections/fzz.ts +36 -0
  29. package/compat/sections/guc.ts +27 -0
  30. package/compat/sections/joi.ts +33 -0
  31. package/compat/sections/jsn.ts +46 -0
  32. package/compat/sections/lim.ts +13 -0
  33. package/compat/sections/par.ts +61 -0
  34. package/compat/sections/pre.ts +26 -0
  35. package/compat/sections/sch.ts +31 -0
  36. package/compat/sections/sel.ts +43 -0
  37. package/compat/sections/seq.ts +31 -0
  38. package/compat/sections/snp.ts +21 -0
  39. package/compat/sections/tok.ts +53 -0
  40. package/compat/sections/trg.ts +57 -0
  41. package/compat/sections/tsr.ts +37 -0
  42. package/compat/sections/txn.ts +39 -0
  43. package/compat/sections/typ.ts +55 -0
  44. package/compat/sections/uni.ts +13 -0
  45. package/compat/sections/win.ts +43 -0
  46. package/compat/smoke-baseline.json +3 -0
  47. package/compat/unsupported-register.json +2526 -0
  48. package/dist/api/bind.d.ts +10 -0
  49. package/dist/api/database.d.ts +69 -0
  50. package/dist/api/statement.d.ts +51 -0
  51. package/dist/ast/nodes.d.ts +810 -0
  52. package/dist/constraints/enforce.d.ts +39 -0
  53. package/dist/errors/error.d.ts +24 -0
  54. package/dist/executor/ddl.d.ts +23 -0
  55. package/dist/executor/dml.d.ts +10 -0
  56. package/dist/executor/execute.d.ts +11 -0
  57. package/dist/executor/relation.d.ts +60 -0
  58. package/dist/executor/select.d.ts +41 -0
  59. package/dist/executor/session.d.ts +21 -0
  60. package/dist/executor/triggers-exec.d.ts +1 -0
  61. package/dist/executor/triggers.d.ts +16 -0
  62. package/dist/executor/window.d.ts +13 -0
  63. package/dist/expressions/context.d.ts +23 -0
  64. package/dist/expressions/eval.d.ts +38 -0
  65. package/dist/expressions/operators.d.ts +8 -0
  66. package/dist/expressions/pattern.d.ts +20 -0
  67. package/dist/functions/aggregates.d.ts +17 -0
  68. package/dist/functions/array-fns.d.ts +2 -0
  69. package/dist/functions/datetime-fns.d.ts +26 -0
  70. package/dist/functions/datetime-registry.d.ts +2 -0
  71. package/dist/functions/json-fns.d.ts +7 -0
  72. package/dist/functions/math-fns.d.ts +2 -0
  73. package/dist/functions/misc-fns.d.ts +8 -0
  74. package/dist/functions/scalar.d.ts +10 -0
  75. package/dist/functions/srf.d.ts +12 -0
  76. package/dist/functions/string-fns.d.ts +4 -0
  77. package/dist/functions/tsearch-fns.d.ts +2 -0
  78. package/dist/functions/util.d.ts +14 -0
  79. package/dist/functions/window.d.ts +7 -0
  80. package/dist/index.d.ts +28 -0
  81. package/dist/index.js +19591 -0
  82. package/dist/index.js.map +7 -0
  83. package/dist/lexer/tokenize.d.ts +8 -0
  84. package/dist/parser/index.d.ts +1 -0
  85. package/dist/parser/parser.d.ts +96 -0
  86. package/dist/runtime/clock.d.ts +14 -0
  87. package/dist/runtime/index.d.ts +3 -0
  88. package/dist/runtime/options.d.ts +25 -0
  89. package/dist/runtime/prng.d.ts +48 -0
  90. package/dist/schema/catalog-tables.d.ts +1 -0
  91. package/dist/schema/catalog.d.ts +12 -0
  92. package/dist/serialization/codec.d.ts +29 -0
  93. package/dist/serialization/index.d.ts +1 -0
  94. package/dist/storage/database-state.d.ts +216 -0
  95. package/dist/transactions/manager.d.ts +25 -0
  96. package/dist/tsearch/stem.d.ts +5 -0
  97. package/dist/tsearch/tsearch.d.ts +59 -0
  98. package/dist/types/cast.d.ts +26 -0
  99. package/dist/types/compare.d.ts +18 -0
  100. package/dist/types/datetime.d.ts +68 -0
  101. package/dist/types/jsonb.d.ts +45 -0
  102. package/dist/types/numeric.d.ts +69 -0
  103. package/dist/types/resolve.d.ts +15 -0
  104. package/dist/types/timezone.d.ts +9 -0
  105. package/dist/types/value.d.ts +99 -0
  106. package/dist/unstable.d.ts +15 -0
  107. package/dist/unstable.js +19153 -0
  108. package/dist/unstable.js.map +7 -0
  109. package/package.json +120 -0
package/README.md ADDED
@@ -0,0 +1,325 @@
1
+ # postgres-mem
2
+
3
+ Pure TypeScript, completely in-memory PostgreSQL implementation aiming for **PostgreSQL 18 SQL dialect parity** (same statements → same results).
4
+
5
+ - Runs in modern browsers and Node.js / Bun
6
+ - **Zero** WASM, native bindings, workers, or filesystem dependencies
7
+ - Entire database stored in memory
8
+ - **Synchronous** ESM-only API (no Promises, no `require`)
9
+ - **SQL dialect verified** against real PostgreSQL 18.3 (PGlite) via differential contracts + fail-closed gate
10
+ - **Not** a drop-in for `pg` / `postgres.js` client APIs, the wire protocol, or on-disk clusters
11
+ - Intentional differences: deterministic `random()` / `now()` by default, and a custom snapshot format (not `pg_dump`)
12
+
13
+ See [COMPATIBILITY.md](COMPATIBILITY.md) for the matrix, [docs/DROP-IN-CONTRACT.md](docs/DROP-IN-CONTRACT.md) for the falsifiable claim, and [docs/GAP-ANALYSIS.md](docs/GAP-ANALYSIS.md) for what is still unproven. Agents: [AGENTS.md](AGENTS.md).
14
+
15
+ ## Documentation
16
+
17
+ | Doc | For |
18
+ | --- | --- |
19
+ | [README.md](README.md) | Install, API, pitfalls (this file) |
20
+ | [AGENTS.md](AGENTS.md) | Architecture, how to change code, test/compat gates |
21
+ | [COMPATIBILITY.md](COMPATIBILITY.md) | Feature matrix + verify commands |
22
+ | [COMPATIBILITY-AUDIT.md](COMPATIBILITY-AUDIT.md) | Audit evidence |
23
+ | [docs/DROP-IN-CONTRACT.md](docs/DROP-IN-CONTRACT.md) | Falsifiable drop-in claim (what “same” means) |
24
+ | [docs/PROOF.md](docs/PROOF.md) | Evidence argument + what is not proven |
25
+ | [docs/GAP-ANALYSIS.md](docs/GAP-ANALYSIS.md) | Gap analysis vs the full PostgreSQL surface |
26
+ | [docs/GAP-CATALOG.md](docs/GAP-CATALOG.md) | Current unproven / thin / intentional inventory |
27
+ | [DIVERGENCES.md](DIVERGENCES.md) | Auto-generated intentional divergences |
28
+ | [docs/SECRETS.md](docs/SECRETS.md) | npm / CI publish setup |
29
+ | [benchmarks/PERFORMANCE.md](benchmarks/PERFORMANCE.md) | Performance notes |
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ bun add @crvouga/postgres-mem
35
+ # or
36
+ npm install @crvouga/postgres-mem
37
+ ```
38
+
39
+ Requires Node.js ≥ 20 or Bun ≥ 1.1. The published package is **ESM only** (`import` from `@crvouga/postgres-mem`).
40
+
41
+ ## Usage
42
+
43
+ ```ts
44
+ import { Database } from "@crvouga/postgres-mem";
45
+
46
+ const db = new Database();
47
+
48
+ db.exec(`
49
+ CREATE TABLE users (
50
+ id serial PRIMARY KEY,
51
+ name text NOT NULL
52
+ )
53
+ `);
54
+
55
+ db.prepare(`INSERT INTO users (name) VALUES ($1)`).run("Alice");
56
+
57
+ const users = db.query<{ id: number; name: string }>(`SELECT * FROM users`);
58
+ console.log(users);
59
+
60
+ const snap = db.snapshot();
61
+ const db2 = new Database();
62
+ db2.restore(snap);
63
+ ```
64
+
65
+ All methods are **synchronous** — do not `await` them. Browser and Node/Bun share the same in-memory JS surface (no filesystem, no server, no wire protocol).
66
+
67
+ ## Example
68
+
69
+ A React + Vite SQL playground lives in [`examples/react-vite`](examples/react-vite):
70
+
71
+ ```bash
72
+ cd examples/react-vite
73
+ bun install
74
+ bun run dev
75
+ ```
76
+
77
+ From the repo root after that install: `bun run example`.
78
+
79
+ ## API
80
+
81
+ ```ts
82
+ import { Database, PostgresError } from "@crvouga/postgres-mem";
83
+
84
+ interface DatabaseOptions {
85
+ seed?: number | bigint; // default 1 — ignored when random is "os"
86
+ random?: "deterministic" | "os"; // default "deterministic"; "os" is CSPRNG like PostgreSQL
87
+ now?: Date | (() => Date) | "system"; // default 2000-01-01T00:00:00.000Z; "system" is wall clock
88
+ }
89
+
90
+ interface Database {
91
+ constructor(options?: DatabaseOptions);
92
+ exec(sql: string): void;
93
+ query<T = QueryRow>(sql: string, params?: BindValue[]): T[];
94
+ prepare(sql: string): Statement;
95
+ transaction<T>(fn: () => T): T;
96
+ copyFrom(sql: string, data: string): number; // COPY t FROM STDIN payload (\copy analog)
97
+ snapshot(): Uint8Array;
98
+ restore(snapshot: Uint8Array): void;
99
+ close(): void;
100
+ [Symbol.dispose]?(): void; // alias for close() when Symbol.dispose exists
101
+ readonly changes: number; // rows affected by the most recent INSERT/UPDATE/DELETE
102
+ }
103
+
104
+ interface Statement {
105
+ run(...params: BindValue[]): RunResult;
106
+ all<T = QueryRow>(...params: BindValue[]): T[];
107
+ get<T = QueryRow>(...params: BindValue[]): T | undefined;
108
+ result(...params: BindValue[]): ResultSet; // includes column metadata for zero rows
109
+ textResult(...params: BindValue[]): TextResultSet; // every cell as canonical PostgreSQL text
110
+ }
111
+
112
+ interface RunResult {
113
+ rowCount: number; // PostgreSQL command-tag count
114
+ command: string; // e.g. "INSERT", "UPDATE", "SELECT"
115
+ }
116
+
117
+ interface ResultSet {
118
+ columns: string[];
119
+ columnTypes: string[]; // PG internal type names, e.g. "int4", "numeric"
120
+ rows: QueryRow[];
121
+ rowCount: number;
122
+ command: string;
123
+ }
124
+
125
+ class PostgresError extends Error {
126
+ readonly category: ErrorCategory; // syntax, undefined_table, constraint_unique, misuse, …
127
+ readonly sqlState: string; // five-character SQLSTATE, e.g. "42P01", "23505"
128
+ readonly code: string; // === sqlState (node-postgres err.code convention)
129
+ }
130
+ ```
131
+
132
+ Stick to `Database`, `Statement`, and `PostgresError` for application code. Advanced internals (`parse`, `tokenize`, `executeStatement`, snapshot codec pieces, `Prng`, …) are available only from `@crvouga/postgres-mem/unstable` and are **exempt from semver**.
133
+
134
+ ### Method semantics
135
+
136
+ | Method | Behavior |
137
+ | --- | --- |
138
+ | `exec(sql)` | Runs all semicolon-separated statements; **discards** row results (`void`). Does **not** accept bind parameters. Read `db.changes` afterward if needed (reflects the **most recent** completed DML statement). |
139
+ | `query(sql, params?)` | **Single statement only** (trailing `;` is fine). Returns all rows. Multi-statement scripts throw `misuse`. |
140
+ | `prepare(sql)` | **Single statement only**. Parses immediately; AST is reused. Pass binds as rest args to `run` / `all` / `get` / `result` on each call. |
141
+ | `transaction(fn)` | If idle: `BEGIN` → `fn()` → `COMMIT`, or `ROLLBACK` + rethrow. If already in a transaction: nested savepoint. Nested SQL `BEGIN` inside is a no-op warning like PostgreSQL. `close()` inside `fn` throws `misuse`. |
142
+ | `copyFrom(sql, data)` | Executes `COPY table [(cols)] FROM STDIN` with `data` as the copy-in payload (text or csv per the COPY options). Returns rows copied. `COPY ... TO STDOUT` output is returned as result rows by `query`. |
143
+ | `snapshot` / `restore` | Custom binary format (see below). |
144
+ | `close()` | Idempotent; rolls back an open SQL transaction; further ops throw `misuse`. Also available as `[Symbol.dispose]` when supported. |
145
+
146
+ SQL `BEGIN` / `COMMIT` / `ROLLBACK` / `SAVEPOINT` / `RELEASE` are first-class. Empty / comment-only SQL on `prepare` / `query` / `exec` throws `misuse` (`empty statement`).
147
+
148
+ ### Parameter binding
149
+
150
+ Parameters are PostgreSQL-style **positional `$1..$n` only** (no `?`, no named parameters — matching the PostgreSQL wire convention).
151
+
152
+ - The JS API takes **rest args** (or a positional array into `query`) — there is **no** sticky `bind()`.
153
+ - Bindable: `null` / `undefined` (→ NULL), `string` (behaves like an untyped literal — coerced by context), `number` (integer-valued → `int4`/`int8`, otherwise `float8`), `bigint` (→ `int8`, range-checked), `boolean`, `Uint8Array` (→ `bytea`), `Date` (→ `timestamptz`).
154
+ - Rejected (`misuse` / `numeric_value_out_of_range`): plain objects, symbols, functions, bigints outside int8, invalid `Date`s.
155
+
156
+ ```ts
157
+ db.query(`SELECT $1::int AS a, $2 AS b`, [1, "Alice"]);
158
+ db.prepare(`SELECT $1::int8 AS id`).get(42n);
159
+ ```
160
+
161
+ ### Returned JavaScript types
162
+
163
+ | PostgreSQL type | JS value | Notes |
164
+ | --- | --- | --- |
165
+ | NULL | `null` | Never `undefined` |
166
+ | `bool` | `boolean` | |
167
+ | `int2` / `int4` | `number` | |
168
+ | `int8` | `bigint` | Always bigint, like node-postgres with int8 parsing |
169
+ | `float4` / `float8` | `number` | |
170
+ | `bytea` | `Uint8Array` | |
171
+ | everything else | `string` | `numeric`, `text`, `date`/`timestamp[tz]`, `interval`, `uuid`, `json[b]`, arrays, enums, … surface as **canonical PostgreSQL text** (what `psql` prints) |
172
+
173
+ Duplicate column names collapse in row objects (last write wins). Use `stmt.result().values`-style access via `rows`/`textResult()` for positional cells.
174
+
175
+ ### Snapshots
176
+
177
+ - Format magic `PGMM` followed by an explicit little-endian format-version `u32` — **not** `pg_dump` output and not loadable by real PostgreSQL.
178
+ - Round-trips schemas, tables, rows, sequences (counters included), indexes, views, enums, domains, SQL functions, change counters, PRNG state, and clock.
179
+ - Cannot `restore()` while a transaction is open (`25P01`).
180
+ - `restore()` replaces `now` with a fixed clock from the snapshot (a live `() => Date` is overwritten). `{ now: "system" }` stays live after restore.
181
+ - Equivalent databases produce byte-identical snapshots (schema/rows sorted) **within a single library version**.
182
+ - **Compatibility policy:** newer library versions can always restore older snapshots; older libraries cannot restore newer format versions (`snapshot_version`). Corrupt magic yields a distinct error.
183
+
184
+ ## Determinism
185
+
186
+ The engine is deterministic by default. Invariants:
187
+
188
+ | Source | Default | Override / notes |
189
+ | --- | --- | --- |
190
+ | `random()` / `gen_random_uuid()` | Seeded xorshift64* (`seed: 1`) | `new Database({ seed })` or `{ random: "os" }` for CSPRNG (not rolled back / not restored) |
191
+ | `now()` / `current_timestamp` / friends | Fixed `2000-01-01T00:00:00.000Z` | `new Database({ now: Date \| (() => Date) \| "system" })` — `"system"` is wall clock and is **not** frozen by `restore()` |
192
+ | `setseed()` / `random()` | Deterministic stream | Matches the engine PRNG, repeatable |
193
+ | Table scans | Insertion order | Same order after `snapshot`/`restore` |
194
+ | Snapshots | Sorted schema/rows + PRNG state + clock | Restored into PRNG and `now` |
195
+ | Transactions | PRNG rolls back with `ROLLBACK`/`SAVEPOINT` | Matches data rollback |
196
+ | `float8 -0` | Sign preserved | `(-0)::text` is `'-0'`, matching PostgreSQL |
197
+
198
+ Fuzz / property tests use a fixed seed (`0x5a17e0e1`) and print it on failure:
199
+
200
+ ```bash
201
+ bun test tests/fuzz
202
+ POSTGRES_MEM_FUZZ_SEED=12345 bun test tests/fuzz
203
+ POSTGRES_MEM_FUZZ_SEED=12345 POSTGRES_MEM_FUZZ_PATH='0:1' bun test tests/fuzz # exact replay
204
+ ```
205
+
206
+ ## Stability policy
207
+
208
+ The exports of the main entry (`@crvouga/postgres-mem`) are **frozen**:
209
+
210
+ - **Never** outside a major: removals, renames, signature changes, or changes to documented behavior of the stable surface.
211
+ - **Allowed in minors:** additions (new methods, new optional `DatabaseOptions` fields, new `ErrorCategory` values). Consumers that `switch` on `category` must include a default case — new categories may appear without a major bump.
212
+ - **`@crvouga/postgres-mem/unstable`** is exempt from semver and may change or disappear in any release.
213
+ - **Snapshots:** newer library → can restore older blobs; older library → cannot restore newer format versions; byte-identical snapshot guarantee holds only within one library version.
214
+
215
+ ## Compatibility notes for integrators
216
+
217
+ Goal: **SQL dialect** behavioral parity vs PostgreSQL **18.3** for the `@crvouga/postgres-mem` sync API. Full matrix: [COMPATIBILITY.md](COMPATIBILITY.md). Contract: [docs/DROP-IN-CONTRACT.md](docs/DROP-IN-CONTRACT.md).
218
+
219
+ This is **not** a drop-in replacement for `pg`, `postgres.js`, or PGlite's client API. There is no wire protocol, no async client, no connection pooling, no `pg_dump` codec, and no multi-session concurrency.
220
+
221
+ **Intentional differences:** custom `PGMM` snapshots; seeded `random()` / fixed `now()` by default (`{ random: "os" }` / `{ now: "system" }` match PostgreSQL entropy and wall clock); single session — no MVCC across connections. Machine-readable list: [DIVERGENCES.md](DIVERGENCES.md).
222
+
223
+ **Know these thin or partial areas** (do not assume full oracle fidelity):
224
+
225
+ - `EXPLAIN` — stub plan shapes, not real planner output
226
+ - Failed statements inside `BEGIN` do **not** poison the transaction (`25P02` aborted-state is not implemented)
227
+ - Triggers fire in **creation order** (PostgreSQL: name order); `UPDATE OF` column lists are ignored; `INSTEAD OF` is unsupported
228
+ - `COMMENT ON` parses but comments are not stored
229
+ - `round(float8)` rounds ties away from zero (PostgreSQL: half-to-even); numeric `round()` has full parity
230
+ - `'1e400'::float8` saturates to `Infinity` instead of raising `22003`
231
+ - `MERGE`, `CALL`/procedures, cursors (`DECLARE`/`FETCH`), `LISTEN`/`NOTIFY`, PL/pgSQL bodies fail loud (`0A000`)
232
+ - `VACUUM` / `ANALYZE` / `CLUSTER` / `REINDEX` / `CHECKPOINT` / `GRANT` / `REVOKE` / `LOCK` are parsed no-ops
233
+ - Collation is `C` semantics (byte order); locale/ICU-dependent ordering is out of scope
234
+
235
+ **Also supported (oracle-parity):** schemas + `search_path`, `pg_catalog` / `information_schema` introspection, sequences (`serial`, identity, `nextval`/`currval`/`setval`), enums, domains, `LANGUAGE sql` functions, row-level triggers, recursive + data-modifying CTEs, window functions with full frame specs, `GROUPING SETS`/`ROLLUP`/`CUBE`, `DISTINCT ON`, `LATERAL`, arrays + `unnest` + subscripting, JSON/JSONB operator + function surface, `tsvector` text search, `ON CONFLICT DO NOTHING/UPDATE`, `RETURNING`, `PREPARE`/`EXECUTE`/`DEALLOCATE`, `SET`/`SHOW`/`RESET` GUCs, `COPY` text and csv.
236
+
237
+ ## Common pitfalls
238
+
239
+ 1. **Do not `await`** — the API is sync.
240
+ 2. **Parameters are `$1..$n` only** — no `?` placeholders, no named parameters, no sticky `bind()`.
241
+ 3. **`query` / `prepare` are single-statement only** — multi-statement scripts belong in `exec()` (which does not take bind parameters).
242
+ 4. **`exec` returns `void` and takes no params** — use `db.prepare(…).run(…)` or `db.query(…)` for binds; use `db.changes` / `stmt.run().rowCount` for counters.
243
+ 5. **`now()` is not wall-clock** unless you pass `{ now: "system" }` or `{ now: () => new Date() }`. Default is year 2000. `restore()` freezes a snapshot clock except when constructed with `"system"`.
244
+ 6. **`random()` is seeded**, not OS entropy, unless you pass `{ random: "os" }`. Snapshots restore the seeded PRNG; OS entropy is not rewound.
245
+ 7. **Snapshots are not `pg_dump` output** and cannot be loaded into real PostgreSQL.
246
+ 8. **`int8` comes back as `bigint`**, `numeric`/dates/json come back as **text** — parse them explicitly if you need JS numbers/objects.
247
+ 9. **A failed statement does not abort the transaction** — real PostgreSQL rejects everything after an error inside `BEGIN` until `ROLLBACK`; postgres-mem keeps executing (documented divergence).
248
+ 10. **Unquoted identifiers fold to lowercase** (PostgreSQL rule — not uppercase like the SQL standard).
249
+ 11. **Do not import `@crvouga/postgres-mem/unstable` in application code** unless you accept breakage in any release.
250
+
251
+ Working examples beyond this README: `examples/react-vite`, `tests/contract/api/`, and `tests/contract/parameters/`.
252
+
253
+ ## Development
254
+
255
+ Requires [Bun](https://bun.sh). For architecture, change checklists, and how to add contract tests, see **[AGENTS.md](AGENTS.md)**.
256
+
257
+ Parity is proven only by differential contracts against real PostgreSQL (PGlite, Postgres 18.3 in WASM). Isolated internal unit tests are not PostgreSQL compatibility proof.
258
+
259
+ ```bash
260
+ bun install
261
+ bun run ci:local # same gates as GitHub Actions CI (except publish)
262
+ bun run check # format + lint + typecheck + postgres-compat suite
263
+ bun run format # write Biome formatting
264
+ bun run lint # Biome lint
265
+ bun run typecheck
266
+ bun run test:postgres-compat # requirements + inventory gate + differential suite
267
+ bun test # contract + fuzz + harness
268
+ bun run build
269
+ ```
270
+
271
+ See [COMPATIBILITY.md](./COMPATIBILITY.md).
272
+
273
+ ## Releasing
274
+
275
+ Publishing is fully automated. You never bump `version` or run `npm publish` by hand.
276
+
277
+ ### How a release happens
278
+
279
+ 1. Push or merge to `main` with [Conventional Commits](https://www.conventionalcommits.org/).
280
+ 2. CI runs commitlint, format/lint/typecheck, build, package verification, tests, browser smoke, and benchmarks.
281
+ 3. If every gate is green, [semantic-release](https://semantic-release.gitbook.io/) analyzes commits since the last git tag, bumps semver, publishes to npm, and creates a GitHub Release.
282
+
283
+ | Commit | Version bump |
284
+ | --- | --- |
285
+ | `fix: …` | patch (`0.1.0` → `0.1.1`) |
286
+ | `feat: …` | minor (`0.1.0` → `0.2.0`) |
287
+ | `feat!: …` or `BREAKING CHANGE:` footer | major (`0.2.0` → `1.0.0`) |
288
+ | `docs:`, `chore:`, `refactor:`, `test:`, … | no release |
289
+
290
+ PR titles must also follow Conventional Commits (enforced in CI). Prefer squash merges with a conventional title.
291
+
292
+ Local checks:
293
+
294
+ ```bash
295
+ bun run ci:local # commitlint + quality + tests + browser + benchmarks
296
+ # dry-run needs a GitHub token for API calls; CI publish uses Trusted Publishing (no NPM_TOKEN)
297
+ bun run release:dry-run
298
+ ```
299
+
300
+ `package.json` version is `0.0.0-development` on purpose — **git tags** (`v0.1.0`, …) are the source of truth.
301
+
302
+ ### One-time setup (maintainers)
303
+
304
+ Do this once so CI can publish. Full checklist: **[docs/SECRETS.md](./docs/SECRETS.md)**.
305
+
306
+ 1. **Create the package on npm (once), then Trusted Publishing.** If https://www.npmjs.com/package/@crvouga/postgres-mem 404s:
307
+
308
+ ```bash
309
+ npm login --auth-type=web
310
+ bun run npm:seed -- --yes
311
+ ```
312
+
313
+ npm does not email a publish code — complete 2FA in the browser or authenticator app.
314
+
315
+ Then on [package Access](https://www.npmjs.com/package/@crvouga/postgres-mem/access) → Trusted Publisher → GitHub Actions (`crvouga/postgres-mem`, workflow `ci.yml`). Do **not** create an Automation / granular access token for CI.
316
+ 2. Confirm GitHub Actions is enabled and can create releases (default `GITHUB_TOKEN` is enough with this workflow’s permissions). No `NPM_TOKEN` repo secret.
317
+ 3. Ensure the baseline tag exists and is pushed: `v0.1.0` (semver continues from there; the next `feat` publishes `0.2.0`).
318
+
319
+ Validate the checklist anytime with `bun run secrets:doctor`.
320
+
321
+ After that, every green push to `main` with releasable commits updates npm automatically.
322
+
323
+ ## License
324
+
325
+ MIT