@crvouga/mockingbird-service-postgres 3.0.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/dist/server.js CHANGED
@@ -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];
@@ -25004,8 +25050,8 @@ var Connection = class {
25004
25050
  return outcome;
25005
25051
  } catch (error) {
25006
25052
  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";
25053
+ if (isPostgresError(error) && error.sqlState === "40P01") this.cluster.unlockAll(this, true);
25054
+ const status = isPostgresError(error) ? error.sqlState : "XX000";
25009
25055
  this.options.onLog?.({ pid: this.pid, sql: text, durationMs: performance.now() - started, status });
25010
25056
  throw error;
25011
25057
  }
@@ -25201,14 +25247,48 @@ var Connection = class {
25201
25247
  }
25202
25248
  });
25203
25249
  }
25250
+ /**
25251
+ * Hold the primary key until the transaction ends. PostgreSQL's unique index makes a
25252
+ * second `INSERT` of the same key wait; `ON CONFLICT DO UPDATE` then sees the committed row.
25253
+ * Without this, each session's workspace inserts the key and commit replay raises 23505.
25254
+ */
25255
+ lockInsertedPrimaryKey(db, text, params) {
25256
+ let statements;
25257
+ try {
25258
+ statements = parse(text);
25259
+ } catch {
25260
+ return;
25261
+ }
25262
+ const stmt = statements.length === 1 ? statements[0] : void 0;
25263
+ if (stmt?.type !== "insert" || stmt.source === "default_values" || stmt.source.body.type !== "values") return;
25264
+ if (!stmt.columns) return;
25265
+ const table = db.state.findTable(stmt.table);
25266
+ const primary = table?.constraints.find((constraint) => constraint.kind === "primary_key");
25267
+ if (!primary || primary.columns.length === 0) return;
25268
+ const indexes = primary.columns.map((column) => stmt.columns?.indexOf(column) ?? -1);
25269
+ if (indexes.some((index) => index < 0)) return;
25270
+ const identities = [];
25271
+ for (const row of stmt.source.body.rows) {
25272
+ const identity = [];
25273
+ for (const index of indexes) {
25274
+ const value = lockLiteral(row[index], params);
25275
+ if (value === void 0) return;
25276
+ identity.push(value);
25277
+ }
25278
+ identities.push(identity);
25279
+ }
25280
+ const relation = stmt.table.join(".");
25281
+ for (const identity of identities) {
25282
+ const key = `row:${this.databaseName}:${relation}:${JSON.stringify(identity)}`;
25283
+ if (!this.cluster.tryLock(this, key, true)) throw new LockWait(key);
25284
+ }
25285
+ }
25204
25286
  runQuery(db, text, params) {
25205
25287
  const lock = ROW_LOCK.exec(text);
25206
25288
  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
- }
25289
+ const lockQuery = lockedMutationQuery(text);
25290
+ if (lockQuery) this.lockRows(db, lockQuery, params, "wait");
25291
+ this.lockInsertedPrimaryKey(db, text, params);
25212
25292
  return db.prepare(text).textResult(...params);
25213
25293
  }
25214
25294
  let query = text.slice(0, lock.index).trimEnd();