@intentius/chant-lexicon-sql 0.124.12 → 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 (148) 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/interpolation.d.ts +18 -1
  21. package/dist/core/interpolation.d.ts.map +1 -1
  22. package/dist/files/clickhouse.d.ts +2 -2
  23. package/dist/files/clickhouse.d.ts.map +1 -1
  24. package/dist/files/common.d.ts +16 -0
  25. package/dist/files/common.d.ts.map +1 -1
  26. package/dist/files/postgres.d.ts +2 -2
  27. package/dist/files/postgres.d.ts.map +1 -1
  28. package/dist/import-parser.d.ts +2 -1
  29. package/dist/import-parser.d.ts.map +1 -1
  30. package/dist/integrity.json +19 -19
  31. package/dist/lint/post-synth/clickhouse-helpers.d.ts +6 -19
  32. package/dist/lint/post-synth/clickhouse-helpers.d.ts.map +1 -1
  33. package/dist/lint/post-synth/postgres-helpers.d.ts +15 -55
  34. package/dist/lint/post-synth/postgres-helpers.d.ts.map +1 -1
  35. package/dist/lint/post-synth/postgres-names.d.ts +2 -2
  36. package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
  37. package/dist/lint/post-synth/sql-helpers.d.ts +5 -6
  38. package/dist/lint/post-synth/sql-helpers.d.ts.map +1 -1
  39. package/dist/lint/post-synth/sqlch102.d.ts.map +1 -1
  40. package/dist/lint/post-synth/sqlch103.d.ts.map +1 -1
  41. package/dist/lint/post-synth/sqlch104.d.ts.map +1 -1
  42. package/dist/lint/post-synth/sqlch105.d.ts.map +1 -1
  43. package/dist/lint/post-synth/sqlch106.d.ts.map +1 -1
  44. package/dist/lint/post-synth/sqlch107.d.ts.map +1 -1
  45. package/dist/lint/post-synth/sqlch111.d.ts.map +1 -1
  46. package/dist/lint/post-synth/sqlch112.d.ts.map +1 -1
  47. package/dist/lint/post-synth/sqlch113.d.ts.map +1 -1
  48. package/dist/lint/post-synth/sqlch114.d.ts.map +1 -1
  49. package/dist/lint/post-synth/sqlch115.d.ts.map +1 -1
  50. package/dist/lint/post-synth/sqlch120.d.ts.map +1 -1
  51. package/dist/lint/post-synth/sqlch125.d.ts.map +1 -1
  52. package/dist/manifest.json +1 -1
  53. package/dist/postgres/apply/statements.d.ts.map +1 -1
  54. package/dist/postgres/entities.d.ts +7 -1
  55. package/dist/postgres/entities.d.ts.map +1 -1
  56. package/dist/postgres/parser.d.ts +10 -2
  57. package/dist/postgres/parser.d.ts.map +1 -1
  58. package/dist/postgres/plan/diff.d.ts.map +1 -1
  59. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  60. package/dist/postgres/plan/rules.d.ts +1 -0
  61. package/dist/postgres/plan/rules.d.ts.map +1 -1
  62. package/dist/postgres.d.ts +1 -0
  63. package/dist/postgres.d.ts.map +1 -1
  64. package/dist/read/clickhouse.d.ts +66 -0
  65. package/dist/read/clickhouse.d.ts.map +1 -0
  66. package/dist/read/objects.d.ts +99 -0
  67. package/dist/read/objects.d.ts.map +1 -0
  68. package/dist/read/postgres.d.ts +83 -0
  69. package/dist/read/postgres.d.ts.map +1 -0
  70. package/dist/read.d.ts +49 -0
  71. package/dist/read.d.ts.map +1 -0
  72. package/dist/rules/clickhouse-helpers.ts +20 -97
  73. package/dist/rules/postgres-helpers.ts +18 -53
  74. package/dist/rules/postgres-names.ts +6 -6
  75. package/dist/rules/sql-helpers.ts +8 -28
  76. package/dist/rules/sqlch102.ts +2 -3
  77. package/dist/rules/sqlch103.ts +2 -3
  78. package/dist/rules/sqlch104.ts +2 -3
  79. package/dist/rules/sqlch105.ts +2 -3
  80. package/dist/rules/sqlch106.ts +2 -3
  81. package/dist/rules/sqlch107.ts +2 -3
  82. package/dist/rules/sqlch111.ts +2 -3
  83. package/dist/rules/sqlch112.ts +2 -3
  84. package/dist/rules/sqlch113.ts +2 -3
  85. package/dist/rules/sqlch114.ts +2 -3
  86. package/dist/rules/sqlch115.ts +2 -3
  87. package/dist/rules/sqlch120.ts +2 -3
  88. package/dist/rules/sqlch125.ts +2 -3
  89. package/dist/serializer.d.ts +11 -1
  90. package/dist/serializer.d.ts.map +1 -1
  91. package/dist/sql-files.d.ts +17 -1
  92. package/dist/sql-files.d.ts.map +1 -1
  93. package/dist/sql-types.d.ts +3 -0
  94. package/dist/sql-types.d.ts.map +1 -1
  95. package/package.json +2 -2
  96. package/src/clickhouse/apply/statements.ts +2 -0
  97. package/src/clickhouse/entities.ts +8 -1
  98. package/src/clickhouse/expressions.ts +108 -0
  99. package/src/clickhouse/parser.ts +113 -31
  100. package/src/clickhouse/plan/diff.ts +19 -0
  101. package/src/clickhouse/plan/normalize.ts +27 -2
  102. package/src/clickhouse/plan/rebuild-handoff.ts +2 -1
  103. package/src/clickhouse/plan/rules.ts +7 -0
  104. package/src/clickhouse/testdata/server-ddl-baseline.json +67 -0
  105. package/src/clickhouse.ts +10 -0
  106. package/src/codegen/classifier-page.ts +2 -0
  107. package/src/codegen/postgres-classifier-page.ts +2 -0
  108. package/src/core/cursor.ts +96 -0
  109. package/src/core/interpolation.ts +32 -1
  110. package/src/files/clickhouse.ts +7 -5
  111. package/src/files/common.ts +23 -0
  112. package/src/files/postgres.ts +12 -8
  113. package/src/import-parser.ts +2 -1
  114. package/src/lint/post-synth/clickhouse-helpers.ts +20 -97
  115. package/src/lint/post-synth/postgres-helpers.ts +18 -53
  116. package/src/lint/post-synth/postgres-names.ts +6 -6
  117. package/src/lint/post-synth/sql-helpers.ts +8 -28
  118. package/src/lint/post-synth/sqlch102.ts +2 -3
  119. package/src/lint/post-synth/sqlch103.ts +2 -3
  120. package/src/lint/post-synth/sqlch104.ts +2 -3
  121. package/src/lint/post-synth/sqlch105.ts +2 -3
  122. package/src/lint/post-synth/sqlch106.ts +2 -3
  123. package/src/lint/post-synth/sqlch107.ts +2 -3
  124. package/src/lint/post-synth/sqlch111.ts +2 -3
  125. package/src/lint/post-synth/sqlch112.ts +2 -3
  126. package/src/lint/post-synth/sqlch113.ts +2 -3
  127. package/src/lint/post-synth/sqlch114.ts +2 -3
  128. package/src/lint/post-synth/sqlch115.ts +2 -3
  129. package/src/lint/post-synth/sqlch120.ts +2 -3
  130. package/src/lint/post-synth/sqlch125.ts +2 -3
  131. package/src/postgres/apply/statements.ts +2 -0
  132. package/src/postgres/entities.ts +9 -1
  133. package/src/postgres/parser.ts +31 -8
  134. package/src/postgres/plan/diff.ts +18 -0
  135. package/src/postgres/plan/normalize.ts +12 -0
  136. package/src/postgres/plan/rules.ts +7 -0
  137. package/src/postgres/testdata/server-ddl-baseline.json +38 -0
  138. package/src/postgres.ts +12 -0
  139. package/src/read/clickhouse.ts +137 -0
  140. package/src/read/objects.ts +137 -0
  141. package/src/read/postgres.ts +180 -0
  142. package/src/read-policies.e2e.test.ts +144 -0
  143. package/src/read.test.ts +143 -0
  144. package/src/read.ts +69 -0
  145. package/src/serializer.ts +37 -1
  146. package/src/sql-files.ts +27 -6
  147. package/src/sql-types.ts +19 -2
  148. package/src/unknown-clauses.test.ts +227 -0
