@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.
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 +246 -0
  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 +271 -0
  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
@@ -0,0 +1,306 @@
1
+ /**
2
+ * The ClickHouse statement classifier (#4407): a migration file's statements,
3
+ * each given the class and rule the schema-diff classifier (`./rules.ts`)
4
+ * gives the same change. It reads text and contacts no server.
5
+ *
6
+ * `CREATE` goes through the declaration parser (`../parser.ts`), which also
7
+ * tells the classifier a table's keys when the file creates the table. The
8
+ * parser has no `ALTER` grammar, so `ALTER`, `DROP` and `RENAME` are read word
9
+ * by word from the same tokens.
10
+ *
11
+ * A statement alone cannot decide some changes, and those answer `undecided`
12
+ * naming both rules, the way `./disruption.ts` answers `unknown`:
13
+ *
14
+ * - `MODIFY COLUMN c <type>` is SQLCH210 (a rewrite), or SQLCH211 (a rebuild)
15
+ * when `c` is in a key. The file states the keys only when it creates the
16
+ * table.
17
+ * - `DROP COLUMN` is SQLCH202 or SQLCH213, and `RENAME COLUMN` SQLCH212 or
18
+ * SQLCH213, on the same condition.
19
+ * - `MODIFY ORDER BY` that the same `ALTER` pairs with `ADD COLUMN` is SQLCH216
20
+ * when it only appends those columns and SQLCH220 (a rebuild) otherwise.
21
+ * Without such a column it is SQLCH220.
22
+ */
23
+
24
+ import { MERGE_TREE_SETTINGS } from "../../generated/clickhouse";
25
+ import { isTrivia, tokenizeText, type Token } from "../tokens";
26
+ import { parseCreate, type CreateNode } from "../parser";
27
+ import type { Span } from "../../core/cursor";
28
+ import { classifiedOf, splitTopLevel, sqlHead, type StatementClassified, type StatementVerdict } from "../../core/statement-class";
29
+ import { CHANGE_CLASSES, CLASSIFIER_RULES, type ClassifierRuleId } from "./rules";
30
+
31
+ const classified = (id: ClassifierRuleId, extra: Partial<StatementClassified> = {}): StatementClassified => classifiedOf(CLASSIFIER_RULES, CHANGE_CLASSES.label, id, extra);
32
+
33
+ type Reading = { rules: Array<{ id: ClassifierRuleId; extra?: Partial<StatementClassified> }> } | { undecided: ClassifierRuleId[]; note: string } | { none: string } | { data: string };
34
+
35
+ const rule = (id: ClassifierRuleId, extra?: Partial<StatementClassified>): Reading => ({ rules: [{ id, ...(extra ? { extra } : {}) }] });
36
+
37
+ /** Settings only a stream engine takes (the same list `./disruption.ts` reads). */
38
+ const STREAM_SETTING = /^(?:kafka_|rabbitmq_|nats_|s3queue_|azure_queue_|keeper_path$)/;
39
+
40
+ /** What a table the file creates tells later statements in it. */
41
+ interface Created {
42
+ /** The columns in the sorting, primary, partition or sampling key. */
43
+ keys: Set<string>;
44
+ orderBy?: string;
45
+ }
46
+
47
+ const bare = (name: string): string => (name.split(".").pop() ?? name).replace(/^[`"]|[`"]$/g, "").toLowerCase();
48
+
49
+ const text = (tokens: readonly Token[], s: Span | undefined): string => (s ? tokens.slice(s.from, s.to).filter((t) => !isTrivia(t)).map((t) => t.text).join(" ") : "");
50
+
51
+ /** The identifier tokens of a key expression that are not function names. */
52
+ function columnsOf(tokens: readonly Token[], s: Span | undefined): string[] {
53
+ if (!s) return [];
54
+ const toks = tokens.slice(s.from, s.to).filter((t) => !isTrivia(t));
55
+ return toks.flatMap((t, i) => ((t.kind === "ident" || t.kind === "qident") && toks[i + 1]?.text !== "(" ? [bare(t.text)] : []));
56
+ }
57
+
58
+ function nameAt(words: readonly string[], i: number): { name: string; next: number } {
59
+ let name = words[i] ?? "";
60
+ let j = i + 1;
61
+ while (words[j] === "." && words[j + 1] !== undefined) {
62
+ name += `.${words[j + 1]}`;
63
+ j += 2;
64
+ }
65
+ return { name, next: j };
66
+ }
67
+
68
+ const KNOWN_VERBS = new Set(["ADD", "DROP", "RENAME", "MODIFY", "COMMENT", "CLEAR", "MATERIALIZE", "UPDATE", "DELETE", "ATTACH", "DETACH", "FREEZE", "FETCH", "MOVE", "REPLACE", "RESET", "REMOVE", "APPLY", "UNFREEZE", "EXECUTE"]);
69
+
70
+ /** Normalise a key expression for comparing an old and a new one: no spaces, no outer parentheses. */
71
+ const keyText = (words: readonly string[]): string => {
72
+ let t = words.join("").toLowerCase();
73
+ while (t.startsWith("(") && t.endsWith(")")) t = t.slice(1, -1);
74
+ return t;
75
+ };
76
+
77
+ interface TableContext {
78
+ created?: Created;
79
+ /** Columns the same ALTER adds. */
80
+ added: Set<string>;
81
+ }
82
+
83
+ function columnVerdict(kind: "drop" | "rename", col: string, ctx: TableContext): Reading {
84
+ const plain: ClassifierRuleId = kind === "drop" ? "SQLCH202" : "SQLCH212";
85
+ const extra = kind === "drop" ? { destructive: true } : undefined;
86
+ if (ctx.created) {
87
+ return ctx.created.keys.has(bare(col)) ? rule("SQLCH213", extra) : rule(plain, extra);
88
+ }
89
+ return { undecided: [plain, "SQLCH213"], note: `${col} is a column change or, in a key, a rebuild; the file does not create the table, so its keys are not known offline` };
90
+ }
91
+
92
+ function tableAction(a: readonly string[], ctx: TableContext): Reading {
93
+ const u = a.map((w) => w.toUpperCase());
94
+ const first = u[0];
95
+ const second = u[1];
96
+ if (first === "ADD") {
97
+ if (second === "COLUMN") return rule("SQLCH201");
98
+ if (second === "INDEX") return rule("SQLCH204");
99
+ if (second === "PROJECTION") return rule("SQLCH214");
100
+ if (second === "CONSTRAINT") return rule("SQLCH215");
101
+ return { none: `ALTER TABLE ${u.slice(0, 2).join(" ")}` };
102
+ }
103
+ if (first === "DROP") {
104
+ if (second === "COLUMN") {
105
+ const k = u[2] === "IF" ? 4 : 2;
106
+ return columnVerdict("drop", a[k] ?? "column", ctx);
107
+ }
108
+ if (second === "INDEX") return rule("SQLCH204");
109
+ if (second === "PROJECTION") return rule("SQLCH214");
110
+ if (second === "CONSTRAINT") return rule("SQLCH215");
111
+ return { none: `ALTER TABLE ${u.slice(0, 2).join(" ")} (removes data parts, no rule covers it)` };
112
+ }
113
+ if (first === "CLEAR") {
114
+ if (second === "INDEX") return rule("SQLCH204");
115
+ if (second === "PROJECTION") return rule("SQLCH214");
116
+ return { none: `ALTER TABLE CLEAR ${second ?? ""}`.trim() };
117
+ }
118
+ if (first === "RENAME" && second === "COLUMN") {
119
+ const k = u[2] === "IF" ? 4 : 2;
120
+ return columnVerdict("rename", a[k] ?? "column", ctx);
121
+ }
122
+ if (first === "COMMENT" && second === "COLUMN") return rule("SQLCH203");
123
+ if (first === "MODIFY" || first === "RESET") {
124
+ if (second === "COLUMN") {
125
+ let k = 2;
126
+ if (u[k] === "IF" && u[k + 1] === "EXISTS") k += 2;
127
+ const col = a[k] ?? "column";
128
+ const after = u[k + 1];
129
+ if (after === "REMOVE") {
130
+ const what = u[k + 2];
131
+ return what === "CODEC" ? rule("SQLCH208") : what === "COMMENT" ? rule("SQLCH203") : what === "TTL" ? rule("SQLCH205") : rule("SQLCH207");
132
+ }
133
+ if (after === "DEFAULT" || after === "MATERIALIZED" || after === "ALIAS" || after === "EPHEMERAL") return rule("SQLCH207");
134
+ if (after === "CODEC") return rule("SQLCH208");
135
+ if (after === "COMMENT") return rule("SQLCH203");
136
+ if (after === "TTL") return rule("SQLCH205");
137
+ if (after === "MODIFY" || after === "RESET") return { none: "MODIFY COLUMN ... SETTING" };
138
+ if (after === undefined) return { none: "MODIFY COLUMN with no change" };
139
+ if (ctx.created) return ctx.created.keys.has(bare(col)) ? rule("SQLCH211") : rule("SQLCH210");
140
+ return { undecided: ["SQLCH210", "SQLCH211"], note: `a type change of ${col} is a background rewrite or, when ${col} is in a key, a rebuild; the file does not create the table, so its keys are not known offline` };
141
+ }
142
+ if (second === "ORDER" && u[2] === "BY") {
143
+ const next = a.slice(3);
144
+ if (ctx.created?.orderBy !== undefined) {
145
+ const was = ctx.created.orderBy;
146
+ const now = keyText(next);
147
+ const tail = now.startsWith(was) ? now.slice(was.length) : undefined;
148
+ const extra = tail === undefined ? [] : tail.split(/[^a-z0-9_]+/).filter(Boolean);
149
+ if (tail !== undefined && extra.length > 0 && extra.every((c) => ctx.added.has(c))) return rule("SQLCH216");
150
+ return rule("SQLCH220");
151
+ }
152
+ const mentions = next.some((w) => ctx.added.has(bare(w)));
153
+ if (mentions) return { undecided: ["SQLCH216", "SQLCH220"], note: "the new key appends the columns this ALTER adds only if its other elements are the old key's, which the file does not state" };
154
+ return rule("SQLCH220");
155
+ }
156
+ if (second === "SAMPLE" && u[2] === "BY") return rule("SQLCH217");
157
+ if (second === "TTL") return rule("SQLCH205");
158
+ if (second === "COMMENT") return rule("SQLCH203");
159
+ if (second === "QUERY") return rule("SQLCH241");
160
+ if (second === "REFRESH") return rule("SQLCH244");
161
+ if (second === "SETTING" || second === "SETTINGS") {
162
+ const key = (a[2] ?? "").toLowerCase();
163
+ return settingRule(key);
164
+ }
165
+ return { none: `ALTER TABLE ${u.slice(0, 2).join(" ")}` };
166
+ }
167
+ if (first === "REMOVE") {
168
+ if (second === "TTL") return rule("SQLCH205");
169
+ if (second === "SAMPLE" && u[2] === "BY") return rule("SQLCH217");
170
+ return { none: `ALTER TABLE REMOVE ${second ?? ""}`.trim() };
171
+ }
172
+ if (first === "UPDATE" || first === "DELETE") return { data: "a mutation: it rewrites the parts it touches in the background (system.mutations) and has no rollback" };
173
+ return { none: `ALTER TABLE ${u.slice(0, 2).join(" ")}` };
174
+ }
175
+
176
+ function settingRule(key: string): Reading {
177
+ if (STREAM_SETTING.test(key)) return rule("SQLCH225", { note: `${key} belongs to a stream engine, which has no MODIFY SETTING` });
178
+ const spec = (MERGE_TREE_SETTINGS as Record<string, { readonly: boolean } | undefined>)[key];
179
+ return spec?.readonly ? rule("SQLCH218", { note: `${key} is fixed at creation` }) : rule("SQLCH206");
180
+ }
181
+
182
+ /** The `ALTER TABLE` actions of one statement, in order, a `MODIFY SETTING` list kept together. */
183
+ function alterTable(words: readonly string[], u: readonly string[], created: Map<string, Created>): Reading[] {
184
+ let i = 2;
185
+ if (u[i] === "IF" && u[i + 1] === "EXISTS") i += 2;
186
+ const { name, next } = nameAt(words, i);
187
+ i = next;
188
+ if (u[i] === "ON" && u[i + 1] === "CLUSTER") i += 3;
189
+ const actions = splitTopLevel(words.slice(i));
190
+ const added = new Set<string>();
191
+ for (const a of actions) {
192
+ const au = a.map((w) => w.toUpperCase());
193
+ if (au[0] === "ADD" && au[1] === "COLUMN") {
194
+ const k = au[2] === "IF" ? 5 : 2;
195
+ if (a[k]) added.add(bare(a[k]!));
196
+ }
197
+ }
198
+ const ctx: TableContext = { added, ...(created.has(bare(name)) ? { created: created.get(bare(name))! } : {}) };
199
+ const out: Reading[] = [];
200
+ let inSettings = false;
201
+ for (const a of actions) {
202
+ const au = a.map((w) => w.toUpperCase());
203
+ if (inSettings && au[0] !== undefined && !KNOWN_VERBS.has(au[0])) {
204
+ out.push(settingRule((a[0] ?? "").toLowerCase()));
205
+ continue;
206
+ }
207
+ inSettings = (au[0] === "MODIFY" || au[0] === "RESET") && (au[1] === "SETTING" || au[1] === "SETTINGS");
208
+ out.push(tableAction(a, ctx));
209
+ }
210
+ return out;
211
+ }
212
+
213
+ const OBJECTS: Record<string, ClassifierRuleId> = { USER: "SQLCH271", ROLE: "SQLCH270", QUOTA: "SQLCH275" };
214
+
215
+ function alterOther(u: readonly string[]): Reading {
216
+ const obj = u[1];
217
+ if (obj && OBJECTS[obj]) return rule(OBJECTS[obj]!);
218
+ if (obj === "ROW" && u[2] === "POLICY") return rule("SQLCH272");
219
+ if (obj === "POLICY") return rule("SQLCH272");
220
+ if (obj === "SETTINGS" && u[2] === "PROFILE") return rule("SQLCH276");
221
+ if (obj === "DATABASE" && u.includes("COMMENT")) return rule("SQLCH203");
222
+ return { none: `ALTER ${obj ?? ""}`.trim() };
223
+ }
224
+
225
+ const DATA_WORDS = new Set(["INSERT", "SELECT", "WITH", "SYSTEM", "OPTIMIZE", "TRUNCATE", "SET", "USE", "EXPLAIN", "EXISTS", "DESCRIBE", "DESC", "SHOW", "CHECK", "KILL", "WATCH"]);
226
+
227
+
228
+ /** A `CREATE` or `ATTACH`: the parser reads tables, views, dictionaries, databases and functions; the rest go by key words. */
229
+ function create(tokens: Token[], u: readonly string[], created: Map<string, Created>): Reading {
230
+ let i = 1;
231
+ const orReplace = u[i] === "OR" && u[i + 1] === "REPLACE";
232
+ if (orReplace) i += 2;
233
+ let node: CreateNode | undefined;
234
+ try {
235
+ node = parseCreate(tokens);
236
+ } catch {
237
+ node = undefined;
238
+ }
239
+ if (node?.statement === "table") {
240
+ const name = text(tokens, node.name).replace(/\s*\.\s*/g, ".");
241
+ const keys = new Set<string>([...columnsOf(tokens, node.orderBy), ...columnsOf(tokens, node.primaryKey), ...columnsOf(tokens, node.partitionBy), ...columnsOf(tokens, node.sampleBy)]);
242
+ for (const c of node.columns) if (c.primaryKey) keys.add(bare(c.name));
243
+ created.set(bare(name), { keys, ...(node.orderBy ? { orderBy: keyText(text(tokens, node.orderBy).split(" ")) } : {}) });
244
+ }
245
+ if (orReplace && u[i] === "FUNCTION") return rule("SQLCH260");
246
+ if (orReplace && (u[i] === "TABLE" || node?.statement === "table")) return rule("SQLCH200", { note: "OR REPLACE drops an existing table of that name, and its rows, before it creates this one" });
247
+ return rule("SQLCH200");
248
+ }
249
+
250
+ function read(sql: string, created: Map<string, Created>): Reading[] {
251
+ let tokens: Token[];
252
+ try {
253
+ tokens = tokenizeText(sql, 0);
254
+ } catch {
255
+ return [{ none: "text the tokenizer could not read" }];
256
+ }
257
+ const words = tokens.filter((t) => !isTrivia(t) && t.text !== ";").map((t) => t.text);
258
+ const u = words.map((w) => w.toUpperCase());
259
+ const head = u[0];
260
+ if (head === undefined) return [{ data: "an empty statement" }];
261
+ if (head === "CREATE") return [create(tokens, u, created)];
262
+ if (head === "ALTER" && u[1] === "TABLE") return alterTable(words, u, created);
263
+ if (head === "ALTER") return [alterOther(u)];
264
+ if (head === "DROP") return [rule("SQLCH250", { destructive: true })];
265
+ if (head === "RENAME") {
266
+ if (u[1] === "DATABASE") return [rule("SQLCH231")];
267
+ if (u[1] === "TABLE" || u[1] === "DICTIONARY") return [rule("SQLCH230")];
268
+ return [{ none: `RENAME ${u[1] ?? ""}`.trim() }];
269
+ }
270
+ if (head === "GRANT") return [rule("SQLCH273")];
271
+ if (head === "REVOKE") return [rule("SQLCH274")];
272
+ if (head === "EXCHANGE") return [{ none: "EXCHANGE TABLES swaps two tables; no rule covers it" }];
273
+ if (head === "TRUNCATE") return [{ data: "TRUNCATE removes every row of the table" }];
274
+ if (DATA_WORDS.has(head)) return [{ data: `${head} reads or writes rows or the session, not the schema` }];
275
+ return [{ none: `${head} is not a statement the classifier reads` }];
276
+ }
277
+
278
+ /**
279
+ * Classify a migration's up statements, in order. The statements are the
280
+ * file's own text, one per entry.
281
+ */
282
+ export function classifyClickHouseStatements(statements: readonly string[]): StatementVerdict[] {
283
+ const created = new Map<string, Created>();
284
+ return statements.map((sql, k) => {
285
+ const readings = read(sql, created);
286
+ const out: StatementVerdict = { index: k + 1, sql: sqlHead(sql), outcome: "classified", changes: [] };
287
+ const none: string[] = [];
288
+ const notes: string[] = [];
289
+ for (const r of readings) {
290
+ if ("rules" in r) out.changes.push(...r.rules.map((x) => classified(x.id, x.extra)));
291
+ else if ("undecided" in r) {
292
+ out.candidates = [...(out.candidates ?? []), ...r.undecided.map((id) => classified(id))];
293
+ notes.push(r.note);
294
+ } else if ("none" in r) none.push(r.none);
295
+ else notes.push(r.data);
296
+ }
297
+ if (out.candidates) out.outcome = "undecided";
298
+ else if (out.changes.length === 0 && none.length === 0) out.outcome = "data";
299
+ else if (none.length > 0) {
300
+ out.outcome = out.changes.length > 0 ? "classified" : "unclassified";
301
+ notes.push(`no rule covers ${none.join("; ")}`);
302
+ }
303
+ if (notes.length > 0) out.note = notes.join("; ");
304
+ return out;
305
+ });
306
+ }
@@ -93,6 +93,29 @@ A rebuild is never DDL: it is a step naming \`ClickHouseRebuildOp\`, or for a vi
93
93
 
94
94
  \`--topology\` renders the statements for where they will run: \`single\` (the default), \`cluster:<name>\`, \`replicated\` or \`cloud\`. On a cluster every statement carries \`ON CLUSTER\` and a MergeTree-family table becomes \`Replicated*MergeTree\` with its Keeper path; in a \`Replicated\` database the tables are \`Replicated*MergeTree\` with no path and no \`ON CLUSTER\`. \`diffStatements(before, after, { topology })\` takes the same choice; see [Applying to a Server](../applying/#topology).
95
95
 
96
+ ## Migration files on a pull request
97
+
98
+ A workspace member that is a goose, golang-migrate, clickhouse-migrations or Flyway directory (see [Workspace kinds](/chant/reference/workspace-kinds/)) has no declaration to diff. On a pull request, \`chant components pr-plan\` lists each migration file the pull request adds to such a member, and the plan note gives every statement in it the class the rules below give the same change. This is the "check" level of adopting chant beside an existing migration tool: it reads files, applies nothing and contacts no server, so it needs no \`--env\`.
99
+
100
+ The up statements are the ones \`chant sql replay\` would run: goose's \`-- +goose Up\` section, the whole \`.up.sql\` file for golang-migrate, the whole file for clickhouse-migrations, and a Flyway file after placeholder substitution with the values in \`flyway.toml\` or \`flyway.conf\`. A file the base commit already has is not new, whatever the pull request does to it. Undo files are left out.
101
+
102
+ \`\`\`text
103
+ \`db/00002_orders.sql\` (member \`ledger\`, postgres)
104
+ - 1. \`CREATE INDEX orders_status_idx ON orders (status)\`: needs CONCURRENTLY (SQLPG241, SHARE)
105
+ - 2. \`ALTER TABLE orders ALTER COLUMN a TYPE bigint\`: undecided between SQLPG206 metadata only and SQLPG207 ACCESS EXCLUSIVE rewrite or scan
106
+ \`\`\`
107
+
108
+ Each statement is read with the dialect's parser (\`CREATE\`, \`GRANT\`, \`COMMENT ON\`) or word by word (\`ALTER\`, \`DROP\`, \`RENAME\`), and the lock on a Postgres statement is the one its rule's text names. The code is \`lexicons/sql/src/postgres/plan/statements.ts\` and \`lexicons/sql/src/clickhouse/plan/statements.ts\`.
109
+
110
+ A statement alone cannot always decide a rule, and then the note names the candidates and does not guess:
111
+
112
+ - \`ALTER COLUMN ... TYPE\` is SQLPG206 or SQLPG207 by the column's old type, and SQLPG208 as well with a \`USING\` expression.
113
+ - \`MODIFY COLUMN <type>\` is SQLCH210 or SQLCH211, \`DROP COLUMN\` SQLCH202 or SQLCH213, and \`RENAME COLUMN\` SQLCH212 or SQLCH213, by whether the column is in a key. When the same file creates the table, its keys are known and the statement is classified.
114
+ - \`MODIFY ORDER BY\` is SQLCH220, a rebuild. It is SQLCH216 or SQLCH220 when the same \`ALTER\` adds the columns the new key ends with, unless the file created the table and shows the old key is kept.
115
+ - \`CREATE OR REPLACE VIEW\` is SQLPG250 or SQLPG251 by whether the view keeps its columns.
116
+
117
+ A statement that reads or writes rows (\`INSERT\`, \`UPDATE\`, \`TRUNCATE\`) is listed as data. A schema change no rule covers, such as \`ALTER TABLE ... OWNER TO\` or a ClickHouse \`DROP PARTITION\`, is listed as having no class.
118
+
96
119
  ## Identity and renames
97
120
 
98
121
  An object is identified by its export name between two builds, so a changed name in the SQL under the same export is a rename. Against a server, which has no export names, an object is identified by \`database.name\`; a declaration renamed since the server last saw it says so with \`-- previously: <old name>\` before its CREATE.
@@ -65,14 +65,14 @@ A column rename or a type change across kinds is never DDL: it is a step naming
65
65
 
66
66
  ## Which major
67
67
 
68
- Two rules change class across the supported majors: a STORED generated column's expression (SQLPG212) is a rewrite from 17 and expand and contract before it, and a table's access method (SQLPG226) is a rewrite from 15 and expand and contract before it.
68
+ Three rules change class across the supported majors: a STORED generated column's expression (SQLPG212) is a rewrite from 17 and expand and contract before it, a table's access method (SQLPG226) is a rewrite from 15 and expand and contract before it, and an operator's commutator, negator, HASHES and MERGES (SQLPG304) are altered in place from 17 and dropped and created before it.
69
69
 
70
70
  - \`chant sql diff\` classifies for the major the builds recorded (\`postgresMajor\` in the build output): the newer build's, else the older build's, else \`sql.postgresMajor\`, else 18. A diff between two revisions does not depend on today's config.
71
71
  - \`chant sql plan\` classifies for the major the server runs (\`server_version_num\`), since the server is what takes the locks. When the build targets another major, the plan says so in a hint.
72
72
 
73
73
  ## Identity and renames
74
74
 
75
- An object is identified by its export name between two builds, and by its schema-qualified name against a server, which has no export names. Tables, views, materialized views, sequences and indexes share one namespace per schema, types and domains another, and schemas and extensions are matched by name. Functions, procedures and aggregates share a namespace of their own and are told apart by their input parameter types, so an overload is another object; composite and range types share the types' namespace, collations have one of their own, extended statistics another, an operator by its symbol and operand types, a cast by its two types and an event trigger, a foreign data wrapper, a foreign server and a publication by their names in the database; a trigger is matched by its name on its table.
75
+ An object is identified by its export name between two builds, and by its schema-qualified name against a server, which has no export names. Tables, foreign tables, views, materialized views, sequences and indexes share one namespace per schema, types and domains another, and schemas and extensions are matched by name. Functions, procedures and aggregates share a namespace of their own and are told apart by their input parameter types, so an overload is another object; composite and range types share the types' namespace, collations have one of their own, extended statistics another, an operator by its symbol and operand types, an operator class and an operator family each by its name and index method, text search configurations one namespace and dictionaries another, a cast by its two types and an event trigger, a foreign data wrapper, a foreign server and a publication by their names in the database; a trigger is matched by its name on its table.
76
76
 
77
77
  A rename is declared where it happens. Before a CREATE, \`-- previously: <old name>\` says the object had another name; on a column's line, it says the column did:
78
78
 
@@ -40,6 +40,11 @@ const KINDS: Record<string, string> = {
40
40
  PostgresCast: POSTGRES_ENTITY_TYPES.cast,
41
41
  PostgresEventTrigger: POSTGRES_ENTITY_TYPES.eventTrigger,
42
42
  PostgresOperator: POSTGRES_ENTITY_TYPES.operator,
43
+ PostgresOperatorClass: POSTGRES_ENTITY_TYPES.operatorClass,
44
+ PostgresOperatorFamily: POSTGRES_ENTITY_TYPES.operatorFamily,
45
+ PostgresForeignTable: POSTGRES_ENTITY_TYPES.foreignTable,
46
+ PostgresTextSearchConfiguration: POSTGRES_ENTITY_TYPES.textSearchConfiguration,
47
+ PostgresTextSearchDictionary: POSTGRES_ENTITY_TYPES.textSearchDictionary,
43
48
  PostgresStatistics: POSTGRES_ENTITY_TYPES.statistics,
44
49
  PostgresForeignDataWrapper: POSTGRES_ENTITY_TYPES.foreignDataWrapper,
45
50
  PostgresForeignServer: POSTGRES_ENTITY_TYPES.foreignServer,
@@ -0,0 +1,88 @@
1
+ /**
2
+ * What the statement classifiers (`../clickhouse/plan/statements.ts`,
3
+ * `../postgres/plan/statements.ts`) answer, in one shape for both dialects
4
+ * (#4407). They read a migration file's statements offline and give each the
5
+ * class the schema-diff classifier gives the same change, citing its rule.
6
+ * Nothing runs against a server.
7
+ */
8
+
9
+ import type { ClassifierRule } from "./classifier";
10
+
11
+ /** One change a statement makes, under one rule. */
12
+ export interface StatementClassified {
13
+ rule: string;
14
+ /** The rule's class, e.g. `rebuild`, `concurrently`. */
15
+ class: string;
16
+ /** The class as a report names it, e.g. `REBUILD`. */
17
+ label: string;
18
+ title: string;
19
+ /** Postgres: the lock the rule's restriction names, e.g. `SHARE`. */
20
+ lock?: string;
21
+ /** Data is removed and not recoverable. */
22
+ destructive?: boolean;
23
+ note?: string;
24
+ }
25
+
26
+ /**
27
+ * - `classified`: every change has a rule.
28
+ * - `undecided`: the statement alone cannot say which of `candidates` applies
29
+ * (a column type change is a rewrite, or a rebuild when the column is in a
30
+ * key); the answer `classifyDisruption()` calls `unknown`.
31
+ * - `unclassified`: a schema change no rule covers; `note` says what.
32
+ * - `data`: the statement reads or writes rows or the session, not the schema.
33
+ */
34
+ export type StatementOutcome = "classified" | "undecided" | "unclassified" | "data";
35
+
36
+ export interface StatementVerdict {
37
+ /** 1-based position among the file's up statements. */
38
+ index: number;
39
+ /** The statement's first line, cut to 120 characters. */
40
+ sql: string;
41
+ outcome: StatementOutcome;
42
+ /** `classified`: one entry per change in the statement (an `ALTER TABLE` can hold several). */
43
+ changes: StatementClassified[];
44
+ /** `undecided`: the rules that could apply, in the order a reader should weigh them. */
45
+ candidates?: StatementClassified[];
46
+ note?: string;
47
+ }
48
+
49
+ /** A statement's own text cut for a note: the first line, at most 120 characters. */
50
+ export function sqlHead(sql: string): string {
51
+ const line = sql.split(/\r?\n/).find((l) => l.trim() !== "" && !l.trim().startsWith("--")) ?? sql;
52
+ const t = line.trim().replace(/\s+/g, " ");
53
+ return t.length > 120 ? `${t.slice(0, 117)}...` : t;
54
+ }
55
+
56
+ /** The pieces of a classified change from a dialect's rule table. */
57
+ export function classifiedOf<C extends string>(
58
+ rules: Readonly<Record<string, ClassifierRule<C>>>,
59
+ labels: Readonly<Record<C, string>>,
60
+ id: string,
61
+ extra: Partial<StatementClassified> = {},
62
+ ): StatementClassified {
63
+ const r = rules[id];
64
+ if (!r) throw new Error(`no classifier rule ${id}`);
65
+ return { rule: id, class: r.class, label: labels[r.class], title: r.title, ...extra };
66
+ }
67
+
68
+ /** Lock names, longest first so `SHARE UPDATE EXCLUSIVE` is not read as `SHARE`. */
69
+ const LOCK = /\b(ACCESS EXCLUSIVE|SHARE UPDATE EXCLUSIVE|SHARE ROW EXCLUSIVE|ROW EXCLUSIVE|ROW SHARE|EXCLUSIVE|SHARE)\b/;
70
+
71
+ /** The first lock a rule's restriction names, or undefined when it names none. */
72
+ export const lockOf = (restriction: string): string | undefined => LOCK.exec(restriction)?.[1];
73
+
74
+ /**
75
+ * Split a statement's tokens' text into the pieces between top-level commas
76
+ * (an `ALTER TABLE`'s actions). Parentheses and brackets nest.
77
+ */
78
+ export function splitTopLevel(words: readonly string[]): string[][] {
79
+ const out: string[][] = [[]];
80
+ let depth = 0;
81
+ for (const w of words) {
82
+ if (w === "(" || w === "[") depth++;
83
+ else if (w === ")" || w === "]") depth--;
84
+ if (w === "," && depth === 0) out.push([]);
85
+ else out[out.length - 1]!.push(w);
86
+ }
87
+ return out.filter((a) => a.length > 0);
88
+ }
@@ -11,16 +11,16 @@
11
11
  { "command": "CREATE EVENT TRIGGER", "kind": "event trigger", "status": "declared", "entity": "Postgres::EventTrigger", "note": "the database's, not a schema's; creating one takes a superuser" },
12
12
  { "command": "CREATE EXTENSION", "kind": "extension", "status": "declared", "entity": "Postgres::Extension" },
13
13
  { "command": "CREATE FOREIGN DATA WRAPPER", "kind": "foreign data wrapper", "status": "declared", "entity": "Postgres::ForeignDataWrapper", "note": "the database's, not a schema's; creating one takes a superuser; one an extension installs comes with the extension; an option that holds a credential is refused" },
14
- { "command": "CREATE FOREIGN TABLE", "kind": "foreign table", "status": "refused", "reason": "no tag declares a foreign table yet" },
14
+ { "command": "CREATE FOREIGN TABLE", "kind": "foreign table", "status": "declared", "entity": "Postgres::ForeignTable", "note": "its own tag, not table: another CREATE and ALTER, no key, index or storage clauses; its server cannot be altered; one that is a partition, inherits or has a generated column is not declared; an option that holds a credential is refused" },
15
15
  { "command": "CREATE FUNCTION", "kind": "function", "status": "declared", "entity": "Postgres::Function" },
16
16
  { "command": "CREATE FUNCTION", "form": "RETURN or BEGIN ATOMIC body", "match": "\\b(?:RETURN|BEGIN\\s+ATOMIC)\\b", "kind": "routine with a SQL-standard body", "status": "refused", "reason": "the server stores a SQL-standard body parsed and prints it back rewritten, so a declaration cannot be compared with it; declare the routine with a string body (AS $$ ... $$)" },
17
17
  { "command": "CREATE GROUP", "kind": "group", "status": "declared", "entity": "Postgres::Role", "note": "an alias of CREATE ROLE" },
18
18
  { "command": "CREATE INDEX", "kind": "index", "status": "declared", "entity": "Postgres::Index" },
19
19
  { "command": "CREATE LANGUAGE", "kind": "procedural language", "status": "refused", "reason": "no tag declares a procedural language; an extension installs one, and the extension is declared instead" },
20
20
  { "command": "CREATE MATERIALIZED VIEW", "kind": "materialized view", "status": "declared", "entity": "Postgres::MaterializedView" },
21
- { "command": "CREATE OPERATOR", "kind": "operator", "status": "declared", "entity": "Postgres::Operator", "note": "identified by its name and operand types; a change is a drop and a create" },
22
- { "command": "CREATE OPERATOR CLASS", "kind": "operator class", "status": "refused", "reason": "no tag declares an operator class yet" },
23
- { "command": "CREATE OPERATOR FAMILY", "kind": "operator family", "status": "refused", "reason": "no tag declares an operator family yet" },
21
+ { "command": "CREATE OPERATOR", "kind": "operator", "status": "declared", "entity": "Postgres::Operator", "note": "identified by its name and operand types; RESTRICT and JOIN change in place, and from 17 so does setting a commutator or negator or turning on HASHES or MERGES; anything else is a drop and a create" },
22
+ { "command": "CREATE OPERATOR CLASS", "kind": "operator class", "status": "declared", "entity": "Postgres::OperatorClass", "note": "identified by its name and index method; a change is a drop and a create, since ALTER OPERATOR CLASS only renames or moves it; its own family, named as the class, comes with it" },
23
+ { "command": "CREATE OPERATOR FAMILY", "kind": "operator family", "status": "declared", "entity": "Postgres::OperatorFamily", "note": "identified by its name and index method; its members come with the classes that name it, and loose members added with ALTER OPERATOR FAMILY are not declared" },
24
24
  { "command": "CREATE POLICY", "kind": "policy", "status": "declared", "entity": "Postgres::Policy", "note": "read where the profile manages access (sql.profiles.<env>.access)" },
25
25
  { "command": "CREATE PROCEDURE", "kind": "procedure", "status": "declared", "entity": "Postgres::Procedure" },
26
26
  { "command": "CREATE PUBLICATION", "kind": "publication", "status": "declared", "entity": "Postgres::Publication", "note": "the database's, not a schema's; a table of it may carry a column list and row filter from 15" },
@@ -34,8 +34,8 @@
34
34
  { "command": "CREATE TABLE", "kind": "table", "status": "declared", "entity": "Postgres::Table" },
35
35
  { "command": "CREATE TABLE AS", "kind": "table from a query", "status": "declared", "entity": "Postgres::Table", "note": "the server keeps no query, so the table it made reads as a CREATE TABLE" },
36
36
  { "command": "CREATE TABLESPACE", "kind": "tablespace", "status": "out-of-scope", "reason": "a tablespace is the cluster's, a directory on the server; a table's TABLESPACE clause is kept" },
37
- { "command": "CREATE TEXT SEARCH CONFIGURATION", "kind": "text search configuration", "status": "refused", "reason": "no tag declares a text search configuration yet" },
38
- { "command": "CREATE TEXT SEARCH DICTIONARY", "kind": "text search dictionary", "status": "refused", "reason": "no tag declares a text search dictionary yet" },
37
+ { "command": "CREATE TEXT SEARCH CONFIGURATION", "kind": "text search configuration", "status": "declared", "entity": "Postgres::TextSearchConfiguration", "note": "PARSER and its ADD MAPPING statements; COPY is refused, since it copies mappings chant does not read; mappings change in place, the parser by drop and create" },
38
+ { "command": "CREATE TEXT SEARCH DICTIONARY", "kind": "text search dictionary", "status": "declared", "entity": "Postgres::TextSearchDictionary", "note": "a template and its options; options change in place, the template by drop and create" },
39
39
  { "command": "CREATE TEXT SEARCH PARSER", "kind": "text search parser", "status": "refused", "reason": "no tag declares a text search parser; one is C code an extension installs" },
40
40
  { "command": "CREATE TEXT SEARCH TEMPLATE", "kind": "text search template", "status": "refused", "reason": "no tag declares a text search template; one is C code an extension installs" },
41
41
  { "command": "CREATE TRANSFORM", "kind": "transform", "status": "refused", "reason": "no tag declares a transform; an extension installs one" },
package/src/drizzle.ts CHANGED
@@ -80,6 +80,11 @@ const SKIP_REASONS: Record<string, string> = {
80
80
  "Postgres::Collation": "Drizzle has no collation",
81
81
  "Postgres::Cast": "Drizzle has no cast",
82
82
  "Postgres::Operator": "Drizzle has no operator",
83
+ "Postgres::OperatorClass": "Drizzle has no operator class",
84
+ "Postgres::OperatorFamily": "Drizzle has no operator family",
85
+ "Postgres::ForeignTable": "Drizzle has no foreign table",
86
+ "Postgres::TextSearchConfiguration": "Drizzle has no text search configuration",
87
+ "Postgres::TextSearchDictionary": "Drizzle has no text search dictionary",
83
88
  "Postgres::Statistics": "Drizzle has no extended statistics",
84
89
  "Postgres::ForeignDataWrapper": "Drizzle has no foreign data wrapper",
85
90
  "Postgres::ForeignServer": "Drizzle has no foreign server",
@@ -150,6 +150,10 @@ export function readPostgres(ddl: string, options: ReadOptions, problems: string
150
150
  mark(node.table);
151
151
  followOns.push({ tokens, node, inserts });
152
152
  continue;
153
+ case "textSearchMapping":
154
+ mark(node.name);
155
+ followOns.push({ tokens, node, inserts });
156
+ continue;
153
157
  case "index":
154
158
  case "trigger":
155
159
  case "policy":
@@ -189,7 +193,7 @@ export function readPostgres(ddl: string, options: ReadOptions, problems: string
189
193
  const k = followOnOwner(f.node, f.tokens, nameOf, qualify);
190
194
  const owner = k ? owners.get(k) : undefined;
191
195
  if (!owner) {
192
- const what = f.node.statement === "comment" ? `COMMENT ON ${f.node.objectType}` : "ALTER TABLE ... ROW LEVEL SECURITY";
196
+ const what = f.node.statement === "comment" ? `COMMENT ON ${f.node.objectType}` : f.node.statement === "textSearchMapping" ? "ALTER TEXT SEARCH CONFIGURATION ... ADD MAPPING" : "ALTER TABLE ... ROW LEVEL SECURITY";
193
197
  problems.push(`${what} for an object the file does not create: ${firstLine(tokensText(f.tokens))}`);
194
198
  continue;
195
199
  }
@@ -283,11 +287,19 @@ function ownerKey(node: StatementNode, tokens: Token[], nameOf: NameOf, qualify:
283
287
  case "foreignServer":
284
288
  case "publication":
285
289
  return `${node.statement} ${nameOf(tokens, node.name).at(-1) ?? ""}`;
290
+ // A class or family is named by its index method too, and a configuration and a dictionary are different namespaces.
291
+ case "operatorClass":
292
+ case "operatorFamily":
293
+ return `${node.statement} ${qkey(qualify(nameOf(tokens, node.name)))} USING ${node.method}`;
294
+ case "textSearchConfiguration":
295
+ case "textSearchDictionary":
296
+ return `${node.statement} ${qkey(qualify(nameOf(tokens, node.name)))}`;
286
297
  case "cast":
287
298
  return `cast ${castKey(tokens, node.source, node.target)}`;
288
299
  case "grant":
289
300
  case "comment":
290
301
  case "rowSecurity":
302
+ case "textSearchMapping":
291
303
  return undefined;
292
304
  default:
293
305
  return `rel ${qkey(qualify(nameOf(tokens, node.name)))}`;
@@ -297,6 +309,7 @@ function ownerKey(node: StatementNode, tokens: Token[], nameOf: NameOf, qualify:
297
309
  /** The key of the object a COMMENT ON or ALTER TABLE ... ROW LEVEL SECURITY finishes. */
298
310
  function followOnOwner(node: StatementNode, tokens: Token[], nameOf: NameOf, qualify: Qualify): string | undefined {
299
311
  if (node.statement === "rowSecurity") return `rel ${qkey(qualify(nameOf(tokens, node.table)))}`;
312
+ if (node.statement === "textSearchMapping") return `textSearchConfiguration ${qkey(qualify(nameOf(tokens, node.name)))}`;
300
313
  if (node.statement !== "comment") return undefined;
301
314
  const target = nameOf(tokens, node.target);
302
315
  switch (node.objectType) {
@@ -321,6 +334,14 @@ function followOnOwner(node: StatementNode, tokens: Token[], nameOf: NameOf, qua
321
334
  return `foreignServer ${target.at(-1) ?? ""}`;
322
335
  case "PUBLICATION":
323
336
  return `publication ${target.at(-1) ?? ""}`;
337
+ case "OPERATOR CLASS":
338
+ return `operatorClass ${qkey(qualify(target))} USING ${node.using ?? ""}`;
339
+ case "OPERATOR FAMILY":
340
+ return `operatorFamily ${qkey(qualify(target))} USING ${node.using ?? ""}`;
341
+ case "TEXT SEARCH CONFIGURATION":
342
+ return `textSearchConfiguration ${qkey(qualify(target))}`;
343
+ case "TEXT SEARCH DICTIONARY":
344
+ return `textSearchDictionary ${qkey(qualify(target))}`;
324
345
  case "CAST":
325
346
  return node.cast ? `cast ${castKey(tokens, node.cast.source, node.cast.target)}` : undefined;
326
347
  case "CONSTRAINT":
@@ -134,6 +134,31 @@
134
134
  "kind": "resource",
135
135
  "lexicon": "sql"
136
136
  },
137
+ "PostgresOperatorClass": {
138
+ "resourceType": "Postgres::OperatorClass",
139
+ "kind": "resource",
140
+ "lexicon": "sql"
141
+ },
142
+ "PostgresOperatorFamily": {
143
+ "resourceType": "Postgres::OperatorFamily",
144
+ "kind": "resource",
145
+ "lexicon": "sql"
146
+ },
147
+ "PostgresForeignTable": {
148
+ "resourceType": "Postgres::ForeignTable",
149
+ "kind": "resource",
150
+ "lexicon": "sql"
151
+ },
152
+ "PostgresTextSearchConfiguration": {
153
+ "resourceType": "Postgres::TextSearchConfiguration",
154
+ "kind": "resource",
155
+ "lexicon": "sql"
156
+ },
157
+ "PostgresTextSearchDictionary": {
158
+ "resourceType": "Postgres::TextSearchDictionary",
159
+ "kind": "resource",
160
+ "lexicon": "sql"
161
+ },
137
162
  "PostgresStatistics": {
138
163
  "resourceType": "Postgres::Statistics",
139
164
  "kind": "resource",
package/src/index.ts CHANGED
@@ -18,7 +18,7 @@ export { database, table, view, dictionary, user, role, policy, quota, settingsP
18
18
  // A tag folds as the function its file imports, so `table` imported from
19
19
  // `@intentius/chant-lexicon-sql/postgres` is Postgres's. Declarations import
20
20
  // them from the dialect subpath.
21
- export { schema, index, sequence, type, domain, extension, func, procedure, aggregate, trigger, eventTrigger, operator, statistics, foreignDataWrapper, foreignServer, publication, collation, cast } from "./postgres/entities";
21
+ export { schema, index, sequence, type, domain, extension, func, procedure, aggregate, trigger, eventTrigger, operator, operatorClass, operatorFamily, foreignTable, textSearchConfiguration, textSearchDictionary, statistics, foreignDataWrapper, foreignServer, publication, collation, cast } from "./postgres/entities";
22
22
 
23
23
  // A schema change's statements between two builds, rendered offline for a
24
24
  // migration file (#3644); also at `@intentius/chant-lexicon-sql/migration-statements`.