@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.
@@ -102,6 +102,12 @@ export declare class Connection implements Session {
102
102
  /** Rebase this transaction's successful writes onto the latest committed state. */
103
103
  private openWorkspace;
104
104
  private replay;
105
+ /**
106
+ * Hold the primary key until the transaction ends. PostgreSQL's unique index makes a
107
+ * second `INSERT` of the same key wait; `ON CONFLICT DO UPDATE` then sees the committed row.
108
+ * Without this, each session's workspace inserts the key and commit replay raises 23505.
109
+ */
110
+ private lockInsertedPrimaryKey;
105
111
  private runQuery;
106
112
  private lockRows;
107
113
  private rowLockKey;
@@ -125,9 +131,12 @@ export declare class Connection implements Session {
125
131
  private bind;
126
132
  private describe;
127
133
  /**
128
- * The row shape of a statement that returns rows, for Describe on a statement: a trial run with
129
- * null parameters inside a transaction that is rolled back. Anything else (or a trial that
130
- * fails) is described as returning no data.
134
+ * The row shape of a statement that returns rows, for Describe on a statement.
135
+ * `INSERT`/`UPDATE`/`DELETE ... RETURNING` is described from the catalog: a trial run binds
136
+ * nulls and fails NOT NULL, and Describe would answer NoData. Bun then reads one DataRow cell
137
+ * and treats the rest of the message as a broken protocol frame.
138
+ * Other row-returning statements are a trial run with null parameters, rolled back. A trial
139
+ * that fails is described as returning no data.
131
140
  */
132
141
  private shapeOf;
133
142
  private executePortal;
@@ -250,6 +250,12 @@ function pgError(category, message2, sqlState) {
250
250
  function unsupported(what) {
251
251
  return new PostgresError("unsupported", `${what} is not supported`);
252
252
  }
253
+ function isPostgresError(err) {
254
+ if (err instanceof PostgresError) return true;
255
+ if (!(err instanceof Error) || err.name !== "PostgresError") return false;
256
+ const sqlState = err.sqlState;
257
+ return typeof sqlState === "string" && /^[0-9A-Z]{5}$/.test(sqlState);
258
+ }
253
259
 
254
260
  // src/runtime/prng.ts
255
261
  var Prng = class _Prng {
@@ -3005,6 +3011,7 @@ function arrayElemNeedsQuote(s) {
3005
3011
  function quoteArrayElem(s) {
3006
3012
  return `"${s.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
3007
3013
  }
3014
+ var UTC_OUTPUT = { zoneOffsetAt: () => 0 };
3008
3015
  function arrayText(arr, ctx) {
3009
3016
  if (arr.dims.length === 0) return "{}";
3010
3017
  const needsLb = arr.lbs.some((l) => l !== 1);
@@ -24587,9 +24594,48 @@ var RENAME_DATABASE = new RegExp(`^alter\\s+database\\s+${IDENT}\\s+rename\\s+to
24587
24594
  var identifier = (quoted, plain) => quoted === void 0 ? plain.toLowerCase() : quoted.replaceAll('""', '"');
24588
24595
  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;
24589
24596
  var LIMIT = /\s+limit\s+(\d+)(?:\s+offset\s+(\d+))?\s*$/i;
24590
- var FROM_TABLE = /\bfrom\s+((?:"[^"]+"|[A-Za-z_][\w$]*)(?:\.(?:"[^"]+"|[A-Za-z_][\w$]*))?)/i;
24591
- 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;
24592
- 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;
24597
+ var SQL_NAME = String.raw`(?:"[^"]+"|[A-Za-z_][\w$]*)`;
24598
+ var SQL_RELATION = String.raw`(?:"[^"]+"|[A-Za-z_][\w$]*)(?:\.(?:"[^"]+"|[A-Za-z_][\w$]*))?`;
24599
+ var FROM_TABLE = new RegExp(String.raw`\bfrom\s+(${SQL_RELATION})`, "i");
24600
+ var UPDATE_TARGET = new RegExp(
24601
+ 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]*)?$`,
24602
+ "i"
24603
+ );
24604
+ var DELETE_TARGET = new RegExp(
24605
+ String.raw`^delete\s+from\s+(${SQL_RELATION})(?:\s+(?:as\s+)?(${SQL_NAME}))?(?:\s+where\s+([\s\S]*?))?(?:\s+returning\s+[\s\S]*)?$`,
24606
+ "i"
24607
+ );
24608
+ var lockedMutationQuery = (sql) => {
24609
+ const match = UPDATE_TARGET.exec(sql) ?? DELETE_TARGET.exec(sql);
24610
+ const relation = match?.[1];
24611
+ if (!relation) return null;
24612
+ const alias = match?.[2];
24613
+ const where = match?.[3];
24614
+ const from = alias ? `${relation} AS ${alias}` : relation;
24615
+ return `SELECT * FROM ${from}${where ? ` WHERE ${where}` : ""}`;
24616
+ };
24617
+ var lockLiteral = (expr, params) => {
24618
+ if (!expr) return void 0;
24619
+ switch (expr.type) {
24620
+ case "null_lit":
24621
+ return null;
24622
+ case "string_lit":
24623
+ return expr.value;
24624
+ case "bool_lit":
24625
+ return expr.value ? "t" : "f";
24626
+ case "number_lit":
24627
+ return expr.raw.replace(/^\+/, "");
24628
+ case "param": {
24629
+ const value = params[expr.index - 1];
24630
+ if (value === void 0) return void 0;
24631
+ const typed = bindValueToTyped(value, expr.index - 1);
24632
+ if (typed.v === null) return null;
24633
+ return datumText(typed.t, typed.v, UTC_OUTPUT);
24634
+ }
24635
+ default:
24636
+ return void 0;
24637
+ }
24638
+ };
24593
24639
  var TYPLEN = {
24594
24640
  bool: 1,
24595
24641
  int2: 2,
@@ -24608,7 +24654,7 @@ var TEXT_TYPES2 = /* @__PURE__ */ new Set(["text", "varchar", "bpchar", "name",
24608
24654
  var PG_EPOCH_MS = 9466848e5;
24609
24655
  var pgError2 = (category, message2, code) => new PostgresError(category, message2, code);
24610
24656
  var errorFields = (error) => {
24611
- if (error instanceof PostgresError) {
24657
+ if (isPostgresError(error)) {
24612
24658
  const message2 = error.message;
24613
24659
  const fields = { code: error.sqlState, message: message2 };
24614
24660
  const constraint = /constraint "([^"]+)"/.exec(message2)?.[1];
@@ -24726,6 +24772,46 @@ var decodeParam = (oid, format, bytes) => {
24726
24772
  throw pgError2("internal", `binary parameter format is not supported for type oid ${oid}`, "0A000");
24727
24773
  }
24728
24774
  };
24775
+ function returningShape(db, sql) {
24776
+ let statements;
24777
+ try {
24778
+ statements = parse(sql);
24779
+ } catch {
24780
+ return null;
24781
+ }
24782
+ const stmt = statements.length === 1 ? statements[0] : void 0;
24783
+ if (!stmt || stmt.type !== "insert" && stmt.type !== "update" && stmt.type !== "delete") return null;
24784
+ const targets = stmt.returning;
24785
+ if (!targets || targets.length === 0) return null;
24786
+ const table = db.state.findTable(stmt.table);
24787
+ if (!table) return null;
24788
+ const label = stmt.alias ?? table.name;
24789
+ const columns = [];
24790
+ const columnTypes = [];
24791
+ for (const target of targets) {
24792
+ if (target.expr.type === "star") {
24793
+ const qualifier = target.expr.table?.at(-1);
24794
+ if (qualifier !== void 0 && qualifier !== label && qualifier !== table.name) return null;
24795
+ for (const column of table.columns) {
24796
+ columns.push(column.name);
24797
+ columnTypes.push(column.type.id);
24798
+ }
24799
+ continue;
24800
+ }
24801
+ if (target.expr.type === "colref") {
24802
+ const name = target.expr.parts.at(-1) ?? "?column?";
24803
+ const column = table.columns.find((item) => item.name === name);
24804
+ columns.push(target.alias ?? name);
24805
+ columnTypes.push(column?.type.id ?? "text");
24806
+ continue;
24807
+ }
24808
+ columns.push(target.alias ?? inferColumnName(target.expr));
24809
+ columnTypes.push("text");
24810
+ }
24811
+ if (columns.length === 0) return null;
24812
+ const command2 = stmt.type === "insert" ? "INSERT" : stmt.type === "update" ? "UPDATE" : "DELETE";
24813
+ return { columns, columnTypes, rows: [], rowCount: 0, command: command2 };
24814
+ }
24729
24815
  var Connection = class {
24730
24816
  constructor(socket, cluster, options) {
24731
24817
  this.socket = socket;
@@ -25004,8 +25090,8 @@ var Connection = class {
25004
25090
  return outcome;
25005
25091
  } catch (error) {
25006
25092
  if (this.inTransaction) this.aborted = true;
25007
- if (error instanceof PostgresError && error.sqlState === "40P01") this.cluster.unlockAll(this, true);
25008
- const status = error instanceof PostgresError ? error.sqlState : "XX000";
25093
+ if (isPostgresError(error) && error.sqlState === "40P01") this.cluster.unlockAll(this, true);
25094
+ const status = isPostgresError(error) ? error.sqlState : "XX000";
25009
25095
  this.options.onLog?.({ pid: this.pid, sql: text, durationMs: performance.now() - started, status });
25010
25096
  throw error;
25011
25097
  }
@@ -25201,14 +25287,48 @@ var Connection = class {
25201
25287
  }
25202
25288
  });
25203
25289
  }
25290
+ /**
25291
+ * Hold the primary key until the transaction ends. PostgreSQL's unique index makes a
25292
+ * second `INSERT` of the same key wait; `ON CONFLICT DO UPDATE` then sees the committed row.
25293
+ * Without this, each session's workspace inserts the key and commit replay raises 23505.
25294
+ */
25295
+ lockInsertedPrimaryKey(db, text, params) {
25296
+ let statements;
25297
+ try {
25298
+ statements = parse(text);
25299
+ } catch {
25300
+ return;
25301
+ }
25302
+ const stmt = statements.length === 1 ? statements[0] : void 0;
25303
+ if (stmt?.type !== "insert" || stmt.source === "default_values" || stmt.source.body.type !== "values") return;
25304
+ if (!stmt.columns) return;
25305
+ const table = db.state.findTable(stmt.table);
25306
+ const primary = table?.constraints.find((constraint) => constraint.kind === "primary_key");
25307
+ if (!primary || primary.columns.length === 0) return;
25308
+ const indexes = primary.columns.map((column) => stmt.columns?.indexOf(column) ?? -1);
25309
+ if (indexes.some((index) => index < 0)) return;
25310
+ const identities = [];
25311
+ for (const row of stmt.source.body.rows) {
25312
+ const identity = [];
25313
+ for (const index of indexes) {
25314
+ const value = lockLiteral(row[index], params);
25315
+ if (value === void 0) return;
25316
+ identity.push(value);
25317
+ }
25318
+ identities.push(identity);
25319
+ }
25320
+ const relation = stmt.table.join(".");
25321
+ for (const identity of identities) {
25322
+ const key = `row:${this.databaseName}:${relation}:${JSON.stringify(identity)}`;
25323
+ if (!this.cluster.tryLock(this, key, true)) throw new LockWait(key);
25324
+ }
25325
+ }
25204
25326
  runQuery(db, text, params) {
25205
25327
  const lock = ROW_LOCK.exec(text);
25206
25328
  if (!lock) {
25207
- const mutation = UPDATE_TARGET.exec(text) ?? DELETE_TARGET.exec(text);
25208
- if (mutation) {
25209
- const lockQuery = `SELECT * FROM ${mutation[1]}${mutation[2] ? ` WHERE ${mutation[2]}` : ""}`;
25210
- this.lockRows(db, lockQuery, params, "wait");
25211
- }
25329
+ const lockQuery = lockedMutationQuery(text);
25330
+ if (lockQuery) this.lockRows(db, lockQuery, params, "wait");
25331
+ this.lockInsertedPrimaryKey(db, text, params);
25212
25332
  return db.prepare(text).textResult(...params);
25213
25333
  }
25214
25334
  let query = text.slice(0, lock.index).trimEnd();
@@ -25572,11 +25692,16 @@ var Connection = class {
25572
25692
  else this.write(backend.rowDescription(this.fields(outcome.result, portal.resultFormats)));
25573
25693
  }
25574
25694
  /**
25575
- * The row shape of a statement that returns rows, for Describe on a statement: a trial run with
25576
- * null parameters inside a transaction that is rolled back. Anything else (or a trial that
25577
- * fails) is described as returning no data.
25695
+ * The row shape of a statement that returns rows, for Describe on a statement.
25696
+ * `INSERT`/`UPDATE`/`DELETE ... RETURNING` is described from the catalog: a trial run binds
25697
+ * nulls and fails NOT NULL, and Describe would answer NoData. Bun then reads one DataRow cell
25698
+ * and treats the rest of the message as a broken protocol frame.
25699
+ * Other row-returning statements are a trial run with null parameters, rolled back. A trial
25700
+ * that fails is described as returning no data.
25578
25701
  */
25579
25702
  async shapeOf(prepared) {
25703
+ const described = returningShape(this.database, prepared.sql);
25704
+ if (described) return described;
25580
25705
  if (!RETURNS_ROWS.test(prepared.sql)) return null;
25581
25706
  const db = this.database;
25582
25707
  await this.cluster.acquireTurn(this);