@intentius/chant-lexicon-sql 0.114.1 → 0.116.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.
Files changed (142) hide show
  1. package/dist/clickhouse/apply/mutations.d.ts +7 -1
  2. package/dist/clickhouse/apply/mutations.d.ts.map +1 -1
  3. package/dist/clickhouse/container.d.ts +22 -7
  4. package/dist/clickhouse/container.d.ts.map +1 -1
  5. package/dist/clickhouse/plan/deep.d.ts +5 -0
  6. package/dist/clickhouse/plan/deep.d.ts.map +1 -1
  7. package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
  8. package/dist/clickhouse/rebuild/backfill.d.ts +11 -5
  9. package/dist/clickhouse/rebuild/backfill.d.ts.map +1 -1
  10. package/dist/clickhouse/rebuild/op.d.ts +29 -0
  11. package/dist/clickhouse/rebuild/op.d.ts.map +1 -1
  12. package/dist/clickhouse/rebuild/partitions.d.ts +8 -0
  13. package/dist/clickhouse/rebuild/partitions.d.ts.map +1 -1
  14. package/dist/clickhouse/rebuild/receipts.d.ts +4 -6
  15. package/dist/clickhouse/rebuild/receipts.d.ts.map +1 -1
  16. package/dist/clickhouse/rebuild/shards.d.ts +59 -0
  17. package/dist/clickhouse/rebuild/shards.d.ts.map +1 -0
  18. package/dist/clickhouse/rebuild/steps.d.ts +3 -0
  19. package/dist/clickhouse/rebuild/steps.d.ts.map +1 -1
  20. package/dist/clickhouse/rebuild/verify.d.ts +4 -0
  21. package/dist/clickhouse/rebuild/verify.d.ts.map +1 -1
  22. package/dist/codegen/registry.d.ts.map +1 -1
  23. package/dist/config.d.ts +14 -0
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/core/receipts.d.ts +5 -3
  26. package/dist/core/receipts.d.ts.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/integrity.json +5 -5
  30. package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
  31. package/dist/lsp/postgres-template.d.ts.map +1 -1
  32. package/dist/manifest.json +1 -1
  33. package/dist/meta.json +15 -0
  34. package/dist/okf/index.md +3 -0
  35. package/dist/okf/types/PostgresFunction.md +9 -0
  36. package/dist/okf/types/PostgresProcedure.md +9 -0
  37. package/dist/okf/types/PostgresTrigger.md +9 -0
  38. package/dist/op/activities/clickhouse-rebuild.d.ts.map +1 -1
  39. package/dist/op/activities/index.d.ts +13 -1
  40. package/dist/op/activities/index.d.ts.map +1 -1
  41. package/dist/op/activities/sql-exec.d.ts +41 -0
  42. package/dist/op/activities/sql-exec.d.ts.map +1 -0
  43. package/dist/op/activity-contracts.d.ts +20 -0
  44. package/dist/op/activity-contracts.d.ts.map +1 -1
  45. package/dist/plugin.d.ts.map +1 -1
  46. package/dist/postgres/apply/apply.d.ts +6 -1
  47. package/dist/postgres/apply/apply.d.ts.map +1 -1
  48. package/dist/postgres/apply/statements.d.ts +9 -2
  49. package/dist/postgres/apply/statements.d.ts.map +1 -1
  50. package/dist/postgres/entities.d.ts +110 -3
  51. package/dist/postgres/entities.d.ts.map +1 -1
  52. package/dist/postgres/entity-types.d.ts +3 -0
  53. package/dist/postgres/entity-types.d.ts.map +1 -1
  54. package/dist/postgres/import/generator.d.ts.map +1 -1
  55. package/dist/postgres/import/live-export.d.ts.map +1 -1
  56. package/dist/postgres/live/catalog.d.ts +17 -5
  57. package/dist/postgres/live/catalog.d.ts.map +1 -1
  58. package/dist/postgres/live/describe-resources.d.ts +6 -1
  59. package/dist/postgres/live/describe-resources.d.ts.map +1 -1
  60. package/dist/postgres/migrate/receipts.d.ts +8 -5
  61. package/dist/postgres/migrate/receipts.d.ts.map +1 -1
  62. package/dist/postgres/parser.d.ts +82 -5
  63. package/dist/postgres/parser.d.ts.map +1 -1
  64. package/dist/postgres/plan/deep.d.ts.map +1 -1
  65. package/dist/postgres/plan/diff.d.ts +1 -1
  66. package/dist/postgres/plan/diff.d.ts.map +1 -1
  67. package/dist/postgres/plan/normalize.d.ts +21 -2
  68. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  69. package/dist/postgres/plan/rules.d.ts +6 -0
  70. package/dist/postgres/plan/rules.d.ts.map +1 -1
  71. package/dist/postgres/plan/schema.d.ts +11 -1
  72. package/dist/postgres/plan/schema.d.ts.map +1 -1
  73. package/dist/postgres/plan/server-normalize.d.ts.map +1 -1
  74. package/dist/postgres.d.ts +1 -1
  75. package/dist/postgres.d.ts.map +1 -1
  76. package/dist/receipts.d.ts +102 -0
  77. package/dist/receipts.d.ts.map +1 -0
  78. package/dist/rules/postgres-templates.ts +2 -2
  79. package/dist/rules/sqlpg001.ts +3 -0
  80. package/dist/validate.d.ts.map +1 -1
  81. package/package.json +2 -2
  82. package/src/clickhouse/apply/mutations.ts +9 -3
  83. package/src/clickhouse/container.ts +39 -11
  84. package/src/clickhouse/plan/cluster-drift.e2e.test.ts +117 -0
  85. package/src/clickhouse/plan/deep.ts +102 -3
  86. package/src/clickhouse/plan/normalize.ts +10 -0
  87. package/src/clickhouse/plan/plan.test.ts +10 -0
  88. package/src/clickhouse/rebuild/backfill.ts +85 -42
  89. package/src/clickhouse/rebuild/op.ts +80 -35
  90. package/src/clickhouse/rebuild/partitions.ts +11 -0
  91. package/src/clickhouse/rebuild/rebuild-sharded.e2e.test.ts +161 -0
  92. package/src/clickhouse/rebuild/rebuild.e2e.test.ts +110 -4
  93. package/src/clickhouse/rebuild/rebuild.test.ts +56 -4
  94. package/src/clickhouse/rebuild/receipts.ts +4 -6
  95. package/src/clickhouse/rebuild/shards.ts +104 -0
  96. package/src/clickhouse/rebuild/steps.ts +4 -0
  97. package/src/clickhouse/rebuild/verify.ts +14 -5
  98. package/src/codegen/postgres-classifier-page.ts +1 -1
  99. package/src/codegen/registry.ts +3 -0
  100. package/src/config.ts +7 -0
  101. package/src/core/receipts.ts +5 -3
  102. package/src/generated/lexicon-sql.json +15 -0
  103. package/src/index.ts +1 -1
  104. package/src/lint/rules/postgres-templates.ts +2 -2
  105. package/src/lint/rules/sqlpg001.ts +3 -0
  106. package/src/lsp/postgres-template.ts +6 -0
  107. package/src/mcp/postgres.ts +1 -1
  108. package/src/op/activities/clickhouse-rebuild.ts +32 -7
  109. package/src/op/activities/index.ts +21 -4
  110. package/src/op/activities/sql-exec.ts +84 -0
  111. package/src/op/activity-contracts.ts +15 -0
  112. package/src/op/effect-receipts.e2e.test.ts +277 -0
  113. package/src/plugin.ts +3 -0
  114. package/src/postgres/apply/apply.ts +21 -5
  115. package/src/postgres/apply/statements.ts +53 -11
  116. package/src/postgres/entities.ts +332 -9
  117. package/src/postgres/entity-types.ts +3 -0
  118. package/src/postgres/import/generator.ts +5 -2
  119. package/src/postgres/import/live-export.ts +4 -0
  120. package/src/postgres/live/catalog.ts +81 -5
  121. package/src/postgres/live/describe-resources.ts +17 -7
  122. package/src/postgres/live/routines.e2e.test.ts +173 -0
  123. package/src/postgres/migrate/batches.e2e.test.ts +2 -1
  124. package/src/postgres/migrate/migrate.e2e.test.ts +2 -1
  125. package/src/postgres/migrate/migrate.test.ts +3 -2
  126. package/src/postgres/migrate/receipts.ts +18 -10
  127. package/src/postgres/parser.ts +314 -8
  128. package/src/postgres/plan/commands.ts +2 -2
  129. package/src/postgres/plan/deep.ts +57 -17
  130. package/src/postgres/plan/diff.ts +46 -6
  131. package/src/postgres/plan/normalize.ts +158 -4
  132. package/src/postgres/plan/rules.ts +36 -0
  133. package/src/postgres/plan/schema.ts +25 -6
  134. package/src/postgres/plan/server-normalize.ts +24 -0
  135. package/src/postgres/routines.test.ts +150 -0
  136. package/src/postgres.ts +11 -0
  137. package/src/receipts.test.ts +91 -0
  138. package/src/receipts.ts +259 -0
  139. package/src/skill-defs.ts +1 -1
  140. package/src/skills/chant-sql-postgres.md +18 -2
  141. package/src/skills/chant-sql-rebuild.md +5 -1
  142. package/src/validate.ts +3 -0
