@intentius/chant-lexicon-sql 0.124.19 → 0.124.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clickhouse/access.d.ts +36 -7
- package/dist/clickhouse/access.d.ts.map +1 -1
- package/dist/clickhouse/apply/statements.d.ts.map +1 -1
- package/dist/clickhouse/entities.d.ts +60 -0
- package/dist/clickhouse/entities.d.ts.map +1 -1
- package/dist/clickhouse/import/generator.d.ts +1 -7
- package/dist/clickhouse/import/generator.d.ts.map +1 -1
- package/dist/clickhouse/live/catalog.d.ts +6 -5
- package/dist/clickhouse/live/catalog.d.ts.map +1 -1
- package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
- package/dist/clickhouse/parser.d.ts +16 -0
- package/dist/clickhouse/parser.d.ts.map +1 -1
- package/dist/clickhouse/plan/deep.d.ts.map +1 -1
- package/dist/clickhouse/plan/diff.d.ts.map +1 -1
- package/dist/clickhouse/plan/normalize.d.ts +6 -3
- package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
- package/dist/clickhouse/plan/rules.d.ts +3 -0
- package/dist/clickhouse/plan/rules.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/registry.d.ts.map +1 -1
- package/dist/coverage/clickhouse.json +8 -8
- package/dist/coverage/postgres.json +6 -6
- package/dist/files/clickhouse.d.ts +2 -1
- package/dist/files/clickhouse.d.ts.map +1 -1
- package/dist/files/postgres.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +10 -10
- package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
- package/dist/lint/post-synth/sql101.d.ts.map +1 -1
- package/dist/lint/rules/sqlch001.d.ts +1 -1
- package/dist/lint/rules/sqlch001.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 +40 -0
- package/dist/okf/index.md +9 -1
- package/dist/okf/rules/SQL101.md +1 -0
- package/dist/okf/rules/SQLCH001.md +2 -2
- package/dist/okf/rules/SQLPG113.md +2 -0
- package/dist/okf/types/PostgresAggregate.md +13 -0
- package/dist/okf/types/PostgresCast.md +9 -0
- package/dist/okf/types/PostgresCollation.md +9 -0
- package/dist/okf/types/PostgresCompositeType.md +13 -0
- package/dist/okf/types/PostgresEventTrigger.md +9 -0
- package/dist/okf/types/PostgresRangeType.md +13 -0
- package/dist/okf/types/Quota.md +9 -0
- package/dist/okf/types/SettingsProfile.md +9 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/postgres/apply/apply.d.ts.map +1 -1
- package/dist/postgres/apply/statements.d.ts +4 -3
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +129 -6
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entity-types.d.ts +6 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts +3 -2
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/live/catalog-kinds.d.ts +28 -0
- package/dist/postgres/live/catalog-kinds.d.ts.map +1 -0
- package/dist/postgres/live/catalog.d.ts +15 -11
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/parser.d.ts +88 -5
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/plan/deep.d.ts.map +1 -1
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts +17 -2
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +3 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/schema.d.ts +3 -2
- package/dist/postgres/plan/schema.d.ts.map +1 -1
- package/dist/postgres/testing/roundtrip-schema.d.ts +4 -2
- package/dist/postgres/testing/roundtrip-schema.d.ts.map +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/rules/postgres-names.ts +15 -2
- package/dist/rules/postgres-templates.ts +2 -2
- package/dist/rules/sql101.ts +1 -0
- package/dist/rules/sqlch001.ts +10 -3
- package/dist/rules/sqlpg001.ts +6 -21
- package/dist/rules/sqlpg113.ts +1 -1
- package/dist/rules/templates.ts +2 -2
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/clickhouse/access.ts +197 -22
- package/src/clickhouse/apply/statements.ts +17 -5
- package/src/clickhouse/entities.ts +78 -2
- package/src/clickhouse/import/generator.ts +16 -3
- package/src/clickhouse/live/catalog.ts +57 -10
- package/src/clickhouse/live/describe-resources.ts +3 -1
- package/src/clickhouse/live/kinds.e2e.test.ts +179 -0
- package/src/clickhouse/live/undeclarable.e2e.test.ts +5 -4
- package/src/clickhouse/parser.test.ts +45 -2
- package/src/clickhouse/parser.ts +111 -3
- package/src/clickhouse/plan/deep.ts +25 -6
- package/src/clickhouse/plan/diff.ts +41 -1
- package/src/clickhouse/plan/normalize.ts +12 -4
- package/src/clickhouse/plan/rules.ts +22 -1
- package/src/clickhouse/quota-profile.test.ts +186 -0
- package/src/clickhouse/table-function.test.ts +54 -0
- package/src/clickhouse/testdata/server-ddl-baseline.json +17 -14
- package/src/clickhouse/topology.ts +4 -2
- package/src/clickhouse.ts +6 -0
- package/src/codegen/postgres-classifier-page.ts +1 -1
- package/src/codegen/registry.ts +8 -0
- package/src/core/undeclarable.test.ts +22 -19
- package/src/coverage/clickhouse.json +8 -8
- package/src/coverage/postgres.json +6 -6
- package/src/files/clickhouse.ts +14 -6
- package/src/files/postgres.ts +19 -5
- package/src/generated/lexicon-sql.json +40 -0
- package/src/index.ts +2 -2
- package/src/lint/post-synth/postgres-names.ts +15 -2
- package/src/lint/post-synth/sql101.ts +1 -0
- package/src/lint/post-synth/sqlpg113.ts +1 -1
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lint/rules/sqlch001.ts +10 -3
- package/src/lint/rules/sqlpg001.ts +6 -21
- package/src/lint/rules/templates.ts +2 -2
- package/src/lsp/postgres-template.ts +14 -0
- package/src/mcp/index.ts +5 -5
- package/src/mcp/postgres.ts +1 -1
- package/src/plugin.ts +7 -1
- package/src/postgres/apply/apply.ts +8 -2
- package/src/postgres/apply/statements.ts +29 -6
- package/src/postgres/entities.test.ts +2 -2
- package/src/postgres/entities.ts +316 -28
- package/src/postgres/entity-types.ts +6 -0
- package/src/postgres/import/generator.ts +31 -4
- package/src/postgres/kinds.test.ts +140 -0
- package/src/postgres/live/catalog-kinds.ts +312 -0
- package/src/postgres/live/catalog.ts +30 -12
- package/src/postgres/live/describe-resources.ts +14 -5
- package/src/postgres/live/kinds.e2e.test.ts +182 -0
- package/src/postgres/live/roundtrip.e2e.test.ts +9 -3
- package/src/postgres/live/undeclarable.e2e.test.ts +15 -13
- package/src/postgres/parser.test.ts +2 -1
- package/src/postgres/parser.ts +307 -17
- package/src/postgres/plan/deep.ts +33 -0
- package/src/postgres/plan/diff.ts +45 -0
- package/src/postgres/plan/normalize.ts +153 -2
- package/src/postgres/plan/rules.ts +21 -0
- package/src/postgres/plan/schema.ts +6 -4
- package/src/postgres/testing/roundtrip-schema.ts +19 -1
- package/src/postgres.ts +19 -0
- package/src/skill-defs.ts +1 -1
- package/src/skills/chant-sql-postgres.md +7 -3
- package/src/skills/chant-sql.md +3 -1
- package/src/unknown-clauses.test.ts +2 -1
- package/src/validate.ts +8 -0
|
@@ -29,10 +29,10 @@ const CATALOG: FakeCatalog = {
|
|
|
29
29
|
schemas: [{ name: "app" }],
|
|
30
30
|
tables: [{ schema: "app", name: "users", columns: [{ name: "id", type: "bigint", notnull: true }] }],
|
|
31
31
|
undeclarable: [
|
|
32
|
-
{ kind: "
|
|
33
|
-
{ kind: "
|
|
34
|
-
{ kind: "
|
|
35
|
-
{ kind: "
|
|
32
|
+
{ kind: "access method", name: "heap2" },
|
|
33
|
+
{ kind: "conversion", schema: "app", name: "latin1_utf8" },
|
|
34
|
+
{ kind: "rule", schema: "app", name: "users_notify ON users" },
|
|
35
|
+
{ kind: "base type", schema: "app", name: "later" },
|
|
36
36
|
],
|
|
37
37
|
};
|
|
38
38
|
|
|
@@ -45,10 +45,10 @@ describe("Postgres live import", () => {
|
|
|
45
45
|
);
|
|
46
46
|
expect(err).toBeInstanceOf(UndeclarableObjectsError);
|
|
47
47
|
const message = (err as Error).message;
|
|
48
|
-
for (const line of [" -
|
|
48
|
+
for (const line of [" - access method heap2", " - conversion app.latin1_utf8", " - rule app.users_notify ON users", " - base type app.later"]) {
|
|
49
49
|
expect(message).toContain(line);
|
|
50
50
|
}
|
|
51
|
-
expect(message).toContain(" -
|
|
51
|
+
expect(message).toContain(" - conversion: no tag declares an encoding conversion");
|
|
52
52
|
expect(message).toContain("--skip-undeclarable");
|
|
53
53
|
});
|
|
54
54
|
|
|
@@ -58,7 +58,7 @@ describe("Postgres live import", () => {
|
|
|
58
58
|
expect(ir.resources.map((r) => r.properties.name)).toContain("users");
|
|
59
59
|
const left = (ir.warnings ?? []).filter((w) => w.includes("is left out (--skip-undeclarable)"));
|
|
60
60
|
expect(left).toHaveLength(4);
|
|
61
|
-
expect(left[0]).toBe("
|
|
61
|
+
expect(left[0]).toBe("access method heap2 is left out (--skip-undeclarable): no tag declares an access method; an extension usually installs one, and the extension is declared instead");
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
test("a selector or owned import does not look", async () => {
|
|
@@ -68,11 +68,11 @@ describe("Postgres live import", () => {
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
test("a managed provider's own objects are its, with a warning, not a refusal", async () => {
|
|
71
|
-
pg.catalog = { ...CATALOG, undeclarable: [{ kind: "
|
|
71
|
+
pg.catalog = { ...CATALOG, undeclarable: [{ kind: "conversion", schema: "auth", name: "x" }] };
|
|
72
72
|
pg.provider = "supabase";
|
|
73
73
|
try {
|
|
74
74
|
const ir = await exportPostgres({ environment: "prod" });
|
|
75
|
-
expect(ir.warnings).toContain("
|
|
75
|
+
expect(ir.warnings).toContain("conversion auth.x is kept by Supabase; left out");
|
|
76
76
|
} finally {
|
|
77
77
|
pg.provider = undefined;
|
|
78
78
|
}
|
|
@@ -90,8 +90,8 @@ describe("ClickHouse live import", () => {
|
|
|
90
90
|
],
|
|
91
91
|
{
|
|
92
92
|
undeclarable: [
|
|
93
|
-
{ kind: "
|
|
94
|
-
{ kind: "
|
|
93
|
+
{ kind: "named collection", name: "nc1" },
|
|
94
|
+
{ kind: "workload", name: "all" },
|
|
95
95
|
{ kind: "window view", database: "analytics", name: "wv" },
|
|
96
96
|
],
|
|
97
97
|
},
|
|
@@ -101,7 +101,7 @@ describe("ClickHouse live import", () => {
|
|
|
101
101
|
const run = (extra: Record<string, unknown> = {}) => exportClickHouse({ environment: "x", config: {}, env: { CLICKHOUSE_URL: server.url }, ...extra });
|
|
102
102
|
|
|
103
103
|
test("fails naming them", async () => {
|
|
104
|
-
await expect(run()).rejects.toThrow(/ -
|
|
104
|
+
await expect(run()).rejects.toThrow(/ - named collection nc1\n - window view analytics\.wv\n - workload all\n/);
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
test("with skipUndeclarable, warns, and the window view is not imported as a table", async () => {
|
|
@@ -115,24 +115,27 @@ describe("file import", () => {
|
|
|
115
115
|
const DDL = `CREATE SCHEMA app;
|
|
116
116
|
CREATE TABLE app.t (id int PRIMARY KEY);
|
|
117
117
|
CREATE AGGREGATE app.total(int) (sfunc = int4pl, stype = int);
|
|
118
|
+
CREATE CONVERSION app.latin1_utf8 FOR 'LATIN1' TO 'UTF8' FROM iso8859_1_to_utf8;
|
|
118
119
|
CREATE TYPE app.pair AS (a int, b text);
|
|
120
|
+
CREATE TYPE app.later;
|
|
119
121
|
CREATE COLLATION app.ci (provider = icu, locale = 'und');`;
|
|
120
122
|
|
|
121
123
|
test("a CREATE of a kind chant cannot declare is an error naming it", () => {
|
|
122
|
-
expect(() => new SqlFileParser("postgres").parse(DDL)).toThrow(/
|
|
124
|
+
expect(() => new SqlFileParser("postgres").parse(DDL)).toThrow(/conversion: chant cannot declare it \(no tag declares an encoding conversion\): CREATE CONVERSION app\.latin1_utf8/);
|
|
123
125
|
});
|
|
124
126
|
|
|
125
127
|
test("skipUndeclarable imports the rest and lists them as warnings", () => {
|
|
126
128
|
const ir = new SqlFileParser("postgres", { skipUndeclarable: true }).parse(DDL);
|
|
127
|
-
|
|
129
|
+
// An aggregate, a composite type and a collation are declared kinds (#3911): imported, not refused.
|
|
130
|
+
expect(ir.resources.map((r) => r.properties.name)).toEqual(["app", "t", "total", "pair", "ci"]);
|
|
128
131
|
expect(ir.warnings).toEqual([
|
|
129
|
-
"left out (skipUndeclarable):
|
|
130
|
-
"left out (skipUndeclarable):
|
|
131
|
-
"left out (skipUndeclarable): collation: chant cannot declare it (no tag declares a collation yet): CREATE COLLATION app.ci (provider = icu, locale = 'und')",
|
|
132
|
+
"left out (skipUndeclarable): conversion: chant cannot declare it (no tag declares an encoding conversion): CREATE CONVERSION app.latin1_utf8 FOR 'LATIN1' TO 'UTF8' FROM iso8859_1_to_utf8",
|
|
133
|
+
"left out (skipUndeclarable): base type: chant cannot declare it (no tag declares a base type; its input and output functions are C code an extension installs): CREATE TYPE app.later",
|
|
132
134
|
]);
|
|
133
135
|
});
|
|
134
136
|
|
|
135
|
-
test("ClickHouse: a
|
|
136
|
-
expect(() => new SqlFileParser("clickhouse").parse("CREATE DATABASE a;\nCREATE
|
|
137
|
+
test("ClickHouse: a named collection in a file is refused too; a quota is declared (#3911)", () => {
|
|
138
|
+
expect(() => new SqlFileParser("clickhouse").parse("CREATE DATABASE a;\nCREATE NAMED COLLECTION c AS url = 'x'")).toThrow(/named collection: chant cannot declare it/);
|
|
139
|
+
expect(new SqlFileParser("clickhouse").parse("CREATE DATABASE a;\nCREATE QUOTA q1").resources.map((r) => r.type)).toEqual(["ClickHouse::Database", "ClickHouse::Quota"]);
|
|
137
140
|
});
|
|
138
141
|
});
|
|
@@ -5,21 +5,21 @@
|
|
|
5
5
|
{ "command": "CREATE DICTIONARY", "kind": "dictionary", "status": "declared", "entity": "ClickHouse::Dictionary" },
|
|
6
6
|
{ "command": "CREATE FUNCTION", "kind": "SQL function", "status": "declared", "entity": "ClickHouse::Function" },
|
|
7
7
|
{ "command": "CREATE FUNCTION", "form": "LANGUAGE WASM, or with a driver", "match": "\\bLANGUAGE\\b|\\bUSING\\s+DRIVER\\b", "kind": "non-SQL function", "status": "refused", "reason": "only a SQL function (CREATE FUNCTION f AS (x) -> ...) is declared" },
|
|
8
|
-
{ "command": "CREATE HANDLER", "kind": "HTTP handler", "status": "refused", "reason": "
|
|
8
|
+
{ "command": "CREATE HANDLER", "kind": "HTTP handler", "status": "refused", "reason": "an HTTP handler is server-level configuration, not a schema object" },
|
|
9
9
|
{ "command": "CREATE INDEX", "kind": "index", "status": "declared", "entity": "ClickHouse::Table", "note": "an index is part of its table: SHOW CREATE TABLE prints it, and the table's declaration holds it" },
|
|
10
|
-
{ "command": "CREATE MASKING POLICY", "kind": "masking policy", "status": "refused", "reason": "
|
|
11
|
-
{ "command": "CREATE NAMED COLLECTION", "kind": "named collection", "status": "refused", "reason": "
|
|
12
|
-
{ "command": "CREATE QUOTA", "kind": "quota", "status": "
|
|
13
|
-
{ "command": "CREATE RESOURCE", "kind": "workload resource", "status": "refused", "reason": "
|
|
10
|
+
{ "command": "CREATE MASKING POLICY", "kind": "masking policy", "status": "refused", "reason": "masking policies are available only in ClickHouse Cloud, not on the pinned server" },
|
|
11
|
+
{ "command": "CREATE NAMED COLLECTION", "kind": "named collection", "status": "refused", "reason": "a named collection holds credentials (a password, a secret key), which are the environment's and never in a build; the server hides its values, so a plan could not compare them either" },
|
|
12
|
+
{ "command": "CREATE QUOTA", "kind": "quota", "status": "declared", "entity": "ClickHouse::Quota", "note": "import reads only the access objects a build declares" },
|
|
13
|
+
{ "command": "CREATE RESOURCE", "kind": "workload resource", "status": "refused", "reason": "a resource is server-level scheduling (a disk's reads or writes), configured with the server, not a schema object" },
|
|
14
14
|
{ "command": "CREATE ROLE", "kind": "role", "status": "declared", "entity": "ClickHouse::Role", "note": "import reads only the access objects a build declares" },
|
|
15
15
|
{ "command": "CREATE ROW POLICY", "kind": "row policy", "status": "declared", "entity": "ClickHouse::RowPolicy", "note": "import reads only the access objects a build declares" },
|
|
16
|
-
{ "command": "CREATE SETTINGS PROFILE", "kind": "settings profile", "status": "
|
|
16
|
+
{ "command": "CREATE SETTINGS PROFILE", "kind": "settings profile", "status": "declared", "entity": "ClickHouse::SettingsProfile", "note": "import reads only the access objects a build declares" },
|
|
17
17
|
{ "command": "CREATE TABLE", "kind": "table", "status": "declared", "entity": "ClickHouse::Table" },
|
|
18
18
|
{ "command": "CREATE TABLE", "form": "TEMPORARY", "match": "^CREATE\\s+TEMPORARY\\b", "kind": "temporary table", "status": "out-of-scope", "reason": "a temporary table lives in one session" },
|
|
19
19
|
{ "command": "CREATE USER", "kind": "user", "status": "declared", "entity": "ClickHouse::User", "note": "import reads only the access objects a build declares" },
|
|
20
20
|
{ "command": "CREATE VIEW", "kind": "view", "status": "declared", "entity": "ClickHouse::View" },
|
|
21
21
|
{ "command": "CREATE VIEW", "form": "MATERIALIZED", "match": "^CREATE\\s+(?:OR\\s+REPLACE\\s+)?MATERIALIZED\\b", "kind": "materialized view", "status": "declared", "entity": "ClickHouse::MaterializedView" },
|
|
22
|
-
{ "command": "CREATE WINDOW VIEW", "kind": "window view", "status": "refused", "reason": "
|
|
23
|
-
{ "command": "CREATE WORKLOAD", "kind": "workload", "status": "refused", "reason": "
|
|
22
|
+
{ "command": "CREATE WINDOW VIEW", "kind": "window view", "status": "refused", "reason": "a window view is experimental, and the pinned server refuses it under its default query analyzer" },
|
|
23
|
+
{ "command": "CREATE WORKLOAD", "kind": "workload", "status": "refused", "reason": "a workload is server-level scheduling, configured with the server, not a schema object" }
|
|
24
24
|
]
|
|
25
25
|
}
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"dialect": "postgres",
|
|
3
3
|
"kinds": [
|
|
4
4
|
{ "command": "CREATE ACCESS METHOD", "kind": "access method", "status": "refused", "reason": "no tag declares an access method; an extension usually installs one, and the extension is declared instead" },
|
|
5
|
-
{ "command": "CREATE AGGREGATE", "kind": "aggregate", "status": "
|
|
6
|
-
{ "command": "CREATE CAST", "kind": "cast", "status": "
|
|
7
|
-
{ "command": "CREATE COLLATION", "kind": "collation", "status": "
|
|
5
|
+
{ "command": "CREATE AGGREGATE", "kind": "aggregate", "status": "declared", "entity": "Postgres::Aggregate" },
|
|
6
|
+
{ "command": "CREATE CAST", "kind": "cast", "status": "declared", "entity": "Postgres::Cast", "note": "the database's, named by its two types; read where a type or its function is in scope" },
|
|
7
|
+
{ "command": "CREATE COLLATION", "kind": "collation", "status": "declared", "entity": "Postgres::Collation" },
|
|
8
8
|
{ "command": "CREATE CONVERSION", "kind": "conversion", "status": "refused", "reason": "no tag declares an encoding conversion" },
|
|
9
9
|
{ "command": "CREATE DATABASE", "kind": "database", "status": "out-of-scope", "reason": "a project's schema lives in one database, which the environment provides (sql.profiles.<env>)" },
|
|
10
10
|
{ "command": "CREATE DOMAIN", "kind": "domain", "status": "declared", "entity": "Postgres::Domain" },
|
|
11
|
-
{ "command": "CREATE EVENT TRIGGER", "kind": "event trigger", "status": "
|
|
11
|
+
{ "command": "CREATE EVENT TRIGGER", "kind": "event trigger", "status": "declared", "entity": "Postgres::EventTrigger", "note": "the database's, not a schema's; creating one takes a superuser" },
|
|
12
12
|
{ "command": "CREATE EXTENSION", "kind": "extension", "status": "declared", "entity": "Postgres::Extension" },
|
|
13
13
|
{ "command": "CREATE FOREIGN DATA WRAPPER", "kind": "foreign data wrapper", "status": "refused", "reason": "no tag declares a foreign data wrapper yet; one an extension installs comes with the extension" },
|
|
14
14
|
{ "command": "CREATE FOREIGN TABLE", "kind": "foreign table", "status": "refused", "reason": "no tag declares a foreign table yet" },
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
{ "command": "CREATE TRANSFORM", "kind": "transform", "status": "refused", "reason": "no tag declares a transform; an extension installs one" },
|
|
42
42
|
{ "command": "CREATE TRIGGER", "kind": "trigger", "status": "declared", "entity": "Postgres::Trigger" },
|
|
43
43
|
{ "command": "CREATE TYPE", "form": "AS ENUM", "match": "\\bAS\\s+ENUM\\b", "kind": "enum type", "status": "declared", "entity": "Postgres::Enum" },
|
|
44
|
-
{ "command": "CREATE TYPE", "form": "AS (...)", "match": "\\bAS\\s*\\(", "kind": "composite type", "status": "
|
|
45
|
-
{ "command": "CREATE TYPE", "form": "AS RANGE", "match": "\\bAS\\s+RANGE\\b", "kind": "range type", "status": "
|
|
44
|
+
{ "command": "CREATE TYPE", "form": "AS (...)", "match": "\\bAS\\s*\\(", "kind": "composite type", "status": "declared", "entity": "Postgres::CompositeType" },
|
|
45
|
+
{ "command": "CREATE TYPE", "form": "AS RANGE", "match": "\\bAS\\s+RANGE\\b", "kind": "range type", "status": "declared", "entity": "Postgres::RangeType", "note": "its multirange type comes with it (MULTIRANGE_TYPE_NAME)" },
|
|
46
46
|
{ "command": "CREATE TYPE", "form": "INPUT, OUTPUT, or a shell type", "kind": "base type", "status": "refused", "reason": "no tag declares a base type; its input and output functions are C code an extension installs" },
|
|
47
47
|
{ "command": "CREATE USER", "kind": "user", "status": "declared", "entity": "Postgres::Role", "note": "an alias of CREATE ROLE ... LOGIN" },
|
|
48
48
|
{ "command": "CREATE USER MAPPING", "kind": "user mapping", "status": "refused", "reason": "no tag declares a user mapping yet" },
|
package/src/files/clickhouse.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* A file of ClickHouse DDL as the objects it declares: one per `CREATE
|
|
3
3
|
* DATABASE`, `TABLE`, `VIEW`, `MATERIALIZED VIEW`, `DICTIONARY` or `FUNCTION`, as `chant import
|
|
4
4
|
* <file>.sql` reads them, and one per `CREATE USER`, `CREATE ROLE`, `CREATE
|
|
5
|
-
* ROW POLICY`
|
|
5
|
+
* ROW POLICY`, `CREATE QUOTA`, `CREATE SETTINGS PROFILE` (#3911) and `GRANT`
|
|
6
|
+
* (#3711), each read by its tag, so what the tag
|
|
6
7
|
* refuses (`IDENTIFIED BY`, a `REVOKE`) is refused with the tag's message.
|
|
7
8
|
* Any other statement, and any statement the parser refuses, is a problem
|
|
8
9
|
* naming it.
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
import type { Declarable } from "@intentius/chant/declarable";
|
|
16
17
|
import { isTrivia, tokenizeText } from "../clickhouse/tokens";
|
|
17
18
|
import { parseCreate } from "../clickhouse/parser";
|
|
18
|
-
import { CLICKHOUSE_ENTITY_TYPES, database, dictionary, func, grant, policy, role, table, user, view } from "../clickhouse/entities";
|
|
19
|
+
import { CLICKHOUSE_ENTITY_TYPES, database, dictionary, func, grant, policy, quota, role, settingsProfile, table, user, view } from "../clickhouse/entities";
|
|
19
20
|
import { isAccessStatement, parseAccess, type AccessNode } from "../clickhouse/access";
|
|
20
21
|
import type { Token } from "../clickhouse/tokens";
|
|
21
22
|
import { CLICKHOUSE_TAG_OF } from "../clickhouse/import/generator";
|
|
@@ -24,7 +25,13 @@ import { fileUnparsed, firstLine, lineAt, quoteBare, splitTokens, tokensText, un
|
|
|
24
25
|
import { templateSafe, templateStrings } from "./postgres";
|
|
25
26
|
|
|
26
27
|
type Tag = (strings: TemplateStringsArray, ...values: unknown[]) => Declarable;
|
|
27
|
-
const TAGS: Record<string, Tag> = { database, table, view, dictionary, func, user, role, policy, grant };
|
|
28
|
+
const TAGS: Record<string, Tag> = { database, table, view, dictionary, func, user, role, policy, quota, settingsProfile, grant };
|
|
29
|
+
|
|
30
|
+
/** The tag an access statement's words name: `CREATE USER`, `CREATE SETTINGS PROFILE`, `GRANT`. */
|
|
31
|
+
function accessTag(words: string[]): Tag {
|
|
32
|
+
if (words[0] !== "CREATE") return grant;
|
|
33
|
+
return words[1] === "USER" ? user : words[1] === "ROLE" ? role : words[1] === "QUOTA" ? quota : words[1] === "SETTINGS" || words[1] === "PROFILE" ? settingsProfile : policy;
|
|
34
|
+
}
|
|
28
35
|
|
|
29
36
|
/** Tokens with `[db.]name`'s bare name qualified with `schema`. A `*` (all tables) is left alone. */
|
|
30
37
|
function qualifyBare(tokens: Token[], span: { from: number; to: number } | undefined, schema: string): string | undefined {
|
|
@@ -44,8 +51,7 @@ function readAccess(sql: string, options: ReadOptions): ImportedObject | string
|
|
|
44
51
|
node = parseAccess(tokens);
|
|
45
52
|
} catch (e) {
|
|
46
53
|
// What the tag refuses (a password) is refused in the tag's words.
|
|
47
|
-
const
|
|
48
|
-
const tag = words[0] === "CREATE" ? (words[1] === "USER" ? user : words[1] === "ROLE" ? role : policy) : grant;
|
|
54
|
+
const tag = accessTag(sql.replace(/--[^\n]*\n/g, " ").trim().split(/\s+/, 2).map((w) => w.toUpperCase()));
|
|
49
55
|
try {
|
|
50
56
|
tag(templateStrings([templateSafe(sql)]));
|
|
51
57
|
} catch (refused) {
|
|
@@ -61,7 +67,7 @@ function readAccess(sql: string, options: ReadOptions): ImportedObject | string
|
|
|
61
67
|
node = parseAccess(tokens);
|
|
62
68
|
}
|
|
63
69
|
}
|
|
64
|
-
const tag =
|
|
70
|
+
const tag = { user, role, rowPolicy: policy, quota, settingsProfile, grant }[node.statement];
|
|
65
71
|
let props: { name: string; database?: string; table?: string };
|
|
66
72
|
try {
|
|
67
73
|
props = (tag(templateStrings([templateSafe(sql)])) as unknown as { props: typeof props }).props;
|
|
@@ -70,6 +76,8 @@ function readAccess(sql: string, options: ReadOptions): ImportedObject | string
|
|
|
70
76
|
}
|
|
71
77
|
if (node.statement === "user") return { type: CLICKHOUSE_ENTITY_TYPES.user, name: props.name, ddl: sql };
|
|
72
78
|
if (node.statement === "role") return { type: CLICKHOUSE_ENTITY_TYPES.role, name: props.name, ddl: sql };
|
|
79
|
+
if (node.statement === "quota") return { type: CLICKHOUSE_ENTITY_TYPES.quota, name: props.name, ddl: sql };
|
|
80
|
+
if (node.statement === "settingsProfile") return { type: CLICKHOUSE_ENTITY_TYPES.settingsProfile, name: props.name, ddl: sql };
|
|
73
81
|
if (node.statement === "rowPolicy") return { type: CLICKHOUSE_ENTITY_TYPES.rowPolicy, ...(props.database ? { database: props.database } : {}), name: props.name, ddl: sql };
|
|
74
82
|
// A grant has no name of its own: what it grants, on what, to whom.
|
|
75
83
|
return { type: CLICKHOUSE_ENTITY_TYPES.grant, name: `grant ${tokensText(tokens.slice(tokens.findIndex((t) => !isTrivia(t)) + 1)).replace(/\s+/g, " ").trim().toLowerCase()}`, ddl: sql };
|
package/src/files/postgres.ts
CHANGED
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
* With `schema`, the names the DDL leaves unqualified are qualified with it,
|
|
17
17
|
* as an ORM's connection would: an object's own name, an index's, trigger's
|
|
18
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.
|
|
19
|
+
* type naming an enum, composite, range or domain type the file creates.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import type { Declarable } from "@intentius/chant/declarable";
|
|
23
23
|
import { isTrivia, tokenizeText, type Token } from "../postgres/tokens";
|
|
24
|
-
import { identValue, parseStatements, type NameNode, type StatementNode } from "../postgres/parser";
|
|
24
|
+
import { identValue, parseStatements, type NameNode, type Span, type StatementNode } from "../postgres/parser";
|
|
25
25
|
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";
|
|
@@ -117,7 +117,7 @@ export function readPostgres(ddl: string, options: ReadOptions, problems: string
|
|
|
117
117
|
// The types the file creates, so a column of one is qualified with the rest.
|
|
118
118
|
const fileTypes = new Set<string>();
|
|
119
119
|
for (const { tokens, node } of parsed) {
|
|
120
|
-
if (node.statement === "enum" || node.statement === "domain") {
|
|
120
|
+
if (node.statement === "enum" || node.statement === "domain" || node.statement === "composite" || node.statement === "range") {
|
|
121
121
|
const pieces = nameOf(tokens, node.name);
|
|
122
122
|
if (pieces.length === 1) fileTypes.add(pieces[0]!);
|
|
123
123
|
}
|
|
@@ -138,7 +138,7 @@ export function readPostgres(ddl: string, options: ReadOptions, problems: string
|
|
|
138
138
|
};
|
|
139
139
|
switch (node.statement) {
|
|
140
140
|
case "comment":
|
|
141
|
-
if (
|
|
141
|
+
if (!["SCHEMA", "EXTENSION", "ROLE", "TRIGGER", "POLICY", "CONSTRAINT", "COLUMN", "CAST", "EVENT TRIGGER"].includes(node.objectType)) mark(node.target);
|
|
142
142
|
if (node.objectType === "COLUMN" && nameOf(tokens, node.target).length === 2 && options.schema) {
|
|
143
143
|
const first = tokens.slice(node.target.span.from, node.target.span.to).find((t) => !isTrivia(t));
|
|
144
144
|
if (first) inserts.add(tokens.indexOf(first));
|
|
@@ -177,7 +177,7 @@ export function readPostgres(ddl: string, options: ReadOptions, problems: string
|
|
|
177
177
|
problems.push(`an index with no name (its declaration needs one): ${firstLine(tokensText(tokens))}`);
|
|
178
178
|
continue;
|
|
179
179
|
}
|
|
180
|
-
const ownName = !["schema", "extension", "index", "trigger", "policy", "role", "grant"].includes(node.statement);
|
|
180
|
+
const ownName = !["schema", "extension", "index", "trigger", "policy", "role", "grant", "cast", "eventTrigger"].includes(node.statement);
|
|
181
181
|
if (ownName && "name" in node) mark(node.name);
|
|
182
182
|
const object: PgFileObject = { node, statements: [rewrite(tokens, inserts, options.schema)], constraints: [], line, unparsed: fileUnparsed("unknown" in node ? node.unknown : undefined, tokens, tokensText(tokens), line) };
|
|
183
183
|
objects.push(object);
|
|
@@ -277,6 +277,10 @@ function ownerKey(node: StatementNode, tokens: Token[], nameOf: NameOf, qualify:
|
|
|
277
277
|
case "trigger":
|
|
278
278
|
case "policy":
|
|
279
279
|
return `${node.statement} ${nameOf(tokens, node.name).at(-1) ?? ""} ON ${qkey(qualify(nameOf(tokens, node.table)))}`;
|
|
280
|
+
case "eventTrigger":
|
|
281
|
+
return `eventTrigger ${nameOf(tokens, node.name).at(-1) ?? ""}`;
|
|
282
|
+
case "cast":
|
|
283
|
+
return `cast ${castKey(tokens, node.source, node.target)}`;
|
|
280
284
|
case "grant":
|
|
281
285
|
case "comment":
|
|
282
286
|
case "rowSecurity":
|
|
@@ -305,6 +309,10 @@ function followOnOwner(node: StatementNode, tokens: Token[], nameOf: NameOf, qua
|
|
|
305
309
|
case "TRIGGER":
|
|
306
310
|
case "POLICY":
|
|
307
311
|
return node.on ? `${node.objectType.toLowerCase()} ${target.at(-1) ?? ""} ON ${qkey(qualify(nameOf(tokens, node.on)))}` : undefined;
|
|
312
|
+
case "EVENT TRIGGER":
|
|
313
|
+
return `eventTrigger ${target.at(-1) ?? ""}`;
|
|
314
|
+
case "CAST":
|
|
315
|
+
return node.cast ? `cast ${castKey(tokens, node.cast.source, node.cast.target)}` : undefined;
|
|
308
316
|
case "CONSTRAINT":
|
|
309
317
|
case "DOMAIN CONSTRAINT":
|
|
310
318
|
return node.on ? `rel ${qkey(qualify(nameOf(tokens, node.on)))}` : undefined;
|
|
@@ -313,6 +321,12 @@ function followOnOwner(node: StatementNode, tokens: Token[], nameOf: NameOf, qua
|
|
|
313
321
|
}
|
|
314
322
|
}
|
|
315
323
|
|
|
324
|
+
/** A cast's two types as one key, spacing and case aside. */
|
|
325
|
+
function castKey(tokens: readonly Token[], source: Span, target: Span): string {
|
|
326
|
+
const text = (sp: Span) => tokens.slice(sp.from, sp.to).filter((t) => !isTrivia(t)).map((t) => (t.kind === "ident" ? t.text.toLowerCase() : t.text)).join(" ");
|
|
327
|
+
return `(${text(source)} AS ${text(target)})`;
|
|
328
|
+
}
|
|
329
|
+
|
|
316
330
|
/** The statement's tokens with `schema.` inserted before each marked one. */
|
|
317
331
|
function rewrite(tokens: readonly Token[], inserts: ReadonlySet<number>, schema: string | undefined): Token[] {
|
|
318
332
|
if (!schema || inserts.size === 0) return [...tokens];
|
|
@@ -49,6 +49,16 @@
|
|
|
49
49
|
"kind": "resource",
|
|
50
50
|
"lexicon": "sql"
|
|
51
51
|
},
|
|
52
|
+
"Quota": {
|
|
53
|
+
"resourceType": "ClickHouse::Quota",
|
|
54
|
+
"kind": "resource",
|
|
55
|
+
"lexicon": "sql"
|
|
56
|
+
},
|
|
57
|
+
"SettingsProfile": {
|
|
58
|
+
"resourceType": "ClickHouse::SettingsProfile",
|
|
59
|
+
"kind": "resource",
|
|
60
|
+
"lexicon": "sql"
|
|
61
|
+
},
|
|
52
62
|
"PostgresSchema": {
|
|
53
63
|
"resourceType": "Postgres::Schema",
|
|
54
64
|
"kind": "resource",
|
|
@@ -84,11 +94,41 @@
|
|
|
84
94
|
"kind": "resource",
|
|
85
95
|
"lexicon": "sql"
|
|
86
96
|
},
|
|
97
|
+
"PostgresCompositeType": {
|
|
98
|
+
"resourceType": "Postgres::CompositeType",
|
|
99
|
+
"kind": "resource",
|
|
100
|
+
"lexicon": "sql"
|
|
101
|
+
},
|
|
102
|
+
"PostgresRangeType": {
|
|
103
|
+
"resourceType": "Postgres::RangeType",
|
|
104
|
+
"kind": "resource",
|
|
105
|
+
"lexicon": "sql"
|
|
106
|
+
},
|
|
87
107
|
"PostgresDomain": {
|
|
88
108
|
"resourceType": "Postgres::Domain",
|
|
89
109
|
"kind": "resource",
|
|
90
110
|
"lexicon": "sql"
|
|
91
111
|
},
|
|
112
|
+
"PostgresAggregate": {
|
|
113
|
+
"resourceType": "Postgres::Aggregate",
|
|
114
|
+
"kind": "resource",
|
|
115
|
+
"lexicon": "sql"
|
|
116
|
+
},
|
|
117
|
+
"PostgresCollation": {
|
|
118
|
+
"resourceType": "Postgres::Collation",
|
|
119
|
+
"kind": "resource",
|
|
120
|
+
"lexicon": "sql"
|
|
121
|
+
},
|
|
122
|
+
"PostgresCast": {
|
|
123
|
+
"resourceType": "Postgres::Cast",
|
|
124
|
+
"kind": "resource",
|
|
125
|
+
"lexicon": "sql"
|
|
126
|
+
},
|
|
127
|
+
"PostgresEventTrigger": {
|
|
128
|
+
"resourceType": "Postgres::EventTrigger",
|
|
129
|
+
"kind": "resource",
|
|
130
|
+
"lexicon": "sql"
|
|
131
|
+
},
|
|
92
132
|
"PostgresExtension": {
|
|
93
133
|
"resourceType": "Postgres::Extension",
|
|
94
134
|
"kind": "resource",
|
package/src/index.ts
CHANGED
|
@@ -12,13 +12,13 @@ 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, dictionary, user, role, policy, grant, literal } from "./clickhouse/entities";
|
|
15
|
+
export { database, table, view, dictionary, user, role, policy, quota, settingsProfile, 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
|
|
19
19
|
// `@intentius/chant-lexicon-sql/postgres` is Postgres's. Declarations import
|
|
20
20
|
// them from the dialect subpath.
|
|
21
|
-
export { schema, index, sequence, type, domain, extension, func, procedure, trigger } from "./postgres/entities";
|
|
21
|
+
export { schema, index, sequence, type, domain, extension, func, procedure, aggregate, trigger, eventTrigger, collation, cast } from "./postgres/entities";
|
|
22
22
|
|
|
23
23
|
// A schema change's statements between two builds, rendered offline for a
|
|
24
24
|
// migration file (#3644); also at `@intentius/chant-lexicon-sql/migration-statements`.
|
|
@@ -137,11 +137,24 @@ export function projectFromObjects(objects: OutputObject[], major: number): PgPr
|
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
const TYPE_OBJECTS = new Set(["Postgres::Enum", "Postgres::Domain", "Postgres::Table", "Postgres::View", "Postgres::MaterializedView"]);
|
|
140
|
+
const TYPE_OBJECTS = new Set(["Postgres::Enum", "Postgres::CompositeType", "Postgres::RangeType", "Postgres::Domain", "Postgres::Table", "Postgres::View", "Postgres::MaterializedView"]);
|
|
141
141
|
const ROUTINES = new Set(["Postgres::Function", "Postgres::Procedure"]);
|
|
142
142
|
|
|
143
|
+
/** A range type's multirange type's name: as written (its last piece), else as Postgres derives it. */
|
|
144
|
+
function multirangeOf(o: OutputObject): string | undefined {
|
|
145
|
+
if (o.type !== "Postgres::RangeType") return undefined;
|
|
146
|
+
const written = o.multirangeTypeName as string | undefined;
|
|
147
|
+
if (written) {
|
|
148
|
+
const last = written.split(".").pop()!.trim();
|
|
149
|
+
return /^"/.test(last) ? last.slice(1, -1).replace(/""/g, '"') : last.toLowerCase();
|
|
150
|
+
}
|
|
151
|
+
const name = String(o.name);
|
|
152
|
+
const at = name.indexOf("range");
|
|
153
|
+
return at >= 0 ? `${name.slice(0, at)}multi${name.slice(at)}` : `${name}_multirange`;
|
|
154
|
+
}
|
|
155
|
+
|
|
143
156
|
function declared(p: PgProject, kinds: ReadonlySet<string>, schema: string | undefined, name: string): OutputObject | undefined {
|
|
144
|
-
return p.objects.find((o) => kinds.has(o.type) && o.name === name && (schema === undefined || o.schema === schema));
|
|
157
|
+
return p.objects.find((o) => kinds.has(o.type) && (o.name === name || multirangeOf(o) === name) && (schema === undefined || o.schema === schema));
|
|
145
158
|
}
|
|
146
159
|
|
|
147
160
|
/** The extension that provides `name` in `section`, if the overlay knows one; declared ones first. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { checkOf } from "./postgres-helpers";
|
|
2
2
|
import { postgresObjects } from "./sql-helpers";
|
|
3
3
|
|
|
4
|
-
const PLACED = new Set(["Postgres::Table", "Postgres::View", "Postgres::MaterializedView", "Postgres::Sequence", "Postgres::Enum", "Postgres::Domain"]);
|
|
4
|
+
const PLACED = new Set(["Postgres::Table", "Postgres::View", "Postgres::MaterializedView", "Postgres::Sequence", "Postgres::Enum", "Postgres::CompositeType", "Postgres::RangeType", "Postgres::Domain"]);
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* SQLPG113: an object in the public schema while the project declares schemas.
|
|
@@ -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", "policy", "role", "grant"],
|
|
20
|
+
tags: ["schema", "table", "index", "view", "sequence", "type", "domain", "extension", "func", "procedure", "aggregate", "trigger", "eventTrigger", "collation", "cast", "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", "policy", "role", "grant"],
|
|
25
|
+
tags: ["schema", "index", "sequence", "type", "domain", "extension", "func", "procedure", "aggregate", "trigger", "eventTrigger", "collation", "cast", "policy", "role", "grant"],
|
|
26
26
|
},
|
|
27
27
|
];
|
|
28
28
|
|
|
@@ -4,15 +4,22 @@ import { parseAccess } from "../../clickhouse/access";
|
|
|
4
4
|
import { SqlSyntaxError } from "../../clickhouse/tokens";
|
|
5
5
|
import { findTemplates, templatePosition, tokensOf } from "./templates";
|
|
6
6
|
|
|
7
|
-
const ACCESS_TAGS = new Set(["user", "role", "policy", "grant"]);
|
|
8
|
-
const ACCESS_STATEMENT: Record<string, string> = {
|
|
7
|
+
const ACCESS_TAGS = new Set(["user", "role", "policy", "quota", "settingsProfile", "grant"]);
|
|
8
|
+
const ACCESS_STATEMENT: Record<string, string> = {
|
|
9
|
+
user: "CREATE USER",
|
|
10
|
+
role: "CREATE ROLE",
|
|
11
|
+
rowPolicy: "CREATE ROW POLICY",
|
|
12
|
+
quota: "CREATE QUOTA",
|
|
13
|
+
settingsProfile: "CREATE SETTINGS PROFILE",
|
|
14
|
+
grant: "GRANT",
|
|
15
|
+
};
|
|
9
16
|
|
|
10
17
|
const STATEMENT: Record<string, string> = { database: "CREATE DATABASE", table: "CREATE TABLE", view: "CREATE VIEW", dictionary: "CREATE DICTIONARY", function: "CREATE FUNCTION" };
|
|
11
18
|
/** The tag a statement belongs in: `func` for a function, the statement's own name otherwise. */
|
|
12
19
|
const tagOf = (statement: string): string => (statement === "function" ? "func" : statement);
|
|
13
20
|
|
|
14
21
|
/**
|
|
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.
|
|
22
|
+
* SQLCH001: the DDL in a ClickHouse template (`database`, `table`, `view`, `dictionary`, `func`, `user`, `role`, `policy`, `quota`, `settingsProfile`, `grant`) does not parse, or holds another statement than its tag.
|
|
16
23
|
*
|
|
17
24
|
* The same parse runs when the build calls the tag, and fails the build there.
|
|
18
25
|
* This rule reports it in the editor and in `chant lint`, at the token, before
|
|
@@ -1,27 +1,12 @@
|
|
|
1
1
|
import type { LintContext, LintDiagnostic, LintRule } from "@intentius/chant/lint/rule";
|
|
2
2
|
import { parseStatements, SqlSyntaxError } from "../../postgres/parser";
|
|
3
|
-
import { followOnStray, STATEMENT_NAMES, type PostgresTag } from "../../postgres/entities";
|
|
3
|
+
import { followOnStray, STATEMENT_NAMES, STATEMENTS_OF, type PostgresTag } from "../../postgres/entities";
|
|
4
4
|
import { findPostgresTemplates, postgresTokensOf, templatePosition, tokenPosition } from "./postgres-templates";
|
|
5
5
|
|
|
6
|
-
/** The
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
index: "index",
|
|
11
|
-
view: "view",
|
|
12
|
-
sequence: "sequence",
|
|
13
|
-
type: "enum",
|
|
14
|
-
domain: "domain",
|
|
15
|
-
extension: "extension",
|
|
16
|
-
func: "function",
|
|
17
|
-
procedure: "procedure",
|
|
18
|
-
trigger: "trigger",
|
|
19
|
-
policy: "policy",
|
|
20
|
-
role: "role",
|
|
21
|
-
grant: "grant",
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const TAG_OF: Record<string, PostgresTag> = Object.fromEntries(Object.entries(STATEMENT).map(([tag, statement]) => [statement, tag as PostgresTag]));
|
|
6
|
+
/** The tag that declares each statement. */
|
|
7
|
+
const TAG_OF: Record<string, PostgresTag> = Object.fromEntries(
|
|
8
|
+
Object.entries(STATEMENTS_OF).flatMap(([tag, statements]) => statements.map((statement) => [statement, tag as PostgresTag])),
|
|
9
|
+
);
|
|
25
10
|
|
|
26
11
|
/**
|
|
27
12
|
* SQLPG001: the DDL in a Postgres template does not parse, or does not hold
|
|
@@ -60,7 +45,7 @@ export const sqlpg001: LintRule = {
|
|
|
60
45
|
if (!tokens) continue;
|
|
61
46
|
try {
|
|
62
47
|
const [node, ...rest] = parseStatements(tokens);
|
|
63
|
-
if (
|
|
48
|
+
if (!STATEMENTS_OF[found.tag].includes(node!.statement)) {
|
|
64
49
|
const use = TAG_OF[node!.statement];
|
|
65
50
|
report(
|
|
66
51
|
use
|
|
@@ -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" | "dictionary" | "func" | "user" | "role" | "policy" | "grant";
|
|
16
|
+
export type SqlTag = "database" | "table" | "view" | "dictionary" | "func" | "user" | "role" | "policy" | "quota" | "settingsProfile" | "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" | "dictionary" | "func" | "us
|
|
|
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", "dictionary", "func", "user", "role", "policy", "grant"],
|
|
25
|
+
tags: ["database", "table", "view", "dictionary", "func", "user", "role", "policy", "quota", "settingsProfile", "grant"],
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
export type FoundTemplate = SqlFoundTemplate<SqlTag>;
|
|
@@ -138,6 +138,20 @@ export function postgresDeclarations(content: string, fileName = "file.ts"): Pos
|
|
|
138
138
|
sqlName = text(node.name.span);
|
|
139
139
|
kind = "enum type";
|
|
140
140
|
break;
|
|
141
|
+
case "composite":
|
|
142
|
+
case "range":
|
|
143
|
+
sqlName = text(node.name.span);
|
|
144
|
+
kind = `${node.statement} type`;
|
|
145
|
+
break;
|
|
146
|
+
case "aggregate":
|
|
147
|
+
case "collation":
|
|
148
|
+
sqlName = text(node.name.span);
|
|
149
|
+
kind = node.statement;
|
|
150
|
+
break;
|
|
151
|
+
case "eventTrigger":
|
|
152
|
+
sqlName = text(node.name.span);
|
|
153
|
+
kind = "event trigger";
|
|
154
|
+
break;
|
|
141
155
|
case "index":
|
|
142
156
|
sqlName = node.name ? text(node.name.span) : undefined;
|
|
143
157
|
break;
|
package/src/mcp/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { McpResourceContribution, McpToolContribution } from "@intentius/ch
|
|
|
10
10
|
import { createCatalogResource, createDiffTool } from "@intentius/chant/lexicon-plugin-helpers";
|
|
11
11
|
import { sqlSerializer } from "../serializer";
|
|
12
12
|
import { catalogIndex } from "../lsp/catalog";
|
|
13
|
-
import { database, dictionary, func, grant, policy, role, table, user, view } from "../clickhouse/entities";
|
|
13
|
+
import { database, dictionary, func, grant, policy, quota, role, settingsProfile, table, user, view } from "../clickhouse/entities";
|
|
14
14
|
import { SqlSyntaxError } from "../clickhouse/tokens";
|
|
15
15
|
import { existsSync, readFileSync } from "fs";
|
|
16
16
|
import { diffSchemas } from "../clickhouse/plan/diff";
|
|
@@ -122,12 +122,12 @@ const searchTool: McpToolContribution = {
|
|
|
122
122
|
const parseTool: McpToolContribution = {
|
|
123
123
|
name: "parse-ddl",
|
|
124
124
|
description:
|
|
125
|
-
"Parse one CREATE statement the way its tag does (dialect clickhouse, the default: table, view, database, dictionary, func, user, role, policy, grant; dialect postgres: schema, table, index, view, sequence, type, domain, extension), and return the entity it builds (columns, engine, keys, settings, and for a view its lineage). A statement that does not parse comes back as the error with its line and column, the same one SQLCH001 (ClickHouse) or SQLPG001 (Postgres) reports. The statement is plain DDL: there are no interpolations.",
|
|
125
|
+
"Parse one CREATE statement the way its tag does (dialect clickhouse, the default: table, view, database, dictionary, func, user, role, policy, quota, settingsProfile, grant; dialect postgres: schema, table, index, view, sequence, type, domain, extension), and return the entity it builds (columns, engine, keys, settings, and for a view its lineage). A statement that does not parse comes back as the error with its line and column, the same one SQLCH001 (ClickHouse) or SQLPG001 (Postgres) reports. The statement is plain DDL: there are no interpolations.",
|
|
126
126
|
inputSchema: {
|
|
127
127
|
type: "object",
|
|
128
128
|
properties: {
|
|
129
129
|
dialect: dialectProp,
|
|
130
|
-
tag: { type: "string", enum: [...new Set(["table", "view", "database", "dictionary", "func", "user", "role", "policy", "grant", ...POSTGRES_TAGS])], description: "The tag the statement belongs in (ClickHouse: table, view, database, dictionary, func, user, role, policy, grant; Postgres: " + POSTGRES_TAGS.join(", ") + ")" },
|
|
130
|
+
tag: { type: "string", enum: [...new Set(["table", "view", "database", "dictionary", "func", "user", "role", "policy", "quota", "settingsProfile", "grant", ...POSTGRES_TAGS])], description: "The tag the statement belongs in (ClickHouse: table, view, database, dictionary, func, user, role, policy, quota, settingsProfile, grant; Postgres: " + POSTGRES_TAGS.join(", ") + ")" },
|
|
131
131
|
ddl: { type: "string", description: "The CREATE statement" },
|
|
132
132
|
},
|
|
133
133
|
required: ["tag", "ddl"],
|
|
@@ -136,8 +136,8 @@ const parseTool: McpToolContribution = {
|
|
|
136
136
|
const ddl = String(params.ddl ?? "");
|
|
137
137
|
const tag = String(params.tag);
|
|
138
138
|
if (asDialect(params.dialect) === "postgres") return postgresParse(tag, ddl, position);
|
|
139
|
-
const build = tag === "table" ? table : tag === "view" ? view : tag === "database" ? database : tag === "dictionary" ? dictionary : tag === "func" ? func : tag === "user" ? user : tag === "role" ? role : tag === "policy" ? policy : tag === "grant" ? grant : undefined;
|
|
140
|
-
if (!build) throw new Error("tag must be table, view, database, dictionary, func, user, role, policy or grant");
|
|
139
|
+
const build = tag === "table" ? table : tag === "view" ? view : tag === "database" ? database : tag === "dictionary" ? dictionary : tag === "func" ? func : tag === "user" ? user : tag === "role" ? role : tag === "policy" ? policy : tag === "quota" ? quota : tag === "settingsProfile" ? settingsProfile : tag === "grant" ? grant : undefined;
|
|
140
|
+
if (!build) throw new Error("tag must be table, view, database, dictionary, func, user, role, policy, quota, settingsProfile or grant");
|
|
141
141
|
const strings = Object.assign([ddl], { raw: [ddl] }) as unknown as TemplateStringsArray;
|
|
142
142
|
try {
|
|
143
143
|
const entity = build(strings);
|