@intentius/chant-lexicon-sql 0.124.11 → 0.124.13

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 (173) hide show
  1. package/dist/clickhouse/apply/statements.d.ts.map +1 -1
  2. package/dist/clickhouse/entities.d.ts +7 -1
  3. package/dist/clickhouse/entities.d.ts.map +1 -1
  4. package/dist/clickhouse/expressions.d.ts +27 -0
  5. package/dist/clickhouse/expressions.d.ts.map +1 -0
  6. package/dist/clickhouse/parser.d.ts +12 -2
  7. package/dist/clickhouse/parser.d.ts.map +1 -1
  8. package/dist/clickhouse/plan/diff.d.ts.map +1 -1
  9. package/dist/clickhouse/plan/normalize.d.ts +11 -1
  10. package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
  11. package/dist/clickhouse/plan/rebuild-handoff.d.ts.map +1 -1
  12. package/dist/clickhouse/plan/rules.d.ts +1 -0
  13. package/dist/clickhouse/plan/rules.d.ts.map +1 -1
  14. package/dist/clickhouse.d.ts +1 -0
  15. package/dist/clickhouse.d.ts.map +1 -1
  16. package/dist/codegen/classifier-page.d.ts.map +1 -1
  17. package/dist/codegen/postgres-classifier-page.d.ts.map +1 -1
  18. package/dist/core/cursor.d.ts +41 -0
  19. package/dist/core/cursor.d.ts.map +1 -1
  20. package/dist/core/find-templates.d.ts +1 -1
  21. package/dist/core/find-templates.d.ts.map +1 -1
  22. package/dist/core/interpolation.d.ts +18 -1
  23. package/dist/core/interpolation.d.ts.map +1 -1
  24. package/dist/files/clickhouse.d.ts +2 -2
  25. package/dist/files/clickhouse.d.ts.map +1 -1
  26. package/dist/files/common.d.ts +16 -0
  27. package/dist/files/common.d.ts.map +1 -1
  28. package/dist/files/postgres.d.ts +2 -2
  29. package/dist/files/postgres.d.ts.map +1 -1
  30. package/dist/import-parser.d.ts +2 -1
  31. package/dist/import-parser.d.ts.map +1 -1
  32. package/dist/integrity.json +24 -24
  33. package/dist/lint/post-synth/clickhouse-helpers.d.ts +6 -19
  34. package/dist/lint/post-synth/clickhouse-helpers.d.ts.map +1 -1
  35. package/dist/lint/post-synth/postgres-helpers.d.ts +15 -55
  36. package/dist/lint/post-synth/postgres-helpers.d.ts.map +1 -1
  37. package/dist/lint/post-synth/postgres-names.d.ts +2 -2
  38. package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
  39. package/dist/lint/post-synth/sql-helpers.d.ts +5 -6
  40. package/dist/lint/post-synth/sql-helpers.d.ts.map +1 -1
  41. package/dist/lint/post-synth/sqlch102.d.ts.map +1 -1
  42. package/dist/lint/post-synth/sqlch103.d.ts.map +1 -1
  43. package/dist/lint/post-synth/sqlch104.d.ts.map +1 -1
  44. package/dist/lint/post-synth/sqlch105.d.ts.map +1 -1
  45. package/dist/lint/post-synth/sqlch106.d.ts.map +1 -1
  46. package/dist/lint/post-synth/sqlch107.d.ts.map +1 -1
  47. package/dist/lint/post-synth/sqlch111.d.ts.map +1 -1
  48. package/dist/lint/post-synth/sqlch112.d.ts.map +1 -1
  49. package/dist/lint/post-synth/sqlch113.d.ts.map +1 -1
  50. package/dist/lint/post-synth/sqlch114.d.ts.map +1 -1
  51. package/dist/lint/post-synth/sqlch115.d.ts.map +1 -1
  52. package/dist/lint/post-synth/sqlch120.d.ts.map +1 -1
  53. package/dist/lint/post-synth/sqlch125.d.ts.map +1 -1
  54. package/dist/lint/rules/postgres-templates.d.ts +1 -1
  55. package/dist/lint/rules/postgres-templates.d.ts.map +1 -1
  56. package/dist/lint/rules/templates.d.ts +1 -1
  57. package/dist/lint/rules/templates.d.ts.map +1 -1
  58. package/dist/lint/rules/typed-names.d.ts +1 -1
  59. package/dist/lint/rules/typed-names.d.ts.map +1 -1
  60. package/dist/manifest.json +1 -1
  61. package/dist/postgres/apply/statements.d.ts.map +1 -1
  62. package/dist/postgres/entities.d.ts +7 -1
  63. package/dist/postgres/entities.d.ts.map +1 -1
  64. package/dist/postgres/parser.d.ts +10 -2
  65. package/dist/postgres/parser.d.ts.map +1 -1
  66. package/dist/postgres/plan/diff.d.ts.map +1 -1
  67. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  68. package/dist/postgres/plan/rules.d.ts +1 -0
  69. package/dist/postgres/plan/rules.d.ts.map +1 -1
  70. package/dist/postgres.d.ts +1 -0
  71. package/dist/postgres.d.ts.map +1 -1
  72. package/dist/read/clickhouse.d.ts +66 -0
  73. package/dist/read/clickhouse.d.ts.map +1 -0
  74. package/dist/read/objects.d.ts +99 -0
  75. package/dist/read/objects.d.ts.map +1 -0
  76. package/dist/read/postgres.d.ts +83 -0
  77. package/dist/read/postgres.d.ts.map +1 -0
  78. package/dist/read.d.ts +49 -0
  79. package/dist/read.d.ts.map +1 -0
  80. package/dist/rules/clickhouse-helpers.ts +20 -97
  81. package/dist/rules/postgres-helpers.ts +18 -53
  82. package/dist/rules/postgres-names.ts +6 -6
  83. package/dist/rules/postgres-templates.ts +1 -1
  84. package/dist/rules/sql-helpers.ts +8 -28
  85. package/dist/rules/sqlch003.ts +1 -1
  86. package/dist/rules/sqlch102.ts +2 -3
  87. package/dist/rules/sqlch103.ts +2 -3
  88. package/dist/rules/sqlch104.ts +2 -3
  89. package/dist/rules/sqlch105.ts +2 -3
  90. package/dist/rules/sqlch106.ts +2 -3
  91. package/dist/rules/sqlch107.ts +2 -3
  92. package/dist/rules/sqlch111.ts +2 -3
  93. package/dist/rules/sqlch112.ts +2 -3
  94. package/dist/rules/sqlch113.ts +2 -3
  95. package/dist/rules/sqlch114.ts +2 -3
  96. package/dist/rules/sqlch115.ts +2 -3
  97. package/dist/rules/sqlch120.ts +2 -3
  98. package/dist/rules/sqlch125.ts +2 -3
  99. package/dist/rules/sqlpg002.ts +1 -1
  100. package/dist/rules/templates.ts +1 -1
  101. package/dist/rules/typed-names.ts +1 -1
  102. package/dist/serializer.d.ts +11 -1
  103. package/dist/serializer.d.ts.map +1 -1
  104. package/dist/sql-files.d.ts +17 -1
  105. package/dist/sql-files.d.ts.map +1 -1
  106. package/dist/sql-types.d.ts +3 -0
  107. package/dist/sql-types.d.ts.map +1 -1
  108. package/package.json +2 -3
  109. package/src/clickhouse/apply/statements.ts +2 -0
  110. package/src/clickhouse/entities.ts +8 -1
  111. package/src/clickhouse/expressions.ts +108 -0
  112. package/src/clickhouse/parser.ts +113 -31
  113. package/src/clickhouse/plan/diff.ts +19 -0
  114. package/src/clickhouse/plan/normalize.ts +27 -2
  115. package/src/clickhouse/plan/rebuild-handoff.ts +2 -1
  116. package/src/clickhouse/plan/rules.ts +7 -0
  117. package/src/clickhouse/testdata/server-ddl-baseline.json +67 -0
  118. package/src/clickhouse.ts +10 -0
  119. package/src/codegen/classifier-page.ts +2 -0
  120. package/src/codegen/postgres-classifier-page.ts +2 -0
  121. package/src/core/cursor.ts +96 -0
  122. package/src/core/find-templates.ts +1 -1
  123. package/src/core/interpolation.ts +32 -1
  124. package/src/files/clickhouse.ts +7 -5
  125. package/src/files/common.ts +23 -0
  126. package/src/files/postgres.ts +12 -8
  127. package/src/import-parser.ts +2 -1
  128. package/src/lint/post-synth/clickhouse-helpers.ts +20 -97
  129. package/src/lint/post-synth/postgres-helpers.ts +18 -53
  130. package/src/lint/post-synth/postgres-names.ts +6 -6
  131. package/src/lint/post-synth/sql-helpers.ts +8 -28
  132. package/src/lint/post-synth/sqlch102.ts +2 -3
  133. package/src/lint/post-synth/sqlch103.ts +2 -3
  134. package/src/lint/post-synth/sqlch104.ts +2 -3
  135. package/src/lint/post-synth/sqlch105.ts +2 -3
  136. package/src/lint/post-synth/sqlch106.ts +2 -3
  137. package/src/lint/post-synth/sqlch107.ts +2 -3
  138. package/src/lint/post-synth/sqlch111.ts +2 -3
  139. package/src/lint/post-synth/sqlch112.ts +2 -3
  140. package/src/lint/post-synth/sqlch113.ts +2 -3
  141. package/src/lint/post-synth/sqlch114.ts +2 -3
  142. package/src/lint/post-synth/sqlch115.ts +2 -3
  143. package/src/lint/post-synth/sqlch120.ts +2 -3
  144. package/src/lint/post-synth/sqlch125.ts +2 -3
  145. package/src/lint/rules/postgres-templates.ts +1 -1
  146. package/src/lint/rules/sqlch003.ts +1 -1
  147. package/src/lint/rules/sqlpg002.ts +1 -1
  148. package/src/lint/rules/templates.ts +1 -1
  149. package/src/lint/rules/typed-names.ts +1 -1
  150. package/src/lint/rules/typescript-7.test.ts +101 -0
  151. package/src/lsp/completions.ts +1 -1
  152. package/src/lsp/postgres-template.ts +1 -1
  153. package/src/lsp/postgres.ts +1 -1
  154. package/src/lsp/refs.ts +1 -1
  155. package/src/lsp/template.ts +1 -1
  156. package/src/postgres/apply/statements.ts +2 -0
  157. package/src/postgres/entities.ts +9 -1
  158. package/src/postgres/parser.ts +31 -8
  159. package/src/postgres/plan/diff.ts +18 -0
  160. package/src/postgres/plan/normalize.ts +12 -0
  161. package/src/postgres/plan/rules.ts +7 -0
  162. package/src/postgres/testdata/server-ddl-baseline.json +38 -0
  163. package/src/postgres.ts +12 -0
  164. package/src/read/clickhouse.ts +137 -0
  165. package/src/read/objects.ts +137 -0
  166. package/src/read/postgres.ts +180 -0
  167. package/src/read-policies.e2e.test.ts +144 -0
  168. package/src/read.test.ts +143 -0
  169. package/src/read.ts +69 -0
  170. package/src/serializer.ts +37 -1
  171. package/src/sql-files.ts +27 -6
  172. package/src/sql-types.ts +19 -2
  173. package/src/unknown-clauses.test.ts +227 -0
