@crvouga/mockingbird-service-postgres 1.5.0 → 1.6.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 +5 -6
- package/CHANGELOG.md +17 -1
- package/COMPATIBILITY.md +10 -5
- package/README.md +23 -13
- package/dist/admin.js +129 -43
- package/dist/admin.js.map +4 -4
- package/dist/ast/nodes.d.ts +1 -0
- package/dist/index.js +118 -32
- package/dist/index.js.map +3 -3
- package/dist/runtime/database-context.d.ts +4 -0
- package/dist/sql/deparse.d.ts +1 -0
- package/dist/unstable.js +96 -31
- package/dist/unstable.js.map +3 -3
- package/dist/wire/cli.js +586 -76
- package/dist/wire/cli.js.map +3 -3
- package/dist/wire/cluster.d.ts +20 -8
- package/dist/wire/connection.d.ts +25 -1
- package/dist/wire/index.d.ts +5 -1
- package/dist/wire/index.js +586 -76
- package/dist/wire/index.js.map +3 -3
- package/dist/wire/protocol.d.ts +4 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -13,12 +13,11 @@ Guidance for humans and coding agents editing this repository. For install and c
|
|
|
13
13
|
3. Single-session **engine**: no MVCC across connections, no aborted-transaction (`25P02`) state in the sync API
|
|
14
14
|
4. `NOT APPLICABLE` items: roles/auth enforcement, replication, VACUUM internals, storage params, full PL/pgSQL
|
|
15
15
|
|
|
16
|
-
**Non-goals:** matching `pg`/`postgres.js` **client APIs**, full PL/pgSQL, or MVCC
|
|
17
|
-
wire-protocol server (`src/wire/`, a Node/Bun-only entry over the one engine) is
|
|
18
|
-
|
|
19
|
-
advisory locks, `LISTEN`/`NOTIFY`, `CancelRequest`
|
|
20
|
-
|
|
21
|
-
streaming.
|
|
16
|
+
**Non-goals:** matching `pg`/`postgres.js` **client APIs**, full PL/pgSQL, or MVCC in the synchronous
|
|
17
|
+
API. The optional wire-protocol server (`src/wire/`, a Node/Bun-only entry over the one engine) is
|
|
18
|
+
in scope: it coordinates per-session `READ COMMITTED` workspaces and adds row locks (`FOR UPDATE`,
|
|
19
|
+
`NOWAIT`, `SKIP LOCKED`, deadlock detection), advisory locks, `LISTEN`/`NOTIFY`, `CancelRequest`
|
|
20
|
+
and per-connection `25P02`. `COPY` streaming remains an explicitly documented boundary.
|
|
22
21
|
|
|
23
22
|
## SQL pipeline
|
|
24
23
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
# Changelog — @crvouga/mockingbird-service-postgres
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1.6.0 (2026-10-01)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- add MVCC row locks ([9e199e0](https://github.com/crvouga/mockingbird/commit/9e199e083c3d16a9a2acfd7a2e9ab00ad733ef4f))
|
|
8
|
+
- stream COPY protocol ([9e7a867](https://github.com/crvouga/mockingbird/commit/9e7a8673206fd6dad1b170e1b3a31e183e5abb2f))
|
|
9
|
+
- add database catalogs ([c9b2c06](https://github.com/crvouga/mockingbird/commit/c9b2c060af4919135926c805412886177b4026a0))
|
|
10
|
+
- parse identity sequence names ([e7a5a03](https://github.com/crvouga/mockingbird/commit/e7a5a03f33e6d5a0dc28a34d30659bb128db7c73))
|
|
11
|
+
- accept tooling session GUCs ([0464416](https://github.com/crvouga/mockingbird/commit/04644162f7bbd5071e580cfb398b3da82a2a4952))
|
|
12
|
+
|
|
13
|
+
## 1.5.1 (2026-10-01)
|
|
14
|
+
|
|
15
|
+
### Fixes and improvements
|
|
16
|
+
|
|
17
|
+
- freeze clock before setting time ([a054a1d](https://github.com/crvouga/mockingbird/commit/a054a1defad62539d719b1d31c15107afea623bd))
|
|
18
|
+
|
|
19
|
+
## 1.5.0 (2026-09-30)
|
|
4
20
|
|
|
5
21
|
### Features
|
|
6
22
|
|
package/COMPATIBILITY.md
CHANGED
|
@@ -16,7 +16,11 @@ Differential tests compare a **tuple** per statement: rows (normalized to canoni
|
|
|
16
16
|
|
|
17
17
|
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 945 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.
|
|
18
18
|
|
|
19
|
-
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, …). Human-readable: [DIVERGENCES.md](DIVERGENCES.md).
|
|
19
|
+
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).
|
|
20
|
+
|
|
21
|
+
The wire server supports protocol-v3 text and CSV `COPY FROM STDIN` / `COPY TO STDOUT`, including
|
|
22
|
+
incremental `CopyData`, client aborts, and statement-atomic rollback. Binary COPY remains outside
|
|
23
|
+
the compatibility surface.
|
|
20
24
|
|
|
21
25
|
## Status vocabulary
|
|
22
26
|
|
|
@@ -33,7 +37,7 @@ Anything a PostgreSQL application can invoke through SQL against the PGlite **18
|
|
|
33
37
|
|
|
34
38
|
1. **Snapshot format** — custom binary codec (`PGMM`), not `pg_dump` / on-disk clusters (logical state still round-trips).
|
|
35
39
|
2. **Deterministic `random()` / `now()`** — seeded PRNG and fixed clock by default (injectable).
|
|
36
|
-
3. **Single
|
|
40
|
+
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.
|
|
37
41
|
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`).
|
|
38
42
|
|
|
39
43
|
The oracle exposes **2787 builtin functions** and **74 operators** in `pg_catalog`; postgres-mem implements **302 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.
|
|
@@ -56,7 +60,7 @@ The oracle exposes **2787 builtin functions** and **74 operators** in `pg_catalo
|
|
|
56
60
|
| GROUPING SETS / ROLLUP / CUBE, DISTINCT ON, LATERAL, set ops | VERIFIED | |
|
|
57
61
|
| Recursive + data-modifying CTEs | VERIFIED | |
|
|
58
62
|
| Constraints: PK / UNIQUE / NOT NULL / CHECK / FK actions | VERIFIED | DEFERRABLE parsed, checked at statement end |
|
|
59
|
-
| Sequences / serial / identity | VERIFIED | |
|
|
63
|
+
| Sequences / serial / identity | VERIFIED | Includes pg_dump identity sequence names and options |
|
|
60
64
|
| Schemas + search_path + pg_catalog / information_schema | VERIFIED | Catalog columns are the commonly-queried subset |
|
|
61
65
|
| Enums, domains, generated columns | VERIFIED | |
|
|
62
66
|
| Triggers (row-level, LANGUAGE sql-expressible) | PARTIALLY VERIFIED | Creation-order firing, `UPDATE OF` ignored, no INSTEAD OF (documented) |
|
|
@@ -65,14 +69,15 @@ The oracle exposes **2787 builtin functions** and **74 operators** in `pg_catalo
|
|
|
65
69
|
| `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`) |
|
|
66
70
|
| `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. |
|
|
67
71
|
| COPY FROM/TO (text, csv) | VERIFIED | Via `copyFrom` API hook / rows out |
|
|
68
|
-
| PREPARE / EXECUTE / DEALLOCATE, SET / SHOW / RESET | VERIFIED | GUC subset |
|
|
72
|
+
| PREPARE / EXECUTE / DEALLOCATE, SET / SHOW / RESET | VERIFIED | GUC subset, including PostgreSQL dump and migration-client preambles |
|
|
69
73
|
| Transactions / savepoints | VERIFIED | No `25P02` aborted state (documented divergence) |
|
|
70
74
|
| Collation / ordering | PARTIALLY VERIFIED | `C` semantics pinned; locale/ICU out of scope |
|
|
71
75
|
| Regex (`~`, `~*`, POSIX functions) | PARTIALLY VERIFIED | JS regex flavor mapped to POSIX ERE; documented edges |
|
|
72
76
|
| EXPLAIN | PARTIALLY VERIFIED | Stub plan shapes |
|
|
73
77
|
| MERGE / CALL / cursors / LISTEN / full PL/pgSQL | UNSUPPORTED | Fail loud `0A000`, registered |
|
|
74
78
|
| Roles / GRANT / VACUUM / ANALYZE / LOCK | NOT APPLICABLE | Parsed no-ops where harmless |
|
|
75
|
-
| Wire protocol / multi-session
|
|
79
|
+
| Wire protocol / multi-session concurrency | PARTIALLY VERIFIED | `READ COMMITTED` workspaces; `FOR UPDATE`, `NOWAIT`, `SKIP LOCKED`, and deadlock detection |
|
|
80
|
+
| On-disk PostgreSQL format | NOT APPLICABLE | PGMM snapshots are the persistence format |
|
|
76
81
|
|
|
77
82
|
## How to verify
|
|
78
83
|
|
package/README.md
CHANGED
|
@@ -173,6 +173,13 @@ const fromUri = await serve("postgres://postgres:secret@127.0.0.1:0/app")
|
|
|
173
173
|
await fromUri.close()
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
The wire server owns a PostgreSQL-style database catalog. Start on `postgres`, then use ordinary
|
|
177
|
+
`CREATE DATABASE`, `DROP DATABASE`, and `ALTER DATABASE … RENAME TO` statements; startup packets
|
|
178
|
+
select an isolated engine and unknown names fail with `3D000`. `CREATE DATABASE child TEMPLATE
|
|
179
|
+
seed` forks `seed` through the same copy-on-write snapshot path as `Snapshot.open()`. The live names
|
|
180
|
+
are available through `pg_database` and `server.databaseNames()`, while
|
|
181
|
+
`server.getDatabase(name)` and `server.snapshot(name)` provide programmatic test controls.
|
|
182
|
+
|
|
176
183
|
Or from the command line (installs a `mockingbird-postgres` bin):
|
|
177
184
|
|
|
178
185
|
```bash
|
|
@@ -190,11 +197,12 @@ The wire server does not speak that HTTP API.
|
|
|
190
197
|
failCommit })` arms the next statement or connection for a drop, a delay, or a `40001` commit
|
|
191
198
|
failure.
|
|
192
199
|
|
|
193
|
-
One engine is shared by every connection.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
adds
|
|
200
|
+
One committed engine is shared by every connection. Explicit transactions execute in per-session
|
|
201
|
+
copy-on-write workspaces that rebase each statement on the latest committed state, providing
|
|
202
|
+
`READ COMMITTED` visibility without holding an engine-wide transaction turn. Successful writes
|
|
203
|
+
are applied atomically at commit; rollback discards the workspace. Across connections the server
|
|
204
|
+
adds row locks for `SELECT … FOR UPDATE` (including blocking, `NOWAIT`, `SKIP LOCKED`, and
|
|
205
|
+
deadlock SQLSTATE `40P01`) and per-session advisory locks (`pg_advisory_lock` /
|
|
198
206
|
`pg_try_advisory_lock` / `_xact_` / `_unlock`) with in-order waiters and deadlock detection
|
|
199
207
|
(`40P01`), `LISTEN`/`NOTIFY` delivered to idle listeners, `CancelRequest` (a blocked statement ends
|
|
200
208
|
`57014` and the session stays usable), and per-connection aborted-transaction state (a failed
|
|
@@ -202,13 +210,15 @@ statement in a block is `25P02` until `ROLLBACK`, with `ReadyForQuery` reporting
|
|
|
202
210
|
SCRAM-SHA-256 and trust auth, the extended query protocol (`Parse`/`Bind`/`Describe`/`Execute`/
|
|
203
211
|
`Sync`) with text and binary parameters and results, real type OIDs in `RowDescription`, and
|
|
204
212
|
`23505`/`23502` errors carrying the constraint, table and column the engine names, all work over
|
|
205
|
-
the wire.
|
|
213
|
+
the wire. `COPY … FROM STDIN` and `COPY … TO STDOUT` use protocol-v3 `CopyInResponse`,
|
|
214
|
+
`CopyOutResponse`, `CopyData`, `CopyDone`, and `CopyFail` messages. Text and CSV streams are parsed
|
|
215
|
+
incrementally across arbitrary frame and quoted-record boundaries; failed imports are atomic and
|
|
216
|
+
leave the connection in PostgreSQL's transaction state.
|
|
206
217
|
|
|
207
|
-
**Not modelled by the server:**
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
another binary type gets `0A000`, and can switch that parameter to text).
|
|
218
|
+
**Not modelled by the server:** transaction ID and snapshot-inspection functions, predicate and
|
|
219
|
+
table locks, isolation levels beyond `READ COMMITTED`, and the binary parameter formats beyond the
|
|
220
|
+
common scalar types (a client that sends another binary type gets `0A000`, and can switch that
|
|
221
|
+
parameter to text). Binary `COPY` is not supported.
|
|
212
222
|
|
|
213
223
|
### Method semantics
|
|
214
224
|
|
|
@@ -308,8 +318,8 @@ Goal: **SQL dialect** behavioural parity vs PostgreSQL **18.3** for the sync API
|
|
|
308
318
|
[DROP-IN-CONTRACT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/postgres/docs/DROP-IN-CONTRACT.md).
|
|
309
319
|
|
|
310
320
|
The in-process API has no async client, no connection pooling and no `pg_dump` codec; the optional
|
|
311
|
-
[wire-protocol server](#wire-protocol-server-separate-processes) adds a TCP listener
|
|
312
|
-
|
|
321
|
+
[wire-protocol server](#wire-protocol-server-separate-processes) adds a TCP listener, per-session
|
|
322
|
+
`READ COMMITTED` transaction workspaces, and row-level worker locks.
|
|
313
323
|
Intentional differences: custom `PGMM` snapshots; seeded `random()` / fixed `now()` by default
|
|
314
324
|
(`{ random: "os" }` / `{ now: "system" }` match PostgreSQL entropy and wall clock).
|
|
315
325
|
|
package/dist/admin.js
CHANGED
|
@@ -717,12 +717,12 @@ function bootAdmin(config) {
|
|
|
717
717
|
throw new Error(`admin shell is missing #${id}`);
|
|
718
718
|
return node;
|
|
719
719
|
};
|
|
720
|
-
const errorMessage = (data,
|
|
720
|
+
const errorMessage = (data, fallback2) => {
|
|
721
721
|
if (!isRecord(data))
|
|
722
|
-
return
|
|
722
|
+
return fallback2;
|
|
723
723
|
const error = data.error;
|
|
724
724
|
if (!isRecord(error) || typeof error.message !== "string" || error.message === "") {
|
|
725
|
-
return
|
|
725
|
+
return fallback2;
|
|
726
726
|
}
|
|
727
727
|
return error.message;
|
|
728
728
|
};
|
|
@@ -981,8 +981,8 @@ function bootAdmin(config) {
|
|
|
981
981
|
const data = await api("GET", "/namespaces");
|
|
982
982
|
const select = el("namespace", HTMLSelectElement);
|
|
983
983
|
const listed2 = requireList(isRecord(data) ? data.namespaces : void 0, "Namespaces");
|
|
984
|
-
const
|
|
985
|
-
const names = (listed2.length > 0 ? listed2.map((name) => String(name)) : [
|
|
984
|
+
const fallback2 = isRecord(data) && typeof data.default === "string" && data.default !== "" ? data.default : "default";
|
|
985
|
+
const names = (listed2.length > 0 ? listed2.map((name) => String(name)) : [fallback2]).slice();
|
|
986
986
|
if (!names.includes(state.namespace))
|
|
987
987
|
names.unshift(state.namespace);
|
|
988
988
|
select.innerHTML = names.map((name) => `<option value="${esc(name)}">${esc(name)}</option>`).join("");
|
|
@@ -1567,6 +1567,8 @@ var createControlPlane = (context) => {
|
|
|
1567
1567
|
return adminError(400, "expected a JSON object");
|
|
1568
1568
|
if (body.reset === true)
|
|
1569
1569
|
context.clock.reset();
|
|
1570
|
+
if (body.freeze === true)
|
|
1571
|
+
context.clock.freeze();
|
|
1570
1572
|
if (body.set !== void 0) {
|
|
1571
1573
|
const instant = parseInstant(body.set);
|
|
1572
1574
|
if (instant === void 0)
|
|
@@ -1579,8 +1581,6 @@ var createControlPlane = (context) => {
|
|
|
1579
1581
|
return adminError(400, 'advance: expected ms or "15m"-style');
|
|
1580
1582
|
context.clock.advance(delta);
|
|
1581
1583
|
}
|
|
1582
|
-
if (body.freeze === true)
|
|
1583
|
-
context.clock.freeze();
|
|
1584
1584
|
if (body.freeze === false)
|
|
1585
1585
|
context.clock.unfreeze();
|
|
1586
1586
|
return json(200, context.clock.state());
|
|
@@ -2629,9 +2629,9 @@ var kindFromType = (type) => {
|
|
|
2629
2629
|
return "unknown";
|
|
2630
2630
|
return "string";
|
|
2631
2631
|
};
|
|
2632
|
-
var countParam = (value,
|
|
2632
|
+
var countParam = (value, fallback2, max) => {
|
|
2633
2633
|
if (value === null)
|
|
2634
|
-
return
|
|
2634
|
+
return fallback2;
|
|
2635
2635
|
if (!/^\d+$/.test(value))
|
|
2636
2636
|
return void 0;
|
|
2637
2637
|
return Math.min(max, Number(value));
|
|
@@ -6344,7 +6344,13 @@ var DatabaseState = class _DatabaseState {
|
|
|
6344
6344
|
this.settings.set("lc_numeric", "C");
|
|
6345
6345
|
this.settings.set("lc_time", "C");
|
|
6346
6346
|
this.settings.set("default_transaction_isolation", "read committed");
|
|
6347
|
+
this.settings.set("lock_timeout", "0");
|
|
6348
|
+
this.settings.set("idle_in_transaction_session_timeout", "0");
|
|
6349
|
+
this.settings.set("transaction_timeout", "0");
|
|
6347
6350
|
this.settings.set("statement_timeout", "0");
|
|
6351
|
+
this.settings.set("check_function_bodies", "on");
|
|
6352
|
+
this.settings.set("xmloption", "content");
|
|
6353
|
+
this.settings.set("row_security", "on");
|
|
6348
6354
|
this.settings.set("array_nulls", "on");
|
|
6349
6355
|
this.settings.set("backslash_quote", "safe_encoding");
|
|
6350
6356
|
}
|
|
@@ -9946,6 +9952,19 @@ function exactDegTan(x) {
|
|
|
9946
9952
|
return Math.tan(x * Math.PI / 180);
|
|
9947
9953
|
}
|
|
9948
9954
|
|
|
9955
|
+
// src/sql/deparse.ts
|
|
9956
|
+
function quoteIdent(name) {
|
|
9957
|
+
if (/^[a-z_][a-z0-9_$]*$/.test(name)) return name;
|
|
9958
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
9959
|
+
}
|
|
9960
|
+
|
|
9961
|
+
// src/runtime/database-context.ts
|
|
9962
|
+
var contexts = /* @__PURE__ */ new WeakMap();
|
|
9963
|
+
var fallback = { name: "postgres", names: () => ["postgres"] };
|
|
9964
|
+
function databaseCatalogContext(state) {
|
|
9965
|
+
return contexts.get(state) ?? fallback;
|
|
9966
|
+
}
|
|
9967
|
+
|
|
9949
9968
|
// src/functions/misc-fns.ts
|
|
9950
9969
|
var PG_VERSION_TEXT = "PostgreSQL 18.3 (postgres-mem) on TypeScript, in-memory engine";
|
|
9951
9970
|
function findSequenceForCall(ctx, name) {
|
|
@@ -10029,8 +10048,8 @@ function sequenceNextval(ctx, seq) {
|
|
|
10029
10048
|
function getMiscFunctions() {
|
|
10030
10049
|
const m = /* @__PURE__ */ new Map();
|
|
10031
10050
|
m.set("version", () => tv("text", PG_VERSION_TEXT));
|
|
10032
|
-
m.set("current_database", () => tv("name",
|
|
10033
|
-
m.set("current_catalog", () => tv("name",
|
|
10051
|
+
m.set("current_database", (ctx) => tv("name", databaseCatalogContext(ctx.state).name));
|
|
10052
|
+
m.set("current_catalog", (ctx) => tv("name", databaseCatalogContext(ctx.state).name));
|
|
10034
10053
|
m.set("current_schema", (ctx) => {
|
|
10035
10054
|
const path = ctx.state.effectiveSearchPath();
|
|
10036
10055
|
const first = path.find((s) => ctx.state.schemas.has(s));
|
|
@@ -10152,7 +10171,7 @@ function getMiscFunctions() {
|
|
|
10152
10171
|
if (!col.identity) return tv("text", null);
|
|
10153
10172
|
return tv(
|
|
10154
10173
|
"text",
|
|
10155
|
-
col.identity.sequence.split(".").map(
|
|
10174
|
+
col.identity.sequence.split(".").map(quoteIdent).join(".")
|
|
10156
10175
|
);
|
|
10157
10176
|
})
|
|
10158
10177
|
);
|
|
@@ -17402,14 +17421,19 @@ var Parser = class {
|
|
|
17402
17421
|
parseIdentityOptions() {
|
|
17403
17422
|
const options = {};
|
|
17404
17423
|
if (this.eatPunct("(")) {
|
|
17405
|
-
Object.assign(options, this.parseSequenceOptions());
|
|
17424
|
+
Object.assign(options, this.parseSequenceOptions(true));
|
|
17406
17425
|
this.expectPunct(")");
|
|
17407
17426
|
}
|
|
17408
17427
|
return options;
|
|
17409
17428
|
}
|
|
17410
|
-
parseSequenceOptions() {
|
|
17429
|
+
parseSequenceOptions(identity = false) {
|
|
17411
17430
|
const options = {};
|
|
17412
17431
|
for (; ; ) {
|
|
17432
|
+
if (identity && this.eatKw("sequence")) {
|
|
17433
|
+
this.expectKw("name");
|
|
17434
|
+
options.sequenceName = this.qualifiedName();
|
|
17435
|
+
continue;
|
|
17436
|
+
}
|
|
17413
17437
|
if (this.eatKw("increment")) {
|
|
17414
17438
|
this.eatKw("by");
|
|
17415
17439
|
options.increment = this.parseSignedBigInt();
|
|
@@ -23411,6 +23435,27 @@ function parseCsvLine(line, delim, quote) {
|
|
|
23411
23435
|
}
|
|
23412
23436
|
return fields;
|
|
23413
23437
|
}
|
|
23438
|
+
function splitCsvRecords(data, quote) {
|
|
23439
|
+
const records = [];
|
|
23440
|
+
let start = 0;
|
|
23441
|
+
let quoted = false;
|
|
23442
|
+
for (let i = 0; i < data.length; i++) {
|
|
23443
|
+
const char = data[i];
|
|
23444
|
+
if (char === quote) {
|
|
23445
|
+
if (quoted && data[i + 1] === quote) i++;
|
|
23446
|
+
else quoted = !quoted;
|
|
23447
|
+
continue;
|
|
23448
|
+
}
|
|
23449
|
+
if (char === "\n" && !quoted) {
|
|
23450
|
+
const end = i > start && data[i - 1] === "\r" ? i - 1 : i;
|
|
23451
|
+
records.push(data.slice(start, end));
|
|
23452
|
+
start = i + 1;
|
|
23453
|
+
}
|
|
23454
|
+
}
|
|
23455
|
+
if (quoted) throw pgError("invalid_text_representation", "unterminated CSV quoted field", "22P04");
|
|
23456
|
+
if (start < data.length) records.push(data.slice(start));
|
|
23457
|
+
return records;
|
|
23458
|
+
}
|
|
23414
23459
|
function executeCopyFromData(env, stmt, data) {
|
|
23415
23460
|
const format = String(stmt.options.format ?? "text").toLowerCase();
|
|
23416
23461
|
if (format !== "text" && format !== "csv") throw unsupported(`COPY format ${format}`);
|
|
@@ -23421,7 +23466,7 @@ function executeCopyFromData(env, stmt, data) {
|
|
|
23421
23466
|
const nullStr = String(stmt.options.null ?? (format === "csv" ? "" : "\\N"));
|
|
23422
23467
|
const quote = String(stmt.options.quote ?? '"');
|
|
23423
23468
|
const header = stmt.options.header === true || String(stmt.options.header ?? "").toLowerCase() === "true";
|
|
23424
|
-
let lines = data.split(/\r?\n/);
|
|
23469
|
+
let lines = format === "csv" ? splitCsvRecords(data, quote) : data.split(/\r?\n/);
|
|
23425
23470
|
const endMarker = lines.indexOf("\\.");
|
|
23426
23471
|
if (endMarker !== -1) lines = lines.slice(0, endMarker);
|
|
23427
23472
|
if (lines.length > 0 && lines[lines.length - 1] === "") lines = lines.slice(0, -1);
|
|
@@ -24155,7 +24200,8 @@ function pgSettings(ctx) {
|
|
|
24155
24200
|
"pg_settings"
|
|
24156
24201
|
);
|
|
24157
24202
|
}
|
|
24158
|
-
function pgDatabase() {
|
|
24203
|
+
function pgDatabase(ctx) {
|
|
24204
|
+
const names = databaseCatalogContext(ctx.state).names();
|
|
24159
24205
|
return rel(
|
|
24160
24206
|
[
|
|
24161
24207
|
["oid", "oid"],
|
|
@@ -24164,7 +24210,7 @@ function pgDatabase() {
|
|
|
24164
24210
|
["datistemplate", "bool"],
|
|
24165
24211
|
["datallowconn", "bool"]
|
|
24166
24212
|
],
|
|
24167
|
-
[
|
|
24213
|
+
names.map((name, index) => [5 + index, name, 6, false, true]),
|
|
24168
24214
|
"pg_database"
|
|
24169
24215
|
);
|
|
24170
24216
|
}
|
|
@@ -24199,13 +24245,14 @@ function pgTrigger(ctx) {
|
|
|
24199
24245
|
"pg_trigger"
|
|
24200
24246
|
);
|
|
24201
24247
|
}
|
|
24202
|
-
var
|
|
24248
|
+
var catalogName = (state) => databaseCatalogContext(state).name;
|
|
24203
24249
|
function infoSchemata(state) {
|
|
24250
|
+
const catalog = catalogName(state);
|
|
24204
24251
|
const rows = [
|
|
24205
|
-
[
|
|
24206
|
-
[
|
|
24252
|
+
[catalog, "pg_catalog", OWNER],
|
|
24253
|
+
[catalog, "information_schema", OWNER]
|
|
24207
24254
|
];
|
|
24208
|
-
for (const s of state.schemas.values()) rows.push([
|
|
24255
|
+
for (const s of state.schemas.values()) rows.push([catalog, s.name, OWNER]);
|
|
24209
24256
|
return rel(
|
|
24210
24257
|
[
|
|
24211
24258
|
["catalog_name", "name"],
|
|
@@ -24217,13 +24264,14 @@ function infoSchemata(state) {
|
|
|
24217
24264
|
);
|
|
24218
24265
|
}
|
|
24219
24266
|
function infoTables(ctx) {
|
|
24267
|
+
const catalog = catalogName(ctx.state);
|
|
24220
24268
|
const rows = [];
|
|
24221
24269
|
for (const schema of ctx.state.schemas.values()) {
|
|
24222
24270
|
for (const t of schema.tables.values()) {
|
|
24223
|
-
rows.push([
|
|
24271
|
+
rows.push([catalog, t.schema, t.name, t.temp ? "LOCAL TEMPORARY" : "BASE TABLE", "YES"]);
|
|
24224
24272
|
}
|
|
24225
24273
|
for (const v of schema.views.values()) {
|
|
24226
|
-
if (!v.materialized) rows.push([
|
|
24274
|
+
if (!v.materialized) rows.push([catalog, v.schema, v.name, "VIEW", "NO"]);
|
|
24227
24275
|
}
|
|
24228
24276
|
}
|
|
24229
24277
|
return rel(
|
|
@@ -24239,6 +24287,7 @@ function infoTables(ctx) {
|
|
|
24239
24287
|
);
|
|
24240
24288
|
}
|
|
24241
24289
|
function infoColumns(ctx) {
|
|
24290
|
+
const catalog = catalogName(ctx.state);
|
|
24242
24291
|
const rows = [];
|
|
24243
24292
|
for (const t of allTables(ctx.state)) {
|
|
24244
24293
|
t.columns.forEach((c, i) => {
|
|
@@ -24249,7 +24298,7 @@ function infoColumns(ctx) {
|
|
|
24249
24298
|
const dataType = isArrayType(id) ? "ARRAY" : isEnumType(id) ? "USER-DEFINED" : typeDisplayName(id);
|
|
24250
24299
|
const udt = isArrayType(id) ? `_${arrayUdt(id)}` : isEnumType(id) ? id.slice(5).split(".").pop() : id;
|
|
24251
24300
|
rows.push([
|
|
24252
|
-
|
|
24301
|
+
catalog,
|
|
24253
24302
|
t.schema,
|
|
24254
24303
|
t.name,
|
|
24255
24304
|
c.name,
|
|
@@ -24294,10 +24343,11 @@ function arrayUdt(t) {
|
|
|
24294
24343
|
return isEnumType(elem) ? elem.slice(5).split(".").pop() : elem;
|
|
24295
24344
|
}
|
|
24296
24345
|
function infoViews(ctx) {
|
|
24346
|
+
const catalog = catalogName(ctx.state);
|
|
24297
24347
|
const rows = [];
|
|
24298
24348
|
for (const schema of ctx.state.schemas.values()) {
|
|
24299
24349
|
for (const v of schema.views.values()) {
|
|
24300
|
-
if (!v.materialized) rows.push([
|
|
24350
|
+
if (!v.materialized) rows.push([catalog, v.schema, v.name, null]);
|
|
24301
24351
|
}
|
|
24302
24352
|
}
|
|
24303
24353
|
return rel(
|
|
@@ -24312,10 +24362,11 @@ function infoViews(ctx) {
|
|
|
24312
24362
|
);
|
|
24313
24363
|
}
|
|
24314
24364
|
function infoSequences(ctx) {
|
|
24365
|
+
const catalog = catalogName(ctx.state);
|
|
24315
24366
|
const rows = [];
|
|
24316
24367
|
for (const s of allSequences(ctx.state)) {
|
|
24317
24368
|
rows.push([
|
|
24318
|
-
|
|
24369
|
+
catalog,
|
|
24319
24370
|
s.schema,
|
|
24320
24371
|
s.name,
|
|
24321
24372
|
typeDisplayName(s.dataType),
|
|
@@ -24343,11 +24394,12 @@ function infoSequences(ctx) {
|
|
|
24343
24394
|
);
|
|
24344
24395
|
}
|
|
24345
24396
|
function infoTableConstraints(ctx) {
|
|
24397
|
+
const catalog = catalogName(ctx.state);
|
|
24346
24398
|
const rows = [];
|
|
24347
24399
|
for (const t of allTables(ctx.state)) {
|
|
24348
24400
|
for (const c of t.constraints) {
|
|
24349
24401
|
const type = c.kind === "primary_key" ? "PRIMARY KEY" : c.kind === "unique" ? "UNIQUE" : c.kind === "check" ? "CHECK" : "FOREIGN KEY";
|
|
24350
|
-
rows.push([
|
|
24402
|
+
rows.push([catalog, t.schema, c.name, catalog, t.schema, t.name, type]);
|
|
24351
24403
|
}
|
|
24352
24404
|
}
|
|
24353
24405
|
return rel(
|
|
@@ -24365,12 +24417,13 @@ function infoTableConstraints(ctx) {
|
|
|
24365
24417
|
);
|
|
24366
24418
|
}
|
|
24367
24419
|
function infoKeyColumnUsage(ctx) {
|
|
24420
|
+
const catalog = catalogName(ctx.state);
|
|
24368
24421
|
const rows = [];
|
|
24369
24422
|
for (const t of allTables(ctx.state)) {
|
|
24370
24423
|
for (const c of t.constraints) {
|
|
24371
24424
|
if (c.kind !== "primary_key" && c.kind !== "unique" && c.kind !== "foreign_key") continue;
|
|
24372
24425
|
c.columns.forEach((col, i) => {
|
|
24373
|
-
rows.push([
|
|
24426
|
+
rows.push([catalog, t.schema, c.name, catalog, t.schema, t.name, col, i + 1]);
|
|
24374
24427
|
});
|
|
24375
24428
|
}
|
|
24376
24429
|
}
|
|
@@ -24390,12 +24443,13 @@ function infoKeyColumnUsage(ctx) {
|
|
|
24390
24443
|
);
|
|
24391
24444
|
}
|
|
24392
24445
|
function infoRoutines(ctx) {
|
|
24446
|
+
const catalog = catalogName(ctx.state);
|
|
24393
24447
|
const rows = [];
|
|
24394
24448
|
for (const schema of ctx.state.schemas.values()) {
|
|
24395
24449
|
for (const fns of schema.functions.values()) {
|
|
24396
24450
|
for (const f of fns) {
|
|
24397
24451
|
rows.push([
|
|
24398
|
-
|
|
24452
|
+
catalog,
|
|
24399
24453
|
f.schema,
|
|
24400
24454
|
f.name,
|
|
24401
24455
|
"FUNCTION",
|
|
@@ -24452,7 +24506,7 @@ function buildCatalogRelation(ctx, schema, name) {
|
|
|
24452
24506
|
case "pg_settings":
|
|
24453
24507
|
return pgSettings(ctx);
|
|
24454
24508
|
case "pg_database":
|
|
24455
|
-
return pgDatabase();
|
|
24509
|
+
return pgDatabase(ctx);
|
|
24456
24510
|
case "pg_roles":
|
|
24457
24511
|
case "pg_user":
|
|
24458
24512
|
return pgRoles();
|
|
@@ -24633,6 +24687,11 @@ var MAX_IDENT = 63;
|
|
|
24633
24687
|
function truncIdent(s) {
|
|
24634
24688
|
return s.length > MAX_IDENT ? s.slice(0, MAX_IDENT) : s;
|
|
24635
24689
|
}
|
|
24690
|
+
function identitySequenceTarget(env, tableSchema, defaultName, options) {
|
|
24691
|
+
const parts = options.sequenceName;
|
|
24692
|
+
if (!parts || parts.length === 0) return { schema: tableSchema, name: defaultName };
|
|
24693
|
+
return targetSchema(env, parts);
|
|
24694
|
+
}
|
|
24636
24695
|
var SEQ_LIMITS = {
|
|
24637
24696
|
int2: { min: -32768n, max: 32767n },
|
|
24638
24697
|
int4: { min: -2147483648n, max: 2147483647n },
|
|
@@ -24809,8 +24868,8 @@ function buildColumn(env, schema, tableName, def, out, taken) {
|
|
|
24809
24868
|
break;
|
|
24810
24869
|
}
|
|
24811
24870
|
case "generated_identity": {
|
|
24812
|
-
const
|
|
24813
|
-
const seq = buildSequence(env, schema,
|
|
24871
|
+
const target = identitySequenceTarget(env, schema, truncIdent(`${tableName}_${def.name}_seq`), c.options);
|
|
24872
|
+
const seq = buildSequence(env, target.schema, target.name, c.options, false);
|
|
24814
24873
|
if (!c.options.as) {
|
|
24815
24874
|
seq.dataType = col.type.id === "int2" || col.type.id === "int4" || col.type.id === "int8" ? col.type.id : "int8";
|
|
24816
24875
|
const limits = SEQ_LIMITS[seq.dataType];
|
|
@@ -24819,7 +24878,7 @@ function buildColumn(env, schema, tableName, def, out, taken) {
|
|
|
24819
24878
|
}
|
|
24820
24879
|
seq.ownedBy = { table: tableName, column: def.name };
|
|
24821
24880
|
out.sequences.push(seq);
|
|
24822
|
-
col.identity = { always: c.always, sequence: `${schema.name}.${
|
|
24881
|
+
col.identity = { always: c.always, sequence: `${target.schema.name}.${target.name}` };
|
|
24823
24882
|
col.notNull = true;
|
|
24824
24883
|
break;
|
|
24825
24884
|
}
|
|
@@ -24955,10 +25014,11 @@ function executeCreateTable(env, stmt) {
|
|
|
24955
25014
|
throw pgError("invalid_table_definition", `multiple primary keys for table "${name}" are not allowed`, "42P16");
|
|
24956
25015
|
}
|
|
24957
25016
|
for (const seq of built.sequences) {
|
|
24958
|
-
|
|
25017
|
+
const seqSchema = state.getSchema(seq.schema);
|
|
25018
|
+
if (seqSchema.hasRelation(seq.name)) {
|
|
24959
25019
|
throw pgError("duplicate_table", `relation "${seq.name}" already exists`, "42P07");
|
|
24960
25020
|
}
|
|
24961
|
-
|
|
25021
|
+
seqSchema.sequences.set(seq.name, seq);
|
|
24962
25022
|
}
|
|
24963
25023
|
const table = new TableData(schema.name, name, built.columns, state.nextOid(), stmt.temp);
|
|
24964
25024
|
table.constraints = built.constraints;
|
|
@@ -25310,7 +25370,13 @@ function executeAlterTable(env, stmt) {
|
|
|
25310
25370
|
const built = { columns: [], constraints: [], sequences: [] };
|
|
25311
25371
|
const taken = constraintNames(table);
|
|
25312
25372
|
const col = buildColumn(env, schema, table.name, action.column, built, taken);
|
|
25313
|
-
for (const seq of built.sequences)
|
|
25373
|
+
for (const seq of built.sequences) {
|
|
25374
|
+
const seqSchema = state.getSchema(seq.schema);
|
|
25375
|
+
if (seqSchema.hasRelation(seq.name)) {
|
|
25376
|
+
throw pgError("duplicate_table", `relation "${seq.name}" already exists`, "42P07");
|
|
25377
|
+
}
|
|
25378
|
+
seqSchema.sequences.set(seq.name, seq);
|
|
25379
|
+
}
|
|
25314
25380
|
table.columns.push(col);
|
|
25315
25381
|
table.constraints.push(...built.constraints);
|
|
25316
25382
|
for (let i = 0; i < rows.length; i++) {
|
|
@@ -25630,12 +25696,32 @@ function executeAlterTable(env, stmt) {
|
|
|
25630
25696
|
"42P16"
|
|
25631
25697
|
);
|
|
25632
25698
|
}
|
|
25633
|
-
|
|
25634
|
-
|
|
25699
|
+
if (!col.notNull) {
|
|
25700
|
+
throw pgError(
|
|
25701
|
+
"object_not_in_prerequisite_state",
|
|
25702
|
+
`column "${action.column}" of relation "${table.name}" must be declared NOT NULL before identity can be added`,
|
|
25703
|
+
"55000"
|
|
25704
|
+
);
|
|
25705
|
+
}
|
|
25706
|
+
const target = identitySequenceTarget(
|
|
25707
|
+
env,
|
|
25708
|
+
schema,
|
|
25709
|
+
truncIdent(`${table.name}_${action.column}_seq`),
|
|
25710
|
+
action.options
|
|
25711
|
+
);
|
|
25712
|
+
if (target.schema.hasRelation(target.name)) {
|
|
25713
|
+
throw pgError("duplicate_table", `relation "${target.name}" already exists`, "42P07");
|
|
25714
|
+
}
|
|
25715
|
+
const seq = buildSequence(env, target.schema, target.name, action.options, false);
|
|
25716
|
+
if (!action.options.as) {
|
|
25717
|
+
seq.dataType = col.type.id === "int2" || col.type.id === "int4" || col.type.id === "int8" ? col.type.id : "int8";
|
|
25718
|
+
const limits = SEQ_LIMITS[seq.dataType];
|
|
25719
|
+
if (action.options.maxValue === void 0) seq.maxValue = seq.increment > 0n ? limits.max : -1n;
|
|
25720
|
+
if (action.options.minValue === void 0) seq.minValue = seq.increment > 0n ? 1n : limits.min;
|
|
25721
|
+
}
|
|
25635
25722
|
seq.ownedBy = { table: table.name, column: action.column };
|
|
25636
|
-
schema.sequences.set(
|
|
25637
|
-
col.identity = { always:
|
|
25638
|
-
col.notNull = true;
|
|
25723
|
+
target.schema.sequences.set(target.name, seq);
|
|
25724
|
+
col.identity = { always: action.always, sequence: `${target.schema.name}.${target.name}` };
|
|
25639
25725
|
break;
|
|
25640
25726
|
}
|
|
25641
25727
|
case "drop_identity": {
|
|
@@ -26504,7 +26590,7 @@ if (typeof disposeKey === "symbol") {
|
|
|
26504
26590
|
|
|
26505
26591
|
// src/admin.ts
|
|
26506
26592
|
var QUERY_CAP = 500;
|
|
26507
|
-
var
|
|
26593
|
+
var quoteIdent2 = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
26508
26594
|
var bounded = (value) => {
|
|
26509
26595
|
if (!Number.isSafeInteger(value) || value < 0) throw new Error("limit and offset must be non-negative integers");
|
|
26510
26596
|
return value;
|
|
@@ -26577,7 +26663,7 @@ var adapt = (db) => ({
|
|
|
26577
26663
|
tables: () => listed(db),
|
|
26578
26664
|
page(schema, table, limit, offset) {
|
|
26579
26665
|
const found = requireTable(db, schema, table);
|
|
26580
|
-
const from = `${
|
|
26666
|
+
const from = `${quoteIdent2(schema)}.${quoteIdent2(table)}`;
|
|
26581
26667
|
const take = bounded(limit);
|
|
26582
26668
|
const skip = bounded(offset);
|
|
26583
26669
|
const totalRow = db.query(`SELECT count(*) AS count FROM ${from}`);
|