@crvouga/mockingbird-service-postgres 3.3.0 → 3.4.1

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/CHANGELOG.md CHANGED
@@ -1,6 +1,18 @@
1
1
  # Changelog — @crvouga/mockingbird-service-postgres
2
2
 
3
- ## 3.3.0 (2026-10-11)
3
+ ## 3.4.1 (2026-10-15)
4
+
5
+ ### Fixes and improvements
6
+
7
+ - resolve snapshot, promotion and webhook issues ([75fb79f](https://github.com/crvouga/mockingbird/commit/75fb79f9a999bf0af29c4ec07fde4f1d7a1f0c22))
8
+
9
+ ## 3.4.0 (2026-10-12)
10
+
11
+ ### Features
12
+
13
+ - unify the emulator control plane ([2068547](https://github.com/crvouga/mockingbird/commit/2068547bedde4e2e2331c1657c1a78120e84428b))
14
+
15
+ ## 3.3.0 (2026-10-10)
4
16
 
5
17
  ### Features
6
18
 
package/COMPATIBILITY.md CHANGED
@@ -58,7 +58,7 @@ for the guarantees and their limits (`tests/wire/durable*.test.ts`, `tests/wire/
58
58
 
59
59
  Anything a PostgreSQL application can invoke through SQL against the PGlite **18.3** oracle must match observable behavior, except:
60
60
 
61
- 1. **Snapshot format** — custom binary codec (`PGMM`), not `pg_dump` / on-disk clusters (logical state still round-trips).
61
+ 1. **Snapshot format** — custom binary codec (`PGMM`), not `pg_dump` / on-disk clusters (logical state still round-trips). Decoding PGMM v3 preserves bigint-backed SQL datums (including small `bigint`, `money`, `time`, `timestamp`, and `timestamptz` values) using their catalog types, so arithmetic remains valid after repeated restores, including materialized views (`tests/contract/snapshots/basic.test.ts`).
62
62
  2. **Deterministic `random()` / `now()`** — seeded PRNG and fixed clock by default (injectable).
63
63
  3. **Single-session sync API** — no concurrent sessions or `25P02` aborted-transaction state. The optional wire server separately provides per-session `READ COMMITTED` workspaces and row locks.
64
64
  4. **NOT APPLICABLE** rows in `compat/coverage.json` (roles, replication, VACUUM internals, LISTEN/NOTIFY, cursors, full PL/pgSQL, extensions other than `pgcrypto`'s `digest()` and `pg_trgm`'s `similarity` / `<%` / `gin_trgm_ops`).
package/README.md CHANGED
@@ -55,44 +55,59 @@ dependency instead of `-D` if you ship it to the browser.
55
55
  ## Usage
56
56
 
57
57
  ```ts
58
- import { Database, Snapshot } from "@crvouga/mockingbird-service-postgres"
58
+ import pg from "pg"
59
+ import { Snapshot } from "@crvouga/mockingbird-service-postgres"
60
+ import { serve } from "@crvouga/mockingbird-service-postgres/wire"
59
61
 
60
- const db = new Database()
62
+ // Test setup: start the emulator on a free port.
63
+ const server = await serve({ port: 0 })
64
+
65
+ // Application code: the `pg` client it already uses, given the emulator's connection string.
66
+ const client = new pg.Client({ connectionString: server.connectionString })
67
+ await client.connect()
61
68
 
62
- db.exec(`
69
+ await client.query(`
63
70
  CREATE TABLE users (
64
71
  id serial PRIMARY KEY,
65
72
  email text UNIQUE NOT NULL,
66
73
  created_at timestamptz NOT NULL DEFAULT now()
67
74
  )
68
75
  `)
76
+ await client.query(`INSERT INTO users (email) VALUES ($1)`, ["ada@example.com"])
69
77
 
70
- db.prepare(`INSERT INTO users (email) VALUES ($1)`).run("ada@example.com")
78
+ const users = await client.query<{ id: number; email: string; created_at: Date }>(
79
+ `SELECT * FROM users`,
80
+ )
81
+ console.log(users.rows) // [{ id: 1, email: "ada@example.com", created_at: 2000-01-01T00:00:00.000Z }]
71
82
 
72
- const users = db.query<{ id: number; email: string; created_at: string }>(`SELECT * FROM users`)
73
- console.log(users) // [{ id: 1, email: "ada@example.com", created_at: "2000-01-01 00:00:00+00" }]
83
+ // Test control, snapshots: freeze a template, boot a fork from it, or persist it as bytes.
84
+ const seed = server.snapshot()
85
+ const fork = await serve({ database: seed })
86
+ const restored = await serve({ database: Snapshot.decode(seed.encode()) })
74
87
 
75
- // Snapshots: freeze a template, fork it cheaply, or persist it as bytes.
76
- const seed = db.snapshot()
77
- const db2 = seed.open()
78
- const bytes = seed.encode()
79
- const db3 = Snapshot.decode(bytes).open()
80
- console.log(db2.query(`SELECT count(*) AS n FROM users`), db3.changes) // [{ n: 1n }] 1
88
+ await client.end()
89
+ await Promise.all([server.close(), fork.close(), restored.close()])
81
90
  ```
82
91
 
83
- All methods are **synchronous**; do not `await` them. Browser and Node/Bun share the same
84
- in-memory surface (no filesystem). For a real TCP listener separate processes can connect to, see
85
- the [wire-protocol server](#wire-protocol-server-separate-processes) (Node/Bun only).
92
+ `postgres.js`, a JDBC client and `psql` connect with the same connection string; see the
93
+ [wire-protocol server](#wire-protocol-server-separate-processes) (Node/Bun only). In the browser,
94
+ where no listener can run, use the embedded `Database` from the package root instead. It shares
95
+ the same in-memory engine (no filesystem) and all its methods are **synchronous**; do not `await`
96
+ them. See [Method semantics](#method-semantics).
86
97
 
87
98
  ### Per-test isolation with snapshots
88
99
 
89
- Run migrations and fixtures once, `snapshot()` the result, and `open()` a copy-on-write fork per
90
- test (microseconds, tables are shared until either side writes):
100
+ Run migrations and fixtures once, `snapshot()` the result, and boot a server from that snapshot
101
+ per test. Each one is a copy-on-write fork (microseconds, tables are shared until either side
102
+ writes); with the embedded `Database`, `seed.open()` is the same fork without a listener:
91
103
 
92
104
  ```ts
93
- import { beforeEach, expect, test } from "bun:test"
94
- import { Database, PostgresError } from "@crvouga/mockingbird-service-postgres"
105
+ import { afterEach, beforeEach, expect, test } from "bun:test"
106
+ import pg from "pg"
107
+ import { Database } from "@crvouga/mockingbird-service-postgres"
108
+ import { type PostgresServer, serve } from "@crvouga/mockingbird-service-postgres/wire"
95
109
 
110
+ // Test setup: build the template once, then boot a server from it for every test.
96
111
  const template = new Database()
97
112
  template.exec(`
98
113
  CREATE TABLE accounts (id serial PRIMARY KEY, email text UNIQUE NOT NULL);
@@ -100,64 +115,68 @@ template.exec(`
100
115
  `)
101
116
  const seed = template.snapshot()
102
117
 
103
- let db: Database
104
- beforeEach(() => {
105
- db = seed.open()
118
+ let server: PostgresServer
119
+ let db: pg.Client
120
+ beforeEach(async () => {
121
+ server = await serve({ database: seed })
122
+ db = new pg.Client({ connectionString: server.connectionString })
123
+ await db.connect()
124
+ })
125
+ afterEach(async () => {
126
+ await db.end()
127
+ await server.close()
106
128
  })
107
129
 
108
- test("unique violation surfaces SQLSTATE 23505", () => {
109
- let error: unknown
110
- try {
111
- db.prepare(`INSERT INTO accounts (email) VALUES ($1)`).run("seed@example.com")
112
- } catch (caught) {
113
- error = caught
114
- }
115
- expect(error).toBeInstanceOf(PostgresError)
116
- expect((error as PostgresError).code).toBe("23505")
117
- expect((error as PostgresError).category).toBe("constraint_unique")
130
+ // Application code under test uses `db`, an ordinary `pg` client.
131
+ test("unique violation surfaces SQLSTATE 23505", async () => {
132
+ const insert = db.query(`INSERT INTO accounts (email) VALUES ($1)`, ["seed@example.com"])
133
+ await expect(insert).rejects.toMatchObject({ code: "23505", constraint: "accounts_email_key" })
118
134
  })
119
135
 
120
- test("each test starts from the seed", () => {
121
- expect(db.query(`SELECT email FROM accounts`)).toEqual([{ email: "seed@example.com" }])
136
+ test("each test starts from the seed", async () => {
137
+ const { rows } = await db.query(`SELECT email FROM accounts`)
138
+ expect(rows).toEqual([{ email: "seed@example.com" }])
122
139
  })
123
140
  ```
124
141
 
125
- ### Adapting it to code written against `pg`
142
+ ### Code written against `pg`
126
143
 
127
- There is no async client, so put a small shim behind whatever query interface your code uses. With
128
- `{ int8: "string" }`, `int8` / `bigserial` / `count(*)` come back as strings, like node-postgres's
129
- default:
144
+ Code written against `pg` needs no adapter: give `pg.Pool` (or `pg.Client`) the server's connection
145
+ string. `pg` applies its own type parsers, so `int8` / `bigserial` / `count(*)` come back as
146
+ strings, as they do against a real server:
130
147
 
131
148
  ```ts
132
- import { type BindValue, Database } from "@crvouga/mockingbird-service-postgres"
133
-
134
- const db = new Database({ int8: "string", now: "system" })
149
+ import pg from "pg"
150
+ import { serve } from "@crvouga/mockingbird-service-postgres/wire"
135
151
 
136
- /** Minimal pg.Pool-shaped facade: `query(text, values)` resolving to `{ rows, rowCount, command }`. */
137
- export const pool = {
138
- query: async <T = Record<string, unknown>>(text: string, values: BindValue[] = []) => {
139
- const result = db.prepare(text).result(...values)
140
- return { rows: result.rows as T[], rowCount: result.rowCount, command: result.command }
141
- },
142
- }
152
+ // Test setup: start the emulator and give the pool its connection string.
153
+ const server = await serve({ port: 0 })
154
+ export const pool = new pg.Pool({ connectionString: server.connectionString })
143
155
 
156
+ // Application code: unchanged `pg` queries.
144
157
  await pool.query(`CREATE TABLE notes (id bigserial PRIMARY KEY, body text)`)
145
158
  const inserted = await pool.query<{ id: string }>(
146
159
  `INSERT INTO notes (body) VALUES ($1) RETURNING id`,
147
160
  ["hello"],
148
161
  )
149
162
  console.log(inserted.rows[0]?.id, inserted.rowCount) // "1" 1
163
+
164
+ await pool.end()
165
+ await server.close()
150
166
  ```
151
167
 
152
- Differences from node-postgres to account for: timestamps, dates, `numeric` and `json`/`jsonb` come
153
- back as PostgreSQL text (node-postgres parses timestamps to `Date` and JSON to objects);
154
- `prepare`/`query` accept one statement at a time (use `exec` for scripts); errors are
155
- `PostgresError` with `code` set to the SQLSTATE, like `pg`'s `DatabaseError.code`.
168
+ Errors are `pg`'s own `DatabaseError` with `code` set to the SQLSTATE. Where no listener can run
169
+ (the browser), put the embedded `Database` behind whatever query interface your code uses. The
170
+ differences from node-postgres to account for there: `int8` is a `bigint` unless you pass
171
+ `{ int8: "string" }`; timestamps, dates, `numeric` and `json`/`jsonb` come back as PostgreSQL text
172
+ (node-postgres parses timestamps to `Date` and JSON to objects); `prepare`/`query` accept one
173
+ statement at a time (use `exec` for scripts); errors are `PostgresError` with `code` set to the
174
+ SQLSTATE, like `pg`'s `DatabaseError.code`.
156
175
 
157
176
  ### Wire-protocol server (separate processes)
158
177
 
159
178
  When another process must connect over TCP — `pg`, `postgres.js`, a JDBC client, `psql`, a service
160
- in a local multi-service stack — start the frontend/backend v3 server instead of a shim. It needs
179
+ in a local multi-service stack — start the frontend/backend v3 server. It needs
161
180
  `node:net`, so it is a separate Node/Bun entry (`/wire`) and the main package stays browser-safe.
162
181
 
163
182
  ```ts