@@ -33,10 +33,56 @@ export const unquoteWith = (text: string, quotes: string): string => {
33
33
  return inner;
34
34
  };
35
35
 
36
+ /**
37
+ * A clause the parser does not know, kept as written (#3872): a server
38
+ * release can add a clause before chant's parser reads it, and the statement
39
+ * still goes to the server as the author wrote it. What chant reads around it
40
+ * is read; the clause itself is opaque.
41
+ */
42
+ export interface UnknownClause {
43
+ /** Its leading words, upper case, as a message names it: `TABLESPACE_POLICY`. */
44
+ clause: string;
45
+ /** The whole clause, from its first word to where a clause the parser knows starts. */
46
+ span: Span;
47
+ /** The column (or dictionary attribute) the clause is written on, when it is one of a column's. */
48
+ column?: string;
49
+ }
50
+
51
+ /** Edit distance with a swap of two neighbours as one edit, for telling a misspelt clause from a new one. */
52
+ function distance(a: string, b: string): number {
53
+ const d = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
54
+ for (let i = 1; i <= a.length; i++) {
55
+ for (let j = 1; j <= b.length; j++) {
56
+ const row = d[i]!;
57
+ row[j] = Math.min(d[i - 1]![j]! + 1, row[j - 1]! + 1, d[i - 1]![j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1));
58
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) row[j] = Math.min(row[j]!, d[i - 2]![j - 2]! + 1);
59
+ }
60
+ }
61
+ return d[a.length]![b.length]!;
62
+ }
63
+
64
+ /**
65
+ * The known clause word `word` is a misspelling of, if any: one edit from a
66
+ * known word of four letters or more (two neighbouring letters swapped is
67
+ * one), two from one of seven or more. A word
68
+ * that close is taken as a typo, which fails, not as a new clause, which warns.
69
+ */
70
+ export function misspelling(word: string, known: readonly string[]): string | undefined {
71
+ const w = word.toUpperCase();
72
+ for (const k of known) {
73
+ if (k.length < 4 || k === w) continue;
74
+ const d = distance(w, k);
75
+ if (d === 1 || (d === 2 && k.length >= 7 && w.length >= 7)) return k;
76
+ }
77
+ return undefined;
78
+ }
79
+
36
80
  export class SqlCursor {
37
81
  /** Indexes of the significant (non-trivia) tokens. */
38
82
  protected readonly sig: number[];
39
83
  protected p = 0;
84
+ /** The clauses read so far that the parser does not know (#3872), in order. */
85
+ protected readonly unknownClauses: UnknownClause[] = [];
40
86
 
41
87
  constructor(protected readonly tokens: Token[]) {
42
88
  this.sig = tokens.map((t, i) => (isTrivia(t) ? -1 : i)).filter((i) => i >= 0);
@@ -161,6 +207,56 @@ export class SqlCursor {
161
207
  return this.span(from, refs);
162
208
  }
163
209
 
210
+ /**
211
+ * At a clause boundary, a clause this parser does not know (#3872): a bare
212
+ * word that is not one of `known`, read up to the next top-level word of
213
+ * `known`, the statement's end, or (with `inList`, inside a column list) the
214
+ * element's `,` or `)`, its parentheses balanced. Recorded and returned.
215
+ *
216
+ * Undefined when the next token cannot start one: the end, punctuation, a
217
+ * string, a number, a quoted name or an interpolation, a word shorter than
218
+ * three letters or with a digit in it, or a word of `known`
219
+ * (a clause the parser knows, out of place). A word one letter from a known
220
+ * one fails here as a misspelling, and a run holding a top-level `SELECT`,
221
+ * `CREATE` or `INSERT` as a query or a second statement: genuinely
222
+ * malformed SQL stays an error.
223
+ */
224
+ protected unknownClause(known: readonly string[], options: { inList?: boolean; column?: string } = {}): UnknownClause | undefined {
225
+ const t = this.peek();
226
+ // A clause starts with a key word: three letters or more, no digits.
227
+ if (t === undefined || t.kind !== "ident" || !/^[A-Za-z_]{3,}$/.test(t.text)) return undefined;
228
+ const word = t.text.toUpperCase();
229
+ if (known.includes(word)) return undefined;
230
+ const near = misspelling(word, known);
231
+ if (near !== undefined) this.fail(`expected a clause ('${t.text}' is not one chant reads, and is one letter from ${near}: is it misspelt?)`, t);
232
+ const from = this.idx();
233
+ const startP = this.p;
234
+ // Its name: the first word, and when that is written in upper case, the upper-case words after it (`SQL SECURITY`), not a name it takes.
235
+ const words = [word];
236
+ const upper = (u: Token | undefined) => u?.kind === "ident" && /^[A-Z_]+$/.test(u.text) && !known.includes(u.text);
237
+ if (upper(t)) for (let n = 1; n < 3 && upper(this.peek(n)); n++) words.push(this.peek(n)!.text);
238
+ const refs: number[] = [];
239
+ let depth = 0;
240
+ for (;;) {
241
+ const u = this.peek();
242
+ if (u === undefined) break;
243
+ if (depth === 0) {
244
+ if (u.kind === "punct" && (u.text === ";" || u.text === ")" || (options.inList === true && u.text === ","))) break;
245
+ if (this.p > startP && u.kind === "ident" && known.includes(u.text.toUpperCase())) break;
246
+ // A query or a second statement is not a clause: `CREATE TABLE ... AS SELECT`, `... PARALLEL WITH CREATE ...`.
247
+ if (kw(u, "SELECT", "CREATE", "INSERT")) this.fail(`expected a clause (${u.text.toUpperCase()} starts a query or a statement, which is not declared here)`, u);
248
+ }
249
+ depth += this.depthChange(u);
250
+ if (depth < 0) break;
251
+ if (u.kind === "ref") refs.push(u.part);
252
+ this.p++;
253
+ }
254
+ if (depth !== 0) this.fail(`unbalanced parentheses in the ${words.join(" ")} clause`, t);
255
+ const clause: UnknownClause = { clause: words.join(" "), span: this.span(from, refs), ...(options.column !== undefined ? { column: options.column } : {}) };
256
+ this.unknownClauses.push(clause);
257
+ return clause;
258
+ }
259
+
164
260
  /** `( ... )` balanced: the span between the parentheses. */
165
261
  protected parenthesized(): Span {
166
262
  this.expectPunct("(");
@@ -12,7 +12,7 @@
12
12
 
13
13
  import type { AttrRef } from "@intentius/chant/attrref";
14
14
  import { isTrivia, SqlSyntaxError, tokenize, tokenizeText, untokenize, type LexicalRules, type Token } from "./tokens";
15
- import type { Span } from "./cursor";
15
+ import type { Span, UnknownClause } from "./cursor";
16
16
  import type { LineageEdge } from "./references";
17
17
  import type { SqlObject } from "./entity";
18
18
  import { interpolationLine, spliceText, SqlTemplateError } from "./template";
@@ -124,6 +124,37 @@ export function spanText(ctx: TemplateCtx, span: Span | undefined, path?: string
124
124
  return untokenize(ctx.tokens.slice(span.from, span.to), (i) => ctx.d.renderReference(ctx.values[i], i)).trim();
125
125
  }
126
126
 
127
+ /**
128
+ * A clause the parser does not know (#3872), as an entity's props carry it:
129
+ * kept in the DDL as written, its text here so a plan can compare it and a
130
+ * build can warn about it.
131
+ */
132
+ export interface UnparsedClause {
133
+ /** Its leading words, upper case: `TABLESPACE_POLICY`. */
134
+ clause: string;
135
+ /** The clause as written, interpolations rendered. */
136
+ text: string;
137
+ /** The column (or dictionary attribute) it is written on, when it is one of a column's. */
138
+ column?: string;
139
+ /** The line of the statement (the props' `ddl`) it starts on, from 1. */
140
+ line: number;
141
+ }
142
+
143
+ /** A parsed statement's unknown clauses as props carry them; undefined when there are none. */
144
+ export function unparsedClauses(ctx: TemplateCtx, unknown: readonly UnknownClause[] | undefined): UnparsedClause[] | undefined {
145
+ if (!unknown || unknown.length === 0) return undefined;
146
+ const render = (i: number) => ctx.d.renderReference(ctx.values[i], i);
147
+ return unknown.map((u) => {
148
+ const before = untokenize(ctx.tokens.slice(0, u.span.from), render).replace(/^\s+/, "");
149
+ return {
150
+ clause: u.clause,
151
+ text: spanText(ctx, u.span, "unparsed") ?? u.clause,
152
+ ...(u.column !== undefined ? { column: u.column } : {}),
153
+ line: before.split("\n").length,
154
+ };
155
+ });
156
+ }
157
+
127
158
  /** A parse error as the tag reports it: on the template line, or in the interpolated text it came from. */
128
159
  export function templateSyntaxError(tag: string, parts: readonly string[], err: SqlSyntaxError): SqlTemplateError {
129
160
  const where =
@@ -20,7 +20,7 @@ import { isAccessStatement, parseAccess, type AccessNode } from "../clickhouse/a
20
20
  import type { Token } from "../clickhouse/tokens";
21
21
  import { CLICKHOUSE_TAG_OF } from "../clickhouse/import/generator";
22
22
  import { describeStatement, type ImportedObject } from "../clickhouse/import/ir";
23
- import { firstLine, quoteBare, splitTokens, tokensText, type ReadOptions } from "./common";
23
+ import { fileUnparsed, firstLine, lineAt, quoteBare, splitTokens, tokensText, type FileLine, type ReadOptions } from "./common";
24
24
  import { templateSafe, templateStrings } from "./postgres";
25
25
 
26
26
  type Tag = (strings: TemplateStringsArray, ...values: unknown[]) => Declarable;
@@ -82,15 +82,16 @@ export function clickhouseTag(type: string): Tag | undefined {
82
82
  }
83
83
 
84
84
  /** The objects a file of ClickHouse DDL declares. Problems are collected, not thrown, so a file is reported whole. */
85
- export function readClickHouse(ddl: string, options: ReadOptions, problems: string[]): ImportedObject[] {
86
- const objects: ImportedObject[] = [];
85
+ export function readClickHouse(ddl: string, options: ReadOptions, problems: string[]): Array<ImportedObject & FileLine> {
86
+ const objects: Array<ImportedObject & FileLine> = [];
87
87
  for (const stmt of splitTokens(tokenizeText(ddl, 0), isTrivia)) {
88
88
  let sql = tokensText(stmt);
89
+ const line = lineAt(ddl, stmt[0]!.start);
89
90
  if (isAccessStatement(sql)) {
90
91
  try {
91
92
  const read = readAccess(sql, options);
92
93
  if (typeof read === "string") problems.push(`${read}: ${firstLine(sql)}`);
93
- else objects.push(read);
94
+ else objects.push({ ...read, line });
94
95
  } catch (e) {
95
96
  problems.push(`does not parse (${e instanceof Error ? e.message : String(e)}): ${firstLine(sql)}`);
96
97
  }
@@ -109,7 +110,8 @@ export function readClickHouse(ddl: string, options: ReadOptions, problems: stri
109
110
  const at = tokens.indexOf(only);
110
111
  sql = tokensText(tokens.slice(0, at)) + `${quoteBare(options.schema)}.` + tokensText(tokens.slice(at));
111
112
  }
112
- objects.push(describeStatement(sql));
113
+ const unparsed = fileUnparsed(node.unknown, tokens, sql, line);
114
+ objects.push({ ...describeStatement(sql), line, ...(unparsed ? { unparsed } : {}) });
113
115
  } catch (e) {
114
116
  problems.push(`does not parse (${e instanceof Error ? e.message : String(e)}): ${firstLine(sql)}`);
115
117
  }
@@ -1,6 +1,7 @@
1
1
  /** What reading a file of DDL shares across dialects. */
2
2
 
3
3
  import type { Token } from "../core/tokens";
4
+ import type { UnknownClause } from "../core/cursor";
4
5
 
5
6
  export interface ReadOptions {
6
7
  /** Names the DDL in errors: a file's path, a command. */
@@ -47,6 +48,28 @@ export function splitTokens(tokens: readonly Token[], isTrivia: (t: Token) => bo
47
48
 
48
49
  export const tokensText = (tokens: readonly Token[]): string => tokens.map((t) => t.text).join("");
49
50
 
51
+ /** Where an object's statement starts in its file, and the clauses in it chant does not read (#3872). */
52
+ export interface FileLine {
53
+ /** The line its statement starts on, from 1. */
54
+ line?: number;
55
+ /** Each clause the parser does not know, with the file line it starts on; absent when there are none. */
56
+ unparsed?: Array<{ clause: string; line: number; column?: string }>;
57
+ }
58
+
59
+ /** A statement's unknown clauses with their lines in the file: `line` is the statement's, `sql` its text, `tokens` its tokens. */
60
+ export function fileUnparsed(
61
+ unknown: readonly UnknownClause[] | undefined,
62
+ tokens: readonly Token[],
63
+ sql: string,
64
+ line: number,
65
+ ): FileLine["unparsed"] {
66
+ if (!unknown || unknown.length === 0) return undefined;
67
+ return unknown.map((u) => ({ clause: u.clause, line: line + lineAt(sql, tokens[u.span.from]?.start ?? 0) - 1, ...(u.column !== undefined ? { column: u.column } : {}) }));
68
+ }
69
+
70
+ /** The line, from 1, that offset `at` of `text` is on. */
71
+ export const lineAt = (text: string, at: number): number => text.slice(0, at).split("\n").length;
72
+
50
73
  /** A statement's first line, for a message. */
51
74
  export const firstLine = (s: string): string => (s.split("\n")[0] ?? "").slice(0, 100);
52
75
 
@@ -26,7 +26,7 @@ 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";
28
28
  import type { ImportedPgObject } from "../postgres/import/ir";
29
- import { firstLine, quoteBare, splitTokens, tokensText, type ReadOptions } from "./common";
29
+ import { fileUnparsed, firstLine, lineAt, quoteBare, splitTokens, tokensText, type FileLine, type ReadOptions } from "./common";
30
30
 
31
31
  type Tag = (strings: TemplateStringsArray, ...values: unknown[]) => Declarable;
32
32
 
@@ -54,6 +54,10 @@ interface PgFileObject {
54
54
  statements: Token[][];
55
55
  /** Table constraints folded in from ALTER TABLE ... ADD, as SQL text. */
56
56
  constraints: string[];
57
+ /** The line its CREATE starts on in the file. */
58
+ line: number;
59
+ /** The clauses in it the parser does not know, with their lines (#3872). */
60
+ unparsed: FileLine["unparsed"];
57
61
  }
58
62
 
59
63
  /** Transaction control, which declares nothing. */
@@ -69,8 +73,8 @@ function isTransactionControl(sig: readonly Token[]): boolean {
69
73
  * joined as one declaration's DDL. Problems are collected, not thrown, so a
70
74
  * file is reported whole.
71
75
  */
72
- export function readPostgres(ddl: string, options: ReadOptions, problems: string[]): ImportedPgObject[] {
73
- const parsed: Array<{ tokens: Token[]; node: StatementNode }> = [];
76
+ export function readPostgres(ddl: string, options: ReadOptions, problems: string[]): Array<ImportedPgObject & FileLine> {
77
+ const parsed: Array<{ tokens: Token[]; node: StatementNode; line: number }> = [];
74
78
  const alters: Array<{ table: string[]; constraint: string; sql: string }> = [];
75
79
  for (const stmt of splitTokens(tokenizeText(ddl, 0), isTrivia)) {
76
80
  const sig = stmt.filter((t) => !isTrivia(t));
@@ -89,7 +93,7 @@ export function readPostgres(ddl: string, options: ReadOptions, problems: string
89
93
  }
90
94
  try {
91
95
  const tokens = tokenizeText(sql, 0);
92
- for (const node of parseStatements(tokens)) parsed.push({ tokens, node });
96
+ for (const node of parseStatements(tokens)) parsed.push({ tokens, node, line: lineAt(ddl, stmt[0]!.start) });
93
97
  } catch (e) {
94
98
  const what = word(sig[0], "ALTER") && word(sig[1], "TABLE") ? "an ALTER TABLE other than ADD of a table constraint or ROW LEVEL SECURITY" : `does not parse (${e instanceof Error ? e.message : String(e)})`;
95
99
  problems.push(`${what}: ${firstLine(sql)}`);
@@ -118,7 +122,7 @@ export function readPostgres(ddl: string, options: ReadOptions, problems: string
118
122
  /** Objects by namespace and name: `rel` (anything in a schema), `index`, `schema`, `extension`, `role`, and `trigger`/`policy` with their table. */
119
123
  const owners = new Map<string, PgFileObject>();
120
124
  const followOns: Array<{ tokens: Token[]; node: StatementNode; inserts: Set<number> }> = [];
121
- for (const { tokens, node } of parsed) {
125
+ for (const { tokens, node, line } of parsed) {
122
126
  const inserts = new Set<number>();
123
127
  /** Qualifies an unqualified name in the statement, when `schema` is set. */
124
128
  const mark = (n: NameNode | undefined) => {
@@ -170,7 +174,7 @@ export function readPostgres(ddl: string, options: ReadOptions, problems: string
170
174
  }
171
175
  const ownName = !["schema", "extension", "index", "trigger", "policy", "role", "grant"].includes(node.statement);
172
176
  if (ownName && "name" in node) mark(node.name);
173
- const object: PgFileObject = { node, statements: [rewrite(tokens, inserts, options.schema)], constraints: [] };
177
+ const object: PgFileObject = { node, statements: [rewrite(tokens, inserts, options.schema)], constraints: [], line, unparsed: fileUnparsed("unknown" in node ? node.unknown : undefined, tokens, tokensText(tokens), line) };
174
178
  objects.push(object);
175
179
  const k = ownerKey(node, tokens, nameOf, qualify);
176
180
  if (k) owners.set(k, object);
@@ -196,7 +200,7 @@ export function readPostgres(ddl: string, options: ReadOptions, problems: string
196
200
  table.constraints.push(qualifyConstraint(alter.constraint, options.schema));
197
201
  }
198
202
 
199
- const out: ImportedPgObject[] = [];
203
+ const out: Array<ImportedPgObject & FileLine> = [];
200
204
  for (const o of objects) {
201
205
  const [create = [], ...rest] = o.statements;
202
206
  const ddlText = [o.constraints.length > 0 ? addConstraints(create, o.constraints) : tokensText(create), ...rest.map((c) => tokensText(c))].join(";\n");
@@ -205,7 +209,7 @@ export function readPostgres(ddl: string, options: ReadOptions, problems: string
205
209
  problems.push(`${identity}: ${firstLine(ddlText)}`);
206
210
  continue;
207
211
  }
208
- out.push({ ...identity, ddl: ddlText });
212
+ out.push({ ...identity, ddl: ddlText, line: o.line, ...(o.unparsed ? { unparsed: o.unparsed } : {}) });
209
213
  }
210
214
  return out;
211
215
  }
@@ -9,7 +9,8 @@
9
9
  * anything else is ClickHouse.
10
10
  *
11
11
  * A statement that declares nothing, or does not parse, is an error naming
12
- * it (`./sql-files.ts`); none is left out.
12
+ * it (`./sql-files.ts`); none is left out. A clause the parser does not know
13
+ * (#3872) is kept in the template as written.
13
14
  */
14
15
 
15
16
  import type { ParserOptions, TemplateIR, TemplateParser } from "@intentius/chant/import/parser";
@@ -8,10 +8,23 @@
8
8
  */
9
9
 
10
10
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
11
- import { TABLE_ENGINES, TYPE_FAMILIES } from "../../generated/clickhouse";
12
11
  import type { ColumnDef, EngineDef } from "../../clickhouse/entities";
12
+ import { baseEngineName, splitTop } from "../../clickhouse/expressions";
13
+ import { readClickHouse, type ClickHouseTableInfo, type ClickHouseViewInfo } from "../../read/clickhouse";
13
14
  import { clickhouseObjects, type OutputObject } from "./sql-helpers";
14
15
 
16
+ export {
17
+ bareName,
18
+ baseEngineName,
19
+ engineSpec,
20
+ isMergeTree,
21
+ isNullableType,
22
+ keyElements,
23
+ splitTop,
24
+ typeFamily,
25
+ unwrapType,
26
+ } from "../../clickhouse/expressions";
27
+
15
28
  export interface TableObject extends OutputObject {
16
29
  columns: ColumnDef[];
17
30
  indexes: Array<{ name: string; expr: string; type: string; granularity?: string }>;
@@ -38,28 +51,14 @@ export const isMaterializedView = (o: OutputObject): o is ViewObject => o.type =
38
51
  export const isAnyView = (o: OutputObject): o is ViewObject =>
39
52
  o.type === "ClickHouse::View" || o.type === "ClickHouse::MaterializedView";
40
53
 
41
- export function tablesOf(ctx: PostSynthContext): TableObject[] {
42
- return clickhouseObjects(ctx).filter(isTable);
43
- }
44
-
45
- export function viewsOf(ctx: PostSynthContext): ViewObject[] {
46
- return clickhouseObjects(ctx).filter(isAnyView);
47
- }
48
-
49
- /** The engine's catalog entry, when the pinned server has the engine. */
50
- export function engineSpec(engine: EngineDef | undefined) {
51
- return engine ? (TABLE_ENGINES as Record<string, (typeof TABLE_ENGINES)[keyof typeof TABLE_ENGINES]>)[engine.name] : undefined;
52
- }
53
-
54
- /** A MergeTree family engine, replicated or not. */
55
- export function isMergeTree(engine: EngineDef | undefined): boolean {
56
- return engineSpec(engine)?.mergeTree === true;
54
+ /** The build's tables as the public read API types them (`@intentius/chant-lexicon-sql/read`), so a project policy reads what the checks read. */
55
+ export function tablesOf(ctx: PostSynthContext): ClickHouseTableInfo[] {
56
+ return readClickHouse(ctx).tables;
57
57
  }
58
58
 
59
- /** The engine without its `Replicated` prefix: `ReplicatedReplacingMergeTree` is `ReplacingMergeTree`. */
60
- export function baseEngineName(engine: EngineDef): string {
61
- const spec = engineSpec(engine) as { replicates?: string } | undefined;
62
- return spec?.replicates ?? engine.name;
59
+ /** The build's views and materialized views, as the public read API types them. */
60
+ export function viewsOf(ctx: PostSynthContext): ClickHouseViewInfo[] {
61
+ return readClickHouse(ctx).views;
63
62
  }
64
63
 
65
64
  /**
@@ -72,63 +71,6 @@ export function engineArguments(engine: EngineDef): string[] {
72
71
  return args;
73
72
  }
74
73
 
75
- /** Split on commas that are not inside parentheses, brackets or quotes. */
76
- export function splitTop(text: string, separator = ","): string[] {
77
- const parts: string[] = [];
78
- let depth = 0;
79
- let quote: string | undefined;
80
- let start = 0;
81
- for (let i = 0; i < text.length; i++) {
82
- const c = text[i]!;
83
- if (quote) {
84
- if (c === "\\") i++;
85
- else if (c === quote) quote = undefined;
86
- continue;
87
- }
88
- if (c === "'" || c === '"' || c === "`") quote = c;
89
- else if (c === "(" || c === "[") depth++;
90
- else if (c === ")" || c === "]") depth--;
91
- else if (c === separator && depth === 0) {
92
- parts.push(text.slice(start, i).trim());
93
- start = i + 1;
94
- }
95
- }
96
- const last = text.slice(start).trim();
97
- if (last !== "" || parts.length > 0) parts.push(last);
98
- return parts.filter((p) => p !== "");
99
- }
100
-
101
- /** `(a, b)` is the list `a`, `b`; `a, b` is the same; `tuple()` and `()` are empty. */
102
- export function keyElements(expr: string): string[] {
103
- let text = expr.trim();
104
- if (/^tuple\s*\(\s*\)$/i.test(text) || text === "()") return [];
105
- if (text.startsWith("(") && closingParen(text, 0) === text.length - 1) text = text.slice(1, -1);
106
- return splitTop(text).map((e) => e.replace(/\s+(ASC|DESC)$/i, "").replace(/\s+/g, " ").trim());
107
- }
108
-
109
- function closingParen(text: string, open: number): number {
110
- let depth = 0;
111
- let quote: string | undefined;
112
- for (let i = open; i < text.length; i++) {
113
- const c = text[i]!;
114
- if (quote) {
115
- if (c === "\\") i++;
116
- else if (c === quote) quote = undefined;
117
- } else if (c === "'" || c === '"' || c === "`") quote = c;
118
- else if (c === "(") depth++;
119
- else if (c === ")" && --depth === 0) return i;
120
- }
121
- return -1;
122
- }
123
-
124
- /** The column a bare name refers to: `a` or `` `a b` ``, else undefined. */
125
- export function bareName(element: string): string | undefined {
126
- const text = element.trim();
127
- if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(text)) return text;
128
- const quoted = /^`((?:[^`]|``)*)`$/.exec(text) ?? /^"((?:[^"]|"")*)"$/.exec(text);
129
- return quoted ? quoted[1]!.replace(/``|""/g, (m) => m[0]!) : undefined;
130
- }
131
-
132
74
  const WORDS = new Set(
133
75
  (
134
76
  "AS AND OR NOT IN IS NULL LIKE ILIKE BETWEEN CASE WHEN THEN ELSE END ASC DESC INTERVAL DISTINCT TRUE FALSE " +
@@ -154,25 +96,6 @@ export function mentionedNames(expr: string): string[] {
154
96
  return names;
155
97
  }
156
98
 
157
- /** A type with its `Nullable(...)` and `LowCardinality(...)` wrappers removed. */
158
- export function unwrapType(type: string | undefined): string {
159
- let t = (type ?? "").trim();
160
- for (;;) {
161
- const m = /^(Nullable|LowCardinality)\s*\(([\s\S]*)\)$/i.exec(t);
162
- if (!m) return t;
163
- t = m[2]!.trim();
164
- }
165
- }
166
-
167
- /** The canonical family of a column's type (`BIGINT UNSIGNED` is `UInt64`), or its leading word when unknown. */
168
- export function typeFamily(type: string | undefined): string {
169
- const head = /^[A-Za-z_][A-Za-z0-9_ ]*?(?=\s*\(|$)/.exec(unwrapType(type))?.[0]?.trim() ?? "";
170
- const table = TYPE_FAMILIES as Record<string, { canonical: string } | undefined>;
171
- return (table[head] ?? table[head.toUpperCase()])?.canonical ?? head;
172
- }
173
-
174
- export const isNullableType = (type: string | undefined): boolean => /^Nullable\s*\(/i.test((type ?? "").trim());
175
-
176
99
  export function columnByName(table: { columns: ColumnDef[] }, name: string): ColumnDef | undefined {
177
100
  return table.columns.find((c) => c.name === name);
178
101
  }
@@ -8,54 +8,26 @@
8
8
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
9
  import { POSTGRES_MAJORS, VERSION_RANGES } from "../../generated/postgres";
10
10
  import type { VersionRange } from "../../postgres/catalog-types";
11
- import type { ColumnDef, KeyDef } from "../../postgres/entities";
12
- import { postgresMajorOf, postgresObjects, type OutputObject } from "./sql-helpers";
11
+ import { readPostgres, type PostgresIndexInfo, type PostgresTableInfo, type PostgresViewInfo } from "../../read/postgres";
12
+ import type { PostgresIndexJson, PostgresTableJson, PostgresViewJson } from "../../read/objects";
13
+ import { postgresMajorOf, type OutputObject } from "./sql-helpers";
13
14
 
14
- export interface PgTable extends OutputObject {
15
- sqlName: string;
16
- like: string[];
17
- schema?: string;
18
- comment?: string;
19
- columns: Array<ColumnDef & { comment?: string }>;
20
- primaryKey?: KeyDef;
21
- uniques: KeyDef[];
22
- checks: Array<{ name?: string; expr: string; notEnforced?: boolean }>;
23
- foreignKeys: Array<{ name?: string; columns: string[]; refTable: string; refColumns: string[] }>;
24
- exclusions: unknown[];
25
- inherits: unknown[];
26
- partitionOf?: unknown;
27
- ofType?: unknown;
28
- persistence?: string;
29
- with?: string;
30
- }
31
-
32
- export interface PgIndex extends OutputObject {
33
- sqlName: string;
34
- schema?: string;
35
- table: string;
36
- tableName: string;
37
- unique?: boolean;
38
- method?: string;
39
- elements: Array<{ expr: string; column?: string }>;
40
- include?: string;
41
- where?: string;
42
- with?: string;
43
- }
44
-
45
- export interface PgView extends OutputObject {
46
- sqlName: string;
47
- schema?: string;
48
- query: string;
49
- with?: string;
50
- }
15
+ /**
16
+ * The objects as the public read API types them (`../../read/postgres.ts`,
17
+ * `@intentius/chant-lexicon-sql/read`): the checks and a project's own
18
+ * `lint.policies` read the same values.
19
+ */
20
+ export type PgTable = PostgresTableInfo;
21
+ export type PgIndex = PostgresIndexInfo;
22
+ export type PgView = PostgresViewInfo;
51
23
 
52
- export const isTable = (o: OutputObject): o is PgTable => o.type === "Postgres::Table";
53
- export const isIndex = (o: OutputObject): o is PgIndex => o.type === "Postgres::Index";
54
- export const isView = (o: OutputObject): o is PgView => o.type === "Postgres::View";
55
- export const isMaterializedView = (o: OutputObject): o is PgView => o.type === "Postgres::MaterializedView";
24
+ export const isTable = (o: OutputObject): o is OutputObject & PostgresTableJson => o.type === "Postgres::Table";
25
+ export const isIndex = (o: OutputObject): o is OutputObject & PostgresIndexJson => o.type === "Postgres::Index";
26
+ export const isView = (o: OutputObject): o is OutputObject & PostgresViewJson => o.type === "Postgres::View";
27
+ export const isMaterializedView = (o: OutputObject): o is OutputObject & PostgresViewJson => o.type === "Postgres::MaterializedView";
56
28
 
57
- export const tablesOf = (ctx: PostSynthContext): PgTable[] => postgresObjects(ctx).filter(isTable);
58
- export const indexesOf = (ctx: PostSynthContext): PgIndex[] => postgresObjects(ctx).filter(isIndex);
29
+ export const tablesOf = (ctx: PostSynthContext): PgTable[] => readPostgres(ctx).tables;
30
+ export const indexesOf = (ctx: PostSynthContext): PgIndex[] => readPostgres(ctx).indexes;
59
31
 
60
32
  /** The major the build targets (`postgresMajor` in the output), else the newest pinned. */
61
33
  export const targetMajor = (ctx: PostSynthContext): number => postgresMajorOf(ctx, PINNED_MAJOR);
@@ -87,14 +59,7 @@ export function storageParams(text: string | undefined): Map<string, string> {
87
59
  }
88
60
 
89
61
  /** The leading bare-column elements of an index, in order; stops at the first expression. */
90
- export function leadingColumns(index: PgIndex): string[] {
91
- const out: string[] = [];
92
- for (const e of index.elements) {
93
- if (!e.column) break;
94
- out.push(e.column);
95
- }
96
- return out;
97
- }
62
+ export const leadingColumns = (index: PgIndex): string[] => index.columns;
98
63
 
99
64
  /** A check over the build's Postgres objects, so each check file holds its rule. */
100
65
  export function checkOf(
@@ -12,13 +12,13 @@
12
12
  */
13
13
 
14
14
  import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
15
- import { COLUMN_TYPES, FUNCTIONS, INDEX_ACCESS_METHODS, KEYWORDS, OPERATOR_CLASSES } from "../../generated/postgres";
15
+ import { COLUMN_TYPES, FUNCTIONS, INDEX_ACCESS_METHODS, KEYWORDS, OPERATOR_CLASSES, POSTGRES_MAJORS } from "../../generated/postgres";
16
16
  import { EXTENSION_OBJECTS } from "../../postgres/overlays/extension-objects";
17
17
  import { GRAMMAR_TYPE_SPELLINGS, SERIAL_TYPES, SERIAL_WIDTH_SPELLINGS, TYPE_SPELLINGS } from "../../postgres/overlays/types";
18
18
  import { isTrivia, tokenizeText, type Token } from "../../postgres/tokens";
19
19
  import type { ColumnTypeSpec } from "../../postgres/catalog-types";
20
- import { targetMajor, type PgTable } from "./postgres-helpers";
21
- import { postgresObjects, type OutputObject } from "./sql-helpers";
20
+ import { dialectObjects, recordedPostgresMajor, type PostgresTableJson as PgTable } from "../../read/objects";
21
+ import type { OutputObject } from "./sql-helpers";
22
22
 
23
23
  // ── Catalog lookups ────────────────────────────────────────────────────
24
24
 
@@ -120,7 +120,7 @@ export interface PgProject {
120
120
  }
121
121
 
122
122
  export function projectOf(ctx: PostSynthContext): PgProject {
123
- return projectFromObjects(postgresObjects(ctx), targetMajor(ctx));
123
+ return projectFromObjects(dialectObjects(ctx, "postgres"), recordedPostgresMajor(ctx, POSTGRES_MAJORS[POSTGRES_MAJORS.length - 1]!));
124
124
  }
125
125
 
126
126
  /** The project a set of output objects declares, at `major`: for a reader that holds the build output itself (`chant sql types`). */
@@ -386,7 +386,7 @@ function providesFunction(e: (typeof EXTENSION_OBJECTS)[string], name: string):
386
386
  export function expressionsOf(o: OutputObject): Array<{ where: string; expr: string }> {
387
387
  const out: Array<{ where: string; expr: string }> = [];
388
388
  if (o.type === "Postgres::Table") {
389
- const t = o as PgTable;
389
+ const t = o as unknown as PgTable;
390
390
  for (const c of t.columns) {
391
391
  if (c.default) out.push({ where: `column ${c.name}'s DEFAULT`, expr: c.default });
392
392
  if (c.generated?.expr) out.push({ where: `generated column ${c.name}`, expr: c.generated.expr });
@@ -483,7 +483,7 @@ export function interpolatedTable(p: PgProject, ref: unknown, sqlName: string |
483
483
  const o = p.byExport.get(ref);
484
484
  if (!o || o.type !== "Postgres::Table") return undefined;
485
485
  if (sqlName !== undefined && o.sqlName !== sqlName) return undefined;
486
- return o as PgTable;
486
+ return o as unknown as PgTable;
487
487
  }
488
488
 
489
489
  /**