@intentius/chant-lexicon-sql 0.124.30 → 0.124.32
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/change-set.js +250 -0
- package/dist/clickhouse/access.js +722 -0
- package/dist/clickhouse/apply/apply.js +243 -0
- package/dist/clickhouse/apply/mutations.js +66 -0
- package/dist/clickhouse/apply/statements.js +545 -0
- package/dist/clickhouse/catalog-types.js +7 -0
- package/dist/clickhouse/container.js +152 -0
- package/dist/clickhouse/engine-syntax.js +159 -0
- package/dist/clickhouse/entities.js +593 -0
- package/dist/clickhouse/expressions.js +107 -0
- package/dist/clickhouse/http.js +55 -0
- package/dist/clickhouse/import/generator.js +192 -0
- package/dist/clickhouse/import/inlined.js +207 -0
- package/dist/clickhouse/import/ir.js +160 -0
- package/dist/clickhouse/import/live-export.js +131 -0
- package/dist/clickhouse/import/parser.js +17 -0
- package/dist/clickhouse/live/bind.js +116 -0
- package/dist/clickhouse/live/catalog.js +223 -0
- package/dist/clickhouse/live/describe-resources.js +91 -0
- package/dist/clickhouse/live/undeclarable.js +59 -0
- package/dist/clickhouse/overlays/codecs.js +57 -0
- package/dist/clickhouse/overlays/engines.js +101 -0
- package/dist/clickhouse/overlays/kinds.js +1 -0
- package/dist/clickhouse/overlays/projections.js +26 -0
- package/dist/clickhouse/overlays/skip-indexes.js +40 -0
- package/dist/clickhouse/overlays/ttl.js +34 -0
- package/dist/clickhouse/overlays/types.js +59 -0
- package/dist/clickhouse/ownership.js +83 -0
- package/dist/clickhouse/parser.js +674 -0
- package/dist/clickhouse/plan/commands.js +159 -0
- package/dist/clickhouse/plan/deep.js +345 -0
- package/dist/clickhouse/plan/diff.js +312 -0
- package/dist/clickhouse/plan/disruption.js +73 -0
- package/dist/clickhouse/plan/normalize.js +545 -0
- package/dist/clickhouse/plan/rebuild-handoff.js +57 -0
- package/dist/clickhouse/plan/report.js +26 -0
- package/dist/clickhouse/plan/rules.js +72 -0
- package/dist/clickhouse/plan/schema.js +67 -0
- package/dist/clickhouse/plan/server-format.js +35 -0
- package/dist/clickhouse/rebuild/backfill.js +265 -0
- package/dist/clickhouse/rebuild/engines.js +146 -0
- package/dist/clickhouse/rebuild/observe.js +260 -0
- package/dist/clickhouse/rebuild/op.js +152 -0
- package/dist/clickhouse/rebuild/partitions.js +65 -0
- package/dist/clickhouse/rebuild/receipts.js +120 -0
- package/dist/clickhouse/rebuild/replicas.js +121 -0
- package/dist/clickhouse/rebuild/shards.js +76 -0
- package/dist/clickhouse/rebuild/steps.js +383 -0
- package/dist/clickhouse/rebuild/verify.js +279 -0
- package/dist/clickhouse/row-types.js +151 -0
- package/dist/clickhouse/testing/fake-server.js +47 -0
- package/dist/clickhouse/testing/roundtrip-schema.js +25 -0
- package/dist/clickhouse/testing/writable-server.js +123 -0
- package/dist/clickhouse/tokens.js +23 -0
- package/dist/clickhouse/topology.js +463 -0
- package/dist/clickhouse/type-syntax.js +73 -0
- package/dist/clickhouse.js +25 -0
- package/dist/codegen/classifier-page.js +93 -0
- package/dist/codegen/clickhouse-module.js +249 -0
- package/dist/codegen/coverage-page.js +70 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +222 -0
- package/dist/codegen/generate-cli.js +13 -0
- package/dist/codegen/generate.js +97 -0
- package/dist/codegen/package.js +28 -0
- package/dist/codegen/postgres-classifier-page.js +95 -0
- package/dist/codegen/postgres-module.js +418 -0
- package/dist/codegen/registry.js +52 -0
- package/dist/composites/audit-log-table.js +35 -0
- package/dist/composites/catalog.js +524 -0
- package/dist/composites/cdc-mirror.js +32 -0
- package/dist/composites/clickhouse.js +6 -0
- package/dist/composites/events-table.js +26 -0
- package/dist/composites/index.js +8 -0
- package/dist/composites/join-table.js +25 -0
- package/dist/composites/postgres.js +6 -0
- package/dist/composites/refreshed-view.js +24 -0
- package/dist/composites/replacing-table.js +28 -0
- package/dist/composites/rollup-view.js +29 -0
- package/dist/composites/sharded-table.js +27 -0
- package/dist/composites/soft-delete-table.js +34 -0
- package/dist/composites/tenant-table.js +26 -0
- package/dist/composites/typed.js +8 -0
- package/dist/config.js +206 -0
- package/dist/core/apply.js +93 -0
- package/dist/core/classifier.js +115 -0
- package/dist/core/container.js +69 -0
- package/dist/core/cursor.js +261 -0
- package/dist/core/diff.js +48 -0
- package/dist/core/entity.js +58 -0
- package/dist/core/find-templates.js +118 -0
- package/dist/core/foreign-tables.js +27 -0
- package/dist/core/handoff.js +32 -0
- package/dist/core/index.js +64 -0
- package/dist/core/interpolation.js +191 -0
- package/dist/core/normalize.js +43 -0
- package/dist/core/ownership.js +119 -0
- package/dist/core/receipts.js +22 -0
- package/dist/core/references.js +61 -0
- package/dist/core/statements.js +42 -0
- package/dist/core/template.js +79 -0
- package/dist/core/testing/http-stub.js +21 -0
- package/dist/core/tokens.js +202 -0
- package/dist/core/typed.js +40 -0
- package/dist/core/undeclarable.js +46 -0
- package/dist/coverage.js +161 -0
- package/dist/detect.js +32 -0
- package/dist/dialects.js +18 -0
- package/dist/files/build-root.js +72 -0
- package/dist/files/clickhouse.js +138 -0
- package/dist/files/common.js +58 -0
- package/dist/files/postgres.js +389 -0
- package/dist/generated/clickhouse-types.js +5 -0
- package/dist/generated/clickhouse.js +4361 -0
- package/dist/generated/postgres-types.js +227 -0
- package/dist/generated/postgres.js +3667 -0
- package/dist/import-generator.js +14 -0
- package/dist/import-parser.js +71 -0
- package/dist/index.js +24 -0
- package/dist/init-templates.js +290 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +31 -0
- package/dist/lint/audit-lineage.js +22 -0
- package/dist/lint/post-synth/audit-entries.js +34 -0
- package/dist/lint/post-synth/clickhouse-helpers.js +79 -0
- package/dist/lint/post-synth/clickhouse-type-check.js +162 -0
- package/dist/lint/post-synth/index.js +112 -0
- package/dist/lint/post-synth/postgres-audit-entries.js +35 -0
- package/dist/lint/post-synth/postgres-helpers.js +65 -0
- package/dist/lint/post-synth/postgres-names.js +507 -0
- package/dist/lint/post-synth/sql-helpers.js +23 -0
- package/dist/lint/post-synth/sql101.js +59 -0
- package/dist/lint/post-synth/sql102.js +34 -0
- package/dist/lint/post-synth/sqlch101.js +34 -0
- package/dist/lint/post-synth/sqlch102.js +23 -0
- package/dist/lint/post-synth/sqlch103.js +45 -0
- package/dist/lint/post-synth/sqlch104.js +41 -0
- package/dist/lint/post-synth/sqlch105.js +36 -0
- package/dist/lint/post-synth/sqlch106.js +27 -0
- package/dist/lint/post-synth/sqlch107.js +39 -0
- package/dist/lint/post-synth/sqlch108.js +28 -0
- package/dist/lint/post-synth/sqlch109.js +21 -0
- package/dist/lint/post-synth/sqlch110.js +31 -0
- package/dist/lint/post-synth/sqlch111.js +41 -0
- package/dist/lint/post-synth/sqlch112.js +28 -0
- package/dist/lint/post-synth/sqlch113.js +19 -0
- package/dist/lint/post-synth/sqlch114.js +26 -0
- package/dist/lint/post-synth/sqlch115.js +30 -0
- package/dist/lint/post-synth/sqlch116.js +22 -0
- package/dist/lint/post-synth/sqlch117.js +26 -0
- package/dist/lint/post-synth/sqlch118.js +27 -0
- package/dist/lint/post-synth/sqlch119.js +32 -0
- package/dist/lint/post-synth/sqlch120.js +20 -0
- package/dist/lint/post-synth/sqlch121.js +11 -0
- package/dist/lint/post-synth/sqlch122.js +11 -0
- package/dist/lint/post-synth/sqlch123.js +10 -0
- package/dist/lint/post-synth/sqlch124.js +21 -0
- package/dist/lint/post-synth/sqlch125.js +55 -0
- package/dist/lint/post-synth/sqlch126.js +62 -0
- package/dist/lint/post-synth/sqlch127.js +132 -0
- package/dist/lint/post-synth/sqlpg101.js +25 -0
- package/dist/lint/post-synth/sqlpg102.js +35 -0
- package/dist/lint/post-synth/sqlpg103.js +23 -0
- package/dist/lint/post-synth/sqlpg104.js +23 -0
- package/dist/lint/post-synth/sqlpg105.js +22 -0
- package/dist/lint/post-synth/sqlpg106.js +22 -0
- package/dist/lint/post-synth/sqlpg107.js +22 -0
- package/dist/lint/post-synth/sqlpg108.js +28 -0
- package/dist/lint/post-synth/sqlpg109.js +39 -0
- package/dist/lint/post-synth/sqlpg110.js +38 -0
- package/dist/lint/post-synth/sqlpg111.js +23 -0
- package/dist/lint/post-synth/sqlpg112.js +25 -0
- package/dist/lint/post-synth/sqlpg113.js +29 -0
- package/dist/lint/post-synth/sqlpg114.js +39 -0
- package/dist/lint/post-synth/sqlpg115.js +48 -0
- package/dist/lint/post-synth/sqlpg116.js +26 -0
- package/dist/lint/post-synth/sqlpg117.js +24 -0
- package/dist/lint/post-synth/sqlpg118.js +22 -0
- package/dist/lint/post-synth/sqlpg119.js +41 -0
- package/dist/lint/post-synth/sqlpg120.js +83 -0
- package/dist/lint/post-synth/sqlpg121.js +33 -0
- package/dist/lint/post-synth/sqlpg122.js +27 -0
- package/dist/lint/post-synth/sqlpg123.js +69 -0
- package/dist/lint/post-synth/sqlpg124.js +50 -0
- package/dist/lint/post-synth/sqlpg125.js +41 -0
- package/dist/lint/post-synth/sqlpg126.js +38 -0
- package/dist/lint/rules/index.js +12 -0
- package/dist/lint/rules/postgres-templates.js +27 -0
- package/dist/lint/rules/sqlch001.js +79 -0
- package/dist/lint/rules/sqlch002.js +124 -0
- package/dist/lint/rules/sqlch003.js +69 -0
- package/dist/lint/rules/sqlch004.js +83 -0
- package/dist/lint/rules/sqlpg001.js +68 -0
- package/dist/lint/rules/sqlpg002.js +69 -0
- package/dist/lint/rules/sqlpg003.js +58 -0
- package/dist/lint/rules/sqlpg004.js +77 -0
- package/dist/lint/rules/sqlpg005.js +86 -0
- package/dist/lint/rules/templates.js +25 -0
- package/dist/lint/rules/typed-names.js +132 -0
- package/dist/live-dialect.js +32 -0
- package/dist/lsp/catalog.js +44 -0
- package/dist/lsp/completions.js +136 -0
- package/dist/lsp/hover.js +115 -0
- package/dist/lsp/postgres-catalog.js +176 -0
- package/dist/lsp/postgres-template.js +171 -0
- package/dist/lsp/postgres.js +266 -0
- package/dist/lsp/refs.js +60 -0
- package/dist/lsp/registry.d.ts.map +1 -1
- package/dist/lsp/registry.js +19 -0
- package/dist/lsp/template.js +143 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +293 -0
- package/dist/mcp/postgres.js +117 -0
- package/dist/migration-statements.js +243 -0
- package/dist/op/activities/clickhouse-apply.js +83 -0
- package/dist/op/activities/clickhouse-emulator.js +53 -0
- package/dist/op/activities/clickhouse-rebuild.js +119 -0
- package/dist/op/activities/index.js +26 -0
- package/dist/op/activities/postgres-apply.js +126 -0
- package/dist/op/activities/postgres-emulator.js +70 -0
- package/dist/op/activities/postgres-migration.js +156 -0
- package/dist/op/activities/sql-exec.js +57 -0
- package/dist/op/activity-contracts.js +103 -0
- package/dist/package-cli.js +14 -0
- package/dist/plugin.js +259 -0
- package/dist/postgres/access/acl.js +344 -0
- package/dist/postgres/access/changes.js +30 -0
- package/dist/postgres/access/import.js +61 -0
- package/dist/postgres/access/live.js +96 -0
- package/dist/postgres/apply/apply.js +557 -0
- package/dist/postgres/apply/statements.js +882 -0
- package/dist/postgres/catalog-types.js +18 -0
- package/dist/postgres/container.js +58 -0
- package/dist/postgres/entities.js +1390 -0
- package/dist/postgres/entity-types.js +28 -0
- package/dist/postgres/import/generator.js +283 -0
- package/dist/postgres/import/ir.js +53 -0
- package/dist/postgres/import/live-export.js +83 -0
- package/dist/postgres/keywords.js +32 -0
- package/dist/postgres/live/bind.js +130 -0
- package/dist/postgres/live/catalog-kinds.js +301 -0
- package/dist/postgres/live/catalog.js +644 -0
- package/dist/postgres/live/client.js +151 -0
- package/dist/postgres/live/describe-resources.js +135 -0
- package/dist/postgres/live/undeclarable.js +111 -0
- package/dist/postgres/migrate/backfill.js +170 -0
- package/dist/postgres/migrate/batches.js +122 -0
- package/dist/postgres/migrate/carry.js +518 -0
- package/dist/postgres/migrate/handoff.js +59 -0
- package/dist/postgres/migrate/names.js +61 -0
- package/dist/postgres/migrate/observe.js +360 -0
- package/dist/postgres/migrate/op.js +178 -0
- package/dist/postgres/migrate/receipts.js +117 -0
- package/dist/postgres/migrate/replication.js +106 -0
- package/dist/postgres/migrate/steps.js +514 -0
- package/dist/postgres/overlays/columns.js +31 -0
- package/dist/postgres/overlays/constraints.js +52 -0
- package/dist/postgres/overlays/extension-objects.js +88 -0
- package/dist/postgres/overlays/indexes.js +25 -0
- package/dist/postgres/overlays/kinds.js +2 -0
- package/dist/postgres/overlays/partitioning.js +23 -0
- package/dist/postgres/overlays/relations.js +25 -0
- package/dist/postgres/overlays/storage.js +65 -0
- package/dist/postgres/overlays/type-ddl.js +36 -0
- package/dist/postgres/overlays/types.js +74 -0
- package/dist/postgres/parser.js +1824 -0
- package/dist/postgres/plan/commands.js +137 -0
- package/dist/postgres/plan/deep.js +324 -0
- package/dist/postgres/plan/diff.js +499 -0
- package/dist/postgres/plan/disruption.js +64 -0
- package/dist/postgres/plan/normalize.js +889 -0
- package/dist/postgres/plan/report.js +21 -0
- package/dist/postgres/plan/rules.js +115 -0
- package/dist/postgres/plan/schema.js +105 -0
- package/dist/postgres/plan/server-normalize.js +166 -0
- package/dist/postgres/providers/aurora.js +44 -0
- package/dist/postgres/providers/azure.js +53 -0
- package/dist/postgres/providers/cloud-sql.js +41 -0
- package/dist/postgres/providers/index.js +73 -0
- package/dist/postgres/providers/neon.js +44 -0
- package/dist/postgres/providers/rds.js +47 -0
- package/dist/postgres/providers/supabase.js +45 -0
- package/dist/postgres/providers/types.js +21 -0
- package/dist/postgres/row-types.js +107 -0
- package/dist/postgres/testing/fake-client.js +34 -0
- package/dist/postgres/testing/migration.js +48 -0
- package/dist/postgres/testing/roundtrip-schema.js +64 -0
- package/dist/postgres/testing/server.js +47 -0
- package/dist/postgres/testing/writable-server.js +160 -0
- package/dist/postgres/tokens.js +41 -0
- package/dist/postgres.js +15 -0
- package/dist/read/clickhouse.js +82 -0
- package/dist/read/objects.js +44 -0
- package/dist/read/postgres.js +104 -0
- package/dist/read.js +52 -0
- package/dist/receipts.js +207 -0
- package/dist/serializer.js +172 -0
- package/dist/skill-defs.js +93 -0
- package/dist/spec/catalog.js +191 -0
- package/dist/spec/fetch.d.ts.map +1 -1
- package/dist/spec/fetch.js +90 -0
- package/dist/spec/pin.js +42 -0
- package/dist/spec/postgres-catalog.js +171 -0
- package/dist/spec/postgres-fetch.js +91 -0
- package/dist/spec/postgres-pin.js +82 -0
- package/dist/sql-files.js +206 -0
- package/dist/sql-types-command.js +97 -0
- package/dist/sql-types.js +323 -0
- package/dist/token-source.js +156 -0
- package/dist/validate-cli.js +5 -0
- package/dist/validate.js +151 -0
- package/dist/workspace-outputs.js +191 -0
- package/package.json +7 -7
- package/src/codegen/package.ts +2 -2
- package/src/lint/rules/typescript-7.test.ts +2 -1
- package/src/lsp/catalog.ts +2 -2
- package/src/lsp/postgres-catalog.ts +2 -2
- package/src/lsp/registry.ts +2 -1
- package/src/mcp/index.ts +4 -2
- package/src/spec/fetch.ts +2 -2
- package/src/spec/postgres-fetch.ts +2 -2
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { parseCreate } from "../../clickhouse/parser.js";
|
|
2
|
+
import { parseAccess } from "../../clickhouse/access.js";
|
|
3
|
+
import { SqlSyntaxError } from "../../clickhouse/tokens.js";
|
|
4
|
+
import { findTemplates, templatePosition, tokensOf } from "./templates.js";
|
|
5
|
+
const ACCESS_TAGS = new Set(["user", "role", "policy", "quota", "settingsProfile", "grant"]);
|
|
6
|
+
const ACCESS_STATEMENT = {
|
|
7
|
+
user: "CREATE USER",
|
|
8
|
+
role: "CREATE ROLE",
|
|
9
|
+
rowPolicy: "CREATE ROW POLICY",
|
|
10
|
+
quota: "CREATE QUOTA",
|
|
11
|
+
settingsProfile: "CREATE SETTINGS PROFILE",
|
|
12
|
+
grant: "GRANT",
|
|
13
|
+
};
|
|
14
|
+
const STATEMENT = { database: "CREATE DATABASE", table: "CREATE TABLE", view: "CREATE VIEW", dictionary: "CREATE DICTIONARY", function: "CREATE FUNCTION" };
|
|
15
|
+
/** The tag a statement belongs in: `func` for a function, the statement's own name otherwise. */
|
|
16
|
+
const tagOf = (statement) => (statement === "function" ? "func" : statement);
|
|
17
|
+
/**
|
|
18
|
+
* 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.
|
|
19
|
+
*
|
|
20
|
+
* The same parse runs when the build calls the tag, and fails the build there.
|
|
21
|
+
* This rule reports it in the editor and in `chant lint`, at the token, before
|
|
22
|
+
* anything is built. Interpolations are references here, since lint cannot
|
|
23
|
+
* know their values, so an error inside text a string interpolation supplies
|
|
24
|
+
* is left to the build.
|
|
25
|
+
*/
|
|
26
|
+
export const sqlch001 = {
|
|
27
|
+
id: "SQLCH001",
|
|
28
|
+
severity: "error",
|
|
29
|
+
category: "correctness",
|
|
30
|
+
description: "ClickHouse DDL in a database, table or view template does not parse",
|
|
31
|
+
check(context) {
|
|
32
|
+
const source = context.sourceFile;
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const found of findTemplates(source)) {
|
|
35
|
+
let tokens;
|
|
36
|
+
try {
|
|
37
|
+
tokens = tokensOf(found);
|
|
38
|
+
if (ACCESS_TAGS.has(found.tag)) {
|
|
39
|
+
const node = parseAccess(tokens);
|
|
40
|
+
const tag = node.statement === "rowPolicy" ? "policy" : node.statement;
|
|
41
|
+
if (tag !== found.tag || node.revoke) {
|
|
42
|
+
out.push({
|
|
43
|
+
ruleId: "SQLCH001",
|
|
44
|
+
severity: "error",
|
|
45
|
+
message: node.revoke
|
|
46
|
+
? `${found.tag}\`...\` holds a REVOKE; declare the grants a grantee keeps, and a plan revokes the rest`
|
|
47
|
+
: `${found.tag}\`...\` holds a ${ACCESS_STATEMENT[node.statement]}; use the ${tag} tag`,
|
|
48
|
+
file: context.filePath,
|
|
49
|
+
...templatePosition(source, found, 0, 0),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const node = parseCreate(tokens);
|
|
55
|
+
if (tagOf(node.statement) !== found.tag) {
|
|
56
|
+
out.push({
|
|
57
|
+
ruleId: "SQLCH001",
|
|
58
|
+
severity: "error",
|
|
59
|
+
message: `${found.tag}\`...\` holds a ${STATEMENT[node.statement]}; use the ${tagOf(node.statement)} tag`,
|
|
60
|
+
file: context.filePath,
|
|
61
|
+
...templatePosition(source, found, 0, 0),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (!(err instanceof SqlSyntaxError))
|
|
67
|
+
throw err;
|
|
68
|
+
out.push({
|
|
69
|
+
ruleId: "SQLCH001",
|
|
70
|
+
severity: "error",
|
|
71
|
+
message: `ClickHouse DDL does not parse: ${err.message}`,
|
|
72
|
+
file: context.filePath,
|
|
73
|
+
...templatePosition(source, found, err.part, err.offset),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { parseCreate, unquote } from "../../clickhouse/parser.js";
|
|
2
|
+
import { isTrivia, SqlSyntaxError } from "../../clickhouse/tokens.js";
|
|
3
|
+
import { findTemplates, tokenPosition, tokensOf } from "./templates.js";
|
|
4
|
+
/** The column's type, as written, reads `Nullable(...)`, also inside `LowCardinality(...)`. */
|
|
5
|
+
function isNullable(tokens, c) {
|
|
6
|
+
if (c.nullable === true)
|
|
7
|
+
return true;
|
|
8
|
+
if (!c.type)
|
|
9
|
+
return false;
|
|
10
|
+
const words = tokens.slice(c.type.from, c.type.to).filter((t) => t.kind === "ident").map((t) => t.text.toLowerCase());
|
|
11
|
+
return words[0] === "nullable" || (words[0] === "lowcardinality" && words[1] === "nullable");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The column names a key names as a key part: bare, or inside the key's tuple
|
|
15
|
+
* parentheses, not an argument of a function (`ifNull(a, '')` makes a
|
|
16
|
+
* non-Nullable key part of a Nullable column) and not a qualified part.
|
|
17
|
+
*/
|
|
18
|
+
function keyColumns(tokens, span) {
|
|
19
|
+
const sig = tokens.slice(span.from, span.to).filter((t) => !isTrivia(t));
|
|
20
|
+
const out = [];
|
|
21
|
+
const frames = []; // true for a function call's parentheses
|
|
22
|
+
sig.forEach((t, i) => {
|
|
23
|
+
const prev = sig[i - 1];
|
|
24
|
+
if (t.kind === "punct" && t.text === "(") {
|
|
25
|
+
frames.push(prev !== undefined && (prev.kind === "ident" || prev.kind === "qident"));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (t.kind === "punct" && t.text === ")") {
|
|
29
|
+
frames.pop();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (t.kind !== "ident" && t.kind !== "qident")
|
|
33
|
+
return;
|
|
34
|
+
if (frames.includes(true))
|
|
35
|
+
return;
|
|
36
|
+
const next = sig[i + 1];
|
|
37
|
+
if (next?.kind === "punct" && next.text === "(")
|
|
38
|
+
return;
|
|
39
|
+
if (prev?.kind === "punct" && prev.text === ".")
|
|
40
|
+
return;
|
|
41
|
+
out.push({ name: unquote(t.text), token: t });
|
|
42
|
+
});
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
function allowsNullableKey(tokens, node) {
|
|
46
|
+
if (node.statement !== "table" && node.statement !== "view")
|
|
47
|
+
return false;
|
|
48
|
+
if (!node.settings)
|
|
49
|
+
return false;
|
|
50
|
+
const s = node.settings.find((x) => x.key === "allow_nullable_key");
|
|
51
|
+
if (!s)
|
|
52
|
+
return false;
|
|
53
|
+
const v = tokens.slice(s.value.from, s.value.to).map((t) => t.text).join("").trim().toLowerCase().replace(/'/g, "");
|
|
54
|
+
return v === "1" || v === "true";
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* SQLCH002: a `Nullable` column in a table's sort key or primary key.
|
|
58
|
+
*
|
|
59
|
+
* ClickHouse refuses to create the table ("Sorting key contains nullable
|
|
60
|
+
* columns") unless the table sets `allow_nullable_key = 1`, and with it a
|
|
61
|
+
* `NULL` sorts as a value of its own. The rule reads the types written in the
|
|
62
|
+
* column list; a key over a column of an inferred type is left to the server.
|
|
63
|
+
*/
|
|
64
|
+
export const sqlch002 = {
|
|
65
|
+
id: "SQLCH002",
|
|
66
|
+
severity: "error",
|
|
67
|
+
category: "correctness",
|
|
68
|
+
description: "A Nullable column in a ClickHouse sort key or primary key",
|
|
69
|
+
check(context) {
|
|
70
|
+
const source = context.sourceFile;
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const found of findTemplates(source)) {
|
|
73
|
+
const tokens = tokensOf(found);
|
|
74
|
+
let node;
|
|
75
|
+
try {
|
|
76
|
+
node = parseCreate(tokens);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (err instanceof SqlSyntaxError)
|
|
80
|
+
continue; // SQLCH001 reports it
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
if ((node.statement !== "table" && node.statement !== "view") || allowsNullableKey(tokens, node))
|
|
84
|
+
continue;
|
|
85
|
+
const nullable = new Set(node.columns.filter((c) => c.name && isNullable(tokens, c)).map((c) => c.name));
|
|
86
|
+
if (nullable.size === 0)
|
|
87
|
+
continue;
|
|
88
|
+
const reported = new Set();
|
|
89
|
+
for (const [clause, span] of [
|
|
90
|
+
["sort key", node.orderBy],
|
|
91
|
+
["primary key", node.primaryKey],
|
|
92
|
+
]) {
|
|
93
|
+
if (!span)
|
|
94
|
+
continue;
|
|
95
|
+
for (const { name, token } of keyColumns(tokens, span)) {
|
|
96
|
+
if (!nullable.has(name) || reported.has(`${clause}:${name}`))
|
|
97
|
+
continue;
|
|
98
|
+
reported.add(`${clause}:${name}`);
|
|
99
|
+
out.push({
|
|
100
|
+
ruleId: "SQLCH002",
|
|
101
|
+
severity: "error",
|
|
102
|
+
message: `column "${name}" is Nullable and is in the ${clause}; ClickHouse refuses this unless the table ` +
|
|
103
|
+
"sets allow_nullable_key = 1. Make the column non-Nullable (a default such as '' or 0 in place of NULL), " +
|
|
104
|
+
"or drop it from the key",
|
|
105
|
+
file: context.filePath,
|
|
106
|
+
...tokenPosition(source, found, token),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const c of node.columns) {
|
|
111
|
+
if (c.primaryKey && nullable.has(c.name)) {
|
|
112
|
+
out.push({
|
|
113
|
+
ruleId: "SQLCH002",
|
|
114
|
+
severity: "error",
|
|
115
|
+
message: `column "${c.name}" is Nullable and is declared PRIMARY KEY; ClickHouse refuses this unless the table sets allow_nullable_key = 1`,
|
|
116
|
+
file: context.filePath,
|
|
117
|
+
...tokenPosition(source, found, tokens[c.nameSpan.from]),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
2
|
+
import { findTemplates } from "./templates.js";
|
|
3
|
+
/** The entity's own members: an interpolation of one of them is never a column. */
|
|
4
|
+
const OWN_MEMBERS = new Set(["kind", "lexicon", "entityType", "props", "sqlName", "dependsOn"]);
|
|
5
|
+
/**
|
|
6
|
+
* Identifiers that hold a ClickHouse entity, as far as one file can tell:
|
|
7
|
+
* bound to a sql tag here, or imported from another file of the project.
|
|
8
|
+
*/
|
|
9
|
+
function entityBindings(source, tagged) {
|
|
10
|
+
const out = new Set();
|
|
11
|
+
for (const stmt of source.statements) {
|
|
12
|
+
if (ts.isImportDeclaration(stmt) && ts.isStringLiteral(stmt.moduleSpecifier) && stmt.moduleSpecifier.text.startsWith(".")) {
|
|
13
|
+
const named = stmt.importClause?.namedBindings;
|
|
14
|
+
if (named && ts.isNamedImports(named))
|
|
15
|
+
for (const el of named.elements)
|
|
16
|
+
out.add(el.name.text);
|
|
17
|
+
}
|
|
18
|
+
if (ts.isVariableStatement(stmt)) {
|
|
19
|
+
for (const d of stmt.declarationList.declarations) {
|
|
20
|
+
if (ts.isIdentifier(d.name) && d.initializer && tagged.has(d.initializer))
|
|
21
|
+
out.add(d.name.text);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* SQLCH003: a column written as `${events.kind}` instead of `${events.columns.kind}`.
|
|
29
|
+
*
|
|
30
|
+
* Columns are reached through `.columns`. `kind`, `lexicon`, `entityType`,
|
|
31
|
+
* `props`, `sqlName` and `dependsOn` are the entity's own fields, so
|
|
32
|
+
* `${events.kind}` is the string `"resource"`, which the tag splices in as SQL
|
|
33
|
+
* text: the statement builds and means something else. This rule flags it for
|
|
34
|
+
* an entity the file declares or imports from another project file.
|
|
35
|
+
*/
|
|
36
|
+
export const sqlch003 = {
|
|
37
|
+
id: "SQLCH003",
|
|
38
|
+
severity: "error",
|
|
39
|
+
category: "correctness",
|
|
40
|
+
description: "A column interpolated without .columns reads the entity's own field instead",
|
|
41
|
+
check(context) {
|
|
42
|
+
const source = context.sourceFile;
|
|
43
|
+
const found = findTemplates(source);
|
|
44
|
+
if (found.length === 0)
|
|
45
|
+
return [];
|
|
46
|
+
const entities = entityBindings(source, new Set(found.map((f) => f.node)));
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const f of found) {
|
|
49
|
+
for (const expr of f.expressions) {
|
|
50
|
+
if (!ts.isPropertyAccessExpression(expr) || !ts.isIdentifier(expr.expression))
|
|
51
|
+
continue;
|
|
52
|
+
const owner = expr.expression.text;
|
|
53
|
+
const member = expr.name.text;
|
|
54
|
+
if (!entities.has(owner) || !OWN_MEMBERS.has(member))
|
|
55
|
+
continue;
|
|
56
|
+
const { line, character } = source.getLineAndCharacterOfPosition(expr.getStart(source));
|
|
57
|
+
out.push({
|
|
58
|
+
ruleId: "SQLCH003",
|
|
59
|
+
severity: "error",
|
|
60
|
+
message: `\${${owner}.${member}} is the entity's own "${member}" field, not a column; write \${${owner}.columns.${member}}`,
|
|
61
|
+
file: context.filePath,
|
|
62
|
+
line: line + 1,
|
|
63
|
+
column: character + 1,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { parseCreate, unquote } from "../../clickhouse/parser.js";
|
|
2
|
+
import { isTrivia, SqlSyntaxError } from "../../clickhouse/tokens.js";
|
|
3
|
+
import { findTemplates, tokensOf } from "./templates.js";
|
|
4
|
+
import { declaredText, generatedNamesFor, nameAgrees, normalizedName, SQL_TYPES_FILE, typeArgumentName } from "./typed-names.js";
|
|
5
|
+
const MODULE = "@intentius/chant-lexicon-sql/clickhouse";
|
|
6
|
+
/**
|
|
7
|
+
* SQLCH004: a typed ClickHouse tag whose type argument does not name the
|
|
8
|
+
* object its template declares, or is not in the generated `sql-types.d.ts`.
|
|
9
|
+
*
|
|
10
|
+
* `table<"shop.events">\`CREATE TABLE shop.events ...\``, and the same for
|
|
11
|
+
* `view` and `dictionary`, types `.columns` from the generated file's
|
|
12
|
+
* `SqlSchema["shop.events"]`. tsc does not compare the argument with the
|
|
13
|
+
* template's text, and an argument the file does not list falls back to
|
|
14
|
+
* unchecked columns, so a typo checks nothing and says nothing. The rule reads
|
|
15
|
+
* the declared name from the template (a database supplied by an interpolation
|
|
16
|
+
* is not known to lint, and is left out of the comparison) and the listed
|
|
17
|
+
* names from the nearest `sql-types.d.ts` above the file. With no generated
|
|
18
|
+
* file, only the first check runs. A name missing from the file is a warning,
|
|
19
|
+
* since an object added since the last `chant sql types` is stale and
|
|
20
|
+
* `chant sql types --check` is what fails on it.
|
|
21
|
+
*/
|
|
22
|
+
export const sqlch004 = {
|
|
23
|
+
id: "SQLCH004",
|
|
24
|
+
severity: "error",
|
|
25
|
+
category: "correctness",
|
|
26
|
+
description: "A ClickHouse tag's type argument does not name the object its template declares, or is not in sql-types.d.ts",
|
|
27
|
+
check(context) {
|
|
28
|
+
const source = context.sourceFile;
|
|
29
|
+
const out = [];
|
|
30
|
+
let listed = null;
|
|
31
|
+
for (const found of findTemplates(source)) {
|
|
32
|
+
if (found.tag !== "table" && found.tag !== "view" && found.tag !== "dictionary")
|
|
33
|
+
continue;
|
|
34
|
+
const arg = typeArgumentName(found.node);
|
|
35
|
+
if (!arg)
|
|
36
|
+
continue;
|
|
37
|
+
const at = (() => {
|
|
38
|
+
const { line, character } = source.getLineAndCharacterOfPosition(arg.node.getStart(source));
|
|
39
|
+
return { line: line + 1, column: character + 1 };
|
|
40
|
+
})();
|
|
41
|
+
let declared;
|
|
42
|
+
try {
|
|
43
|
+
const tokens = tokensOf(found);
|
|
44
|
+
const node = parseCreate(tokens);
|
|
45
|
+
if (node.statement === found.tag) {
|
|
46
|
+
declared = tokens
|
|
47
|
+
.slice(node.name.from, node.name.to)
|
|
48
|
+
.filter((t) => !isTrivia(t) && !(t.kind === "punct" && t.text === "."))
|
|
49
|
+
.map((t) => (t.kind === "ref" ? "" : unquote(t.text)));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (err instanceof SqlSyntaxError)
|
|
54
|
+
continue; // SQLCH001 reports it
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
if (!declared)
|
|
58
|
+
continue; // SQLCH001: the tag holds another statement
|
|
59
|
+
if (!nameAgrees(declared, arg.name)) {
|
|
60
|
+
out.push({
|
|
61
|
+
ruleId: "SQLCH004",
|
|
62
|
+
severity: "error",
|
|
63
|
+
message: `${found.tag}<"${arg.name}"> names another object than the template declares (${declaredText(declared)}); the type argument is the qualified name the build renders`,
|
|
64
|
+
file: context.filePath,
|
|
65
|
+
...at,
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (listed === null)
|
|
70
|
+
listed = generatedNamesFor(context.filePath, MODULE);
|
|
71
|
+
if (listed && !listed.has(normalizedName(arg.name))) {
|
|
72
|
+
out.push({
|
|
73
|
+
ruleId: "SQLCH004",
|
|
74
|
+
severity: "warning",
|
|
75
|
+
message: `"${arg.name}" is not in ${SQL_TYPES_FILE}, so its columns are unchecked; run \`chant sql types\` to add it`,
|
|
76
|
+
file: context.filePath,
|
|
77
|
+
...at,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { parseStatements, SqlSyntaxError } from "../../postgres/parser.js";
|
|
2
|
+
import { followOnStray, STATEMENT_NAMES, STATEMENTS_OF } from "../../postgres/entities.js";
|
|
3
|
+
import { findPostgresTemplates, postgresTokensOf, templatePosition, tokenPosition } from "./postgres-templates.js";
|
|
4
|
+
/** The tag that declares each statement. */
|
|
5
|
+
const TAG_OF = Object.fromEntries(Object.entries(STATEMENTS_OF).flatMap(([tag, statements]) => statements.map((statement) => [statement, tag])));
|
|
6
|
+
/**
|
|
7
|
+
* SQLPG001: the DDL in a Postgres template does not parse, or does not hold
|
|
8
|
+
* what its tag declares.
|
|
9
|
+
*
|
|
10
|
+
* The same parse runs when the build calls the tag, and fails the build there.
|
|
11
|
+
* This rule reports it in the editor and in `chant lint`, at the token, as a
|
|
12
|
+
* line and column of the `.ts` file, before anything is built. It also reports
|
|
13
|
+
* what the tag refuses after parsing: another statement than the tag's, a
|
|
14
|
+
* second CREATE in one template, and an index with no name (its name is its
|
|
15
|
+
* identity on the server). Interpolations are references here, since lint
|
|
16
|
+
* cannot know their values, so an error inside text a string interpolation
|
|
17
|
+
* supplies is left to the build.
|
|
18
|
+
*/
|
|
19
|
+
export const sqlpg001 = {
|
|
20
|
+
id: "SQLPG001",
|
|
21
|
+
severity: "error",
|
|
22
|
+
category: "correctness",
|
|
23
|
+
description: "Postgres DDL in a tagged template does not parse, or holds another statement than its tag",
|
|
24
|
+
check(context) {
|
|
25
|
+
const source = context.sourceFile;
|
|
26
|
+
const out = [];
|
|
27
|
+
const report = (message, at) => out.push({ ruleId: "SQLPG001", severity: "error", message, file: context.filePath, ...at });
|
|
28
|
+
for (const found of findPostgresTemplates(source)) {
|
|
29
|
+
const tokens = (() => {
|
|
30
|
+
try {
|
|
31
|
+
return postgresTokensOf(found);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
if (!(err instanceof SqlSyntaxError))
|
|
35
|
+
throw err;
|
|
36
|
+
report(`Postgres DDL does not parse: ${err.message}`, templatePosition(source, found, err.part, err.offset));
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
40
|
+
if (!tokens)
|
|
41
|
+
continue;
|
|
42
|
+
try {
|
|
43
|
+
const [node, ...rest] = parseStatements(tokens);
|
|
44
|
+
if (!STATEMENTS_OF[found.tag].includes(node.statement)) {
|
|
45
|
+
const use = TAG_OF[node.statement];
|
|
46
|
+
report(use
|
|
47
|
+
? `${found.tag}\`...\` holds a ${STATEMENT_NAMES[node.statement]}; use the ${use} tag`
|
|
48
|
+
: `${found.tag}\`...\` starts with a ${STATEMENT_NAMES[node.statement]}; it goes after the CREATE it belongs to`, templatePosition(source, found, 0, 0));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const stray = followOnStray(found.tag, rest);
|
|
52
|
+
if (stray) {
|
|
53
|
+
report(`${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" : ""}`, templatePosition(source, found, 0, 0));
|
|
54
|
+
}
|
|
55
|
+
if (node.statement === "index" && !node.name) {
|
|
56
|
+
const on = tokens.find((t) => t.kind === "ident" && t.text.toUpperCase() === "ON");
|
|
57
|
+
report("an index needs a name: the name is its identity on the server, and an unnamed index gets a generated one", on ? tokenPosition(source, found, on) : templatePosition(source, found, 0, 0));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
if (!(err instanceof SqlSyntaxError))
|
|
62
|
+
throw err;
|
|
63
|
+
report(`Postgres DDL does not parse: ${err.message}`, err.token ? tokenPosition(source, found, err.token) : templatePosition(source, found, err.part, err.offset));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
2
|
+
import { findPostgresTemplates as findTemplates } from "./postgres-templates.js";
|
|
3
|
+
/** The entity's own members: an interpolation of one of them is never a column. */
|
|
4
|
+
const OWN_MEMBERS = new Set(["kind", "lexicon", "entityType", "props", "sqlName", "dependsOn"]);
|
|
5
|
+
/**
|
|
6
|
+
* Identifiers that hold a Postgres entity, as far as one file can tell:
|
|
7
|
+
* bound to a sql tag here, or imported from another file of the project.
|
|
8
|
+
*/
|
|
9
|
+
function entityBindings(source, tagged) {
|
|
10
|
+
const out = new Set();
|
|
11
|
+
for (const stmt of source.statements) {
|
|
12
|
+
if (ts.isImportDeclaration(stmt) && ts.isStringLiteral(stmt.moduleSpecifier) && stmt.moduleSpecifier.text.startsWith(".")) {
|
|
13
|
+
const named = stmt.importClause?.namedBindings;
|
|
14
|
+
if (named && ts.isNamedImports(named))
|
|
15
|
+
for (const el of named.elements)
|
|
16
|
+
out.add(el.name.text);
|
|
17
|
+
}
|
|
18
|
+
if (ts.isVariableStatement(stmt)) {
|
|
19
|
+
for (const d of stmt.declarationList.declarations) {
|
|
20
|
+
if (ts.isIdentifier(d.name) && d.initializer && tagged.has(d.initializer))
|
|
21
|
+
out.add(d.name.text);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* SQLPG002: a column written as `${events.kind}` instead of `${events.columns.kind}`.
|
|
29
|
+
*
|
|
30
|
+
* Columns are reached through `.columns`. `kind`, `lexicon`, `entityType`,
|
|
31
|
+
* `props`, `sqlName` and `dependsOn` are the entity's own fields, so
|
|
32
|
+
* `${events.kind}` is the string `"resource"`, which the tag splices in as SQL
|
|
33
|
+
* text: the statement builds and means something else. This rule flags it for
|
|
34
|
+
* an entity the file declares or imports from another project file.
|
|
35
|
+
*/
|
|
36
|
+
export const sqlpg002 = {
|
|
37
|
+
id: "SQLPG002",
|
|
38
|
+
severity: "error",
|
|
39
|
+
category: "correctness",
|
|
40
|
+
description: "A column interpolated without .columns reads the entity's own field instead",
|
|
41
|
+
check(context) {
|
|
42
|
+
const source = context.sourceFile;
|
|
43
|
+
const found = findTemplates(source);
|
|
44
|
+
if (found.length === 0)
|
|
45
|
+
return [];
|
|
46
|
+
const entities = entityBindings(source, new Set(found.map((f) => f.node)));
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const f of found) {
|
|
49
|
+
for (const expr of f.expressions) {
|
|
50
|
+
if (!ts.isPropertyAccessExpression(expr) || !ts.isIdentifier(expr.expression))
|
|
51
|
+
continue;
|
|
52
|
+
const owner = expr.expression.text;
|
|
53
|
+
const member = expr.name.text;
|
|
54
|
+
if (!entities.has(owner) || !OWN_MEMBERS.has(member))
|
|
55
|
+
continue;
|
|
56
|
+
const { line, character } = source.getLineAndCharacterOfPosition(expr.getStart(source));
|
|
57
|
+
out.push({
|
|
58
|
+
ruleId: "SQLPG002",
|
|
59
|
+
severity: "error",
|
|
60
|
+
message: `\${${owner}.${member}} is the entity's own "${member}" field, not a column; write \${${owner}.columns.${member}}`,
|
|
61
|
+
file: context.filePath,
|
|
62
|
+
line: line + 1,
|
|
63
|
+
column: character + 1,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isTrivia, SqlSyntaxError } from "../../postgres/tokens.js";
|
|
2
|
+
import { findPostgresTemplates, postgresTokensOf, tokenPosition } from "./postgres-templates.js";
|
|
3
|
+
const REGCLASS_FUNCTIONS = ["NEXTVAL", "CURRVAL", "SETVAL"];
|
|
4
|
+
/**
|
|
5
|
+
* SQLPG003: an object named in a string where Postgres reads a `regclass`:
|
|
6
|
+
* `nextval('app.ticket_seq')`, `currval(...)`, `setval(...)`, or
|
|
7
|
+
* `'app.ticket_seq'::regclass`.
|
|
8
|
+
*
|
|
9
|
+
* A name inside a string is text to the build, so it records no reference:
|
|
10
|
+
* the table does not depend on the sequence, the creation order can put the
|
|
11
|
+
* table first, and the create fails. Interpolating the object instead,
|
|
12
|
+
* `nextval(${ticketSeq})`, renders the same `regclass` literal the catalog
|
|
13
|
+
* prints (`nextval('app.ticket_seq'::regclass)`) and keeps the edge. A
|
|
14
|
+
* sequence another tool owns is the exception, which is why this is a
|
|
15
|
+
* warning.
|
|
16
|
+
*/
|
|
17
|
+
export const sqlpg003 = {
|
|
18
|
+
id: "SQLPG003",
|
|
19
|
+
severity: "warning",
|
|
20
|
+
category: "correctness",
|
|
21
|
+
description: "A sequence or relation named in a regclass string makes no reference; interpolate the object",
|
|
22
|
+
check(context) {
|
|
23
|
+
const source = context.sourceFile;
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const found of findPostgresTemplates(source)) {
|
|
26
|
+
let tokens;
|
|
27
|
+
try {
|
|
28
|
+
tokens = postgresTokensOf(found);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
if (err instanceof SqlSyntaxError)
|
|
32
|
+
continue;
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
const sig = tokens.filter((t) => !isTrivia(t));
|
|
36
|
+
sig.forEach((t, k) => {
|
|
37
|
+
if (t.kind !== "string" || !t.text.startsWith("'"))
|
|
38
|
+
return;
|
|
39
|
+
const prev = sig[k - 1];
|
|
40
|
+
const fn = sig[k - 2];
|
|
41
|
+
const inCall = prev?.kind === "punct" && prev.text === "(" && fn?.kind === "ident" && REGCLASS_FUNCTIONS.includes(fn.text.toUpperCase());
|
|
42
|
+
const cast = sig[k + 1]?.kind === "op" && sig[k + 1].text === "::" && sig[k + 2]?.kind === "ident" && sig[k + 2].text.toLowerCase() === "regclass";
|
|
43
|
+
if (!inCall && !cast)
|
|
44
|
+
return;
|
|
45
|
+
const named = t.text.slice(1, -1).replace(/''/g, "'");
|
|
46
|
+
const how = inCall ? `${fn.text}(\${...})` : "${...}::regclass";
|
|
47
|
+
out.push({
|
|
48
|
+
ruleId: "SQLPG003",
|
|
49
|
+
severity: "warning",
|
|
50
|
+
message: `'${named}' names an object in a string, which records no reference, so the build can create this object before it; interpolate the declared object instead: ${how}`,
|
|
51
|
+
file: context.filePath,
|
|
52
|
+
...tokenPosition(source, found, t),
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { isTrivia, SqlSyntaxError } from "../../postgres/tokens.js";
|
|
2
|
+
import { isPostgresProvider, providerAllowsExtension, providerData } from "../../postgres/providers/index.js";
|
|
3
|
+
import { findPostgresTemplates, postgresTokensOf, tokenPosition } from "./postgres-templates.js";
|
|
4
|
+
/**
|
|
5
|
+
* The provider a lint run is for: `sql.provider`, else the one provider every
|
|
6
|
+
* profile that names one agrees on. No answer (self-hosted, or profiles that
|
|
7
|
+
* disagree) leaves the rule silent. The lint engine passes the whole project
|
|
8
|
+
* config as `projectConfig`.
|
|
9
|
+
*/
|
|
10
|
+
export function providerFromConfig(config) {
|
|
11
|
+
const sql = config?.sql;
|
|
12
|
+
if (!sql)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (isPostgresProvider(sql.provider))
|
|
15
|
+
return sql.provider;
|
|
16
|
+
const named = new Set(Object.values(sql.profiles ?? {}).map((p) => p.provider).filter(isPostgresProvider));
|
|
17
|
+
return named.size === 1 ? [...named][0] : undefined;
|
|
18
|
+
}
|
|
19
|
+
const unquote = (t) => (t.kind === "qident" ? t.text.slice(1, -1).replace(/""/g, '"') : t.text.toLowerCase());
|
|
20
|
+
/**
|
|
21
|
+
* SQLPG004: a declared extension the configured provider does not allow.
|
|
22
|
+
*
|
|
23
|
+
* `extension\`CREATE EXTENSION pg_stat_monitor\`` builds, and fails when
|
|
24
|
+
* applied to a provider whose list lacks it. The lists are the providers'
|
|
25
|
+
* documentation snapshots (see postgres/providers/types.ts), so a provider
|
|
26
|
+
* that added an extension since reports it here until the list is refreshed;
|
|
27
|
+
* disable the rule for that line. When a provider's list is partial, a name
|
|
28
|
+
* outside it is not reported.
|
|
29
|
+
*/
|
|
30
|
+
export const sqlpg004 = {
|
|
31
|
+
id: "SQLPG004",
|
|
32
|
+
severity: "error",
|
|
33
|
+
category: "correctness",
|
|
34
|
+
description: "A declared extension the configured Postgres provider does not allow",
|
|
35
|
+
check(context) {
|
|
36
|
+
const provider = providerFromConfig(context.projectConfig);
|
|
37
|
+
if (!provider)
|
|
38
|
+
return [];
|
|
39
|
+
const label = providerData(provider).label;
|
|
40
|
+
const source = context.sourceFile;
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const found of findPostgresTemplates(source)) {
|
|
43
|
+
if (found.tag !== "extension")
|
|
44
|
+
continue;
|
|
45
|
+
let tokens;
|
|
46
|
+
try {
|
|
47
|
+
tokens = postgresTokensOf(found);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
if (err instanceof SqlSyntaxError)
|
|
51
|
+
continue;
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
const sig = tokens.filter((t) => !isTrivia(t));
|
|
55
|
+
const word = (k) => (sig[k]?.kind === "ident" ? sig[k].text.toUpperCase() : "");
|
|
56
|
+
if (word(0) !== "CREATE" || word(1) !== "EXTENSION")
|
|
57
|
+
continue;
|
|
58
|
+
let k = 2;
|
|
59
|
+
if (word(k) === "IF" && word(k + 1) === "NOT" && word(k + 2) === "EXISTS")
|
|
60
|
+
k += 3;
|
|
61
|
+
const nameToken = sig[k];
|
|
62
|
+
if (!nameToken || (nameToken.kind !== "ident" && nameToken.kind !== "qident"))
|
|
63
|
+
continue;
|
|
64
|
+
const name = unquote(nameToken);
|
|
65
|
+
if (providerAllowsExtension(provider, name) !== false)
|
|
66
|
+
continue;
|
|
67
|
+
out.push({
|
|
68
|
+
ruleId: "SQLPG004",
|
|
69
|
+
severity: "error",
|
|
70
|
+
message: `${label} does not allow the extension "${name}"; CREATE EXTENSION would fail there. Use an extension it lists, or change sql.provider`,
|
|
71
|
+
file: context.filePath,
|
|
72
|
+
...tokenPosition(source, found, nameToken),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
},
|
|
77
|
+
};
|