@crvouga/mockingbird-service-postgres 1.5.1 → 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 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. The optional
17
- wire-protocol server (`src/wire/`, a Node/Bun-only entry over the one engine) is in scope: it
18
- speaks frontend/backend v3 and coordinates connections by serializing transaction blocks, adding
19
- advisory locks, `LISTEN`/`NOTIFY`, `CancelRequest` and per-connection `25P02`. It deliberately does
20
- **not** add row-level lock contention (`SELECT … FOR UPDATE SKIP LOCKED` distribution) or `COPY`
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,5 +1,15 @@
1
1
  # Changelog — @crvouga/mockingbird-service-postgres
2
2
 
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
+
3
13
  ## 1.5.1 (2026-10-01)
4
14
 
5
15
  ### Fixes and improvements
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 session** — no MVCC across connections, no isolation levels beyond one session, no `25P02` aborted-transaction state (documented divergence).
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 MVCC / on-disk format | NOT APPLICABLE | |
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. The engine runs one statement at a time, so a connection
194
- inside an explicit `BEGIN` block holds it until `COMMIT`/`ROLLBACK` and other connections queue
195
- behind it — which keeps read-committed visibility (an uncommitted row is never seen by another
196
- connection) by serializing transaction blocks rather than by MVCC. Across connections the server
197
- adds what a single engine does not: per-session advisory locks (`pg_advisory_lock` /
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:** row-level lock contention, so `SELECT … FOR UPDATE SKIP LOCKED`
208
- parses and returns rows but does not distribute disjoint rows across concurrent workers (there are
209
- no row locks); `COPY` streaming (`CopyInResponse`/`CopyData`) — use `copyFrom` on a shared
210
- `Database`; and the binary parameter formats beyond the common scalar types (a client that sends
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 and coordinates
312
- several connections over the one engine, but by serializing transaction blocks, not by MVCC.
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, fallback) => {
720
+ const errorMessage = (data, fallback2) => {
721
721
  if (!isRecord(data))
722
- return fallback;
722
+ return fallback2;
723
723
  const error = data.error;
724
724
  if (!isRecord(error) || typeof error.message !== "string" || error.message === "") {
725
- return fallback;
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 fallback = isRecord(data) && typeof data.default === "string" && data.default !== "" ? data.default : "default";
985
- const names = (listed2.length > 0 ? listed2.map((name) => String(name)) : [fallback]).slice();
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("");
@@ -2629,9 +2629,9 @@ var kindFromType = (type) => {
2629
2629
  return "unknown";
2630
2630
  return "string";
2631
2631
  };
2632
- var countParam = (value, fallback, max) => {
2632
+ var countParam = (value, fallback2, max) => {
2633
2633
  if (value === null)
2634
- return fallback;
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", "postgres"));
10033
- m.set("current_catalog", () => tv("name", "postgres"));
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((p) => p).join(".")
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
- [[5, "postgres", 6, false, true]],
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 CATALOG_NAME = "postgres";
24248
+ var catalogName = (state) => databaseCatalogContext(state).name;
24203
24249
  function infoSchemata(state) {
24250
+ const catalog = catalogName(state);
24204
24251
  const rows = [
24205
- [CATALOG_NAME, "pg_catalog", OWNER],
24206
- [CATALOG_NAME, "information_schema", OWNER]
24252
+ [catalog, "pg_catalog", OWNER],
24253
+ [catalog, "information_schema", OWNER]
24207
24254
  ];
24208
- for (const s of state.schemas.values()) rows.push([CATALOG_NAME, s.name, OWNER]);
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([CATALOG_NAME, t.schema, t.name, t.temp ? "LOCAL TEMPORARY" : "BASE TABLE", "YES"]);
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([CATALOG_NAME, v.schema, v.name, "VIEW", "NO"]);
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
- CATALOG_NAME,
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([CATALOG_NAME, v.schema, v.name, null]);
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
- CATALOG_NAME,
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([CATALOG_NAME, t.schema, c.name, CATALOG_NAME, t.schema, t.name, type]);
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([CATALOG_NAME, t.schema, c.name, CATALOG_NAME, t.schema, t.name, col, i + 1]);
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
- CATALOG_NAME,
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 seqName = truncIdent(`${tableName}_${def.name}_seq`);
24813
- const seq = buildSequence(env, schema, seqName, c.options, false);
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}.${seqName}` };
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
- if (schema.hasRelation(seq.name)) {
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
- schema.sequences.set(seq.name, seq);
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) schema.sequences.set(seq.name, seq);
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
- const seqName = truncIdent(`${table.name}_${action.column}_seq`);
25634
- const seq = buildSequence(env, schema, seqName, action.options, false);
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(seqName, seq);
25637
- col.identity = { always: true, sequence: `${schema.name}.${seqName}` };
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 quoteIdent = (name) => `"${name.replaceAll('"', '""')}"`;
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 = `${quoteIdent(schema)}.${quoteIdent(table)}`;
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}`);