@crvouga/mockingbird-service-postgres 3.2.0 → 3.4.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/CHANGELOG.md +17 -1
- package/COMPATIBILITY.md +35 -5
- package/README.md +219 -57
- package/compat/coverage.json +8 -7
- package/compat/divergences.json +37 -2
- package/compat/requirements.json +1 -1
- package/compat/sections/cat.ts +49 -0
- package/compat/sections/ddl.ts +11 -0
- package/compat/unsupported-register.json +0 -3
- package/dist/admin.js +4812 -987
- package/dist/admin.js.map +4 -4
- package/dist/api/database.d.ts +11 -0
- package/dist/ast/nodes.d.ts +43 -1
- package/dist/constraints/enforce.d.ts +2 -0
- package/dist/executor/alter-column.d.ts +27 -0
- package/dist/executor/comment.d.ts +4 -0
- package/dist/executor/ddl.d.ts +1 -2
- package/dist/executor/dml.d.ts +2 -0
- package/dist/executor/extensions.d.ts +11 -0
- package/dist/executor/maintenance.d.ts +25 -0
- package/dist/expressions/context.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3249 -549
- package/dist/index.js.map +4 -4
- package/dist/parser/parser.d.ts +9 -0
- package/dist/schema/catalog-extensions.d.ts +5 -0
- package/dist/schema/extensions.d.ts +33 -0
- package/dist/schema/index-catalog.d.ts +40 -0
- package/dist/schema/persistence.d.ts +32 -0
- package/dist/schema/storage.d.ts +22 -0
- package/dist/server.js +5390 -744
- package/dist/server.js.map +4 -4
- package/dist/sql/deparse-default.d.ts +12 -0
- package/dist/sql/deparse-index.d.ts +12 -2
- package/dist/sql/keywords.d.ts +6 -0
- package/dist/sql/ruleutils.d.ts +27 -0
- package/dist/storage/database-state.d.ts +51 -0
- package/dist/types/regnamespace.d.ts +23 -0
- package/dist/types/value.d.ts +2 -0
- package/dist/unstable.js +11520 -8875
- package/dist/unstable.js.map +4 -4
- package/dist/wire/cli.js +6445 -1260
- package/dist/wire/cli.js.map +4 -4
- package/dist/wire/cluster.d.ts +8 -0
- package/dist/wire/connection.d.ts +38 -0
- package/dist/wire/durable.d.ts +82 -0
- package/dist/wire/file-storage.d.ts +3 -0
- package/dist/wire/fleet.d.ts +210 -1
- package/dist/wire/index.d.ts +21 -1
- package/dist/wire/index.js +5383 -741
- package/dist/wire/index.js.map +4 -4
- package/dist/wire/startup.d.ts +36 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
# Changelog — @crvouga/mockingbird-service-postgres
|
|
2
2
|
|
|
3
|
-
## 3.
|
|
3
|
+
## 3.4.0 (2026-10-12)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- unify the emulator control plane ([2068547](https://github.com/crvouga/mockingbird/commit/2068547bedde4e2e2331c1657c1a78120e84428b))
|
|
8
|
+
|
|
9
|
+
## 3.3.0 (2026-10-10)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- resolve reported emulator coverage gaps ([6edc643](https://github.com/crvouga/mockingbird/commit/6edc6438444853a51aca27849c4c8c0449109ea0))
|
|
14
|
+
|
|
15
|
+
### Fixes and improvements
|
|
16
|
+
|
|
17
|
+
- make README examples pass consumer smoke ([9f5bf6a](https://github.com/crvouga/mockingbird/commit/9f5bf6a212fffb03b1f70fb67bce7fcb49f08c40))
|
|
18
|
+
|
|
19
|
+
## 3.2.0 (2026-10-08)
|
|
4
20
|
|
|
5
21
|
### Features
|
|
6
22
|
|
package/COMPATIBILITY.md
CHANGED
|
@@ -18,7 +18,7 @@ clock controls. This supervisor surface does not change the SQL dialect compatib
|
|
|
18
18
|
|
|
19
19
|
Differential tests compare a **tuple** per statement: rows (normalized to canonical PostgreSQL text where typed), column names and type names where requested, error SQLSTATE class + normalized message, command tag / `rowCount`, and transaction status, plus a **logical state dump** (catalog names, column definitions, row payloads, sequence values) after write sequences.
|
|
20
20
|
|
|
21
|
-
A catalog ID appearing in a test file is **not** proof by itself. Trivial probes are tracked in [`compat/smoke-baseline.json`](compat/smoke-baseline.json) and ratcheted downward (currently **0 smoke stubs** across
|
|
21
|
+
A catalog ID appearing in a test file is **not** proof by itself. Trivial probes are tracked in [`compat/smoke-baseline.json`](compat/smoke-baseline.json) and ratcheted downward (currently **0 smoke stubs** across 998 catalog scenarios). Generated operator/cast matrices live under [`tests/contract/matrices/`](tests/contract/matrices/). Observed mem≠oracle diffs must bind to a `compat/divergences.json` entry or be a **FAILURE** — unexplained diffs are not allowed.
|
|
22
22
|
|
|
23
23
|
Intentional differences are finite and machine-readable in `compat/divergences.json` (PGMM snapshots, seeded `random()`/`now()`, sync single-session API, no aborted-transaction state, EXPLAIN stubs, trigger-order/`UPDATE OF` edges, float8 rounding/overflow edges, …). The wire server supports isolated database catalogs, startup selection, database lifecycle DDL, and copy-on-write template cloning. Human-readable: [DIVERGENCES.md](DIVERGENCES.md).
|
|
24
24
|
|
|
@@ -26,6 +26,25 @@ The wire server supports protocol-v3 text and CSV `COPY FROM STDIN` / `COPY TO S
|
|
|
26
26
|
incremental `CopyData`, client aborts, and statement-atomic rollback. Binary COPY remains outside
|
|
27
27
|
the compatibility surface.
|
|
28
28
|
|
|
29
|
+
The wire server keeps settings per connection and applies the startup packet's run-time parameters
|
|
30
|
+
(libpq `options=-c name=value` / `--name=value`, split like `pg_split_opts`, then parameters sent by
|
|
31
|
+
name) as session defaults that `RESET` returns to. Malformed options, unknown parameters, bad
|
|
32
|
+
values and read-only parameters end the connection with the `FATAL` SQLSTATEs PostgreSQL 18.3
|
|
33
|
+
reports (`42601`, `42704`, `22023`, `55P02`), pinned by `tests/wire/startup-options.test.ts`.
|
|
34
|
+
Known gaps: parameters the engine has no setting for are `42704`, `postgres` switches other than
|
|
35
|
+
`-c` / `--` are `0A000`, `ParameterStatus` is not re-sent when a reported setting changes,
|
|
36
|
+
`SET LOCAL` does not carry across the statements of a wire transaction block, `DISCARD ALL` is a
|
|
37
|
+
no-op, and with no schema of `search_path` present an unqualified `CREATE` goes to `public` where
|
|
38
|
+
PostgreSQL raises `3F000`.
|
|
39
|
+
|
|
40
|
+
The wire server's opt-in durable mode (`serve({ durable })`, `--durable <dir>`) persists the
|
|
41
|
+
cluster before a commit is acknowledged, so acknowledged commits survive a killed process. It is
|
|
42
|
+
snapshot persistence of PGMM bytes behind an injectable storage port, with an atomic file-backed
|
|
43
|
+
implementation. It is not a write-ahead log and not PostgreSQL's on-disk format: there is no
|
|
44
|
+
`pg_wal`, no data directory another PostgreSQL could open, no physical replication or
|
|
45
|
+
point-in-time recovery, and each commit costs an encode of the database it changed. See the README
|
|
46
|
+
for the guarantees and their limits (`tests/wire/durable*.test.ts`, `tests/wire/file-storage.test.ts`).
|
|
47
|
+
|
|
29
48
|
## Status vocabulary
|
|
30
49
|
|
|
31
50
|
| Status | Meaning |
|
|
@@ -44,7 +63,7 @@ Anything a PostgreSQL application can invoke through SQL against the PGlite **18
|
|
|
44
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.
|
|
45
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`).
|
|
46
65
|
|
|
47
|
-
The oracle exposes **2787 builtin functions** and **74 operators** in `pg_catalog`; postgres-mem implements **
|
|
66
|
+
The oracle exposes **2787 builtin functions** and **74 operators** in `pg_catalog`; postgres-mem implements **306 functions** and **41 operators**, and every remaining item is an explicit entry in [`compat/unsupported-register.json`](compat/unsupported-register.json) with a reason (trigger/internal plumbing, admin/monitoring, unsupported type families, …). The gate fails closed on silence.
|
|
48
67
|
|
|
49
68
|
## Requirements coverage (PostgreSQL 18 SQL commands)
|
|
50
69
|
|
|
@@ -67,10 +86,21 @@ The oracle exposes **2787 builtin functions** and **74 operators** in `pg_catalo
|
|
|
67
86
|
| Sequences / serial / identity | VERIFIED | Includes pg_dump identity sequence names and options |
|
|
68
87
|
| Schemas + search_path + pg_catalog / information_schema | VERIFIED | Catalog columns are the commonly-queried subset |
|
|
69
88
|
| Enums, domains, generated columns | VERIFIED | |
|
|
89
|
+
| `ALTER COLUMN SET / DROP NOT NULL`, `SET / DROP DEFAULT` | VERIFIED | PostgreSQL's errors for existing NULLs (`23502`), primary-key and identity columns, column references and subqueries in a default, and literals that are not valid input for the column type. Every action of one `ALTER TABLE` applies or none does. An undefined function in a default is reported when a row first uses it, not by `ALTER TABLE` (`tests/contract/alter-table/set-not-null.test.ts`, `set-default.test.ts`) |
|
|
90
|
+
| `information_schema.columns.column_default` | PARTIALLY VERIFIED | Rendered the way `pg_get_expr` prints the analyzed default: constants of every type, casts, SQL value functions, `nextval`, the built-in functions with a signature on record, operators, `ARRAY[...]`, `CASE`, `IN`, `BETWEEN`, `LIKE`. Functions outside that table print an untyped literal argument as `text`; `CURRENT_TIMESTAMP(n)`, `current_schema()` and unary `+` lose the detail the parser drops; a nested `CASE` is printed on one line. `pg_attrdef` and `pg_get_expr` are not exposed |
|
|
91
|
+
| `ALTER COLUMN SET STORAGE`, `STORAGE` column clause, `pg_attribute.attstorage` | VERIFIED | SQL and catalog contract only: `PLAIN` / `EXTERNAL` / `EXTENDED` / `MAIN` / `DEFAULT`, per-type defaults, `LIKE ... INCLUDING STORAGE`, PostgreSQL's errors for an unknown mode (`22023`), a non-toastable type (`0A000`) and a missing column. **No TOAST**: values are never compressed or moved out of line, so the mode changes nothing about how rows are held. `SET STORAGE` on a materialized view fails loud (`0A000`); `SET COMPRESSION` is unsupported (`tests/contract/alter-table/set-storage.test.ts`) |
|
|
92
|
+
| `CREATE UNLOGGED TABLE / SEQUENCE`, `SET LOGGED / UNLOGGED`, `pg_class.relpersistence` | VERIFIED | SQL and catalog contract only: `p` / `u` / `t` for tables, their indexes and owned sequences; contents, indexes and constraints are kept across a conversion; PostgreSQL's errors for temporary tables, for logged / unlogged / temporary foreign-key combinations (`42P16`) and for a second change in one statement. **No WAL and no crash recovery**: an unlogged table is stored exactly like a logged one, is not truncated after a crash, and is included in snapshots (`tests/contract/alter-table/set-logged.test.ts`) |
|
|
93
|
+
| Index identity: `pg_class` / `pg_index` / `pg_indexes` / `pg_am` / `pg_constraint.conindid` | VERIFIED | Every index is a relation with its own oid, distinct from its table, kept through `ALTER INDEX / TABLE / SCHEMA ... RENAME`, `SET SCHEMA`, transactions and snapshots; `DROP` + `CREATE` and `REINDEX CONCURRENTLY` make a new one. The index behind a `PRIMARY KEY` or `UNIQUE` constraint appears in all of them and follows `ADD / RENAME / DROP CONSTRAINT`; dropping it directly is `2BP01`. Snapshots written before indexes had oids load with distinct ones. `pg_index` has the flag columns only: `indexprs` / `indpred` are the text `pg_get_expr()` returns, and `indkey`, `indclass`, `indoption`, `indcollation` are absent. A constraint's index name must be free among the schema's relations (`42P07`), and a generated name steps past one that is taken (`t_pkey1`) (`tests/contract/catalogs/index-definitions.test.ts`) |
|
|
94
|
+
| `pg_get_indexdef(oid)`, `pg_get_indexdef(oid, column, pretty)`, `pg_indexes.indexdef` | VERIFIED | `UNIQUE`, `USING` method, `DESC` / `NULLS FIRST / LAST`, `INCLUDE`, non-default operator classes, `COLLATE`, `NULLS NOT DISTINCT`, `WITH (...)`, quoted identifiers (every non-unreserved keyword), and expressions and predicates printed as PostgreSQL prints the analyzed tree: typed constants, the implicit casts operator resolution adds, `= ANY (ARRAY[...])` for `IN`, `~~` for `LIKE`, expanded `BETWEEN`, flattened `AND` / `OR`, `CASE` layout, SQL-syntax functions. Column positions past the end or negative are `''`, an oid that is not an index is NULL, and pretty-printing drops the schema and redundant parentheses. Differential over a 127-index construct matrix; expressions outside it (subqueries, row constructors, `SIMILAR TO`, array slices) are best-effort. Access methods other than btree are recorded and rendered, not implemented differently (`tests/contract/catalogs/index-definitions.test.ts`) |
|
|
95
|
+
| `pg_index.indisvalid`, `CREATE / DROP INDEX CONCURRENTLY`, `REINDEX` | VERIFIED | Completed indexes are valid, ready and live. A concurrent unique build that hits duplicates fails (`23505`) and leaves the index in the catalog, invalid and not enforced; `DROP INDEX` removes it and `REINDEX INDEX / TABLE / SCHEMA / DATABASE` repairs it once the data allows; a failed `REINDEX INDEX CONCURRENTLY` leaves `name_ccnew`. The commands PostgreSQL refuses in a transaction block are `25001`. An *interrupted* build (cancel, deadlock) cannot be provoked in the oracle: `Database.fault({ concurrentIndexBuild: "57014" })` and `server.fault({ failConcurrentIndexBuild })` arm one for tests, in the build-phase state (`indisready = false`); the state a failure in the later validation scan leaves (`indisready = true`, still enforcing) is not modelled (divergence `concurrent-index-build-fault`). `REINDEX` has no physical index to rebuild (`tests/contract/catalogs/index-validity.test.ts`, `tests/wire/index-catalog.test.ts`) |
|
|
96
|
+
| `COMMENT ON INDEX`, `pg_description`, `obj_description` | VERIFIED | Stored on the index: replace, clear with `NULL` or `''`, removed with the index / table / schema, kept through renames, transactional, in snapshots, isolated per wire transaction; `42P01` / `3F000` / `42809` for a missing index, a missing schema, a relation that is not an index. Comments on every other kind of object are still accepted and not stored (divergence `comment-on-not-stored`) (`tests/contract/catalogs/index-comments.test.ts`) |
|
|
97
|
+
| `regnamespace`, `to_regnamespace` | VERIFIED | Input (`-`, an all-digit oid, or exactly one identifier, case-folded unless quoted), output (quoted like `quote_ident`, numeric when no schema has the oid, `-` for zero), casts from text / name / oid / integers, implicit use as `oid` in comparisons, `pg_toast` / `pg_catalog` / `information_schema`; `3F000`, `42602` and `22003` as PostgreSQL raises them. `public` has a generated oid, not 2200 (`tests/contract/catalogs/regnamespace.test.ts`) |
|
|
98
|
+
| `pg_stat_user_tables`, `pg_stat_user_indexes`, `ANALYZE` | PARTIALLY VERIFIED | PostgreSQL 18's columns and types; one row per user table and materialized view (and per index), `relid` joins `pg_class.oid`, follows create / drop / rename / schema moves, restored with snapshots. `n_live_tup` is the exact row count, immediately; `ANALYZE` validates its targets (`42P01`, `42703`, `42701`, `42601`) and records `analyze_count` / `last_analyze` from the database clock. Not tracked (read zero / NULL): scans, tuples inserted / updated / deleted, dead tuples, vacuum; `idx_scan` counts only the unique-key lookups made for `SELECT`. `VACUUM ANALYZE` does not count. Temporary tables are listed under the schema they were created in, not `pg_temp` (divergence `pg-stat-counters-immediate`) (`tests/contract/catalogs/table-statistics.test.ts`) |
|
|
70
99
|
| Triggers (row-level, LANGUAGE sql-expressible) | PARTIALLY VERIFIED | Creation-order firing, `UPDATE OF` ignored, no INSTEAD OF (documented) |
|
|
71
100
|
| CREATE FUNCTION LANGUAGE sql / plpgsql-lite | PARTIALLY VERIFIED | Scalar + set-returning; plpgsql subset: DECLARE, EXCEPTION WHEN others, CASE, FOR-IN-SELECT, RETURN NEXT |
|
|
72
101
|
| Text search (tsvector / tsquery / @@ / ts_rank) | PARTIALLY VERIFIED | `simple`-style config; no ispell/synonym dictionaries |
|
|
73
102
|
| `CREATE EXTENSION pgcrypto` / `digest()` | PARTIALLY VERIFIED | `digest(bytea, text)` and `digest(text, text)` for md5, sha1, sha224, sha256, sha384, sha512. `crypt`, `hmac`, `gen_salt`, and PGP functions are not installed. OpenSSL-only names (sha3, blake2, ripemd160, sm3) are not available. Extensions other than `pgcrypto` and `pg_trgm` still fail loud (`0A000`) |
|
|
103
|
+
| `pg_available_extensions`, `pg_available_extension_versions`, `pg_extension` | VERIFIED | List exactly what `CREATE EXTENSION` can install here: `pgcrypto` (1.3, 1.4), `pg_trgm` (1.3 to 1.6) and the built-in `plpgsql` (1.0), with `installed_version`, schema and version following `CREATE` / `DROP EXTENSION`, rollback and snapshots. A stock PostgreSQL server lists every contrib module it ships; those are absent here (zero rows, and `CREATE EXTENSION` is `0A000`). An older version installs the same partial function set as the default one. `DROP EXTENSION` reports dependents (`2BP01`) and `CASCADE` removes dependent defaults, generated columns, CHECK constraints, views and indexes; `DROP EXTENSION plpgsql` and `ALTER EXTENSION` fail loud (`0A000`) (`tests/contract/extensions/catalogs.test.ts`) |
|
|
74
104
|
| `CREATE EXTENSION pg_trgm` | PARTIALLY VERIFIED | `similarity(text, text)`, `<%` at the default word-similarity threshold 0.6, and a single-column `gin_trgm_ops` index on `text` or `varchar`. `gist_trgm_ops`, multicolumn `gin_trgm_ops`, and non-default thresholds are not installed. |
|
|
75
105
|
| COPY FROM/TO (text, csv) | VERIFIED | Via `copyFrom` API hook / rows out |
|
|
76
106
|
| PREPARE / EXECUTE / DEALLOCATE, SET / SHOW / RESET | VERIFIED | GUC subset, including PostgreSQL dump and migration-client preambles |
|
|
@@ -79,9 +109,9 @@ The oracle exposes **2787 builtin functions** and **74 operators** in `pg_catalo
|
|
|
79
109
|
| Regex (`~`, `~*`, POSIX functions) | PARTIALLY VERIFIED | JS regex flavor mapped to POSIX ERE; documented edges |
|
|
80
110
|
| EXPLAIN | PARTIALLY VERIFIED | Stub plan shapes |
|
|
81
111
|
| MERGE / CALL / cursors / LISTEN / full PL/pgSQL | UNSUPPORTED | Fail loud `0A000`, registered |
|
|
82
|
-
| Roles / GRANT / VACUUM /
|
|
83
|
-
| Wire protocol / multi-session concurrency | PARTIALLY VERIFIED | `READ COMMITTED` workspaces; `FOR UPDATE`, `NOWAIT`, `SKIP LOCKED`, and deadlock detection |
|
|
84
|
-
| On-disk PostgreSQL format | NOT APPLICABLE | PGMM snapshots are the persistence format |
|
|
112
|
+
| Roles / GRANT / VACUUM / LOCK | NOT APPLICABLE | Parsed no-ops where harmless |
|
|
113
|
+
| Wire protocol / multi-session concurrency | PARTIALLY VERIFIED | `READ COMMITTED` workspaces; `FOR UPDATE`, `NOWAIT`, `SKIP LOCKED`, and deadlock detection; per-connection settings and startup `options` / run-time parameters |
|
|
114
|
+
| On-disk PostgreSQL format | NOT APPLICABLE | PGMM snapshots are the persistence format, also for the wire server's opt-in durable mode (snapshot persistence, not WAL) |
|
|
85
115
|
|
|
86
116
|
## How to verify
|
|
87
117
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
the
|
|
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
|
|
90
|
-
test (microseconds, tables are shared until either side
|
|
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
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
142
|
+
### Code written against `pg`
|
|
126
143
|
|
|
127
|
-
|
|
128
|
-
`
|
|
129
|
-
|
|
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
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
`
|
|
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
|
|
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
|
|
@@ -185,6 +204,7 @@ Or from the command line (installs a `mockingbird-postgres` bin):
|
|
|
185
204
|
```bash
|
|
186
205
|
mockingbird-postgres serve --port 55432 # trust auth
|
|
187
206
|
mockingbird-postgres serve postgres://postgres:secret@0.0.0.0:55432/app --log
|
|
207
|
+
mockingbird-postgres serve --port 55432 --durable ./.postgres # commits survive a restart
|
|
188
208
|
```
|
|
189
209
|
|
|
190
210
|
`serve({ database })` shares an existing `Database`, and `serve({ database: snapshot })` boots every
|
|
@@ -202,7 +222,10 @@ return a conflict until closed.
|
|
|
202
222
|
|
|
203
223
|
`server.snapshot()` freezes the live state; `server.fault({ dropConnection | delayStatementMs |
|
|
204
224
|
failCommit })` arms the next statement or connection for a drop, a delay, or a `40001` commit
|
|
205
|
-
failure.
|
|
225
|
+
failure. `server.fault({ failConcurrentIndexBuild: "57014" })` (and `database.fault({
|
|
226
|
+
concurrentIndexBuild: "57014" })` on the sync API) interrupts the next `CREATE INDEX CONCURRENTLY`
|
|
227
|
+
or `REINDEX ... CONCURRENTLY` with that SQLSTATE and, as PostgreSQL does, leaves the index it was
|
|
228
|
+
building in the catalog with `pg_index.indisvalid = false` until it is dropped or reindexed.
|
|
206
229
|
|
|
207
230
|
One committed engine is shared by every connection. Explicit transactions execute in per-session
|
|
208
231
|
copy-on-write workspaces that rebase each statement on the latest committed state, providing
|
|
@@ -222,20 +245,155 @@ the wire. `COPY … FROM STDIN` and `COPY … TO STDOUT` use protocol-v3 `CopyIn
|
|
|
222
245
|
incrementally across arbitrary frame and quoted-record boundaries; failed imports are atomic and
|
|
223
246
|
leave the connection in PostgreSQL's transaction state.
|
|
224
247
|
|
|
248
|
+
**Session settings and startup parameters.** Every connection has its own settings: a `SET`,
|
|
249
|
+
`RESET` or `set_config()` on one never reaches another, a `SET` inside a transaction block is
|
|
250
|
+
undone by `ROLLBACK`, and a new connection starts from the database's settings, not a neighbour's.
|
|
251
|
+
The startup packet's run-time parameters are applied before the first query, the way a PostgreSQL
|
|
252
|
+
backend applies them: libpq's `options` string first, then every parameter sent by name (which
|
|
253
|
+
therefore wins).
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import pg from "pg"
|
|
257
|
+
import { serve } from "@crvouga/mockingbird-service-postgres/wire"
|
|
258
|
+
|
|
259
|
+
const server = await serve({ port: 0 })
|
|
260
|
+
const client = new pg.Client({
|
|
261
|
+
connectionString: `${server.connectionString}?options=-c%20search_path%3Dapp`,
|
|
262
|
+
})
|
|
263
|
+
await client.connect()
|
|
264
|
+
await client.query("CREATE SCHEMA app")
|
|
265
|
+
await client.query("CREATE TABLE app.notes (id integer)")
|
|
266
|
+
await client.query("SET search_path TO app")
|
|
267
|
+
await client.end()
|
|
268
|
+
await server.close()
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Other clients set the same startup option with their own configuration (for example, JDBC's
|
|
272
|
+
`currentSchema` or postgres.js / Bun SQL's `connection.search_path`).
|
|
273
|
+
|
|
274
|
+
`options` is split like `pg_split_opts` (whitespace separates arguments, `\ ` is a literal space,
|
|
275
|
+
`\\` a backslash) and accepts `-c name=value`, `-cname=value` and `--name=value` (dashes in a
|
|
276
|
+
`--name` are underscores). A schema named in `search_path` need not exist yet: it starts resolving
|
|
277
|
+
once it is created. Startup values are the session's defaults, so `RESET search_path`,
|
|
278
|
+
`SET search_path TO DEFAULT` and `RESET ALL` return to them. Invalid input ends the connection
|
|
279
|
+
with a `FATAL` ErrorResponse, as PostgreSQL 18.3 does: `42601` for a malformed `options` string
|
|
280
|
+
(`-c name` without a value, a stray argument, an unknown switch), `42704` for an unknown
|
|
281
|
+
parameter, `22023` for a bad value (`search_path` list syntax, booleans, integers and timeouts with
|
|
282
|
+
units, enum parameters, `DateStyle`, `TimeZone`) and `55P02` for a read-only parameter such as
|
|
283
|
+
`server_version`. The session-aware functions (`pg_advisory_lock`, `pg_backend_pid`, `pg_notify`,
|
|
284
|
+
…) live in `pg_catalog`, so they keep working when `search_path` leaves out `public`.
|
|
285
|
+
|
|
286
|
+
Boundaries: a parameter is known when the engine has a setting for it (the same rule as `SET`), so
|
|
287
|
+
a real PostgreSQL parameter the engine does not model, such as `work_mem` or `jit`, is `42704`
|
|
288
|
+
at startup too; names with a dot are custom parameters and always accepted. The other `postgres`
|
|
289
|
+
switches (`-e`, `-S`, `-d`, …) fail loud with `0A000`. `client_encoding`, `lc_*` and
|
|
290
|
+
`application_name` values are stored as sent, an unquoted upper-case schema in a startup
|
|
291
|
+
`search_path` is shown folded to lower case, and `ParameterStatus` is sent at startup only, not
|
|
292
|
+
again when a reported setting changes. `SET LOCAL` and `DISCARD ALL` keep their engine behaviour
|
|
293
|
+
(see [COMPATIBILITY.md](COMPATIBILITY.md)).
|
|
294
|
+
|
|
225
295
|
**Not modelled by the server:** transaction ID and snapshot-inspection functions, predicate and
|
|
226
296
|
table locks, isolation levels beyond `READ COMMITTED`, and the binary parameter formats beyond the
|
|
227
297
|
common scalar types (a client that sends another binary type gets `0A000`, and can switch that
|
|
228
298
|
parameter to text). Binary `COPY` is not supported.
|
|
229
299
|
|
|
300
|
+
#### Durable mode (opt-in)
|
|
301
|
+
|
|
302
|
+
The server keeps everything in memory, and that stays the default: no option, no filesystem, the
|
|
303
|
+
same bytes on every run. For local development, where the server replaces a native database and
|
|
304
|
+
a killed process must not lose acknowledged work, `durable` persists the cluster before each
|
|
305
|
+
commit is acknowledged:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import { serve } from "@crvouga/mockingbird-service-postgres/wire"
|
|
309
|
+
|
|
310
|
+
const server = await serve({ port: 55432, durable: "./.postgres" }) // a directory
|
|
311
|
+
// … the process is SIGKILLed …
|
|
312
|
+
const again = await serve({ port: 55432, durable: "./.postgres" }) // resumes from the last acknowledged commit
|
|
313
|
+
await again.close()
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
| | Guarantee |
|
|
317
|
+
| --- | --- |
|
|
318
|
+
| Acknowledged commit | An autocommit write, a `COMMIT`, the implicit commit of a multi-statement query, a `COPY … FROM STDIN` and `CREATE` / `DROP` / `ALTER DATABASE` are in storage before `CommandComplete` (for an implicit commit, before `ReadyForQuery`) is sent. They survive `SIGKILL`. |
|
|
319
|
+
| Uncommitted work | A transaction that was open or rolled back is never restored: it only ever existed in its session's workspace. |
|
|
320
|
+
| Storage failure | The commit is not acknowledged. The statement fails with `58030` (`io_error`), or `53100` (`disk_full`) when the storage reports `ENOSPC` / `EDQUOT`; the change is taken back out of memory, the transaction is gone (as after any failed `COMMIT`: the session is idle, not in an aborted block), its `NOTIFY`s are dropped, and the next statement runs normally. |
|
|
321
|
+
| Atomic | The file-backed store writes each file under a temporary name, fsyncs it, renames it into place and fsyncs the directory; the manifest swap is the commit point. A crash leaves the previous state or the new one. Leftovers of an interrupted write are removed on the next start, and a snapshot whose length or SHA-256 does not match its manifest entry is refused, never decoded. |
|
|
322
|
+
| Whole cluster | Every database with its catalog, data, sequences and deterministic random position. A restart restores all of them; separate directories (or storages) are separate clusters. |
|
|
323
|
+
| Visibility | A commit being persisted holds the statement turn, so no other connection reads it before it is durable. |
|
|
324
|
+
|
|
325
|
+
It is **snapshot persistence, not a write-ahead log, and not PostgreSQL's on-disk format**: the
|
|
326
|
+
payload is the [PGMM snapshot](#snapshots) of each database (`<sha256>.pgmm`)
|
|
327
|
+
plus a `MANIFEST.json` naming the current one per database. Nothing reads a PostgreSQL data
|
|
328
|
+
directory, and `pg_dump`, physical replication and point-in-time recovery do not apply.
|
|
329
|
+
|
|
330
|
+
The trade-off is cost per commit: every commit re-encodes the database it changed (other
|
|
331
|
+
databases are not rewritten) and then waits for two file fsyncs and two directory fsyncs. That is
|
|
332
|
+
proportional to the size of that database, not to the size of the change: about 10 ms for a
|
|
333
|
+
200 KB snapshot and several hundred milliseconds for 2 MB (100 000 short rows) on a laptop. It
|
|
334
|
+
suits development datasets; batch bulk writes into one transaction (one `COMMIT`, one encode), and
|
|
335
|
+
keep the in-memory default for tests. Reads, `SET` and statements inside an open transaction
|
|
336
|
+
never touch storage; a statement that could have written (a `SELECT` calling a user-defined
|
|
337
|
+
function, a view or `nextval`) is encoded and compared, and written only if the snapshot differs.
|
|
338
|
+
`fsync` is as strong as the platform makes it: surviving a killed process does not depend on it,
|
|
339
|
+
surviving power loss does.
|
|
340
|
+
|
|
341
|
+
Boundaries of durable mode:
|
|
342
|
+
|
|
343
|
+
- One server process per directory; nothing locks it.
|
|
344
|
+
- `database` seeds an empty storage (the seed is persisted at startup). Once storage holds a
|
|
345
|
+
cluster, that cluster is served and `database` only supplies the runtime: clock, seed, entropy
|
|
346
|
+
mode and `int8` mode. Read state through `server.database`, not the object you passed.
|
|
347
|
+
- What a PGMM snapshot does not hold is not restored: functions added with `registerFunction`,
|
|
348
|
+
session state (settings, prepared statements, advisory locks, `LISTEN`s), and faults.
|
|
349
|
+
- Writes made from the host process (`server.database.exec(...)`, `restoreAll()`) are not commits
|
|
350
|
+
of a connection: call `await server.persist()` to store them. A host-side read during a commit
|
|
351
|
+
can see it before it is durable.
|
|
352
|
+
- `nextval()` and `setval()` are persisted at once. The random position consumed by a statement
|
|
353
|
+
that wrote nothing (`SELECT gen_random_uuid()`) is stored with the next commit of that database.
|
|
354
|
+
- `mockingbird-postgres serve --config` (the fleet) has no durable option.
|
|
355
|
+
|
|
356
|
+
The storage is a port, so a test can model failure and crashes without a filesystem:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import { type ClusterImage, type DurableStorage, fileStorage, serve } from "@crvouga/mockingbird-service-postgres/wire"
|
|
360
|
+
|
|
361
|
+
let image: ClusterImage | null = null
|
|
362
|
+
let failing = false
|
|
363
|
+
const paused = Promise.resolve()
|
|
364
|
+
const storage: DurableStorage = {
|
|
365
|
+
read: async () => image,
|
|
366
|
+
write: async (next) => {
|
|
367
|
+
if (failing) throw Object.assign(new Error("disk full"), { code: "ENOSPC" }) // → 53100, not acknowledged
|
|
368
|
+
image = new Map(next)
|
|
369
|
+
},
|
|
370
|
+
}
|
|
371
|
+
const server = await serve({
|
|
372
|
+
durable: {
|
|
373
|
+
storage, // or a directory, or fileStorage(directory)
|
|
374
|
+
beforeAcknowledge: (commit) => paused, // persisted, not acknowledged yet: crash here
|
|
375
|
+
afterAcknowledge: (commit) => paused, // the acknowledgement is on the socket: crash here
|
|
376
|
+
},
|
|
377
|
+
})
|
|
378
|
+
const restarted = await serve({ durable: storage }) // "restart" from what storage holds
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
`write` receives the whole cluster as PGMM bytes per database and must be atomic: resolve once the
|
|
382
|
+
image would survive a kill, or reject and leave the previous image in place. The server never
|
|
383
|
+
calls it concurrently and passes the same `Uint8Array` for a database that did not change.
|
|
384
|
+
`beforeAcknowledge` / `afterAcknowledge` receive `{ pid, database, sql }` and are awaited, which
|
|
385
|
+
is how a crash-regression test stops the server on either side of the acknowledgement.
|
|
386
|
+
|
|
230
387
|
### Method semantics
|
|
231
388
|
|
|
232
389
|
| Method | Behaviour |
|
|
233
390
|
| --- | --- |
|
|
234
391
|
| `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. |
|
|
235
|
-
| `registerFunction(spec)` | Install a JavaScript scalar. Not stored in PGMM snapshots; `open()` of a live snapshot copies the implementation by reference. |
|
|
392
|
+
| `registerFunction(spec)` | Install a JavaScript scalar. Not stored in PGMM snapshots; `open()` of a live snapshot copies the implementation by reference. A `pg_catalog.`-qualified name is found ahead of `search_path`, as PostgreSQL's own functions are. |
|
|
236
393
|
| `query(sql, params?, { at? }?)` | **Single statement only** (trailing `;` is fine). Returns all rows. `at` is a `Snapshot` or a timeline checkpoint id and queries that checkpoint without changing live state. Multi-statement scripts throw `misuse`. |
|
|
237
394
|
| `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. |
|
|
238
395
|
| `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`. |
|
|
396
|
+
| `fault({ concurrentIndexBuild })` | Test control: arm the next `CREATE INDEX CONCURRENTLY` / `REINDEX ... CONCURRENTLY` to fail with the given SQLSTATE (`"57014"` canceled, `"40P01"` deadlock) and leave its index invalid (`pg_index.indisvalid = false`), as an interrupted build does on PostgreSQL. One shot; not part of snapshots. |
|
|
239
397
|
| `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`. |
|
|
240
398
|
| `snapshot()` | Freeze a reusable `Snapshot` template (no encode) and commit it on the followed timeline branch. Illegal inside a transaction (`25P01`). |
|
|
241
399
|
| `checkpoint()` / `branch(at?)` | `checkpoint` records the same timeline point as `snapshot`. `branch` opens an isolated database from a snapshot (or the current state). |
|
|
@@ -335,12 +493,16 @@ Intentional differences: custom `PGMM` snapshots; seeded `random()` / fixed `now
|
|
|
335
493
|
- `EXPLAIN`: stub plan shapes, not real planner output
|
|
336
494
|
- The in-process API does **not** poison a transaction after a failed statement (no `25P02`); the wire-protocol server does, per connection
|
|
337
495
|
- Triggers fire in **creation order** (PostgreSQL: name order); `UPDATE OF` column lists are ignored; `INSTEAD OF` is unsupported
|
|
338
|
-
- `COMMENT ON`
|
|
496
|
+
- `COMMENT ON INDEX` is stored (`pg_description`, `obj_description`); comments on every other kind of object parse but are not stored
|
|
339
497
|
- `round(float8)` rounds ties away from zero (PostgreSQL: half-to-even); numeric `round()` has full parity
|
|
340
498
|
- `'1e400'::float8` saturates to `Infinity` instead of raising `22003`
|
|
341
499
|
- In the engine `MERGE`, `CALL`/procedures, cursors (`DECLARE`/`FETCH`), `LISTEN`/`NOTIFY`, and full PL/pgSQL (packages, NOTICE, cursors) fail loud (`0A000`); the wire-protocol server implements `LISTEN`/`NOTIFY` itself
|
|
342
|
-
- `VACUUM` / `
|
|
500
|
+
- `VACUUM` / `CLUSTER` / `CHECKPOINT` / `GRANT` / `REVOKE` / `LOCK` are parsed no-ops. `ANALYZE` validates its targets and records `analyze_count` / `last_analyze`, and `REINDEX` validates its targets and repairs an invalid index; neither has planner statistics or a physical index behind it
|
|
501
|
+
- `pg_stat_user_tables` / `pg_stat_user_indexes` have PostgreSQL 18's columns, but only `n_live_tup` (the exact row count, immediately), the `ANALYZE` bookkeeping and `idx_scan` for unique-key lookups are real; the other activity counters read zero
|
|
502
|
+
- Index access methods other than btree (`USING hash` / `gin` / …) are recorded and shown by `pg_get_indexdef` and `pg_am`, not implemented differently; `pg_index` has no `indkey` (use `pg_get_indexdef(oid, n, pretty)`)
|
|
343
503
|
- Collation is `C` semantics (byte order); locale/ICU-dependent ordering is out of scope
|
|
504
|
+
- Column `STORAGE` modes (`pg_attribute.attstorage`) and `UNLOGGED` persistence (`pg_class.relpersistence`) follow PostgreSQL's SQL and catalog contract but are metadata only: there is no TOAST and no WAL, so an unlogged table is stored, snapshotted and kept like any other
|
|
505
|
+
- `pg_available_extensions` / `pg_extension` list only what `CREATE EXTENSION` can install here (`pgcrypto`, `pg_trgm`) plus the built-in `plpgsql`; any other extension is absent from the views and `CREATE EXTENSION` fails with `0A000`
|
|
344
506
|
|
|
345
507
|
**Also supported (oracle parity):** schemas + `search_path`, `pg_catalog` / `information_schema`
|
|
346
508
|
introspection, sequences (`serial`, identity, `nextval`/`currval`/`setval`), enums, domains,
|
package/compat/coverage.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-
|
|
2
|
+
"generatedAt": "2026-10-11T00:19:02.759Z",
|
|
3
3
|
"referenceServerVersion": "18.3",
|
|
4
4
|
"counts": {
|
|
5
5
|
"total": 183,
|
|
@@ -251,9 +251,9 @@
|
|
|
251
251
|
"sql-analyze": {
|
|
252
252
|
"status": "PARTIALLY_VERIFIED",
|
|
253
253
|
"evidence": [
|
|
254
|
-
"tests/contract/
|
|
254
|
+
"tests/contract/catalogs/"
|
|
255
255
|
],
|
|
256
|
-
"notes": "
|
|
256
|
+
"notes": "targets, columns and options are validated and pg_stat_user_tables records analyze_count / last_analyze; no planner statistics are gathered (divergence pg-stat-counters-immediate)"
|
|
257
257
|
},
|
|
258
258
|
"sql-begin": {
|
|
259
259
|
"status": "VERIFIED",
|
|
@@ -289,9 +289,10 @@
|
|
|
289
289
|
"sql-comment": {
|
|
290
290
|
"status": "PARTIALLY_VERIFIED",
|
|
291
291
|
"evidence": [
|
|
292
|
-
"tests/contract/catalog/"
|
|
292
|
+
"tests/contract/catalog/",
|
|
293
|
+
"tests/contract/catalogs/"
|
|
293
294
|
],
|
|
294
|
-
"notes": "
|
|
295
|
+
"notes": "COMMENT ON INDEX is stored (pg_description, obj_description); comments on other objects are accepted and not stored (divergence comment-on-not-stored)"
|
|
295
296
|
},
|
|
296
297
|
"sql-commit": {
|
|
297
298
|
"status": "VERIFIED",
|
|
@@ -924,9 +925,9 @@
|
|
|
924
925
|
"sql-reindex": {
|
|
925
926
|
"status": "PARTIALLY_VERIFIED",
|
|
926
927
|
"evidence": [
|
|
927
|
-
"tests/contract/
|
|
928
|
+
"tests/contract/catalogs/"
|
|
928
929
|
],
|
|
929
|
-
"notes": "
|
|
930
|
+
"notes": "INDEX / TABLE / SCHEMA / DATABASE / SYSTEM with CONCURRENTLY: targets are validated, a unique index is re-checked, an invalid index is repaired and a failed concurrent rebuild leaves its _ccnew index; there is no physical index to rebuild and TABLESPACE is ignored"
|
|
930
931
|
},
|
|
931
932
|
"sql-release-savepoint": {
|
|
932
933
|
"status": "VERIFIED",
|
package/compat/divergences.json
CHANGED
|
@@ -145,8 +145,8 @@
|
|
|
145
145
|
{
|
|
146
146
|
"id": "comment-on-not-stored",
|
|
147
147
|
"scope": "sql",
|
|
148
|
-
"predicate": "COMMENT ON parses and succeeds but the comment is not stored; obj_description()
|
|
149
|
-
"specifiedBehavior": "COMMENT ON is accepted for compatibility;
|
|
148
|
+
"predicate": "COMMENT ON for any object other than an index parses and succeeds but the comment is not stored; obj_description() / col_description() return NULL for it and pg_description has no row",
|
|
149
|
+
"specifiedBehavior": "COMMENT ON INDEX is persisted (pg_description, obj_description) and differential-tested. For tables, columns and every other object kind COMMENT ON is accepted for compatibility and the description is not persisted; a missing target of those kinds is not an error either.",
|
|
150
150
|
"pinnedBy": ["DDL-comment-01"]
|
|
151
151
|
},
|
|
152
152
|
{
|
|
@@ -190,6 +190,41 @@
|
|
|
190
190
|
"predicate": "pg_get_viewdef() is not implemented and raises 42883",
|
|
191
191
|
"specifiedBehavior": "View definitions are visible via information_schema.views.view_definition (stored SQL text).",
|
|
192
192
|
"pinnedBy": ["CAT-viewdef-01"]
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"id": "storage-persistence-catalog-only",
|
|
196
|
+
"scope": "sql",
|
|
197
|
+
"predicate": "Column storage modes (STORAGE / SET STORAGE, pg_attribute.attstorage) and relation persistence (UNLOGGED, SET LOGGED / UNLOGGED, pg_class.relpersistence) are catalog metadata only: there is no TOAST and no write-ahead log",
|
|
198
|
+
"specifiedBehavior": "The SQL and catalog contract matches PostgreSQL, including its errors for invalid modes, targets and conversions, and is differential-tested. Physically nothing changes: values are never compressed or moved out of line, an unlogged table is stored like a logged one, is not truncated by a crash and is part of every snapshot. PostgreSQL also keeps a storage mode for materialized-view columns; here ALTER COLUMN ... SET STORAGE on a materialized view fails loud with 0A000.",
|
|
199
|
+
"pinnedBy": ["tests/contract/alter-table/set-storage.test.ts", "tests/contract/alter-table/set-logged.test.ts"]
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"id": "extension-catalog-subset",
|
|
203
|
+
"scope": "catalog",
|
|
204
|
+
"predicate": "pg_available_extensions, pg_available_extension_versions and pg_extension list only the extensions the engine can install (pgcrypto, pg_trgm) plus the built-in plpgsql; DROP EXTENSION plpgsql is refused with 0A000",
|
|
205
|
+
"specifiedBehavior": "A stock PostgreSQL server lists every contrib module it ships; here an extension that is not implemented is absent from the views (zero rows) and CREATE EXTENSION fails with 0A000, as it does on a server without that control file. The PGlite oracle is built with exactly the same three extensions, so the views are compared in full. plpgsql cannot be dropped because the language is part of the engine; PostgreSQL allows dropping and recreating it.",
|
|
206
|
+
"pinnedBy": ["tests/contract/extensions/catalogs.test.ts"]
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"id": "concurrent-index-build-fault",
|
|
210
|
+
"scope": "engine",
|
|
211
|
+
"predicate": "Database.fault({ concurrentIndexBuild: sqlstate }) and, on the wire server, server.fault({ failConcurrentIndexBuild: sqlstate }) make the next CREATE INDEX CONCURRENTLY or REINDEX ... CONCURRENTLY fail with that SQLSTATE and leave the index it was building in the catalog with indisvalid = false, indisready = false, indislive = true",
|
|
212
|
+
"specifiedBehavior": "PostgreSQL leaves such an invalid index when a concurrent build is canceled, deadlocks or otherwise fails partway (sql-createindex, 'Building Indexes Concurrently'); an interruption cannot be provoked deterministically in the oracle, so this one-shot test control stands in for it. The failure PostgreSQL can be made to produce, duplicate keys in a concurrent unique build, is differential-tested, as are DROP INDEX and REINDEX as the repair. The preset models an interruption in the build phase, the state PostgreSQL shows for the duplicate-key failure (not ready, so not maintained and not enforcing). A failure in the later validation scan, where PostgreSQL leaves indisready = true and a unique index keeps enforcing, is not modeled. The fault is not part of snapshots and is not undone by a rollback.",
|
|
213
|
+
"pinnedBy": ["CAT-index-valid-07", "tests/contract/catalogs/index-validity.test.ts", "tests/wire/index-catalog.test.ts"]
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"id": "pg-stat-counters-immediate",
|
|
217
|
+
"scope": "catalog",
|
|
218
|
+
"predicate": "pg_stat_user_tables.n_live_tup is the exact current row count, current as soon as a statement finishes; seq_scan, seq_tup_read, idx_tup_fetch, n_tup_ins / n_tup_upd / n_tup_del, n_tup_hot_upd, n_dead_tup, n_mod_since_analyze and the vacuum columns are not tracked and read zero or NULL; idx_scan (also in pg_stat_user_indexes) counts only the unique-key equality lookups the engine performs for SELECT; VACUUM, including VACUUM ANALYZE, is a no-op and does not count",
|
|
219
|
+
"specifiedBehavior": "PostgreSQL reports these counters asynchronously (after pg_stat_force_next_flush() or a delay) and n_live_tup is an estimate that ANALYZE resets. The differential tests read after pg_stat_force_next_flush() and ANALYZE, where PostgreSQL's value is the row count too, and compare relid, schemaname, relname, n_live_tup, analyze_count, last_analyze and the column list and types of both views. There are no dead tuples and no planner, so the activity counters have nothing to count; treat them as absent, not as zero activity.",
|
|
220
|
+
"pinnedBy": ["CAT-table-stat-04", "tests/contract/catalogs/table-statistics.test.ts"]
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"id": "pg-index-catalog-subset",
|
|
224
|
+
"scope": "catalog",
|
|
225
|
+
"predicate": "pg_index exposes indexrelid, indrelid, indnatts, indnkeyatts and the boolean flags; indexprs and indpred are the deparsed text that pg_get_expr() returns rather than pg_node_tree, and indkey, indcollation, indclass and indoption are absent. Index access methods other than btree are recorded and rendered (USING hash / gin / gist / spgist / brin) but every index is maintained the same way",
|
|
226
|
+
"specifiedBehavior": "pg_get_indexdef(), pg_indexes.indexdef and pg_get_expr(indpred | indexprs, indrelid) are differential-tested against PostgreSQL across methods, ordering, NULLS placement, INCLUDE, operator classes, collations, storage parameters, quoted identifiers and an expression / predicate matrix. Read index columns through pg_get_indexdef(oid, n, pretty) instead of indkey. Expressions outside that matrix (subqueries, row constructors, SIMILAR TO, array slices, window or aggregate calls) are rendered best-effort.",
|
|
227
|
+
"pinnedBy": ["tests/contract/catalogs/index-definitions.test.ts"]
|
|
193
228
|
}
|
|
194
229
|
]
|
|
195
230
|
}
|
package/compat/requirements.json
CHANGED