@@ -2,7 +2,13 @@
2
2
  * A hand-written, lossless parser for the Postgres statements the dialect
3
3
  * declares (chant #3278, #3279): `CREATE SCHEMA`, `TABLE`, `INDEX`, `VIEW`,
4
4
  * `MATERIALIZED VIEW`, `SEQUENCE`, `TYPE ... AS ENUM`, `DOMAIN`, `EXTENSION`,
5
- * and `COMMENT ON` for those objects.
5
+ * `FUNCTION`, `PROCEDURE` and `TRIGGER` (#3680), and `COMMENT ON` for those
6
+ * objects.
7
+ *
8
+ * A function's or procedure's body is a string (`AS $$ ... $$`), kept
9
+ * verbatim: Postgres stores it as written and prints it back unchanged. A
10
+ * SQL-standard body (`RETURN expr`, `BEGIN ATOMIC ... END`) is refused, since
11
+ * the server stores it parsed and prints it back rewritten.
6
12
  *
7
13
  * As for ClickHouse (#3196), statement structure is parsed and expressions are
8
14
  * not: a default, a check, a generated expression, an index element, a
@@ -22,7 +28,8 @@
22
28
  * Grammar source: the Postgres 18 reference pages for each statement
23
29
  * (sql-createtable, sql-createindex, sql-createview,
24
30
  * sql-creatematerializedview, sql-createsequence, sql-createtype,
25
- * sql-createdomain, sql-createextension, sql-createschema, sql-comment) and
31
+ * sql-createdomain, sql-createextension, sql-createschema, sql-createfunction,
32
+ * sql-createprocedure, sql-createtrigger, sql-comment) and
26
33
  * `src/backend/parser/gram.y` at `REL_18_6` where a page is silent.
27
34
  */
28
35
 
@@ -205,13 +212,63 @@ export interface ExtensionNode {
205
212
  cascade: boolean;
206
213
  }
207
214
 
215
+ /** One parameter of a function or procedure: `[mode] [name] type [DEFAULT expr]`. */
216
+ export interface RoutineArgNode {
217
+ mode?: "IN" | "OUT" | "INOUT" | "VARIADIC";
218
+ name?: string;
219
+ nameSpan?: Span;
220
+ type: Span;
221
+ default?: Span;
222
+ }
223
+
224
+ export interface RoutineNode {
225
+ statement: "function" | "procedure";
226
+ orReplace: boolean;
227
+ name: NameNode;
228
+ args: RoutineArgNode[];
229
+ /** `RETURNS [SETOF] type`: the type, `SETOF` excluded. */
230
+ returns?: { setof: boolean; type: Span };
231
+ /** `RETURNS TABLE (name type, ...)`. */
232
+ returnsTable?: Array<{ name: string; nameSpan: Span; type: Span }>;
233
+ language?: Span;
234
+ /** The attributes after the signature, by their key word: `VOLATILITY` (IMMUTABLE, STABLE, VOLATILE), `STRICT`, `SECURITY`, `LEAKPROOF`, `PARALLEL`, `COST`, `ROWS`, `SUPPORT`, `WINDOW`, `TRANSFORM`. */
235
+ options: Array<{ option: string; value?: Span; flag?: boolean }>;
236
+ /** `SET name { TO | = } value`: the parameter, folded, and its value as written. */
237
+ set: Array<{ name: string; value: Span }>;
238
+ /** `AS 'definition'`, or `AS 'obj_file', 'link_symbol'`: the string constants. */
239
+ body: Span[];
240
+ }
241
+
242
+ export interface TriggerNode {
243
+ statement: "trigger";
244
+ orReplace: boolean;
245
+ constraint: boolean;
246
+ name: NameNode;
247
+ timing: "BEFORE" | "AFTER" | "INSTEAD OF";
248
+ events: Array<{ event: "INSERT" | "UPDATE" | "DELETE" | "TRUNCATE"; columns: Array<{ name: string; span: Span }> }>;
249
+ table: NameNode;
250
+ /** A constraint trigger's `FROM referenced_table`. */
251
+ from?: NameNode;
252
+ deferrable?: boolean;
253
+ initiallyDeferred?: boolean;
254
+ /** `REFERENCING OLD TABLE AS o NEW TABLE AS n`. */
255
+ referencing: Array<{ which: "OLD" | "NEW"; name: string }>;
256
+ forEach?: "ROW" | "STATEMENT";
257
+ /** The `WHEN (...)` condition, parentheses excluded. */
258
+ when?: Span;
259
+ function: NameNode;
260
+ args: Span[];
261
+ }
262
+
208
263
  export interface CommentNode {
209
264
  statement: "comment";
210
- /** TABLE, COLUMN, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, SCHEMA, EXTENSION, CONSTRAINT, DOMAIN CONSTRAINT. */
265
+ /** TABLE, COLUMN, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, SCHEMA, EXTENSION, FUNCTION, PROCEDURE, TRIGGER, CONSTRAINT, DOMAIN CONSTRAINT. */
211
266
  objectType: string;
212
267
  target: NameNode;
213
- /** For `COMMENT ON CONSTRAINT c ON [DOMAIN] t`: the table or domain. */
268
+ /** For `COMMENT ON CONSTRAINT c ON [DOMAIN] t` and `COMMENT ON TRIGGER t ON table`: the table or domain. */
214
269
  on?: NameNode;
270
+ /** For `COMMENT ON FUNCTION f(...)`: the parameter list, parentheses excluded. */
271
+ args?: Span;
215
272
  /** The comment, a string, or undefined for `IS NULL`. */
216
273
  text?: Span;
217
274
  }
@@ -225,6 +282,8 @@ export type StatementNode =
225
282
  | EnumNode
226
283
  | DomainNode
227
284
  | ExtensionNode
285
+ | RoutineNode
286
+ | TriggerNode
228
287
  | CommentNode;
229
288
 
230
289
  const isNameToken = (t: Token | undefined): boolean =>
@@ -748,6 +807,10 @@ class PgParser extends SqlCursor {
748
807
  if (this.accept("COMMENT")) return this.comment();
749
808
  this.expect("CREATE");
750
809
  const orReplace = this.acceptSeq("OR", "REPLACE");
810
+ if (this.accept("FUNCTION")) return this.routine("function", orReplace);
811
+ if (this.accept("PROCEDURE")) return this.routine("procedure", orReplace);
812
+ if (this.accept("TRIGGER")) return this.trigger(orReplace, false);
813
+ if (this.acceptSeq("CONSTRAINT", "TRIGGER")) return this.trigger(orReplace, true);
751
814
  if (!orReplace) {
752
815
  if (this.accept("SCHEMA")) return this.schema();
753
816
  if (this.accept("EXTENSION")) return this.extension();
@@ -762,7 +825,7 @@ class PgParser extends SqlCursor {
762
825
  const recursive = this.accept("RECURSIVE");
763
826
  if (!orReplace && !recursive && persistence === undefined && this.acceptSeq("MATERIALIZED", "VIEW")) return this.view(true, false, false, false);
764
827
  if (this.accept("VIEW")) return this.view(false, orReplace, persistence === "temporary", recursive);
765
- return this.fail("expected SCHEMA, TABLE, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN or EXTENSION");
828
+ return this.fail("expected SCHEMA, TABLE, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, EXTENSION, FUNCTION, PROCEDURE or TRIGGER");
766
829
  }
767
830
 
768
831
  private schema(): SchemaNode {
@@ -1007,26 +1070,269 @@ class PgParser extends SqlCursor {
1007
1070
  }
1008
1071
  }
1009
1072
 
1073
+ /** Whether `a b` continues one type name (`double precision`, `character varying`, `timestamp with time zone`) rather than being a name and a type. */
1074
+ private continuesType(a: Token | undefined, b: Token | undefined): boolean {
1075
+ if (a?.kind !== "ident" || b?.kind !== "ident") return false;
1076
+ const x = a.text.toUpperCase();
1077
+ const y = b.text.toUpperCase();
1078
+ if (x === "DOUBLE") return y === "PRECISION";
1079
+ if (["CHARACTER", "CHAR", "NCHAR", "BIT", "VARCHAR"].includes(x)) return y === "VARYING";
1080
+ if (x === "NATIONAL") return y === "CHARACTER" || y === "CHAR";
1081
+ if (x === "TIME" || x === "TIMESTAMP") return y === "WITH" || y === "WITHOUT";
1082
+ if (x === "INTERVAL") return ["YEAR", "MONTH", "DAY", "HOUR", "MINUTE", "SECOND"].includes(y);
1083
+ return false;
1084
+ }
1085
+
1086
+ /** `[IN | OUT | INOUT | VARIADIC] [name] type [{DEFAULT | =} expr]`. */
1087
+ private routineArg(): RoutineArgNode {
1088
+ const arg: RoutineArgNode = { type: { from: 0, to: 0, refs: [] } };
1089
+ // A mode word is a mode unless it is the whole parameter (a type of that name) or the parameter's type follows nothing else.
1090
+ if (kw(this.peek(), "IN", "OUT", "INOUT", "VARIADIC") && !(this.isPunct(",", 1) || this.isPunct(")", 1))) {
1091
+ arg.mode = this.next().text.toUpperCase() as RoutineArgNode["mode"];
1092
+ }
1093
+ const a = this.peek();
1094
+ const b = this.peek(1);
1095
+ const named =
1096
+ a !== undefined &&
1097
+ (a.kind === "ident" || a.kind === "qident") &&
1098
+ b !== undefined &&
1099
+ (b.kind === "ident" || b.kind === "qident" || b.kind === "ref") &&
1100
+ !(b.kind === "ident" && keywordCategory(b.text) === "R") &&
1101
+ !kw(b, "DEFAULT") &&
1102
+ !this.continuesType(a, b);
1103
+ if (named) {
1104
+ const { text, span } = this.nameTok("a parameter name");
1105
+ arg.name = text;
1106
+ arg.nameSpan = span;
1107
+ }
1108
+ if (kw(this.peek(), "SETOF")) this.fail("expected a parameter type (SETOF is a return type)");
1109
+ arg.type = this.typeName();
1110
+ if (this.isOp("%")) this.fail("expected the end of the parameter (write the column's type: %TYPE is resolved once, when the function is created)");
1111
+ if (this.accept("DEFAULT") || (this.isOp("=") && (this.p++, true))) arg.default = this.checkedExpr([], true);
1112
+ return arg;
1113
+ }
1114
+
1115
+ private routine(kind: "function" | "procedure", orReplace: boolean): RoutineNode {
1116
+ const node: RoutineNode = { statement: kind, orReplace, name: this.qualifiedName(`a ${kind} name`), args: [], options: [], set: [], body: [] };
1117
+ this.expectPunct("(");
1118
+ if (!this.acceptPunct(")")) {
1119
+ for (;;) {
1120
+ node.args.push(this.routineArg());
1121
+ if (this.acceptPunct(",")) continue;
1122
+ this.expectPunct(")");
1123
+ break;
1124
+ }
1125
+ }
1126
+ if (kind === "function" && this.accept("RETURNS")) {
1127
+ if (this.accept("TABLE")) {
1128
+ this.expectPunct("(");
1129
+ node.returnsTable = [];
1130
+ for (;;) {
1131
+ const { text, span } = this.nameTok("a column name");
1132
+ node.returnsTable.push({ name: text, nameSpan: span, type: this.typeName() });
1133
+ if (this.acceptPunct(",")) continue;
1134
+ this.expectPunct(")");
1135
+ break;
1136
+ }
1137
+ } else {
1138
+ const setof = this.accept("SETOF");
1139
+ node.returns = { setof, type: this.typeName() };
1140
+ }
1141
+ }
1142
+ const word = (what: string): Span => {
1143
+ const from = this.idx();
1144
+ const t = this.next();
1145
+ if (!["ident", "qident", "string", "ref", "number"].includes(t.kind)) this.fail(`expected ${what}`, t);
1146
+ return this.span(from, t.kind === "ref" ? [t.part] : []);
1147
+ };
1148
+ const option = (option: string, value?: Span, flag?: boolean) => node.options.push({ option, ...(value ? { value } : {}), ...(flag !== undefined ? { flag } : {}) });
1149
+ const forFunction = (what: string) => {
1150
+ if (kind === "procedure") this.fail(`expected LANGUAGE, SECURITY, SET or AS (${what} is not an attribute of a procedure)`, this.lastSeen());
1151
+ };
1152
+ for (;;) {
1153
+ if (this.accept("LANGUAGE")) node.language = word("a language name");
1154
+ else if (this.accept("TRANSFORM")) {
1155
+ const from = this.idx();
1156
+ const refs: number[] = [];
1157
+ do {
1158
+ this.expect("FOR");
1159
+ this.expect("TYPE");
1160
+ refs.push(...this.typeName().refs);
1161
+ } while (this.acceptPunct(","));
1162
+ option("TRANSFORM", this.span(from, refs));
1163
+ } else if (kw(this.peek(), "IMMUTABLE", "STABLE", "VOLATILE")) {
1164
+ forFunction(this.peek()!.text.toUpperCase());
1165
+ option("VOLATILITY", word("a volatility"));
1166
+ } else if (this.acceptSeq("NOT", "LEAKPROOF")) {
1167
+ forFunction("LEAKPROOF");
1168
+ option("LEAKPROOF", undefined, false);
1169
+ } else if (this.accept("LEAKPROOF")) {
1170
+ forFunction("LEAKPROOF");
1171
+ option("LEAKPROOF", undefined, true);
1172
+ } else if (this.acceptSeq("CALLED", "ON", "NULL", "INPUT")) {
1173
+ forFunction("CALLED ON NULL INPUT");
1174
+ option("STRICT", undefined, false);
1175
+ } else if (this.acceptSeq("RETURNS", "NULL", "ON", "NULL", "INPUT") || this.accept("STRICT")) {
1176
+ forFunction("STRICT");
1177
+ option("STRICT", undefined, true);
1178
+ } else if (this.acceptSeq("EXTERNAL", "SECURITY") || this.accept("SECURITY")) {
1179
+ if (this.accept("DEFINER")) option("SECURITY", undefined, true);
1180
+ else {
1181
+ this.expect("INVOKER");
1182
+ option("SECURITY", undefined, false);
1183
+ }
1184
+ } else if (this.accept("PARALLEL")) {
1185
+ forFunction("PARALLEL");
1186
+ if (!kw(this.peek(), "UNSAFE", "RESTRICTED", "SAFE")) this.fail("expected UNSAFE, RESTRICTED or SAFE");
1187
+ option("PARALLEL", word("UNSAFE, RESTRICTED or SAFE"));
1188
+ } else if (kw(this.peek(), "COST", "ROWS")) {
1189
+ const o = this.next().text.toUpperCase();
1190
+ forFunction(o);
1191
+ const from = this.idx();
1192
+ const t = this.next();
1193
+ if (t.kind !== "number" && t.kind !== "ref") this.fail("expected a number", t);
1194
+ option(o, this.span(from, t.kind === "ref" ? [t.part] : []));
1195
+ } else if (this.accept("SUPPORT")) {
1196
+ forFunction("SUPPORT");
1197
+ option("SUPPORT", this.qualifiedName("a support function").span);
1198
+ } else if (this.accept("WINDOW")) {
1199
+ forFunction("WINDOW");
1200
+ option("WINDOW", undefined, true);
1201
+ } else if (this.accept("SET")) {
1202
+ const param = this.qualifiedName("a configuration parameter");
1203
+ if (this.acceptSeq("FROM", "CURRENT")) {
1204
+ this.fail("expected TO or = (SET ... FROM CURRENT takes the value of the session that runs the CREATE; write the value)", this.lastSeen());
1205
+ }
1206
+ if (!this.accept("TO")) {
1207
+ if (!this.isOp("=")) this.fail("expected TO or =");
1208
+ this.p++;
1209
+ }
1210
+ const from = this.idx();
1211
+ const refs: number[] = [];
1212
+ for (;;) {
1213
+ if (this.isOp("-") || this.isOp("+")) this.p++;
1214
+ const t = this.next();
1215
+ if (t.kind === "ref") refs.push(t.part);
1216
+ else if (!["ident", "qident", "string", "number"].includes(t.kind)) this.fail("expected a value", t);
1217
+ if (this.acceptPunct(",")) continue;
1218
+ break;
1219
+ }
1220
+ node.set.push({ name: param.pieces.join("."), value: this.span(from, refs) });
1221
+ } else if (this.accept("AS")) {
1222
+ if (node.body.length > 0) this.fail("expected one AS clause", this.lastSeen());
1223
+ for (;;) {
1224
+ const from = this.idx();
1225
+ const t = this.next();
1226
+ if (t.kind !== "string") this.fail("expected the definition as a string constant (AS $$ ... $$)", t);
1227
+ node.body.push(this.span(from, []));
1228
+ if (this.acceptPunct(",")) continue;
1229
+ break;
1230
+ }
1231
+ } else if (kw(this.peek(), "RETURN") || this.is("BEGIN", "ATOMIC")) {
1232
+ this.fail("expected AS (a SQL-standard body is stored parsed and printed back rewritten, so it cannot be compared with what was written; write the body as a string: AS $$ ... $$)");
1233
+ } else break;
1234
+ }
1235
+ if (node.body.length === 0) this.fail("expected AS and the body, as a string constant (AS $$ ... $$)");
1236
+ return node;
1237
+ }
1238
+
1239
+ /** The last significant token read. */
1240
+ private lastSeen(): Token | undefined {
1241
+ return this.tokens[this.idx(-1)];
1242
+ }
1243
+
1244
+ private trigger(orReplace: boolean, constraint: boolean): TriggerNode {
1245
+ const { text, span } = this.nameTok("a trigger name");
1246
+ if (this.isPunct(".")) this.fail("expected BEFORE, AFTER or INSTEAD OF (a trigger name is not schema-qualified; the trigger is its table's)");
1247
+ const name: NameNode = { pieces: [text], span };
1248
+ let timing: TriggerNode["timing"];
1249
+ if (this.accept("BEFORE")) timing = "BEFORE";
1250
+ else if (this.accept("AFTER")) timing = "AFTER";
1251
+ else if (this.acceptSeq("INSTEAD", "OF")) timing = "INSTEAD OF";
1252
+ else return this.fail("expected BEFORE, AFTER or INSTEAD OF");
1253
+ const events: TriggerNode["events"] = [];
1254
+ do {
1255
+ if (this.accept("INSERT", "DELETE", "TRUNCATE")) events.push({ event: this.lastSeen()!.text.toUpperCase() as "INSERT", columns: [] });
1256
+ else if (this.accept("UPDATE")) {
1257
+ const columns: Array<{ name: string; span: Span }> = [];
1258
+ if (this.accept("OF")) {
1259
+ do columns.push((({ text, span }) => ({ name: text, span }))(this.nameTok("a column name")));
1260
+ while (this.acceptPunct(","));
1261
+ }
1262
+ events.push({ event: "UPDATE", columns });
1263
+ } else this.fail("expected INSERT, UPDATE, DELETE or TRUNCATE");
1264
+ } while (this.accept("OR"));
1265
+ this.expect("ON");
1266
+ const node: TriggerNode = { statement: "trigger", orReplace, constraint, name, timing, events, table: this.qualifiedName("a table name"), referencing: [], function: { pieces: [], span: { from: 0, to: 0, refs: [] } }, args: [] };
1267
+ for (;;) {
1268
+ if (this.accept("FROM")) node.from = this.qualifiedName("a referenced table");
1269
+ else if (this.acceptSeq("NOT", "DEFERRABLE")) node.deferrable = false;
1270
+ else if (this.accept("DEFERRABLE")) node.deferrable = true;
1271
+ else if (this.acceptSeq("INITIALLY", "DEFERRED")) node.initiallyDeferred = true;
1272
+ else if (this.acceptSeq("INITIALLY", "IMMEDIATE")) node.initiallyDeferred = false;
1273
+ else if (this.accept("REFERENCING")) {
1274
+ do {
1275
+ if (!this.accept("OLD", "NEW")) this.fail("expected OLD or NEW");
1276
+ const which = this.lastSeen()!.text.toUpperCase() as "OLD" | "NEW";
1277
+ this.expect("TABLE");
1278
+ this.accept("AS");
1279
+ node.referencing.push({ which, name: this.nameTok("a transition table name").text });
1280
+ } while (kw(this.peek(), "OLD", "NEW"));
1281
+ } else if (this.accept("FOR")) {
1282
+ this.accept("EACH");
1283
+ if (!this.accept("ROW", "STATEMENT")) this.fail("expected ROW or STATEMENT");
1284
+ node.forEach = this.lastSeen()!.text.toUpperCase() as "ROW" | "STATEMENT";
1285
+ } else if (this.accept("WHEN")) {
1286
+ if (!this.isPunct("(")) this.fail("expected '(' (a WHEN condition is parenthesized)");
1287
+ node.when = this.parenthesized();
1288
+ } else break;
1289
+ }
1290
+ this.expect("EXECUTE");
1291
+ this.expect("FUNCTION", "PROCEDURE");
1292
+ node.function = this.qualifiedName("a trigger function");
1293
+ this.expectPunct("(");
1294
+ if (!this.acceptPunct(")")) {
1295
+ for (;;) {
1296
+ const from = this.idx();
1297
+ if (this.isOp("-")) this.p++;
1298
+ const t = this.next();
1299
+ if (!["string", "number", "ident", "qident", "ref"].includes(t.kind)) this.fail("expected a trigger argument (a string, a number or a name)", t);
1300
+ node.args.push(this.span(from, t.kind === "ref" ? [t.part] : []));
1301
+ if (this.acceptPunct(",")) continue;
1302
+ this.expectPunct(")");
1303
+ break;
1304
+ }
1305
+ }
1306
+ return node;
1307
+ }
1308
+
1010
1309
  private comment(): CommentNode {
1011
1310
  this.expect("ON");
1012
1311
  let objectType: string;
1013
1312
  let on: NameNode | undefined;
1014
1313
  let target: NameNode;
1314
+ let args: Span | undefined;
1015
1315
  if (this.accept("CONSTRAINT")) {
1016
1316
  objectType = "CONSTRAINT";
1017
1317
  target = this.qualifiedName("a constraint name");
1018
1318
  this.expect("ON");
1019
1319
  if (this.accept("DOMAIN")) objectType = "DOMAIN CONSTRAINT";
1020
1320
  on = this.qualifiedName("a table name");
1321
+ } else if (this.accept("TRIGGER")) {
1322
+ objectType = "TRIGGER";
1323
+ target = this.qualifiedName("a trigger name");
1324
+ this.expect("ON");
1325
+ on = this.qualifiedName("a table name");
1021
1326
  } else {
1022
1327
  if (this.acceptSeq("MATERIALIZED", "VIEW")) objectType = "MATERIALIZED VIEW";
1023
- else if (this.accept("TABLE", "COLUMN", "INDEX", "VIEW", "SEQUENCE", "TYPE", "DOMAIN", "SCHEMA", "EXTENSION")) {
1328
+ else if (this.accept("TABLE", "COLUMN", "INDEX", "VIEW", "SEQUENCE", "TYPE", "DOMAIN", "SCHEMA", "EXTENSION", "FUNCTION", "PROCEDURE")) {
1024
1329
  objectType = this.tokens[this.idx(-1)]!.text.toUpperCase();
1025
- } else return this.fail("expected TABLE, COLUMN, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, SCHEMA, EXTENSION or CONSTRAINT");
1330
+ } else return this.fail("expected TABLE, COLUMN, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, SCHEMA, EXTENSION, FUNCTION, PROCEDURE, TRIGGER or CONSTRAINT");
1026
1331
  target = this.qualifiedName("an object name");
1332
+ if ((objectType === "FUNCTION" || objectType === "PROCEDURE") && this.isPunct("(")) args = this.parenthesized();
1027
1333
  }
1028
1334
  this.expect("IS");
1029
- const node: CommentNode = { statement: "comment", objectType, target, on };
1335
+ const node: CommentNode = { statement: "comment", objectType, target, on, ...(args ? { args } : {}) };
1030
1336
  if (!this.accept("NULL")) {
1031
1337
  const t = this.peek();
1032
1338
  if (t?.kind !== "string" && t?.kind !== "ref") this.fail("expected a string or NULL");
@@ -94,7 +94,7 @@ export async function planAgainstClient(
94
94
  const declared: PgSchemaObject[] = [];
95
95
  for (const o of declaredRaw) {
96
96
  const l = liveByKey.get(o.key);
97
- if (l && differs(o, l) && (o.canonical.kind === "table" || o.canonical.kind === "view" || o.canonical.kind === "materializedView")) {
97
+ if (l && differs(o, l) && (o.canonical.kind === "table" || o.canonical.kind === "view" || o.canonical.kind === "materializedView" || o.canonical.kind === "trigger")) {
98
98
  declared.push({ ...o, canonical: { ...o.canonical, ...(await (options.serverNormalize ?? serverNormalized)(client, o.canonical, target.defaultSchema)) } });
99
99
  } else declared.push(o);
100
100
  }
@@ -115,7 +115,7 @@ export async function planPgAgainstServer(environment: string, buildFile: string
115
115
  const targeted = pgBuildFileMajor(buildFile) ?? (await projectMajor(options));
116
116
  const { declared, diff } = await planAgainstClient(client, target, pgSchemaFromBuildFile(buildFile, target.defaultSchema), { ...(targeted !== undefined ? { major: targeted } : {}), environment });
117
117
  const { migrationOps } = withMigrationOps(diff, declared, environment, target.defaultSchema);
118
- const label = new Map(declared.map((o) => [o.key, `${o.canonical.exportName} (${o.key.split(" ")[1]})`]));
118
+ const label = new Map(declared.map((o) => [o.key, `${o.canonical.exportName} (${o.key.slice(o.key.indexOf(" ") + 1)})`]));
119
119
  const changes = diff.changes.map((c) => ({ ...c, object: label.get(c.object) ?? c.object }));
120
120
  return { changes, hints: diff.hints, refused: changes.filter((c) => c.class === "expand"), ...(migrationOps ? { migrationOps } : {}) };
121
121
  } finally {
@@ -21,6 +21,8 @@ import { declaredAddress, scopeFor } from "../live/describe-resources";
21
21
  import * as tags from "../entities";
22
22
  import { POSTGRES_ENTITY_TYPES } from "../entity-types";
23
23
  import { canonicalPgObject, columnShape, sameConstraint, type CanonicalPgObject } from "./normalize";
24
+ import { serverNormalized } from "./server-normalize";
25
+ import type { PostgresClient } from "../live/client";
24
26
 
25
27
  type Props = Record<string, unknown>;
26
28
 
@@ -34,6 +36,9 @@ const TAG: Record<string, (s: TemplateStringsArray) => { props: object }> = {
34
36
  [POSTGRES_ENTITY_TYPES.enum]: tags.type,
35
37
  [POSTGRES_ENTITY_TYPES.domain]: tags.domain,
36
38
  [POSTGRES_ENTITY_TYPES.extension]: tags.extension,
39
+ [POSTGRES_ENTITY_TYPES.function]: tags.func,
40
+ [POSTGRES_ENTITY_TYPES.procedure]: tags.procedure,
41
+ [POSTGRES_ENTITY_TYPES.trigger]: tags.trigger,
37
42
  };
38
43
 
39
44
  /** The props of what the server printed, parsed with the declaration's tag. */
@@ -74,6 +79,32 @@ const PROP_OF_FIELD: Record<string, string[]> = {
74
79
  partitionBound: ["partitionBound"],
75
80
  inherits: ["inherits"],
76
81
  using: ["using"],
82
+ args: ["args"],
83
+ returns: ["returns", "returnsTable"],
84
+ language: ["language"],
85
+ body: ["body"],
86
+ link: ["link"],
87
+ securityDefiner: ["securityDefiner"],
88
+ set: ["set"],
89
+ transform: ["transform"],
90
+ volatility: ["volatility"],
91
+ strict: ["strict"],
92
+ leakproof: ["leakproof"],
93
+ parallel: ["parallel"],
94
+ cost: ["cost"],
95
+ rows: ["rows"],
96
+ support: ["support"],
97
+ window: ["window"],
98
+ constraint: ["constraint"],
99
+ timing: ["timing"],
100
+ events: ["events"],
101
+ forEach: ["forEach"],
102
+ when: ["when"],
103
+ function: ["function", "functionName"],
104
+ from: ["from"],
105
+ deferrable: ["deferrable"],
106
+ initiallyDeferred: ["initiallyDeferred"],
107
+ referencing: ["referencing"],
77
108
  };
78
109
 
79
110
  /** Props keys that hold constraints, by constraint kind. */
@@ -150,39 +181,48 @@ export async function observeResourcesDeep(
150
181
  const unobserved: Record<string, UnobservedEntity> = {};
151
182
  let target;
152
183
  let live: LivePgObject[];
184
+ let client: PostgresClient;
153
185
  const declared = options.entityNames.map((name) => ({ name, entity: options.entities.get(name) }));
154
186
  try {
155
187
  const bound = await bindPostgres(options);
156
188
  target = bound.target;
189
+ client = bound.client;
157
190
  try {
158
191
  live = await readLiveSchema(bound.client, {
159
192
  schemas: scopeFor(target, declared.filter((x) => x.entity?.entityType.startsWith("Postgres::")).map((x) => ({ type: x.entity!.entityType, props: x.entity!.props }))),
160
193
  });
161
- } finally {
194
+ } catch (err) {
162
195
  await bound.client.end();
196
+ throw err;
163
197
  }
164
198
  } catch (err) {
165
199
  const why = classifyPostgresFailure(err);
166
200
  for (const { name, entity } of declared) unobserved[name] = { type: entity?.entityType ?? "", reason: why.reason, detail: why.detail };
167
201
  return deepObservation({}, unobserved);
168
202
  }
169
- const byKey = new Map(live.map((o) => [liveKey(o.type, o.schema, o.name), o]));
170
- for (const { name, entity } of declared) {
171
- if (!entity || !entity.entityType.startsWith("Postgres::")) {
172
- unobserved[name] = { type: entity?.entityType ?? "", reason: "unsupported-kind" };
173
- continue;
174
- }
175
- const { schema, name: objectName } = declaredAddress({ type: entity.entityType, props: entity.props }, target.defaultSchema);
176
- const o = byKey.get(liveKey(entity.entityType, schema, objectName));
177
- if (!o) continue;
178
- try {
179
- const lp = liveProps(o);
180
- const d = canonicalPgObject(entity.entityType, entity.props, target.defaultSchema);
181
- const l = canonicalPgObject(o.type, lp, target.defaultSchema);
182
- resources[name] = { type: o.type, physicalId: o.oid, properties: inDeclaredVocabulary(entity.props, lp, d, l) };
183
- } catch (err) {
184
- unobserved[name] = { type: entity.entityType, reason: "read-failed", detail: `the server's definition does not parse: ${(err as Error).message}` };
203
+ try {
204
+ const byKey = new Map(live.map((o) => [liveKey(o.type, o.schema, o.name, o.signature), o]));
205
+ for (const { name, entity } of declared) {
206
+ if (!entity || !entity.entityType.startsWith("Postgres::")) {
207
+ unobserved[name] = { type: entity?.entityType ?? "", reason: "unsupported-kind" };
208
+ continue;
209
+ }
210
+ const { schema, name: objectName, signature } = declaredAddress({ type: entity.entityType, props: entity.props }, target.defaultSchema);
211
+ const o = byKey.get(liveKey(entity.entityType, schema, objectName, signature));
212
+ if (!o) continue;
213
+ try {
214
+ const lp = liveProps(o);
215
+ let d = canonicalPgObject(entity.entityType, entity.props, target.defaultSchema);
216
+ const l = canonicalPgObject(o.type, lp, target.defaultSchema);
217
+ // A trigger's WHEN as the server would print it, when the rules leave it different.
218
+ if (d.kind === "trigger" && d.fields.when !== l.fields.when) d = await serverNormalized(client, d, target.defaultSchema);
219
+ resources[name] = { type: o.type, physicalId: o.oid, properties: inDeclaredVocabulary(entity.props, lp, d, l) };
220
+ } catch (err) {
221
+ unobserved[name] = { type: entity.entityType, reason: "read-failed", detail: `the server's definition does not parse: ${(err as Error).message}` };
222
+ }
185
223
  }
224
+ } finally {
225
+ await client.end();
186
226
  }
187
227
  return deepObservation(resources, unobserved);
188
228
  }
@@ -17,7 +17,7 @@ import { classifiedChange, type ChangeSet, type ClassifiedChange } from "../../c
17
17
  import { matchByIdentity } from "../../core/diff";
18
18
  import { PG_CLASSIFIER_RULES, type PgChangeClass, type PgClassifierRuleId } from "./rules";
19
19
  import { sameConstraint, serialBase, type CanonicalColumn, type CanonicalConstraint } from "./normalize";
20
- import type { PgDiffObject, PgSchemaObject } from "./schema";
20
+ import { pgNamespace, type PgDiffObject, type PgSchemaObject } from "./schema";
21
21
  import { POSTGRES_LATEST_MAJOR } from "../../spec/postgres-pin";
22
22
  import type { PostgresMigrationOpSuggestion } from "../migrate/handoff";
23
23
 
@@ -38,7 +38,7 @@ export interface PgSchemaDiff extends ChangeSet<PgChange> {
38
38
  const change = (object: string, field: string, rule: PgClassifierRuleId, before?: unknown, after?: unknown, extra: Partial<PgChange> = {}): PgChange =>
39
39
  classifiedChange(PG_CLASSIFIER_RULES, object, field, rule, before, after, extra);
40
40
 
41
- const qualified = (o: PgDiffObject) => `${o.schema ? `${o.schema}.` : ""}${o.name}`;
41
+ const qualified = (o: PgDiffObject) => `${o.schema ? `${o.schema}.` : ""}${o.name}${o.kind === "trigger" ? "" : (o.signature ?? "")}`;
42
42
 
43
43
  // ── Columns ────────────────────────────────────────────────────────────
44
44
 
@@ -258,6 +258,42 @@ function diffObject(key: string, before: PgDiffObject, after: PgDiffObject, out:
258
258
  field("columnComments", "SQLPG216");
259
259
  break;
260
260
  }
261
+ case "function":
262
+ case "procedure": {
263
+ // CREATE OR REPLACE cannot change the result, the OUT parameters, an input parameter's name, a removed default or a window function's kind.
264
+ const ba = (before.fields.args as string[] | undefined) ?? [];
265
+ const aa = (after.fields.args as string[] | undefined) ?? [];
266
+ const inputName = (a: string) => (/^(in|inout|variadic) /.test(a) ? a.split(" ")[1] : undefined);
267
+ const reasons: string[] = [];
268
+ // Between two builds, the same export with other parameter types is another function on the server.
269
+ if (before.signature !== after.signature) reasons.push(`the parameter types ${before.signature} -> ${after.signature}`);
270
+ if (!same(before.fields.returns, after.fields.returns)) reasons.push(`the result ${show(before.fields.returns) ?? "-"} -> ${show(after.fields.returns) ?? "-"}`);
271
+ const outs = (l: string[]) => l.filter((a) => /^(out|inout) /.test(a));
272
+ if (!same(outs(ba), outs(aa))) reasons.push("the output parameters");
273
+ if (ba.some((a, i) => aa[i] !== undefined && inputName(a) !== inputName(aa[i]!) && /^(in|inout|variadic) /.test(aa[i]!))) reasons.push("a parameter's name");
274
+ if (ba.some((a, i) => / default /.test(a) && aa[i] !== undefined && !/ default /.test(aa[i]!))) reasons.push("a parameter's default is removed");
275
+ if (!same(before.fields.window, after.fields.window)) reasons.push("WINDOW");
276
+ const definition = ["args", "returns", "language", "body", "link", "securityDefiner", "set", "transform", "volatility", "strict", "leakproof", "parallel", "cost", "rows", "support", "window"];
277
+ const changed = definition.filter((k) => !same(before.fields[k], after.fields[k]));
278
+ if (changed.length === 0 && reasons.length === 0) break;
279
+ if (changed.length === 0) changed.push("args");
280
+ const what = changed.join(", ");
281
+ if (reasons.length === 0) out.push(change(key, what, "SQLPG280", undefined, undefined, { note: `CREATE OR REPLACE ${after.kind.toUpperCase()}` }));
282
+ else if (before.dependents && before.dependents.length > 0) {
283
+ out.push(change(key, what, "SQLPG282", undefined, undefined, { note: `${reasons.join(", ")} changes, and ${before.dependents.join(", ")} depend${before.dependents.length === 1 ? "s" : ""} on it` }));
284
+ } else out.push(change(key, what, "SQLPG281", undefined, undefined, { note: `${reasons.join(", ")} changes, which CREATE OR REPLACE refuses` }));
285
+ break;
286
+ }
287
+ case "trigger": {
288
+ const definition = ["table", "constraint", "timing", "events", "forEach", "when", "function", "args", "from", "deferrable", "initiallyDeferred", "referencing"];
289
+ const changed = definition.filter((k) => !same(before.fields[k], after.fields[k]));
290
+ if (changed.length > 0) {
291
+ out.push(
292
+ change(key, changed.join(", "), "SQLPG284", changed.map((k) => `${k} ${show(before.fields[k]) ?? "-"}`).join("; "), changed.map((k) => `${k} ${show(after.fields[k]) ?? "-"}`).join("; "), after.fields.constraint || before.fields.constraint || !same(before.fields.table, after.fields.table) ? { note: "dropped and created in one transaction: a constraint trigger has no OR REPLACE, and OR REPLACE keeps a trigger on its table" } : {}),
293
+ );
294
+ }
295
+ break;
296
+ }
261
297
  case "table":
262
298
  diffColumns(key, before, after, out, hints, false, major);
263
299
  diffConstraints(key, before, after, out, false);
@@ -288,11 +324,12 @@ function createdTables(matches: ReturnType<typeof matchByIdentity<PgDiffObject>>
288
324
  */
289
325
  export function matchPgObjects(before: readonly PgSchemaObject[], after: readonly PgSchemaObject[]): ReturnType<typeof matchByIdentity<PgDiffObject>> {
290
326
  return matchByIdentity(before, after, {
291
- qualified: (o) => `${o.kind === "schema" ? "schema" : o.kind === "extension" ? "extension" : o.kind === "enum" || o.kind === "domain" ? "type" : "relation"} ${qualified(o)}`,
327
+ qualified: (o) => `${pgNamespace(o.kind)} ${o.schema ? `${o.schema}.` : ""}${o.name}${o.signature ?? ""}`,
292
328
  previously: (o) => o.previously,
293
329
  previousNames: (o, prev) => {
294
- const space = o.kind === "schema" ? "schema" : o.kind === "extension" ? "extension" : o.kind === "enum" || o.kind === "domain" ? "type" : "relation";
295
- return [`${space} ${prev}`, ...(o.schema && !prev.includes(".") ? [`${space} ${o.schema}.${prev}`] : [])];
330
+ const space = pgNamespace(o.kind);
331
+ const sig = o.signature ?? "";
332
+ return [`${space} ${prev}${sig}`, ...(o.schema && !prev.includes(".") ? [`${space} ${o.schema}.${prev}${sig}`] : [])];
296
333
  },
297
334
  });
298
335
  }
@@ -308,6 +345,8 @@ export function diffPgSchemas(before: readonly PgSchemaObject[], after: readonly
308
345
  const o = m.after.canonical;
309
346
  if (o.kind === "index" && !newTables.has(String(o.fields.table))) {
310
347
  changes.push(change(m.after.key, "index", o.concurrently ? "SQLPG240" : "SQLPG241", undefined, qualified(o)));
348
+ } else if (o.kind === "trigger" && !newTables.has(String(o.fields.table))) {
349
+ changes.push(change(m.after.key, "trigger", "SQLPG283", undefined, `${o.name}${o.signature ?? ""}`));
311
350
  } else changes.push(change(m.after.key, o.kind, "SQLPG200", undefined, qualified(o)));
312
351
  } else if (m.kind === "dropped") {
313
352
  const o = m.before.canonical;
@@ -316,7 +355,8 @@ export function diffPgSchemas(before: readonly PgSchemaObject[], after: readonly
316
355
  continue;
317
356
  }
318
357
  const destructive = o.kind === "table" || o.kind === "materializedView" || o.kind === "sequence";
319
- changes.push(change(m.before.key, o.kind, o.kind === "index" ? "SQLPG242" : "SQLPG270", qualified(o), undefined, destructive ? { destructive: true } : {}));
358
+ const rule = o.kind === "index" ? "SQLPG242" : o.kind === "trigger" ? "SQLPG285" : "SQLPG270";
359
+ changes.push(change(m.before.key, o.kind, rule, o.kind === "trigger" ? `${o.name}${o.signature ?? ""}` : qualified(o), undefined, destructive ? { destructive: true } : {}));
320
360
  } else diffObject(m.after.key, m.before.canonical, m.after.canonical, changes, hints, major);
321
361
  }
322
362
  return { changes, hints, refused: changes.filter((c) => c.class === "expand") };