@intentius/chant-lexicon-sql 0.124.19 → 0.124.21

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 (156) hide show
  1. package/dist/clickhouse/access.d.ts +36 -7
  2. package/dist/clickhouse/access.d.ts.map +1 -1
  3. package/dist/clickhouse/apply/statements.d.ts.map +1 -1
  4. package/dist/clickhouse/entities.d.ts +60 -0
  5. package/dist/clickhouse/entities.d.ts.map +1 -1
  6. package/dist/clickhouse/import/generator.d.ts +1 -7
  7. package/dist/clickhouse/import/generator.d.ts.map +1 -1
  8. package/dist/clickhouse/live/catalog.d.ts +6 -5
  9. package/dist/clickhouse/live/catalog.d.ts.map +1 -1
  10. package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
  11. package/dist/clickhouse/parser.d.ts +16 -0
  12. package/dist/clickhouse/parser.d.ts.map +1 -1
  13. package/dist/clickhouse/plan/deep.d.ts.map +1 -1
  14. package/dist/clickhouse/plan/diff.d.ts.map +1 -1
  15. package/dist/clickhouse/plan/normalize.d.ts +6 -3
  16. package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
  17. package/dist/clickhouse/plan/rules.d.ts +3 -0
  18. package/dist/clickhouse/plan/rules.d.ts.map +1 -1
  19. package/dist/clickhouse/topology.d.ts.map +1 -1
  20. package/dist/clickhouse.d.ts +1 -1
  21. package/dist/clickhouse.d.ts.map +1 -1
  22. package/dist/codegen/registry.d.ts.map +1 -1
  23. package/dist/coverage/clickhouse.json +8 -8
  24. package/dist/coverage/postgres.json +6 -6
  25. package/dist/files/clickhouse.d.ts +2 -1
  26. package/dist/files/clickhouse.d.ts.map +1 -1
  27. package/dist/files/postgres.d.ts +1 -1
  28. package/dist/index.d.ts +2 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/integrity.json +10 -10
  31. package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
  32. package/dist/lint/post-synth/sql101.d.ts.map +1 -1
  33. package/dist/lint/rules/sqlch001.d.ts +1 -1
  34. package/dist/lint/rules/sqlch001.d.ts.map +1 -1
  35. package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
  36. package/dist/lint/rules/templates.d.ts +1 -1
  37. package/dist/lint/rules/templates.d.ts.map +1 -1
  38. package/dist/lsp/postgres-template.d.ts.map +1 -1
  39. package/dist/manifest.json +1 -1
  40. package/dist/meta.json +40 -0
  41. package/dist/okf/index.md +9 -1
  42. package/dist/okf/rules/SQL101.md +1 -0
  43. package/dist/okf/rules/SQLCH001.md +2 -2
  44. package/dist/okf/rules/SQLPG113.md +2 -0
  45. package/dist/okf/types/PostgresAggregate.md +13 -0
  46. package/dist/okf/types/PostgresCast.md +9 -0
  47. package/dist/okf/types/PostgresCollation.md +9 -0
  48. package/dist/okf/types/PostgresCompositeType.md +13 -0
  49. package/dist/okf/types/PostgresEventTrigger.md +9 -0
  50. package/dist/okf/types/PostgresRangeType.md +13 -0
  51. package/dist/okf/types/Quota.md +9 -0
  52. package/dist/okf/types/SettingsProfile.md +9 -0
  53. package/dist/plugin.d.ts.map +1 -1
  54. package/dist/postgres/apply/apply.d.ts.map +1 -1
  55. package/dist/postgres/apply/statements.d.ts +4 -3
  56. package/dist/postgres/apply/statements.d.ts.map +1 -1
  57. package/dist/postgres/entities.d.ts +129 -6
  58. package/dist/postgres/entities.d.ts.map +1 -1
  59. package/dist/postgres/entity-types.d.ts +6 -0
  60. package/dist/postgres/entity-types.d.ts.map +1 -1
  61. package/dist/postgres/import/generator.d.ts +3 -2
  62. package/dist/postgres/import/generator.d.ts.map +1 -1
  63. package/dist/postgres/live/catalog-kinds.d.ts +28 -0
  64. package/dist/postgres/live/catalog-kinds.d.ts.map +1 -0
  65. package/dist/postgres/live/catalog.d.ts +15 -11
  66. package/dist/postgres/live/catalog.d.ts.map +1 -1
  67. package/dist/postgres/live/describe-resources.d.ts.map +1 -1
  68. package/dist/postgres/parser.d.ts +88 -5
  69. package/dist/postgres/parser.d.ts.map +1 -1
  70. package/dist/postgres/plan/deep.d.ts.map +1 -1
  71. package/dist/postgres/plan/diff.d.ts.map +1 -1
  72. package/dist/postgres/plan/normalize.d.ts +17 -2
  73. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  74. package/dist/postgres/plan/rules.d.ts +3 -0
  75. package/dist/postgres/plan/rules.d.ts.map +1 -1
  76. package/dist/postgres/plan/schema.d.ts +3 -2
  77. package/dist/postgres/plan/schema.d.ts.map +1 -1
  78. package/dist/postgres/testing/roundtrip-schema.d.ts +4 -2
  79. package/dist/postgres/testing/roundtrip-schema.d.ts.map +1 -1
  80. package/dist/postgres.d.ts +1 -1
  81. package/dist/postgres.d.ts.map +1 -1
  82. package/dist/rules/postgres-names.ts +15 -2
  83. package/dist/rules/postgres-templates.ts +2 -2
  84. package/dist/rules/sql101.ts +1 -0
  85. package/dist/rules/sqlch001.ts +10 -3
  86. package/dist/rules/sqlpg001.ts +6 -21
  87. package/dist/rules/sqlpg113.ts +1 -1
  88. package/dist/rules/templates.ts +2 -2
  89. package/dist/validate.d.ts.map +1 -1
  90. package/package.json +2 -2
  91. package/src/clickhouse/access.ts +197 -22
  92. package/src/clickhouse/apply/statements.ts +17 -5
  93. package/src/clickhouse/entities.ts +78 -2
  94. package/src/clickhouse/import/generator.ts +16 -3
  95. package/src/clickhouse/live/catalog.ts +57 -10
  96. package/src/clickhouse/live/describe-resources.ts +3 -1
  97. package/src/clickhouse/live/kinds.e2e.test.ts +179 -0
  98. package/src/clickhouse/live/undeclarable.e2e.test.ts +5 -4
  99. package/src/clickhouse/parser.test.ts +45 -2
  100. package/src/clickhouse/parser.ts +111 -3
  101. package/src/clickhouse/plan/deep.ts +25 -6
  102. package/src/clickhouse/plan/diff.ts +41 -1
  103. package/src/clickhouse/plan/normalize.ts +12 -4
  104. package/src/clickhouse/plan/rules.ts +22 -1
  105. package/src/clickhouse/quota-profile.test.ts +186 -0
  106. package/src/clickhouse/table-function.test.ts +54 -0
  107. package/src/clickhouse/testdata/server-ddl-baseline.json +17 -14
  108. package/src/clickhouse/topology.ts +4 -2
  109. package/src/clickhouse.ts +6 -0
  110. package/src/codegen/postgres-classifier-page.ts +1 -1
  111. package/src/codegen/registry.ts +8 -0
  112. package/src/core/undeclarable.test.ts +22 -19
  113. package/src/coverage/clickhouse.json +8 -8
  114. package/src/coverage/postgres.json +6 -6
  115. package/src/files/clickhouse.ts +14 -6
  116. package/src/files/postgres.ts +19 -5
  117. package/src/generated/lexicon-sql.json +40 -0
  118. package/src/index.ts +2 -2
  119. package/src/lint/post-synth/postgres-names.ts +15 -2
  120. package/src/lint/post-synth/sql101.ts +1 -0
  121. package/src/lint/post-synth/sqlpg113.ts +1 -1
  122. package/src/lint/rules/postgres-templates.ts +2 -2
  123. package/src/lint/rules/sqlch001.ts +10 -3
  124. package/src/lint/rules/sqlpg001.ts +6 -21
  125. package/src/lint/rules/templates.ts +2 -2
  126. package/src/lsp/postgres-template.ts +14 -0
  127. package/src/mcp/index.ts +5 -5
  128. package/src/mcp/postgres.ts +1 -1
  129. package/src/plugin.ts +7 -1
  130. package/src/postgres/apply/apply.ts +8 -2
  131. package/src/postgres/apply/statements.ts +29 -6
  132. package/src/postgres/entities.test.ts +2 -2
  133. package/src/postgres/entities.ts +316 -28
  134. package/src/postgres/entity-types.ts +6 -0
  135. package/src/postgres/import/generator.ts +31 -4
  136. package/src/postgres/kinds.test.ts +140 -0
  137. package/src/postgres/live/catalog-kinds.ts +312 -0
  138. package/src/postgres/live/catalog.ts +30 -12
  139. package/src/postgres/live/describe-resources.ts +14 -5
  140. package/src/postgres/live/kinds.e2e.test.ts +182 -0
  141. package/src/postgres/live/roundtrip.e2e.test.ts +9 -3
  142. package/src/postgres/live/undeclarable.e2e.test.ts +15 -13
  143. package/src/postgres/parser.test.ts +2 -1
  144. package/src/postgres/parser.ts +307 -17
  145. package/src/postgres/plan/deep.ts +33 -0
  146. package/src/postgres/plan/diff.ts +45 -0
  147. package/src/postgres/plan/normalize.ts +153 -2
  148. package/src/postgres/plan/rules.ts +21 -0
  149. package/src/postgres/plan/schema.ts +6 -4
  150. package/src/postgres/testing/roundtrip-schema.ts +19 -1
  151. package/src/postgres.ts +19 -0
  152. package/src/skill-defs.ts +1 -1
  153. package/src/skills/chant-sql-postgres.md +7 -3
  154. package/src/skills/chant-sql.md +3 -1
  155. package/src/unknown-clauses.test.ts +2 -1
  156. package/src/validate.ts +8 -0