@@ -35,11 +35,11 @@
35
35
  * `src/backend/parser/gram.y` at `REL_18_6` where a page is silent.
36
36
  */
37
37
 
38
- import { kw, SqlCursor, type Span } from "../core/cursor";
38
+ import { kw, SqlCursor, type Span, type UnknownClause } from "../core/cursor";
39
39
  import { SqlSyntaxError, type Token } from "../core/tokens";
40
40
  import { keywordCategory } from "./keywords";
41
41
 
42
- export type { Span } from "../core/cursor";
42
+ export type { Span, UnknownClause } from "../core/cursor";
43
43
  export { SqlSyntaxError };
44
44
 
45
45
  /** An identifier as Postgres reads it: an unquoted name folds to lower case, a quoted one is exact. */
@@ -116,6 +116,8 @@ export interface ColumnNode {
116
116
 
117
117
  export interface TableNode {
118
118
  statement: "table";
119
+ /** Clauses the parser does not know (#3872), kept in the DDL as written; absent when there are none. */
120
+ unknown?: UnknownClause[];
119
121
  persistence?: "temporary" | "unlogged";
120
122
  ifNotExists: boolean;
121
123
  name: NameNode;
@@ -141,6 +143,8 @@ export interface IndexElement {
141
143
 
142
144
  export interface IndexNode {
143
145
  statement: "index";
146
+ /** Clauses the parser does not know (#3872), kept in the DDL as written; absent when there are none. */
147
+ unknown?: UnknownClause[];
144
148
  unique: boolean;
145
149
  concurrently: boolean;
146
150
  ifNotExists: boolean;
@@ -158,6 +162,8 @@ export interface IndexNode {
158
162
 
159
163
  export interface ViewNode {
160
164
  statement: "view";
165
+ /** Clauses the parser does not know (#3872), kept in the DDL as written; absent when there are none. */
166
+ unknown?: UnknownClause[];
161
167
  materialized: boolean;
162
168
  orReplace: boolean;
163
169
  temporary: boolean;
@@ -176,6 +182,8 @@ export interface ViewNode {
176
182
 
177
183
  export interface SequenceNode {
178
184
  statement: "sequence";
185
+ /** Clauses the parser does not know (#3872), kept in the DDL as written; absent when there are none. */
186
+ unknown?: UnknownClause[];
179
187
  persistence?: "temporary" | "unlogged";
180
188
  ifNotExists: boolean;
181
189
  name: NameNode;
@@ -366,6 +374,18 @@ const COLUMN_CLAUSE = [
366
374
  "ENFORCED",
367
375
  ];
368
376
 
377
+ /** A column's clause words, and its constraints' attributes: an unknown clause stops at the next (#3872). */
378
+ const COLUMN_WORDS = [...COLUMN_CLAUSE, "REFERENCES", "ON", "MATCH", "NO", "WITH", "INCLUDE", "USING"];
379
+
380
+ /** The clauses after a table's column list. */
381
+ const TABLE_WORDS = ["INHERITS", "PARTITION", "USING", "WITH", "WITHOUT", "ON", "TABLESPACE", "FOR", "DEFAULT"];
382
+
383
+ /** The clauses after an index's elements. */
384
+ const INDEX_WORDS = ["INCLUDE", "NULLS", "WITH", "TABLESPACE", "WHERE"];
385
+
386
+ /** The clauses before a view's AS. */
387
+ const VIEW_WORDS = ["USING", "WITH", "TABLESPACE", "AS"];
388
+
369
389
  /** `LIKE source_table [ like_option ... ]`'s option words. */
370
390
  const LIKE_OPTIONS = ["COMMENTS", "COMPRESSION", "CONSTRAINTS", "DEFAULTS", "GENERATED", "IDENTITY", "INDEXES", "STATISTICS", "STORAGE", "ALL"];
371
391
 
@@ -653,7 +673,7 @@ class PgParser extends SqlCursor {
653
673
  if (this.accept("COMPRESSION")) col.compression = this.expr(COLUMN_CLAUSE);
654
674
  else if (this.accept("STORAGE")) col.storage = this.expr(COLUMN_CLAUSE);
655
675
  else if (this.accept("COLLATE")) col.collate = this.expr(COLUMN_CLAUSE);
656
- else if (!this.columnConstraint(col)) break;
676
+ else if (!this.columnConstraint(col) && !this.unknownClause(COLUMN_WORDS, { inList: true, column: text || undefined })) break;
657
677
  }
658
678
  const after = this.peek();
659
679
  if (after && !(after.kind === "punct" && (after.text === "," || after.text === ")"))) this.fail("expected a column constraint, ',' or ')'");
@@ -991,7 +1011,7 @@ class PgParser extends SqlCursor {
991
1011
  if (!(this.acceptSeq("PRESERVE", "ROWS") || this.acceptSeq("DELETE", "ROWS") || this.accept("DROP"))) this.fail("expected PRESERVE ROWS, DELETE ROWS or DROP");
992
1012
  node.onCommit = this.span(from, []);
993
1013
  } else if (this.accept("TABLESPACE")) node.tablespace = this.nameTok("a tablespace").span;
994
- else break;
1014
+ else if (!this.unknownClause(TABLE_WORDS)) break;
995
1015
  }
996
1016
  return node;
997
1017
  }
@@ -1061,7 +1081,7 @@ class PgParser extends SqlCursor {
1061
1081
  node.with = this.reloptions();
1062
1082
  } else if (this.accept("TABLESPACE")) node.tablespace = this.nameTok("a tablespace").span;
1063
1083
  else if (this.accept("WHERE")) node.where = this.checkedExpr([], false);
1064
- else return node;
1084
+ else if (!this.unknownClause(INDEX_WORDS)) return node;
1065
1085
  }
1066
1086
  }
1067
1087
 
@@ -1085,7 +1105,7 @@ class PgParser extends SqlCursor {
1085
1105
  this.p++;
1086
1106
  node.with = this.reloptions();
1087
1107
  } else if (materialized && this.accept("TABLESPACE")) node.tablespace = this.nameTok("a tablespace").span;
1088
- else break;
1108
+ else if (!this.unknownClause(VIEW_WORDS)) break;
1089
1109
  }
1090
1110
  this.expect("AS");
1091
1111
  const tail = materialized
@@ -1131,7 +1151,7 @@ class PgParser extends SqlCursor {
1131
1151
  else if (this.acceptSeq("OWNED", "BY")) {
1132
1152
  if (this.accept("NONE")) node.options.push({ option: "OWNED BY", value: this.span(this.idx(-1), []) });
1133
1153
  else node.options.push({ option: "OWNED BY", value: this.qualifiedName("a column").span });
1134
- } else return node;
1154
+ } else if (!this.unknownClause(STOP)) return node;
1135
1155
  }
1136
1156
  }
1137
1157
 
@@ -1575,7 +1595,10 @@ class PgParser extends SqlCursor {
1575
1595
  const out: StatementNode[] = [];
1576
1596
  while (this.peek() !== undefined) {
1577
1597
  if (this.acceptPunct(";")) continue;
1578
- out.push(this.parse());
1598
+ const before = this.unknownClauses.length;
1599
+ const node = this.parse();
1600
+ if (this.unknownClauses.length > before) (node as { unknown?: UnknownClause[] }).unknown = this.unknownClauses.slice(before);
1601
+ out.push(node);
1579
1602
  if (this.peek() !== undefined && !this.isPunct(";")) this.fail(`unexpected token after the ${describe(out[out.length - 1]!)}`);
1580
1603
  }
1581
1604
  if (out.length === 0) this.fail("expected a statement");
@@ -197,7 +197,25 @@ function diffConstraints(key: string, before: PgDiffObject, after: PgDiffObject,
197
197
  const same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b);
198
198
  const show = (v: unknown): string | undefined => (v === undefined ? undefined : typeof v === "string" ? v : JSON.stringify(v));
199
199
 
200
+ /**
201
+ * The clauses chant does not read (#3872), compared as text: one the
202
+ * declaration has and the server does not, or has differently, is a change
203
+ * to review (SQLPG299); one only the server prints is a hint, since a
204
+ * default a newer server prints cannot be told from a clause removed.
205
+ */
206
+ function diffUnparsed(key: string, before: PgDiffObject, after: PgDiffObject, out: PgChange[], hints: string[]): void {
207
+ const b = (before.fields.unparsed as Record<string, string> | undefined) ?? {};
208
+ const a = (after.fields.unparsed as Record<string, string> | undefined) ?? {};
209
+ for (const clause of Object.keys(a)) {
210
+ if (b[clause] !== a[clause]) out.push(change(key, `unparsed.${clause}`, "SQLPG299", b[clause], a[clause], { note: "chant does not read this clause; review the change" }));
211
+ }
212
+ for (const clause of Object.keys(b)) {
213
+ if (!(clause in a)) hints.push(`${key}: the current definition has ${clause} (${b[clause]}), a clause chant does not read and the declaration does not have; it is not compared`);
214
+ }
215
+ }
216
+
200
217
  function diffObject(key: string, before: PgDiffObject, after: PgDiffObject, out: PgChange[], hints: string[], major: number): void {
218
+ diffUnparsed(key, before, after, out, hints);
201
219
  if (before.kind !== after.kind) {
202
220
  out.push(change(key, "kind", "SQLPG268", before.kind, after.kind));
203
221
  return;
@@ -797,6 +797,18 @@ export function canonicalPgObject(entityType: string, props: Record<string, unkn
797
797
  break;
798
798
  }
799
799
  }
800
+ // Clauses the parser does not know (#3872): compared as text, by clause.
801
+ const unparsed = props.unparsed as Array<{ clause: string; text: string; column?: string }> | undefined;
802
+ if (unparsed && unparsed.length > 0) {
803
+ const out: Record<string, string> = {};
804
+ for (const u of unparsed) {
805
+ const base = u.column !== undefined ? `columns.${u.column}.${u.clause}` : u.clause;
806
+ let key = base;
807
+ for (let n = 2; key in out; n++) key = `${base}#${n}`;
808
+ out[key] = canonicalExpr(u.text) ?? u.text;
809
+ }
810
+ fields.unparsed = out;
811
+ }
800
812
  return { ...base, fields: Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined && v !== false)), columns, constraints };
801
813
  }
