@intentius/chant-lexicon-sql 0.117.0 → 0.118.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/dist/clickhouse/access.d.ts +132 -0
  2. package/dist/clickhouse/access.d.ts.map +1 -0
  3. package/dist/clickhouse/apply/apply.d.ts.map +1 -1
  4. package/dist/clickhouse/apply/statements.d.ts +2 -0
  5. package/dist/clickhouse/apply/statements.d.ts.map +1 -1
  6. package/dist/clickhouse/entities.d.ts +171 -2
  7. package/dist/clickhouse/entities.d.ts.map +1 -1
  8. package/dist/clickhouse/http.d.ts +3 -0
  9. package/dist/clickhouse/http.d.ts.map +1 -1
  10. package/dist/clickhouse/import/generator.d.ts +28 -0
  11. package/dist/clickhouse/import/generator.d.ts.map +1 -1
  12. package/dist/clickhouse/import/ir.d.ts.map +1 -1
  13. package/dist/clickhouse/import/parser.d.ts +3 -3
  14. package/dist/clickhouse/import/parser.d.ts.map +1 -1
  15. package/dist/clickhouse/live/bind.d.ts.map +1 -1
  16. package/dist/clickhouse/live/catalog.d.ts +41 -6
  17. package/dist/clickhouse/live/catalog.d.ts.map +1 -1
  18. package/dist/clickhouse/live/describe-resources.d.ts +3 -2
  19. package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
  20. package/dist/clickhouse/parser.d.ts +54 -3
  21. package/dist/clickhouse/parser.d.ts.map +1 -1
  22. package/dist/clickhouse/plan/commands.d.ts +3 -1
  23. package/dist/clickhouse/plan/commands.d.ts.map +1 -1
  24. package/dist/clickhouse/plan/deep.d.ts.map +1 -1
  25. package/dist/clickhouse/plan/diff.d.ts +12 -0
  26. package/dist/clickhouse/plan/diff.d.ts.map +1 -1
  27. package/dist/clickhouse/plan/normalize.d.ts +61 -1
  28. package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
  29. package/dist/clickhouse/plan/report.d.ts.map +1 -1
  30. package/dist/clickhouse/plan/rules.d.ts +7 -0
  31. package/dist/clickhouse/plan/rules.d.ts.map +1 -1
  32. package/dist/clickhouse/plan/schema.d.ts +8 -4
  33. package/dist/clickhouse/plan/schema.d.ts.map +1 -1
  34. package/dist/clickhouse/rebuild/backfill.d.ts +3 -1
  35. package/dist/clickhouse/rebuild/backfill.d.ts.map +1 -1
  36. package/dist/clickhouse/rebuild/observe.d.ts +7 -3
  37. package/dist/clickhouse/rebuild/observe.d.ts.map +1 -1
  38. package/dist/clickhouse/rebuild/op.d.ts +8 -0
  39. package/dist/clickhouse/rebuild/op.d.ts.map +1 -1
  40. package/dist/clickhouse/rebuild/steps.d.ts +48 -0
  41. package/dist/clickhouse/rebuild/steps.d.ts.map +1 -1
  42. package/dist/clickhouse/testing/fake-server.d.ts.map +1 -1
  43. package/dist/clickhouse/testing/writable-server.d.ts.map +1 -1
  44. package/dist/clickhouse/topology.d.ts.map +1 -1
  45. package/dist/clickhouse.d.ts +1 -1
  46. package/dist/clickhouse.d.ts.map +1 -1
  47. package/dist/codegen/classifier-page.d.ts.map +1 -1
  48. package/dist/codegen/registry.d.ts.map +1 -1
  49. package/dist/config.d.ts +50 -7
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/core/references.d.ts +4 -2
  52. package/dist/core/references.d.ts.map +1 -1
  53. package/dist/core/statements.d.ts +11 -0
  54. package/dist/core/statements.d.ts.map +1 -1
  55. package/dist/files/build-root.d.ts +26 -0
  56. package/dist/files/build-root.d.ts.map +1 -0
  57. package/dist/files/clickhouse.d.ts +19 -0
  58. package/dist/files/clickhouse.d.ts.map +1 -0
  59. package/dist/files/common.d.ts +25 -0
  60. package/dist/files/common.d.ts.map +1 -0
  61. package/dist/files/postgres.d.ts +36 -0
  62. package/dist/files/postgres.d.ts.map +1 -0
  63. package/dist/import-parser.d.ts +25 -0
  64. package/dist/import-parser.d.ts.map +1 -0
  65. package/dist/index.d.ts +3 -2
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/integrity.json +6 -6
  68. package/dist/lint/rules/sqlch001.d.ts +1 -1
  69. package/dist/lint/rules/sqlch001.d.ts.map +1 -1
  70. package/dist/lint/rules/sqlch002.d.ts.map +1 -1
  71. package/dist/lint/rules/templates.d.ts +1 -1
  72. package/dist/lint/rules/templates.d.ts.map +1 -1
  73. package/dist/manifest.json +1 -1
  74. package/dist/meta.json +30 -0
  75. package/dist/okf/index.md +7 -1
  76. package/dist/okf/rules/SQLCH001.md +2 -2
  77. package/dist/okf/types/Dictionary.md +9 -0
  78. package/dist/okf/types/Function.md +9 -0
  79. package/dist/okf/types/Grant.md +9 -0
  80. package/dist/okf/types/Role.md +9 -0
  81. package/dist/okf/types/RowPolicy.md +9 -0
  82. package/dist/okf/types/User.md +9 -0
  83. package/dist/op/activities/clickhouse-rebuild.d.ts.map +1 -1
  84. package/dist/op/activity-contracts.d.ts +9 -0
  85. package/dist/op/activity-contracts.d.ts.map +1 -1
  86. package/dist/plugin.d.ts.map +1 -1
  87. package/dist/postgres/apply/statements.d.ts +68 -0
  88. package/dist/postgres/apply/statements.d.ts.map +1 -1
  89. package/dist/postgres/import/generator.d.ts +33 -0
  90. package/dist/postgres/import/generator.d.ts.map +1 -1
  91. package/dist/postgres/live/bind.d.ts.map +1 -1
  92. package/dist/postgres/live/client.d.ts +3 -0
  93. package/dist/postgres/live/client.d.ts.map +1 -1
  94. package/dist/rules/sqlch001.ts +26 -4
  95. package/dist/rules/sqlch002.ts +3 -2
  96. package/dist/rules/templates.ts +2 -2
  97. package/dist/serializer.d.ts +2 -1
  98. package/dist/serializer.d.ts.map +1 -1
  99. package/dist/sql-files.d.ts +79 -0
  100. package/dist/sql-files.d.ts.map +1 -0
  101. package/dist/token-source.d.ts +88 -0
  102. package/dist/token-source.d.ts.map +1 -0
  103. package/dist/validate.d.ts.map +1 -1
  104. package/package.json +2 -2
  105. package/src/clickhouse/access.test.ts +203 -0
  106. package/src/clickhouse/access.ts +595 -0
  107. package/src/clickhouse/apply/apply.ts +29 -20
  108. package/src/clickhouse/apply/statements.ts +105 -8
  109. package/src/clickhouse/dictionary.test.ts +177 -0
  110. package/src/clickhouse/entities.ts +356 -6
  111. package/src/clickhouse/function.test.ts +91 -0
  112. package/src/clickhouse/http.ts +11 -2
  113. package/src/clickhouse/import/generator.ts +81 -46
  114. package/src/clickhouse/import/import.test.ts +9 -3
  115. package/src/clickhouse/import/ir.ts +6 -2
  116. package/src/clickhouse/import/parser.ts +10 -18
  117. package/src/clickhouse/live/access.e2e.test.ts +218 -0
  118. package/src/clickhouse/live/bind.ts +17 -6
  119. package/src/clickhouse/live/catalog.ts +106 -10
  120. package/src/clickhouse/live/describe-resources.ts +23 -10
  121. package/src/clickhouse/live/dictionaries.e2e.test.ts +186 -0
  122. package/src/clickhouse/live/functions.e2e.test.ts +155 -0
  123. package/src/clickhouse/live/live.test.ts +2 -2
  124. package/src/clickhouse/parser.ts +136 -4
  125. package/src/clickhouse/plan/commands.ts +18 -7
  126. package/src/clickhouse/plan/deep.ts +71 -11
  127. package/src/clickhouse/plan/diff.ts +65 -2
  128. package/src/clickhouse/plan/normalize.ts +200 -6
  129. package/src/clickhouse/plan/report.ts +14 -3
  130. package/src/clickhouse/plan/rules.ts +44 -1
  131. package/src/clickhouse/plan/schema.ts +40 -17
  132. package/src/clickhouse/plan/server-format.ts +1 -1
  133. package/src/clickhouse/rebuild/backfill.ts +5 -25
  134. package/src/clickhouse/rebuild/observe.ts +7 -3
  135. package/src/clickhouse/rebuild/op.ts +9 -0
  136. package/src/clickhouse/rebuild/rebuild.e2e.test.ts +62 -0
  137. package/src/clickhouse/rebuild/rebuild.test.ts +54 -0
  138. package/src/clickhouse/rebuild/steps.ts +136 -1
  139. package/src/clickhouse/testing/fake-server.ts +2 -0
  140. package/src/clickhouse/testing/writable-server.ts +2 -0
  141. package/src/clickhouse/topology.ts +17 -2
  142. package/src/clickhouse.ts +19 -0
  143. package/src/codegen/classifier-page.ts +2 -0
  144. package/src/codegen/registry.ts +6 -0
  145. package/src/config.ts +12 -3
  146. package/src/core/references.ts +5 -3
  147. package/src/core/statements.ts +11 -1
  148. package/src/files/build-root.ts +62 -0
  149. package/src/files/clickhouse.ts +52 -0
  150. package/src/files/common.ts +54 -0
  151. package/src/files/postgres.ts +371 -0
  152. package/src/generated/lexicon-sql.json +30 -0
  153. package/src/import-parser.ts +61 -0
  154. package/src/index.ts +15 -2
  155. package/src/lint/rules/sqlch001.ts +26 -4
  156. package/src/lint/rules/sqlch002.ts +3 -2
  157. package/src/lint/rules/templates.ts +2 -2
  158. package/src/lsp/template.ts +3 -3
  159. package/src/mcp/index.ts +6 -6
  160. package/src/migration-statements.test.ts +25 -0
  161. package/src/migration-statements.ts +1 -0
  162. package/src/op/activities/clickhouse-rebuild.ts +1 -0
  163. package/src/op/activity-contracts.ts +1 -0
  164. package/src/plugin.ts +29 -8
  165. package/src/postgres/apply/constraint-sequences.e2e.test.ts +179 -0
  166. package/src/postgres/apply/safe-sequences.e2e.test.ts +176 -0
  167. package/src/postgres/apply/statements.test.ts +138 -6
  168. package/src/postgres/apply/statements.ts +156 -10
  169. package/src/postgres/import/generator.ts +118 -45
  170. package/src/postgres/live/bind.ts +17 -6
  171. package/src/postgres/live/client.ts +10 -2
  172. package/src/postgres/plan/plan.test.ts +2 -2
  173. package/src/postgres/plan/rules.ts +5 -5
  174. package/src/serializer.ts +14 -9
  175. package/src/skills/chant-sql-postgres-plan.md +3 -3
  176. package/src/skills/chant-sql-rebuild.md +3 -2
  177. package/src/skills/chant-sql.md +7 -1
  178. package/src/sql-files.test.ts +188 -0
  179. package/src/sql-files.ts +218 -0
  180. package/src/token-source.e2e.test.ts +142 -0
  181. package/src/token-source.test.ts +119 -0
  182. package/src/token-source.ts +189 -0
  183. package/src/validate.ts +6 -0
