@intentius/chant-lexicon-sql 0.124.19 → 0.124.22

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
@@ -37,7 +37,7 @@ import { ACCESS_ENTITY_TYPES, ACCESS_UNMANAGED_DETAIL, accessOf, readLiveAccess,
37
37
  import { clickhouseQuery } from "../http";
38
38
  import { sqlString } from "../apply/statements";
39
39
  import { stripMarkerFromStatement } from "../ownership";
40
- import { database, dictionary, func, policy, role, table, user, view, CLICKHOUSE_ENTITY_TYPES, type ColumnDef } from "../entities";
40
+ import { database, dictionary, func, policy, quota, role, settingsProfile, table, user, view, CLICKHOUSE_ENTITY_TYPES, type ColumnDef } from "../entities";
41
41
  import { canonicalObject, grantsObject, objectKey, type CanonicalColumn, type CanonicalObject } from "./normalize";
42
42
  import { grantStatement, grantsByGrantee, revokeStatement, type GrantAtom } from "../access";
43
43
  import { renderFor } from "../topology";
@@ -67,7 +67,11 @@ function liveProps(o: LiveObject): Props {
67
67
  ? role
68
68
  : o.type === CLICKHOUSE_ENTITY_TYPES.rowPolicy
69
69
  ? policy
70
- : view;
70
+ : o.type === CLICKHOUSE_ENTITY_TYPES.quota
71
+ ? quota
72
+ : o.type === CLICKHOUSE_ENTITY_TYPES.settingsProfile
73
+ ? settingsProfile
74
+ : view;
71
75
  const strings = Object.assign([o.statement], { raw: [o.statement] }) as unknown as TemplateStringsArray;
72
76
  return { ...(tag(strings).props as unknown as Props) };
73
77
  }
@@ -89,8 +93,10 @@ export function inDeclaredVocabulary(declared: Props, live: Props, d: CanonicalO
89
93
  adopt("name", d.name === l.name);
90
94
  adopt("database", d.database === l.database);
91
95
  adopt("engine", same("engine"));
92
- for (const k of ["orderBy", "primaryKey", "partitionBy", "sampleBy", "ttl", "comment", "select", "refresh", "dataSource", "layout", "lifetime", "range"] as const) adopt(k, same(k));
96
+ for (const k of ["orderBy", "primaryKey", "partitionBy", "sampleBy", "ttl", "comment", "select", "refresh", "dataSource", "layout", "lifetime", "range", "asFunction"] as const) adopt(k, same(k));
93
97
  adopt("settings", same("settings"));
98
+ // A view's DEFINER and SQL SECURITY are not compared by a plan; the server adds its own to a refreshable materialized view (#3911).
99
+ adopt("security", d.kind === "view" || d.kind === "materializedView");
94
100
  // A user's, role's or row policy's fields, compared as `../access.ts` reads them.
95
101
  if (d.kind === "user" || d.kind === "role" || d.kind === "rowPolicy") {
96
102
  for (const k of ["identified", "host", "validUntil", "defaultRole", "defaultDatabase", "grantees", "settings", "as", "for", "using", "to"]) {
@@ -100,6 +106,12 @@ export function inDeclaredVocabulary(declared: Props, live: Props, d: CanonicalO
100
106
  // A user's authentication is the environment's unless the declaration says it.
101
107
  if (d.kind === "user" && declared.identified === undefined) delete out.identified;
102
108
  }
109
+ // A quota's or settings profile's fields; a quota's intervals are one prop, the same when every interval is.
110
+ if (d.kind === "quota" || d.kind === "settingsProfile") {
111
+ const fields = (o: CanonicalObject, pick: (k: string) => boolean) => JSON.stringify(Object.entries(o.access ?? {}).filter(([k]) => pick(k)));
112
+ for (const k of ["keyed", "settings", "to"]) adopt(k, d.access?.[k] === l.access?.[k]);
113
+ adopt("intervals", fields(d, (k) => k.startsWith("interval.")) === fields(l, (k) => k.startsWith("interval.")));
114
+ }
103
115
  // A function's parameters and expression are one lambda: the same lambda is the declaration's words.
104
116
  adopt("params", d.kind === "function" && same("lambda"));
105
117
  adopt("body", d.kind === "function" && same("lambda"));
@@ -108,8 +120,8 @@ export function inDeclaredVocabulary(declared: Props, live: Props, d: CanonicalO
108
120
  adopt("constraints", same("constraints"));
109
121
 
110
122
  const declaredColumns = (declared.columns as ColumnDef[] | undefined) ?? [];
111
- if (d.kind === "view" || d.kind === "materializedView") {
112
- // The server infers a view's columns; a declaration that lists none is not drift.
123
+ if (d.kind === "view" || d.kind === "materializedView" || (d.kind === "table" && d.asFunction !== undefined && declaredColumns.length === 0)) {
124
+ // The server infers a view's columns, and a table function's (#3911); a declaration that lists none is not drift.
113
125
  if (declaredColumns.length === 0) out.columns = [];
114
126
  } else if (d.kind === "table" || d.kind === "dictionary") {
115
127
  const liveColumns = (live.columns as ColumnDef[] | undefined) ?? [];
@@ -124,7 +136,14 @@ export function inDeclaredVocabulary(declared: Props, live: Props, d: CanonicalO
124
136
  }
125
137
 
126
138
  /** The entity types that belong to no database. */
127
- const GLOBAL_TYPES: ReadonlySet<string> = new Set([CLICKHOUSE_ENTITY_TYPES.database, CLICKHOUSE_ENTITY_TYPES.function, CLICKHOUSE_ENTITY_TYPES.user, CLICKHOUSE_ENTITY_TYPES.role]);
139
+ const GLOBAL_TYPES: ReadonlySet<string> = new Set([
140
+ CLICKHOUSE_ENTITY_TYPES.database,
141
+ CLICKHOUSE_ENTITY_TYPES.function,
142
+ CLICKHOUSE_ENTITY_TYPES.user,
143
+ CLICKHOUSE_ENTITY_TYPES.role,
144
+ CLICKHOUSE_ENTITY_TYPES.quota,
145
+ CLICKHOUSE_ENTITY_TYPES.settingsProfile,
146
+ ]);
128
147
 
129
148
  /** Whether the server's formatter prints two expressions the same. A formatter that cannot parse them says no. */
130
149
  async function sameExpression(target: ClickHouseTarget, a: string, b: string): Promise<boolean> {
@@ -179,6 +179,17 @@ function diffObject(key: string, before: CanonicalObject, after: CanonicalObject
179
179
  return;
180
180
  }
181
181
 
182
+ if (before.kind === "quota" || before.kind === "settingsProfile") {
183
+ const rule = before.kind === "quota" ? "SQLCH275" : "SQLCH276";
184
+ const b = before.access ?? {};
185
+ const a = after.access ?? {};
186
+ // In the order the server prints them: the key, each interval by length, the settings, whom it applies to.
187
+ const rank = (f: string) => (f === "keyed" ? 0 : f.startsWith("interval.") ? 1 + Number(f.slice("interval.".length)) / 1e9 : f === "settings" ? 2 : 3);
188
+ const fields = [...new Set([...Object.keys(b), ...Object.keys(a)])].sort((x, y) => rank(x) - rank(y));
189
+ for (const field of fields) if (b[field] !== a[field]) out.push(change(key, field, rule, b[field], a[field]));
190
+ return;
191
+ }
192
+
182
193
  if (before.kind === "grants") {
183
194
  const b = before.access ?? {};
184
195
  const a = after.access ?? {};
@@ -212,6 +223,11 @@ function diffObject(key: string, before: CanonicalObject, after: CanonicalObject
212
223
  return;
213
224
  }
214
225
 
226
+ if (before.asFunction !== undefined || after.asFunction !== undefined) {
227
+ diffTableFunction(key, before, after, out);
228
+ return;
229
+ }
230
+
215
231
  // A table.
216
232
  if (before.engine !== after.engine) out.push(change(key, "engine", "SQLCH223", before.engine, after.engine));
217
233
  const added = diffColumns(key, before, after, out, hints);
@@ -244,6 +260,30 @@ function diffObject(key: string, before: CanonicalObject, after: CanonicalObject
244
260
  diffMap(key, "constraints", before.constraints, after.constraints, "SQLCH215", out);
245
261
  }
246
262
 
263
+ /**
264
+ * A table over a table function (#3911), on either side. It stores nothing,
265
+ * so every change but its comment replaces it (SQLCH219); a table with data
266
+ * of its own becoming one would lose that data, which is a rebuild
267
+ * (SQLCH223). A declaration with no column list takes the columns the server
268
+ * infers, so they are compared only when it lists them.
269
+ */
270
+ function diffTableFunction(key: string, before: CanonicalObject, after: CanonicalObject, out: Change[]): void {
271
+ if (before.asFunction === undefined) {
272
+ out.push(change(key, "asFunction", "SQLCH223", before.engine, `AS ${after.asFunction}`));
273
+ return;
274
+ }
275
+ if (before.asFunction !== after.asFunction) out.push(change(key, "asFunction", "SQLCH219", before.asFunction, after.asFunction));
276
+ if (after.asFunction !== undefined && after.columns.length === 0) return;
277
+ const column = (c: CanonicalColumn) => [c.type, c.nullable ? "NULL" : "", c.defaultKind ?? "", c.defaultExpr ?? "", c.codec ? `CODEC(${c.codec})` : "", c.comment ? `COMMENT ${c.comment}` : ""].filter(Boolean).join(" ");
278
+ const b = new Map(before.columns.map((c) => [c.name, column(c)]));
279
+ const a = new Map(after.columns.map((c) => [c.name, column(c)]));
280
+ for (const name of new Set([...b.keys(), ...a.keys()])) {
281
+ if (b.get(name) !== a.get(name)) out.push(change(key, `columns.${name}`, "SQLCH219", b.get(name), a.get(name)));
282
+ }
283
+ const order = (o: CanonicalObject) => o.columns.map((c) => c.name).join(", ");
284
+ if (order(before) !== order(after) && a.size === b.size && [...a.keys()].every((n) => b.has(n))) out.push(change(key, "columns.order", "SQLCH219", order(before), order(after)));
285
+ }
286
+
247
287
  /** A dictionary: every change but its comment replaces it (SQLCH245). */
248
288
  function diffDictionary(key: string, before: CanonicalObject, after: CanonicalObject, out: Change[]): void {
249
289
  const attribute = (c: CanonicalColumn) =>
@@ -272,7 +312,7 @@ export function diffSchemas(before: readonly SchemaObject[], after: readonly Sch
272
312
  const matches = matchByIdentity(before, after, {
273
313
  qualified,
274
314
  // A function, a user, a role, a row policy and a grantee's grants are not renamed: one under a new name is a create.
275
- previously: (o) => (o.kind === "function" || o.kind === "user" || o.kind === "role" || o.kind === "rowPolicy" || o.kind === "grants" ? undefined : o.previously),
315
+ previously: (o) => (o.kind === "function" || o.kind === "user" || o.kind === "role" || o.kind === "rowPolicy" || o.kind === "quota" || o.kind === "settingsProfile" || o.kind === "grants" ? undefined : o.previously),
276
316
  previousNames: (o, prev) => [prev, `${o.database}.${prev}`],
277
317
  });
278
318
  for (const m of matches) {
@@ -41,7 +41,7 @@ import { accessCanonical, grantsCanonical, isAccessStatement } from "../access";
41
41
 
42
42
  export { previouslyIn } from "../../core/normalize";
43
43
 
44
- export type ObjectKind = "database" | "table" | "view" | "materializedView" | "dictionary" | "function" | "user" | "role" | "rowPolicy" | "grants";
44
+ export type ObjectKind = "database" | "table" | "view" | "materializedView" | "dictionary" | "function" | "user" | "role" | "rowPolicy" | "quota" | "settingsProfile" | "grants";
45
45
 
46
46
  export interface CanonicalColumn {
47
47
  name: string;
@@ -84,6 +84,8 @@ export interface CanonicalObject {
84
84
  select?: string;
85
85
  to?: string;
86
86
  refresh?: string;
87
+ /** A table over a table function (#3911): the call after `AS`. */
88
+ asFunction?: string;
87
89
  /** A dictionary's `SOURCE`, `LAYOUT`, `LIFETIME` and `RANGE`, each what is inside the parentheses. */
88
90
  dataSource?: string;
89
91
  layout?: string;
@@ -94,8 +96,9 @@ export interface CanonicalObject {
94
96
  /** A row policy's table. */
95
97
  table?: string;
96
98
  /**
97
- * A user's, role's or row policy's fields (`../access.ts`), each absent at
98
- * the server's default; a grantee's grants, by atom.
99
+ * A user's, role's, row policy's, quota's or settings profile's fields
100
+ * (`../access.ts`), each absent at the server's default; a grantee's
101
+ * grants, by atom.
99
102
  */
100
103
  access?: Record<string, string>;
101
104
  /** `-- previously: <name>` before the statement: the object's previous name. */
@@ -374,6 +377,7 @@ export function canonicalObject(ddl: string, defaultDatabase = "default"): Canon
374
377
  }
375
378
 
376
379
  if (node.statement === "table") {
380
+ if (node.asFunction) obj.asFunction = canonicalExpression(spanText(tokens, node.asFunction) ?? "", database);
377
381
  node.columns.forEach((c, i) => obj.columns.push(column(tokens, c, i, database)));
378
382
  for (const ix of node.indexes) {
379
383
  obj.indexes[ix.name] = [expr(ix.expr), "TYPE", expr(ix.type), ix.granularity ? `GRANULARITY ${expr(ix.granularity)}` : ""]
@@ -556,6 +560,8 @@ export function objectKey(o: { kind?: ObjectKind; type?: string; database?: stri
556
560
  if (kind === "function") return `function ${o.name}`;
557
561
  if (kind === "user") return `user ${o.name}`;
558
562
  if (kind === "role") return `role ${o.name}`;
563
+ if (kind === "quota") return `quota ${o.name}`;
564
+ if (kind === "settingsProfile") return `settings profile ${o.name}`;
559
565
  if (kind === "grants") return `grants ${o.name}`;
560
566
  if (kind === "rowPolicy") return `row policy ${o.name} ON ${o.database}.${o.table}`;
561
567
  return o.database !== undefined ? `${o.database}.${o.name}` : o.name;
@@ -566,6 +572,8 @@ const KIND_OF_TYPE: Record<string, ObjectKind> = {
566
572
  "ClickHouse::User": "user",
567
573
  "ClickHouse::Role": "role",
568
574
  "ClickHouse::RowPolicy": "rowPolicy",
575
+ "ClickHouse::Quota": "quota",
576
+ "ClickHouse::SettingsProfile": "settingsProfile",
569
577
  "ClickHouse::Grant": "grants",
570
578
  };
571
579
 
@@ -582,7 +590,7 @@ export function grantsObject(ddl: string, grantee: string, defaultDatabase = "de
582
590
  */
583
591
  export function scopeOf(o: { kind?: ObjectKind; type?: string; database?: string; name: string; table?: string }): string {
584
592
  const kind = o.kind ?? KIND_OF_TYPE[o.type ?? ""];
585
- if (kind === "function" || kind === "user" || kind === "role" || kind === "grants") return objectKey(o);
593
+ if (kind === "function" || kind === "user" || kind === "role" || kind === "quota" || kind === "settingsProfile" || kind === "grants") return objectKey(o);
586
594
  return o.database ?? o.name;
587
595
  }
588
596
 
@@ -35,7 +35,7 @@ const DOCS = "https://clickhouse.com/docs/sql-reference/statements";
35
35
  const rule = (id: string, cls: ChangeClass, title: string, restriction: string, cite: string): ClassifierRule => classifierRule(id, cls, title, restriction, cite);
36
36
 
37
37
  export const CLASSIFIER_RULES = {
38
- SQLCH200: rule("SQLCH200", "create", "Create an object", "A new database, table, view, dictionary, function, user, role or row policy is created, or a grantee gets its first grants; nothing existing changes.", `${DOCS}/create`),
38
+ SQLCH200: rule("SQLCH200", "create", "Create an object", "A new database, table, view, dictionary, function, user, role, row policy, quota or settings profile is created, or a grantee gets its first grants; nothing existing changes.", `${DOCS}/create`),
39
39
  SQLCH201: rule(
40
40
  "SQLCH201",
41
41
  "metadata",
@@ -150,6 +150,13 @@ export const CLASSIFIER_RULES = {
150
150
  "A table setting the server marks read-only (such as index_granularity) is fixed when the table is created and needs a new table to change.",
151
151
  `${DOCS}/alter/setting`,
152
152
  ),
153
+ SQLCH219: rule(
154
+ "SQLCH219",
155
+ "metadata",
156
+ "Replace a table over a table function",
157
+ "A table declared AS a table function (numbers, remote, s3, url) stores no data of its own and has no ALTER for its function; CREATE OR REPLACE TABLE replaces its call and columns, and queries read through the new one.",
158
+ `${DOCS}/create/table#from-a-table-function`,
159
+ ),
153
160
  SQLCH220: rule(
154
161
  "SQLCH220",
155
162
  "rebuild",
@@ -278,6 +285,20 @@ export const CLASSIFIER_RULES = {
278
285
  "REVOKE takes a privilege or a role away, including one granted by hand to a grantee the build declares grants for; queries that relied on it fail from then on.",
279
286
  `${DOCS}/revoke`,
280
287
  ),
288
+ SQLCH275: rule(
289
+ "SQLCH275",
290
+ "metadata",
291
+ "Change a quota",
292
+ "ALTER QUOTA sets the quota's key, one interval's limits (NO LIMITS takes the interval away) or whom it applies to, on the quota that is there, so what names it still does.",
293
+ `${DOCS}/alter/quota`,
294
+ ),
295
+ SQLCH276: rule(
296
+ "SQLCH276",
297
+ "metadata",
298
+ "Change a settings profile",
299
+ "ALTER SETTINGS PROFILE replaces the profile's settings, constraints and inherited profiles, or whom it applies to; the users, roles and profiles that name it keep it. Sessions already open keep the old settings.",
300
+ `${DOCS}/alter/settings-profile`,
301
+ ),
281
302
  SQLCH290: rule(
282
303
  "SQLCH290",
283
304
  "rebuild",
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Quotas and settings profiles (#3911): the `quota` and `settingsProfile`
3
+ * tags, each compared with what the pinned server prints for it, the
4
+ * `ALTER` each change takes, `ON CLUSTER` per topology, and reading them
5
+ * from a `.sql` file. `live/quota-profile.e2e.test.ts` runs them against a
6
+ * server.
7
+ */
8
+
9
+ import { describe, expect, test } from "vitest";
10
+ import { quota, role, settingsProfile, CLICKHOUSE_ENTITY_TYPES } from "./entities";
11
+ import { canonicalObject, objectKey } from "./plan/normalize";
12
+ import { diffSchemas } from "./plan/diff";
13
+ import { planStatements, type DeclaredObject } from "./apply/statements";
14
+ import { renderStatement } from "./topology";
15
+ import { readClickHouse } from "../files/clickhouse";
16
+ import { clickhouseTemplates } from "./import/generator";
17
+
18
+ const t = (s: string) => [s] as unknown as TemplateStringsArray;
19
+
20
+ describe("the tags", () => {
21
+ test("a quota: its key, its intervals as written, whom it applies to", () => {
22
+ const reader = role`CREATE ROLE reader`;
23
+ const q = quota`CREATE QUOTA IF NOT EXISTS per_user KEYED BY user_name FOR INTERVAL 1 hour MAX queries = 100, errors = 5, FOR RANDOMIZED INTERVAL 1 day TRACKING ONLY TO ${reader}`;
24
+ expect(q.entityType).toBe("ClickHouse::Quota");
25
+ expect(q.props).toMatchObject({
26
+ name: "per_user",
27
+ ifNotExists: true,
28
+ keyed: "user_name",
29
+ intervals: "FOR INTERVAL 1 hour MAX queries = 100, errors = 5, FOR RANDOMIZED INTERVAL 1 day TRACKING ONLY",
30
+ to: "reader",
31
+ });
32
+ expect(q.props.ddl).toBe("CREATE QUOTA IF NOT EXISTS per_user KEYED BY user_name FOR INTERVAL 1 hour MAX queries = 100, errors = 5, FOR RANDOMIZED INTERVAL 1 day TRACKING ONLY TO reader");
33
+ });
34
+
35
+ test("a settings profile, written either way, inheriting another", () => {
36
+ const base = settingsProfile`CREATE SETTINGS PROFILE base SETTINGS max_threads = 8`;
37
+ const p = settingsProfile`CREATE PROFILE analysts SETTINGS PROFILE ${base}, max_memory_usage = 10000000000 MIN 1000000 MAX 20000000000, readonly = 1 CONST TO ALL EXCEPT admin`;
38
+ expect(p.entityType).toBe("ClickHouse::SettingsProfile");
39
+ expect(p.props).toMatchObject({ name: "analysts", settings: "PROFILE base, max_memory_usage = 10000000000 MIN 1000000 MAX 20000000000, readonly = 1 CONST", to: "ALL EXCEPT admin" });
40
+ expect(p.dependsOn).toContain(base);
41
+ });
42
+
43
+ test("one per template, the wrong tag, and malformed intervals are refused", () => {
44
+ expect(() => quota(t("CREATE QUOTA a, b"))).toThrow(/one quota per template/);
45
+ expect(() => settingsProfile(t("CREATE SETTINGS PROFILE a, b"))).toThrow(/one settings profile per template/);
46
+ expect(() => quota(t("CREATE SETTINGS PROFILE p"))).toThrow(/holds a CREATE SETTINGS PROFILE; use the settingsProfile tag/);
47
+ expect(() => settingsProfile(t("CREATE QUOTA q"))).toThrow(/holds a CREATE QUOTA; use the quota tag/);
48
+ expect(() => role(t("CREATE QUOTA q"))).toThrow(/use the quota tag/);
49
+ expect(() => quota(t("CREATE QUOTA q KEYED BY user_name NOT KEYED"))).toThrow(/both written/);
50
+ expect(() => quota(t("CREATE QUOTA q FOR INTERVAL 1 hour MAX queries = 1 TO r FOR INTERVAL 1 day MAX queries = 2"))).toThrow(/FOR is written twice/);
51
+ expect(() => canonicalObject("CREATE QUOTA q FOR INTERVAL 1 fortnight MAX queries = 1")).toThrow(/whole number and a unit/);
52
+ });
53
+ });
54
+
55
+ describe("compared with what the server prints", () => {
56
+ const same = (declared: string, shown: string) => diffSchemas([{ key: "k", canonical: canonicalObject(shown) }], [{ key: "k", canonical: canonicalObject(declared) }]).changes;
57
+
58
+ test("a quota: units in seconds, limits sorted and lower case, numbers as the server keeps them, NO LIMITS as no interval", () => {
59
+ expect(
60
+ same(
61
+ "CREATE QUOTA q KEYED BY 'user_name' FOR INTERVAL 60 MINUTE MAX QUERIES 100, errors = 5, FOR RANDOMIZED INTERVAL 1 DAY MAX execution_time = 1000.0, read_bytes = 1e9, FOR INTERVAL 1 week NO LIMITS TO r1",
62
+ "CREATE QUOTA q KEYED BY user_name FOR INTERVAL 1 hour MAX queries = 100, errors = 5, FOR RANDOMIZED INTERVAL 1 day MAX read_bytes = 1000000000, execution_time = 1000 TO r1",
63
+ ),
64
+ ).toEqual([]);
65
+ // Three months are a quarter, as the server counts them.
66
+ expect(same("CREATE QUOTA q FOR INTERVAL 3 month MAX queries = 1", "CREATE QUOTA q FOR INTERVAL 1 quarter MAX queries = 1")).toEqual([]);
67
+ expect(same("CREATE QUOTA q NOT KEYED TO NONE", "CREATE QUOTA q")).toEqual([]);
68
+ // `KEY BY` is `KEYED BY`; an IP key's prefix bits are part of the key.
69
+ expect(same("CREATE QUOTA q KEY BY ip_address ipv6_prefix_bits 64", "CREATE QUOTA q KEYED BY ip_address IPV6_PREFIX_BITS 64")).toEqual([]);
70
+ expect(canonicalObject("CREATE QUOTA q KEYED BY ip_address ipv4_prefix_bits 16 ipv6_prefix_bits 48").access).toEqual({ keyed: "ip_address ipv4_prefix_bits 16 ipv6_prefix_bits 48" });
71
+ // A name may start with a digit, as the server's own tests write them.
72
+ expect(canonicalObject("CREATE SETTINGS PROFILE 02294_profile1 SETTINGS max_execution_time = 0.5 TO default").name).toBe("02294_profile1");
73
+ expect(canonicalObject("CREATE QUOTA `q x` KEYED BY client_key, user_name FOR INTERVAL 2 week MAX queries = 1 TO ALL").access).toEqual({
74
+ keyed: "client_key , user_name",
75
+ "interval.1209600": "MAX queries = 1",
76
+ to: "ALL",
77
+ });
78
+ });
79
+
80
+ test("a changed quota is SQLCH275 per field and per interval", () => {
81
+ expect(
82
+ same(
83
+ "CREATE QUOTA q KEYED BY ip_address FOR INTERVAL 1 hour MAX queries = 7, FOR INTERVAL 1 day TRACKING ONLY",
84
+ "CREATE QUOTA q KEYED BY user_name FOR INTERVAL 1 hour MAX queries = 5, errors = 2, FOR INTERVAL 1 week MAX queries = 1 TO r",
85
+ ).map((c) => [c.field, c.rule, c.before, c.after]),
86
+ ).toEqual([
87
+ ["keyed", "SQLCH275", "user_name", "ip_address"],
88
+ ["interval.3600", "SQLCH275", "MAX errors = 2 , queries = 5", "MAX queries = 7"],
89
+ ["interval.86400", "SQLCH275", undefined, "TRACKING ONLY"],
90
+ ["interval.604800", "SQLCH275", "MAX queries = 1", undefined],
91
+ ["to", "SQLCH275", "r", undefined],
92
+ ]);
93
+ });
94
+
95
+ test("a settings profile: READONLY as CONST, PROFILE 'x' as INHERIT x, the name quoted or not", () => {
96
+ expect(
97
+ same(
98
+ "CREATE PROFILE p2 SETTINGS PROFILE 'p1', readonly = 1, max_threads READONLY, max_memory_usage = 100 MIN 90 MAX 110 TO b, a",
99
+ "CREATE SETTINGS PROFILE `p2` SETTINGS INHERIT `p1`, readonly = 1, max_memory_usage = 100 MIN 90 MAX 110, max_threads CONST TO a, b",
100
+ ),
101
+ ).toEqual([]);
102
+ expect(same("CREATE SETTINGS PROFILE p SETTINGS max_threads = 4", "CREATE SETTINGS PROFILE `p` SETTINGS max_threads = 5 TO r").map((c) => [c.field, c.rule])).toEqual([
103
+ ["settings", "SQLCH276"],
104
+ ["to", "SQLCH276"],
105
+ ]);
106
+ });
107
+ });
108
+
109
+ describe("the statements", () => {
110
+ const decl = (type: string, ddl: string): DeclaredObject => {
111
+ const canonical = canonicalObject(ddl);
112
+ return { exportName: "x", type: type as DeclaredObject["type"], key: objectKey(canonical), ddl, canonical, dependsOn: [] };
113
+ };
114
+ const steps = (obj: DeclaredObject, shown?: string) => {
115
+ const live = shown === undefined ? undefined : canonicalObject(shown);
116
+ const changes = diffSchemas(live ? [{ key: obj.key, canonical: live }] : [], [{ key: obj.key, canonical: obj.canonical }]).changes;
117
+ const entry = planStatements({ declared: [obj], changes, current: new Map(live ? [[obj.key, live]] : []) }).objects[0]!;
118
+ return entry.verdict === "create" || entry.verdict === "alter" ? entry.steps.map((s) => s.sql) : [entry.verdict];
119
+ };
120
+
121
+ test("a quota is created as declared, then changed with ALTER QUOTA, never replaced", () => {
122
+ const q = decl(CLICKHOUSE_ENTITY_TYPES.quota, "CREATE QUOTA q KEYED BY ip_address FOR RANDOMIZED INTERVAL 1 hour MAX queries = 7, errors = 1 TO r");
123
+ expect(q.key).toBe("quota q");
124
+ expect(steps(q)).toEqual(["CREATE QUOTA q KEYED BY ip_address FOR RANDOMIZED INTERVAL 1 hour MAX queries = 7, errors = 1 TO r"]);
125
+ expect(steps(q, "CREATE QUOTA q FOR INTERVAL 1 hour MAX queries = 5, FOR INTERVAL 1 day MAX queries = 100")).toEqual([
126
+ "ALTER QUOTA `q` KEYED BY ip_address",
127
+ "ALTER QUOTA `q` FOR RANDOMIZED INTERVAL 3600 second MAX errors = 1, queries = 7",
128
+ "ALTER QUOTA `q` FOR INTERVAL 86400 second NO LIMITS",
129
+ "ALTER QUOTA `q` TO r",
130
+ ]);
131
+ expect(steps(decl(CLICKHOUSE_ENTITY_TYPES.quota, "CREATE QUOTA q"), "CREATE QUOTA q KEYED BY user_name TO r")).toEqual(["ALTER QUOTA `q` NOT KEYED", "ALTER QUOTA `q` TO NONE"]);
132
+ });
133
+
134
+ test("a settings profile is changed with ALTER SETTINGS PROFILE, so what inherits it keeps it", () => {
135
+ const p = decl(CLICKHOUSE_ENTITY_TYPES.settingsProfile, "CREATE SETTINGS PROFILE p SETTINGS max_threads = 4 TO r");
136
+ expect(p.key).toBe("settings profile p");
137
+ expect(steps(p, "CREATE SETTINGS PROFILE `p` SETTINGS max_threads = 8")).toEqual(["ALTER SETTINGS PROFILE `p` SETTINGS max_threads = 4", "ALTER SETTINGS PROFILE `p` TO r"]);
138
+ expect(steps(decl(CLICKHOUSE_ENTITY_TYPES.settingsProfile, "CREATE PROFILE p"), "CREATE SETTINGS PROFILE `p` SETTINGS max_threads = 8 TO r")).toEqual([
139
+ "ALTER SETTINGS PROFILE `p` SETTINGS NONE",
140
+ "ALTER SETTINGS PROFILE `p` TO NONE",
141
+ ]);
142
+ });
143
+
144
+ test("neither is ever dropped", () => {
145
+ const live = [
146
+ { key: "quota q", canonical: canonicalObject("CREATE QUOTA q") },
147
+ { key: "settings profile p", canonical: canonicalObject("CREATE SETTINGS PROFILE p") },
148
+ ];
149
+ const gone = diffSchemas(live, []).changes;
150
+ expect(planStatements({ declared: [], changes: gone, current: new Map(live.map((l) => [l.key, l.canonical])), allowDestructive: true }).drops).toEqual([]);
151
+ });
152
+
153
+ test("ON CLUSTER on a cluster, since neither is in a database", () => {
154
+ const cluster = { kind: "cluster" as const, cluster: "main" };
155
+ expect(renderStatement("CREATE QUOTA q KEYED BY user_name", cluster)).toBe("CREATE QUOTA q ON CLUSTER `main` KEYED BY user_name");
156
+ expect(renderStatement("CREATE SETTINGS PROFILE p SETTINGS max_threads = 1", cluster)).toBe("CREATE SETTINGS PROFILE p ON CLUSTER `main` SETTINGS max_threads = 1");
157
+ expect(renderStatement("CREATE PROFILE p", cluster)).toBe("CREATE PROFILE p ON CLUSTER `main`");
158
+ expect(renderStatement("ALTER QUOTA `q` TO r", cluster)).toBe("ALTER QUOTA `q` ON CLUSTER `main` TO r");
159
+ expect(renderStatement("ALTER SETTINGS PROFILE `p` SETTINGS NONE", cluster)).toBe("ALTER SETTINGS PROFILE `p` ON CLUSTER `main` SETTINGS NONE");
160
+ });
161
+ });
162
+
163
+ describe("import", () => {
164
+ test("a .sql file's quotas and settings profiles are read by their tags", () => {
165
+ const problems: string[] = [];
166
+ const objects = readClickHouse(
167
+ "CREATE SETTINGS PROFILE `p` SETTINGS max_threads = 4;\nCREATE QUOTA q KEYED BY user_name FOR INTERVAL 1 hour MAX queries = 5 TO r;\nCREATE NAMED COLLECTION c AS password = 'x';\n",
168
+ { origin: "access.sql" },
169
+ problems,
170
+ );
171
+ expect(objects.map((o) => [o.type, o.name])).toEqual([
172
+ ["ClickHouse::SettingsProfile", "p"],
173
+ ["ClickHouse::Quota", "q"],
174
+ ]);
175
+ expect(problems).toEqual([expect.stringMatching(/^named collection: chant cannot declare it \(a named collection holds credentials/)]);
176
+ });
177
+
178
+ test("the generator writes them with their tags, never as a relation another object names", () => {
179
+ const items = [
180
+ { exportName: "q", type: CLICKHOUSE_ENTITY_TYPES.quota, name: "q", ddl: "CREATE QUOTA q TO events" },
181
+ { exportName: "events", type: CLICKHOUSE_ENTITY_TYPES.table, database: "default", name: "events", ddl: "CREATE TABLE default.events (a UInt8) ENGINE = Log" },
182
+ ];
183
+ const { bodies } = clickhouseTemplates(items);
184
+ expect(bodies.get("q")!.refs).toEqual([]);
185
+ });
186
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * A table over a table function (#3911): `CREATE TABLE t AS numbers(10)`,
3
+ * with or without its columns, as the tag reads it, as the server prints it
4
+ * back, and the one statement a change takes. `live/kinds.e2e.test.ts` runs
5
+ * it against a server.
6
+ */
7
+
8
+ import { describe, expect, test } from "vitest";
9
+ import { table, CLICKHOUSE_ENTITY_TYPES } from "./entities";
10
+ import { canonicalObject, objectKey } from "./plan/normalize";
11
+ import { diffSchemas } from "./plan/diff";
12
+ import { planStatements, type DeclaredObject } from "./apply/statements";
13
+ import { renderStatement } from "./topology";
14
+
15
+ const shown = "CREATE TABLE default.nums\n(\n `number` UInt64\n) AS numbers(10)\nCOMMENT 'counting'";
16
+
17
+ describe("a table over a table function", () => {
18
+ test("the tag reads the call, and the columns when written", () => {
19
+ const bare = table`CREATE TABLE default.nums AS numbers(10) COMMENT 'counting'`;
20
+ expect(bare.props).toMatchObject({ name: "nums", database: "default", asFunction: "numbers(10)", columns: [], comment: "counting" });
21
+ expect(bare.props.engine).toBeUndefined();
22
+ const typed = table`CREATE TABLE default.nums (number UInt64) AS numbers(10)`;
23
+ expect(typed.props.columns.map((c) => [c.name, c.type])).toEqual([["number", "UInt64"]]);
24
+ });
25
+
26
+ test("a declaration with no columns agrees with the columns the server inferred; one that lists them is compared on them", () => {
27
+ const same = (declared: string) => diffSchemas([{ key: "k", canonical: canonicalObject(shown) }], [{ key: "k", canonical: canonicalObject(declared) }]).changes;
28
+ expect(same("CREATE TABLE default.nums AS numbers(10) COMMENT 'counting'")).toEqual([]);
29
+ expect(same("CREATE TABLE default.nums (number UInt64) AS numbers(10) COMMENT 'counting'")).toEqual([]);
30
+ expect(same("CREATE TABLE default.nums (number UInt32) AS numbers(20) COMMENT 'counting'").map((c) => [c.field, c.rule, c.class])).toEqual([
31
+ ["asFunction", "SQLCH219", "metadata"],
32
+ ["columns.number", "SQLCH219", "metadata"],
33
+ ]);
34
+ });
35
+
36
+ test("a change replaces it whole, its comment with it; a table with data becoming one is a rebuild", () => {
37
+ const ddl = "CREATE TABLE default.nums AS numbers(20)";
38
+ const canonical = canonicalObject(ddl);
39
+ const obj: DeclaredObject = { exportName: "nums", type: CLICKHOUSE_ENTITY_TYPES.table, key: objectKey(canonical), ddl, canonical, dependsOn: [] };
40
+ const live = canonicalObject(shown);
41
+ const changes = diffSchemas([{ key: obj.key, canonical: live }], [{ key: obj.key, canonical }]).changes;
42
+ expect(changes.map((c) => c.field)).toEqual(["comment", "asFunction"]);
43
+ const entry = planStatements({ declared: [obj], changes, current: new Map([[obj.key, live]]) }).objects[0]!;
44
+ expect(entry.verdict === "alter" ? entry.steps.map((s) => s.sql) : []).toEqual(["CREATE OR REPLACE TABLE default.nums AS numbers(20) COMMENT '[chant managed-by=chant]'"]);
45
+
46
+ const stored = canonicalObject("CREATE TABLE default.nums (number UInt64) ENGINE = MergeTree ORDER BY number");
47
+ const rebuild = diffSchemas([{ key: obj.key, canonical: stored }], [{ key: obj.key, canonical }]).changes;
48
+ expect(rebuild.map((c) => [c.field, c.rule, c.class])).toEqual([["asFunction", "SQLCH223", "rebuild"]]);
49
+ });
50
+
51
+ test("ON CLUSTER after the name", () => {
52
+ expect(renderStatement("CREATE TABLE default.nums AS numbers(10)", { kind: "cluster", cluster: "main" })).toBe("CREATE TABLE default.nums ON CLUSTER `main` AS numbers(10)");
53
+ });
54
+ });
@@ -22,6 +22,14 @@
22
22
  "YEARS"
23
23
  ],
24
24
  "failures": [
25
+ "CREATE TABLE ... AS <table> is not declared: the server copies the other table's columns and engine and keeps no link to it. Declare them (a composite can share them), or write AS <table function>(...) for a table over a table function at 'AS'",
26
+ "CREATE TABLE ... AS <table> is not declared: the server copies the other table's columns and engine and keeps no link to it. Declare them (a composite can share them), or write AS <table function>(...) for a table over a table function at 'as'",
27
+ "CREATE TABLE ... AS SELECT is not declared: it creates the table and fills it once, and the server keeps the columns, not the query. Declare the table's columns and fill it with INSERT ... SELECT, or declare a materialized view at 'AS'",
28
+ "CREATE TABLE ... AS SELECT is not declared: it creates the table and fills it once, and the server keeps the columns, not the query. Declare the table's columns and fill it with INSERT ... SELECT, or declare a materialized view at 'as'",
29
+ "CREATE TABLE ... CLONE AS is not declared: it copies another table's columns, engine and data once. Declare the columns and engine at 'CLONE'",
30
+ "CREATE TABLE ... EMPTY AS SELECT is not declared: the server keeps the query's columns, not the query. Declare the columns at 'empty'",
31
+ "declare one quota per template at ','",
32
+ "declare one settings profile per template at ','",
25
33
  "expected ')' at 'WITH'",
26
34
  "expected '='",
27
35
  "expected '=' at '('",
@@ -48,20 +56,15 @@
48
56
  "expected an expression",
49
57
  "expected an expression at ')'",
50
58
  "expected an expression at 'TYPE'",
51
- "expected the column list (CREATE TABLE ... AS is not declared here) at 'AS'",
52
- "expected the column list (CREATE TABLE ... AS is not declared here) at 'CLONE'",
53
- "expected the column list (CREATE TABLE ... AS is not declared here) at 'COMMENT'",
54
- "expected the column list (CREATE TABLE ... AS is not declared here) at 'ENGINE'",
55
- "expected the column list (CREATE TABLE ... AS is not declared here) at 'Engine'",
56
- "expected the column list (CREATE TABLE ... AS is not declared here) at 'ORDER'",
57
- "expected the column list (CREATE TABLE ... AS is not declared here) at 'SQL'",
58
- "expected the column list (CREATE TABLE ... AS is not declared here) at 'TO'",
59
- "expected the column list (CREATE TABLE ... AS is not declared here) at 'as'",
60
- "expected the column list (CREATE TABLE ... AS is not declared here) at 'empty'",
61
- "expected the column list (CREATE TABLE ... AS is not declared here) at 'engine'",
59
+ "expected the column list (a table declared without one, CREATE TABLE ... ENGINE = ... AS SELECT, is not declared: declare the columns) at 'COMMENT'",
60
+ "expected the column list (a table declared without one, CREATE TABLE ... ENGINE = ... AS SELECT, is not declared: declare the columns) at 'ENGINE'",
61
+ "expected the column list (a table declared without one, CREATE TABLE ... ENGINE = ... AS SELECT, is not declared: declare the columns) at 'Engine'",
62
+ "expected the column list (a table declared without one, CREATE TABLE ... ENGINE = ... AS SELECT, is not declared: declare the columns) at 'ORDER'",
63
+ "expected the column list (a table declared without one, CREATE TABLE ... ENGINE = ... AS SELECT, is not declared: declare the columns) at 'SQL'",
64
+ "expected the column list (a table declared without one, CREATE TABLE ... ENGINE = ... AS SELECT, is not declared: declare the columns) at 'TO'",
65
+ "expected the column list (a table declared without one, CREATE TABLE ... ENGINE = ... AS SELECT, is not declared: declare the columns) at 'engine'",
62
66
  "unexpected end of statement",
63
- "unexpected token after the table definition at 'AS'",
64
- "unexpected token after the table definition at 'SETTINGS'",
65
- "unexpected token after the table definition at 'as'"
67
+ "unexpected ipv6_prefix_bits: expected IN, KEYED BY, KEY BY, NOT KEYED, FOR, TO at 'ipv6_prefix_bits'",
68
+ "unexpected token after the table definition at 'SETTINGS'"
66
69
  ]
67
70
  }
@@ -352,7 +352,7 @@ function renderOtherCreate(tokens: Token[], topology: Topology): string {
352
352
  if (w.accept("OR")) w.accept("REPLACE");
353
353
  w.accept("TEMPORARY");
354
354
  // Access control (`./access.ts`) belongs to no database: like a database's own DDL, it reaches every replica through the cluster.
355
- if (w.accept("USER", "ROLE", "POLICY", "QUOTA") || w.acceptSeq("ROW", "POLICY") || w.acceptSeq("SETTINGS", "PROFILE")) {
355
+ if (w.accept("USER", "ROLE", "POLICY", "QUOTA", "PROFILE") || w.acceptSeq("ROW", "POLICY") || w.acceptSeq("SETTINGS", "PROFILE")) {
356
356
  w.acceptSeq("IF", "NOT", "EXISTS") || w.acceptSeq("OR", "REPLACE");
357
357
  if (!w.name()) return applyEdits(tokens, []);
358
358
  const edit = onClusterEdit(w, databaseCluster(topology, true), w.end());
@@ -430,7 +430,9 @@ export function renderStatement(sql: string, topology: Topology): string {
430
430
  if (word(head, "DROP", "DETACH", "ATTACH")) w.accept("TEMPORARY");
431
431
  if (w.accept("DATABASE")) return renderAfterName(tokens, w, databaseCluster(topology, word(head, "DROP")));
432
432
  // A function and access control belong to no database: like the database's own DDL, they reach every replica through the cluster.
433
- if (w.accept("FUNCTION", "USER", "ROLE", "POLICY") || w.acceptSeq("ROW", "POLICY")) return renderAfterName(tokens, w, databaseCluster(topology, true));
433
+ if (w.accept("FUNCTION", "USER", "ROLE", "POLICY", "QUOTA", "PROFILE") || w.acceptSeq("ROW", "POLICY") || w.acceptSeq("SETTINGS", "PROFILE")) {
434
+ return renderAfterName(tokens, w, databaseCluster(topology, true));
435
+ }
434
436
  // `TRUNCATE t` may leave out TABLE.
435
437
  if (w.accept("TABLE", "VIEW", "DICTIONARY") || word(head, "TRUNCATE")) return renderAfterName(tokens, w, objectCluster(topology));
436
438
  return sql;
package/src/clickhouse.ts CHANGED
@@ -18,6 +18,8 @@ export {
18
18
  role,
19
19
  policy,
20
20
  grant,
21
+ quota,
22
+ settingsProfile,
21
23
  literal,
22
24
  SqlLiteral,
23
25
  SqlTemplateError,
@@ -35,6 +37,10 @@ export {
35
37
  type ClickHouseRole,
36
38
  type ClickHouseRowPolicy,
37
39
  type ClickHouseGrant,
40
+ type ClickHouseQuota,
41
+ type ClickHouseSettingsProfile,
42
+ type QuotaProps,
43
+ type SettingsProfileProps,
38
44
  type UserProps,
39
45
  type RoleProps,
40
46
  type RowPolicyProps,
@@ -72,7 +72,7 @@ Two rules change class across the supported majors: a STORED generated column's
72
72
 
73
73
  ## Identity and renames
74
74
 
75
- An object is identified by its export name between two builds, and by its schema-qualified name against a server, which has no export names. Tables, views, materialized views, sequences and indexes share one namespace per schema, types and domains another, and schemas and extensions are matched by name. Functions and procedures share a namespace of their own and are told apart by their input parameter types, so an overload is another object; a trigger is matched by its name on its table.
75
+ An object is identified by its export name between two builds, and by its schema-qualified name against a server, which has no export names. Tables, views, materialized views, sequences and indexes share one namespace per schema, types and domains another, and schemas and extensions are matched by name. Functions, procedures and aggregates share a namespace of their own and are told apart by their input parameter types, so an overload is another object; composite and range types share the types' namespace, collations have one of their own, a cast is named by its two types and an event trigger by its name in the database; a trigger is matched by its name on its table.
76
76
 
77
77
  A rename is declared where it happens. Before a CREATE, \`-- previously: <old name>\` says the object had another name; on a column's line, it says the column did:
78
78
 
@@ -23,6 +23,8 @@ const KINDS: Record<string, string> = {
23
23
  Role: CLICKHOUSE_ENTITY_TYPES.role,
24
24
  RowPolicy: CLICKHOUSE_ENTITY_TYPES.rowPolicy,
25
25
  Grant: CLICKHOUSE_ENTITY_TYPES.grant,
26
+ Quota: CLICKHOUSE_ENTITY_TYPES.quota,
27
+ SettingsProfile: CLICKHOUSE_ENTITY_TYPES.settingsProfile,
26
28
  PostgresSchema: POSTGRES_ENTITY_TYPES.schema,
27
29
  PostgresTable: POSTGRES_ENTITY_TYPES.table,
28
30
  PostgresIndex: POSTGRES_ENTITY_TYPES.index,
@@ -30,7 +32,13 @@ const KINDS: Record<string, string> = {
30
32
  PostgresMaterializedView: POSTGRES_ENTITY_TYPES.materializedView,
31
33
  PostgresSequence: POSTGRES_ENTITY_TYPES.sequence,
32
34
  PostgresEnum: POSTGRES_ENTITY_TYPES.enum,
35
+ PostgresCompositeType: POSTGRES_ENTITY_TYPES.composite,
36
+ PostgresRangeType: POSTGRES_ENTITY_TYPES.range,
33
37
  PostgresDomain: POSTGRES_ENTITY_TYPES.domain,
38
+ PostgresAggregate: POSTGRES_ENTITY_TYPES.aggregate,
39
+ PostgresCollation: POSTGRES_ENTITY_TYPES.collation,
40
+ PostgresCast: POSTGRES_ENTITY_TYPES.cast,
41
+ PostgresEventTrigger: POSTGRES_ENTITY_TYPES.eventTrigger,
34
42
  PostgresExtension: POSTGRES_ENTITY_TYPES.extension,
35
43
  PostgresFunction: POSTGRES_ENTITY_TYPES.function,
36
44
  PostgresProcedure: POSTGRES_ENTITY_TYPES.procedure,