802
814
 
@@ -350,6 +350,13 @@ export const PG_CLASSIFIER_RULES = {
350
350
  "ALTER DEFAULT PRIVILEGES changes what objects created afterwards are given; objects that exist keep their privileges.",
351
351
  `${DOCS}/sql-alterdefaultprivileges.html`,
352
352
  ),
353
+ SQLPG299: rule(
354
+ "SQLPG299",
355
+ "expand",
356
+ "Change a clause chant does not read",
357
+ "chant's parser does not know this clause, so it cannot tell which lock the change takes or whether an in-place statement makes it; the plan refuses it rather than guess. Review it: make it by hand with the ALTER the server documents for the clause, or as expand and contract.",
358
+ `${DOCS}/sql-altertable.html`,
359
+ ),
353
360
  SQLPG270: rule("SQLPG270", "drop", "Drop an object", "The object and, for a table, a materialized view or a sequence, its data are gone.", `${DOCS}/sql-droptable.html`),
354
361
  } as const satisfies Record<string, PgClassifierRule>;
355
362
 
@@ -0,0 +1,38 @@
1
+ {
2
+ "release": "REL_18_6",
3
+ "unknownClauses": [
4
+ "USING"
5
+ ],
6
+ "failures": [
7
+ "expected AS at 'USING'",
8
+ "expected IDENTITY (a generated column is GENERATED ALWAYS AS) at '('",
9
+ "expected NOT NULL, NULL or CHECK at 'REFERENCES'",
10
+ "expected NOT NULL, NULL or CHECK at 'generated'",
11
+ "expected RANGE or LIST or HASH at 'MAGIC'",
12
+ "expected a column constraint, ',' or ')' at 'NOT'",
13
+ "expected a column name ('with' is a reserved key word; quote it to use it as a name) at 'with'",
14
+ "expected a column type at ')'",
15
+ "expected a column type at ','",
16
+ "expected a partition bound value at ')'",
17
+ "expected a table name at '\\'",
18
+ "expected an expression at ')'",
19
+ "expected an index name ('ON' is a reserved key word; quote it to use it as a name) at 'ON'",
20
+ "expected the column list (CREATE TABLE ... AS is not declared here) at 'AS'",
21
+ "expected the column list (CREATE TABLE ... AS is not declared here) at 'TABLESPACE'",
22
+ "expected the column list (CREATE TABLE ... AS is not declared here) at 'USING'",
23
+ "expected the column list (CREATE TABLE ... AS is not declared here) at 'WITH'",
24
+ "expected the column list (CREATE TABLE ... AS is not declared here) at 'as'",
25
+ "expected the end of the default (a default cannot hold this operator unparenthesized) at 'IN'",
26
+ "schema elements inside CREATE SCHEMA are not declared here; declare each object on its own at 'CREATE'",
27
+ "schema elements inside CREATE SCHEMA are not declared here; declare each object on its own at 'create'",
28
+ "unexpected end of statement",
29
+ "unexpected token after the domain definition at 'ENFORCED'",
30
+ "unexpected token after the domain definition at 'NOT'",
31
+ "unexpected token after the domain definition at 'PRIMARY'",
32
+ "unexpected token after the domain definition at 'deferrable'",
33
+ "unexpected token after the domain definition at 'no'",
34
+ "unexpected token after the domain definition at 'unique'",
35
+ "unexpected token after the table definition at 'WITH'",
36
+ "unexpected token after the table definition at '\\'"
37
+ ]
38
+ }
package/src/postgres.ts CHANGED
@@ -105,3 +105,15 @@ export {
105
105
  } from "./postgres/migrate/op";
