@crvouga/mockingbird-service-postgres 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 (118) hide show
  1. package/AGENTS.md +174 -0
  2. package/COMPATIBILITY-AUDIT.md +156 -0
  3. package/COMPATIBILITY.md +87 -0
  4. package/LICENSE +21 -0
  5. package/README.md +416 -0
  6. package/compat/coverage.json +1081 -0
  7. package/compat/divergences.json +195 -0
  8. package/compat/requirements.json +1105 -0
  9. package/compat/scenario-types.ts +98 -0
  10. package/compat/scenarios.ts +98 -0
  11. package/compat/sections/agg.ts +45 -0
  12. package/compat/sections/api.ts +34 -0
  13. package/compat/sections/arr.ts +45 -0
  14. package/compat/sections/cat.ts +47 -0
  15. package/compat/sections/con.ts +32 -0
  16. package/compat/sections/cpy.ts +24 -0
  17. package/compat/sections/cte.ts +29 -0
  18. package/compat/sections/dat.ts +60 -0
  19. package/compat/sections/ddl.ts +55 -0
  20. package/compat/sections/det.ts +20 -0
  21. package/compat/sections/dml.ts +37 -0
  22. package/compat/sections/eco.ts +16 -0
  23. package/compat/sections/err.ts +16 -0
  24. package/compat/sections/exp.ts +53 -0
  25. package/compat/sections/fun.ts +58 -0
  26. package/compat/sections/fzz.ts +41 -0
  27. package/compat/sections/guc.ts +27 -0
  28. package/compat/sections/joi.ts +33 -0
  29. package/compat/sections/jsn.ts +46 -0
  30. package/compat/sections/lim.ts +13 -0
  31. package/compat/sections/par.ts +54 -0
  32. package/compat/sections/pre.ts +26 -0
  33. package/compat/sections/sch.ts +31 -0
  34. package/compat/sections/sel.ts +43 -0
  35. package/compat/sections/seq.ts +31 -0
  36. package/compat/sections/snp.ts +22 -0
  37. package/compat/sections/tok.ts +53 -0
  38. package/compat/sections/trg.ts +57 -0
  39. package/compat/sections/tsr.ts +37 -0
  40. package/compat/sections/txn.ts +39 -0
  41. package/compat/sections/typ.ts +55 -0
  42. package/compat/sections/uni.ts +13 -0
  43. package/compat/sections/win.ts +43 -0
  44. package/compat/smoke-baseline.json +3 -0
  45. package/compat/unsupported-register.json +2524 -0
  46. package/dist/api/bind.d.ts +11 -0
  47. package/dist/api/database.d.ts +87 -0
  48. package/dist/api/snapshot.d.ts +21 -0
  49. package/dist/api/statement.d.ts +51 -0
  50. package/dist/ast/nodes.d.ts +812 -0
  51. package/dist/constraints/enforce.d.ts +40 -0
  52. package/dist/errors/error.d.ts +24 -0
  53. package/dist/executor/ddl.d.ts +23 -0
  54. package/dist/executor/dml.d.ts +10 -0
  55. package/dist/executor/execute.d.ts +11 -0
  56. package/dist/executor/plpgsql.d.ts +72 -0
  57. package/dist/executor/relation.d.ts +60 -0
  58. package/dist/executor/select.d.ts +45 -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 +42 -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 +23581 -0
  82. package/dist/index.js.map +7 -0
  83. package/dist/indexes/index.d.ts +14 -0
  84. package/dist/indexes/maintain.d.ts +11 -0
  85. package/dist/lexer/tokenize.d.ts +8 -0
  86. package/dist/parser/index.d.ts +1 -0
  87. package/dist/parser/parser.d.ts +98 -0
  88. package/dist/planner/access.d.ts +15 -0
  89. package/dist/runtime/assert.d.ts +5 -0
  90. package/dist/runtime/clock.d.ts +14 -0
  91. package/dist/runtime/index.d.ts +3 -0
  92. package/dist/runtime/options.d.ts +34 -0
  93. package/dist/runtime/prng.d.ts +48 -0
  94. package/dist/schema/catalog-tables.d.ts +1 -0
  95. package/dist/schema/catalog.d.ts +12 -0
  96. package/dist/serialization/codec.d.ts +22 -0
  97. package/dist/serialization/index.d.ts +1 -0
  98. package/dist/serialization/wire.d.ts +63 -0
  99. package/dist/sql/deparse.d.ts +3 -0
  100. package/dist/storage/columnar-slab.d.ts +37 -0
  101. package/dist/storage/database-state.d.ts +253 -0
  102. package/dist/transactions/manager.d.ts +25 -0
  103. package/dist/tsearch/stem.d.ts +5 -0
  104. package/dist/tsearch/tsearch.d.ts +59 -0
  105. package/dist/types/cast.d.ts +26 -0
  106. package/dist/types/compare.d.ts +18 -0
  107. package/dist/types/datetime.d.ts +68 -0
  108. package/dist/types/jsonb.d.ts +47 -0
  109. package/dist/types/jsonpath.d.ts +16 -0
  110. package/dist/types/numeric.d.ts +69 -0
  111. package/dist/types/range.d.ts +17 -0
  112. package/dist/types/resolve.d.ts +15 -0
  113. package/dist/types/timezone.d.ts +9 -0
  114. package/dist/types/value.d.ts +99 -0
  115. package/dist/unstable.d.ts +15 -0
  116. package/dist/unstable.js +21810 -0
  117. package/dist/unstable.js.map +7 -0
  118. package/package.json +131 -0
