@crvouga/mockingbird-service-postgres 3.7.0 → 3.8.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.
@@ -56,6 +56,15 @@ type FaultRule = {
56
56
  requestHeaders?: Record<string, string>;
57
57
  /** Fault only requests carrying these query parameters. `"*"` matches any value. */
58
58
  query?: Record<string, string>;
59
+ /**
60
+ * Fault only requests whose JSON body carries these fields. Each key is a dotted path into
61
+ * the parsed body (`"patient_details.email"`; a number steps into an array, `"items.0.sku"`)
62
+ * and each value is what the field must equal: a string, number, boolean or `null`, compared
63
+ * strictly (`3` is not `"3"`). `"*"` matches any value that is present. A request whose body
64
+ * is missing, is not JSON, or is not a JSON object never matches. Not `body`, which is the
65
+ * response a rule injects.
66
+ */
67
+ requestBody?: Record<string, RequestBodyValue>;
59
68
  /**
60
69
  * Fault only this namespace. Omit (or `"*"`) to fault every namespace — which is what
61
70
  * an in-process caller usually wants, and what a parallel worker usually does not:
@@ -122,6 +131,8 @@ type FaultRule = {
122
131
  /** The preset this rule was expanded from, if any. */
123
132
  fromPreset?: string;
124
133
  };
134
+ /** What a field of the request body must equal in {@link FaultRule.requestBody}. */
135
+ type RequestBodyValue = string | number | boolean | null;
125
136
 
126
137
  /**
127
138
  * The admin controls every mock shares, as one document.
@@ -309,6 +320,13 @@ type ConfigService = {
309
320
  adminPrefix?: string;
310
321
  adminKey?: string;
311
322
  seed?: string;
323
+ /**
324
+ * Where this service's generated ids start: a non-negative integer or `"boot"` (derived from
325
+ * the clock). Replaces the fleet-wide `idBase`. Default: the same ids on every boot.
326
+ */
327
+ idBase?: number | string;
328
+ /** Boot settings, checked by the service's settings schema; every namespace starts from them. */
329
+ settings?: Record<string, unknown>;
312
330
  /** Admin controls this service boots with. A key given here replaces the fleet-wide one. */
313
331
  adminControls?: MockControls;
314
332
  fixtures?: Record<string, unknown>;
@@ -9965,7 +9965,7 @@ function evalTrgmWordSimilar(ctx, left, right) {
9965
9965
  const score = wordSimilarity(a.v, b.v);
9966
9966
  return tv("bool", score >= WORD_SIMILARITY_THRESHOLD);
9967
9967
  }
