@intentius/chant-lexicon-sql 0.124.48 → 0.124.50

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 (138) hide show
  1. package/dist/clickhouse/plan/statements.d.ts +29 -0
  2. package/dist/clickhouse/plan/statements.d.ts.map +1 -0
  3. package/dist/clickhouse/plan/statements.js +334 -0
  4. package/dist/codegen/classifier-page.d.ts.map +1 -1
  5. package/dist/codegen/classifier-page.js +23 -0
  6. package/dist/codegen/postgres-classifier-page.js +2 -2
  7. package/dist/codegen/registry.d.ts.map +1 -1
  8. package/dist/codegen/registry.js +5 -0
  9. package/dist/core/statement-class.d.ts +55 -0
  10. package/dist/core/statement-class.d.ts.map +1 -0
  11. package/dist/core/statement-class.js +43 -0
  12. package/dist/coverage/postgres.json +6 -6
  13. package/dist/drizzle.d.ts.map +1 -1
  14. package/dist/drizzle.js +5 -0
  15. package/dist/files/postgres.d.ts.map +1 -1
  16. package/dist/files/postgres.js +23 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/integrity.json +5 -5
  21. package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
  22. package/dist/lint/post-synth/postgres-names.js +3 -0
  23. package/dist/lint/rules/postgres-templates.js +2 -2
  24. package/dist/lsp/postgres-template.d.ts.map +1 -1
  25. package/dist/lsp/postgres-template.js +10 -0
  26. package/dist/manifest.json +1 -1
  27. package/dist/mcp/postgres.js +1 -1
  28. package/dist/meta.json +25 -0
  29. package/dist/migration-classify.d.ts +23 -0
  30. package/dist/migration-classify.d.ts.map +1 -0
  31. package/dist/migration-classify.js +44 -0
  32. package/dist/okf/index.md +5 -0
  33. package/dist/okf/types/PostgresForeignTable.md +9 -0
  34. package/dist/okf/types/PostgresOperatorClass.md +9 -0
  35. package/dist/okf/types/PostgresOperatorFamily.md +9 -0
  36. package/dist/okf/types/PostgresTextSearchConfiguration.md +9 -0
  37. package/dist/okf/types/PostgresTextSearchDictionary.md +9 -0
  38. package/dist/plugin.d.ts.map +1 -1
  39. package/dist/plugin.js +13 -0
  40. package/dist/postgres/access/acl.js +1 -1
  41. package/dist/postgres/access/import.js +1 -1
  42. package/dist/postgres/apply/apply.js +5 -0
  43. package/dist/postgres/apply/statements.d.ts.map +1 -1
  44. package/dist/postgres/apply/statements.js +156 -2
  45. package/dist/postgres/entities.d.ts +120 -2
  46. package/dist/postgres/entities.d.ts.map +1 -1
  47. package/dist/postgres/entities.js +180 -5
  48. package/dist/postgres/entity-types.d.ts +5 -0
  49. package/dist/postgres/entity-types.d.ts.map +1 -1
  50. package/dist/postgres/entity-types.js +5 -0
  51. package/dist/postgres/import/generator.d.ts.map +1 -1
  52. package/dist/postgres/import/generator.js +16 -1
  53. package/dist/postgres/live/catalog-kinds.d.ts +6 -0
  54. package/dist/postgres/live/catalog-kinds.d.ts.map +1 -1
  55. package/dist/postgres/live/catalog-kinds.js +251 -3
  56. package/dist/postgres/live/describe-resources.d.ts.map +1 -1
  57. package/dist/postgres/live/describe-resources.js +2 -0
  58. package/dist/postgres/migrate/observe.d.ts.map +1 -1
  59. package/dist/postgres/migrate/observe.js +4 -1
  60. package/dist/postgres/migrate/replication.d.ts +42 -0
  61. package/dist/postgres/migrate/replication.d.ts.map +1 -1
  62. package/dist/postgres/migrate/replication.js +53 -0
  63. package/dist/postgres/parser.d.ts +73 -1
  64. package/dist/postgres/parser.d.ts.map +1 -1
  65. package/dist/postgres/parser.js +181 -7
  66. package/dist/postgres/plan/deep.d.ts.map +1 -1
  67. package/dist/postgres/plan/deep.js +15 -4
  68. package/dist/postgres/plan/diff.d.ts.map +1 -1
  69. package/dist/postgres/plan/diff.js +120 -5
  70. package/dist/postgres/plan/normalize.d.ts +12 -1
  71. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  72. package/dist/postgres/plan/normalize.js +151 -0
  73. package/dist/postgres/plan/rules.d.ts +3 -0
  74. package/dist/postgres/plan/rules.d.ts.map +1 -1
  75. package/dist/postgres/plan/rules.js +5 -2
  76. package/dist/postgres/plan/schema.js +1 -1
  77. package/dist/postgres/plan/statements.d.ts +26 -0
  78. package/dist/postgres/plan/statements.d.ts.map +1 -0
  79. package/dist/postgres/plan/statements.js +364 -0
  80. package/dist/postgres/testing/roundtrip-schema.d.ts +3 -2
  81. package/dist/postgres/testing/roundtrip-schema.d.ts.map +1 -1
  82. package/dist/postgres/testing/roundtrip-schema.js +12 -1
  83. package/dist/postgres.d.ts +1 -1
  84. package/dist/postgres.d.ts.map +1 -1
  85. package/dist/postgres.js +1 -1
  86. package/dist/rules/postgres-names.ts +2 -0
  87. package/dist/rules/postgres-templates.ts +2 -2
  88. package/dist/validate.d.ts.map +1 -1
  89. package/dist/validate.js +5 -0
  90. package/package.json +2 -2
  91. package/src/clickhouse/plan/statements.test.ts +61 -0
  92. package/src/clickhouse/plan/statements.ts +306 -0
  93. package/src/codegen/classifier-page.ts +23 -0
  94. package/src/codegen/postgres-classifier-page.ts +2 -2
  95. package/src/codegen/registry.ts +5 -0
  96. package/src/core/statement-class.ts +88 -0
  97. package/src/coverage/postgres.json +6 -6
  98. package/src/drizzle.ts +5 -0
  99. package/src/files/postgres.ts +22 -1
  100. package/src/generated/lexicon-sql.json +25 -0
  101. package/src/index.ts +1 -1
  102. package/src/lint/post-synth/postgres-checks.test.ts +8 -1
  103. package/src/lint/post-synth/postgres-names.ts +2 -0
  104. package/src/lint/rules/postgres-templates.ts +2 -2
  105. package/src/lsp/postgres-template.ts +9 -0
  106. package/src/mcp/postgres.ts +1 -1
  107. package/src/migration-classify.test.ts +150 -0
  108. package/src/migration-classify.ts +45 -0
  109. package/src/op/watch-pipeline.test.ts +8 -1
  110. package/src/plugin.ts +14 -0
  111. package/src/postgres/access/acl.ts +1 -1
  112. package/src/postgres/access/import.ts +1 -1
  113. package/src/postgres/apply/apply.ts +5 -0
  114. package/src/postgres/apply/statements.ts +151 -3
  115. package/src/postgres/database-kinds.test.ts +498 -17
  116. package/src/postgres/entities.ts +276 -5
  117. package/src/postgres/entity-types.ts +5 -0
  118. package/src/postgres/import/generator.ts +13 -1
  119. package/src/postgres/live/catalog-kinds.ts +275 -3
  120. package/src/postgres/live/describe-resources.ts +2 -0
  121. package/src/postgres/migrate/observe.ts +3 -1
  122. package/src/postgres/migrate/replication.ts +66 -0
  123. package/src/postgres/migrate/subscriber.e2e.test.ts +141 -0
  124. package/src/postgres/migrate/subscriber.test.ts +59 -0
  125. package/src/postgres/parser.ts +225 -6
  126. package/src/postgres/plan/deep.ts +15 -4
  127. package/src/postgres/plan/diff.ts +107 -6
  128. package/src/postgres/plan/normalize.ts +155 -0
  129. package/src/postgres/plan/rules.ts +25 -4
  130. package/src/postgres/plan/schema.ts +1 -1
  131. package/src/postgres/plan/statements.test.ts +78 -0
  132. package/src/postgres/plan/statements.ts +299 -0
  133. package/src/postgres/testing/roundtrip-schema.ts +12 -1
  134. package/src/postgres.ts +17 -0
  135. package/src/skills/chant-sql-postgres-migration.md +1 -0
  136. package/src/skills/chant-sql-postgres.md +5 -0
  137. package/src/sql-files.test.ts +33 -0
  138. package/src/validate.ts +5 -0
@@ -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.accept("COMPRESSION")) col.compression = this.expr(COLUMN_CLAUSE);
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")) return this.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 === "domain" ? ["CHECK"] : Object.keys(CONSTRAINT_PROPS);
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 was = replaced.map((k) => `${k} ${show(before.fields[k]) ?? "-"}`).join("; ");
252
- const now = replaced.map((k) => `${k} ${show(after.fields[k]) ?? "-"}`).join("; ");
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, "SQLPG303", show(before.fields[k]), show(after.fields[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");