package/README.md ADDED
@@ -0,0 +1,416 @@
1
+ # @crvouga/mockingbird-service-postgres
2
+
3
+ Pure TypeScript, completely in-memory PostgreSQL engine aiming for **PostgreSQL 18 SQL dialect
4
+ parity** (same statements, same results). Use it in tests (or the browser) wherever you want real
5
+ PostgreSQL SQL semantics without a server: schema + migrations, constraints and SQLSTATE errors,
6
+ transactions, CTEs, window functions, JSONB, sequences, and copy-on-write snapshots for per-test
7
+ isolation.
8
+
9
+ > Formerly [`@crvouga/postgres-mem`](https://www.npmjs.com/package/@crvouga/postgres-mem)
10
+ > ([archived repo](https://github.com/crvouga/postgres-mem)). Migrate by replacing the package
11
+ > name; the API is unchanged.
12
+
13
+ - Runs in modern browsers, Node.js and Bun
14
+ - **Zero** WASM, native bindings, workers, or filesystem dependencies; the whole database lives in memory
15
+ - **Synchronous**, ESM-only API (no Promises, no `require`)
16
+ - **SQL dialect verified** against real PostgreSQL 18.3 (PGlite by default; optional native server)
17
+ via differential contracts and a fail-closed gate
18
+ - **Not** a drop-in for the `pg` / `postgres.js` client APIs, the wire protocol, or on-disk clusters
19
+ - Intentional differences: deterministic `random()` / `now()` by default, and a custom snapshot
20
+ format (not `pg_dump`)
21
+
22
+ It is not a Mockingbird HTTP mock and is not the storage engine Mockingbird's HTTP mocks use (they
23
+ use the SQLite-dialect [`@crvouga/mockingbird-service-sqlite`](https://github.com/crvouga/mockingbird/tree/main/packages/service/sqlite#readme)
24
+ through the `SqliteClient` port). Use this package as the database for your own code under test.
25
+
26
+ ### Documentation
27
+
28
+ Files marked (shipped) are included in the npm package next to this README.
29
+
30
+ | Doc | For |
31
+ | --- | --- |
32
+ | [COMPATIBILITY.md](./COMPATIBILITY.md) (shipped) | Feature matrix + verify commands |
33
+ | [COMPATIBILITY-AUDIT.md](./COMPATIBILITY-AUDIT.md) (shipped) | Audit evidence |
34
+ | [AGENTS.md](./AGENTS.md) (shipped) | Contributor docs: architecture, how to change code, test/compat gates |
35
+ | [DROP-IN-CONTRACT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/postgres/docs/DROP-IN-CONTRACT.md) | Falsifiable drop-in claim (what "same" means) |
36
+ | [PROOF.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/postgres/docs/PROOF.md) | Evidence argument + what is not proven |
37
+ | [GAP-ANALYSIS.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/postgres/docs/GAP-ANALYSIS.md) | Gap analysis vs the full PostgreSQL surface |
38
+ | [GAP-CATALOG.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/postgres/docs/GAP-CATALOG.md) | Current unproven / thin / intentional inventory |
39
+ | [DIVERGENCES.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/postgres/DIVERGENCES.md) | Auto-generated intentional divergences (machine-readable: `compat/divergences.json`, shipped) |
40
+ | [PERFORMANCE.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/postgres/benchmarks/PERFORMANCE.md) | Performance notes |
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ npm install -D @crvouga/mockingbird-service-postgres
46
+ # or: bun add -d @crvouga/mockingbird-service-postgres
47
+ ```
48
+
49
+ Requires Node.js >= 20 or Bun >= 1.1 (`engines`); the rest of Mockingbird targets Node >= 22 /
50
+ Bun >= 1.2. The package is **ESM only** and has no runtime dependencies. Install it as a regular
51
+ dependency instead of `-D` if you ship it to the browser.
52
+
53
+ ## Usage
54
+
55
+ ```ts
56
+ import { Database, Snapshot } from "@crvouga/mockingbird-service-postgres"
57
+
58
+ const db = new Database()
59
+
60
+ db.exec(`
61
+ CREATE TABLE users (
62
+ id serial PRIMARY KEY,
63
+ email text UNIQUE NOT NULL,
64
+ created_at timestamptz NOT NULL DEFAULT now()
65
+ )
66
+ `)
67
+
68
+ db.prepare(`INSERT INTO users (email) VALUES ($1)`).run("ada@example.com")
69
+
70
+ const users = db.query<{ id: number; email: string; created_at: string }>(`SELECT * FROM users`)
71
+ console.log(users) // [{ id: 1, email: "ada@example.com", created_at: "2000-01-01 00:00:00+00" }]
72
+
73
+ // Snapshots: freeze a template, fork it cheaply, or persist it as bytes.
74
+ const seed = db.snapshot()
75
+ const db2 = seed.open()
76
+ const bytes = seed.encode()
77
+ const db3 = Snapshot.decode(bytes).open()
78
+ console.log(db2.query(`SELECT count(*) AS n FROM users`), db3.changes) // [{ n: 1n }] 1
79
+ ```
80
+
81
+ All methods are **synchronous**; do not `await` them. Browser and Node/Bun share the same
82
+ in-memory surface (no filesystem, no server, no wire protocol).
83
+
84
+ ### Per-test isolation with snapshots
85
+
86
+ Run migrations and fixtures once, `snapshot()` the result, and `open()` a copy-on-write fork per
87
+ test (microseconds, tables are shared until either side writes):
88
+
89
+ ```ts
90
+ import { beforeEach, expect, test } from "bun:test"
91
+ import { Database, PostgresError } from "@crvouga/mockingbird-service-postgres"
92
+
93
+ const template = new Database()
94
+ template.exec(`
95
+ CREATE TABLE accounts (id serial PRIMARY KEY, email text UNIQUE NOT NULL);
96
+ INSERT INTO accounts (email) VALUES ('seed@example.com');
97
+ `)
98
+ const seed = template.snapshot()
99
+
100
+ let db: Database
101
+ beforeEach(() => {
102
+ db = seed.open()
103
+ })
104
+
105
+ test("unique violation surfaces SQLSTATE 23505", () => {
106
+ let error: unknown
107
+ try {
108
+ db.prepare(`INSERT INTO accounts (email) VALUES ($1)`).run("seed@example.com")
109
+ } catch (caught) {
110
+ error = caught
111
+ }
112
+ expect(error).toBeInstanceOf(PostgresError)
113
+ expect((error as PostgresError).code).toBe("23505")
114
+ expect((error as PostgresError).category).toBe("constraint_unique")
115
+ })
116
+
117
+ test("each test starts from the seed", () => {
118
+ expect(db.query(`SELECT email FROM accounts`)).toEqual([{ email: "seed@example.com" }])
119
+ })
120
+ ```
121
+
122
+ ### Adapting it to code written against `pg`
123
+
124
+ There is no async client, so put a small shim behind whatever query interface your code uses. With
125
+ `{ int8: "string" }`, `int8` / `bigserial` / `count(*)` come back as strings, like node-postgres's
126
+ default:
127
+
128
+ ```ts
129
+ import { type BindValue, Database } from "@crvouga/mockingbird-service-postgres"
130
+
131
+ const db = new Database({ int8: "string", now: "system" })
132
+
133
+ /** Minimal pg.Pool-shaped facade: `query(text, values)` resolving to `{ rows, rowCount, command }`. */
134
+ export const pool = {
135
+ query: async <T = Record<string, unknown>>(text: string, values: BindValue[] = []) => {
136
+ const result = db.prepare(text).result(...values)
137
+ return { rows: result.rows as T[], rowCount: result.rowCount, command: result.command }
138
+ },
139
+ }
140
+
141
+ await pool.query(`CREATE TABLE notes (id bigserial PRIMARY KEY, body text)`)
142
+ const inserted = await pool.query<{ id: string }>(
143
+ `INSERT INTO notes (body) VALUES ($1) RETURNING id`,
144
+ ["hello"],
145
+ )
146
+ console.log(inserted.rows[0]?.id, inserted.rowCount) // "1" 1
147
+ ```
148
+
149
+ Differences from node-postgres to account for: timestamps, dates, `numeric` and `json`/`jsonb` come
150
+ back as PostgreSQL text (node-postgres parses timestamps to `Date` and JSON to objects);
151
+ `prepare`/`query` accept one statement at a time (use `exec` for scripts); errors are
152
+ `PostgresError` with `code` set to the SQLSTATE, like `pg`'s `DatabaseError.code`.
153
+
154
+ ### Method semantics
155
+
156
+ | Method | Behaviour |
157
+ | --- | --- |
158
+ | `exec(sql)` | Runs all semicolon-separated statements; **discards** row results (`void`). Does **not** accept bind parameters. Read `db.changes` afterwards if needed (reflects the **most recent** completed DML statement). Dump-only `DO` blocks and `ALTER TABLE ... SET (` storage parameters are no-ops. |
159
+ | `registerFunction(spec)` | Install a JavaScript scalar. Not stored in PGMM snapshots; `open()` of a live snapshot copies the implementation by reference. |
160
+ | `query(sql, params?)` | **Single statement only** (trailing `;` is fine). Returns all rows. Multi-statement scripts throw `misuse`. |
161
+ | `prepare(sql)` | **Single statement only**. Parses immediately; the AST is reused. Pass binds as rest args to `run` / `all` / `get` / `result` / `textResult` on each call. |
162
+ | `transaction(fn)` | If idle: `BEGIN`, `fn()`, `COMMIT`, or `ROLLBACK` + rethrow. If already in a transaction: nested savepoint. A nested SQL `BEGIN` inside is a no-op warning like PostgreSQL. `close()` inside `fn` throws `misuse`. |
163
+ | `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`. |
164
+ | `snapshot()` | Freeze a reusable `Snapshot` template (no encode). Illegal inside a transaction (`25P01`). |
165
+ | `Snapshot.open()` | Copy-on-write fork from a template. The parent stays open. |
166
+ | `Snapshot.encode()` | Lazy PGMM blob for persistence / worker boot (computed once, cached). |
167
+ | `Snapshot.decode(bytes)` | Decode a blob once per `Uint8Array` (WeakMap); later `open()` calls are copy-on-write. |
168
+ | `close()` | Idempotent; rolls back an open SQL transaction; further operations throw `misuse`. Also available as `[Symbol.dispose]` when the runtime defines `Symbol.dispose`. |
169
+
170
+ SQL `BEGIN` / `COMMIT` / `ROLLBACK` / `SAVEPOINT` / `RELEASE` are first-class. Empty or
171
+ comment-only SQL on `prepare` / `query` / `exec` throws `misuse` (`empty statement`).
172
+
173
+ ### Parameter binding
174
+
175
+ Parameters are PostgreSQL-style **positional `$1..$n` only** (no `?`, no named parameters, matching
176
+ the PostgreSQL wire convention).
177
+
178
+ - The JS API takes **rest args** (or a positional array into `query`); there is **no** sticky `bind()`.
179
+ - Bindable: `null` / `undefined` (NULL), `string` (behaves like an untyped literal, coerced by
180
+ context), `number` (integer-valued becomes `int4`/`int8`, otherwise `float8`), `bigint` (`int8`,
181
+ range-checked), `boolean`, `Uint8Array` (`bytea`), `Date` (`timestamptz`).
182
+ - Rejected (`misuse` / `numeric_value_out_of_range`): plain objects, symbols, functions, bigints
183
+ outside int8, invalid `Date`s.
184
+
185
+ ```ts
186
+ import { Database } from "@crvouga/mockingbird-service-postgres"
187
+
188
+ const db = new Database()
189
+ console.log(db.query(`SELECT $1::int AS a, $2 AS b`, [1, "Alice"])) // [{ a: 1, b: "Alice" }]
190
+ console.log(db.prepare(`SELECT $1::int8 AS id`).get(42n)) // { id: 42n }
191
+ ```
192
+
193
+ ### Returned JavaScript types
194
+
195
+ | PostgreSQL type | JS value | Notes |
196
+ | --- | --- | --- |
197
+ | NULL | `null` | Never `undefined` |
198
+ | `bool` | `boolean` | |
199
+ | `int2` / `int4` | `number` | |
200
+ | `int8` | `bigint` | Default; `{ int8: "number" }` or `{ int8: "string" }` changes it (`"number"` is unsafe beyond `Number.MAX_SAFE_INTEGER`) |
201
+ | `float4` / `float8` | `number` | |
202
+ | `bytea` | `Uint8Array` | |
203
+ | everything else | `string` | `numeric`, `text`, `date`/`timestamp[tz]`, `interval`, `uuid`, `json[b]`, arrays, enums, ... surface as **canonical PostgreSQL text** (what `psql` prints) |
204
+
205
+ Duplicate column names collapse in row objects (last write wins). Use `stmt.textResult()` (rows as
206
+ positional `(string | null)[]` arrays plus `columns`) when you need every cell.
207
+
208
+ ### Snapshots
209
+
210
+ - `db.snapshot()` returns a frozen in-memory `Snapshot`. Per-test isolation should `seed.open()`
211
+ (copy-on-write, microseconds). Encoded bytes are **lazy** via `snapshot.encode()`.
212
+ - Format: magic `PGMM` followed by an explicit little-endian format-version `u32`. **Not**
213
+ `pg_dump` output and not loadable by real PostgreSQL.
214
+ - Round-trips schemas, tables, rows, sequences (counters included), indexes, views, enums,
215
+ domains, SQL functions, change counters, PRNG state, and clock. JavaScript `registerFunction`
216
+ implementations are omitted.
217
+ - Cannot `snapshot()` while a transaction is open (`25P01`).
218
+ - `Snapshot.decode(bytes)` does not mutate the input `Uint8Array`. The same buffer object is
219
+ decoded once (WeakMap) and later opens are copy-on-write.
220
+ - `open()` shares frozen tables until either side writes; idle `open().snapshot().encode()` is
221
+ byte-identical to `snapshot().encode()`.
222
+ - `open()` uses a fixed clock from the snapshot unless you pass `{ now: "system" }`, which stays live.
223
+ - Equivalent databases produce byte-identical `encode()` output (schema/rows sorted) **within a
224
+ single library version**.
225
+ - **Compatibility policy:** newer library versions can always decode older snapshots; older
226
+ libraries cannot decode newer format versions (`snapshot_version`). Corrupt magic yields a
227
+ distinct error.
228
+
229
+ ### Determinism
230
+
231
+ The engine is deterministic by default:
232
+
233
+ | Source | Default | Override / notes |
234
+ | --- | --- | --- |
235
+ | `random()` / `gen_random_uuid()` | Seeded xorshift64* (`seed: 1`) | `new Database({ seed })`, or `{ random: "os" }` for CSPRNG (not rolled back / not restored) |
236
+ | `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 `open()` |
237
+ | `setseed()` / `random()` | Deterministic stream | Matches the engine PRNG, repeatable |
238
+ | Table scans | Insertion order | Same order after snapshot/restore |
239
+ | Snapshots | Sorted schema/rows + PRNG state + clock | Restored into PRNG and `now` |
240
+ | Transactions | PRNG rolls back with `ROLLBACK`/`SAVEPOINT` | Matches data rollback |
241
+ | `float8 -0` | Sign preserved | `(-0)::text` is `'-0'`, matching PostgreSQL |
242
+
243
+ ### Compatibility notes for integrators
244
+
245
+ Goal: **SQL dialect** behavioural parity vs PostgreSQL **18.3** for the sync API. Full matrix:
246
+ [COMPATIBILITY.md](./COMPATIBILITY.md). Contract:
247
+ [DROP-IN-CONTRACT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/postgres/docs/DROP-IN-CONTRACT.md).
248
+
249
+ There is no wire protocol, no async client, no connection pooling, no `pg_dump` codec, and no
250
+ multi-session concurrency. Intentional differences: custom `PGMM` snapshots; seeded `random()` /
251
+ fixed `now()` by default (`{ random: "os" }` / `{ now: "system" }` match PostgreSQL entropy and
252
+ wall clock); single session, no MVCC across connections.
253
+
254
+ **Thin or partial areas** (do not assume full oracle fidelity):
255
+
256
+ - `EXPLAIN`: stub plan shapes, not real planner output
257
+ - Failed statements inside `BEGIN` do **not** poison the transaction (`25P02` aborted-state is not implemented)
258
+ - Triggers fire in **creation order** (PostgreSQL: name order); `UPDATE OF` column lists are ignored; `INSTEAD OF` is unsupported
259
+ - `COMMENT ON` parses but comments are not stored
260
+ - `round(float8)` rounds ties away from zero (PostgreSQL: half-to-even); numeric `round()` has full parity
261
+ - `'1e400'::float8` saturates to `Infinity` instead of raising `22003`
262
+ - `MERGE`, `CALL`/procedures, cursors (`DECLARE`/`FETCH`), `LISTEN`/`NOTIFY`, and full PL/pgSQL (packages, NOTICE, cursors) fail loud (`0A000`)
263
+ - `VACUUM` / `ANALYZE` / `CLUSTER` / `REINDEX` / `CHECKPOINT` / `GRANT` / `REVOKE` / `LOCK` are parsed no-ops
264
+ - Collation is `C` semantics (byte order); locale/ICU-dependent ordering is out of scope
265
+
266
+ **Also supported (oracle parity):** schemas + `search_path`, `pg_catalog` / `information_schema`
267
+ introspection, sequences (`serial`, identity, `nextval`/`currval`/`setval`), enums, domains,
268
+ `LANGUAGE sql` functions, plpgsql-lite UDFs (`DECLARE`, `EXCEPTION WHEN others`, `RETURN NEXT`),
269
+ row-level triggers, recursive + data-modifying CTEs, window functions with full frame specs,
270
+ `GROUPING SETS`/`ROLLUP`/`CUBE`, `DISTINCT ON`, `LATERAL`, arrays + `unnest` + subscripting,
271
+ JSON/JSONB operator + function surface including `jsonb_path_query_first`, `tsvector` text
272
+ search, `ON CONFLICT DO NOTHING/UPDATE`, `RETURNING`, `PREPARE`/`EXECUTE`/`DEALLOCATE`,
273
+ `SET`/`SHOW`/`RESET` GUCs, `COPY` text and csv.
274
+
275
+ ### Common pitfalls
276
+
277
+ 1. **Do not `await`**: the API is sync.
278
+ 2. **Parameters are `$1..$n` only**: no `?` placeholders, no named parameters, no sticky `bind()`.
279
+ 3. **`query` / `prepare` are single-statement only**: multi-statement scripts belong in `exec()` (which does not take bind parameters).
280
+ 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.
281
+ 5. **`now()` is not wall-clock** unless you pass `{ now: "system" }` or `{ now: () => new Date() }`. The default is year 2000. `open()` freezes a snapshot clock except when constructed with `"system"`.
282
+ 6. **`random()` is seeded**, not OS entropy, unless you pass `{ random: "os" }`. Snapshots restore the seeded PRNG; OS entropy is not rewound.
283
+ 7. **Snapshots are not `pg_dump` output** and cannot be loaded into real PostgreSQL.
284
+ 8. **`int8` comes back as `bigint` by default** (`count(*)` included); `{ int8: "number" | "string" }` opts out. `numeric`, dates and JSON come back as **text**; parse them explicitly if you need JS numbers/objects.
285
+ 9. **A failed statement does not abort the transaction**: real PostgreSQL rejects everything after an error inside `BEGIN` until `ROLLBACK`; this engine keeps executing (documented divergence).
286
+ 10. **Unquoted identifiers fold to lowercase** (the PostgreSQL rule, not uppercase like the SQL standard).
287
+ 11. **Do not import `@crvouga/mockingbird-service-postgres/unstable` in application code** unless you accept breakage in any release.
288
+
289
+ ## API
290
+
291
+ Stable runtime exports of the main entry:
292
+
293
+ | Export | Description |
294
+ | --- | --- |
295
+ | `Database` | Class. `new Database(options?: DatabaseOptions)` — one in-memory PostgreSQL database and session. |
296
+ | `Snapshot` | Class. Frozen template from `db.snapshot()` or `Snapshot.decode(bytes)`; `open(options?)` forks a `Database`, `encode()` serializes. |
297
+ | `Statement` | Class returned by `db.prepare(sql)` (not constructed directly): `run`, `all`, `get`, `result`, `textResult`, and `sql`. |
298
+ | `PostgresError` | Error class thrown for SQL and API errors: `category` (`ErrorCategory`), `sqlState` / `code` (five-character SQLSTATE, e.g. `"42P01"`, `"23505"`). |
299
+
300
+ Signatures (types are exported too: `DatabaseOptions`, `RegisterFunctionOptions`, `ResultSet`,
301
+ `RunResult`, `ErrorCategory`, `BindValue`, `JsValue`, `QueryRow`):
302
+
303
+ ```text
304
+ interface DatabaseOptions {
305
+ seed?: number | bigint // default 1; ignored when random is "os"
306
+ random?: "deterministic" | "os" // default "deterministic"; "os" is CSPRNG like PostgreSQL
307
+ now?: Date | (() => Date) | "system" // default 2000-01-01T00:00:00.000Z; "system" is wall clock
308
+ int8?: "bigint" | "number" | "string" // default "bigint"; "number" is unsafe beyond MAX_SAFE_INTEGER
309
+ }
310
+
311
+ class Database {
312
+ constructor(options?: DatabaseOptions)
313
+ exec(sql: string): void
314
+ registerFunction(spec: { name: string; args: string[]; returns: string; strict?: boolean;
315
+ fn: (...args: JsValue[]) => JsValue }): void
316
+ query<T = QueryRow>(sql: string, params?: readonly BindValue[]): T[]
317
+ prepare(sql: string): Statement
318
+ transaction<T>(fn: () => T): T
319
+ copyFrom(sql: string, data: string): number // COPY t FROM STDIN payload (\copy analog)
320
+ snapshot(): Snapshot
321
+ close(): void // also [Symbol.dispose] when available
322
+ readonly changes: number // rows affected by the most recent INSERT/UPDATE/DELETE
323
+ readonly seed: number | bigint
324
+ readonly randomMode: "deterministic" | "os"
325
+ readonly int8Mode: "bigint" | "number" | "string"
326
+ }
327
+
328
+ class Snapshot {
329
+ open(options?: DatabaseOptions): Database
330
+ encode(): Uint8Array
331
+ static decode(bytes: Uint8Array): Snapshot
332
+ }
333
+
334
+ class Statement {
335
+ readonly sql: string
336
+ run(...params: BindValue[]): RunResult
337
+ all<T = QueryRow>(...params: BindValue[]): T[]
338
+ get<T = QueryRow>(...params: BindValue[]): T | undefined
339
+ result(...params: BindValue[]): ResultSet // includes column metadata for zero rows
340
+ textResult(...params: BindValue[]): TextResultSet // every cell as canonical PostgreSQL text
341
+ }
342
+
343
+ interface RunResult { rowCount: number; command: string } // command e.g. "INSERT", "SELECT"
344
+ interface ResultSet { columns: string[]; columnTypes: string[]; rows: QueryRow[]; rowCount: number; command: string }
345
+ interface TextResultSet { columns: string[]; columnTypes: string[]; rows: (string | null)[][]; rowCount: number; command: string }
346
+ // columnTypes are PostgreSQL internal type names, e.g. "int4", "numeric"
347
+
348
+ class PostgresError extends Error {
349
+ readonly category: ErrorCategory // "syntax", "undefined_table", "constraint_unique", "misuse", ...
350
+ readonly sqlState: string // five-character SQLSTATE
351
+ readonly code: string // === sqlState (node-postgres err.code convention)
352
+ }
353
+
354
+ type BindValue = null | undefined | boolean | number | bigint | string | Uint8Array | Date
355
+ type JsValue = null | boolean | number | bigint | string | Uint8Array
356
+ type QueryRow = Record<string, JsValue>
357
+ ```
358
+
359
+ Stick to `Database`, `Snapshot`, `Statement`, and `PostgresError` in application code. Advanced
360
+ internals (`parse`, `tokenize`, `executeStatement`, snapshot codec pieces, `Prng`, ...) are
361
+ available only from `@crvouga/mockingbird-service-postgres/unstable` and are **exempt from semver**.
362
+
363
+ ### Stability policy
364
+
365
+ The exports of the main entry (`@crvouga/mockingbird-service-postgres`) are **frozen**:
366
+
367
+ - **Never** outside a major: removals, renames, signature changes, or changes to documented
368
+ behaviour of the stable surface.
369
+ - **Allowed in minors:** additions (new methods, new optional `DatabaseOptions` fields, new
370
+ `ErrorCategory` values). Consumers that `switch` on `category` must include a default case.
371
+ - **`@crvouga/mockingbird-service-postgres/unstable`** is exempt from semver and may change or
372
+ disappear in any release.
373
+ - **Snapshots:** newer library versions restore older blobs; older library versions cannot
374
+ restore newer format versions; the byte-identical guarantee holds only within one library version.
375
+
376
+ ## Development
377
+
378
+ For contributors to the mockingbird repo only. Requires [Bun](https://bun.sh). For
379
+ architecture, change checklists, and how to add contract tests, see [AGENTS.md](./AGENTS.md).
380
+
381
+ Parity is proven by differential contracts against real PostgreSQL: the default oracle is PGlite
382
+ (18.3 in WASM), plus optional native PostgreSQL 18.3 via `bun run test:postgres-native`. Isolated
383
+ internal unit tests are not PostgreSQL compatibility proof.
384
+
385
+ ```bash
386
+ bun install
387
+ bun run check:full # same gates as GitHub Actions CI (except publish)
388
+ bun run check # format + lint + typecheck + postgres-compat suite
389
+ bun run format # write Biome formatting
390
+ bun run lint # Biome lint
391
+ bun run typecheck
392
+ bun run test:postgres-compat # requirements + inventory gate + differential suite (PGlite)
393
+ bun run test:postgres-native # same differential suite vs real PostgreSQL 18.3
394
+ bun test # contract + fuzz + harness
395
+ bun run build
396
+ ```
397
+
398
+ Fuzz / property tests use a fixed seed (`0x5a17e0e1`) and print it on failure:
399
+
400
+ ```bash
401
+ bun test tests/fuzz
402
+ bun run test:pbt:random -- 50 # N random seeds, fail fast on first mismatch
403
+ POSTGRES_MEM_FUZZ_SEED=12345 bun test tests/fuzz
404
+ POSTGRES_MEM_FUZZ_SEED=12345 POSTGRES_MEM_FUZZ_PATH='0:1' bun test tests/fuzz # exact replay
405
+ ```
406
+
407
+ A React + Vite SQL playground lives in
408
+ [`examples/react-vite`](https://github.com/crvouga/mockingbird/tree/main/packages/service/postgres/examples/react-vite)
409
+ (`bun run example` from this package after `bun install` there). More working examples:
410
+ [`tests/contract/api/`](https://github.com/crvouga/mockingbird/tree/main/packages/service/postgres/tests/contract/api)
411
+ and [`tests/contract/parameters/`](https://github.com/crvouga/mockingbird/tree/main/packages/service/postgres/tests/contract/parameters).
412
+
413
+ Released automatically from the [Mockingbird monorepo](https://github.com/crvouga/mockingbird)
414
+ (see the root README, Releasing). License: MIT ([LICENSE](./LICENSE)).
415
+
416
+ Part of [mockingbird](https://github.com/crvouga/mockingbird) — agent integration guide: [`@crvouga/mockingbird`](https://github.com/crvouga/mockingbird/tree/main/packages/facade#readme).