@intentius/chant-lexicon-sql 0.124.48 → 0.124.49
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/clickhouse/plan/statements.d.ts +29 -0
- package/dist/clickhouse/plan/statements.d.ts.map +1 -0
- package/dist/clickhouse/plan/statements.js +334 -0
- package/dist/codegen/classifier-page.d.ts.map +1 -1
- package/dist/codegen/classifier-page.js +23 -0
- package/dist/codegen/postgres-classifier-page.js +2 -2
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/codegen/registry.js +5 -0
- package/dist/core/statement-class.d.ts +55 -0
- package/dist/core/statement-class.d.ts.map +1 -0
- package/dist/core/statement-class.js +43 -0
- package/dist/coverage/postgres.json +6 -6
- package/dist/drizzle.d.ts.map +1 -1
- package/dist/drizzle.js +5 -0
- package/dist/files/postgres.d.ts.map +1 -1
- package/dist/files/postgres.js +23 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/integrity.json +5 -5
- package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
- package/dist/lint/post-synth/postgres-names.js +3 -0
- package/dist/lint/rules/postgres-templates.js +2 -2
- package/dist/lsp/postgres-template.d.ts.map +1 -1
- package/dist/lsp/postgres-template.js +10 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/postgres.js +1 -1
- package/dist/meta.json +25 -0
- package/dist/migration-classify.d.ts +23 -0
- package/dist/migration-classify.d.ts.map +1 -0
- package/dist/migration-classify.js +44 -0
- package/dist/okf/index.md +5 -0
- package/dist/okf/types/PostgresForeignTable.md +9 -0
- package/dist/okf/types/PostgresOperatorClass.md +9 -0
- package/dist/okf/types/PostgresOperatorFamily.md +9 -0
- package/dist/okf/types/PostgresTextSearchConfiguration.md +9 -0
- package/dist/okf/types/PostgresTextSearchDictionary.md +9 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +13 -0
- package/dist/postgres/access/acl.js +1 -1
- package/dist/postgres/access/import.js +1 -1
- package/dist/postgres/apply/apply.js +5 -0
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/apply/statements.js +156 -2
- package/dist/postgres/entities.d.ts +120 -2
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entities.js +180 -5
- package/dist/postgres/entity-types.d.ts +5 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/entity-types.js +5 -0
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/import/generator.js +16 -1
- package/dist/postgres/live/catalog-kinds.d.ts +6 -0
- package/dist/postgres/live/catalog-kinds.d.ts.map +1 -1
- package/dist/postgres/live/catalog-kinds.js +246 -0
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.js +2 -0
- package/dist/postgres/migrate/observe.d.ts.map +1 -1
- package/dist/postgres/migrate/observe.js +4 -1
- package/dist/postgres/migrate/replication.d.ts +42 -0
- package/dist/postgres/migrate/replication.d.ts.map +1 -1
- package/dist/postgres/migrate/replication.js +53 -0
- package/dist/postgres/parser.d.ts +73 -1
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/parser.js +181 -7
- package/dist/postgres/plan/deep.d.ts.map +1 -1
- package/dist/postgres/plan/deep.js +15 -4
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/diff.js +120 -5
- package/dist/postgres/plan/normalize.d.ts +12 -1
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.js +151 -0
- package/dist/postgres/plan/rules.d.ts +3 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/rules.js +5 -2
- package/dist/postgres/plan/schema.js +1 -1
- package/dist/postgres/plan/statements.d.ts +26 -0
- package/dist/postgres/plan/statements.d.ts.map +1 -0
- package/dist/postgres/plan/statements.js +364 -0
- package/dist/postgres/testing/roundtrip-schema.d.ts +3 -2
- package/dist/postgres/testing/roundtrip-schema.d.ts.map +1 -1
- package/dist/postgres/testing/roundtrip-schema.js +12 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +1 -1
- package/dist/rules/postgres-names.ts +2 -0
- package/dist/rules/postgres-templates.ts +2 -2
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +5 -0
- package/package.json +2 -2
- package/src/clickhouse/plan/statements.test.ts +61 -0
- package/src/clickhouse/plan/statements.ts +306 -0
- package/src/codegen/classifier-page.ts +23 -0
- package/src/codegen/postgres-classifier-page.ts +2 -2
- package/src/codegen/registry.ts +5 -0
- package/src/core/statement-class.ts +88 -0
- package/src/coverage/postgres.json +6 -6
- package/src/drizzle.ts +5 -0
- package/src/files/postgres.ts +22 -1
- package/src/generated/lexicon-sql.json +25 -0
- package/src/index.ts +1 -1
- package/src/lint/post-synth/postgres-checks.test.ts +8 -1
- package/src/lint/post-synth/postgres-names.ts +2 -0
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lsp/postgres-template.ts +9 -0
- package/src/mcp/postgres.ts +1 -1
- package/src/migration-classify.test.ts +150 -0
- package/src/migration-classify.ts +45 -0
- package/src/op/watch-pipeline.test.ts +8 -1
- package/src/plugin.ts +14 -0
- package/src/postgres/access/acl.ts +1 -1
- package/src/postgres/access/import.ts +1 -1
- package/src/postgres/apply/apply.ts +5 -0
- package/src/postgres/apply/statements.ts +151 -3
- package/src/postgres/database-kinds.test.ts +498 -17
- package/src/postgres/entities.ts +276 -5
- package/src/postgres/entity-types.ts +5 -0
- package/src/postgres/import/generator.ts +13 -1
- package/src/postgres/live/catalog-kinds.ts +271 -0
- package/src/postgres/live/describe-resources.ts +2 -0
- package/src/postgres/migrate/observe.ts +3 -1
- package/src/postgres/migrate/replication.ts +66 -0
- package/src/postgres/migrate/subscriber.e2e.test.ts +141 -0
- package/src/postgres/migrate/subscriber.test.ts +59 -0
- package/src/postgres/parser.ts +225 -6
- package/src/postgres/plan/deep.ts +15 -4
- package/src/postgres/plan/diff.ts +107 -6
- package/src/postgres/plan/normalize.ts +155 -0
- package/src/postgres/plan/rules.ts +25 -4
- package/src/postgres/plan/schema.ts +1 -1
- package/src/postgres/plan/statements.test.ts +78 -0
- package/src/postgres/plan/statements.ts +299 -0
- package/src/postgres/testing/roundtrip-schema.ts +12 -1
- package/src/postgres.ts +17 -0
- package/src/skills/chant-sql-postgres-migration.md +1 -0
- package/src/skills/chant-sql-postgres.md +5 -0
- package/src/sql-files.test.ts +33 -0
- package/src/validate.ts +5 -0
package/src/postgres/parser.ts
CHANGED
|
@@ -113,6 +113,8 @@ export interface ColumnNode {
|
|
|
113
113
|
notNullName?: string;
|
|
114
114
|
default?: Span;
|
|
115
115
|
generated?: { kind: "stored" | "virtual" | "identity"; expr?: Span; always: boolean; options?: Span };
|
|
116
|
+
/** A foreign table column's `OPTIONS (...)`. */
|
|
117
|
+
options?: GenericOption[];
|
|
116
118
|
/** Column constraints other than NOT NULL, NULL, DEFAULT and GENERATED. */
|
|
117
119
|
constraints: ConstraintNode[];
|
|
118
120
|
}
|
|
@@ -298,6 +300,66 @@ export interface GenericOption {
|
|
|
298
300
|
value: Span;
|
|
299
301
|
}
|
|
300
302
|
|
|
303
|
+
/** `CREATE FOREIGN TABLE [IF NOT EXISTS] name (columns) SERVER server [OPTIONS (...)]`. */
|
|
304
|
+
export interface ForeignTableNode {
|
|
305
|
+
statement: "foreignTable";
|
|
306
|
+
/** Clauses the parser does not know (#3872), kept in the DDL as written; absent when there are none. */
|
|
307
|
+
unknown?: UnknownClause[];
|
|
308
|
+
ifNotExists: boolean;
|
|
309
|
+
name: NameNode;
|
|
310
|
+
columns: ColumnNode[];
|
|
311
|
+
constraints: ConstraintNode[];
|
|
312
|
+
server: NameNode;
|
|
313
|
+
options: GenericOption[];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** One member of an operator class: an operator, a support function, or the storage type. */
|
|
317
|
+
export type OperatorClassItem =
|
|
318
|
+
| { kind: "operator"; number: Span; operator: NameNode; types?: Span; orderBy?: NameNode }
|
|
319
|
+
| { kind: "function"; number: Span; types?: Span; function: NameNode; args: Span }
|
|
320
|
+
| { kind: "storage"; type: Span };
|
|
321
|
+
|
|
322
|
+
/** `CREATE OPERATOR CLASS name [DEFAULT] FOR TYPE type USING method [FAMILY family] AS item, ...`. */
|
|
323
|
+
export interface OperatorClassNode {
|
|
324
|
+
statement: "operatorClass";
|
|
325
|
+
name: NameNode;
|
|
326
|
+
default: boolean;
|
|
327
|
+
type: Span;
|
|
328
|
+
/** The index access method, folded. */
|
|
329
|
+
method: string;
|
|
330
|
+
family?: NameNode;
|
|
331
|
+
items: OperatorClassItem[];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** `CREATE OPERATOR FAMILY name USING method`. */
|
|
335
|
+
export interface OperatorFamilyNode {
|
|
336
|
+
statement: "operatorFamily";
|
|
337
|
+
name: NameNode;
|
|
338
|
+
method: string;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** `CREATE TEXT SEARCH DICTIONARY name (TEMPLATE = t [, option = value, ...])`; the template is the `template` option. */
|
|
342
|
+
export interface TextSearchDictionaryNode {
|
|
343
|
+
statement: "textSearchDictionary";
|
|
344
|
+
name: NameNode;
|
|
345
|
+
options: DefElem[];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** `CREATE TEXT SEARCH CONFIGURATION name (PARSER = p)`. */
|
|
349
|
+
export interface TextSearchConfigurationNode {
|
|
350
|
+
statement: "textSearchConfiguration";
|
|
351
|
+
name: NameNode;
|
|
352
|
+
options: DefElem[];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** `ALTER TEXT SEARCH CONFIGURATION name ADD MAPPING FOR token_type, ... WITH dictionary, ...`: a configuration's mapping, written after its CREATE. */
|
|
356
|
+
export interface TextSearchMappingNode {
|
|
357
|
+
statement: "textSearchMapping";
|
|
358
|
+
name: NameNode;
|
|
359
|
+
tokens: Array<{ name: string; span: Span }>;
|
|
360
|
+
dictionaries: NameNode[];
|
|
361
|
+
}
|
|
362
|
+
|
|
301
363
|
/** `CREATE FOREIGN DATA WRAPPER name [HANDLER f | NO HANDLER] [VALIDATOR f | NO VALIDATOR] [OPTIONS (...)]`. */
|
|
302
364
|
export interface ForeignDataWrapperNode {
|
|
303
365
|
statement: "foreignDataWrapper";
|
|
@@ -462,6 +524,8 @@ export interface CommentNode {
|
|
|
462
524
|
on?: NameNode;
|
|
463
525
|
/** For `COMMENT ON FUNCTION f(...)` and `AGGREGATE a(...)`: the parameter list, parentheses excluded. */
|
|
464
526
|
args?: Span;
|
|
527
|
+
/** For `COMMENT ON OPERATOR CLASS c USING m` and `OPERATOR FAMILY`: the index method, folded. */
|
|
528
|
+
using?: string;
|
|
465
529
|
/** For `COMMENT ON CAST (source AS target)`: the two types. */
|
|
466
530
|
cast?: { source: Span; target: Span };
|
|
467
531
|
/** The comment, a string, or undefined for `IS NULL`. */
|
|
@@ -482,6 +546,12 @@ export type StatementNode =
|
|
|
482
546
|
| CastNode
|
|
483
547
|
| EventTriggerNode
|
|
484
548
|
| OperatorNode
|
|
549
|
+
| OperatorClassNode
|
|
550
|
+
| OperatorFamilyNode
|
|
551
|
+
| ForeignTableNode
|
|
552
|
+
| TextSearchDictionaryNode
|
|
553
|
+
| TextSearchConfigurationNode
|
|
554
|
+
| TextSearchMappingNode
|
|
485
555
|
| StatisticsNode
|
|
486
556
|
| ForeignDataWrapperNode
|
|
487
557
|
| ForeignServerNode
|
|
@@ -845,7 +915,7 @@ class PgParser extends SqlCursor {
|
|
|
845
915
|
return node;
|
|
846
916
|
}
|
|
847
917
|
|
|
848
|
-
private column(typed: boolean): ColumnNode {
|
|
918
|
+
private column(typed: boolean, foreign = false): ColumnNode {
|
|
849
919
|
const { text, span } = this.nameTok("a column name");
|
|
850
920
|
const col: ColumnNode = { name: text, nameSpan: span, constraints: [] };
|
|
851
921
|
if (!typed) this.acceptSeq("WITH", "OPTIONS");
|
|
@@ -854,7 +924,10 @@ class PgParser extends SqlCursor {
|
|
|
854
924
|
if (typed && !bare && !this.stopsAt(COLUMN_CLAUSE)) col.type = this.typeName();
|
|
855
925
|
else if (typed) this.fail("expected a column type");
|
|
856
926
|
for (;;) {
|
|
857
|
-
if (this.
|
|
927
|
+
if (foreign && kw(this.peek(), "OPTIONS") && this.isPunct("(", 1)) {
|
|
928
|
+
this.p++;
|
|
929
|
+
col.options = this.genericOptions();
|
|
930
|
+
} else if (this.accept("COMPRESSION")) col.compression = this.expr(COLUMN_CLAUSE);
|
|
858
931
|
else if (this.accept("STORAGE")) col.storage = this.expr(COLUMN_CLAUSE);
|
|
859
932
|
else if (this.accept("COLLATE")) col.collate = this.expr(COLUMN_CLAUSE);
|
|
860
933
|
else if (!this.columnConstraint(col) && !this.unknownClause(COLUMN_WORDS, { inList: true, column: text || undefined })) break;
|
|
@@ -1070,6 +1143,7 @@ class PgParser extends SqlCursor {
|
|
|
1070
1143
|
if (this.accept("GRANT")) return this.grant(false, false);
|
|
1071
1144
|
if (this.accept("REVOKE")) return this.grant(true, false);
|
|
1072
1145
|
if (this.acceptSeq("ALTER", "DEFAULT", "PRIVILEGES")) return this.defaultPrivileges();
|
|
1146
|
+
if (this.acceptSeq("ALTER", "TEXT", "SEARCH", "CONFIGURATION")) return this.textSearchMapping();
|
|
1073
1147
|
if (this.acceptSeq("ALTER", "TABLE")) return this.rowSecurity();
|
|
1074
1148
|
this.expect("CREATE");
|
|
1075
1149
|
const orReplace = this.acceptSeq("OR", "REPLACE");
|
|
@@ -1089,8 +1163,21 @@ class PgParser extends SqlCursor {
|
|
|
1089
1163
|
if (this.accept("COLLATION")) return this.collation();
|
|
1090
1164
|
if (this.accept("CAST")) return this.cast();
|
|
1091
1165
|
if (this.acceptSeq("EVENT", "TRIGGER")) return this.eventTrigger();
|
|
1092
|
-
if (this.accept("OPERATOR"))
|
|
1166
|
+
if (this.accept("OPERATOR")) {
|
|
1167
|
+
// CLASS and FAMILY are words an operator's schema may be named: `class.+` is still an operator.
|
|
1168
|
+
if (kw(this.peek(), "CLASS") && !this.isPunct(".", 1)) {
|
|
1169
|
+
this.p++;
|
|
1170
|
+
return this.operatorClass();
|
|
1171
|
+
}
|
|
1172
|
+
if (kw(this.peek(), "FAMILY") && !this.isPunct(".", 1)) {
|
|
1173
|
+
this.p++;
|
|
1174
|
+
return this.operatorFamily();
|
|
1175
|
+
}
|
|
1176
|
+
return this.operator();
|
|
1177
|
+
}
|
|
1178
|
+
if (this.acceptSeq("TEXT", "SEARCH")) return this.textSearch();
|
|
1093
1179
|
if (this.accept("STATISTICS")) return this.statistics();
|
|
1180
|
+
if (this.acceptSeq("FOREIGN", "TABLE")) return this.foreignTable();
|
|
1094
1181
|
if (this.acceptSeq("FOREIGN", "DATA", "WRAPPER")) return this.foreignDataWrapper();
|
|
1095
1182
|
if (this.accept("SERVER")) return this.foreignServer();
|
|
1096
1183
|
if (this.accept("PUBLICATION")) return this.publication();
|
|
@@ -1103,7 +1190,7 @@ class PgParser extends SqlCursor {
|
|
|
1103
1190
|
const recursive = this.accept("RECURSIVE");
|
|
1104
1191
|
if (!orReplace && !recursive && persistence === undefined && this.acceptSeq("MATERIALIZED", "VIEW")) return this.view(true, false, false, false);
|
|
1105
1192
|
if (this.accept("VIEW")) return this.view(false, orReplace, persistence === "temporary", recursive);
|
|
1106
|
-
return this.fail("expected SCHEMA, TABLE, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, EXTENSION, FUNCTION, PROCEDURE, AGGREGATE, TRIGGER, EVENT TRIGGER, COLLATION, CAST, OPERATOR, STATISTICS, FOREIGN DATA WRAPPER, SERVER, PUBLICATION, POLICY or ROLE");
|
|
1193
|
+
return this.fail("expected SCHEMA, TABLE, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, EXTENSION, FUNCTION, PROCEDURE, AGGREGATE, TRIGGER, EVENT TRIGGER, COLLATION, CAST, OPERATOR, OPERATOR CLASS, OPERATOR FAMILY, TEXT SEARCH CONFIGURATION, TEXT SEARCH DICTIONARY, STATISTICS, FOREIGN TABLE, FOREIGN DATA WRAPPER, SERVER, PUBLICATION, POLICY or ROLE");
|
|
1107
1194
|
}
|
|
1108
1195
|
|
|
1109
1196
|
private schema(): SchemaNode {
|
|
@@ -1372,7 +1459,6 @@ class PgParser extends SqlCursor {
|
|
|
1372
1459
|
}
|
|
1373
1460
|
|
|
1374
1461
|
private operator(): OperatorNode {
|
|
1375
|
-
if (kw(this.peek(), "CLASS", "FAMILY") && !this.isPunct(".", 1)) this.fail("expected the operator's name (CREATE OPERATOR CLASS and CREATE OPERATOR FAMILY are not declared)");
|
|
1376
1462
|
const name = this.operatorName();
|
|
1377
1463
|
const options = this.definition("an operator option");
|
|
1378
1464
|
this.checkDefinition(options, OPERATOR_OPTIONS, [], "an operator");
|
|
@@ -1380,6 +1466,122 @@ class PgParser extends SqlCursor {
|
|
|
1380
1466
|
return { statement: "operator", name, options };
|
|
1381
1467
|
}
|
|
1382
1468
|
|
|
1469
|
+
/** The member list of `CREATE OPERATOR CLASS ... AS`: an operator, a support function or the storage type. */
|
|
1470
|
+
private operatorClassItem(): OperatorClassItem {
|
|
1471
|
+
const number = (): Span => {
|
|
1472
|
+
const t = this.next();
|
|
1473
|
+
if (t.kind !== "number" && t.kind !== "ref") this.fail("expected the strategy or support number", t);
|
|
1474
|
+
return this.span(this.idx(-1), t.kind === "ref" ? [t.part] : []);
|
|
1475
|
+
};
|
|
1476
|
+
if (this.accept("OPERATOR")) {
|
|
1477
|
+
const item: Extract<OperatorClassItem, { kind: "operator" }> = { kind: "operator", number: number(), operator: this.operatorName() };
|
|
1478
|
+
if (this.isPunct("(")) item.types = this.parenthesized();
|
|
1479
|
+
if (this.accept("FOR")) {
|
|
1480
|
+
if (this.accept("SEARCH")) return item;
|
|
1481
|
+
if (!this.acceptSeq("ORDER", "BY")) this.fail("expected SEARCH or ORDER BY");
|
|
1482
|
+
item.orderBy = this.qualifiedName("a sort operator family");
|
|
1483
|
+
}
|
|
1484
|
+
return item;
|
|
1485
|
+
}
|
|
1486
|
+
if (this.accept("FUNCTION")) {
|
|
1487
|
+
const n = number();
|
|
1488
|
+
const types = this.isPunct("(") ? this.parenthesized() : undefined;
|
|
1489
|
+
const fn = this.qualifiedName("a function name");
|
|
1490
|
+
if (!this.isPunct("(")) this.fail("expected '(' and the function's argument types");
|
|
1491
|
+
return { kind: "function", number: n, ...(types ? { types } : {}), function: fn, args: this.parenthesized() };
|
|
1492
|
+
}
|
|
1493
|
+
if (this.accept("STORAGE")) return { kind: "storage", type: this.typeName() };
|
|
1494
|
+
return this.fail("expected OPERATOR, FUNCTION or STORAGE");
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
private operatorClass(): OperatorClassNode {
|
|
1498
|
+
const name = this.qualifiedName("an operator class name");
|
|
1499
|
+
const isDefault = this.accept("DEFAULT");
|
|
1500
|
+
this.expect("FOR");
|
|
1501
|
+
this.expect("TYPE");
|
|
1502
|
+
const type = this.typeName();
|
|
1503
|
+
this.expect("USING");
|
|
1504
|
+
const method = this.nameTok("an index access method").text;
|
|
1505
|
+
const node: OperatorClassNode = { statement: "operatorClass", name, default: isDefault, type, method, items: [] };
|
|
1506
|
+
if (this.accept("FAMILY")) node.family = this.qualifiedName("an operator family name");
|
|
1507
|
+
this.expect("AS");
|
|
1508
|
+
do node.items.push(this.operatorClassItem());
|
|
1509
|
+
while (this.acceptPunct(","));
|
|
1510
|
+
return node;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
private operatorFamily(): OperatorFamilyNode {
|
|
1514
|
+
const name = this.qualifiedName("an operator family name");
|
|
1515
|
+
this.expect("USING");
|
|
1516
|
+
return { statement: "operatorFamily", name, method: this.nameTok("an index access method").text };
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/** After `CREATE TEXT SEARCH`: a dictionary or a configuration. A parser and a template are C code. */
|
|
1520
|
+
private textSearch(): TextSearchDictionaryNode | TextSearchConfigurationNode {
|
|
1521
|
+
if (this.accept("DICTIONARY")) {
|
|
1522
|
+
const name = this.qualifiedName("a text search dictionary name");
|
|
1523
|
+
const options = this.definition("a dictionary option");
|
|
1524
|
+
// The options past TEMPLATE belong to the template; a name given twice is still wrong.
|
|
1525
|
+
this.checkDefinition(options, options.map((o) => o.name), ["template"], "a text search dictionary");
|
|
1526
|
+
return { statement: "textSearchDictionary", name, options };
|
|
1527
|
+
}
|
|
1528
|
+
if (this.accept("CONFIGURATION")) {
|
|
1529
|
+
const name = this.qualifiedName("a text search configuration name");
|
|
1530
|
+
const options = this.definition("a configuration option");
|
|
1531
|
+
this.checkDefinition(options, ["parser", "copy"], [], "a text search configuration");
|
|
1532
|
+
if (options.some((o) => o.name === "copy")) this.fail("expected PARSER (COPY copies mappings chant does not read; name the parser and write the mappings)", this.tokens[options.find((o) => o.name === "copy")!.nameSpan.from]);
|
|
1533
|
+
if (!options.some((o) => o.name === "parser")) this.fail("expected PARSER (a configuration needs the parser that splits text into tokens)", this.lastSeen());
|
|
1534
|
+
return { statement: "textSearchConfiguration", name, options };
|
|
1535
|
+
}
|
|
1536
|
+
return this.fail("expected DICTIONARY or CONFIGURATION (a text search parser and a text search template are C code an extension installs)");
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
/** `ALTER TEXT SEARCH CONFIGURATION name ADD MAPPING FOR token_type [, ...] WITH dictionary [, ...]`. */
|
|
1540
|
+
private textSearchMapping(): TextSearchMappingNode {
|
|
1541
|
+
const name = this.qualifiedName("a text search configuration name");
|
|
1542
|
+
if (!this.acceptSeq("ADD", "MAPPING")) this.fail("expected ADD MAPPING (a configuration's mappings are declared with ADD MAPPING after its CREATE)");
|
|
1543
|
+
this.expect("FOR");
|
|
1544
|
+
const node: TextSearchMappingNode = { statement: "textSearchMapping", name, tokens: [], dictionaries: [] };
|
|
1545
|
+
do node.tokens.push((({ text, span }) => ({ name: text, span }))(this.nameTok("a token type")));
|
|
1546
|
+
while (this.acceptPunct(","));
|
|
1547
|
+
this.expect("WITH");
|
|
1548
|
+
do node.dictionaries.push(this.qualifiedName("a text search dictionary name"));
|
|
1549
|
+
while (this.acceptPunct(","));
|
|
1550
|
+
return node;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/** `CREATE FOREIGN TABLE`: the column list form. A key, a generated column, INHERITS and PARTITION OF are not declared. */
|
|
1554
|
+
private foreignTable(): ForeignTableNode {
|
|
1555
|
+
const ifNotExists = this.ifNotExists();
|
|
1556
|
+
const name = this.qualifiedName("a foreign table name");
|
|
1557
|
+
if (kw(this.peek(), "PARTITION")) this.fail("expected the column list (a foreign table that is a partition, PARTITION OF, is not declared here)");
|
|
1558
|
+
const node: ForeignTableNode = { statement: "foreignTable", ifNotExists, name, columns: [], constraints: [], server: { pieces: [], span: this.span(this.idx(), []) }, options: [] };
|
|
1559
|
+
this.expectPunct("(");
|
|
1560
|
+
if (!this.acceptPunct(")")) {
|
|
1561
|
+
for (;;) {
|
|
1562
|
+
if (kw(this.peek(), "LIKE")) this.fail("LIKE is not declared on a foreign table; write its columns");
|
|
1563
|
+
const c = this.tableConstraint();
|
|
1564
|
+
if (c) node.constraints.push(c);
|
|
1565
|
+
else node.columns.push(this.column(true, true));
|
|
1566
|
+
if (this.acceptPunct(",")) continue;
|
|
1567
|
+
this.expectPunct(")");
|
|
1568
|
+
break;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (kw(this.peek(), "INHERITS")) this.fail("expected SERVER (INHERITS is not declared on a foreign table)");
|
|
1572
|
+
this.expect("SERVER");
|
|
1573
|
+
node.server = this.databaseName("a server name");
|
|
1574
|
+
if (this.accept("OPTIONS")) node.options = this.genericOptions();
|
|
1575
|
+
// A foreign table has CHECK and NOT NULL constraints and a default; Postgres refuses the rest, and a generated column is not read.
|
|
1576
|
+
const bad = (c: ConstraintNode, what: string): never => this.fail(`${what} (a foreign table holds CHECK and NOT NULL constraints only; the remote server keeps the rest)`, this.tokens[c.span.from]);
|
|
1577
|
+
for (const c of [...node.constraints, ...node.columns.flatMap((x) => x.constraints)]) {
|
|
1578
|
+
if (c.kind !== "CHECK" && c.kind !== "NOT NULL") bad(c, `expected CHECK or NOT NULL, not ${c.kind}`);
|
|
1579
|
+
if (c.notEnforced !== undefined) bad(c, "ENFORCED and NOT ENFORCED are 18's and are not declared on a foreign table");
|
|
1580
|
+
}
|
|
1581
|
+
for (const col of node.columns) if (col.generated) this.fail(`expected no generated column (${col.name} is GENERATED ${col.generated.kind.toUpperCase()}; a foreign table's generated columns are not declared)`, this.tokens[col.nameSpan.from]);
|
|
1582
|
+
return node;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1383
1585
|
private statistics(): StatisticsNode {
|
|
1384
1586
|
const ifNotExists = this.ifNotExists();
|
|
1385
1587
|
const name = this.qualifiedName("a statistics name");
|
|
@@ -2074,6 +2276,23 @@ class PgParser extends SqlCursor {
|
|
|
2074
2276
|
} else if (this.acceptSeq("FOREIGN", "DATA", "WRAPPER")) {
|
|
2075
2277
|
objectType = "FOREIGN DATA WRAPPER";
|
|
2076
2278
|
target = this.qualifiedName("a foreign data wrapper name");
|
|
2279
|
+
} else if (kw(this.peek(), "OPERATOR") && kw(this.peek(1), "CLASS", "FAMILY") && !this.isPunct(".", 2)) {
|
|
2280
|
+
this.p++;
|
|
2281
|
+
objectType = `OPERATOR ${this.next().text.toUpperCase()}`;
|
|
2282
|
+
target = this.qualifiedName(`an ${objectType.toLowerCase()} name`);
|
|
2283
|
+
this.expect("USING");
|
|
2284
|
+
const using = this.nameTok("an index access method").text;
|
|
2285
|
+
this.expect("IS");
|
|
2286
|
+
const node: CommentNode = { statement: "comment", objectType, target, using };
|
|
2287
|
+
if (!this.accept("NULL")) node.text = this.commentText();
|
|
2288
|
+
return node;
|
|
2289
|
+
} else if (this.acceptSeq("TEXT", "SEARCH")) {
|
|
2290
|
+
if (!this.accept("CONFIGURATION", "DICTIONARY")) this.fail("expected CONFIGURATION or DICTIONARY");
|
|
2291
|
+
objectType = `TEXT SEARCH ${this.lastSeen()!.text.toUpperCase()}`;
|
|
2292
|
+
target = this.qualifiedName(`a ${objectType.toLowerCase()} name`);
|
|
2293
|
+
} else if (this.acceptSeq("FOREIGN", "TABLE")) {
|
|
2294
|
+
objectType = "FOREIGN TABLE";
|
|
2295
|
+
target = this.qualifiedName("a foreign table name");
|
|
2077
2296
|
} else if (this.accept("OPERATOR")) {
|
|
2078
2297
|
objectType = "OPERATOR";
|
|
2079
2298
|
target = this.operatorName();
|
|
@@ -2093,7 +2312,7 @@ class PgParser extends SqlCursor {
|
|
|
2093
2312
|
if (this.acceptSeq("MATERIALIZED", "VIEW")) objectType = "MATERIALIZED VIEW";
|
|
2094
2313
|
else if (this.accept("TABLE", "COLUMN", "INDEX", "VIEW", "SEQUENCE", "TYPE", "DOMAIN", "SCHEMA", "EXTENSION", "FUNCTION", "PROCEDURE")) {
|
|
2095
2314
|
objectType = this.tokens[this.idx(-1)]!.text.toUpperCase();
|
|
2096
|
-
} else return this.fail("expected TABLE, COLUMN, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, SCHEMA, EXTENSION, FUNCTION, PROCEDURE, AGGREGATE, COLLATION, CAST, OPERATOR, TRIGGER, EVENT TRIGGER, STATISTICS, FOREIGN DATA WRAPPER, SERVER, PUBLICATION, POLICY, ROLE or CONSTRAINT");
|
|
2315
|
+
} else return this.fail("expected TABLE, COLUMN, INDEX, VIEW, MATERIALIZED VIEW, SEQUENCE, TYPE, DOMAIN, SCHEMA, EXTENSION, FUNCTION, PROCEDURE, AGGREGATE, COLLATION, CAST, OPERATOR, TRIGGER, EVENT TRIGGER, STATISTICS, FOREIGN TABLE, FOREIGN DATA WRAPPER, SERVER, PUBLICATION, OPERATOR CLASS, OPERATOR FAMILY, TEXT SEARCH CONFIGURATION, TEXT SEARCH DICTIONARY, POLICY, ROLE or CONSTRAINT");
|
|
2097
2316
|
target = this.qualifiedName("an object name");
|
|
2098
2317
|
if ((objectType === "FUNCTION" || objectType === "PROCEDURE") && this.isPunct("(")) args = this.parenthesized();
|
|
2099
2318
|
}
|
|
@@ -44,6 +44,11 @@ const TAG: Record<string, (s: TemplateStringsArray) => { props: object }> = {
|
|
|
44
44
|
[POSTGRES_ENTITY_TYPES.cast]: tags.cast,
|
|
45
45
|
[POSTGRES_ENTITY_TYPES.eventTrigger]: tags.eventTrigger,
|
|
46
46
|
[POSTGRES_ENTITY_TYPES.operator]: tags.operator,
|
|
47
|
+
[POSTGRES_ENTITY_TYPES.operatorClass]: tags.operatorClass,
|
|
48
|
+
[POSTGRES_ENTITY_TYPES.operatorFamily]: tags.operatorFamily,
|
|
49
|
+
[POSTGRES_ENTITY_TYPES.foreignTable]: tags.foreignTable,
|
|
50
|
+
[POSTGRES_ENTITY_TYPES.textSearchConfiguration]: tags.textSearchConfiguration,
|
|
51
|
+
[POSTGRES_ENTITY_TYPES.textSearchDictionary]: tags.textSearchDictionary,
|
|
47
52
|
[POSTGRES_ENTITY_TYPES.statistics]: tags.statistics,
|
|
48
53
|
[POSTGRES_ENTITY_TYPES.foreignDataWrapper]: tags.foreignDataWrapper,
|
|
49
54
|
[POSTGRES_ENTITY_TYPES.foreignServer]: tags.foreignServer,
|
|
@@ -148,6 +153,12 @@ const PROP_OF_FIELD: Record<string, string[]> = {
|
|
|
148
153
|
validator: ["validator", "validatorName", "noValidator"],
|
|
149
154
|
serverType: ["serverType"],
|
|
150
155
|
wrapper: ["wrapper", "wrapperName"],
|
|
156
|
+
server: ["serverName"],
|
|
157
|
+
family: ["family"],
|
|
158
|
+
items: ["items"],
|
|
159
|
+
template: ["template"],
|
|
160
|
+
parser: ["parser"],
|
|
161
|
+
mappings: ["mappings"],
|
|
151
162
|
allTables: ["allTables"],
|
|
152
163
|
tables: ["tables"],
|
|
153
164
|
schemas: ["schemas"],
|
|
@@ -190,7 +201,7 @@ export function inDeclaredVocabulary(declared: Props, live: Props, d: CanonicalP
|
|
|
190
201
|
if (!fieldKeys.has(f)) for (const key of keys) if (f !== "schema" || d.kind === "extension") adopt(key);
|
|
191
202
|
}
|
|
192
203
|
|
|
193
|
-
if (d.kind === "table") {
|
|
204
|
+
if (d.kind === "table" || d.kind === "foreignTable") {
|
|
194
205
|
const declaredColumns = (declared.columns as unknown[] | undefined) ?? [];
|
|
195
206
|
const liveColumns = (live.columns as unknown[] | undefined) ?? [];
|
|
196
207
|
// Columns are matched by name and listed in the declaration's order, the
|
|
@@ -216,7 +227,7 @@ export function inDeclaredVocabulary(declared: Props, live: Props, d: CanonicalP
|
|
|
216
227
|
? ["definition"]
|
|
217
228
|
: d.kind === "collation"
|
|
218
229
|
? ["provider", "lcCollate", "lcCtype", "locale", "nondeterministic", "rules"]
|
|
219
|
-
: d.kind === "foreignDataWrapper" || d.kind === "foreignServer"
|
|
230
|
+
: d.kind === "foreignDataWrapper" || d.kind === "foreignServer" || d.kind === "foreignTable" || d.kind === "textSearchDictionary"
|
|
220
231
|
? ["options"]
|
|
221
232
|
: d.kind === "publication"
|
|
222
233
|
? ["publish", "viaRoot", "generatedColumns"]
|
|
@@ -230,9 +241,9 @@ export function inDeclaredVocabulary(declared: Props, live: Props, d: CanonicalP
|
|
|
230
241
|
out.method = live.method;
|
|
231
242
|
out.functionArgs = live.functionArgs;
|
|
232
243
|
}
|
|
233
|
-
if (d.kind === "table" || d.kind === "domain") {
|
|
244
|
+
if (d.kind === "table" || d.kind === "domain" || d.kind === "foreignTable") {
|
|
234
245
|
// A constraint the declaration says the same as the server is the declaration's (its name included or left out).
|
|
235
|
-
const kinds = d.kind === "
|
|
246
|
+
const kinds = d.kind === "table" ? Object.keys(CONSTRAINT_PROPS) : ["CHECK"];
|
|
236
247
|
for (const kind of kinds) {
|
|
237
248
|
const key = CONSTRAINT_PROPS[kind]!;
|
|
238
249
|
const dl = d.constraints.filter((c) => c.kind === kind);
|
|
@@ -215,7 +215,7 @@ function diffUnparsed(key: string, before: PgDiffObject, after: PgDiffObject, ou
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
/** The kinds (#3911) whose definition has no ALTER: a change is a drop and a create, or (an aggregate's) CREATE OR REPLACE. */
|
|
218
|
-
const REPLACED_KINDS: ReadonlySet<string> = new Set(["composite", "range", "aggregate", "collation", "cast", "eventTrigger", "operator", "statistics"]);
|
|
218
|
+
const REPLACED_KINDS: ReadonlySet<string> = new Set(["composite", "range", "aggregate", "collation", "cast", "eventTrigger", "operator", "operatorClass", "operatorFamily", "statistics"]);
|
|
219
219
|
|
|
220
220
|
/** What each of {@link REPLACED_KINDS} compares besides its name and comment. */
|
|
221
221
|
const DEFINITION: Readonly<Record<string, readonly string[]>> = {
|
|
@@ -226,14 +226,20 @@ const DEFINITION: Readonly<Record<string, readonly string[]>> = {
|
|
|
226
226
|
cast: ["castMethod", "function", "context"],
|
|
227
227
|
eventTrigger: ["event", "tags", "function"],
|
|
228
228
|
operator: ["definition"],
|
|
229
|
+
// A class's members, type, family and default flag have no ALTER; a family has no definition besides its name and method (its identity).
|
|
230
|
+
operatorClass: ["default", "dataType", "family", "items"],
|
|
231
|
+
operatorFamily: [],
|
|
229
232
|
statistics: ["table", "kinds", "targets"],
|
|
230
233
|
};
|
|
231
234
|
|
|
232
235
|
/** The kinds (#3911) the server alters in place, with the fields that cannot be altered (the object is dropped and created) and those that can. */
|
|
233
|
-
const ALTERED_KINDS: Readonly<Record<string, { recreate: readonly string[]; inPlace: readonly string[] }>> = {
|
|
236
|
+
const ALTERED_KINDS: Readonly<Record<string, { recreate: readonly string[]; inPlace: readonly string[]; rule?: PgClassifierRuleId }>> = {
|
|
234
237
|
foreignDataWrapper: { recreate: [], inPlace: ["handler", "validator", "options"] },
|
|
235
238
|
foreignServer: { recreate: ["serverType", "wrapper"], inPlace: ["version", "options"] },
|
|
236
239
|
publication: { recreate: ["allTables"], inPlace: ["tables", "schemas", "publish", "viaRoot", "generatedColumns"] },
|
|
240
|
+
// ALTER TEXT SEARCH DICTIONARY sets options and cannot change the template; ALTER TEXT SEARCH CONFIGURATION maps token types and cannot change the parser (SQLPG305).
|
|
241
|
+
textSearchDictionary: { recreate: ["template"], inPlace: ["options"], rule: "SQLPG305" },
|
|
242
|
+
textSearchConfiguration: { recreate: ["parser"], inPlace: ["mappings"], rule: "SQLPG305" },
|
|
237
243
|
};
|
|
238
244
|
|
|
239
245
|
/**
|
|
@@ -245,17 +251,98 @@ const ALTERED_KINDS: Readonly<Record<string, { recreate: readonly string[]; inPl
|
|
|
245
251
|
*/
|
|
246
252
|
function diffAltered(key: string, before: PgDiffObject, after: PgDiffObject, out: PgChange[]): void {
|
|
247
253
|
if (!same(before.fields.comment, after.fields.comment)) out.push(change(key, "comment", "SQLPG216", show(before.fields.comment), show(after.fields.comment)));
|
|
248
|
-
const { recreate, inPlace } = ALTERED_KINDS[after.kind]!;
|
|
254
|
+
const { recreate, inPlace, rule = "SQLPG303" } = ALTERED_KINDS[after.kind]!;
|
|
249
255
|
const replaced = recreate.filter((k) => !same(before.fields[k], after.fields[k]));
|
|
256
|
+
// An object that lives in a schema is another object under another name: a rename between builds is a drop and a create.
|
|
257
|
+
if (after.schema !== undefined && (before.schema !== after.schema || before.name !== after.name)) replaced.unshift("name");
|
|
250
258
|
if (replaced.length > 0) {
|
|
251
|
-
const
|
|
252
|
-
const
|
|
259
|
+
const value = (o: PgDiffObject, k: string) => (k === "name" ? qualified(o) : (show(o.fields[k]) ?? "-"));
|
|
260
|
+
const was = replaced.map((k) => `${k} ${value(before, k)}`).join("; ");
|
|
261
|
+
const now = replaced.map((k) => `${k} ${value(after, k)}`).join("; ");
|
|
253
262
|
if (before.dependents && before.dependents.length > 0) {
|
|
254
263
|
out.push(change(key, replaced.join(", "), "SQLPG301", was, now, { note: `${before.dependents.join(", ")} depend${before.dependents.length === 1 ? "s" : ""} on it` }));
|
|
255
264
|
} else out.push(change(key, replaced.join(", "), "SQLPG300", was, now));
|
|
256
265
|
return;
|
|
257
266
|
}
|
|
258
|
-
for (const k of inPlace) if (!same(before.fields[k], after.fields[k])) out.push(change(key, k,
|
|
267
|
+
for (const k of inPlace) if (!same(before.fields[k], after.fields[k])) out.push(change(key, k, rule, show(before.fields[k]), show(after.fields[k])));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Which of an operator's attributes `ALTER OPERATOR ... SET` can change from
|
|
272
|
+
* what the server holds (the Postgres 18 page, with the supported majors):
|
|
273
|
+
* RESTRICT and JOIN on every major, set or removed; from 17 COMMUTATOR and
|
|
274
|
+
* NEGATOR when the operator has none, and HASHES and MERGES when off, never
|
|
275
|
+
* back. Anything else about the definition is dropped and created.
|
|
276
|
+
*/
|
|
277
|
+
function operatorAlterable(key: string, before: Record<string, unknown>, after: Record<string, unknown>, major: number): boolean {
|
|
278
|
+
if (key === "restrict" || key === "join") return true;
|
|
279
|
+
if (major < 17) return false;
|
|
280
|
+
if (key === "commutator" || key === "negator") return before[key] === undefined && after[key] !== undefined;
|
|
281
|
+
if (key === "hashes" || key === "merges") return before[key] === undefined && after[key] === "true";
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** An operator's change: in place (SQLPG304) when every attribute that changed is one ALTER OPERATOR reaches, else dropped and created like the other kinds without an ALTER. */
|
|
286
|
+
function diffOperator(key: string, before: PgDiffObject, after: PgDiffObject, out: PgChange[], major: number): void {
|
|
287
|
+
const bo = (before.fields.definition ?? {}) as Record<string, unknown>;
|
|
288
|
+
const ao = (after.fields.definition ?? {}) as Record<string, unknown>;
|
|
289
|
+
const renamed = before.schema !== after.schema || before.name !== after.name || before.signature !== after.signature;
|
|
290
|
+
// In the order the page lists them, so the statement reads the same whatever the order the options were written in.
|
|
291
|
+
const ORDER = ["restrict", "join", "commutator", "negator", "hashes", "merges"];
|
|
292
|
+
const rank = (k: string) => (ORDER.includes(k) ? ORDER.indexOf(k) : ORDER.length);
|
|
293
|
+
const changed = [...new Set([...Object.keys(bo), ...Object.keys(ao)])].filter((k) => !same(bo[k], ao[k])).sort((a, b) => rank(a) - rank(b) || (a < b ? -1 : 1));
|
|
294
|
+
if (renamed || !changed.every((k) => operatorAlterable(k, bo, ao, major))) {
|
|
295
|
+
diffReplaced(key, before, after, out);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (!same(before.fields.comment, after.fields.comment)) out.push(change(key, "comment", "SQLPG216", show(before.fields.comment), show(after.fields.comment)));
|
|
299
|
+
const note = major < 17 ? undefined : "from 17 ALTER OPERATOR also sets a commutator's or negator's own commutator or negator";
|
|
300
|
+
for (const k of changed) out.push(change(key, `definition.${k}`, "SQLPG304", show(bo[k]), show(ao[k]), note && (k === "commutator" || k === "negator") ? { note } : {}));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* A foreign table's columns, as ALTER FOREIGN TABLE changes them (SQLPG306):
|
|
305
|
+
* all catalog changes, none reading the remote side. A column is its name
|
|
306
|
+
* (a rename is a drop and an add); its comment is the generic comment rule.
|
|
307
|
+
*/
|
|
308
|
+
function diffForeignColumns(key: string, before: PgDiffObject, after: PgDiffObject, out: PgChange[]): void {
|
|
309
|
+
const byName = new Map(before.columns.map((c) => [c.name, c]));
|
|
310
|
+
const afterNames = new Set(after.columns.map((c) => c.name));
|
|
311
|
+
for (const b of before.columns) if (!afterNames.has(b.name)) out.push(change(key, `columns.${b.name}`, "SQLPG306", `${b.name} ${b.type ?? ""}`.trim(), undefined));
|
|
312
|
+
for (const a of after.columns) {
|
|
313
|
+
const b = byName.get(a.name);
|
|
314
|
+
if (!b) {
|
|
315
|
+
out.push(change(key, `columns.${a.name}`, "SQLPG306", undefined, `${a.name} ${a.type ?? ""}`.trim()));
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const at = `columns.${a.name}`;
|
|
319
|
+
if (b.type !== a.type) out.push(change(key, `${at}.type`, "SQLPG306", b.type, a.type));
|
|
320
|
+
if (b.collate !== a.collate) out.push(change(key, `${at}.collate`, "SQLPG306", b.collate, a.collate));
|
|
321
|
+
if (b.default !== a.default) out.push(change(key, `${at}.default`, "SQLPG306", b.default, a.default));
|
|
322
|
+
if (b.notNull !== a.notNull) out.push(change(key, `${at}.notNull`, "SQLPG306", b.notNull ? "NOT NULL" : "NULL", a.notNull ? "NOT NULL" : "NULL"));
|
|
323
|
+
if (!same(b.options, a.options)) out.push(change(key, `${at}.options`, "SQLPG306", show(b.options), show(a.options)));
|
|
324
|
+
if (b.comment !== a.comment) out.push(change(key, `${at}.comment`, "SQLPG216", b.comment, a.comment));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** A foreign table's CHECK constraints: a dropped one first, so a constraint replaced under its name is dropped before it is added. */
|
|
329
|
+
function diffForeignConstraints(key: string, before: PgDiffObject, after: PgDiffObject, out: PgChange[]): void {
|
|
330
|
+
const label = (c: CanonicalConstraint) => (c.name ? `${c.name}: ${c.body}` : c.body);
|
|
331
|
+
const remaining = [...before.constraints];
|
|
332
|
+
const added: CanonicalConstraint[] = [];
|
|
333
|
+
const comments: PgChange[] = [];
|
|
334
|
+
for (const a of after.constraints) {
|
|
335
|
+
const i = remaining.findIndex((b) => sameConstraint(a, b));
|
|
336
|
+
if (i < 0) {
|
|
337
|
+
added.push(a);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const b = remaining.splice(i, 1)[0]!;
|
|
341
|
+
if (b.comment !== a.comment) comments.push(change(key, `constraints.${a.name ?? b.name}.comment`, "SQLPG216", b.comment, a.comment));
|
|
342
|
+
}
|
|
343
|
+
for (const b of remaining) out.push(change(key, `constraints.${b.name ?? b.kind.toLowerCase()}`, "SQLPG306", label(b), undefined));
|
|
344
|
+
for (const a of added) out.push(change(key, `constraints.${a.name ?? a.kind.toLowerCase()}`, "SQLPG306", undefined, label(a)));
|
|
345
|
+
out.push(...comments);
|
|
259
346
|
}
|
|
260
347
|
|
|
261
348
|
/**
|
|
@@ -292,6 +379,15 @@ function diffObject(key: string, before: PgDiffObject, after: PgDiffObject, out:
|
|
|
292
379
|
const [b, a] = f(name);
|
|
293
380
|
if (!same(b, a)) out.push(change(key, name, rule, show(b), show(a), extra));
|
|
294
381
|
};
|
|
382
|
+
if (after.kind === "operator") {
|
|
383
|
+
diffOperator(key, before, after, out, major);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
// A foreign table moved to another server is another table: ALTER FOREIGN TABLE has no SERVER action.
|
|
387
|
+
if (after.kind === "foreignTable" && before.schema === after.schema && before.name === after.name && !same(before.fields.server, after.fields.server)) {
|
|
388
|
+
out.push(change(key, "server", before.dependents && before.dependents.length > 0 ? "SQLPG301" : "SQLPG300", show(before.fields.server), show(after.fields.server), before.dependents && before.dependents.length > 0 ? { note: `${before.dependents.join(", ")} depend${before.dependents.length === 1 ? "s" : ""} on it` } : {}));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
295
391
|
if (REPLACED_KINDS.has(after.kind)) {
|
|
296
392
|
// A cast is named by its types, and an aggregate's argument types are its identity: either changed, between two builds, is another object.
|
|
297
393
|
if (after.kind === "cast" && before.name !== after.name) {
|
|
@@ -421,6 +517,11 @@ function diffObject(key: string, before: PgDiffObject, after: PgDiffObject, out:
|
|
|
421
517
|
field("attributes", "SQLPG294");
|
|
422
518
|
field("connectionLimit", "SQLPG294");
|
|
423
519
|
break;
|
|
520
|
+
case "foreignTable":
|
|
521
|
+
diffForeignColumns(key, before, after, out);
|
|
522
|
+
diffForeignConstraints(key, before, after, out);
|
|
523
|
+
field("options", "SQLPG306");
|
|
524
|
+
break;
|
|
424
525
|
case "table":
|
|
425
526
|
if (!same(before.fields.rowSecurity, after.fields.rowSecurity) || !same(before.fields.forceRowSecurity, after.fields.forceRowSecurity)) {
|
|
426
527
|
const state = (o: PgDiffObject) => (o.fields.rowSecurity ? (o.fields.forceRowSecurity ? "enabled, forced" : "enabled") : o.fields.forceRowSecurity ? "disabled, forced" : "disabled");
|