@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.
- package/dist/clickhouse/apply/statements.d.ts.map +1 -1
- package/dist/clickhouse/entities.d.ts +7 -1
- package/dist/clickhouse/entities.d.ts.map +1 -1
- package/dist/clickhouse/expressions.d.ts +27 -0
- package/dist/clickhouse/expressions.d.ts.map +1 -0
- package/dist/clickhouse/parser.d.ts +12 -2
- package/dist/clickhouse/parser.d.ts.map +1 -1
- package/dist/clickhouse/plan/diff.d.ts.map +1 -1
- package/dist/clickhouse/plan/normalize.d.ts +11 -1
- package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
- package/dist/clickhouse/plan/rebuild-handoff.d.ts.map +1 -1
- package/dist/clickhouse/plan/rules.d.ts +1 -0
- package/dist/clickhouse/plan/rules.d.ts.map +1 -1
- package/dist/clickhouse.d.ts +1 -0
- package/dist/clickhouse.d.ts.map +1 -1
- package/dist/codegen/classifier-page.d.ts.map +1 -1
- package/dist/codegen/postgres-classifier-page.d.ts.map +1 -1
- package/dist/core/cursor.d.ts +41 -0
- package/dist/core/cursor.d.ts.map +1 -1
- package/dist/core/find-templates.d.ts +1 -1
- package/dist/core/find-templates.d.ts.map +1 -1
- package/dist/core/interpolation.d.ts +18 -1
- package/dist/core/interpolation.d.ts.map +1 -1
- package/dist/files/clickhouse.d.ts +2 -2
- package/dist/files/clickhouse.d.ts.map +1 -1
- package/dist/files/common.d.ts +16 -0
- package/dist/files/common.d.ts.map +1 -1
- package/dist/files/postgres.d.ts +2 -2
- package/dist/files/postgres.d.ts.map +1 -1
- package/dist/import-parser.d.ts +2 -1
- package/dist/import-parser.d.ts.map +1 -1
- package/dist/integrity.json +24 -24
- package/dist/lint/post-synth/clickhouse-helpers.d.ts +6 -19
- package/dist/lint/post-synth/clickhouse-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/postgres-helpers.d.ts +15 -55
- package/dist/lint/post-synth/postgres-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/postgres-names.d.ts +2 -2
- package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
- package/dist/lint/post-synth/sql-helpers.d.ts +5 -6
- package/dist/lint/post-synth/sql-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch102.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch103.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch104.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch105.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch106.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch107.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch111.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch112.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch113.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch114.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch115.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch120.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch125.d.ts.map +1 -1
- package/dist/lint/rules/postgres-templates.d.ts +1 -1
- package/dist/lint/rules/postgres-templates.d.ts.map +1 -1
- package/dist/lint/rules/templates.d.ts +1 -1
- package/dist/lint/rules/templates.d.ts.map +1 -1
- package/dist/lint/rules/typed-names.d.ts +1 -1
- package/dist/lint/rules/typed-names.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +7 -1
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/parser.d.ts +10 -2
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +1 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres.d.ts +1 -0
- package/dist/postgres.d.ts.map +1 -1
- package/dist/read/clickhouse.d.ts +66 -0
- package/dist/read/clickhouse.d.ts.map +1 -0
- package/dist/read/objects.d.ts +99 -0
- package/dist/read/objects.d.ts.map +1 -0
- package/dist/read/postgres.d.ts +83 -0
- package/dist/read/postgres.d.ts.map +1 -0
- package/dist/read.d.ts +49 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/rules/clickhouse-helpers.ts +20 -97
- package/dist/rules/postgres-helpers.ts +18 -53
- package/dist/rules/postgres-names.ts +6 -6
- package/dist/rules/postgres-templates.ts +1 -1
- package/dist/rules/sql-helpers.ts +8 -28
- package/dist/rules/sqlch003.ts +1 -1
- package/dist/rules/sqlch102.ts +2 -3
- package/dist/rules/sqlch103.ts +2 -3
- package/dist/rules/sqlch104.ts +2 -3
- package/dist/rules/sqlch105.ts +2 -3
- package/dist/rules/sqlch106.ts +2 -3
- package/dist/rules/sqlch107.ts +2 -3
- package/dist/rules/sqlch111.ts +2 -3
- package/dist/rules/sqlch112.ts +2 -3
- package/dist/rules/sqlch113.ts +2 -3
- package/dist/rules/sqlch114.ts +2 -3
- package/dist/rules/sqlch115.ts +2 -3
- package/dist/rules/sqlch120.ts +2 -3
- package/dist/rules/sqlch125.ts +2 -3
- package/dist/rules/sqlpg002.ts +1 -1
- package/dist/rules/templates.ts +1 -1
- package/dist/rules/typed-names.ts +1 -1
- package/dist/serializer.d.ts +11 -1
- package/dist/serializer.d.ts.map +1 -1
- package/dist/sql-files.d.ts +17 -1
- package/dist/sql-files.d.ts.map +1 -1
- package/dist/sql-types.d.ts +3 -0
- package/dist/sql-types.d.ts.map +1 -1
- package/package.json +2 -3
- package/src/clickhouse/apply/statements.ts +2 -0
- package/src/clickhouse/entities.ts +8 -1
- package/src/clickhouse/expressions.ts +108 -0
- package/src/clickhouse/parser.ts +113 -31
- package/src/clickhouse/plan/diff.ts +19 -0
- package/src/clickhouse/plan/normalize.ts +27 -2
- package/src/clickhouse/plan/rebuild-handoff.ts +2 -1
- package/src/clickhouse/plan/rules.ts +7 -0
- package/src/clickhouse/testdata/server-ddl-baseline.json +67 -0
- package/src/clickhouse.ts +10 -0
- package/src/codegen/classifier-page.ts +2 -0
- package/src/codegen/postgres-classifier-page.ts +2 -0
- package/src/core/cursor.ts +96 -0
- package/src/core/find-templates.ts +1 -1
- package/src/core/interpolation.ts +32 -1
- package/src/files/clickhouse.ts +7 -5
- package/src/files/common.ts +23 -0
- package/src/files/postgres.ts +12 -8
- package/src/import-parser.ts +2 -1
- package/src/lint/post-synth/clickhouse-helpers.ts +20 -97
- package/src/lint/post-synth/postgres-helpers.ts +18 -53
- package/src/lint/post-synth/postgres-names.ts +6 -6
- package/src/lint/post-synth/sql-helpers.ts +8 -28
- package/src/lint/post-synth/sqlch102.ts +2 -3
- package/src/lint/post-synth/sqlch103.ts +2 -3
- package/src/lint/post-synth/sqlch104.ts +2 -3
- package/src/lint/post-synth/sqlch105.ts +2 -3
- package/src/lint/post-synth/sqlch106.ts +2 -3
- package/src/lint/post-synth/sqlch107.ts +2 -3
- package/src/lint/post-synth/sqlch111.ts +2 -3
- package/src/lint/post-synth/sqlch112.ts +2 -3
- package/src/lint/post-synth/sqlch113.ts +2 -3
- package/src/lint/post-synth/sqlch114.ts +2 -3
- package/src/lint/post-synth/sqlch115.ts +2 -3
- package/src/lint/post-synth/sqlch120.ts +2 -3
- package/src/lint/post-synth/sqlch125.ts +2 -3
- package/src/lint/rules/postgres-templates.ts +1 -1
- package/src/lint/rules/sqlch003.ts +1 -1
- package/src/lint/rules/sqlpg002.ts +1 -1
- package/src/lint/rules/templates.ts +1 -1
- package/src/lint/rules/typed-names.ts +1 -1
- package/src/lint/rules/typescript-7.test.ts +101 -0
- package/src/lsp/completions.ts +1 -1
- package/src/lsp/postgres-template.ts +1 -1
- package/src/lsp/postgres.ts +1 -1
- package/src/lsp/refs.ts +1 -1
- package/src/lsp/template.ts +1 -1
- package/src/postgres/apply/statements.ts +2 -0
- package/src/postgres/entities.ts +9 -1
- package/src/postgres/parser.ts +31 -8
- package/src/postgres/plan/diff.ts +18 -0
- package/src/postgres/plan/normalize.ts +12 -0
- package/src/postgres/plan/rules.ts +7 -0
- package/src/postgres/testdata/server-ddl-baseline.json +38 -0
- package/src/postgres.ts +12 -0
- package/src/read/clickhouse.ts +137 -0
- package/src/read/objects.ts +137 -0
- package/src/read/postgres.ts +180 -0
- package/src/read-policies.e2e.test.ts +144 -0
- package/src/read.test.ts +143 -0
- package/src/read.ts +69 -0
- package/src/serializer.ts +37 -1
- package/src/sql-files.ts +27 -6
- package/src/sql-types.ts +19 -2
- package/src/unknown-clauses.test.ts +227 -0
package/src/postgres/parser.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|