@intentius/chant-lexicon-sql 0.116.0 → 0.118.0
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/access.d.ts +132 -0
- package/dist/clickhouse/access.d.ts.map +1 -0
- package/dist/clickhouse/apply/apply.d.ts.map +1 -1
- package/dist/clickhouse/apply/statements.d.ts +2 -0
- package/dist/clickhouse/apply/statements.d.ts.map +1 -1
- package/dist/clickhouse/entities.d.ts +171 -2
- package/dist/clickhouse/entities.d.ts.map +1 -1
- package/dist/clickhouse/http.d.ts +3 -0
- package/dist/clickhouse/http.d.ts.map +1 -1
- package/dist/clickhouse/import/generator.d.ts +28 -0
- package/dist/clickhouse/import/generator.d.ts.map +1 -1
- package/dist/clickhouse/import/ir.d.ts.map +1 -1
- package/dist/clickhouse/import/parser.d.ts +3 -3
- package/dist/clickhouse/import/parser.d.ts.map +1 -1
- package/dist/clickhouse/live/bind.d.ts.map +1 -1
- package/dist/clickhouse/live/catalog.d.ts +41 -6
- package/dist/clickhouse/live/catalog.d.ts.map +1 -1
- package/dist/clickhouse/live/describe-resources.d.ts +3 -2
- package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
- package/dist/clickhouse/parser.d.ts +54 -3
- package/dist/clickhouse/parser.d.ts.map +1 -1
- package/dist/clickhouse/plan/commands.d.ts +3 -1
- package/dist/clickhouse/plan/commands.d.ts.map +1 -1
- package/dist/clickhouse/plan/deep.d.ts.map +1 -1
- package/dist/clickhouse/plan/diff.d.ts +12 -0
- package/dist/clickhouse/plan/diff.d.ts.map +1 -1
- package/dist/clickhouse/plan/normalize.d.ts +61 -1
- package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
- package/dist/clickhouse/plan/report.d.ts.map +1 -1
- package/dist/clickhouse/plan/rules.d.ts +7 -0
- package/dist/clickhouse/plan/rules.d.ts.map +1 -1
- package/dist/clickhouse/plan/schema.d.ts +8 -4
- package/dist/clickhouse/plan/schema.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/backfill.d.ts +3 -1
- package/dist/clickhouse/rebuild/backfill.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/observe.d.ts +7 -3
- package/dist/clickhouse/rebuild/observe.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/op.d.ts +8 -0
- package/dist/clickhouse/rebuild/op.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/steps.d.ts +48 -0
- package/dist/clickhouse/rebuild/steps.d.ts.map +1 -1
- package/dist/clickhouse/testing/fake-server.d.ts.map +1 -1
- package/dist/clickhouse/testing/writable-server.d.ts.map +1 -1
- package/dist/clickhouse/topology.d.ts.map +1 -1
- package/dist/clickhouse.d.ts +1 -1
- package/dist/clickhouse.d.ts.map +1 -1
- package/dist/codegen/classifier-page.d.ts.map +1 -1
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/config.d.ts +70 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/core/references.d.ts +4 -2
- package/dist/core/references.d.ts.map +1 -1
- package/dist/core/statements.d.ts +11 -0
- package/dist/core/statements.d.ts.map +1 -1
- package/dist/files/build-root.d.ts +26 -0
- package/dist/files/build-root.d.ts.map +1 -0
- package/dist/files/clickhouse.d.ts +19 -0
- package/dist/files/clickhouse.d.ts.map +1 -0
- package/dist/files/common.d.ts +25 -0
- package/dist/files/common.d.ts.map +1 -0
- package/dist/files/postgres.d.ts +36 -0
- package/dist/files/postgres.d.ts.map +1 -0
- package/dist/import-parser.d.ts +25 -0
- package/dist/import-parser.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +8 -8
- package/dist/lint/rules/sqlch001.d.ts +1 -1
- package/dist/lint/rules/sqlch001.d.ts.map +1 -1
- package/dist/lint/rules/sqlch002.d.ts.map +1 -1
- package/dist/lint/rules/sqlpg001.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/lsp/postgres-template.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/meta.json +50 -0
- package/dist/okf/index.md +11 -1
- package/dist/okf/rules/SQLCH001.md +2 -2
- package/dist/okf/types/Dictionary.md +9 -0
- package/dist/okf/types/Function.md +9 -0
- package/dist/okf/types/Grant.md +9 -0
- package/dist/okf/types/PostgresDefaultPrivileges.md +9 -0
- package/dist/okf/types/PostgresGrant.md +9 -0
- package/dist/okf/types/PostgresPolicy.md +9 -0
- package/dist/okf/types/PostgresRole.md +9 -0
- package/dist/okf/types/Role.md +9 -0
- package/dist/okf/types/RowPolicy.md +9 -0
- package/dist/okf/types/User.md +9 -0
- package/dist/op/activities/clickhouse-rebuild.d.ts.map +1 -1
- package/dist/op/activities/postgres-migration.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +19 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/postgres/access/acl.d.ts +119 -0
- package/dist/postgres/access/acl.d.ts.map +1 -0
- package/dist/postgres/access/import.d.ts +13 -0
- package/dist/postgres/access/import.d.ts.map +1 -0
- package/dist/postgres/access/live.d.ts +24 -0
- package/dist/postgres/access/live.d.ts.map +1 -0
- package/dist/postgres/apply/apply.d.ts.map +1 -1
- package/dist/postgres/apply/statements.d.ts +71 -0
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +106 -4
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entity-types.d.ts +4 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts +37 -1
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/import/live-export.d.ts +6 -0
- package/dist/postgres/import/live-export.d.ts.map +1 -1
- package/dist/postgres/live/bind.d.ts +2 -0
- package/dist/postgres/live/bind.d.ts.map +1 -1
- package/dist/postgres/live/catalog.d.ts +11 -2
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/client.d.ts +3 -0
- package/dist/postgres/live/client.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.d.ts +9 -0
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/migrate/op.d.ts +29 -0
- package/dist/postgres/migrate/op.d.ts.map +1 -1
- package/dist/postgres/parser.d.ts +76 -3
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/plan/commands.d.ts +12 -0
- package/dist/postgres/plan/commands.d.ts.map +1 -1
- package/dist/postgres/plan/deep.d.ts.map +1 -1
- package/dist/postgres/plan/diff.d.ts +10 -1
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts +8 -1
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +8 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/schema.d.ts +2 -1
- package/dist/postgres/plan/schema.d.ts.map +1 -1
- package/dist/postgres/plan/server-normalize.d.ts +4 -1
- package/dist/postgres/plan/server-normalize.d.ts.map +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/rules/postgres-templates.ts +2 -2
- package/dist/rules/sqlch001.ts +26 -4
- package/dist/rules/sqlch002.ts +3 -2
- package/dist/rules/sqlpg001.ts +7 -4
- package/dist/rules/templates.ts +2 -2
- package/dist/serializer.d.ts +2 -1
- package/dist/serializer.d.ts.map +1 -1
- package/dist/sql-files.d.ts +79 -0
- package/dist/sql-files.d.ts.map +1 -0
- package/dist/token-source.d.ts +88 -0
- package/dist/token-source.d.ts.map +1 -0
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/clickhouse/access.test.ts +203 -0
- package/src/clickhouse/access.ts +595 -0
- package/src/clickhouse/apply/apply.ts +29 -20
- package/src/clickhouse/apply/statements.ts +105 -8
- package/src/clickhouse/dictionary.test.ts +177 -0
- package/src/clickhouse/entities.ts +356 -6
- package/src/clickhouse/function.test.ts +91 -0
- package/src/clickhouse/http.ts +11 -2
- package/src/clickhouse/import/generator.ts +81 -46
- package/src/clickhouse/import/import.test.ts +9 -3
- package/src/clickhouse/import/ir.ts +6 -2
- package/src/clickhouse/import/parser.ts +10 -18
- package/src/clickhouse/live/access.e2e.test.ts +218 -0
- package/src/clickhouse/live/bind.ts +17 -6
- package/src/clickhouse/live/catalog.ts +106 -10
- package/src/clickhouse/live/describe-resources.ts +23 -10
- package/src/clickhouse/live/dictionaries.e2e.test.ts +186 -0
- package/src/clickhouse/live/functions.e2e.test.ts +155 -0
- package/src/clickhouse/live/live.test.ts +2 -2
- package/src/clickhouse/parser.ts +136 -4
- package/src/clickhouse/plan/commands.ts +18 -7
- package/src/clickhouse/plan/deep.ts +71 -11
- package/src/clickhouse/plan/diff.ts +65 -2
- package/src/clickhouse/plan/normalize.ts +200 -6
- package/src/clickhouse/plan/report.ts +14 -3
- package/src/clickhouse/plan/rules.ts +44 -1
- package/src/clickhouse/plan/schema.ts +40 -17
- package/src/clickhouse/plan/server-format.ts +1 -1
- package/src/clickhouse/rebuild/backfill.ts +5 -25
- package/src/clickhouse/rebuild/observe.ts +7 -3
- package/src/clickhouse/rebuild/op.ts +9 -0
- package/src/clickhouse/rebuild/rebuild.e2e.test.ts +62 -0
- package/src/clickhouse/rebuild/rebuild.test.ts +54 -0
- package/src/clickhouse/rebuild/steps.ts +136 -1
- package/src/clickhouse/testing/fake-server.ts +2 -0
- package/src/clickhouse/testing/writable-server.ts +2 -0
- package/src/clickhouse/topology.ts +17 -2
- package/src/clickhouse.ts +19 -0
- package/src/codegen/classifier-page.ts +2 -0
- package/src/codegen/registry.ts +10 -0
- package/src/config.ts +22 -3
- package/src/core/references.ts +5 -3
- package/src/core/statements.ts +11 -1
- package/src/files/build-root.ts +62 -0
- package/src/files/clickhouse.ts +52 -0
- package/src/files/common.ts +54 -0
- package/src/files/postgres.ts +371 -0
- package/src/generated/lexicon-sql.json +50 -0
- package/src/import-parser.ts +61 -0
- package/src/index.ts +14 -1
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lint/rules/sqlch001.ts +26 -4
- package/src/lint/rules/sqlch002.ts +3 -2
- package/src/lint/rules/sqlpg001.ts +7 -4
- package/src/lint/rules/templates.ts +2 -2
- package/src/lsp/postgres-template.ts +2 -0
- package/src/lsp/template.ts +3 -3
- package/src/mcp/index.ts +6 -6
- package/src/mcp/postgres.ts +1 -1
- package/src/migration-statements.test.ts +25 -0
- package/src/migration-statements.ts +8 -0
- package/src/op/activities/clickhouse-rebuild.ts +1 -0
- package/src/op/activities/postgres-migration.ts +27 -2
- package/src/op/activity-contracts.ts +2 -0
- package/src/plugin.ts +29 -5
- package/src/postgres/access/access.e2e.test.ts +191 -0
- package/src/postgres/access/access.test.ts +147 -0
- package/src/postgres/access/acl.ts +400 -0
- package/src/postgres/access/import.ts +64 -0
- package/src/postgres/access/live.ts +123 -0
- package/src/postgres/apply/apply.ts +117 -4
- package/src/postgres/apply/constraint-sequences.e2e.test.ts +179 -0
- package/src/postgres/apply/safe-sequences.e2e.test.ts +176 -0
- package/src/postgres/apply/statements.test.ts +138 -6
- package/src/postgres/apply/statements.ts +210 -18
- package/src/postgres/entities.ts +287 -11
- package/src/postgres/entity-types.ts +4 -0
- package/src/postgres/import/generator.ts +129 -46
- package/src/postgres/import/live-export.ts +12 -2
- package/src/postgres/live/bind.ts +20 -6
- package/src/postgres/live/catalog.ts +79 -3
- package/src/postgres/live/client.ts +10 -2
- package/src/postgres/live/describe-resources.ts +27 -3
- package/src/postgres/migrate/migrate.test.ts +24 -1
- package/src/postgres/migrate/op.ts +80 -35
- package/src/postgres/migrate/outer.e2e.test.ts +163 -0
- package/src/postgres/parser.ts +247 -8
- package/src/postgres/plan/commands.ts +60 -7
- package/src/postgres/plan/deep.ts +24 -3
- package/src/postgres/plan/diff.ts +71 -6
- package/src/postgres/plan/normalize.ts +115 -2
- package/src/postgres/plan/plan.test.ts +2 -2
- package/src/postgres/plan/rules.ts +49 -5
- package/src/postgres/plan/schema.ts +4 -2
- package/src/postgres/plan/server-normalize.ts +29 -1
- package/src/postgres.ts +11 -0
- package/src/serializer.ts +14 -9
- package/src/skill-defs.ts +1 -1
- package/src/skills/chant-sql-postgres-migration.md +3 -1
- package/src/skills/chant-sql-postgres-plan.md +3 -3
- package/src/skills/chant-sql-postgres.md +9 -2
- package/src/skills/chant-sql-rebuild.md +3 -2
- package/src/skills/chant-sql.md +7 -1
- package/src/sql-files.test.ts +188 -0
- package/src/sql-files.ts +218 -0
- package/src/token-source.e2e.test.ts +142 -0
- package/src/token-source.test.ts +119 -0
- package/src/token-source.ts +189 -0
- package/src/validate.ts +10 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A file of Postgres DDL as the objects it declares: each `CREATE` (and each
|
|
3
|
+
* `GRANT`, `REVOKE` and `ALTER DEFAULT PRIVILEGES`) is one object, and the
|
|
4
|
+
* statements that only finish one are folded into it:
|
|
5
|
+
*
|
|
6
|
+
* - `COMMENT ON` joins the object it comments on, wherever in the file it is;
|
|
7
|
+
* - `ALTER TABLE ... ENABLE | DISABLE | FORCE ROW LEVEL SECURITY` joins its table;
|
|
8
|
+
* - `ALTER TABLE <t> ADD [CONSTRAINT <c>] FOREIGN KEY | UNIQUE | PRIMARY KEY |
|
|
9
|
+
* CHECK | EXCLUDE ...`, the form `pg_dump` and ORMs print constraints in,
|
|
10
|
+
* becomes a table constraint in `<t>`'s `CREATE TABLE`.
|
|
11
|
+
*
|
|
12
|
+
* `BEGIN` and `COMMIT` are skipped. Any other statement, and any statement
|
|
13
|
+
* the parser or its tag refuses, is a problem naming it: a statement left
|
|
14
|
+
* out would be a schema change nobody declared.
|
|
15
|
+
*
|
|
16
|
+
* With `schema`, the names the DDL leaves unqualified are qualified with it,
|
|
17
|
+
* as an ORM's connection would: an object's own name, an index's, trigger's
|
|
18
|
+
* or policy's table, a foreign key's table, a comment's target, and a column
|
|
19
|
+
* type naming an enum or domain the file creates.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
23
|
+
import { isTrivia, tokenizeText, type Token } from "../postgres/tokens";
|
|
24
|
+
import { identValue, parseStatements, type NameNode, type StatementNode } from "../postgres/parser";
|
|
25
|
+
import * as tags from "../postgres/entities";
|
|
26
|
+
import { POSTGRES_ENTITY_TYPES, type PostgresEntityType } from "../postgres/entity-types";
|
|
27
|
+
import { POSTGRES_TAG_OF } from "../postgres/import/generator";
|
|
28
|
+
import type { ImportedPgObject } from "../postgres/import/ir";
|
|
29
|
+
import { firstLine, quoteBare, splitTokens, tokensText, type ReadOptions } from "./common";
|
|
30
|
+
|
|
31
|
+
type Tag = (strings: TemplateStringsArray, ...values: unknown[]) => Declarable;
|
|
32
|
+
|
|
33
|
+
/** The tag that declares an entity type. */
|
|
34
|
+
export function postgresTag(type: string): Tag | undefined {
|
|
35
|
+
const name = POSTGRES_TAG_OF[type];
|
|
36
|
+
return name ? ((tags as unknown as Record<string, Tag>)[name] ?? undefined) : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A tagged-template call's strings from template-literal text: `raw` as written, as the tag reads it. */
|
|
40
|
+
export function templateStrings(parts: readonly string[]): TemplateStringsArray {
|
|
41
|
+
return Object.assign([...parts], { raw: [...parts] }) as unknown as TemplateStringsArray;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const isName = (t: Token | undefined): boolean => t !== undefined && (t.kind === "ident" || t.kind === "qident");
|
|
45
|
+
const word = (t: Token | undefined, ...words: string[]): boolean => t !== undefined && t.kind === "ident" && words.includes(t.text.toUpperCase());
|
|
46
|
+
|
|
47
|
+
/** Text that sits inside a template literal: a backquote and `${` escaped. */
|
|
48
|
+
const templateSafe = (s: string) => s.replace(/\\(?=`|\$\{)/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
49
|
+
|
|
50
|
+
interface PgFileObject {
|
|
51
|
+
node: StatementNode;
|
|
52
|
+
/** The CREATE's tokens, then each statement folded into it. */
|
|
53
|
+
statements: Token[][];
|
|
54
|
+
/** Table constraints folded in from ALTER TABLE ... ADD, as SQL text. */
|
|
55
|
+
constraints: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Transaction control, which declares nothing. */
|
|
59
|
+
function isTransactionControl(sig: readonly Token[]): boolean {
|
|
60
|
+
const words = sig.map((t) => (t.kind === "ident" ? t.text.toUpperCase() : ""));
|
|
61
|
+
const [first, ...rest] = words;
|
|
62
|
+
if (first === "BEGIN" || first === "COMMIT" || first === "END") return rest.every((w) => w === "TRANSACTION" || w === "WORK");
|
|
63
|
+
return first === "START" && rest.length === 1 && rest[0] === "TRANSACTION";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The objects a file of Postgres DDL declares, each with its statements
|
|
68
|
+
* joined as one declaration's DDL. Problems are collected, not thrown, so a
|
|
69
|
+
* file is reported whole.
|
|
70
|
+
*/
|
|
71
|
+
export function readPostgres(ddl: string, options: ReadOptions, problems: string[]): ImportedPgObject[] {
|
|
72
|
+
const parsed: Array<{ tokens: Token[]; node: StatementNode }> = [];
|
|
73
|
+
const alters: Array<{ table: string[]; constraint: string; sql: string }> = [];
|
|
74
|
+
for (const stmt of splitTokens(tokenizeText(ddl, 0), isTrivia)) {
|
|
75
|
+
const sig = stmt.filter((t) => !isTrivia(t));
|
|
76
|
+
const sql = tokensText(stmt);
|
|
77
|
+
if (isTransactionControl(sig)) continue;
|
|
78
|
+
if (word(sig[0], "ALTER") && word(sig[1], "TABLE")) {
|
|
79
|
+
const alter = readAlterAdd(stmt, sig);
|
|
80
|
+
if (alter) {
|
|
81
|
+
alters.push({ ...alter, sql });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!word(sig[0], "CREATE", "COMMENT", "GRANT", "REVOKE", "ALTER")) {
|
|
86
|
+
problems.push(`not a statement that declares an object: ${firstLine(sql)}`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const tokens = tokenizeText(sql, 0);
|
|
91
|
+
for (const node of parseStatements(tokens)) parsed.push({ tokens, node });
|
|
92
|
+
} catch (e) {
|
|
93
|
+
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)})`;
|
|
94
|
+
problems.push(`${what}: ${firstLine(sql)}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const nameOf = (tokens: Token[], n: NameNode): string[] =>
|
|
99
|
+
n.pieces.length > 0 && n.pieces.every((p) => p !== "") ? n.pieces : tokens.slice(n.span.from, n.span.to).filter((t) => isName(t)).map((t) => identValue(t));
|
|
100
|
+
const qualify = (pieces: readonly string[]): { schema?: string; name: string } => {
|
|
101
|
+
const name = pieces[pieces.length - 1] ?? "";
|
|
102
|
+
const schema = pieces.length >= 2 ? pieces[pieces.length - 2] : options.schema;
|
|
103
|
+
return schema ? { schema, name } : { name };
|
|
104
|
+
};
|
|
105
|
+
const key = (q: { schema?: string; name: string }) => `${q.schema ?? ""}.${q.name}`;
|
|
106
|
+
|
|
107
|
+
// The types the file creates, so a column of one is qualified with the rest.
|
|
108
|
+
const fileTypes = new Set<string>();
|
|
109
|
+
for (const { tokens, node } of parsed) {
|
|
110
|
+
if (node.statement === "enum" || node.statement === "domain") {
|
|
111
|
+
const pieces = nameOf(tokens, node.name);
|
|
112
|
+
if (pieces.length === 1) fileTypes.add(pieces[0]!);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const objects: PgFileObject[] = [];
|
|
117
|
+
/** Objects by namespace and name: `rel` (anything in a schema), `index`, `schema`, `extension`, `role`, and `trigger`/`policy` with their table. */
|
|
118
|
+
const owners = new Map<string, PgFileObject>();
|
|
119
|
+
const followOns: Array<{ tokens: Token[]; node: StatementNode; inserts: Set<number> }> = [];
|
|
120
|
+
for (const { tokens, node } of parsed) {
|
|
121
|
+
const inserts = new Set<number>();
|
|
122
|
+
/** Qualifies an unqualified name in the statement, when `schema` is set. */
|
|
123
|
+
const mark = (n: NameNode | undefined) => {
|
|
124
|
+
if (!n || !options.schema) return;
|
|
125
|
+
const names = tokens.slice(n.span.from, n.span.to).filter((t) => !isTrivia(t));
|
|
126
|
+
const [only] = names;
|
|
127
|
+
if (only && names.length === 1 && isName(only)) inserts.add(tokens.indexOf(only));
|
|
128
|
+
};
|
|
129
|
+
switch (node.statement) {
|
|
130
|
+
case "comment":
|
|
131
|
+
if (node.objectType !== "SCHEMA" && node.objectType !== "EXTENSION" && node.objectType !== "ROLE" && node.objectType !== "TRIGGER" && node.objectType !== "POLICY" && node.objectType !== "CONSTRAINT" && node.objectType !== "COLUMN") mark(node.target);
|
|
132
|
+
if (node.objectType === "COLUMN" && nameOf(tokens, node.target).length === 2 && options.schema) {
|
|
133
|
+
const first = tokens.slice(node.target.span.from, node.target.span.to).find((t) => !isTrivia(t));
|
|
134
|
+
if (first) inserts.add(tokens.indexOf(first));
|
|
135
|
+
}
|
|
136
|
+
mark(node.on);
|
|
137
|
+
followOns.push({ tokens, node, inserts });
|
|
138
|
+
continue;
|
|
139
|
+
case "rowSecurity":
|
|
140
|
+
mark(node.table);
|
|
141
|
+
followOns.push({ tokens, node, inserts });
|
|
142
|
+
continue;
|
|
143
|
+
case "index":
|
|
144
|
+
case "trigger":
|
|
145
|
+
case "policy":
|
|
146
|
+
mark(node.table);
|
|
147
|
+
break;
|
|
148
|
+
case "table":
|
|
149
|
+
for (const c of [...node.columns.flatMap((col) => col.constraints), ...node.constraints]) mark(c.references?.table);
|
|
150
|
+
if (options.schema) {
|
|
151
|
+
for (const col of node.columns) {
|
|
152
|
+
if (!col.type) continue;
|
|
153
|
+
const type = tokens.slice(col.type.from, col.type.to).filter((t) => !isTrivia(t));
|
|
154
|
+
const first = type[0];
|
|
155
|
+
if (first && isName(first) && fileTypes.has(identValue(first)) && !(type[1]?.kind === "punct" && type[1].text === ".")) inserts.add(tokens.indexOf(first));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
default:
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
if (node.statement === "schema" && !node.name) {
|
|
163
|
+
problems.push(`a CREATE SCHEMA with no name: ${firstLine(tokensText(tokens))}`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (node.statement === "index" && !node.name) {
|
|
167
|
+
problems.push(`an index with no name (its declaration needs one): ${firstLine(tokensText(tokens))}`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const ownName = !["schema", "extension", "index", "trigger", "policy", "role", "grant"].includes(node.statement);
|
|
171
|
+
if (ownName && "name" in node) mark(node.name);
|
|
172
|
+
const object: PgFileObject = { node, statements: [rewrite(tokens, inserts, options.schema)], constraints: [] };
|
|
173
|
+
objects.push(object);
|
|
174
|
+
const k = ownerKey(node, tokens, nameOf, qualify);
|
|
175
|
+
if (k) owners.set(k, object);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const f of followOns) {
|
|
179
|
+
const k = followOnOwner(f.node, f.tokens, nameOf, qualify);
|
|
180
|
+
const owner = k ? owners.get(k) : undefined;
|
|
181
|
+
if (!owner) {
|
|
182
|
+
const what = f.node.statement === "comment" ? `COMMENT ON ${f.node.objectType}` : "ALTER TABLE ... ROW LEVEL SECURITY";
|
|
183
|
+
problems.push(`${what} for an object the file does not create: ${firstLine(tokensText(f.tokens))}`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
owner.statements.push(rewrite(f.tokens, f.inserts, options.schema));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const alter of alters) {
|
|
190
|
+
const table = owners.get(`rel ${key(qualify(alter.table))}`);
|
|
191
|
+
if (!table || table.node.statement !== "table") {
|
|
192
|
+
problems.push(`ALTER TABLE of a table the file does not create: ${firstLine(alter.sql)}`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
table.constraints.push(qualifyConstraint(alter.constraint, options.schema));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const out: ImportedPgObject[] = [];
|
|
199
|
+
for (const o of objects) {
|
|
200
|
+
const [create = [], ...rest] = o.statements;
|
|
201
|
+
const ddlText = [o.constraints.length > 0 ? addConstraints(create, o.constraints) : tokensText(create), ...rest.map((c) => tokensText(c))].join(";\n");
|
|
202
|
+
const identity = identify(ddlText);
|
|
203
|
+
if (typeof identity === "string") {
|
|
204
|
+
problems.push(`${identity}: ${firstLine(ddlText)}`);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
out.push({ ...identity, ddl: ddlText });
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** An object's entity type, schema and name, read by declaring it with its tag; a string is the tag's refusal. */
|
|
213
|
+
function identify(ddl: string): { type: PostgresEntityType; schema?: string; name: string } | string {
|
|
214
|
+
let tokens: Token[];
|
|
215
|
+
let node: StatementNode | undefined;
|
|
216
|
+
try {
|
|
217
|
+
tokens = tokenizeText(ddl, 0);
|
|
218
|
+
[node] = parseStatements(tokens);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
return `does not parse (${e instanceof Error ? e.message : String(e)})`;
|
|
221
|
+
}
|
|
222
|
+
const type = node ? entityTypeOf(node) : undefined;
|
|
223
|
+
const tag = type ? postgresTag(type) : undefined;
|
|
224
|
+
if (!type || !tag) return "not a statement that declares an object";
|
|
225
|
+
try {
|
|
226
|
+
const entity = tag(templateStrings([templateSafe(ddl)]));
|
|
227
|
+
const p = (entity as unknown as { props: { schema?: string; name: string } }).props;
|
|
228
|
+
return { type: entity.entityType as PostgresEntityType, ...(p.schema ? { schema: p.schema } : {}), name: p.name };
|
|
229
|
+
} catch (e) {
|
|
230
|
+
return e instanceof Error ? e.message : String(e);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The entity type a statement declares. */
|
|
235
|
+
function entityTypeOf(node: StatementNode): PostgresEntityType | undefined {
|
|
236
|
+
switch (node.statement) {
|
|
237
|
+
case "view":
|
|
238
|
+
return node.materialized ? POSTGRES_ENTITY_TYPES.materializedView : POSTGRES_ENTITY_TYPES.view;
|
|
239
|
+
case "grant":
|
|
240
|
+
return node.defaults ? POSTGRES_ENTITY_TYPES.defaultPrivileges : POSTGRES_ENTITY_TYPES.grant;
|
|
241
|
+
case "comment":
|
|
242
|
+
case "rowSecurity":
|
|
243
|
+
return undefined;
|
|
244
|
+
default:
|
|
245
|
+
return (POSTGRES_ENTITY_TYPES as Record<string, PostgresEntityType>)[node.statement];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
type NameOf = (tokens: Token[], n: NameNode) => string[];
|
|
250
|
+
type Qualify = (pieces: readonly string[]) => { schema?: string; name: string };
|
|
251
|
+
const qkey = (q: { schema?: string; name: string }) => `${q.schema ?? ""}.${q.name}`;
|
|
252
|
+
|
|
253
|
+
/** The key a created object is found under by the statements folded into it. */
|
|
254
|
+
function ownerKey(node: StatementNode, tokens: Token[], nameOf: NameOf, qualify: Qualify): string | undefined {
|
|
255
|
+
switch (node.statement) {
|
|
256
|
+
case "schema":
|
|
257
|
+
return node.name ? `schema ${nameOf(tokens, node.name).at(-1) ?? ""}` : undefined;
|
|
258
|
+
case "extension":
|
|
259
|
+
return `extension ${nameOf(tokens, node.name).at(-1) ?? ""}`;
|
|
260
|
+
case "role":
|
|
261
|
+
return `role ${nameOf(tokens, node.name).at(-1) ?? ""}`;
|
|
262
|
+
case "index": {
|
|
263
|
+
// An index is in its table's schema.
|
|
264
|
+
const table = nameOf(tokens, node.table);
|
|
265
|
+
return node.name ? `index ${qkey(qualify([...table.slice(0, -1), nameOf(tokens, node.name).at(-1) ?? ""]))}` : undefined;
|
|
266
|
+
}
|
|
267
|
+
case "trigger":
|
|
268
|
+
case "policy":
|
|
269
|
+
return `${node.statement} ${nameOf(tokens, node.name).at(-1) ?? ""} ON ${qkey(qualify(nameOf(tokens, node.table)))}`;
|
|
270
|
+
case "grant":
|
|
271
|
+
case "comment":
|
|
272
|
+
case "rowSecurity":
|
|
273
|
+
return undefined;
|
|
274
|
+
default:
|
|
275
|
+
return `rel ${qkey(qualify(nameOf(tokens, node.name)))}`;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** The key of the object a COMMENT ON or ALTER TABLE ... ROW LEVEL SECURITY finishes. */
|
|
280
|
+
function followOnOwner(node: StatementNode, tokens: Token[], nameOf: NameOf, qualify: Qualify): string | undefined {
|
|
281
|
+
if (node.statement === "rowSecurity") return `rel ${qkey(qualify(nameOf(tokens, node.table)))}`;
|
|
282
|
+
if (node.statement !== "comment") return undefined;
|
|
283
|
+
const target = nameOf(tokens, node.target);
|
|
284
|
+
switch (node.objectType) {
|
|
285
|
+
case "SCHEMA":
|
|
286
|
+
return `schema ${target.at(-1) ?? ""}`;
|
|
287
|
+
case "EXTENSION":
|
|
288
|
+
return `extension ${target.at(-1) ?? ""}`;
|
|
289
|
+
case "ROLE":
|
|
290
|
+
return `role ${target.at(-1) ?? ""}`;
|
|
291
|
+
case "INDEX":
|
|
292
|
+
return `index ${qkey(qualify(target))}`;
|
|
293
|
+
case "COLUMN":
|
|
294
|
+
return `rel ${qkey(qualify(target.slice(0, -1)))}`;
|
|
295
|
+
case "TRIGGER":
|
|
296
|
+
case "POLICY":
|
|
297
|
+
return node.on ? `${node.objectType.toLowerCase()} ${target.at(-1) ?? ""} ON ${qkey(qualify(nameOf(tokens, node.on)))}` : undefined;
|
|
298
|
+
case "CONSTRAINT":
|
|
299
|
+
case "DOMAIN CONSTRAINT":
|
|
300
|
+
return node.on ? `rel ${qkey(qualify(nameOf(tokens, node.on)))}` : undefined;
|
|
301
|
+
default:
|
|
302
|
+
return `rel ${qkey(qualify(target))}`;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** The statement's tokens with `schema.` inserted before each marked one. */
|
|
307
|
+
function rewrite(tokens: readonly Token[], inserts: ReadonlySet<number>, schema: string | undefined): Token[] {
|
|
308
|
+
if (!schema || inserts.size === 0) return [...tokens];
|
|
309
|
+
const out: Token[] = [];
|
|
310
|
+
tokens.forEach((t, i) => {
|
|
311
|
+
if (inserts.has(i)) out.push({ kind: "ident", text: `${quoteBare(schema)}.`, part: 0, start: 0, end: 0 });
|
|
312
|
+
out.push(t);
|
|
313
|
+
});
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** `ALTER TABLE [IF EXISTS] [ONLY] <name> ADD <table constraint>`: the table's name pieces and the constraint's text. */
|
|
318
|
+
function readAlterAdd(stmt: readonly Token[], sig: readonly Token[]): { table: string[]; constraint: string } | undefined {
|
|
319
|
+
let i = 2;
|
|
320
|
+
if (word(sig[i], "IF") && word(sig[i + 1], "EXISTS")) i += 2;
|
|
321
|
+
if (word(sig[i], "ONLY")) i++;
|
|
322
|
+
const table: string[] = [];
|
|
323
|
+
for (let t = sig[i]; t && isName(t); t = sig[i]) {
|
|
324
|
+
table.push(identValue(t));
|
|
325
|
+
i++;
|
|
326
|
+
const dot = sig[i];
|
|
327
|
+
if (dot?.kind === "punct" && dot.text === ".") i++;
|
|
328
|
+
else break;
|
|
329
|
+
}
|
|
330
|
+
if (table.length === 0 || !word(sig[i], "ADD")) return undefined;
|
|
331
|
+
i++;
|
|
332
|
+
if (!word(sig[i], "CONSTRAINT", "FOREIGN", "UNIQUE", "PRIMARY", "CHECK", "EXCLUDE")) return undefined;
|
|
333
|
+
// One constraint: a comma at depth 0 starts another ALTER action, which is not read.
|
|
334
|
+
let depth = 0;
|
|
335
|
+
for (const t of sig.slice(i)) {
|
|
336
|
+
if (t.kind === "punct" && t.text === "(") depth++;
|
|
337
|
+
else if (t.kind === "punct" && t.text === ")") depth--;
|
|
338
|
+
else if (t.kind === "punct" && t.text === "," && depth === 0) return undefined;
|
|
339
|
+
}
|
|
340
|
+
const start = sig[i];
|
|
341
|
+
return start ? { table, constraint: tokensText(stmt.slice(stmt.indexOf(start))) } : undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Qualifies the table a folded FOREIGN KEY references, when it is unqualified. */
|
|
345
|
+
function qualifyConstraint(constraint: string, schema: string | undefined): string {
|
|
346
|
+
if (!schema) return constraint;
|
|
347
|
+
const tokens = tokenizeText(constraint, 0);
|
|
348
|
+
const sig = tokens.map((t, i) => ({ t, i })).filter((x) => !isTrivia(x.t));
|
|
349
|
+
const inserts = new Set<number>();
|
|
350
|
+
sig.forEach((x, k) => {
|
|
351
|
+
const target = sig[k + 1];
|
|
352
|
+
const dot = sig[k + 2]?.t;
|
|
353
|
+
if (word(x.t, "REFERENCES") && target && isName(target.t) && !(dot?.kind === "punct" && dot.text === ".")) inserts.add(target.i);
|
|
354
|
+
});
|
|
355
|
+
return tokensText(rewrite(tokens, inserts, schema));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** A CREATE TABLE with table constraints added at the end of its column list (the `)` matching the first `(`). */
|
|
359
|
+
function addConstraints(create: readonly Token[], constraints: readonly string[]): string {
|
|
360
|
+
let depth = 0;
|
|
361
|
+
const close = create.findIndex((t) => {
|
|
362
|
+
if (t.kind !== "punct") return false;
|
|
363
|
+
if (t.text === "(") depth++;
|
|
364
|
+
else if (t.text === ")") return --depth === 0;
|
|
365
|
+
return false;
|
|
366
|
+
});
|
|
367
|
+
if (close < 0) return tokensText(create);
|
|
368
|
+
const before = tokensText(create.slice(0, close)).replace(/\s+$/, "");
|
|
369
|
+
const indent = /\n([ \t]+)\S[^\n]*$/.exec(before)?.[1] ?? " ";
|
|
370
|
+
return `${before}${constraints.map((c) => `,\n${indent}${c}`).join("")}\n${tokensText(create.slice(close))}`;
|
|
371
|
+
}
|
|
@@ -19,6 +19,36 @@
|
|
|
19
19
|
"kind": "resource",
|
|
20
20
|
"lexicon": "sql"
|
|
21
21
|
},
|
|
22
|
+
"Dictionary": {
|
|
23
|
+
"resourceType": "ClickHouse::Dictionary",
|
|
24
|
+
"kind": "resource",
|
|
25
|
+
"lexicon": "sql"
|
|
26
|
+
},
|
|
27
|
+
"Function": {
|
|
28
|
+
"resourceType": "ClickHouse::Function",
|
|
29
|
+
"kind": "resource",
|
|
30
|
+
"lexicon": "sql"
|
|
31
|
+
},
|
|
32
|
+
"User": {
|
|
33
|
+
"resourceType": "ClickHouse::User",
|
|
34
|
+
"kind": "resource",
|
|
35
|
+
"lexicon": "sql"
|
|
36
|
+
},
|
|
37
|
+
"Role": {
|
|
38
|
+
"resourceType": "ClickHouse::Role",
|
|
39
|
+
"kind": "resource",
|
|
40
|
+
"lexicon": "sql"
|
|
41
|
+
},
|
|
42
|
+
"RowPolicy": {
|
|
43
|
+
"resourceType": "ClickHouse::RowPolicy",
|
|
44
|
+
"kind": "resource",
|
|
45
|
+
"lexicon": "sql"
|
|
46
|
+
},
|
|
47
|
+
"Grant": {
|
|
48
|
+
"resourceType": "ClickHouse::Grant",
|
|
49
|
+
"kind": "resource",
|
|
50
|
+
"lexicon": "sql"
|
|
51
|
+
},
|
|
22
52
|
"PostgresSchema": {
|
|
23
53
|
"resourceType": "Postgres::Schema",
|
|
24
54
|
"kind": "resource",
|
|
@@ -78,5 +108,25 @@
|
|
|
78
108
|
"resourceType": "Postgres::Trigger",
|
|
79
109
|
"kind": "resource",
|
|
80
110
|
"lexicon": "sql"
|
|
111
|
+
},
|
|
112
|
+
"PostgresPolicy": {
|
|
113
|
+
"resourceType": "Postgres::Policy",
|
|
114
|
+
"kind": "resource",
|
|
115
|
+
"lexicon": "sql"
|
|
116
|
+
},
|
|
117
|
+
"PostgresRole": {
|
|
118
|
+
"resourceType": "Postgres::Role",
|
|
119
|
+
"kind": "resource",
|
|
120
|
+
"lexicon": "sql"
|
|
121
|
+
},
|
|
122
|
+
"PostgresGrant": {
|
|
123
|
+
"resourceType": "Postgres::Grant",
|
|
124
|
+
"kind": "resource",
|
|
125
|
+
"lexicon": "sql"
|
|
126
|
+
},
|
|
127
|
+
"PostgresDefaultPrivileges": {
|
|
128
|
+
"resourceType": "Postgres::DefaultPrivileges",
|
|
129
|
+
"kind": "resource",
|
|
130
|
+
"lexicon": "sql"
|
|
81
131
|
}
|
|
82
132
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant import schema.sql`: a file of DDL into the import IR, in either
|
|
3
|
+
* dialect. `--parser-option dialect=postgres` (or `clickhouse`) names it;
|
|
4
|
+
* without one it is read off the statements: an `ENGINE =` clause, a
|
|
5
|
+
* `CREATE DATABASE`, a `CREATE DICTIONARY` or a lambda `CREATE FUNCTION f AS
|
|
6
|
+
* (x) -> ...` is ClickHouse, a statement
|
|
7
|
+
* only Postgres has (`CREATE SCHEMA`, `CREATE INDEX`, `COMMENT ON`, `ALTER
|
|
8
|
+
* TABLE`, `GRANT`, ...) or a `CREATE TABLE` with no engine is Postgres, and
|
|
9
|
+
* anything else is ClickHouse.
|
|
10
|
+
*
|
|
11
|
+
* A statement that declares nothing, or does not parse, is an error naming
|
|
12
|
+
* it (`./sql-files.ts`); none is left out.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ParserOptions, TemplateIR, TemplateParser } from "@intentius/chant/import/parser";
|
|
16
|
+
import { SQL_DIALECTS, type SqlDialect } from "./dialects";
|
|
17
|
+
import { readClickHouse } from "./files/clickhouse";
|
|
18
|
+
import { readPostgres } from "./files/postgres";
|
|
19
|
+
import { SqlFileError } from "./files/common";
|
|
20
|
+
import { objectsToIR as chObjectsToIR } from "./clickhouse/import/ir";
|
|
21
|
+
import { objectsToIR as pgObjectsToIR } from "./postgres/import/ir";
|
|
22
|
+
|
|
23
|
+
const CLICKHOUSE_ONLY = [
|
|
24
|
+
/\bENGINE\s*=/i,
|
|
25
|
+
/^\s*CREATE\s+(?:DATABASE|(?:OR\s+REPLACE\s+)?DICTIONARY)\b/im,
|
|
26
|
+
// A SQL function: CREATE FUNCTION f AS (x) -> ...
|
|
27
|
+
/^\s*CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+[^\s(]+(?:\s+ON\s+CLUSTER\s+\S+)?\s+AS\s*\(/im,
|
|
28
|
+
];
|
|
29
|
+
const POSTGRES_ONLY = [
|
|
30
|
+
/^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:SCHEMA|EXTENSION|TYPE|DOMAIN|SEQUENCE|(?:UNIQUE\s+)?INDEX|(?:CONSTRAINT\s+)?TRIGGER|POLICY|ROLE|FUNCTION|PROCEDURE|(?:UNLOGGED|TEMP|TEMPORARY)\s+TABLE)\b/im,
|
|
31
|
+
/^\s*(?:COMMENT\s+ON|ALTER\s+TABLE|ALTER\s+DEFAULT\s+PRIVILEGES|GRANT|REVOKE|BEGIN)\b/im,
|
|
32
|
+
/^\s*CREATE\s+TABLE\b/im,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** The dialect a file of DDL is written in, read off its statements. */
|
|
36
|
+
export function guessDialect(content: string): SqlDialect {
|
|
37
|
+
const text = content.replace(/--[^\n]*/g, "");
|
|
38
|
+
if (CLICKHOUSE_ONLY.some((re) => re.test(text))) return "clickhouse";
|
|
39
|
+
if (POSTGRES_ONLY.some((re) => re.test(text))) return "postgres";
|
|
40
|
+
return "clickhouse";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The `dialect` parser option, checked. */
|
|
44
|
+
export function dialectOption(options: ParserOptions | undefined): SqlDialect | undefined {
|
|
45
|
+
const d = options?.dialect;
|
|
46
|
+
if (d === undefined) return undefined;
|
|
47
|
+
if (typeof d !== "string" || !(SQL_DIALECTS as readonly string[]).includes(d)) throw new Error(`--parser-option dialect: expected ${SQL_DIALECTS.join(" or ")}, got ${String(d)}`);
|
|
48
|
+
return d as SqlDialect;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class SqlFileParser implements TemplateParser {
|
|
52
|
+
constructor(private readonly dialect?: SqlDialect) {}
|
|
53
|
+
|
|
54
|
+
parse(content: string): TemplateIR {
|
|
55
|
+
const dialect = this.dialect ?? guessDialect(content);
|
|
56
|
+
const problems: string[] = [];
|
|
57
|
+
const ir = dialect === "postgres" ? pgObjectsToIR(readPostgres(content, { origin: "the DDL" }, problems)) : chObjectsToIR(readClickHouse(content, { origin: "the DDL" }, problems));
|
|
58
|
+
if (problems.length > 0) throw new SqlFileError(`the ${dialect === "postgres" ? "Postgres" : "ClickHouse"} DDL`, problems);
|
|
59
|
+
return ir;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { sqlConfigSchema, type SqlConfig } from "./config";
|
|
|
12
12
|
// them as intrinsics (check-lexicon resolves a registered intrinsic against the
|
|
13
13
|
// root's exports). Declarations import them from the dialect subpath,
|
|
14
14
|
// `@intentius/chant-lexicon-sql/clickhouse`.
|
|
15
|
-
export { database, table, view, literal } from "./clickhouse/entities";
|
|
15
|
+
export { database, table, view, dictionary, user, role, policy, grant, literal } from "./clickhouse/entities";
|
|
16
16
|
|
|
17
17
|
// The Postgres tags whose names ClickHouse does not export, for the same reason.
|
|
18
18
|
// A tag folds as the function its file imports, so `table` imported from
|
|
@@ -34,3 +34,16 @@ export {
|
|
|
34
34
|
type StatementStep,
|
|
35
35
|
type StepObject,
|
|
36
36
|
} from "./migration-statements";
|
|
37
|
+
|
|
38
|
+
// Plain `.sql` files as a schema source (#3646): DDL read into the declarations
|
|
39
|
+
// the tagged templates make; also at `@intentius/chant-lexicon-sql/sql-files`.
|
|
40
|
+
export {
|
|
41
|
+
readSqlFile,
|
|
42
|
+
sqlFileDeclarations,
|
|
43
|
+
sqlFileEntities,
|
|
44
|
+
sqlFileOf,
|
|
45
|
+
SqlFileError,
|
|
46
|
+
type SqlFileObject,
|
|
47
|
+
type SqlFileOptions,
|
|
48
|
+
type SqlFileSource,
|
|
49
|
+
} from "./sql-files";
|
|
@@ -17,12 +17,12 @@ export const POSTGRES_TEMPLATE_SOURCES: readonly TemplateSource<PostgresTag>[] =
|
|
|
17
17
|
{
|
|
18
18
|
dialect: "postgres",
|
|
19
19
|
modules: ["@intentius/chant-lexicon-sql/postgres"],
|
|
20
|
-
tags: ["schema", "table", "index", "view", "sequence", "type", "domain", "extension", "func", "procedure", "trigger"],
|
|
20
|
+
tags: ["schema", "table", "index", "view", "sequence", "type", "domain", "extension", "func", "procedure", "trigger", "policy", "role", "grant"],
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
dialect: "postgres",
|
|
24
24
|
modules: ["@intentius/chant-lexicon-sql"],
|
|
25
|
-
tags: ["schema", "index", "sequence", "type", "domain", "extension", "func", "procedure", "trigger"],
|
|
25
|
+
tags: ["schema", "index", "sequence", "type", "domain", "extension", "func", "procedure", "trigger", "policy", "role", "grant"],
|
|
26
26
|
},
|
|
27
27
|
];
|
|
28
28
|
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import type { LintContext, LintDiagnostic, LintRule } from "@intentius/chant/lint/rule";
|
|
2
2
|
import { parseCreate } from "../../clickhouse/parser";
|
|
3
|
+
import { parseAccess } from "../../clickhouse/access";
|
|
3
4
|
import { SqlSyntaxError } from "../../clickhouse/tokens";
|
|
4
5
|
import { findTemplates, templatePosition, tokensOf } from "./templates";
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
+
const ACCESS_TAGS = new Set(["user", "role", "policy", "grant"]);
|
|
8
|
+
const ACCESS_STATEMENT: Record<string, string> = { user: "CREATE USER", role: "CREATE ROLE", rowPolicy: "CREATE ROW POLICY", grant: "GRANT" };
|
|
9
|
+
|
|
10
|
+
const STATEMENT: Record<string, string> = { database: "CREATE DATABASE", table: "CREATE TABLE", view: "CREATE VIEW", dictionary: "CREATE DICTIONARY", function: "CREATE FUNCTION" };
|
|
11
|
+
/** The tag a statement belongs in: `func` for a function, the statement's own name otherwise. */
|
|
12
|
+
const tagOf = (statement: string): string => (statement === "function" ? "func" : statement);
|
|
7
13
|
|
|
8
14
|
/**
|
|
9
|
-
* SQLCH001: the DDL in a `database`, `table`
|
|
15
|
+
* SQLCH001: the DDL in a ClickHouse template (`database`, `table`, `view`, `dictionary`, `func`, `user`, `role`, `policy`, `grant`) does not parse, or holds another statement than its tag.
|
|
10
16
|
*
|
|
11
17
|
* The same parse runs when the build calls the tag, and fails the build there.
|
|
12
18
|
* This rule reports it in the editor and in `chant lint`, at the token, before
|
|
@@ -27,12 +33,28 @@ export const sqlch001: LintRule = {
|
|
|
27
33
|
let tokens;
|
|
28
34
|
try {
|
|
29
35
|
tokens = tokensOf(found);
|
|
36
|
+
if (ACCESS_TAGS.has(found.tag)) {
|
|
37
|
+
const node = parseAccess(tokens);
|
|
38
|
+
const tag = node.statement === "rowPolicy" ? "policy" : node.statement;
|
|
39
|
+
if (tag !== found.tag || node.revoke) {
|
|
40
|
+
out.push({
|
|
41
|
+
ruleId: "SQLCH001",
|
|
42
|
+
severity: "error",
|
|
43
|
+
message: node.revoke
|
|
44
|
+
? `${found.tag}\`...\` holds a REVOKE; declare the grants a grantee keeps, and a plan revokes the rest`
|
|
45
|
+
: `${found.tag}\`...\` holds a ${ACCESS_STATEMENT[node.statement]}; use the ${tag} tag`,
|
|
46
|
+
file: context.filePath,
|
|
47
|
+
...templatePosition(source, found, 0, 0),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
30
52
|
const node = parseCreate(tokens);
|
|
31
|
-
if (node.statement !== found.tag) {
|
|
53
|
+
if (tagOf(node.statement) !== found.tag) {
|
|
32
54
|
out.push({
|
|
33
55
|
ruleId: "SQLCH001",
|
|
34
56
|
severity: "error",
|
|
35
|
-
message: `${found.tag}\`...\` holds a ${STATEMENT[node.statement]}; use the ${node.statement} tag`,
|
|
57
|
+
message: `${found.tag}\`...\` holds a ${STATEMENT[node.statement]}; use the ${tagOf(node.statement)} tag`,
|
|
36
58
|
file: context.filePath,
|
|
37
59
|
...templatePosition(source, found, 0, 0),
|
|
38
60
|
});
|
|
@@ -41,7 +41,8 @@ function keyColumns(tokens: Token[], span: Span): Array<{ name: string; token: T
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function allowsNullableKey(tokens: Token[], node: CreateNode): boolean {
|
|
44
|
-
if (node.statement
|
|
44
|
+
if (node.statement !== "table" && node.statement !== "view") return false;
|
|
45
|
+
if (!node.settings) return false;
|
|
45
46
|
const s = node.settings.find((x) => x.key === "allow_nullable_key");
|
|
46
47
|
if (!s) return false;
|
|
47
48
|
const v = tokens.slice(s.value.from, s.value.to).map((t) => t.text).join("").trim().toLowerCase().replace(/'/g, "");
|
|
@@ -74,7 +75,7 @@ export const sqlch002: LintRule = {
|
|
|
74
75
|
if (err instanceof SqlSyntaxError) continue; // SQLCH001 reports it
|
|
75
76
|
throw err;
|
|
76
77
|
}
|
|
77
|
-
if (node.statement
|
|
78
|
+
if ((node.statement !== "table" && node.statement !== "view") || allowsNullableKey(tokens, node)) continue;
|
|
78
79
|
const nullable = new Set(node.columns.filter((c) => c.name && isNullable(tokens, c)).map((c) => c.name));
|
|
79
80
|
if (nullable.size === 0) continue;
|
|
80
81
|
const reported = new Set<string>();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { LintContext, LintDiagnostic, LintRule } from "@intentius/chant/lint/rule";
|
|
2
2
|
import { parseStatements, SqlSyntaxError } from "../../postgres/parser";
|
|
3
|
-
import { STATEMENT_NAMES, type PostgresTag } from "../../postgres/entities";
|
|
3
|
+
import { followOnStray, STATEMENT_NAMES, type PostgresTag } from "../../postgres/entities";
|
|
4
4
|
import { findPostgresTemplates, postgresTokensOf, templatePosition, tokenPosition } from "./postgres-templates";
|
|
5
5
|
|
|
6
6
|
/** The statement each tag holds. */
|
|
@@ -16,6 +16,9 @@ const STATEMENT: Record<PostgresTag, string> = {
|
|
|
16
16
|
func: "function",
|
|
17
17
|
procedure: "procedure",
|
|
18
18
|
trigger: "trigger",
|
|
19
|
+
policy: "policy",
|
|
20
|
+
role: "role",
|
|
21
|
+
grant: "grant",
|
|
19
22
|
};
|
|
20
23
|
|
|
21
24
|
const TAG_OF: Record<string, PostgresTag> = Object.fromEntries(Object.entries(STATEMENT).map(([tag, statement]) => [statement, tag as PostgresTag]));
|
|
@@ -62,15 +65,15 @@ export const sqlpg001: LintRule = {
|
|
|
62
65
|
report(
|
|
63
66
|
use
|
|
64
67
|
? `${found.tag}\`...\` holds a ${STATEMENT_NAMES[node!.statement]}; use the ${use} tag`
|
|
65
|
-
: `${found.tag}\`...\` starts with a
|
|
68
|
+
: `${found.tag}\`...\` starts with a ${STATEMENT_NAMES[node!.statement]}; it goes after the CREATE it belongs to`,
|
|
66
69
|
templatePosition(source, found, 0, 0),
|
|
67
70
|
);
|
|
68
71
|
continue;
|
|
69
72
|
}
|
|
70
|
-
const stray =
|
|
73
|
+
const stray = followOnStray(found.tag, rest);
|
|
71
74
|
if (stray) {
|
|
72
75
|
report(
|
|
73
|
-
`${found.tag}\`...\` holds a second statement (${STATEMENT_NAMES[stray.statement]}); a template declares one object, followed only by COMMENT ON statements for it`,
|
|
76
|
+
`${found.tag}\`...\` holds a second statement (${STATEMENT_NAMES[stray.statement]}); a template declares one object, followed only by COMMENT ON statements for it${found.tag === "table" ? " and ALTER TABLE ... ROW LEVEL SECURITY" : ""}`,
|
|
74
77
|
templatePosition(source, found, 0, 0),
|
|
75
78
|
);
|
|
76
79
|
}
|
|
@@ -13,7 +13,7 @@ import { findSqlTemplates, type FoundTemplate as SqlFoundTemplate, type Template
|
|
|
13
13
|
|
|
14
14
|
export { templatePosition, tokenPosition } from "../../core/find-templates";
|
|
15
15
|
|
|
16
|
-
export type SqlTag = "database" | "table" | "view";
|
|
16
|
+
export type SqlTag = "database" | "table" | "view" | "dictionary" | "func" | "user" | "role" | "policy" | "grant";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* The ClickHouse tags, by the modules they are imported from. The package
|
|
@@ -22,7 +22,7 @@ export type SqlTag = "database" | "table" | "view";
|
|
|
22
22
|
export const CLICKHOUSE_TEMPLATE_SOURCE: TemplateSource<SqlTag> = {
|
|
23
23
|
dialect: "clickhouse",
|
|
24
24
|
modules: ["@intentius/chant-lexicon-sql", "@intentius/chant-lexicon-sql/clickhouse"],
|
|
25
|
-
tags: ["database", "table", "view"],
|
|
25
|
+
tags: ["database", "table", "view", "dictionary", "func", "user", "role", "policy", "grant"],
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
export type FoundTemplate = SqlFoundTemplate<SqlTag>;
|