@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.
- package/AGENTS.md +159 -0
- package/COMPATIBILITY-AUDIT.md +155 -0
- package/COMPATIBILITY.md +87 -0
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/compat/coverage.json +1081 -0
- package/compat/divergences.json +191 -0
- package/compat/gate-report.json +80 -0
- package/compat/requirements.json +1105 -0
- package/compat/requirements.raw.html +484 -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 +28 -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 +36 -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 +61 -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 +21 -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 +2526 -0
- package/dist/api/bind.d.ts +10 -0
- package/dist/api/database.d.ts +69 -0
- package/dist/api/statement.d.ts +51 -0
- package/dist/ast/nodes.d.ts +810 -0
- package/dist/constraints/enforce.d.ts +39 -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/relation.d.ts +60 -0
- package/dist/executor/select.d.ts +41 -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 +38 -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 +19591 -0
- package/dist/index.js.map +7 -0
- package/dist/lexer/tokenize.d.ts +8 -0
- package/dist/parser/index.d.ts +1 -0
- package/dist/parser/parser.d.ts +96 -0
- package/dist/runtime/clock.d.ts +14 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/options.d.ts +25 -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 +29 -0
- package/dist/serialization/index.d.ts +1 -0
- package/dist/storage/database-state.d.ts +216 -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 +45 -0
- package/dist/types/numeric.d.ts +69 -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 +19153 -0
- package/dist/unstable.js.map +7 -0
- 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
|