@crvouga/mockingbird-service-postgres 2.3.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,32 @@
1
1
  # Changelog — @crvouga/mockingbird-service-postgres
2
2
 
3
- ## 2.3.0 (2026-10-05)
3
+ ## 3.0.1 (2026-10-07)
4
+
5
+ ### Fixes and improvements
6
+
7
+ - keep row locks and SQLSTATE on the wire ([91f9033](https://github.com/crvouga/mockingbird/commit/91f903301eec4a32f23db3f56bcaf7ee534508df))
8
+
9
+ ## 3.0.0 (2026-10-07)
10
+
11
+ ### ⚠️ Breaking changes
12
+
13
+ - point the repo at crvouga/mockingbird ([1bf35a4](https://github.com/crvouga/mockingbird/commit/1bf35a422b84a85b50b09b72bd7cc7093082a86e))
14
+ - point the repo at crvouga/emulates ([cdb5e53](https://github.com/crvouga/mockingbird/commit/cdb5e536ed8c1f52345e3984f089001c25fdb08a))
15
+ - rename Emulators to Emulates ([2e10d9c](https://github.com/crvouga/mockingbird/commit/2e10d9cff84c54ac37768fc9a19d1aefd3943744))
16
+ - rebrand Mockingbird to Emulators ([1adca65](https://github.com/crvouga/mockingbird/commit/1adca65ce28242f11cff98eac05d2be88f680cbe))
17
+
18
+ ### Features
19
+
20
+ - add index metadata and regressions ([2866d9d](https://github.com/crvouga/mockingbird/commit/2866d9d1242dab51c5c4f655dc107ac4aac2ca57))
21
+
22
+ ### Fixes and improvements
23
+
24
+ - typecheck the brand gate before builds ([fc36095](https://github.com/crvouga/mockingbird/commit/fc3609514de70ffe3170a5d216fb8a49f9ce7cbb))
25
+ - serve reconciliation catalog queries ([74a0fb7](https://github.com/crvouga/mockingbird/commit/74a0fb765ff9cd58b5fa61fbeb7db6ca39ec6da4))
26
+ - format the emulates records query ([8698872](https://github.com/crvouga/mockingbird/commit/8698872509d06002be5a8fcdc85d79f8462320c3))
27
+ - complete index regressions ([fa7c0dc](https://github.com/crvouga/mockingbird/commit/fa7c0dc9c76856e0a50db981b7176506d84a1665))
28
+
29
+ ## 2.3.0 (2026-10-04)
4
30
 
5
31
  ### Features
6
32
 
package/dist/admin.js CHANGED
@@ -1441,6 +1441,9 @@ var PG_CATALOG_RELATIONS = /* @__PURE__ */ new Set([
1441
1441
  "pg_enum",
1442
1442
  "pg_constraint",
1443
1443
  "pg_index",
1444
+ "pg_description",
1445
+ "pg_stat_user_tables",
1446
+ "pg_stat_user_indexes",
1444
1447
  "pg_sequence",
1445
1448
  "pg_sequences",
1446
1449
  "pg_tables",
@@ -3718,6 +3721,7 @@ var TYPE_ALIASES = {
3718
3721
  regclass: "regclass",
3719
3722
  regtype: "regtype",
3720
3723
  regproc: "regproc",
3724
+ regnamespace: "regnamespace",
3721
3725
  oid: "oid",
3722
3726
  void: "void",
3723
3727
  record: "record",
@@ -3811,6 +3815,7 @@ var TYPE_OIDS = {
3811
3815
  numeric: 1700,
3812
3816
  regclass: 2205,
3813
3817
  regtype: 2206,
3818
+ regnamespace: 4089,
3814
3819
  record: 2249,
3815
3820
  uuid: 2950,
3816
3821
  jsonb: 3802,
@@ -4111,6 +4116,7 @@ function recordText(rec, ctx) {
4111
4116
  return `(${parts.join(",")})`;
4112
4117
  }
4113
4118
  function datumText(t, v, ctx) {
4119
+ if (t === "regnamespace") return ctx.namespaceName?.(v) ?? String(v);
4114
4120
  if (v === null) throw pgError("internal", "datumText called with null");
4115
4121
  if (isArrayType(t)) return arrayText(v, ctx);
4116
4122
  if (isEnumType(t)) return v;
@@ -4184,6 +4190,11 @@ function datumText(t, v, ctx) {
4184
4190
  }
4185
4191
  var UTC_INPUT = { zoneOffsetForNaive: () => 0 };
4186
4192
  function datumFromText(t, text, ctx) {
4193
+ if (t === "regnamespace") {
4194
+ if (/^\s*\d+\s*$/.test(text)) return datumFromText("oid", text, ctx);
4195
+ if (!ctx.namespaceOid) throw unsupported("regnamespace input without a catalog");
4196
+ return ctx.namespaceOid(text);
4197
+ }
4187
4198
  if (isArrayType(t)) return parseArrayText(t, text, ctx);
4188
4199
  if (isEnumType(t)) {
4189
4200
  if (ctx.enumHasLabel && !ctx.enumHasLabel(t, text)) {
@@ -5040,7 +5051,11 @@ function castTo(env, value, target, opts = {}) {
5040
5051
  );
5041
5052
  return tv(target, makeArray(toElem, items, arr.dims.slice(), arr.lbs.slice()));
5042
5053
  }
5043
- if ((from === "int4" || from === "int8" || from === "int2") && (target === "oid" || target === "regclass" || target === "regtype" || target === "regproc")) {
5054
+ if (from === "oid" && target === "regnamespace") return tv(target, v);
5055
+ if (from === "regnamespace" && (target === "oid" || target === "int4" || target === "int8")) {
5056
+ return tv(target, target === "int8" ? BigInt(v) : v);
5057
+ }
5058
+ if ((from === "int4" || from === "int8" || from === "int2") && (target === "oid" || target === "regclass" || target === "regtype" || target === "regproc" || target === "regnamespace")) {
5044
5059
  return tv(target, Number(v));
5045
5060
  }
5046
5061
  if (from === "oid" && (target === "int4" || target === "int8")) {
@@ -5988,6 +6003,7 @@ function datumCompare(t, a, b, ctx = DEFAULT_COMPARE_CTX) {
5988
6003
  case "int2":
5989
6004
  case "int4":
5990
6005
  case "oid":
6006
+ case "regnamespace":
5991
6007
  case "date":
5992
6008
  return expectNumber(a, t) - expectNumber(b, t);
5993
6009
  case "float4":
@@ -8143,10 +8159,99 @@ function exactDegTan(x) {
8143
8159
  }
8144
8160
 
8145
8161
  // src/sql/deparse.ts
8162
+ function deparseSelect(stmt) {
8163
+ return deparseBody(stmt.body);
8164
+ }
8165
+ function deparseBody(body) {
8166
+ switch (body.type) {
8167
+ case "select_core":
8168
+ return deparseSelectCore(body);
8169
+ case "setop":
8170
+ return `${deparseBody(body.left)} ${body.op.toUpperCase()}${body.all ? " ALL" : ""} ${deparseBody(body.right)}`;
8171
+ case "values":
8172
+ return `VALUES ${body.rows.map((r) => `(${r.map(deparseExpr).join(", ")})`).join(", ")}`;
8173
+ }
8174
+ }
8175
+ function deparseSelectCore(body) {
8176
+ const targets = body.targets.length === 0 ? "*" : body.targets.map((t) => {
8177
+ const expr = deparseExpr(t.expr);
8178
+ return t.alias ? `${expr} AS ${quoteIdent(t.alias)}` : expr;
8179
+ }).join(", ");
8180
+ let sql = `SELECT ${targets}`;
8181
+ if (body.from.length > 0) {
8182
+ sql += ` FROM ${body.from.map(deparseFrom).join(", ")}`;
8183
+ }
8184
+ if (body.where) sql += ` WHERE ${deparseExpr(body.where)}`;
8185
+ if (body.groupBy && body.groupBy.length > 0) {
8186
+ sql += ` GROUP BY ${body.groupBy.map((g) => g.kind === "expr" ? deparseExpr(g.expr) : "?").join(", ")}`;
8187
+ }
8188
+ if (body.having) sql += ` HAVING ${deparseExpr(body.having)}`;
8189
+ return sql;
8190
+ }
8191
+ function deparseFrom(item) {
8192
+ if (item.type === "from_table") {
8193
+ const name = item.name.join(".");
8194
+ return item.alias ? `${name} ${quoteIdent(item.alias)}` : name;
8195
+ }
8196
+ return "(subquery)";
8197
+ }
8146
8198
  function quoteIdent(name) {
8147
8199
  if (/^[a-z_][a-z0-9_$]*$/.test(name)) return name;
8148
8200
  return `"${name.replaceAll('"', '""')}"`;
8149
8201
  }
8202
+ function deparseExpr(e) {
8203
+ switch (e.type) {
8204
+ case "null_lit":
8205
+ return "NULL";
8206
+ case "string_lit":
8207
+ return `'${e.value.replaceAll("'", "''")}'`;
8208
+ case "number_lit":
8209
+ return e.raw;
8210
+ case "bool_lit":
8211
+ return e.value ? "true" : "false";
8212
+ case "colref":
8213
+ return e.parts.join(".");
8214
+ case "star":
8215
+ return e.table ? `${e.table.join(".")}.*` : "*";
8216
+ case "binop":
8217
+ return `(${deparseExpr(e.left)} ${e.op} ${deparseExpr(e.right)})`;
8218
+ case "unop":
8219
+ return `${e.op}${deparseExpr(e.operand)}`;
8220
+ case "cast":
8221
+ return `CAST(${deparseExpr(e.expr)} AS ${e.target.parts.join(".")})`;
8222
+ case "func":
8223
+ return `${e.name.join(".")}(${e.args.map(deparseExpr).join(", ")})`;
8224
+ case "case": {
8225
+ let out = "CASE";
8226
+ if (e.operand) out += ` ${deparseExpr(e.operand)}`;
8227
+ for (const arm of e.whens) {
8228
+ out += ` WHEN ${deparseExpr(arm.when)} THEN ${deparseExpr(arm.then)}`;
8229
+ }
8230
+ if (e.elseExpr) out += ` ELSE ${deparseExpr(e.elseExpr)}`;
8231
+ return `${out} END`;
8232
+ }
8233
+ case "subquery_expr":
8234
+ return `(${deparseSelect(e.query)})`;
8235
+ default:
8236
+ return "?";
8237
+ }
8238
+ }
8239
+
8240
+ // src/sql/deparse-index.ts
8241
+ function deparseIndexColumn(column) {
8242
+ let result = column.column === null ? deparseExpr(column.expr) : quoteIdent(column.column);
8243
+ if (column.dir === "desc") result += " DESC";
8244
+ const defaultNulls = column.dir === "desc" ? "first" : "last";
8245
+ if (column.nulls !== defaultNulls) result += ` NULLS ${column.nulls.toUpperCase()}`;
8246
+ return result;
8247
+ }
8248
+ function deparseIndex(index) {
8249
+ const columns = index.columns.map(deparseIndexColumn).join(", ");
8250
+ const unique = index.unique ? "UNIQUE " : "";
8251
+ const nulls = index.nullsNotDistinct ? " NULLS NOT DISTINCT" : "";
8252
+ const predicate = index.where ? ` WHERE ${deparseExpr(index.where)}` : "";
8253
+ return `CREATE ${unique}INDEX ${quoteIdent(index.name)} ON ${quoteIdent(index.schema)}.${quoteIdent(index.table)} USING btree (${columns})${nulls}${predicate}`;
8254
+ }
8150
8255
 
8151
8256
  // src/runtime/database-context.ts
8152
8257
  var contexts = /* @__PURE__ */ new WeakMap();
@@ -8237,6 +8342,22 @@ function sequenceNextval(ctx, seq) {
8237
8342
  }
8238
8343
  function getMiscFunctions() {
8239
8344
  const m = /* @__PURE__ */ new Map();
8345
+ m.set(
8346
+ "pg_get_indexdef",
8347
+ strict("text", (ctx, args) => {
8348
+ const oid = argInt(ctx, args[0]);
8349
+ const column = args[1] ? argInt(ctx, args[1]) : 0;
8350
+ for (const schema of ctx.state.schemas.values()) {
8351
+ for (const index of schema.indexes.values()) {
8352
+ if (index.oid !== oid) continue;
8353
+ if (column === 0) return tv("text", deparseIndex(index));
8354
+ const part = index.columns[column - 1];
8355
+ return tv("text", part ? deparseIndexColumn(part) : "");
8356
+ }
8357
+ }
8358
+ return tv("text", null);
8359
+ })
8360
+ );
8240
8361
  m.set("version", () => tv("text", PG_VERSION_TEXT));
8241
8362
  m.set("current_database", (ctx) => tv("name", databaseCatalogContext(ctx.state).name));
8242
8363
  m.set("current_catalog", (ctx) => tv("name", databaseCatalogContext(ctx.state).name));
@@ -13457,6 +13578,9 @@ function tryIndexedTableRows(env, table, alias, where) {
13457
13578
  const key = uniqueKeyOf(env, table, spec, probeRow);
13458
13579
  if (key === null) return [];
13459
13580
  const hits = indexStoreFor(env, table, spec).lookup(key);
13581
+ const schema = env.ctx.state.schemas.get(table.schema);
13582
+ const index = schema?.indexes.get(spec.name);
13583
+ if (schema && index) schema.indexes.set(spec.name, { ...index, scans: (index.scans ?? 0n) + 1n });
13460
13584
  return hits.map((i) => table.rowAt(i));
13461
13585
  }
13462
13586
  function tryIndexedFromItem(env, item, where) {
@@ -16219,8 +16343,8 @@ var Parser = class {
16219
16343
  this.eatKw("column");
16220
16344
  const column = this.ident();
16221
16345
  let alterType = this.eatKw("type");
16222
- if (!alterType && this.eatKw("set") && this.atKw("data")) {
16223
- this.pos += 1;
16346
+ if (!alterType && this.atKw("set") && this.atKw("data", 1)) {
16347
+ this.pos += 2;
16224
16348
  this.expectKw("type");
16225
16349
  alterType = true;
16226
16350
  }
@@ -20572,7 +20696,7 @@ function uniqueSpecsFor(env, table) {
20572
20696
  const schema = env.ctx.state.schemas.get(table.schema);
20573
20697
  if (schema) {
20574
20698
  for (const idx of schema.indexes.values()) {
20575
- if (idx.table !== table.name || !idx.unique || idx.isConstraint) continue;
20699
+ if (idx.table !== table.name || !idx.unique || idx.isConstraint || idx.valid === false) continue;
20576
20700
  specs.push({
20577
20701
  name: idx.name,
20578
20702
  keys: idx.columns.map((c) => ({
@@ -21723,6 +21847,25 @@ var EngineCtx = class {
21723
21847
  timezone() {
21724
21848
  return this.state.getSetting("timezone") ?? "UTC";
21725
21849
  }
21850
+ namespaceOid(input) {
21851
+ const trimmed = input.trim();
21852
+ const name = trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1).replaceAll('""', '"') : trimmed.toLowerCase();
21853
+ if (name === "pg_catalog") return 11;
21854
+ if (name === "information_schema") return 13212;
21855
+ const schema = this.state.schemas.get(name);
21856
+ if (!schema) throw pgError("undefined_object", `schema "${name}" does not exist`, "3F000");
21857
+ return schema.oid;
21858
+ }
21859
+ namespaceName(oid) {
21860
+ if (oid === 11) return "pg_catalog";
21861
+ if (oid === 13212) return "information_schema";
21862
+ for (const schema of this.state.schemas.values()) {
21863
+ if (schema.oid === oid) {
21864
+ return /^[a-z_][a-z0-9_$]*$/.test(schema.name) ? schema.name : `"${schema.name.replaceAll('"', '""')}"`;
21865
+ }
21866
+ }
21867
+ return null;
21868
+ }
21726
21869
  zoneOffsetAt(utcMicros) {
21727
21870
  return zoneOffsetAtUtc(this.timezone(), utcMicros);
21728
21871
  }
@@ -21993,7 +22136,7 @@ function pgClass(ctx) {
21993
22136
  for (const s of schemaData.sequences.values()) push(s.oid, s.name, s.schema, "S", 3, 1, false, true, s.temp);
21994
22137
  for (const i of schemaData.indexes.values()) {
21995
22138
  push(
21996
- state.schemas.get(i.schema)?.tables.get(i.table)?.oid ?? 0,
22139
+ i.oid ?? state.schemas.get(i.schema)?.tables.get(i.table)?.oid ?? 0,
21997
22140
  i.name,
21998
22141
  i.schema,
21999
22142
  "i",
@@ -22230,7 +22373,15 @@ function pgIndex(ctx) {
22230
22373
  for (const i of schema.indexes.values()) {
22231
22374
  const table = schema.tables.get(i.table);
22232
22375
  const isPrimary = i.isConstraint && table?.constraints.some((c) => c.kind === "primary_key" && c.name === i.name) === true;
22233
- rows.push([table?.oid ?? 0, table?.oid ?? 0, i.columns.length, i.unique, isPrimary, i.where !== null]);
22376
+ rows.push([
22377
+ i.oid ?? table?.oid ?? 0,
22378
+ table?.oid ?? 0,
22379
+ i.columns.length,
22380
+ i.unique,
22381
+ isPrimary,
22382
+ i.where !== null,
22383
+ i.valid !== false
22384
+ ]);
22234
22385
  }
22235
22386
  }
22236
22387
  return rel(
@@ -22240,12 +22391,73 @@ function pgIndex(ctx) {
22240
22391
  ["indnatts", "int2"],
22241
22392
  ["indisunique", "bool"],
22242
22393
  ["indisprimary", "bool"],
22243
- ["indpred", "bool"]
22394
+ ["indpred", "bool"],
22395
+ ["indisvalid", "bool"]
22244
22396
  ],
22245
22397
  rows,
22246
22398
  "pg_index"
22247
22399
  );
22248
22400
  }
22401
+ function pgStatUserTables(ctx) {
22402
+ return rel(
22403
+ [
22404
+ ["relid", "oid"],
22405
+ ["schemaname", "name"],
22406
+ ["relname", "name"],
22407
+ ["n_live_tup", "int8"]
22408
+ ],
22409
+ [...allTables(ctx.state)].map((table) => [table.oid, table.schema, table.name, BigInt(table.rowCount())]),
22410
+ "pg_stat_user_tables"
22411
+ );
22412
+ }
22413
+ function pgDescription(ctx) {
22414
+ const rows = [];
22415
+ for (const schema of ctx.state.schemas.values()) {
22416
+ for (const index of schema.indexes.values()) {
22417
+ if (index.comment) rows.push([index.oid ?? 0, 1259, 0, index.comment]);
22418
+ }
22419
+ }
22420
+ return rel(
22421
+ [
22422
+ ["objoid", "oid"],
22423
+ ["classoid", "oid"],
22424
+ ["objsubid", "int4"],
22425
+ ["description", "text"]
22426
+ ],
22427
+ rows,
22428
+ "pg_description"
22429
+ );
22430
+ }
22431
+ function pgStatUserIndexes(ctx) {
22432
+ const rows = [];
22433
+ for (const schema of ctx.state.schemas.values()) {
22434
+ if (schema.name === "pg_catalog" || schema.name === "information_schema" || schema.name.startsWith("pg_toast"))
22435
+ continue;
22436
+ for (const index of schema.indexes.values()) {
22437
+ const table = schema.tables.get(index.table);
22438
+ rows.push([
22439
+ table?.oid ?? 0,
22440
+ index.oid ?? table?.oid ?? 0,
22441
+ schema.name,
22442
+ index.table,
22443
+ index.name,
22444
+ index.scans ?? 0n
22445
+ ]);
22446
+ }
22447
+ }
22448
+ return rel(
22449
+ [
22450
+ ["relid", "oid"],
22451
+ ["indexrelid", "oid"],
22452
+ ["schemaname", "name"],
22453
+ ["relname", "name"],
22454
+ ["indexrelname", "name"],
22455
+ ["idx_scan", "int8"]
22456
+ ],
22457
+ rows,
22458
+ "pg_stat_user_indexes"
22459
+ );
22460
+ }
22249
22461
  function pgSequence(ctx) {
22250
22462
  const rows = [];
22251
22463
  for (const s of allSequences(ctx.state)) {
@@ -22360,7 +22572,7 @@ function pgIndexesView(ctx) {
22360
22572
  const rows = [];
22361
22573
  for (const schema of ctx.state.schemas.values()) {
22362
22574
  for (const i of schema.indexes.values()) {
22363
- rows.push([i.schema, i.table, i.name, null, null]);
22575
+ rows.push([i.schema, i.table, i.name, null, deparseIndex(i)]);
22364
22576
  }
22365
22577
  }
22366
22578
  return rel(
@@ -22681,6 +22893,12 @@ function buildCatalogRelation(ctx, schema, name) {
22681
22893
  return pgConstraint(ctx);
22682
22894
  case "pg_index":
22683
22895
  return pgIndex(ctx);
22896
+ case "pg_description":
22897
+ return pgDescription(ctx);
22898
+ case "pg_stat_user_tables":
22899
+ return pgStatUserTables(ctx);
22900
+ case "pg_stat_user_indexes":
22901
+ return pgStatUserIndexes(ctx);
22684
22902
  case "pg_sequence":
22685
22903
  return pgSequence(ctx);
22686
22904
  case "pg_sequences":
@@ -23264,6 +23482,7 @@ function executeCreateIndex(env, stmt) {
23264
23482
  }
23265
23483
  assertGinTrgmIndex(state, table, stmt);
23266
23484
  schema.indexes.set(name, {
23485
+ oid: state.nextOid(),
23267
23486
  name,
23268
23487
  schema: schema.name,
23269
23488
  table: table.name,
@@ -23276,7 +23495,8 @@ function executeCreateIndex(env, stmt) {
23276
23495
  })),
23277
23496
  where: stmt.where,
23278
23497
  nullsNotDistinct: stmt.nullsNotDistinct,
23279
- isConstraint: false
23498
+ isConstraint: false,
23499
+ valid: true
23280
23500
  });
23281
23501
  if (stmt.unique) {
23282
23502
  try {
@@ -23284,7 +23504,9 @@ function executeCreateIndex(env, stmt) {
23284
23504
  checkUnique(env, table, table.rowAt(i), i);
23285
23505
  }
23286
23506
  } catch (err) {
23287
- schema.indexes.delete(name);
23507
+ if (stmt.concurrently) schema.indexes.get(name).valid = false;
23508
+ else schema.indexes.delete(name);
23509
+ table.indexStores = null;
23288
23510
  throw err;
23289
23511
  }
23290
23512
  }
@@ -24404,6 +24626,9 @@ var Statement = class _Statement {
24404
24626
  const typed = params.map((p, i) => bindValueToTyped(p, i));
24405
24627
  let last = null;
24406
24628
  for (const stmt of this.statements) {
24629
+ if (stmt.type === "create_index" && stmt.concurrently && this.database.transactions.inTransaction) {
24630
+ throw pgError("misuse", "CREATE INDEX CONCURRENTLY cannot run inside a transaction block", "25001");
24631
+ }
24407
24632
  const env = {
24408
24633
  ctx: new EngineCtx(this.database.state),
24409
24634
  params: typed,