@@ -29,10 +29,10 @@ const CATALOG: FakeCatalog = {
29
29
  schemas: [{ name: "app" }],
30
30
  tables: [{ schema: "app", name: "users", columns: [{ name: "id", type: "bigint", notnull: true }] }],
31
31
  undeclarable: [
32
- { kind: "aggregate", schema: "app", name: "total(integer)" },
33
- { kind: "cast", name: "(app.pair AS text)" },
34
- { kind: "composite type", schema: "app", name: "pair" },
35
- { kind: "event trigger", name: "audit_ddl" },
32
+ { kind: "access method", name: "heap2" },
33
+ { kind: "conversion", schema: "app", name: "latin1_utf8" },
34
+ { kind: "rule", schema: "app", name: "users_notify ON users" },
35
+ { kind: "base type", schema: "app", name: "later" },
36
36
  ],
37
37
  };
38
38
 
@@ -45,10 +45,10 @@ describe("Postgres live import", () => {
45
45
  );
46
46
  expect(err).toBeInstanceOf(UndeclarableObjectsError);
47
47
  const message = (err as Error).message;
48
- for (const line of [" - aggregate app.total(integer)", " - cast (app.pair AS text)", " - composite type app.pair", " - event trigger audit_ddl"]) {
48
+ for (const line of [" - access method heap2", " - conversion app.latin1_utf8", " - rule app.users_notify ON users", " - base type app.later"]) {
49
49
  expect(message).toContain(line);
50
50
  }
51
- expect(message).toContain(" - aggregate: no tag declares an aggregate yet");
51
+ expect(message).toContain(" - conversion: no tag declares an encoding conversion");
52
52
  expect(message).toContain("--skip-undeclarable");
53
53
  });
54
54
 
@@ -58,7 +58,7 @@ describe("Postgres live import", () => {
58
58
  expect(ir.resources.map((r) => r.properties.name)).toContain("users");
59
59
  const left = (ir.warnings ?? []).filter((w) => w.includes("is left out (--skip-undeclarable)"));
60
60
  expect(left).toHaveLength(4);
61
- expect(left[0]).toBe("aggregate app.total(integer) is left out (--skip-undeclarable): no tag declares an aggregate yet");
61
+ expect(left[0]).toBe("access method heap2 is left out (--skip-undeclarable): no tag declares an access method; an extension usually installs one, and the extension is declared instead");
62
62
  });
63
63
 