9968
- function assertGinTrgmIndex(state, table, stmt) {
9968
+ function assertGinTrgmIndex(state, table, stmt, expressionType) {
9969
9969
  const trgmCols = stmt.columns.filter((col2) => isGinTrgmOpclass(col2.opclass));
9970
9970
  if (trgmCols.length === 0) return;
9971
9971
  if (trgmCols.length !== stmt.columns.length || stmt.columns.length !== 1) {
@@ -9985,11 +9985,7 @@ function assertGinTrgmIndex(state, table, stmt) {
9985
9985
  if (stmt.unique) {
9986
9986
  throw pgError("feature_not_supported", `access method "gin" does not support unique indexes`, "0A000");
9987
9987
  }
9988
- if (col.expr.type !== "colref" || col.expr.parts.length !== 1) {
9989
- throw unsupported("expression gin_trgm_ops");
9990
- }
9991
- const index = table.columnIndex(col.expr.parts[0]);
9992
- const typeId = table.columns[index].type.id;
9988
+ const typeId = col.expr.type === "colref" && col.expr.parts.length === 1 ? table.columns[table.columnIndex(col.expr.parts[0])].type.id : expressionType(col.expr);
9993
9989
  if (!GIN_TRGM_TYPES.has(typeId)) {
9994
9990
  throw pgError(
9995
9991
  "datatype_mismatch",
@@ -13627,6 +13623,17 @@ function tableScope(table, row) {
13627
13623
  const cols = table.columns.map((c) => ({ name: c.name, type: c.type.id, table: table.name }));
13628
13624
  return new RowScope(cols, row, null, /* @__PURE__ */ new Set([table.name]));
13629
13625
  }
13626
+ function expressionTypeOf(env, table, expr) {
13627
+ const scope = makeEvalScope(
13628
+ env,
13629
+ tableScope(
13630
+ table,
13631
+ table.columns.map(() => null)
13632
+ )
13633
+ );
13634
+ const v = evalExpr(env.ctx, scope, expr);
13635
+ return v.t === "unknown" ? "text" : v.t;
13636
+ }
13630
13637
  function checkNotNull(env, table, row) {
13631
13638
  void env;
13632
13639
  for (let i = 0; i < table.columns.length; i++) {
@@ -16549,7 +16556,6 @@ var Parser = class {
16549
16556
  } while (this.eatKw("or"));
16550
16557
  this.expectKw("on");
16551
16558
  const table = this.qualifiedName();
16552
- if (this.eatKw("referencing")) throw unsupported("trigger transition tables");
16553
16559
  let deferrable = false;
16554
16560
  let initiallyDeferred = false;
16555
16561
  if (this.eatKw("not")) this.expectKw("deferrable");
@@ -16562,6 +16568,23 @@ var Parser = class {
16562
16568
  this.errorAt(this.peek(), "only constraint triggers can be deferred");
16563
16569
  if (initiallyDeferred && !deferrable)
16564
16570
  this.errorAt(this.peek(), "constraint declared INITIALLY DEFERRED must be DEFERRABLE");
16571
+ const transitions = [];
16572
+ if (!constraint && this.eatKw("referencing")) {
16573
+ do {
16574
+ const isNew = this.eatKw("new");
16575
+ if (!isNew) this.expectKw("old");
16576
+ if (this.eatKw("row")) {
16577
+ throw pgError(
16578
+ "feature_not_supported",
16579
+ "ROW variable naming in the REFERENCING clause is not supported",
16580
+ "0A000"
16581
+ );
16582
+ }
16583
+ this.expectKw("table");
16584
+ this.eatKw("as");
16585
+ transitions.push({ isNew, name: this.ident() });
16586
+ } while (this.atKw("new") || this.atKw("old"));
16587
+ }
16565
16588
  let forEachRow = false;
16566
16589
  if (this.eatKw("for")) {
16567
16590
  this.eatKw("each");
@@ -16599,7 +16622,8 @@ var Parser = class {
16599
16622
  forEachRow,
16600
16623
  when,
16601
16624
  funcName,
16602
- funcArgs
16625
+ funcArgs,
16626
+ ...transitions.length > 0 ? { transitions } : {}
16603
16627
  };
16604
16628
  }
16605
16629
  // --- ALTER --------------------------------------------------------------------
@@ -21145,6 +21169,29 @@ function fireRowTriggers(env, table, timing, event, oldRow, newRow) {
21145
21169
  }
21146
21170
  return { row };
21147
21171
  }
21172
+ function transitionRelation(table, name, rows) {
21173
+ return {
21174
+ columns: table.columns.map((c) => ({ name: c.name, type: c.type.id, table: name })),
21175
+ rows
21176
+ };
21177
+ }
21178
+ function fireStatementTriggers(env, table, timing, event, transitions) {
21179
+ const fired = table.triggers.filter((t) => !t.forEachRow && t.timing === timing && matches(t, event)).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
21180
+ for (const t of fired) {
21181
+ if (!executor) throw unsupported(`trigger "${t.name}" execution`);
21182
+ const ctes = /* @__PURE__ */ new Map();
21183
+ if (t.oldTable !== void 0)
21184
+ ctes.set(t.oldTable, transitionRelation(table, t.oldTable, transitions?.oldRows ?? []));
21185
+ if (t.newTable !== void 0)
21186
+ ctes.set(t.newTable, transitionRelation(table, t.newTable, transitions?.newRows ?? []));
21187
+ const changes = env.ctx.state.changes;
21188
+ try {
21189
+ executor({ ctx: env.ctx, params: null, ctes, outer: null }, table, t, event, null, null);
21190
+ } finally {
21191
+ env.ctx.state.changes = changes;
21192
+ }
21193
+ }
21194
+ }
21148
21195
  var keyOf = (schema, name) => JSON.stringify([schema, name]);
21149
21196
  function flushConstraintTriggers(state, all = false) {
21150
21197
  const env = { ctx: new EngineCtx(state), params: null, ctes: /* @__PURE__ */ new Map(), outer: null };
@@ -21528,7 +21575,13 @@ function executeInsert(env0, stmt) {
21528
21575
  const sourceRows = insertSourceRows(env, stmt, colIdxs);
21529
21576
  const insertedRows = [];
21530
21577
  let insertedCount = 0;
21578
+ const addedRows = [];
21579
+ const conflictOldRows = [];
21580
+ const conflictNewRows = [];
21581
+ const updatesOnConflict = stmt.onConflict !== null && stmt.onConflict.action !== "nothing";
21531
21582
  return withStatementRollback(env, () => {
21583
+ fireStatementTriggers(env, table, "before", "insert");
21584
+ if (updatesOnConflict) fireStatementTriggers(env, table, "before", "update");
21532
21585
  for (const srcRow of sourceRows) {
21533
21586
  const row = table.columns.map(() => null);
21534
21587
  const provided = /* @__PURE__ */ new Set();
@@ -21574,6 +21627,7 @@ function executeInsert(env0, stmt) {
21574
21627
  }
21575
21628
  if (conflictIdx !== null && conflictWhereOk) {
21576
21629
  if (stmt.onConflict.action === "nothing") continue;
21630
+ const replaced = rows[conflictIdx];
21577
21631
  const did = applyOnConflictUpdate(
21578
21632
  env,
21579
21633
  table,
@@ -21586,6 +21640,8 @@ function executeInsert(env0, stmt) {
21586
21640
  if (did) {
21587
21641
  insertedCount++;
21588
21642
  insertedRows.push(rows[conflictIdx]);
21643
+ conflictOldRows.push(replaced);
21644
+ conflictNewRows.push(rows[conflictIdx]);
21589
21645
  fireRowTriggers(env, table, "after", "update", null, rows[conflictIdx]);
21590
21646
  }
21591
21647
  continue;
@@ -21600,8 +21656,13 @@ function executeInsert(env0, stmt) {
21600
21656
  indexInsertRow(env, table, rows.length - 1, newRow);
21601
21657
  insertedCount++;
21602
21658
  insertedRows.push(newRow);
21659
+ addedRows.push(newRow);
21603
21660
  fireRowTriggers(env, table, "after", "insert", null, newRow);
21604
21661
  }
21662
+ fireStatementTriggers(env, table, "after", "insert", { oldRows: [], newRows: addedRows });
21663
+ if (updatesOnConflict) {
21664
+ fireStatementTriggers(env, table, "after", "update", { oldRows: conflictOldRows, newRows: conflictNewRows });
21665
+ }
21605
21666
  env.ctx.state.changes = insertedCount;
21606
21667
  if (stmt.returning) {
21607
21668
  const res = evalReturning(env, table, label, stmt.returning, insertedRows, "INSERT");
@@ -21682,10 +21743,12 @@ function executeUpdate(env0, stmt) {
21682
21743
  const env = applyWith(env0, stmt.with);
21683
21744
  const table = requireTargetTable(env, stmt.table, "update");
21684
21745
  const label = stmt.alias ?? table.name;
21746
+ fireStatementTriggers(env, table, "before", "update");
21685
21747
  const fromSource = stmt.from.length > 0 ? buildFrom(env, stmt.from) : null;
21686
21748
  const rows = table.mutableRows();
21687
21749
  const targetCols = table.columns.map((c) => ({ name: c.name, type: c.type.id, table: label }));
21688
21750
  const updatedRows = [];
21751
+ const replacedRows = [];
21689
21752
  let updateCount = 0;
21690
21753
  for (let ri = 0; ri < rows.length; ri++) {
21691
21754
  const oldRow = rows[ri];
@@ -21728,8 +21791,10 @@ function executeUpdate(env0, stmt) {
21728
21791
  handleReferencedUpdate(env, table, [oldRow], [finalRow]);
21729
21792
  updateCount++;
21730
21793
  updatedRows.push(finalRow);
21794
+ replacedRows.push(oldRow);
21731
21795
  fireRowTriggers(env, table, "after", "update", oldRow, finalRow);
21732
21796
  }
21797
+ fireStatementTriggers(env, table, "after", "update", { oldRows: replacedRows, newRows: updatedRows });
21733
21798
  env.ctx.state.changes = updateCount;
21734
21799
  if (stmt.returning) {
21735
21800
  const res = evalReturning(env, table, label, stmt.returning, updatedRows, "UPDATE");
@@ -21741,6 +21806,7 @@ function executeDelete(env0, stmt) {
21741
21806
  const env = applyWith(env0, stmt.with);
21742
21807
  const table = requireTargetTable(env, stmt.table, "delete from");
21743
21808
  const label = stmt.alias ?? table.name;
21809
+ fireStatementTriggers(env, table, "before", "delete");
21744
21810
  const rows = table.mutableRows();
21745
21811
  const usingSource = stmt.using.length > 0 ? buildFrom(env, stmt.using) : null;
21746
21812
  const targetCols = table.columns.map((c) => ({ name: c.name, type: c.type.id, table: label }));
@@ -21778,6 +21844,7 @@ function executeDelete(env0, stmt) {
21778
21844
  fireRowTriggers(env, table, "after", "delete", row, null);
21779
21845
  }
21780
21846
  if (deletedRows.length > 0) rebuildTableIndexes(env, table);
21847
+ fireStatementTriggers(env, table, "after", "delete", { oldRows: deletedRows, newRows: [] });
21781
21848
  env.ctx.state.changes = deletedRows.length;
21782
21849
  if (stmt.returning) {
21783
21850
  const res = evalReturning(env, table, label, stmt.returning, deletedRows, "DELETE");
@@ -23920,7 +23987,7 @@ function pgTrigger(ctx) {
23920
23987
  let oid = 4e4;
23921
23988
  for (const t of allTables(ctx.state)) {
23922
23989
  for (const tr of t.triggers) {
23923
- rows.push([oid++, tr.name, t.oid, !tr.forEachRow]);
23990
+ rows.push([oid++, tr.name, t.oid, !tr.forEachRow, tr.oldTable ?? null, tr.newTable ?? null]);
23924
23991
  }
23925
23992
  }
23926
23993
  return rel2(
@@ -23928,7 +23995,9 @@ function pgTrigger(ctx) {
23928
23995
  ["oid", "oid"],
23929
23996
  ["tgname", "name"],
23930
23997
  ["tgrelid", "oid"],
23931
- ["tgisinternal", "bool"]
23998
+ ["tgisinternal", "bool"],
23999
+ ["tgoldtable", "name"],
24000
+ ["tgnewtable", "name"]
23932
24001
  ],
23933
24002
  rows,
23934
24003
  "pg_trigger"
@@ -25355,7 +25424,7 @@ function executeCreateIndex(env, stmt) {
25355
25424
  }
25356
25425
  }
25357
25426
  }
25358
- assertGinTrgmIndex(state, table, stmt);
25427
+ assertGinTrgmIndex(state, table, stmt, (expr) => expressionTypeOf(env, table, expr));
25359
25428
  const method = stmt.using ?? "btree";
25360
25429
  if (!ACCESS_METHODS.has(method) || method === "heap") {
25361
25430
  throw pgError("undefined_object", `access method "${method}" does not exist`, "42704");
@@ -25610,6 +25679,50 @@ function executeCreateFunction(env, stmt) {
25610
25679
  schema.functions.set(name, existing);
25611
25680
  return commandResult("CREATE FUNCTION", 0);
25612
25681
  }
25682
+ function validateTransitionTables(stmt) {
25683
+ const transitions = stmt.transitions ?? [];
25684
+ if (transitions.length === 0) return {};
25685
+ const invalid = (message2) => pgError("invalid_table_definition", message2, "42P17");
25686
+ const unsupportedClause = (message2) => pgError("feature_not_supported", message2, "0A000");
25687
+ if (stmt.timing !== "after") throw invalid("transition table name can only be specified for an AFTER trigger");
25688
+ if (stmt.events.some((e) => e.event === "truncate")) {
25689
+ throw unsupportedClause("TRUNCATE triggers with transition tables are not supported");
25690
+ }
25691
+ if (stmt.events.some((e) => e.columns !== null)) {
25692
+ throw unsupportedClause("transition tables cannot be specified for triggers with column lists");
25693
+ }
25694
+ if (stmt.events.length > 1) {
25695
+ throw unsupportedClause("transition tables cannot be specified for triggers with more than one event");
25696
+ }
25697
+ const event = stmt.events[0].event;
25698
+ let oldTable;
25699
+ let newTable;
25700
+ for (const transition of transitions) {
25701
+ if (transition.isNew) {
25702
+ if (event !== "insert" && event !== "update") {
25703
+ throw invalid("NEW TABLE can only be specified for an INSERT or UPDATE trigger");
25704
+ }
25705
+ if (newTable !== void 0) throw invalid("NEW TABLE cannot be specified multiple times");
25706
+ newTable = transition.name;
25707
+ } else {
25708
+ if (event !== "delete" && event !== "update") {
25709
+ throw invalid("OLD TABLE can only be specified for a DELETE or UPDATE trigger");
25710
+ }
25711
+ if (oldTable !== void 0) throw invalid("OLD TABLE cannot be specified multiple times");
25712
+ oldTable = transition.name;
25713
+ }
25714
+ }
25715
+ if (oldTable !== void 0 && oldTable === newTable) {
25716
+ throw invalid("OLD TABLE name and NEW TABLE name cannot be the same");
25717
+ }
25718
+ if (stmt.forEachRow) {
25719
+ throw pgError("unsupported", "transition tables on FOR EACH ROW triggers are not supported", "0A000");
25720
+ }
25721
+ return {
25722
+ ...oldTable !== void 0 ? { oldTable } : {},
25723
+ ...newTable !== void 0 ? { newTable } : {}
25724
+ };
25725
+ }
25613
25726
  function executeCreateTrigger(env, stmt) {
25614
25727
  if (stmt.constraint && (stmt.timing !== "after" || !stmt.forEachRow))
25615
25728
  throw pgError("syntax", "constraint triggers must be AFTER ROW triggers", "42601");
@@ -25619,6 +25732,7 @@ function executeCreateTrigger(env, stmt) {
25619
25732
  throw pgError("undefined_table", `relation "${stmt.table.join(".")}" does not exist`, "42P01");
25620
25733
  }
25621
25734
  const table = state.ensureWritableTable(found);
25735
+ const transitionTables = validateTransitionTables(stmt);
25622
25736
  const fnSchema = stmt.funcName.length >= 2 ? stmt.funcName[stmt.funcName.length - 2] : null;
25623
25737
  const fnName = stmt.funcName[stmt.funcName.length - 1];
25624
25738
  const fns = state.findFunctions(stmt.funcName);
@@ -25637,6 +25751,7 @@ function executeCreateTrigger(env, stmt) {
25637
25751
  timing: stmt.timing,
25638
25752
  events: stmt.events,
25639
25753
  forEachRow: stmt.forEachRow,
25754
+ ...transitionTables,
25640
25755
  when: stmt.when,
25641
25756
  funcSchema: fnSchema ?? fns[0].schema,
25642
25757
  funcName: fnName,