@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.
@@ -645,6 +645,11 @@ export interface CreateTriggerStmt {
645
645
  readonly when: Expr | null;
646
646
  readonly funcName: string[];
647
647
  readonly funcArgs: string[];
648
+ /** `REFERENCING { OLD | NEW } TABLE [AS] name` items in source order; validated when the trigger is created */
649
+ readonly transitions?: ReadonlyArray<{
650
+ isNew: boolean;
651
+ name: string;
652
+ }>;
648
653
  }
649
654
  export type AlterTableAction = {
650
655
  kind: "add_column";
@@ -1,6 +1,12 @@
1
+ import type { Expr } from "../ast/nodes.js";
1
2
  import type { ExecEnv } from "../executor/relation.js";
2
3
  import type { IndexMeta, TableData } from "../storage/database-state.js";
3
- import { type Datum } from "../types/value.js";
4
+ import { type Datum, type TypeId } from "../types/value.js";
5
+ /**
6
+ * The type an expression over `table` yields, found by evaluating it against an all-NULL row.
7
+ * Binding errors (unknown column or function) surface as they would when PostgreSQL parses the expression.
8
+ */
9
+ export declare function expressionTypeOf(env: ExecEnv, table: TableData, expr: Expr): TypeId;
4
10
  /** NOT NULL column constraints (23502). */
5
11
  export declare function checkNotNull(env: ExecEnv, table: TableData, row: Datum[]): void;
6
12
  /** CHECK constraints (23514); NULL results pass. */
@@ -16,5 +16,18 @@ export type TriggerExecutor = (env: ExecEnv, table: TableData, trigger: TriggerM
16
16
  export declare function setTriggerExecutor(e: TriggerExecutor): void;
17
17
  export declare function fireRowTriggers(env: ExecEnv, table: TableData, timing: "before" | "after" | "instead_of", event: TriggerEvent, oldRow: Datum[] | null, newRow: Datum[] | null): TriggerFireResult;
18
18
  export declare function hasRowTriggers(table: TableData, timing: "before" | "after" | "instead_of", event: TriggerEvent): boolean;
19
+ /** The rows a statement changed, as the `REFERENCING` transition tables of its AFTER statement triggers. */
20
+ export interface TransitionRows {
21
+ /** rows before the change (UPDATE, DELETE) */
22
+ oldRows: Datum[][];
23
+ /** rows after the change (INSERT, UPDATE); position-aligned with `oldRows` for UPDATE */
24
+ newRows: Datum[][];
25
+ }
26
+ /**
27
+ * Fire the statement-level triggers of `table` for `event`, once, in trigger-name order. AFTER triggers
28
+ * that declare transition tables read them as relations named by their `REFERENCING` clause; the
29
+ * function body sees nothing else of the statement (no outer CTEs, parameters or scope).
30
+ */
31
+ export declare function fireStatementTriggers(env: ExecEnv, table: TableData, timing: "before" | "after", event: TriggerEvent, transitions?: TransitionRows): void;
19
32
  export declare function flushConstraintTriggers(state: DatabaseState, all?: boolean): void;
20
33
  export declare function setConstraints(state: DatabaseState, stmt: SetConstraintsStmt): void;
@@ -1,7 +1,7 @@
1
- import type { CreateIndexStmt } from "../ast/nodes.js";
1
+ import type { CreateIndexStmt, Expr } from "../ast/nodes.js";
2
2
  import type { EngineCtx } from "../expressions/context.js";
3
3
  import type { DatabaseState, FunctionData, TableData } from "../storage/database-state.js";
4
- import { type TypedValue } from "../types/value.js";
4
+ import { type TypedValue, type TypeId } from "../types/value.js";
5
5
  export declare function pgTrgmSchema(state: DatabaseState): string | null;
6
6
  export declare function pgTrgmInstalled(state: DatabaseState): boolean;
7
7
  export declare function installPgTrgm(state: DatabaseState, schemaName: string): void;
@@ -11,10 +11,10 @@ export declare function evalPgTrgmFunction(ctx: EngineCtx, fn: FunctionData, arg
11
11
  export declare function evalTrgmWordSimilar(ctx: EngineCtx, left: TypedValue, right: TypedValue): TypedValue;
12
12
  /**
13
13
  * Reject `gin_trgm_ops` unless pg_trgm is installed for access method gin.
14
- * A single text/varchar column is stored as an ordinary non-unique index:
14
+ * A single text/varchar column or expression is stored as an ordinary non-unique index:
15
15
  * lookup speed is not modelled.
16
16
  */
17
- export declare function assertGinTrgmIndex(state: DatabaseState, table: TableData, stmt: CreateIndexStmt): void;
17
+ export declare function assertGinTrgmIndex(state: DatabaseState, table: TableData, stmt: CreateIndexStmt, expressionType: (expr: Expr) => TypeId): void;
18
18
  /** `|intersection| / |union|` as float4. Either empty trigram set yields 0. */
19
19
  export declare function trigramSimilarity(left: string, right: string): number;
20
20
  /**
package/dist/index.js CHANGED
@@ -9955,7 +9955,7 @@ function evalTrgmWordSimilar(ctx, left, right) {
9955
9955
  const score = wordSimilarity(a.v, b.v);
9956
9956
  return tv("bool", score >= WORD_SIMILARITY_THRESHOLD);
9957
9957
  }
9958
- function assertGinTrgmIndex(state, table, stmt) {
9958
+ function assertGinTrgmIndex(state, table, stmt, expressionType) {
9959
9959
  const trgmCols = stmt.columns.filter((col2) => isGinTrgmOpclass(col2.opclass));
9960
9960
  if (trgmCols.length === 0) return;
9961
9961
  if (trgmCols.length !== stmt.columns.length || stmt.columns.length !== 1) {
@@ -9975,11 +9975,7 @@ function assertGinTrgmIndex(state, table, stmt) {
9975
9975
  if (stmt.unique) {
9976
9976
  throw pgError("feature_not_supported", `access method "gin" does not support unique indexes`, "0A000");
9977
9977
  }
9978
- if (col.expr.type !== "colref" || col.expr.parts.length !== 1) {
9979
- throw unsupported("expression gin_trgm_ops");
9980
- }
9981
- const index = table.columnIndex(col.expr.parts[0]);
9982
- const typeId = table.columns[index].type.id;
9978
+ const typeId = col.expr.type === "colref" && col.expr.parts.length === 1 ? table.columns[table.columnIndex(col.expr.parts[0])].type.id : expressionType(col.expr);
9983
9979
  if (!GIN_TRGM_TYPES.has(typeId)) {
9984
9980
  throw pgError(
9985
9981
  "datatype_mismatch",
@@ -13617,6 +13613,17 @@ function tableScope(table, row) {
13617
13613
  const cols = table.columns.map((c) => ({ name: c.name, type: c.type.id, table: table.name }));
13618
13614
  return new RowScope(cols, row, null, /* @__PURE__ */ new Set([table.name]));
13619
13615
  }
13616
+ function expressionTypeOf(env, table, expr) {
13617
+ const scope = makeEvalScope(
13618
+ env,
13619
+ tableScope(
13620
+ table,
13621
+ table.columns.map(() => null)
13622
+ )
13623
+ );
13624
+ const v = evalExpr(env.ctx, scope, expr);
13625
+ return v.t === "unknown" ? "text" : v.t;
13626
+ }
13620
13627
  function checkNotNull(env, table, row) {
13621
13628
  void env;
13622
13629
  for (let i = 0; i < table.columns.length; i++) {
@@ -16539,7 +16546,6 @@ var Parser = class {
16539
16546
  } while (this.eatKw("or"));
16540
16547
  this.expectKw("on");
16541
16548
  const table = this.qualifiedName();
16542
- if (this.eatKw("referencing")) throw unsupported("trigger transition tables");
16543
16549
  let deferrable = false;
16544
16550
  let initiallyDeferred = false;
16545
16551
  if (this.eatKw("not")) this.expectKw("deferrable");
@@ -16552,6 +16558,23 @@ var Parser = class {
16552
16558
  this.errorAt(this.peek(), "only constraint triggers can be deferred");
16553
16559
  if (initiallyDeferred && !deferrable)
16554
16560
  this.errorAt(this.peek(), "constraint declared INITIALLY DEFERRED must be DEFERRABLE");
16561
+ const transitions = [];
16562
+ if (!constraint && this.eatKw("referencing")) {
16563
+ do {
16564
+ const isNew = this.eatKw("new");
16565
+ if (!isNew) this.expectKw("old");
16566
+ if (this.eatKw("row")) {
16567
+ throw pgError(
16568
+ "feature_not_supported",
16569
+ "ROW variable naming in the REFERENCING clause is not supported",
16570
+ "0A000"
16571
+ );
16572
+ }
16573
+ this.expectKw("table");
16574
+ this.eatKw("as");
16575
+ transitions.push({ isNew, name: this.ident() });
16576
+ } while (this.atKw("new") || this.atKw("old"));
16577
+ }
16555
16578
  let forEachRow = false;
16556
16579
  if (this.eatKw("for")) {
16557
16580
  this.eatKw("each");
@@ -16589,7 +16612,8 @@ var Parser = class {
16589
16612
  forEachRow,
16590
16613
  when,
16591
16614
  funcName,
16592
- funcArgs
16615
+ funcArgs,
16616
+ ...transitions.length > 0 ? { transitions } : {}
16593
16617
  };
16594
16618
  }
16595
16619
  // --- ALTER --------------------------------------------------------------------
@@ -21135,6 +21159,29 @@ function fireRowTriggers(env, table, timing, event, oldRow, newRow) {
21135
21159
  }
21136
21160
  return { row };
21137
21161
  }
21162
+ function transitionRelation(table, name, rows) {
21163
+ return {
21164
+ columns: table.columns.map((c) => ({ name: c.name, type: c.type.id, table: name })),
21165
+ rows
21166
+ };
21167
+ }
21168
+ function fireStatementTriggers(env, table, timing, event, transitions) {
21169
+ 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);
21170
+ for (const t of fired) {
21171
+ if (!executor) throw unsupported(`trigger "${t.name}" execution`);
21172
+ const ctes = /* @__PURE__ */ new Map();
21173
+ if (t.oldTable !== void 0)
21174
+ ctes.set(t.oldTable, transitionRelation(table, t.oldTable, transitions?.oldRows ?? []));
21175
+ if (t.newTable !== void 0)
21176
+ ctes.set(t.newTable, transitionRelation(table, t.newTable, transitions?.newRows ?? []));
21177
+ const changes = env.ctx.state.changes;
21178
+ try {
21179
+ executor({ ctx: env.ctx, params: null, ctes, outer: null }, table, t, event, null, null);
21180
+ } finally {
21181
+ env.ctx.state.changes = changes;
21182
+ }
21183
+ }
21184
+ }
21138
21185
  var keyOf = (schema, name) => JSON.stringify([schema, name]);
21139
21186
  function flushConstraintTriggers(state, all = false) {
21140
21187
  const env = { ctx: new EngineCtx(state), params: null, ctes: /* @__PURE__ */ new Map(), outer: null };
@@ -21518,7 +21565,13 @@ function executeInsert(env0, stmt) {
21518
21565
  const sourceRows = insertSourceRows(env, stmt, colIdxs);
21519
21566
  const insertedRows = [];
21520
21567
  let insertedCount = 0;
21568
+ const addedRows = [];
21569
+ const conflictOldRows = [];
21570
+ const conflictNewRows = [];
21571
+ const updatesOnConflict = stmt.onConflict !== null && stmt.onConflict.action !== "nothing";
21521
21572
  return withStatementRollback(env, () => {
21573
+ fireStatementTriggers(env, table, "before", "insert");
21574
+ if (updatesOnConflict) fireStatementTriggers(env, table, "before", "update");
21522
21575
  for (const srcRow of sourceRows) {
21523
21576
  const row = table.columns.map(() => null);
21524
21577
  const provided = /* @__PURE__ */ new Set();
@@ -21564,6 +21617,7 @@ function executeInsert(env0, stmt) {
21564
21617
  }
21565
21618
  if (conflictIdx !== null && conflictWhereOk) {
21566
21619
  if (stmt.onConflict.action === "nothing") continue;
21620
+ const replaced = rows[conflictIdx];
21567
21621
  const did = applyOnConflictUpdate(
21568
21622
  env,
21569
21623
  table,
@@ -21576,6 +21630,8 @@ function executeInsert(env0, stmt) {
21576
21630
  if (did) {
21577
21631
  insertedCount++;
21578
21632
  insertedRows.push(rows[conflictIdx]);
21633
+ conflictOldRows.push(replaced);
21634
+ conflictNewRows.push(rows[conflictIdx]);
21579
21635
  fireRowTriggers(env, table, "after", "update", null, rows[conflictIdx]);
21580
21636
  }
21581
21637
  continue;
@@ -21590,8 +21646,13 @@ function executeInsert(env0, stmt) {
21590
21646
  indexInsertRow(env, table, rows.length - 1, newRow);
21591
21647
  insertedCount++;
21592
21648
  insertedRows.push(newRow);
21649
+ addedRows.push(newRow);
21593
21650
  fireRowTriggers(env, table, "after", "insert", null, newRow);
21594
21651
  }
21652
+ fireStatementTriggers(env, table, "after", "insert", { oldRows: [], newRows: addedRows });
21653
+ if (updatesOnConflict) {
21654
+ fireStatementTriggers(env, table, "after", "update", { oldRows: conflictOldRows, newRows: conflictNewRows });
21655
+ }
21595
21656
  env.ctx.state.changes = insertedCount;
21596
21657
  if (stmt.returning) {
21597
21658
  const res = evalReturning(env, table, label, stmt.returning, insertedRows, "INSERT");
@@ -21672,10 +21733,12 @@ function executeUpdate(env0, stmt) {
21672
21733
  const env = applyWith(env0, stmt.with);
21673
21734
  const table = requireTargetTable(env, stmt.table, "update");
21674
21735
  const label = stmt.alias ?? table.name;
21736
+ fireStatementTriggers(env, table, "before", "update");
21675
21737
  const fromSource = stmt.from.length > 0 ? buildFrom(env, stmt.from) : null;
21676
21738
  const rows = table.mutableRows();
21677
21739
  const targetCols = table.columns.map((c) => ({ name: c.name, type: c.type.id, table: label }));
21678
21740
  const updatedRows = [];
21741
+ const replacedRows = [];
21679
21742
  let updateCount = 0;
21680
21743
  for (let ri = 0; ri < rows.length; ri++) {
21681
21744
  const oldRow = rows[ri];
@@ -21718,8 +21781,10 @@ function executeUpdate(env0, stmt) {
21718
21781
  handleReferencedUpdate(env, table, [oldRow], [finalRow]);
21719
21782
  updateCount++;
21720
21783
  updatedRows.push(finalRow);
21784
+ replacedRows.push(oldRow);
21721
21785
  fireRowTriggers(env, table, "after", "update", oldRow, finalRow);
21722
21786
  }
21787
+ fireStatementTriggers(env, table, "after", "update", { oldRows: replacedRows, newRows: updatedRows });
21723
21788
  env.ctx.state.changes = updateCount;
21724
21789
  if (stmt.returning) {
21725
21790
  const res = evalReturning(env, table, label, stmt.returning, updatedRows, "UPDATE");
@@ -21731,6 +21796,7 @@ function executeDelete(env0, stmt) {
21731
21796
  const env = applyWith(env0, stmt.with);
21732
21797
  const table = requireTargetTable(env, stmt.table, "delete from");
21733
21798
  const label = stmt.alias ?? table.name;
21799
+ fireStatementTriggers(env, table, "before", "delete");
21734
21800
  const rows = table.mutableRows();
21735
21801
  const usingSource = stmt.using.length > 0 ? buildFrom(env, stmt.using) : null;
21736
21802
  const targetCols = table.columns.map((c) => ({ name: c.name, type: c.type.id, table: label }));
@@ -21768,6 +21834,7 @@ function executeDelete(env0, stmt) {
21768
21834
  fireRowTriggers(env, table, "after", "delete", row, null);
21769
21835
  }
21770
21836
  if (deletedRows.length > 0) rebuildTableIndexes(env, table);
21837
+ fireStatementTriggers(env, table, "after", "delete", { oldRows: deletedRows, newRows: [] });
21771
21838
  env.ctx.state.changes = deletedRows.length;
21772
21839
  if (stmt.returning) {
21773
21840
  const res = evalReturning(env, table, label, stmt.returning, deletedRows, "DELETE");
@@ -23910,7 +23977,7 @@ function pgTrigger(ctx) {
23910
23977
  let oid = 4e4;
23911
23978
  for (const t of allTables(ctx.state)) {
23912
23979
  for (const tr of t.triggers) {
23913
- rows.push([oid++, tr.name, t.oid, !tr.forEachRow]);
23980
+ rows.push([oid++, tr.name, t.oid, !tr.forEachRow, tr.oldTable ?? null, tr.newTable ?? null]);
23914
23981
  }
23915
23982
  }
23916
23983
  return rel2(
@@ -23918,7 +23985,9 @@ function pgTrigger(ctx) {
23918
23985
  ["oid", "oid"],
23919
23986
  ["tgname", "name"],
23920
23987
  ["tgrelid", "oid"],
23921
- ["tgisinternal", "bool"]
23988
+ ["tgisinternal", "bool"],
23989
+ ["tgoldtable", "name"],
23990
+ ["tgnewtable", "name"]
23922
23991
  ],
23923
23992
  rows,
23924
23993
  "pg_trigger"
@@ -25342,7 +25411,7 @@ function executeCreateIndex(env, stmt) {
25342
25411
  }
25343
25412
  }
25344
25413
  }
25345
- assertGinTrgmIndex(state, table, stmt);
25414
+ assertGinTrgmIndex(state, table, stmt, (expr) => expressionTypeOf(env, table, expr));
25346
25415
  const method = stmt.using ?? "btree";
25347
25416
  if (!ACCESS_METHODS.has(method) || method === "heap") {
25348
25417
  throw pgError("undefined_object", `access method "${method}" does not exist`, "42704");
@@ -25597,6 +25666,50 @@ function executeCreateFunction(env, stmt) {
25597
25666
  schema.functions.set(name, existing);
25598
25667
  return commandResult("CREATE FUNCTION", 0);
25599
25668
  }
25669
+ function validateTransitionTables(stmt) {
25670
+ const transitions = stmt.transitions ?? [];
25671
+ if (transitions.length === 0) return {};
25672
+ const invalid = (message) => pgError("invalid_table_definition", message, "42P17");
25673
+ const unsupportedClause = (message) => pgError("feature_not_supported", message, "0A000");
25674
+ if (stmt.timing !== "after") throw invalid("transition table name can only be specified for an AFTER trigger");
25675
+ if (stmt.events.some((e) => e.event === "truncate")) {
25676
+ throw unsupportedClause("TRUNCATE triggers with transition tables are not supported");
25677
+ }
25678
+ if (stmt.events.some((e) => e.columns !== null)) {
25679
+ throw unsupportedClause("transition tables cannot be specified for triggers with column lists");
25680
+ }
25681
+ if (stmt.events.length > 1) {
25682
+ throw unsupportedClause("transition tables cannot be specified for triggers with more than one event");
25683
+ }
25684
+ const event = stmt.events[0].event;
25685
+ let oldTable;
25686
+ let newTable;
25687
+ for (const transition of transitions) {
25688
+ if (transition.isNew) {
25689
+ if (event !== "insert" && event !== "update") {
25690
+ throw invalid("NEW TABLE can only be specified for an INSERT or UPDATE trigger");
25691
+ }
25692
+ if (newTable !== void 0) throw invalid("NEW TABLE cannot be specified multiple times");
25693
+ newTable = transition.name;
25694
+ } else {
25695
+ if (event !== "delete" && event !== "update") {
25696
+ throw invalid("OLD TABLE can only be specified for a DELETE or UPDATE trigger");
25697
+ }
25698
+ if (oldTable !== void 0) throw invalid("OLD TABLE cannot be specified multiple times");
25699
+ oldTable = transition.name;
25700
+ }
25701
+ }
25702
+ if (oldTable !== void 0 && oldTable === newTable) {
25703
+ throw invalid("OLD TABLE name and NEW TABLE name cannot be the same");
25704
+ }
25705
+ if (stmt.forEachRow) {
25706
+ throw pgError("unsupported", "transition tables on FOR EACH ROW triggers are not supported", "0A000");
25707
+ }
25708
+ return {
25709
+ ...oldTable !== void 0 ? { oldTable } : {},
25710
+ ...newTable !== void 0 ? { newTable } : {}
25711
+ };
25712
+ }
25600
25713
  function executeCreateTrigger(env, stmt) {
25601
25714
  if (stmt.constraint && (stmt.timing !== "after" || !stmt.forEachRow))
25602
25715
  throw pgError("syntax", "constraint triggers must be AFTER ROW triggers", "42601");
@@ -25606,6 +25719,7 @@ function executeCreateTrigger(env, stmt) {
25606
25719
  throw pgError("undefined_table", `relation "${stmt.table.join(".")}" does not exist`, "42P01");
25607
25720
  }
25608
25721
  const table = state.ensureWritableTable(found);
25722
+ const transitionTables = validateTransitionTables(stmt);
25609
25723
  const fnSchema = stmt.funcName.length >= 2 ? stmt.funcName[stmt.funcName.length - 2] : null;
25610
25724
  const fnName = stmt.funcName[stmt.funcName.length - 1];
25611
25725
  const fns = state.findFunctions(stmt.funcName);
@@ -25624,6 +25738,7 @@ function executeCreateTrigger(env, stmt) {
25624
25738
  timing: stmt.timing,
25625
25739
  events: stmt.events,
25626
25740
  forEachRow: stmt.forEachRow,
25741
+ ...transitionTables,
25627
25742
  when: stmt.when,
25628
25743
  funcSchema: fnSchema ?? fns[0].schema,
25629
25744
  funcName: fnName,
@@ -28246,7 +28361,7 @@ function analyzeSchema(sql, options = {}) {
28246
28361
  const diagnosticLimit = bounded(options.maxDiagnostics, 100, 1e3);
28247
28362
  const report = {
28248
28363
  engine: "mockingbird-postgres",
28249
- version: true ? "3.7.0" : "source-tree",
28364
+ version: true ? "3.8.0" : "source-tree",
28250
28365
  statements: 0,
28251
28366
  accepted: 0,
28252
28367
  compatible: null,