64
64
  test("a selector or owned import does not look", async () => {
@@ -68,11 +68,11 @@ describe("Postgres live import", () => {
68
68
  });
69
69
 
70
70
  test("a managed provider's own objects are its, with a warning, not a refusal", async () => {
71
- pg.catalog = { ...CATALOG, undeclarable: [{ kind: "aggregate", schema: "auth", name: "x(integer)" }] };
71
+ pg.catalog = { ...CATALOG, undeclarable: [{ kind: "conversion", schema: "auth", name: "x" }] };
72
72
  pg.provider = "supabase";
73
73
  try {
74
74
  const ir = await exportPostgres({ environment: "prod" });
75
- expect(ir.warnings).toContain("aggregate auth.x(integer) is kept by Supabase; left out");
75
+ expect(ir.warnings).toContain("conversion auth.x is kept by Supabase; left out");
76
76
  } finally {
77
77
  pg.provider = undefined;
78
78
  }
@@ -90,8 +90,8 @@ describe("ClickHouse live import", () => {
90
90
  ],
91
91
  {
92
92
  undeclarable: [
93
- { kind: "quota", name: "q1" },
94
- { kind: "settings profile", name: "sp1" },
93
+ { kind: "named collection", name: "nc1" },
94
+ { kind: "workload", name: "all" },
95
95
  { kind: "window view", database: "analytics", name: "wv" },
96
96
  ],
97
97
  },
@@ -101,7 +101,7 @@ describe("ClickHouse live import", () => {
101
101
  const run = (extra: Record<string, unknown> = {}) => exportClickHouse({ environment: "x", config: {}, env: { CLICKHOUSE_URL: server.url }, ...extra });
102
102
 
103
103
  test("fails naming them", async () => {
104
- await expect(run()).rejects.toThrow(/ - quota q1\n - settings profile sp1\n - window view analytics\.wv\n/);
104
+ await expect(run()).rejects.toThrow(/ - named collection nc1\n - window view analytics\.wv\n - workload all\n/);
105
105
  });
106
106
 
107
107
  test("with skipUndeclarable, warns, and the window view is not imported as a table", async () => {
@@ -115,24 +115,27 @@ describe("file import", () => {
115
115
  const DDL = `CREATE SCHEMA app;
116
116
  CREATE TABLE app.t (id int PRIMARY KEY);
117
117
  CREATE AGGREGATE app.total(int) (sfunc = int4pl, stype = int);
118
+ CREATE CONVERSION app.latin1_utf8 FOR 'LATIN1' TO 'UTF8' FROM iso8859_1_to_utf8;
118
119
  CREATE TYPE app.pair AS (a int, b text);
120
+ CREATE TYPE app.later;
119
121
  CREATE COLLATION app.ci (provider = icu, locale = 'und');`;
120
122
 
121
123
  test("a CREATE of a kind chant cannot declare is an error naming it", () => {
122
- expect(() => new SqlFileParser("postgres").parse(DDL)).toThrow(/aggregate: chant cannot declare it \(no tag declares an aggregate yet\): CREATE AGGREGATE app\.total/);
124
+ expect(() => new SqlFileParser("postgres").parse(DDL)).toThrow(/conversion: chant cannot declare it \(no tag declares an encoding conversion\): CREATE CONVERSION app\.latin1_utf8/);
123
125
  });
124
126
 
125
127
  test("skipUndeclarable imports the rest and lists them as warnings", () => {
126
128
  const ir = new SqlFileParser("postgres", { skipUndeclarable: true }).parse(DDL);
127
- expect(ir.resources.map((r) => r.properties.name)).toEqual(["app", "t"]);
129
+ // An aggregate, a composite type and a collation are declared kinds (#3911): imported, not refused.
130
+ expect(ir.resources.map((r) => r.properties.name)).toEqual(["app", "t", "total", "pair", "ci"]);
128
131
  expect(ir.warnings).toEqual([
129
- "left out (skipUndeclarable): aggregate: chant cannot declare it (no tag declares an aggregate yet): CREATE AGGREGATE app.total(int) (sfunc = int4pl, stype = int)",
130
- "left out (skipUndeclarable): composite type: chant cannot declare it (no tag declares a composite type yet): CREATE TYPE app.pair AS (a int, b text)",
131
- "left out (skipUndeclarable): collation: chant cannot declare it (no tag declares a collation yet): CREATE COLLATION app.ci (provider = icu, locale = 'und')",
132
+ "left out (skipUndeclarable): conversion: chant cannot declare it (no tag declares an encoding conversion): CREATE CONVERSION app.latin1_utf8 FOR 'LATIN1' TO 'UTF8' FROM iso8859_1_to_utf8",
133
+ "left out (skipUndeclarable): base type: chant cannot declare it (no tag declares a base type; its input and output functions are C code an extension installs): CREATE TYPE app.later",
132
134
  ]);
133
135
  });
134
136
 
135
- test("ClickHouse: a quota in a file is refused too", () => {
136
- expect(() => new SqlFileParser("clickhouse").parse("CREATE DATABASE a;\nCREATE QUOTA q1")).toThrow(/quota: chant cannot declare it/);
137
+ test("ClickHouse: a named collection in a file is refused too; a quota is declared (#3911)", () => {
138
+ expect(() => new SqlFileParser("clickhouse").parse("CREATE DATABASE a;\nCREATE NAMED COLLECTION c AS url = 'x'")).toThrow(/named collection: chant cannot declare it/);
139
+ expect(new SqlFileParser("clickhouse").parse("CREATE DATABASE a;\nCREATE QUOTA q1").resources.map((r) => r.type)).toEqual(["ClickHouse::Database", "ClickHouse::Quota"]);
137
140
  });
138
141
  });
@@ -5,21 +5,21 @@
5
5
  { "command": "CREATE DICTIONARY", "kind": "dictionary", "status": "declared", "entity": "ClickHouse::Dictionary" },
6
6
  { "command": "CREATE FUNCTION", "kind": "SQL function", "status": "declared", "entity": "ClickHouse::Function" },
7
7
  { "command": "CREATE FUNCTION", "form": "LANGUAGE WASM, or with a driver", "match": "\\bLANGUAGE\\b|\\bUSING\\s+DRIVER\\b", "kind": "non-SQL function", "status": "refused", "reason": "only a SQL function (CREATE FUNCTION f AS (x) -> ...) is declared" },
8
- { "command": "CREATE HANDLER", "kind": "HTTP handler", "status": "refused", "reason": "no declaration holds an HTTP handler yet" },
8
+ { "command": "CREATE HANDLER", "kind": "HTTP handler", "status": "refused", "reason": "an HTTP handler is server-level configuration, not a schema object" },
9
9
  { "command": "CREATE INDEX", "kind": "index", "status": "declared", "entity": "ClickHouse::Table", "note": "an index is part of its table: SHOW CREATE TABLE prints it, and the table's declaration holds it" },
10
- { "command": "CREATE MASKING POLICY", "kind": "masking policy", "status": "refused", "reason": "no declaration holds a masking policy yet" },
11
- { "command": "CREATE NAMED COLLECTION", "kind": "named collection", "status": "refused", "reason": "no declaration holds a named collection yet; one usually carries credentials" },
12
- { "command": "CREATE QUOTA", "kind": "quota", "status": "refused", "reason": "no declaration holds a quota yet" },
13
- { "command": "CREATE RESOURCE", "kind": "workload resource", "status": "refused", "reason": "no declaration holds a workload scheduling resource yet" },
10
+ { "command": "CREATE MASKING POLICY", "kind": "masking policy", "status": "refused", "reason": "masking policies are available only in ClickHouse Cloud, not on the pinned server" },
11
+ { "command": "CREATE NAMED COLLECTION", "kind": "named collection", "status": "refused", "reason": "a named collection holds credentials (a password, a secret key), which are the environment's and never in a build; the server hides its values, so a plan could not compare them either" },
12
+ { "command": "CREATE QUOTA", "kind": "quota", "status": "declared", "entity": "ClickHouse::Quota", "note": "import reads only the access objects a build declares" },
13
+ { "command": "CREATE RESOURCE", "kind": "workload resource", "status": "refused", "reason": "a resource is server-level scheduling (a disk's reads or writes), configured with the server, not a schema object" },
14
14
  { "command": "CREATE ROLE", "kind": "role", "status": "declared", "entity": "ClickHouse::Role", "note": "import reads only the access objects a build declares" },
15
15
  { "command": "CREATE ROW POLICY", "kind": "row policy", "status": "declared", "entity": "ClickHouse::RowPolicy", "note": "import reads only the access objects a build declares" },
16
- { "command": "CREATE SETTINGS PROFILE", "kind": "settings profile", "status": "refused", "reason": "no declaration holds a settings profile yet" },
16
+ { "command": "CREATE SETTINGS PROFILE", "kind": "settings profile", "status": "declared", "entity": "ClickHouse::SettingsProfile", "note": "import reads only the access objects a build declares" },
17
17
  { "command": "CREATE TABLE", "kind": "table", "status": "declared", "entity": "ClickHouse::Table" },
18
18
  { "command": "CREATE TABLE", "form": "TEMPORARY", "match": "^CREATE\\s+TEMPORARY\\b", "kind": "temporary table", "status": "out-of-scope", "reason": "a temporary table lives in one session" },
19
19
  { "command": "CREATE USER", "kind": "user", "status": "declared", "entity": "ClickHouse::User", "note": "import reads only the access objects a build declares" },
20
20
  { "command": "CREATE VIEW", "kind": "view", "status": "declared", "entity": "ClickHouse::View" },
21
21
  { "command": "CREATE VIEW", "form": "MATERIALIZED", "match": "^CREATE\\s+(?:OR\\s+REPLACE\\s+)?MATERIALIZED\\b", "kind": "materialized view", "status": "declared", "entity": "ClickHouse::MaterializedView" },
22
- { "command": "CREATE WINDOW VIEW", "kind": "window view", "status": "refused", "reason": "no declaration holds a window view, an experimental feature" },
23
- { "command": "CREATE WORKLOAD", "kind": "workload", "status": "refused", "reason": "no declaration holds a workload yet" }
22
+ { "command": "CREATE WINDOW VIEW", "kind": "window view", "status": "refused", "reason": "a window view is experimental, and the pinned server refuses it under its default query analyzer" },
23
+ { "command": "CREATE WORKLOAD", "kind": "workload", "status": "refused", "reason": "a workload is server-level scheduling, configured with the server, not a schema object" }
24
24
  ]
25
25
  }
@@ -2,13 +2,13 @@
2
2
  "dialect": "postgres",
3
3
  "kinds": [
4
4
  { "command": "CREATE ACCESS METHOD", "kind": "access method", "status": "refused", "reason": "no tag declares an access method; an extension usually installs one, and the extension is declared instead" },
5
- { "command": "CREATE AGGREGATE", "kind": "aggregate", "status": "refused", "reason": "no tag declares an aggregate yet" },
6
- { "command": "CREATE CAST", "kind": "cast", "status": "refused", "reason": "no tag declares a cast yet" },
7
- { "command": "CREATE COLLATION", "kind": "collation", "status": "refused", "reason": "no tag declares a collation yet" },
5
+ { "command": "CREATE AGGREGATE", "kind": "aggregate", "status": "declared", "entity": "Postgres::Aggregate" },
6
+ { "command": "CREATE CAST", "kind": "cast", "status": "declared", "entity": "Postgres::Cast", "note": "the database's, named by its two types; read where a type or its function is in scope" },
7
+ { "command": "CREATE COLLATION", "kind": "collation", "status": "declared", "entity": "Postgres::Collation" },
8
8
  { "command": "CREATE CONVERSION", "kind": "conversion", "status": "refused", "reason": "no tag declares an encoding conversion" },
9
9
  { "command": "CREATE DATABASE", "kind": "database", "status": "out-of-scope", "reason": "a project's schema lives in one database, which the environment provides (sql.profiles.<env>)" },
10
10
  { "command": "CREATE DOMAIN", "kind": "domain", "status": "declared", "entity": "Postgres::Domain" },
11
- { "command": "CREATE EVENT TRIGGER", "kind": "event trigger", "status": "refused", "reason": "no tag declares an event trigger yet" },
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": "refused", "reason": "no tag declares a foreign data wrapper yet; one an extension installs comes with the extension" },
14
14
  { "command": "CREATE FOREIGN TABLE", "kind": "foreign table", "status": "refused", "reason": "no tag declares a foreign table yet" },
@@ -41,8 +41,8 @@
41
41
  { "command": "CREATE TRANSFORM", "kind": "transform", "status": "refused", "reason": "no tag declares a transform; an extension installs one" },
42
42
  { "command": "CREATE TRIGGER", "kind": "trigger", "status": "declared", "entity": "Postgres::Trigger" },
43
43
  { "command": "CREATE TYPE", "form": "AS ENUM", "match": "\\bAS\\s+ENUM\\b", "kind": "enum type", "status": "declared", "entity": "Postgres::Enum" },
44
- { "command": "CREATE TYPE", "form": "AS (...)", "match": "\\bAS\\s*\\(", "kind": "composite type", "status": "refused", "reason": "no tag declares a composite type yet" },
45
- { "command": "CREATE TYPE", "form": "AS RANGE", "match": "\\bAS\\s+RANGE\\b", "kind": "range type", "status": "refused", "reason": "no tag declares a range type yet" },
44
+ { "command": "CREATE TYPE", "form": "AS (...)", "match": "\\bAS\\s*\\(", "kind": "composite type", "status": "declared", "entity": "Postgres::CompositeType" },
45
+ { "command": "CREATE TYPE", "form": "AS RANGE", "match": "\\bAS\\s+RANGE\\b", "kind": "range type", "status": "declared", "entity": "Postgres::RangeType", "note": "its multirange type comes with it (MULTIRANGE_TYPE_NAME)" },
46
46
  { "command": "CREATE TYPE", "form": "INPUT, OUTPUT, or a shell type", "kind": "base type", "status": "refused", "reason": "no tag declares a base type; its input and output functions are C code an extension installs" },
47
47
  { "command": "CREATE USER", "kind": "user", "status": "declared", "entity": "Postgres::Role", "note": "an alias of CREATE ROLE ... LOGIN" },
48
48
  { "command": "CREATE USER MAPPING", "kind": "user mapping", "status": "refused", "reason": "no tag declares a user mapping yet" },
@@ -2,7 +2,8 @@
2
2
  * A file of ClickHouse DDL as the objects it declares: one per `CREATE
3
3
  * DATABASE`, `TABLE`, `VIEW`, `MATERIALIZED VIEW`, `DICTIONARY` or `FUNCTION`, as `chant import
4
4
  * <file>.sql` reads them, and one per `CREATE USER`, `CREATE ROLE`, `CREATE
5
- * ROW POLICY` and `GRANT` (#3711), each read by its tag, so what the tag
5
+ * ROW POLICY`, `CREATE QUOTA`, `CREATE SETTINGS PROFILE` (#3911) and `GRANT`
6
+ * (#3711), each read by its tag, so what the tag
6
7
  * refuses (`IDENTIFIED BY`, a `REVOKE`) is refused with the tag's message.
7
8
  * Any other statement, and any statement the parser refuses, is a problem
8
9
  * naming it.
@@ -15,7 +16,7 @@
15
16
  import type { Declarable } from "@intentius/chant/declarable";
16
17
  import { isTrivia, tokenizeText } from "../clickhouse/tokens";
17
18
  import { parseCreate } from "../clickhouse/parser";
18
- import { CLICKHOUSE_ENTITY_TYPES, database, dictionary, func, grant, policy, role, table, user, view } from "../clickhouse/entities";
19
+ import { CLICKHOUSE_ENTITY_TYPES, database, dictionary, func, grant, policy, quota, role, settingsProfile, table, user, view } from "../clickhouse/entities";
19
20
  import { isAccessStatement, parseAccess, type AccessNode } from "../clickhouse/access";
20
21
  import type { Token } from "../clickhouse/tokens";
21
22
  import { CLICKHOUSE_TAG_OF } from "../clickhouse/import/generator";
@@ -24,7 +25,13 @@ import { fileUnparsed, firstLine, lineAt, quoteBare, splitTokens, tokensText, un
24
25
  import { templateSafe, templateStrings } from "./postgres";
25
26
 
26
27
  type Tag = (strings: TemplateStringsArray, ...values: unknown[]) => Declarable;
27
- const TAGS: Record<string, Tag> = { database, table, view, dictionary, func, user, role, policy, grant };
28
+ const TAGS: Record<string, Tag> = { database, table, view, dictionary, func, user, role, policy, quota, settingsProfile, grant };
29
+
30
+ /** The tag an access statement's words name: `CREATE USER`, `CREATE SETTINGS PROFILE`, `GRANT`. */
31
+ function accessTag(words: string[]): Tag {
32
+ if (words[0] !== "CREATE") return grant;
33
+ return words[1] === "USER" ? user : words[1] === "ROLE" ? role : words[1] === "QUOTA" ? quota : words[1] === "SETTINGS" || words[1] === "PROFILE" ? settingsProfile : policy;
34
+ }
28
35
 
29
36
  /** Tokens with `[db.]name`'s bare name qualified with `schema`. A `*` (all tables) is left alone. */
30
37
  function qualifyBare(tokens: Token[], span: { from: number; to: number } | undefined, schema: string): string | undefined {
@@ -44,8 +51,7 @@ function readAccess(sql: string, options: ReadOptions): ImportedObject | string
44
51
  node = parseAccess(tokens);
45
52
  } catch (e) {
46
53
  // What the tag refuses (a password) is refused in the tag's words.
47
- const words = sql.replace(/--[^\n]*\n/g, " ").trim().split(/\s+/, 2).map((w) => w.toUpperCase());
48
- const tag = words[0] === "CREATE" ? (words[1] === "USER" ? user : words[1] === "ROLE" ? role : policy) : grant;
54
+ const tag = accessTag(sql.replace(/--[^\n]*\n/g, " ").trim().split(/\s+/, 2).map((w) => w.toUpperCase()));
49
55
  try {
50
56
  tag(templateStrings([templateSafe(sql)]));
51
57
  } catch (refused) {
@@ -61,7 +67,7 @@ function readAccess(sql: string, options: ReadOptions): ImportedObject | string
61
67
  node = parseAccess(tokens);
62
68
  }
63
69
  }
64
- const tag = node.statement === "user" ? user : node.statement === "role" ? role : node.statement === "rowPolicy" ? policy : grant;
70
+ const tag = { user, role, rowPolicy: policy, quota, settingsProfile, grant }[node.statement];
65
71
  let props: { name: string; database?: string; table?: string };
66
72
  try {
67
73
  props = (tag(templateStrings([templateSafe(sql)])) as unknown as { props: typeof props }).props;
@@ -70,6 +76,8 @@ function readAccess(sql: string, options: ReadOptions): ImportedObject | string
70
76
  }
71
77
  if (node.statement === "user") return { type: CLICKHOUSE_ENTITY_TYPES.user, name: props.name, ddl: sql };
72
78
  if (node.statement === "role") return { type: CLICKHOUSE_ENTITY_TYPES.role, name: props.name, ddl: sql };
79
+ if (node.statement === "quota") return { type: CLICKHOUSE_ENTITY_TYPES.quota, name: props.name, ddl: sql };
80
+ if (node.statement === "settingsProfile") return { type: CLICKHOUSE_ENTITY_TYPES.settingsProfile, name: props.name, ddl: sql };
73
81
  if (node.statement === "rowPolicy") return { type: CLICKHOUSE_ENTITY_TYPES.rowPolicy, ...(props.database ? { database: props.database } : {}), name: props.name, ddl: sql };
74
82
  // A grant has no name of its own: what it grants, on what, to whom.
75
83
  return { type: CLICKHOUSE_ENTITY_TYPES.grant, name: `grant ${tokensText(tokens.slice(tokens.findIndex((t) => !isTrivia(t)) + 1)).replace(/\s+/g, " ").trim().toLowerCase()}`, ddl: sql };
@@ -16,12 +16,12 @@
16
16
  * With `schema`, the names the DDL leaves unqualified are qualified with it,
17
17
  * as an ORM's connection would: an object's own name, an index's, trigger's
18
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.
19
+ * type naming an enum, composite, range or domain type the file creates.
20
20
  */
21
21
 
22
22
  import type { Declarable } from "@intentius/chant/declarable";
23
23
  import { isTrivia, tokenizeText, type Token } from "../postgres/tokens";
24
- import { identValue, parseStatements, type NameNode, type StatementNode } from "../postgres/parser";
24
+ import { identValue, parseStatements, type NameNode, type Span, type StatementNode } from "../postgres/parser";
25
25
  import * as tags from "../postgres/entities";
26
26
  import { POSTGRES_ENTITY_TYPES, type PostgresEntityType } from "../postgres/entity-types";
27
27
  import { POSTGRES_TAG_OF } from "../postgres/import/generator";
@@ -117,7 +117,7 @@ export function readPostgres(ddl: string, options: ReadOptions, problems: string
117
117
  // The types the file creates, so a column of one is qualified with the rest.
118
118
  const fileTypes = new Set<string>();
119
119
  for (const { tokens, node } of parsed) {
120
- if (node.statement === "enum" || node.statement === "domain") {
120
+ if (node.statement === "enum" || node.statement === "domain" || node.statement === "composite" || node.statement === "range") {
121
121
  const pieces = nameOf(tokens, node.name);
122
122
  if (pieces.length === 1) fileTypes.add(pieces[0]!);
123
123
  }
@@ -138,7 +138,7 @@ export function readPostgres(ddl: string, options: ReadOptions, problems: string
138
138
  };
139
139
  switch (node.statement) {
140
140
  case "comment":
141
- 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);
141
+ if (!["SCHEMA", "EXTENSION", "ROLE", "TRIGGER", "POLICY", "CONSTRAINT", "COLUMN", "CAST", "EVENT TRIGGER"].includes(node.objectType)) mark(node.target);
142
142
  if (node.objectType === "COLUMN" && nameOf(tokens, node.target).length === 2 && options.schema) {
143
143
  const first = tokens.slice(node.target.span.from, node.target.span.to).find((t) => !isTrivia(t));
144
144
  if (first) inserts.add(tokens.indexOf(first));
@@ -177,7 +177,7 @@ export function readPostgres(ddl: string, options: ReadOptions, problems: string
177
177
  problems.push(`an index with no name (its declaration needs one): ${firstLine(tokensText(tokens))}`);
178
178
  continue;
179
179
  }
180
- const ownName = !["schema", "extension", "index", "trigger", "policy", "role", "grant"].includes(node.statement);
180
+ const ownName = !["schema", "extension", "index", "trigger", "policy", "role", "grant", "cast", "eventTrigger"].includes(node.statement);
181
181
  if (ownName && "name" in node) mark(node.name);
182
182
  const object: PgFileObject = { node, statements: [rewrite(tokens, inserts, options.schema)], constraints: [], line, unparsed: fileUnparsed("unknown" in node ? node.unknown : undefined, tokens, tokensText(tokens), line) };
183
183
  objects.push(object);
@@ -277,6 +277,10 @@ function ownerKey(node: StatementNode, tokens: Token[], nameOf: NameOf, qualify:
277
277
  case "trigger":
278
278
  case "policy":
279
279
  return `${node.statement} ${nameOf(tokens, node.name).at(-1) ?? ""} ON ${qkey(qualify(nameOf(tokens, node.table)))}`;
280
+ case "eventTrigger":
281
+ return `eventTrigger ${nameOf(tokens, node.name).at(-1) ?? ""}`;
282
+ case "cast":
283
+ return `cast ${castKey(tokens, node.source, node.target)}`;
280
284
  case "grant":
281
285
  case "comment":
282
286
  case "rowSecurity":
@@ -305,6 +309,10 @@ function followOnOwner(node: StatementNode, tokens: Token[], nameOf: NameOf, qua
305
309
  case "TRIGGER":
306
310
  case "POLICY":
307
311
  return node.on ? `${node.objectType.toLowerCase()} ${target.at(-1) ?? ""} ON ${qkey(qualify(nameOf(tokens, node.on)))}` : undefined;
312
+ case "EVENT TRIGGER":
313
+ return `eventTrigger ${target.at(-1) ?? ""}`;
314
+ case "CAST":
315
+ return node.cast ? `cast ${castKey(tokens, node.cast.source, node.cast.target)}` : undefined;
308
316
  case "CONSTRAINT":
309
317
  case "DOMAIN CONSTRAINT":
310
318
  return node.on ? `rel ${qkey(qualify(nameOf(tokens, node.on)))}` : undefined;
@@ -313,6 +321,12 @@ function followOnOwner(node: StatementNode, tokens: Token[], nameOf: NameOf, qua
313
321
  }
314
322
  }
315
323
 
324
+ /** A cast's two types as one key, spacing and case aside. */
325
+ function castKey(tokens: readonly Token[], source: Span, target: Span): string {
326
+ const text = (sp: Span) => tokens.slice(sp.from, sp.to).filter((t) => !isTrivia(t)).map((t) => (t.kind === "ident" ? t.text.toLowerCase() : t.text)).join(" ");
327
+ return `(${text(source)} AS ${text(target)})`;
328
+ }
329
+
316
330
  /** The statement's tokens with `schema.` inserted before each marked one. */
317
331
  function rewrite(tokens: readonly Token[], inserts: ReadonlySet<number>, schema: string | undefined): Token[] {
318
332
  if (!schema || inserts.size === 0) return [...tokens];
@@ -49,6 +49,16 @@
49
49
  "kind": "resource",
50
50
  "lexicon": "sql"
51
51
  },
52
+ "Quota": {
53
+ "resourceType": "ClickHouse::Quota",
54
+ "kind": "resource",
55
+ "lexicon": "sql"
56
+ },
57
+ "SettingsProfile": {
58
+ "resourceType": "ClickHouse::SettingsProfile",
59
+ "kind": "resource",
60
+ "lexicon": "sql"
61
+ },
52
62
  "PostgresSchema": {
53
63
  "resourceType": "Postgres::Schema",
54
64
  "kind": "resource",
@@ -84,11 +94,41 @@
84
94
  "kind": "resource",
85
95
  "lexicon": "sql"
86
96
  },
97
+ "PostgresCompositeType": {
98
+ "resourceType": "Postgres::CompositeType",
99
+ "kind": "resource",
100
+ "lexicon": "sql"
101
+ },
102
+ "PostgresRangeType": {
103
+ "resourceType": "Postgres::RangeType",
104
+ "kind": "resource",
105
+ "lexicon": "sql"
106
+ },
87
107
  "PostgresDomain": {
88
108
  "resourceType": "Postgres::Domain",
89
109
  "kind": "resource",
90
110
  "lexicon": "sql"
91
111
  },
112
+ "PostgresAggregate": {
113
+ "resourceType": "Postgres::Aggregate",
114
+ "kind": "resource",
115
+ "lexicon": "sql"
116
+ },
117
+ "PostgresCollation": {
118
+ "resourceType": "Postgres::Collation",
119
+ "kind": "resource",
120
+ "lexicon": "sql"
121
+ },
122
+ "PostgresCast": {
123
+ "resourceType": "Postgres::Cast",
124
+ "kind": "resource",
125
+ "lexicon": "sql"
126
+ },
127
+ "PostgresEventTrigger": {
128
+ "resourceType": "Postgres::EventTrigger",
129
+ "kind": "resource",
130
+ "lexicon": "sql"
131
+ },
92
132
  "PostgresExtension": {
93
133
  "resourceType": "Postgres::Extension",
94
134
  "kind": "resource",
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, dictionary, user, role, policy, grant, literal } from "./clickhouse/entities";
15
+ export { database, table, view, dictionary, user, role, policy, quota, settingsProfile, 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 } from "./postgres/entities";
21
+ export { schema, index, sequence, type, domain, extension, func, procedure, aggregate, trigger, eventTrigger, 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`.
@@ -137,11 +137,24 @@ export function projectFromObjects(objects: OutputObject[], major: number): PgPr
137
137
  };
138
138
  }
139
139
 
140
- const TYPE_OBJECTS = new Set(["Postgres::Enum", "Postgres::Domain", "Postgres::Table", "Postgres::View", "Postgres::MaterializedView"]);
140
+ const TYPE_OBJECTS = new Set(["Postgres::Enum", "Postgres::CompositeType", "Postgres::RangeType", "Postgres::Domain", "Postgres::Table", "Postgres::View", "Postgres::MaterializedView"]);
141
141
  const ROUTINES = new Set(["Postgres::Function", "Postgres::Procedure"]);
142
142
 
143
+ /** A range type's multirange type's name: as written (its last piece), else as Postgres derives it. */
144
+ function multirangeOf(o: OutputObject): string | undefined {
145
+ if (o.type !== "Postgres::RangeType") return undefined;
146
+ const written = o.multirangeTypeName as string | undefined;
147
+ if (written) {
148
+ const last = written.split(".").pop()!.trim();
149
+ return /^"/.test(last) ? last.slice(1, -1).replace(/""/g, '"') : last.toLowerCase();
150
+ }
151
+ const name = String(o.name);
152
+ const at = name.indexOf("range");
153
+ return at >= 0 ? `${name.slice(0, at)}multi${name.slice(at)}` : `${name}_multirange`;
154
+ }
155
+
143
156
  function declared(p: PgProject, kinds: ReadonlySet<string>, schema: string | undefined, name: string): OutputObject | undefined {
144
- return p.objects.find((o) => kinds.has(o.type) && o.name === name && (schema === undefined || o.schema === schema));
157
+ return p.objects.find((o) => kinds.has(o.type) && (o.name === name || multirangeOf(o) === name) && (schema === undefined || o.schema === schema));
145
158
  }
146
159
 
147
160
  /** The extension that provides `name` in `section`, if the overlay knows one; declared ones first. */
@@ -14,6 +14,7 @@ const NOT_UNIQUE = new Set([
14
14
  "Postgres::DefaultPrivileges",
15
15
  "Postgres::Function",
16
16
  "Postgres::Procedure",
17
+ "Postgres::Aggregate",
17
18
  "Postgres::Trigger",
18
19
  "Postgres::Policy",
19
20
  ]);
@@ -1,7 +1,7 @@
1
1
  import { checkOf } from "./postgres-helpers";
2
2
  import { postgresObjects } from "./sql-helpers";
3
3
 
4
- const PLACED = new Set(["Postgres::Table", "Postgres::View", "Postgres::MaterializedView", "Postgres::Sequence", "Postgres::Enum", "Postgres::Domain"]);
4
+ const PLACED = new Set(["Postgres::Table", "Postgres::View", "Postgres::MaterializedView", "Postgres::Sequence", "Postgres::Enum", "Postgres::CompositeType", "Postgres::RangeType", "Postgres::Domain"]);
5
5
 
6
6
  /**
7
7
  * SQLPG113: an object in the public schema while the project declares schemas.
@@ -17,12 +17,12 @@ export const POSTGRES_TEMPLATE_SOURCES: readonly TemplateSource<PostgresTag>[] =
17
17
  {
18
18
  dialect: "postgres",
19
19
  modules: ["@intentius/chant-lexicon-sql/postgres"],
20
- tags: ["schema", "table", "index", "view", "sequence", "type", "domain", "extension", "func", "procedure", "trigger", "policy", "role", "grant"],
20
+ tags: ["schema", "table", "index", "view", "sequence", "type", "domain", "extension", "func", "procedure", "aggregate", "trigger", "eventTrigger", "collation", "cast", "policy", "role", "grant"],
21
21
  },
22
22
  {
23
23
  dialect: "postgres",
24
24
  modules: ["@intentius/chant-lexicon-sql"],
25
- tags: ["schema", "index", "sequence", "type", "domain", "extension", "func", "procedure", "trigger", "policy", "role", "grant"],
25
+ tags: ["schema", "index", "sequence", "type", "domain", "extension", "func", "procedure", "aggregate", "trigger", "eventTrigger", "collation", "cast", "policy", "role", "grant"],
26
26
  },
27
27
  ];
28
28
 
@@ -4,15 +4,22 @@ import { parseAccess } from "../../clickhouse/access";
4
4
  import { SqlSyntaxError } from "../../clickhouse/tokens";
5
5
  import { findTemplates, templatePosition, tokensOf } from "./templates";
6
6
 
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" };
7
+ const ACCESS_TAGS = new Set(["user", "role", "policy", "quota", "settingsProfile", "grant"]);
8
+ const ACCESS_STATEMENT: Record<string, string> = {
9
+ user: "CREATE USER",
10
+ role: "CREATE ROLE",
11
+ rowPolicy: "CREATE ROW POLICY",
12
+ quota: "CREATE QUOTA",
13
+ settingsProfile: "CREATE SETTINGS PROFILE",
14
+ grant: "GRANT",
15
+ };
9
16
 
10
17
  const STATEMENT: Record<string, string> = { database: "CREATE DATABASE", table: "CREATE TABLE", view: "CREATE VIEW", dictionary: "CREATE DICTIONARY", function: "CREATE FUNCTION" };
11
18
  /** The tag a statement belongs in: `func` for a function, the statement's own name otherwise. */
12
19
  const tagOf = (statement: string): string => (statement === "function" ? "func" : statement);
13
20
 
14
21
  /**
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.
22
+ * SQLCH001: the DDL in a ClickHouse template (`database`, `table`, `view`, `dictionary`, `func`, `user`, `role`, `policy`, `quota`, `settingsProfile`, `grant`) does not parse, or holds another statement than its tag.
16
23
  *
17
24
  * The same parse runs when the build calls the tag, and fails the build there.
18
25
  * This rule reports it in the editor and in `chant lint`, at the token, before
@@ -1,27 +1,12 @@
1
1
  import type { LintContext, LintDiagnostic, LintRule } from "@intentius/chant/lint/rule";
2
2
  import { parseStatements, SqlSyntaxError } from "../../postgres/parser";
3
- import { followOnStray, STATEMENT_NAMES, type PostgresTag } from "../../postgres/entities";
3
+ import { followOnStray, STATEMENT_NAMES, STATEMENTS_OF, type PostgresTag } from "../../postgres/entities";
4
4
  import { findPostgresTemplates, postgresTokensOf, templatePosition, tokenPosition } from "./postgres-templates";
5
5
 
6
- /** The statement each tag holds. */
7
- const STATEMENT: Record<PostgresTag, string> = {
8
- schema: "schema",
9
- table: "table",
10
- index: "index",
11
- view: "view",
12
- sequence: "sequence",
13
- type: "enum",
14
- domain: "domain",
15
- extension: "extension",
16
- func: "function",
17
- procedure: "procedure",
18
- trigger: "trigger",
19
- policy: "policy",
20
- role: "role",
21
- grant: "grant",
22
- };
23
-
24
- const TAG_OF: Record<string, PostgresTag> = Object.fromEntries(Object.entries(STATEMENT).map(([tag, statement]) => [statement, tag as PostgresTag]));
6
+ /** The tag that declares each statement. */
7
+ const TAG_OF: Record<string, PostgresTag> = Object.fromEntries(
8
+ Object.entries(STATEMENTS_OF).flatMap(([tag, statements]) => statements.map((statement) => [statement, tag as PostgresTag])),
9
+ );
25
10
 
26
11
  /**
27
12
  * SQLPG001: the DDL in a Postgres template does not parse, or does not hold
@@ -60,7 +45,7 @@ export const sqlpg001: LintRule = {
60
45
  if (!tokens) continue;
61
46
  try {
62
47
  const [node, ...rest] = parseStatements(tokens);
63
- if (node!.statement !== STATEMENT[found.tag]) {
48
+ if (!STATEMENTS_OF[found.tag].includes(node!.statement)) {
64
49
  const use = TAG_OF[node!.statement];
65
50
  report(
66
51
  use
@@ -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" | "dictionary" | "func" | "user" | "role" | "policy" | "grant";
16
+ export type SqlTag = "database" | "table" | "view" | "dictionary" | "func" | "user" | "role" | "policy" | "quota" | "settingsProfile" | "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" | "dictionary" | "func" | "us
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", "dictionary", "func", "user", "role", "policy", "grant"],
25
+ tags: ["database", "table", "view", "dictionary", "func", "user", "role", "policy", "quota", "settingsProfile", "grant"],
26
26
  };
27
27
 
28
28
  export type FoundTemplate = SqlFoundTemplate<SqlTag>;
@@ -138,6 +138,20 @@ export function postgresDeclarations(content: string, fileName = "file.ts"): Pos
138
138
  sqlName = text(node.name.span);
139
139
  kind = "enum type";
140
140
  break;
141
+ case "composite":
142
+ case "range":
143
+ sqlName = text(node.name.span);
144
+ kind = `${node.statement} type`;
145
+ break;
146
+ case "aggregate":
147
+ case "collation":
148
+ sqlName = text(node.name.span);
149
+ kind = node.statement;
150
+ break;
151
+ case "eventTrigger":
152
+ sqlName = text(node.name.span);
153
+ kind = "event trigger";
154
+ break;
141
155
  case "index":
142
156
  sqlName = node.name ? text(node.name.span) : undefined;
143
157
  break;
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, dictionary, func, grant, policy, role, table, user, view } from "../clickhouse/entities";
13
+ import { database, dictionary, func, grant, policy, quota, role, settingsProfile, 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, 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.",
125
+ "Parse one CREATE statement the way its tag does (dialect clickhouse, the default: table, view, database, dictionary, func, user, role, policy, quota, settingsProfile, 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", "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(", ") + ")" },
130
+ tag: { type: "string", enum: [...new Set(["table", "view", "database", "dictionary", "func", "user", "role", "policy", "quota", "settingsProfile", "grant", ...POSTGRES_TAGS])], description: "The tag the statement belongs in (ClickHouse: table, view, database, dictionary, func, user, role, policy, quota, settingsProfile, 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 : 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");
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 === "quota" ? quota : tag === "settingsProfile" ? settingsProfile : tag === "grant" ? grant : undefined;
140
+ if (!build) throw new Error("tag must be table, view, database, dictionary, func, user, role, policy, quota, settingsProfile or grant");
141
141
  const strings = Object.assign([ddl], { raw: [ddl] }) as unknown as TemplateStringsArray;
142
142
  try {
143
143
  const entity = build(strings);