@crvouga/mockingbird-service-postgres 3.0.0 → 3.0.2

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/dist/wire/cli.js CHANGED
@@ -1098,6 +1098,12 @@ function pgError(category, message2, sqlState) {
1098
1098
  function unsupported(what) {
1099
1099
  return new PostgresError("unsupported", `${what} is not supported`);
1100
1100
  }
1101
+ function isPostgresError(err) {
1102
+ if (err instanceof PostgresError) return true;
1103
+ if (!(err instanceof Error) || err.name !== "PostgresError") return false;
1104
+ const sqlState = err.sqlState;
1105
+ return typeof sqlState === "string" && /^[0-9A-Z]{5}$/.test(sqlState);
1106
+ }
1101
1107
 
1102
1108
  // src/runtime/prng.ts
1103
1109
  var Prng = class _Prng {
@@ -3853,6 +3859,7 @@ function arrayElemNeedsQuote(s) {
3853
3859
  function quoteArrayElem(s) {
3854
3860
  return `"${s.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
3855
3861
  }
3862
+ var UTC_OUTPUT = { zoneOffsetAt: () => 0 };
3856
3863
  function arrayText(arr, ctx) {
3857
3864
  if (arr.dims.length === 0) return "{}";
3858
3865
  const needsLb = arr.lbs.some((l) => l !== 1);
@@ -25435,9 +25442,48 @@ var RENAME_DATABASE = new RegExp(`^alter\\s+database\\s+${IDENT}\\s+rename\\s+to
25435
25442
  var identifier = (quoted, plain) => quoted === void 0 ? plain.toLowerCase() : quoted.replaceAll('""', '"');
25436
25443
  var ROW_LOCK = /\s+for\s+(?:no\s+key\s+update|key\s+share|update|share)(?:\s+of\s+(?:(?:"[^"]+"|[A-Za-z_][\w$]*)(?:\.(?:"[^"]+"|[A-Za-z_][\w$]*))?)(?:\s*,\s*(?:(?:"[^"]+"|[A-Za-z_][\w$]*)(?:\.(?:"[^"]+"|[A-Za-z_][\w$]*))?))*)?(?:\s+(nowait|skip\s+locked))?\s*$/i;
25437
25444
  var LIMIT = /\s+limit\s+(\d+)(?:\s+offset\s+(\d+))?\s*$/i;
25438
- var FROM_TABLE = /\bfrom\s+((?:"[^"]+"|[A-Za-z_][\w$]*)(?:\.(?:"[^"]+"|[A-Za-z_][\w$]*))?)/i;
25439
- var UPDATE_TARGET = /^update\s+((?:"[^"]+"|[A-Za-z_][\w$]*)(?:\.(?:"[^"]+"|[A-Za-z_][\w$]*))?)(?:\s+(?:as\s+)?(?:"[^"]+"|[A-Za-z_][\w$]*))?\s+set\s+[\s\S]*?(?:\s+where\s+([\s\S]*?))?(?:\s+returning\s+[\s\S]*)?$/i;
25440
- var DELETE_TARGET = /^delete\s+from\s+((?:"[^"]+"|[A-Za-z_][\w$]*)(?:\.(?:"[^"]+"|[A-Za-z_][\w$]*))?)(?:\s+(?:as\s+)?(?:"[^"]+"|[A-Za-z_][\w$]*))?(?:\s+where\s+([\s\S]*?))?(?:\s+returning\s+[\s\S]*)?$/i;
25445
+ var SQL_NAME = String.raw`(?:"[^"]+"|[A-Za-z_][\w$]*)`;
25446
+ var SQL_RELATION = String.raw`(?:"[^"]+"|[A-Za-z_][\w$]*)(?:\.(?:"[^"]+"|[A-Za-z_][\w$]*))?`;
25447
+ var FROM_TABLE = new RegExp(String.raw`\bfrom\s+(${SQL_RELATION})`, "i");
25448
+ var UPDATE_TARGET = new RegExp(
25449
+ String.raw`^update\s+(${SQL_RELATION})(?:\s+(?:as\s+)?(${SQL_NAME}))?\s+set\s+[\s\S]*?(?:\s+where\s+([\s\S]*?))?(?:\s+returning\s+[\s\S]*)?$`,
25450
+ "i"
25451
+ );
25452
+ var DELETE_TARGET = new RegExp(
25453
+ String.raw`^delete\s+from\s+(${SQL_RELATION})(?:\s+(?:as\s+)?(${SQL_NAME}))?(?:\s+where\s+([\s\S]*?))?(?:\s+returning\s+[\s\S]*)?$`,
25454
+ "i"
25455
+ );
25456
+ var lockedMutationQuery = (sql) => {
25457
+ const match = UPDATE_TARGET.exec(sql) ?? DELETE_TARGET.exec(sql);
25458
+ const relation = match?.[1];
25459
+ if (!relation) return null;
25460
+ const alias = match?.[2];
25461
+ const where = match?.[3];
25462
+ const from = alias ? `${relation} AS ${alias}` : relation;
25463
+ return `SELECT * FROM ${from}${where ? ` WHERE ${where}` : ""}`;
25464
+ };
25465
+ var lockLiteral = (expr, params) => {
25466
+ if (!expr) return void 0;
25467
+ switch (expr.type) {
25468
+ case "null_lit":
25469
+ return null;
25470
+ case "string_lit":
25471
+ return expr.value;
25472
+ case "bool_lit":
25473
+ return expr.value ? "t" : "f";
25474
+ case "number_lit":
25475
+ return expr.raw.replace(/^\+/, "");
25476
+ case "param": {
25477
+ const value = params[expr.index - 1];
25478
+ if (value === void 0) return void 0;
25479
+ const typed = bindValueToTyped(value, expr.index - 1);
25480
+ if (typed.v === null) return null;
25481
+ return datumText(typed.t, typed.v, UTC_OUTPUT);
25482
+ }
25483
+ default:
25484
+ return void 0;
25485
+ }
25486
+ };
25441
25487
  var TYPLEN = {
25442
25488
  bool: 1,
25443
25489
  int2: 2,
@@ -25456,7 +25502,7 @@ var TEXT_TYPES2 = /* @__PURE__ */ new Set(["text", "varchar", "bpchar", "name",
25456
25502
  var PG_EPOCH_MS = 9466848e5;
25457
25503
  var pgError2 = (category, message2, code) => new PostgresError(category, message2, code);
25458
25504
  var errorFields = (error) => {
25459
- if (error instanceof PostgresError) {
25505
+ if (isPostgresError(error)) {
25460
25506
  const message2 = error.message;
25461
25507
  const fields = { code: error.sqlState, message: message2 };
25462
25508
  const constraint = /constraint "([^"]+)"/.exec(message2)?.[1];
@@ -25574,6 +25620,46 @@ var decodeParam = (oid, format, bytes) => {
25574
25620
  throw pgError2("internal", `binary parameter format is not supported for type oid ${oid}`, "0A000");
25575
25621
  }
25576
25622
  };
25623
+ function returningShape(db, sql) {
25624
+ let statements;
25625
+ try {
25626
+ statements = parse(sql);
25627
+ } catch {
25628
+ return null;
25629
+ }
25630
+ const stmt = statements.length === 1 ? statements[0] : void 0;
25631
+ if (!stmt || stmt.type !== "insert" && stmt.type !== "update" && stmt.type !== "delete") return null;
25632
+ const targets = stmt.returning;
25633
+ if (!targets || targets.length === 0) return null;
25634
+ const table = db.state.findTable(stmt.table);
25635
+ if (!table) return null;
25636
+ const label = stmt.alias ?? table.name;
25637
+ const columns = [];
25638
+ const columnTypes = [];
25639
+ for (const target of targets) {
25640
+ if (target.expr.type === "star") {
25641
+ const qualifier = target.expr.table?.at(-1);
25642
+ if (qualifier !== void 0 && qualifier !== label && qualifier !== table.name) return null;
25643
+ for (const column of table.columns) {
25644
+ columns.push(column.name);
25645
+ columnTypes.push(column.type.id);
25646
+ }
25647
+ continue;
25648
+ }
25649
+ if (target.expr.type === "colref") {
25650
+ const name = target.expr.parts.at(-1) ?? "?column?";
25651
+ const column = table.columns.find((item) => item.name === name);
25652
+ columns.push(target.alias ?? name);
25653
+ columnTypes.push(column?.type.id ?? "text");
25654
+ continue;
25655
+ }
25656
+ columns.push(target.alias ?? inferColumnName(target.expr));
25657
+ columnTypes.push("text");
25658
+ }
25659
+ if (columns.length === 0) return null;
25660
+ const command2 = stmt.type === "insert" ? "INSERT" : stmt.type === "update" ? "UPDATE" : "DELETE";
25661
+ return { columns, columnTypes, rows: [], rowCount: 0, command: command2 };
25662
+ }
25577
25663
  var Connection = class {
25578
25664
  constructor(socket, cluster, options2) {
25579
25665
  this.socket = socket;
@@ -25852,8 +25938,8 @@ var Connection = class {
25852
25938
  return outcome;
25853
25939
  } catch (error) {
25854
25940
  if (this.inTransaction) this.aborted = true;
25855
- if (error instanceof PostgresError && error.sqlState === "40P01") this.cluster.unlockAll(this, true);
25856
- const status = error instanceof PostgresError ? error.sqlState : "XX000";
25941
+ if (isPostgresError(error) && error.sqlState === "40P01") this.cluster.unlockAll(this, true);
25942
+ const status = isPostgresError(error) ? error.sqlState : "XX000";
25857
25943
  this.options.onLog?.({ pid: this.pid, sql: text, durationMs: performance.now() - started, status });
25858
25944
  throw error;
25859
25945
  }
@@ -26049,14 +26135,48 @@ var Connection = class {
26049
26135
  }
26050
26136
  });
26051
26137
  }
26138
+ /**
26139
+ * Hold the primary key until the transaction ends. PostgreSQL's unique index makes a
26140
+ * second `INSERT` of the same key wait; `ON CONFLICT DO UPDATE` then sees the committed row.
26141
+ * Without this, each session's workspace inserts the key and commit replay raises 23505.
26142
+ */
26143
+ lockInsertedPrimaryKey(db, text, params) {
26144
+ let statements;
26145
+ try {
26146
+ statements = parse(text);
26147
+ } catch {
26148
+ return;
26149
+ }
26150
+ const stmt = statements.length === 1 ? statements[0] : void 0;
26151
+ if (stmt?.type !== "insert" || stmt.source === "default_values" || stmt.source.body.type !== "values") return;
26152
+ if (!stmt.columns) return;
26153
+ const table = db.state.findTable(stmt.table);
26154
+ const primary = table?.constraints.find((constraint) => constraint.kind === "primary_key");
26155
+ if (!primary || primary.columns.length === 0) return;
26156
+ const indexes = primary.columns.map((column) => stmt.columns?.indexOf(column) ?? -1);
26157
+ if (indexes.some((index) => index < 0)) return;
26158
+ const identities = [];
26159
+ for (const row of stmt.source.body.rows) {
26160
+ const identity = [];
26161
+ for (const index of indexes) {
26162
+ const value = lockLiteral(row[index], params);
26163
+ if (value === void 0) return;
26164
+ identity.push(value);
26165
+ }
26166
+ identities.push(identity);
26167
+ }
26168
+ const relation = stmt.table.join(".");
26169
+ for (const identity of identities) {
26170
+ const key = `row:${this.databaseName}:${relation}:${JSON.stringify(identity)}`;
26171
+ if (!this.cluster.tryLock(this, key, true)) throw new LockWait(key);
26172
+ }
26173
+ }
26052
26174
  runQuery(db, text, params) {
26053
26175
  const lock = ROW_LOCK.exec(text);
26054
26176
  if (!lock) {
26055
- const mutation = UPDATE_TARGET.exec(text) ?? DELETE_TARGET.exec(text);
26056
- if (mutation) {
26057
- const lockQuery = `SELECT * FROM ${mutation[1]}${mutation[2] ? ` WHERE ${mutation[2]}` : ""}`;
26058
- this.lockRows(db, lockQuery, params, "wait");
26059
- }
26177
+ const lockQuery = lockedMutationQuery(text);
26178
+ if (lockQuery) this.lockRows(db, lockQuery, params, "wait");
26179
+ this.lockInsertedPrimaryKey(db, text, params);
26060
26180
  return db.prepare(text).textResult(...params);
26061
26181
  }
26062
26182
  let query = text.slice(0, lock.index).trimEnd();
@@ -26420,11 +26540,16 @@ var Connection = class {
26420
26540
  else this.write(backend.rowDescription(this.fields(outcome.result, portal.resultFormats)));
26421
26541
  }
26422
26542
  /**
26423
- * The row shape of a statement that returns rows, for Describe on a statement: a trial run with
26424
- * null parameters inside a transaction that is rolled back. Anything else (or a trial that
26425
- * fails) is described as returning no data.
26543
+ * The row shape of a statement that returns rows, for Describe on a statement.
26544
+ * `INSERT`/`UPDATE`/`DELETE ... RETURNING` is described from the catalog: a trial run binds
26545
+ * nulls and fails NOT NULL, and Describe would answer NoData. Bun then reads one DataRow cell
26546
+ * and treats the rest of the message as a broken protocol frame.
26547
+ * Other row-returning statements are a trial run with null parameters, rolled back. A trial
26548
+ * that fails is described as returning no data.
26426
26549
  */
26427
26550
  async shapeOf(prepared) {
26551
+ const described = returningShape(this.database, prepared.sql);
26552
+ if (described) return described;
26428
26553
  if (!RETURNS_ROWS.test(prepared.sql)) return null;
26429
26554
  const db = this.database;
26430
26555
  await this.cluster.acquireTurn(this);