@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.
- package/AGENTS.md +174 -0
- package/COMPATIBILITY-AUDIT.md +156 -0
- package/COMPATIBILITY.md +87 -0
- package/LICENSE +21 -0
- package/README.md +416 -0
- package/compat/coverage.json +1081 -0
- package/compat/divergences.json +195 -0
- package/compat/requirements.json +1105 -0
- package/compat/scenario-types.ts +98 -0
- package/compat/scenarios.ts +98 -0
- package/compat/sections/agg.ts +45 -0
- package/compat/sections/api.ts +34 -0
- package/compat/sections/arr.ts +45 -0
- package/compat/sections/cat.ts +47 -0
- package/compat/sections/con.ts +32 -0
- package/compat/sections/cpy.ts +24 -0
- package/compat/sections/cte.ts +29 -0
- package/compat/sections/dat.ts +60 -0
- package/compat/sections/ddl.ts +55 -0
- package/compat/sections/det.ts +20 -0
- package/compat/sections/dml.ts +37 -0
- package/compat/sections/eco.ts +16 -0
- package/compat/sections/err.ts +16 -0
- package/compat/sections/exp.ts +53 -0
- package/compat/sections/fun.ts +58 -0
- package/compat/sections/fzz.ts +41 -0
- package/compat/sections/guc.ts +27 -0
- package/compat/sections/joi.ts +33 -0
- package/compat/sections/jsn.ts +46 -0
- package/compat/sections/lim.ts +13 -0
- package/compat/sections/par.ts +54 -0
- package/compat/sections/pre.ts +26 -0
- package/compat/sections/sch.ts +31 -0
- package/compat/sections/sel.ts +43 -0
- package/compat/sections/seq.ts +31 -0
- package/compat/sections/snp.ts +22 -0
- package/compat/sections/tok.ts +53 -0
- package/compat/sections/trg.ts +57 -0
- package/compat/sections/tsr.ts +37 -0
- package/compat/sections/txn.ts +39 -0
- package/compat/sections/typ.ts +55 -0
- package/compat/sections/uni.ts +13 -0
- package/compat/sections/win.ts +43 -0
- package/compat/smoke-baseline.json +3 -0
- package/compat/unsupported-register.json +2524 -0
- package/dist/api/bind.d.ts +11 -0
- package/dist/api/database.d.ts +87 -0
- package/dist/api/snapshot.d.ts +21 -0
- package/dist/api/statement.d.ts +51 -0
- package/dist/ast/nodes.d.ts +812 -0
- package/dist/constraints/enforce.d.ts +40 -0
- package/dist/errors/error.d.ts +24 -0
- package/dist/executor/ddl.d.ts +23 -0
- package/dist/executor/dml.d.ts +10 -0
- package/dist/executor/execute.d.ts +11 -0
- package/dist/executor/plpgsql.d.ts +72 -0
- package/dist/executor/relation.d.ts +60 -0
- package/dist/executor/select.d.ts +45 -0
- package/dist/executor/session.d.ts +21 -0
- package/dist/executor/triggers-exec.d.ts +1 -0
- package/dist/executor/triggers.d.ts +16 -0
- package/dist/executor/window.d.ts +13 -0
- package/dist/expressions/context.d.ts +23 -0
- package/dist/expressions/eval.d.ts +42 -0
- package/dist/expressions/operators.d.ts +8 -0
- package/dist/expressions/pattern.d.ts +20 -0
- package/dist/functions/aggregates.d.ts +17 -0
- package/dist/functions/array-fns.d.ts +2 -0
- package/dist/functions/datetime-fns.d.ts +26 -0
- package/dist/functions/datetime-registry.d.ts +2 -0
- package/dist/functions/json-fns.d.ts +7 -0
- package/dist/functions/math-fns.d.ts +2 -0
- package/dist/functions/misc-fns.d.ts +8 -0
- package/dist/functions/scalar.d.ts +10 -0
- package/dist/functions/srf.d.ts +12 -0
- package/dist/functions/string-fns.d.ts +4 -0
- package/dist/functions/tsearch-fns.d.ts +2 -0
- package/dist/functions/util.d.ts +14 -0
- package/dist/functions/window.d.ts +7 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +23581 -0
- package/dist/index.js.map +7 -0
- package/dist/indexes/index.d.ts +14 -0
- package/dist/indexes/maintain.d.ts +11 -0
- package/dist/lexer/tokenize.d.ts +8 -0
- package/dist/parser/index.d.ts +1 -0
- package/dist/parser/parser.d.ts +98 -0
- package/dist/planner/access.d.ts +15 -0
- package/dist/runtime/assert.d.ts +5 -0
- package/dist/runtime/clock.d.ts +14 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/options.d.ts +34 -0
- package/dist/runtime/prng.d.ts +48 -0
- package/dist/schema/catalog-tables.d.ts +1 -0
- package/dist/schema/catalog.d.ts +12 -0
- package/dist/serialization/codec.d.ts +22 -0
- package/dist/serialization/index.d.ts +1 -0
- package/dist/serialization/wire.d.ts +63 -0
- package/dist/sql/deparse.d.ts +3 -0
- package/dist/storage/columnar-slab.d.ts +37 -0
- package/dist/storage/database-state.d.ts +253 -0
- package/dist/transactions/manager.d.ts +25 -0
- package/dist/tsearch/stem.d.ts +5 -0
- package/dist/tsearch/tsearch.d.ts +59 -0
- package/dist/types/cast.d.ts +26 -0
- package/dist/types/compare.d.ts +18 -0
- package/dist/types/datetime.d.ts +68 -0
- package/dist/types/jsonb.d.ts +47 -0
- package/dist/types/jsonpath.d.ts +16 -0
- package/dist/types/numeric.d.ts +69 -0
- package/dist/types/range.d.ts +17 -0
- package/dist/types/resolve.d.ts +15 -0
- package/dist/types/timezone.d.ts +9 -0
- package/dist/types/value.d.ts +99 -0
- package/dist/unstable.d.ts +15 -0
- package/dist/unstable.js +21810 -0
- package/dist/unstable.js.map +7 -0
- 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).
|