@@ -0,0 +1,371 @@
1
+ /**
2
+ * A file of Postgres DDL as the objects it declares: each `CREATE` (and each
3
+ * `GRANT`, `REVOKE` and `ALTER DEFAULT PRIVILEGES`) is one object, and the
4
+ * statements that only finish one are folded into it:
5
+ *
6
+ * - `COMMENT ON` joins the object it comments on, wherever in the file it is;
7
+ * - `ALTER TABLE ... ENABLE | DISABLE | FORCE ROW LEVEL SECURITY` joins its table;
8
+ * - `ALTER TABLE <t> ADD [CONSTRAINT <c>] FOREIGN KEY | UNIQUE | PRIMARY KEY |
9
+ * CHECK | EXCLUDE ...`, the form `pg_dump` and ORMs print constraints in,
10
+ * becomes a table constraint in `<t>`'s `CREATE TABLE`.
11
+ *
12
+ * `BEGIN` and `COMMIT` are skipped. Any other statement, and any statement
13
+ * the parser or its tag refuses, is a problem naming it: a statement left
14
+ * out would be a schema change nobody declared.
15
+ *
16
+ * With `schema`, the names the DDL leaves unqualified are qualified with it,
17
+ * as an ORM's connection would: an object's own name, an index's, trigger's
18
+ * or policy's table, a foreign key's table, a comment's target, and a column
19
+ * type naming an enum or domain the file creates.
20
+ */
21
+
22
+ import type { Declarable } from "@intentius/chant/declarable";
23
+ import { isTrivia, tokenizeText, type Token } from "../postgres/tokens";
24
+ import { identValue, parseStatements, type NameNode, type StatementNode } from "../postgres/parser";
25
+ import * as tags from "../postgres/entities";
26
+ import { POSTGRES_ENTITY_TYPES, type PostgresEntityType } from "../postgres/entity-types";
27
+ import { POSTGRES_TAG_OF } from "../postgres/import/generator";
28
+ import type { ImportedPgObject } from "../postgres/import/ir";
29
+ import { firstLine, quoteBare, splitTokens, tokensText, type ReadOptions } from "./common";
30
+
31
+ type Tag = (strings: TemplateStringsArray, ...values: unknown[]) => Declarable;
32
+
33
+ /** The tag that declares an entity type. */
34
+ export function postgresTag(type: string): Tag | undefined {
35
+ const name = POSTGRES_TAG_OF[type];
36
+ return name ? ((tags as unknown as Record<string, Tag>)[name] ?? undefined) : undefined;
37
+ }
38
+
39
+ /** A tagged-template call's strings from template-literal text: `raw` as written, as the tag reads it. */
40
+ export function templateStrings(parts: readonly string[]): TemplateStringsArray {
41
+ return Object.assign([...parts], { raw: [...parts] }) as unknown as TemplateStringsArray;
42
+ }
43
+
44
+ const isName = (t: Token | undefined): boolean => t !== undefined && (t.kind === "ident" || t.kind === "qident");
45
+ const word = (t: Token | undefined, ...words: string[]): boolean => t !== undefined && t.kind === "ident" && words.includes(t.text.toUpperCase());
46
+
47
+ /** Text that sits inside a template literal: a backquote and `${` escaped. */
48
+ const templateSafe = (s: string) => s.replace(/\\(?=`|\$\{)/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
49
+
50
+ interface PgFileObject {
51
+ node: StatementNode;
52
+ /** The CREATE's tokens, then each statement folded into it. */
53
+ statements: Token[][];
54
+ /** Table constraints folded in from ALTER TABLE ... ADD, as SQL text. */
55
+ constraints: string[];
56
+ }
57
+
58
+ /** Transaction control, which declares nothing. */
59
+ function isTransactionControl(sig: readonly Token[]): boolean {
60
+ const words = sig.map((t) => (t.kind === "ident" ? t.text.toUpperCase() : ""));
61
+ const [first, ...rest] = words;
62
+ if (first === "BEGIN" || first === "COMMIT" || first === "END") return rest.every((w) => w === "TRANSACTION" || w === "WORK");
63
+ return first === "START" && rest.length === 1 && rest[0] === "TRANSACTION";
64
+ }
65
+
66
+ /**
67
+ * The objects a file of Postgres DDL declares, each with its statements
68
+ * joined as one declaration's DDL. Problems are collected, not thrown, so a
69
+ * file is reported whole.
70
+ */
71
+ export function readPostgres(ddl: string, options: ReadOptions, problems: string[]): ImportedPgObject[] {
72
+ const parsed: Array<{ tokens: Token[]; node: StatementNode }> = [];
73
+ const alters: Array<{ table: string[]; constraint: string; sql: string }> = [];
74
+ for (const stmt of splitTokens(tokenizeText(ddl, 0), isTrivia)) {
75
+ const sig = stmt.filter((t) => !isTrivia(t));
76
+ const sql = tokensText(stmt);
77
+ if (isTransactionControl(sig)) continue;
78
+ if (word(sig[0], "ALTER") && word(sig[1], "TABLE")) {
79
+ const alter = readAlterAdd(stmt, sig);
80
+ if (alter) {
81
+ alters.push({ ...alter, sql });
82
+ continue;
83
+ }
84
+ }
85
+ if (!word(sig[0], "CREATE", "COMMENT", "GRANT", "REVOKE", "ALTER")) {
86
+ problems.push(`not a statement that declares an object: ${firstLine(sql)}`);
87
+ continue;
88
+ }
89
+ try {
90
+ const tokens = tokenizeText(sql, 0);
91
+ for (const node of parseStatements(tokens)) parsed.push({ tokens, node });
92
+ } catch (e) {
93
+ const what = word(sig[0], "ALTER") && word(sig[1], "TABLE") ? "an ALTER TABLE other than ADD of a table constraint or ROW LEVEL SECURITY" : `does not parse (${e instanceof Error ? e.message : String(e)})`;
94
+ problems.push(`${what}: ${firstLine(sql)}`);
95
+ }
96
+ }
97
+
98
+ const nameOf = (tokens: Token[], n: NameNode): string[] =>
99
+ n.pieces.length > 0 && n.pieces.every((p) => p !== "") ? n.pieces : tokens.slice(n.span.from, n.span.to).filter((t) => isName(t)).map((t) => identValue(t));
100
+ const qualify = (pieces: readonly string[]): { schema?: string; name: string } => {
101
+ const name = pieces[pieces.length - 1] ?? "";
102
+ const schema = pieces.length >= 2 ? pieces[pieces.length - 2] : options.schema;
103
+ return schema ? { schema, name } : { name };
104
+ };
105
+ const key = (q: { schema?: string; name: string }) => `${q.schema ?? ""}.${q.name}`;
106
+
107
+ // The types the file creates, so a column of one is qualified with the rest.
108
+ const fileTypes = new Set<string>();
109
+ for (const { tokens, node } of parsed) {
110
+ if (node.statement === "enum" || node.statement === "domain") {
111
+ const pieces = nameOf(tokens, node.name);
112
+ if (pieces.length === 1) fileTypes.add(pieces[0]!);
113
+ }
114
+ }
115
+
116
+ const objects: PgFileObject[] = [];
117
+ /** Objects by namespace and name: `rel` (anything in a schema), `index`, `schema`, `extension`, `role`, and `trigger`/`policy` with their table. */
118
+ const owners = new Map<string, PgFileObject>();
119
+ const followOns: Array<{ tokens: Token[]; node: StatementNode; inserts: Set<number> }> = [];
120
+ for (const { tokens, node } of parsed) {
121
+ const inserts = new Set<number>();
122
+ /** Qualifies an unqualified name in the statement, when `schema` is set. */
123
+ const mark = (n: NameNode | undefined) => {
124
+ if (!n || !options.schema) return;
125
+ const names = tokens.slice(n.span.from, n.span.to).filter((t) => !isTrivia(t));
126
+ const [only] = names;
127
+ if (only && names.length === 1 && isName(only)) inserts.add(tokens.indexOf(only));
128
+ };
129
+ switch (node.statement) {
130
+ case "comment":
131
+ if (node.objectType !== "SCHEMA" && node.objectType !== "EXTENSION" && node.objectType !== "ROLE" && node.objectType !== "TRIGGER" && node.objectType !== "POLICY" && node.objectType !== "CONSTRAINT" && node.objectType !== "COLUMN") mark(node.target);
132
+ if (node.objectType === "COLUMN" && nameOf(tokens, node.target).length === 2 && options.schema) {
133
+ const first = tokens.slice(node.target.span.from, node.target.span.to).find((t) => !isTrivia(t));
134
+ if (first) inserts.add(tokens.indexOf(first));
135
+ }
136
+ mark(node.on);
137
+ followOns.push({ tokens, node, inserts });
138
+ continue;
139
+ case "rowSecurity":
140
+ mark(node.table);
141
+ followOns.push({ tokens, node, inserts });
142
+ continue;
143
+ case "index":
144
+ case "trigger":
145
+ case "policy":
146
+ mark(node.table);
147
+ break;
148
+ case "table":
149
+ for (const c of [...node.columns.flatMap((col) => col.constraints), ...node.constraints]) mark(c.references?.table);
150
+ if (options.schema) {
151
+ for (const col of node.columns) {
152
+ if (!col.type) continue;
153
+ const type = tokens.slice(col.type.from, col.type.to).filter((t) => !isTrivia(t));
154
+ const first = type[0];
155
+ if (first && isName(first) && fileTypes.has(identValue(first)) && !(type[1]?.kind === "punct" && type[1].text === ".")) inserts.add(tokens.indexOf(first));
156
+ }
157
+ }
158
+ break;
159
+ default:
160
+ break;
161
+ }
162
+ if (node.statement === "schema" && !node.name) {
163
+ problems.push(`a CREATE SCHEMA with no name: ${firstLine(tokensText(tokens))}`);
164
+ continue;
165
+ }
166
+ if (node.statement === "index" && !node.name) {
167
+ problems.push(`an index with no name (its declaration needs one): ${firstLine(tokensText(tokens))}`);
168
+ continue;
169
+ }
170
+ const ownName = !["schema", "extension", "index", "trigger", "policy", "role", "grant"].includes(node.statement);
171
+ if (ownName && "name" in node) mark(node.name);
172
+ const object: PgFileObject = { node, statements: [rewrite(tokens, inserts, options.schema)], constraints: [] };
173
+ objects.push(object);
174
+ const k = ownerKey(node, tokens, nameOf, qualify);
175
+ if (k) owners.set(k, object);
176
+ }
177
+
178
+ for (const f of followOns) {
179
+ const k = followOnOwner(f.node, f.tokens, nameOf, qualify);
180
+ const owner = k ? owners.get(k) : undefined;
181
+ if (!owner) {
182
+ const what = f.node.statement === "comment" ? `COMMENT ON ${f.node.objectType}` : "ALTER TABLE ... ROW LEVEL SECURITY";
183
+ problems.push(`${what} for an object the file does not create: ${firstLine(tokensText(f.tokens))}`);
184
+ continue;
185
+ }
186
+ owner.statements.push(rewrite(f.tokens, f.inserts, options.schema));
187
+ }
188
+
189
+ for (const alter of alters) {
190
+ const table = owners.get(`rel ${key(qualify(alter.table))}`);
191
+ if (!table || table.node.statement !== "table") {
192
+ problems.push(`ALTER TABLE of a table the file does not create: ${firstLine(alter.sql)}`);
193
+ continue;
194
+ }
195
+ table.constraints.push(qualifyConstraint(alter.constraint, options.schema));
196
+ }
197
+
198
+ const out: ImportedPgObject[] = [];
199
+ for (const o of objects) {
200
+ const [create = [], ...rest] = o.statements;
201
+ const ddlText = [o.constraints.length > 0 ? addConstraints(create, o.constraints) : tokensText(create), ...rest.map((c) => tokensText(c))].join(";\n");
202
+ const identity = identify(ddlText);
203
+ if (typeof identity === "string") {
204
+ problems.push(`${identity}: ${firstLine(ddlText)}`);
205
+ continue;
206
+ }
207
+ out.push({ ...identity, ddl: ddlText });
208
+ }
209
+ return out;
210
+ }
211
+
212
+ /** An object's entity type, schema and name, read by declaring it with its tag; a string is the tag's refusal. */
213
+ function identify(ddl: string): { type: PostgresEntityType; schema?: string; name: string } | string {
214
+ let tokens: Token[];
215
+ let node: StatementNode | undefined;
216
+ try {
217
+ tokens = tokenizeText(ddl, 0);
218
+ [node] = parseStatements(tokens);
219
+ } catch (e) {
220
+ return `does not parse (${e instanceof Error ? e.message : String(e)})`;
221
+ }
222
+ const type = node ? entityTypeOf(node) : undefined;
223
+ const tag = type ? postgresTag(type) : undefined;
224
+ if (!type || !tag) return "not a statement that declares an object";
225
+ try {
226
+ const entity = tag(templateStrings([templateSafe(ddl)]));
227
+ const p = (entity as unknown as { props: { schema?: string; name: string } }).props;
228
+ return { type: entity.entityType as PostgresEntityType, ...(p.schema ? { schema: p.schema } : {}), name: p.name };
229
+ } catch (e) {
230
+ return e instanceof Error ? e.message : String(e);
231
+ }
232
+ }
233
+
234
+ /** The entity type a statement declares. */
235
+ function entityTypeOf(node: StatementNode): PostgresEntityType | undefined {
236
+ switch (node.statement) {
237
+ case "view":
238
+ return node.materialized ? POSTGRES_ENTITY_TYPES.materializedView : POSTGRES_ENTITY_TYPES.view;
239
+ case "grant":
240
+ return node.defaults ? POSTGRES_ENTITY_TYPES.defaultPrivileges : POSTGRES_ENTITY_TYPES.grant;
241
+ case "comment":
242
+ case "rowSecurity":
243
+ return undefined;
244
+ default:
245
+ return (POSTGRES_ENTITY_TYPES as Record<string, PostgresEntityType>)[node.statement];
246
+ }
247
+ }
248
+
249
+ type NameOf = (tokens: Token[], n: NameNode) => string[];
250
+ type Qualify = (pieces: readonly string[]) => { schema?: string; name: string };
251
+ const qkey = (q: { schema?: string; name: string }) => `${q.schema ?? ""}.${q.name}`;
252
+
253
+ /** The key a created object is found under by the statements folded into it. */
254
+ function ownerKey(node: StatementNode, tokens: Token[], nameOf: NameOf, qualify: Qualify): string | undefined {
255
+ switch (node.statement) {
256
+ case "schema":
257
+ return node.name ? `schema ${nameOf(tokens, node.name).at(-1) ?? ""}` : undefined;
258
+ case "extension":
259
+ return `extension ${nameOf(tokens, node.name).at(-1) ?? ""}`;
260
+ case "role":
261
+ return `role ${nameOf(tokens, node.name).at(-1) ?? ""}`;
262
+ case "index": {
263
+ // An index is in its table's schema.
264
+ const table = nameOf(tokens, node.table);
265
+ return node.name ? `index ${qkey(qualify([...table.slice(0, -1), nameOf(tokens, node.name).at(-1) ?? ""]))}` : undefined;
266
+ }
267
+ case "trigger":
268
+ case "policy":
269
+ return `${node.statement} ${nameOf(tokens, node.name).at(-1) ?? ""} ON ${qkey(qualify(nameOf(tokens, node.table)))}`;
270
+ case "grant":
271
+ case "comment":
272
+ case "rowSecurity":
273
+ return undefined;
274
+ default:
275
+ return `rel ${qkey(qualify(nameOf(tokens, node.name)))}`;
276
+ }
277
+ }
278
+
279
+ /** The key of the object a COMMENT ON or ALTER TABLE ... ROW LEVEL SECURITY finishes. */
280
+ function followOnOwner(node: StatementNode, tokens: Token[], nameOf: NameOf, qualify: Qualify): string | undefined {
281
+ if (node.statement === "rowSecurity") return `rel ${qkey(qualify(nameOf(tokens, node.table)))}`;
282
+ if (node.statement !== "comment") return undefined;
283
+ const target = nameOf(tokens, node.target);
284
+ switch (node.objectType) {
285
+ case "SCHEMA":
286
+ return `schema ${target.at(-1) ?? ""}`;
287
+ case "EXTENSION":
288
+ return `extension ${target.at(-1) ?? ""}`;
289
+ case "ROLE":
290
+ return `role ${target.at(-1) ?? ""}`;
291
+ case "INDEX":
292
+ return `index ${qkey(qualify(target))}`;
293
+ case "COLUMN":
294
+ return `rel ${qkey(qualify(target.slice(0, -1)))}`;
295
+ case "TRIGGER":
296
+ case "POLICY":
297
+ return node.on ? `${node.objectType.toLowerCase()} ${target.at(-1) ?? ""} ON ${qkey(qualify(nameOf(tokens, node.on)))}` : undefined;
298
+ case "CONSTRAINT":
299
+ case "DOMAIN CONSTRAINT":
300
+ return node.on ? `rel ${qkey(qualify(nameOf(tokens, node.on)))}` : undefined;
301
+ default:
302
+ return `rel ${qkey(qualify(target))}`;
303
+ }
304
+ }
305
+
306
+ /** The statement's tokens with `schema.` inserted before each marked one. */
307
+ function rewrite(tokens: readonly Token[], inserts: ReadonlySet<number>, schema: string | undefined): Token[] {
308
+ if (!schema || inserts.size === 0) return [...tokens];
309
+ const out: Token[] = [];
310
+ tokens.forEach((t, i) => {
311
+ if (inserts.has(i)) out.push({ kind: "ident", text: `${quoteBare(schema)}.`, part: 0, start: 0, end: 0 });
312
+ out.push(t);
313
+ });
314
+ return out;
315
+ }
316
+
317
+ /** `ALTER TABLE [IF EXISTS] [ONLY] <name> ADD <table constraint>`: the table's name pieces and the constraint's text. */
318
+ function readAlterAdd(stmt: readonly Token[], sig: readonly Token[]): { table: string[]; constraint: string } | undefined {
319
+ let i = 2;
320
+ if (word(sig[i], "IF") && word(sig[i + 1], "EXISTS")) i += 2;
321
+ if (word(sig[i], "ONLY")) i++;
322
+ const table: string[] = [];
323
+ for (let t = sig[i]; t && isName(t); t = sig[i]) {
324
+ table.push(identValue(t));
325
+ i++;
326
+ const dot = sig[i];
327
+ if (dot?.kind === "punct" && dot.text === ".") i++;
328
+ else break;
329
+ }
330
+ if (table.length === 0 || !word(sig[i], "ADD")) return undefined;
331
+ i++;
332
+ if (!word(sig[i], "CONSTRAINT", "FOREIGN", "UNIQUE", "PRIMARY", "CHECK", "EXCLUDE")) return undefined;
333
+ // One constraint: a comma at depth 0 starts another ALTER action, which is not read.
334
+ let depth = 0;
335
+ for (const t of sig.slice(i)) {
336
+ if (t.kind === "punct" && t.text === "(") depth++;
337
+ else if (t.kind === "punct" && t.text === ")") depth--;
338
+ else if (t.kind === "punct" && t.text === "," && depth === 0) return undefined;
339
+ }
340
+ const start = sig[i];
341
+ return start ? { table, constraint: tokensText(stmt.slice(stmt.indexOf(start))) } : undefined;
342
+ }
343
+
344
+ /** Qualifies the table a folded FOREIGN KEY references, when it is unqualified. */
345
+ function qualifyConstraint(constraint: string, schema: string | undefined): string {
346
+ if (!schema) return constraint;
347
+ const tokens = tokenizeText(constraint, 0);
348
+ const sig = tokens.map((t, i) => ({ t, i })).filter((x) => !isTrivia(x.t));
349
+ const inserts = new Set<number>();
350
+ sig.forEach((x, k) => {
351
+ const target = sig[k + 1];
352
+ const dot = sig[k + 2]?.t;
353
+ if (word(x.t, "REFERENCES") && target && isName(target.t) && !(dot?.kind === "punct" && dot.text === ".")) inserts.add(target.i);
354
+ });
355
+ return tokensText(rewrite(tokens, inserts, schema));
356
+ }
357
+
358
+ /** A CREATE TABLE with table constraints added at the end of its column list (the `)` matching the first `(`). */
359
+ function addConstraints(create: readonly Token[], constraints: readonly string[]): string {
360
+ let depth = 0;
361
+ const close = create.findIndex((t) => {
362
+ if (t.kind !== "punct") return false;
363
+ if (t.text === "(") depth++;
364
+ else if (t.text === ")") return --depth === 0;
365
+ return false;
366
+ });
367
+ if (close < 0) return tokensText(create);
368
+ const before = tokensText(create.slice(0, close)).replace(/\s+$/, "");
369
+ const indent = /\n([ \t]+)\S[^\n]*$/.exec(before)?.[1] ?? " ";
370
+ return `${before}${constraints.map((c) => `,\n${indent}${c}`).join("")}\n${tokensText(create.slice(close))}`;
371
+ }
@@ -19,6 +19,36 @@
19
19
  "kind": "resource",
20
20
  "lexicon": "sql"
21
21
  },
22
+ "Dictionary": {
23
+ "resourceType": "ClickHouse::Dictionary",
24
+ "kind": "resource",
25
+ "lexicon": "sql"
26
+ },
27
+ "Function": {
28
+ "resourceType": "ClickHouse::Function",
29
+ "kind": "resource",
30
+ "lexicon": "sql"
31
+ },
32
+ "User": {
33
+ "resourceType": "ClickHouse::User",
34
+ "kind": "resource",
35
+ "lexicon": "sql"
36
+ },
37
+ "Role": {
38
+ "resourceType": "ClickHouse::Role",
39
+ "kind": "resource",
40
+ "lexicon": "sql"
41
+ },
42
+ "RowPolicy": {
43
+ "resourceType": "ClickHouse::RowPolicy",
44
+ "kind": "resource",
45
+ "lexicon": "sql"
46
+ },
47
+ "Grant": {
48
+ "resourceType": "ClickHouse::Grant",
49
+ "kind": "resource",
50
+ "lexicon": "sql"
51
+ },
22
52
  "PostgresSchema": {
23
53
  "resourceType": "Postgres::Schema",
24
54
  "kind": "resource",
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `chant import schema.sql`: a file of DDL into the import IR, in either
3
+ * dialect. `--parser-option dialect=postgres` (or `clickhouse`) names it;
4
+ * without one it is read off the statements: an `ENGINE =` clause, a
5
+ * `CREATE DATABASE`, a `CREATE DICTIONARY` or a lambda `CREATE FUNCTION f AS
6
+ * (x) -> ...` is ClickHouse, a statement
7
+ * only Postgres has (`CREATE SCHEMA`, `CREATE INDEX`, `COMMENT ON`, `ALTER
8
+ * TABLE`, `GRANT`, ...) or a `CREATE TABLE` with no engine is Postgres, and
9
+ * anything else is ClickHouse.
10
+ *
11
+ * A statement that declares nothing, or does not parse, is an error naming
12
+ * it (`./sql-files.ts`); none is left out.
13
+ */
14
+
15
+ import type { ParserOptions, TemplateIR, TemplateParser } from "@intentius/chant/import/parser";
16
+ import { SQL_DIALECTS, type SqlDialect } from "./dialects";
17
+ import { readClickHouse } from "./files/clickhouse";
18
+ import { readPostgres } from "./files/postgres";
19
+ import { SqlFileError } from "./files/common";
20
+ import { objectsToIR as chObjectsToIR } from "./clickhouse/import/ir";
21
+ import { objectsToIR as pgObjectsToIR } from "./postgres/import/ir";
22
+
23
+ const CLICKHOUSE_ONLY = [
24
+ /\bENGINE\s*=/i,
25
+ /^\s*CREATE\s+(?:DATABASE|(?:OR\s+REPLACE\s+)?DICTIONARY)\b/im,
26
+ // A SQL function: CREATE FUNCTION f AS (x) -> ...
27
+ /^\s*CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+[^\s(]+(?:\s+ON\s+CLUSTER\s+\S+)?\s+AS\s*\(/im,
28
+ ];
29
+ const POSTGRES_ONLY = [
30
+ /^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:SCHEMA|EXTENSION|TYPE|DOMAIN|SEQUENCE|(?:UNIQUE\s+)?INDEX|(?:CONSTRAINT\s+)?TRIGGER|POLICY|ROLE|FUNCTION|PROCEDURE|(?:UNLOGGED|TEMP|TEMPORARY)\s+TABLE)\b/im,
31
+ /^\s*(?:COMMENT\s+ON|ALTER\s+TABLE|ALTER\s+DEFAULT\s+PRIVILEGES|GRANT|REVOKE|BEGIN)\b/im,
32
+ /^\s*CREATE\s+TABLE\b/im,
33
+ ];
34
+
35
+ /** The dialect a file of DDL is written in, read off its statements. */
36
+ export function guessDialect(content: string): SqlDialect {
37
+ const text = content.replace(/--[^\n]*/g, "");
38
+ if (CLICKHOUSE_ONLY.some((re) => re.test(text))) return "clickhouse";
39
+ if (POSTGRES_ONLY.some((re) => re.test(text))) return "postgres";
40
+ return "clickhouse";
41
+ }
42
+
43
+ /** The `dialect` parser option, checked. */
44
+ export function dialectOption(options: ParserOptions | undefined): SqlDialect | undefined {
45
+ const d = options?.dialect;
46
+ if (d === undefined) return undefined;
47
+ if (typeof d !== "string" || !(SQL_DIALECTS as readonly string[]).includes(d)) throw new Error(`--parser-option dialect: expected ${SQL_DIALECTS.join(" or ")}, got ${String(d)}`);
48
+ return d as SqlDialect;
49
+ }
50
+
51
+ export class SqlFileParser implements TemplateParser {
52
+ constructor(private readonly dialect?: SqlDialect) {}
53
+
54
+ parse(content: string): TemplateIR {
55
+ const dialect = this.dialect ?? guessDialect(content);
56
+ const problems: string[] = [];
57
+ const ir = dialect === "postgres" ? pgObjectsToIR(readPostgres(content, { origin: "the DDL" }, problems)) : chObjectsToIR(readClickHouse(content, { origin: "the DDL" }, problems));
58
+ if (problems.length > 0) throw new SqlFileError(`the ${dialect === "postgres" ? "Postgres" : "ClickHouse"} DDL`, problems);
59
+ return ir;
60
+ }
61
+ }
package/src/index.ts CHANGED
@@ -12,13 +12,13 @@ export { sqlConfigSchema, type SqlConfig } from "./config";
12
12
  // them as intrinsics (check-lexicon resolves a registered intrinsic against the
13
13
  // root's exports). Declarations import them from the dialect subpath,
14
14
  // `@intentius/chant-lexicon-sql/clickhouse`.
15
- export { database, table, view, literal } from "./clickhouse/entities";
15
+ export { database, table, view, dictionary, user, role, policy, grant, literal } from "./clickhouse/entities";
16
16
 
17
17
  // The Postgres tags whose names ClickHouse does not export, for the same reason.
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, trigger, policy, role, grant } from "./postgres/entities";
21
+ export { schema, index, sequence, type, domain, extension, func, procedure, trigger } 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`.
@@ -34,3 +34,16 @@ export {
34
34
  type StatementStep,
35
35
  type StepObject,
36
36
  } from "./migration-statements";
37
+
38
+ // Plain `.sql` files as a schema source (#3646): DDL read into the declarations
39
+ // the tagged templates make; also at `@intentius/chant-lexicon-sql/sql-files`.
40
+ export {
41
+ readSqlFile,
42
+ sqlFileDeclarations,
43
+ sqlFileEntities,
44
+ sqlFileOf,
45
+ SqlFileError,
46
+ type SqlFileObject,
47
+ type SqlFileOptions,
48
+ type SqlFileSource,
49
+ } from "./sql-files";
@@ -1,12 +1,18 @@
1
1
  import type { LintContext, LintDiagnostic, LintRule } from "@intentius/chant/lint/rule";
2
2
  import { parseCreate } from "../../clickhouse/parser";
3
+ import { parseAccess } from "../../clickhouse/access";
3
4
  import { SqlSyntaxError } from "../../clickhouse/tokens";
4
5
  import { findTemplates, templatePosition, tokensOf } from "./templates";
5
6
 
6
- const STATEMENT: Record<string, string> = { database: "CREATE DATABASE", table: "CREATE TABLE", view: "CREATE VIEW" };
7
+ const ACCESS_TAGS = new Set(["user", "role", "policy", "grant"]);
8
+ const ACCESS_STATEMENT: Record<string, string> = { user: "CREATE USER", role: "CREATE ROLE", rowPolicy: "CREATE ROW POLICY", grant: "GRANT" };
9
+
10
+ const STATEMENT: Record<string, string> = { database: "CREATE DATABASE", table: "CREATE TABLE", view: "CREATE VIEW", dictionary: "CREATE DICTIONARY", function: "CREATE FUNCTION" };
11
+ /** The tag a statement belongs in: `func` for a function, the statement's own name otherwise. */
12
+ const tagOf = (statement: string): string => (statement === "function" ? "func" : statement);
7
13
 
8
14
  /**
9
- * SQLCH001: the DDL in a `database`, `table` or `view` template does not parse, or holds another statement than its tag.
15
+ * SQLCH001: the DDL in a ClickHouse template (`database`, `table`, `view`, `dictionary`, `func`, `user`, `role`, `policy`, `grant`) does not parse, or holds another statement than its tag.
10
16
  *
11
17
  * The same parse runs when the build calls the tag, and fails the build there.
12
18
  * This rule reports it in the editor and in `chant lint`, at the token, before
@@ -27,12 +33,28 @@ export const sqlch001: LintRule = {
27
33
  let tokens;
28
34
  try {
29
35
  tokens = tokensOf(found);
36
+ if (ACCESS_TAGS.has(found.tag)) {
37
+ const node = parseAccess(tokens);
38
+ const tag = node.statement === "rowPolicy" ? "policy" : node.statement;
39
+ if (tag !== found.tag || node.revoke) {
40
+ out.push({
41
+ ruleId: "SQLCH001",
42
+ severity: "error",
43
+ message: node.revoke
44
+ ? `${found.tag}\`...\` holds a REVOKE; declare the grants a grantee keeps, and a plan revokes the rest`
45
+ : `${found.tag}\`...\` holds a ${ACCESS_STATEMENT[node.statement]}; use the ${tag} tag`,
46
+ file: context.filePath,
47
+ ...templatePosition(source, found, 0, 0),
48
+ });
49
+ }
50
+ continue;
51
+ }
30
52
  const node = parseCreate(tokens);
31
- if (node.statement !== found.tag) {
53
+ if (tagOf(node.statement) !== found.tag) {
32
54
  out.push({
33
55
  ruleId: "SQLCH001",
34
56
  severity: "error",
35
- message: `${found.tag}\`...\` holds a ${STATEMENT[node.statement]}; use the ${node.statement} tag`,
57
+ message: `${found.tag}\`...\` holds a ${STATEMENT[node.statement]}; use the ${tagOf(node.statement)} tag`,
36
58
  file: context.filePath,
37
59
  ...templatePosition(source, found, 0, 0),
38
60
  });
@@ -41,7 +41,8 @@ function keyColumns(tokens: Token[], span: Span): Array<{ name: string; token: T
41
41
  }
42
42
 
43
43
  function allowsNullableKey(tokens: Token[], node: CreateNode): boolean {
44
- if (node.statement === "database" || !node.settings) return false;
44
+ if (node.statement !== "table" && node.statement !== "view") return false;
45
+ if (!node.settings) return false;
45
46
  const s = node.settings.find((x) => x.key === "allow_nullable_key");
46
47
  if (!s) return false;
47
48
  const v = tokens.slice(s.value.from, s.value.to).map((t) => t.text).join("").trim().toLowerCase().replace(/'/g, "");
@@ -74,7 +75,7 @@ export const sqlch002: LintRule = {
74
75
  if (err instanceof SqlSyntaxError) continue; // SQLCH001 reports it
75
76
  throw err;
76
77
  }
77
- if (node.statement === "database" || allowsNullableKey(tokens, node)) continue;
78
+ if ((node.statement !== "table" && node.statement !== "view") || allowsNullableKey(tokens, node)) continue;
78
79
  const nullable = new Set(node.columns.filter((c) => c.name && isNullable(tokens, c)).map((c) => c.name));
79
80
  if (nullable.size === 0) continue;
80
81
  const reported = new Set<string>();
@@ -13,7 +13,7 @@ import { findSqlTemplates, type FoundTemplate as SqlFoundTemplate, type Template
13
13
 
14
14
  export { templatePosition, tokenPosition } from "../../core/find-templates";
15
15
 
16
- export type SqlTag = "database" | "table" | "view";
16
+ export type SqlTag = "database" | "table" | "view" | "dictionary" | "func" | "user" | "role" | "policy" | "grant";
17
17
 
18
18
  /**
19
19
  * The ClickHouse tags, by the modules they are imported from. The package
@@ -22,7 +22,7 @@ export type SqlTag = "database" | "table" | "view";
22
22
  export const CLICKHOUSE_TEMPLATE_SOURCE: TemplateSource<SqlTag> = {
23
23
  dialect: "clickhouse",
24
24
  modules: ["@intentius/chant-lexicon-sql", "@intentius/chant-lexicon-sql/clickhouse"],
25
- tags: ["database", "table", "view"],
25
+ tags: ["database", "table", "view", "dictionary", "func", "user", "role", "policy", "grant"],
26
26
  };
27
27
 
28
28
  export type FoundTemplate = SqlFoundTemplate<SqlTag>;
@@ -143,13 +143,13 @@ export function declarations(content: string, fileName = "file.ts"): Declared[]
143
143
  const tokens = tokenize(found.parts);
144
144
  const node = parseCreate(tokens);
145
145
  const columns =
146
- node.statement === "database"
146
+ node.statement === "database" || node.statement === "function"
147
147
  ? []
148
- : node.columns.map((c) => ({
148
+ : (node.statement === "dictionary" ? node.attributes : node.columns).map((c) => ({
149
149
  name: c.name || spanText(found, tokens, c.nameSpan, source) || "",
150
150
  ...(spanText(found, tokens, c.type, source) ? { type: spanText(found, tokens, c.type, source)! } : {}),
151
151
  }));
152
- const engine = node.engine?.name || spanText(found, tokens, node.engine?.nameSpan, source);
152
+ const engine = node.statement === "dictionary" || node.statement === "function" ? undefined : node.engine?.name || spanText(found, tokens, node.engine?.nameSpan, source);
153
153
  out.push({
154
154
  name: decl.name.text,
155
155
  tag: found.tag,
package/src/mcp/index.ts CHANGED
@@ -10,7 +10,7 @@ import type { McpResourceContribution, McpToolContribution } from "@intentius/ch
10
10
  import { createCatalogResource, createDiffTool } from "@intentius/chant/lexicon-plugin-helpers";
11
11
  import { sqlSerializer } from "../serializer";
12
12
  import { catalogIndex } from "../lsp/catalog";
13
- import { database, table, view } from "../clickhouse/entities";
13
+ import { database, dictionary, func, grant, policy, role, table, user, view } from "../clickhouse/entities";
14
14
  import { SqlSyntaxError } from "../clickhouse/tokens";
15
15
  import { existsSync, readFileSync } from "fs";
16
16
  import { diffSchemas } from "../clickhouse/plan/diff";
@@ -122,12 +122,12 @@ const searchTool: McpToolContribution = {
122
122
  const parseTool: McpToolContribution = {
123
123
  name: "parse-ddl",
124
124
  description:
125
- "Parse one CREATE statement the way its tag does (dialect clickhouse, the default: table, view, database; dialect postgres: schema, table, index, view, sequence, type, domain, extension), and return the entity it builds (columns, engine, keys, settings, and for a view its lineage). A statement that does not parse comes back as the error with its line and column, the same one SQLCH001 (ClickHouse) or SQLPG001 (Postgres) reports. The statement is plain DDL: there are no interpolations.",
125
+ "Parse one CREATE statement the way its tag does (dialect clickhouse, the default: table, view, database, dictionary, func, user, role, policy, grant; dialect postgres: schema, table, index, view, sequence, type, domain, extension), and return the entity it builds (columns, engine, keys, settings, and for a view its lineage). A statement that does not parse comes back as the error with its line and column, the same one SQLCH001 (ClickHouse) or SQLPG001 (Postgres) reports. The statement is plain DDL: there are no interpolations.",
126
126
  inputSchema: {
127
127
  type: "object",
128
128
  properties: {
129
129
  dialect: dialectProp,
130
- tag: { type: "string", enum: [...new Set(["table", "view", "database", ...POSTGRES_TAGS])], description: "The tag the statement belongs in (ClickHouse: table, view, database; Postgres: " + POSTGRES_TAGS.join(", ") + ")" },
130
+ tag: { type: "string", enum: [...new Set(["table", "view", "database", "dictionary", "func", "user", "role", "policy", "grant", ...POSTGRES_TAGS])], description: "The tag the statement belongs in (ClickHouse: table, view, database, dictionary, func, user, role, policy, grant; Postgres: " + POSTGRES_TAGS.join(", ") + ")" },
131
131
  ddl: { type: "string", description: "The CREATE statement" },
132
132
  },
133
133
  required: ["tag", "ddl"],
@@ -136,8 +136,8 @@ const parseTool: McpToolContribution = {
136
136
  const ddl = String(params.ddl ?? "");
137
137
  const tag = String(params.tag);
138
138
  if (asDialect(params.dialect) === "postgres") return postgresParse(tag, ddl, position);
139
- const build = tag === "table" ? table : tag === "view" ? view : tag === "database" ? database : undefined;
140
- if (!build) throw new Error("tag must be table, view or database");
139
+ const build = tag === "table" ? table : tag === "view" ? view : tag === "database" ? database : tag === "dictionary" ? dictionary : tag === "func" ? func : tag === "user" ? user : tag === "role" ? role : tag === "policy" ? policy : tag === "grant" ? grant : undefined;
140
+ if (!build) throw new Error("tag must be table, view, database, dictionary, func, user, role, policy or grant");
141
141
  const strings = Object.assign([ddl], { raw: [ddl] }) as unknown as TemplateStringsArray;
142
142
  try {
143
143
  const entity = build(strings);
@@ -276,7 +276,7 @@ export function sqlMcpResources(): McpResourceContribution[] {
276
276
  createCatalogResource(
277
277
  new URL("../plugin.ts", import.meta.url).href,
278
278
  "SQL Entity Catalog",
279
- "The entity kinds the sql lexicon declares (database, table, view, materialized view)",
279
+ "The entity kinds the sql lexicon declares (database, table, view, materialized view, dictionary, function, user, role, row policy, grant)",
280
280
  "lexicon-sql.json",
281
281
  "sql",
282
282
  ),