106
106
  export { POSTGRES_RECEIPTS_TABLE } from "./postgres/migrate/receipts";
107
107
  export * from "./composites/postgres";
108
+
109
+ // The build's Postgres objects as typed values, for a project's own
110
+ // `lint.policies` (#3870); also at `@intentius/chant-lexicon-sql/read`.
111
+ export {
112
+ readPostgres,
113
+ canonicalPostgresType,
114
+ type PostgresRead,
115
+ type PostgresTableInfo,
116
+ type PostgresColumnInfo,
117
+ type PostgresIndexInfo,
118
+ type PostgresViewInfo,
119
+ } from "./read/postgres";
@@ -0,0 +1,137 @@
1
+ /**
2
+ * The ClickHouse half of the public read API (`../read.ts`): the build's
3
+ * tables, columns, sort keys, skip indexes, engines and views as typed values,
4
+ * built from the serialized output. The lexicon's post-synth checks read the
5
+ * same values (`../lint/post-synth/clickhouse-helpers.ts`).
6
+ */
7
+
8
+ import { relationKey } from "../core/typed";
9
+ import type { ColumnDef } from "../clickhouse/entities";
10
+ import { bareName, baseEngineName, isMergeTree, isNullableType, keyElements, typeFamily, unwrapType } from "../clickhouse/expressions";
11
+ import { dialectObjects, type ClickHouseTableJson, type ClickHouseViewJson, type SqlObjectJson, type SqlReadSource } from "./objects";
12
+
13
+ /** A column of a ClickHouse table or view, as declared, with what follows from the declaration. */
14
+ export interface ClickHouseColumnInfo extends ColumnDef {
15
+ /** Whether the column can hold NULL: its type is `Nullable(...)` (inside `LowCardinality` too), or it is declared `NULL`. */
16
+ nullable: boolean;
17
+ /** `LowCardinality(...)`. */
18
+ lowCardinality: boolean;
19
+ /** The type without its `Nullable` and `LowCardinality` wrappers, as written: `DateTime64(3, 'UTC')`. */
20
+ baseType?: string;
21
+ /** The type family's canonical name, as the pinned server's catalog has it: `BIGINT UNSIGNED` is `UInt64`, `DateTime64(3)` is `DateTime64`. */
22
+ family?: string;
23
+ /** A bare element of the table's sort key (`ORDER BY`). */
24
+ inSortKey: boolean;
25
+ }
26
+
27
+ /** A ClickHouse table, as declared: its columns, engine, keys and skip indexes. */
28
+ export interface ClickHouseTableInfo extends Omit<ClickHouseTableJson, "columns"> {
29
+ /** `database.name` when the DDL qualifies it, else the name: the table's key in `SqlSchema`. */
30
+ qualifiedName: string;
31
+ columns: ClickHouseColumnInfo[];
32
+ /** The engine without a `Replicated` prefix: `ReplicatedReplacingMergeTree` is `ReplacingMergeTree`. */
33
+ engineFamily?: string;
34
+ /** A MergeTree family engine, replicated or not. */
35
+ mergeTree: boolean;
36
+ /** The `ORDER BY` elements, as written, without `ASC`/`DESC`; empty for `tuple()` or none. */
37
+ sortKey: string[];
38
+ /** The `PRIMARY KEY` elements, else the sort key's, which is what ClickHouse uses when none is written. */
39
+ primaryKeyElements: string[];
40
+ /** The column named `name`, if the table declares one. */
41
+ column(name: string): ClickHouseColumnInfo | undefined;
42
+ }
43
+
44
+ /** A view or materialized view, with the names of its output columns. */
45
+ export interface ClickHouseViewInfo extends Omit<ClickHouseViewJson, "columns"> {
46
+ qualifiedName: string;
47
+ materialized: boolean;
48
+ columns: ClickHouseColumnInfo[];
49
+ /** The declared columns, else the outputs of the query's select list. */
50
+ outputs: string[];
51
+ }
52
+
53
+ /** What a ClickHouse build declares. */
54
+ export interface ClickHouseRead {
55
+ dialect: "clickhouse";
56
+ tables: ClickHouseTableInfo[];
57
+ views: ClickHouseViewInfo[];
58
+ /** Every serialized object, databases, dictionaries, users and grants included, in apply order. */
59
+ objects: SqlObjectJson[];
60
+ /** A table by export name, `database.name`, or name. */
61
+ table(ref: string): ClickHouseTableInfo | undefined;
62
+ }
63
+
64
+ function columnInfo(c: ColumnDef, sortKey: ReadonlySet<string>): ClickHouseColumnInfo {
65
+ const inner = /^LowCardinality\s*\(/i.test((c.type ?? "").trim());
66
+ const nullable = c.nullable === true || isNullableType(c.type) || (inner && isNullableType(unwrapLowCardinality(c.type!)));
67
+ return {
68
+ ...c,
69
+ nullable,
70
+ lowCardinality: inner,
71
+ ...(c.type !== undefined ? { baseType: unwrapType(c.type), family: typeFamily(c.type) } : {}),
72
+ inSortKey: sortKey.has(c.name),
73
+ };
74
+ }
75
+
76
+ function unwrapLowCardinality(type: string): string {
77
+ return type.trim().replace(/^LowCardinality\s*\(([\s\S]*)\)$/i, "$1").trim();
78
+ }
79
+
80
+ function build(objects: SqlObjectJson[]): ClickHouseRead {
81
+ const tables = objects
82
+ .filter((o) => o.type === "ClickHouse::Table")
83
+ .map((o): ClickHouseTableInfo => {
84
+ const t = o as unknown as ClickHouseTableJson;
85
+ const sortKey = t.orderBy ? keyElements(t.orderBy) : [];
86
+ const bare = new Set(sortKey.map(bareName).filter((n): n is string => n !== undefined));
87
+ const columns = (t.columns ?? []).map((c) => columnInfo(c, bare));
88
+ return {
89
+ ...t,
90
+ qualifiedName: relationKey(t),
91
+ columns,
92
+ ...(t.engine ? { engineFamily: baseEngineName(t.engine) } : {}),
93
+ mergeTree: isMergeTree(t.engine),
94
+ sortKey,
95
+ primaryKeyElements: t.primaryKey ? keyElements(t.primaryKey) : sortKey,
96
+ column: (name) => columns.find((c) => c.name === name),
97
+ };
98
+ });
99
+ const views = objects
100
+ .filter((o) => o.type === "ClickHouse::View" || o.type === "ClickHouse::MaterializedView")
101
+ .map((o): ClickHouseViewInfo => {
102
+ const v = o as unknown as ClickHouseViewJson;
103
+ const columns = (v.columns ?? []).map((c) => columnInfo(c, new Set()));
104
+ return {
105
+ ...v,
106
+ qualifiedName: relationKey(v),
107
+ materialized: v.type === "ClickHouse::MaterializedView",
108
+ columns,
109
+ outputs: columns.length > 0 ? columns.map((c) => c.name) : (v.lineage ?? []).map((e) => e.output),
110
+ };
111
+ });
112
+ return {
113
+ dialect: "clickhouse",
114
+ tables,
115
+ views,
116
+ objects,
117
+ table: (ref) => tables.find((t) => t.export === ref) ?? tables.find((t) => t.qualifiedName === ref) ?? tables.find((t) => t.name === ref),
118
+ };
119
+ }
120
+
121
+ const cache = new WeakMap<object, ClickHouseRead>();
122
+
123
+ /**
124
+ * What a ClickHouse build declares, read from a post-synth context's `docs`
125
+ * (a `lint.policies` check's `ctx`), or from the serialized output document
126
+ * itself. Empty when the build has no ClickHouse output. The result is cached
127
+ * per context; treat it as read-only.
128
+ */
129
+ export function readClickHouse(source: SqlReadSource | { dialect: string; objects: unknown[] }): ClickHouseRead {
130
+ if ("objects" in source) return build(source.dialect === "clickhouse" ? (source.objects as SqlObjectJson[]) : []);
131
+ const key = source.docs;
132
+ const hit = key ? cache.get(key) : undefined;
133
+ if (hit) return hit;
134
+ const read = build(dialectObjects(source, "clickhouse"));
135
+ if (key) cache.set(key, read);
136
+ return read;
137
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * The sql build output as the serializer writes it (`../serializer.ts`): one
3
+ * JSON document per build with `dialect`, `applyOrder` and `objects`, each
4
+ * object its entity's props with the template source dropped, the DDL moved
5
+ * to the end, and every reference to another declared object written as that
6
+ * object's export name.
7
+ *
8
+ * This is the one place that finds those documents in a post-synth context.
9
+ * The public read API (`../read.ts`) and the lexicon's own post-synth checks
10
+ * (`../lint/post-synth/sql-helpers.ts`) both start here.
11
+ */
12
+
13
+ import type { OutputDoc } from "@intentius/chant/lint/post-synth";
14
+ import type * as ch from "../clickhouse/entities";
15
+ import type * as pg from "../postgres/entities";
16
+
17
+ /** What a reader takes: a post-synth context, or anything else carrying its `docs`. */
18
+ export interface SqlReadSource {
19
+ readonly docs?: readonly OutputDoc[];
20
+ }
21
+
22
+ /** One build output document, as the serializer writes it. */
23
+ export interface SqlOutputDocument {
24
+ dialect: "clickhouse" | "postgres";
25
+ /** Postgres only: the major the build targets (`sql.postgresMajor`, else the newest pinned). */
26
+ postgresMajor?: number;
27
+ applyOrder?: string[];
28
+ objects: SqlObjectJson[];
29
+ }
30
+
31
+ /** A reference to another declared object: its export name, or null when it is not one the build declares. */
32
+ export type RefJson = string | null;
33
+
34
+ /** The fields every serialized object has. */
35
+ export interface SqlObjectBase {
36
+ /** The export name of the declaration. */
37
+ export: string;
38
+ /** The entity type: `Postgres::Table`, `ClickHouse::MaterializedView`, ... */
39
+ type: string;
40
+ /** The object's name in the database, unquoted. */
41
+ name: string;
42
+ /** The name it is referred to by in SQL: `name`, or `schema.name` / `database.name`. */
43
+ sqlName?: string;
44
+ /** The export names it must be created after. */
45
+ dependsOn?: string[];
46
+ /** The statements as they will be sent. */
47
+ ddl?: string;
48
+ }
49
+
50
+ /** Any serialized object: the common fields, and the rest of its props as written. */
51
+ export interface SqlObjectJson extends SqlObjectBase {
52
+ [key: string]: unknown;
53
+ }
54
+
55
+ type Serialized<P> = Omit<P, "source" | "ddl"> & Omit<SqlObjectBase, "type"> & { sqlName: string; dependsOn: string[]; ddl: string };
56
+
57
+ /** A lineage edge as written: the output column, its expression, and the `export.column` names it reads. */
58
+ export interface LineageJson {
59
+ output: string;
60
+ expr: string;
61
+ from: Array<string | null>;
62
+ }
63
+
64
+ // ── Postgres ──────────────────────────────────────────────────────────
65
+
66
+ export interface PostgresForeignKeyJson extends Omit<pg.ForeignKeyDef, "references"> {
67
+ /** The referenced table's export name, or null when the build does not declare it. */
68
+ references: RefJson;
69
+ }
70
+
71
+ export interface PostgresTableJson
72
+ extends Omit<Serialized<pg.TableProps>, "partitionOf" | "ofType" | "inherits" | "foreignKeys"> {
73
+ type: "Postgres::Table";
74
+ partitionOf?: RefJson;
75
+ ofType?: RefJson;
76
+ inherits: RefJson[];
77
+ foreignKeys: PostgresForeignKeyJson[];
78
+ }
79
+
80
+ export interface PostgresIndexJson extends Omit<Serialized<pg.IndexProps>, "table"> {
81
+ type: "Postgres::Index";
82
+ /** The indexed table's export name, or null when the build does not declare it. */
83
+ table: RefJson;
84
+ }
85
+
86
+ export interface PostgresViewJson extends Omit<Serialized<pg.ViewProps>, "reads" | "lineage"> {
87
+ type: "Postgres::View" | "Postgres::MaterializedView";
88
+ reads: RefJson[];
89
+ lineage: LineageJson[];
90
+ }
91
+
92
+ // ── ClickHouse ────────────────────────────────────────────────────────
93
+
94
+ export interface ClickHouseTableJson extends Serialized<ch.TableProps> {
95
+ type: "ClickHouse::Table";
96
+ }
97
+
98
+ export interface ClickHouseViewJson extends Omit<Serialized<ch.ViewProps>, "to" | "reads" | "lineage"> {
99
+ type: "ClickHouse::View" | "ClickHouse::MaterializedView";
100
+ /** A materialized view's target: its export name when declared, else the name as written. */
101
+ to?: RefJson;
102
+ reads: RefJson[];
103
+ lineage: LineageJson[];
104
+ }
105
+
106
+ // ── Finding the documents ─────────────────────────────────────────────
107
+
108
+ /** True for a value shaped like the serializer's document. */
109
+ export function isSqlOutputDocument(value: unknown): value is SqlOutputDocument {
110
+ const v = value as { dialect?: unknown; objects?: unknown } | null;
111
+ return v !== null && typeof v === "object" && (v.dialect === "clickhouse" || v.dialect === "postgres") && Array.isArray(v.objects);
112
+ }
113
+
114
+ /** Every sql output document in the context, in output order. */
115
+ export function sqlDocuments(source: SqlReadSource): SqlOutputDocument[] {
116
+ const out: SqlOutputDocument[] = [];
117
+ for (const doc of source.docs ?? []) {
118
+ if (doc.error || doc.lexicon !== "sql" || doc.format !== "json") continue;
119
+ if (isSqlOutputDocument(doc.value)) out.push(doc.value);
120
+ }
121
+ return out;
122
+ }
123
+
124
+ /** Every serialized object of one dialect, across the context's sql documents. */
125
+ export function dialectObjects(source: SqlReadSource, dialect: SqlOutputDocument["dialect"]): SqlObjectJson[] {
126
+ const out: SqlObjectJson[] = [];
127
+ for (const doc of sqlDocuments(source)) if (doc.dialect === dialect) out.push(...doc.objects);
128
+ return out;
129
+ }
130
+
131
+ /** The Postgres major the build output records, else `fallback`. */
132
+ export function recordedPostgresMajor(source: SqlReadSource, fallback: number): number {
133
+ for (const doc of sqlDocuments(source)) {
134
+ if (doc.dialect === "postgres" && typeof doc.postgresMajor === "number") return doc.postgresMajor;
135
+ }
136
+ return fallback;
137
+ }