@intentius/chant-lexicon-sql 0.115.0 → 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.
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +5 -5
- package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
- package/dist/lsp/postgres-template.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/meta.json +15 -0
- package/dist/okf/index.md +3 -0
- package/dist/okf/types/PostgresFunction.md +9 -0
- package/dist/okf/types/PostgresProcedure.md +9 -0
- package/dist/okf/types/PostgresTrigger.md +9 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/postgres/apply/apply.d.ts +6 -1
- package/dist/postgres/apply/apply.d.ts.map +1 -1
- package/dist/postgres/apply/statements.d.ts +9 -2
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +110 -3
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entity-types.d.ts +3 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/import/live-export.d.ts.map +1 -1
- package/dist/postgres/live/catalog.d.ts +16 -4
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.d.ts +6 -1
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/parser.d.ts +82 -5
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/plan/deep.d.ts.map +1 -1
- package/dist/postgres/plan/diff.d.ts +1 -1
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts +21 -2
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +6 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/schema.d.ts +11 -1
- package/dist/postgres/plan/schema.d.ts.map +1 -1
- package/dist/postgres/plan/server-normalize.d.ts.map +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/rules/postgres-templates.ts +2 -2
- package/dist/rules/sqlpg001.ts +3 -0
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/codegen/postgres-classifier-page.ts +1 -1
- package/src/codegen/registry.ts +3 -0
- package/src/generated/lexicon-sql.json +15 -0
- package/src/index.ts +1 -1
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lint/rules/sqlpg001.ts +3 -0
- package/src/lsp/postgres-template.ts +6 -0
- package/src/mcp/postgres.ts +1 -1
- package/src/plugin.ts +3 -0
- package/src/postgres/apply/apply.ts +21 -5
- package/src/postgres/apply/statements.ts +53 -11
- package/src/postgres/entities.ts +332 -9
- package/src/postgres/entity-types.ts +3 -0
- package/src/postgres/import/generator.ts +5 -2
- package/src/postgres/import/live-export.ts +4 -0
- package/src/postgres/live/catalog.ts +78 -4
- package/src/postgres/live/describe-resources.ts +17 -7
- package/src/postgres/live/routines.e2e.test.ts +173 -0
- package/src/postgres/migrate/batches.e2e.test.ts +2 -1
- package/src/postgres/migrate/migrate.e2e.test.ts +2 -1
- package/src/postgres/parser.ts +314 -8
- package/src/postgres/plan/commands.ts +2 -2
- package/src/postgres/plan/deep.ts +57 -17
- package/src/postgres/plan/diff.ts +46 -6
- package/src/postgres/plan/normalize.ts +158 -4
- package/src/postgres/plan/rules.ts +36 -0
- package/src/postgres/plan/schema.ts +25 -6
- package/src/postgres/plan/server-normalize.ts +24 -0
- package/src/postgres/routines.test.ts +150 -0
- package/src/postgres.ts +11 -0
- package/src/skill-defs.ts +1 -1
- package/src/skills/chant-sql-postgres.md +18 -2
- package/src/validate.ts +3 -0
package/src/postgres/parser.ts
CHANGED
|
@@ -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
|
|
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-
|
|
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
|
|
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.
|
|
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
|
-
}
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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
|
|
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
|
|
295
|
-
|
|
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
|
-
|
|
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") };
|