@intentius/chant-lexicon-sql 0.121.1 → 0.122.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clickhouse/catalog-types.d.ts +8 -0
- package/dist/clickhouse/catalog-types.d.ts.map +1 -1
- package/dist/clickhouse/overlays/codecs.d.ts.map +1 -1
- package/dist/clickhouse/overlays/kinds.d.ts +1 -1
- package/dist/clickhouse/overlays/kinds.d.ts.map +1 -1
- package/dist/clickhouse/overlays/types.d.ts.map +1 -1
- package/dist/clickhouse/type-syntax.d.ts +33 -0
- package/dist/clickhouse/type-syntax.d.ts.map +1 -0
- package/dist/codegen/clickhouse-module.d.ts.map +1 -1
- package/dist/codegen/postgres-module.d.ts.map +1 -1
- package/dist/generated/clickhouse.d.ts +7 -1
- package/dist/generated/clickhouse.d.ts.map +1 -1
- package/dist/generated/postgres.d.ts +6 -0
- package/dist/generated/postgres.d.ts.map +1 -1
- package/dist/integrity.json +23 -5
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/audit-entries.d.ts.map +1 -1
- package/dist/lint/post-synth/clickhouse-type-check.d.ts +37 -0
- package/dist/lint/post-synth/clickhouse-type-check.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/postgres-audit-entries.d.ts.map +1 -1
- package/dist/lint/post-synth/postgres-names.d.ts +149 -0
- package/dist/lint/post-synth/postgres-names.d.ts.map +1 -0
- package/dist/lint/post-synth/sql101.d.ts +9 -0
- package/dist/lint/post-synth/sql101.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlch121.d.ts +10 -0
- package/dist/lint/post-synth/sqlch121.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlch122.d.ts +10 -0
- package/dist/lint/post-synth/sqlch122.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlch123.d.ts +9 -0
- package/dist/lint/post-synth/sqlch123.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlch124.d.ts +6 -0
- package/dist/lint/post-synth/sqlch124.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlch125.d.ts +11 -0
- package/dist/lint/post-synth/sqlch125.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlch126.d.ts +12 -0
- package/dist/lint/post-synth/sqlch126.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlch127.d.ts +22 -0
- package/dist/lint/post-synth/sqlch127.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlpg119.d.ts +18 -0
- package/dist/lint/post-synth/sqlpg119.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlpg120.d.ts +17 -0
- package/dist/lint/post-synth/sqlpg120.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlpg121.d.ts +12 -0
- package/dist/lint/post-synth/sqlpg121.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlpg122.d.ts +12 -0
- package/dist/lint/post-synth/sqlpg122.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlpg123.d.ts +17 -0
- package/dist/lint/post-synth/sqlpg123.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlpg124.d.ts +16 -0
- package/dist/lint/post-synth/sqlpg124.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlpg125.d.ts +17 -0
- package/dist/lint/post-synth/sqlpg125.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlpg126.d.ts +19 -0
- package/dist/lint/post-synth/sqlpg126.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +16 -0
- package/dist/okf/rules/SQL101.md +23 -0
- package/dist/okf/rules/SQLCH121.md +11 -0
- package/dist/okf/rules/SQLCH122.md +11 -0
- package/dist/okf/rules/SQLCH123.md +11 -0
- package/dist/okf/rules/SQLCH124.md +18 -0
- package/dist/okf/rules/SQLCH125.md +11 -0
- package/dist/okf/rules/SQLCH126.md +15 -0
- package/dist/okf/rules/SQLCH127.md +15 -0
- package/dist/okf/rules/SQLPG119.md +16 -0
- package/dist/okf/rules/SQLPG120.md +16 -0
- package/dist/okf/rules/SQLPG121.md +11 -0
- package/dist/okf/rules/SQLPG122.md +15 -0
- package/dist/okf/rules/SQLPG123.md +16 -0
- package/dist/okf/rules/SQLPG124.md +11 -0
- package/dist/okf/rules/SQLPG125.md +11 -0
- package/dist/okf/rules/SQLPG126.md +11 -0
- package/dist/okf/types/Dictionary.md +4 -0
- package/dist/okf/types/Function.md +5 -0
- package/dist/okf/types/Grant.md +6 -0
- package/dist/okf/types/MaterializedView.md +1 -0
- package/dist/okf/types/PostgresDefaultPrivileges.md +4 -0
- package/dist/okf/types/PostgresDomain.md +2 -0
- package/dist/okf/types/PostgresEnum.md +1 -0
- package/dist/okf/types/PostgresFunction.md +4 -0
- package/dist/okf/types/PostgresGrant.md +5 -0
- package/dist/okf/types/PostgresPolicy.md +4 -0
- package/dist/okf/types/PostgresProcedure.md +4 -0
- package/dist/okf/types/PostgresSequence.md +2 -0
- package/dist/okf/types/PostgresTrigger.md +4 -0
- package/dist/okf/types/RowPolicy.md +4 -0
- package/dist/okf/types/Table.md +1 -0
- package/dist/okf/types/View.md +1 -0
- package/dist/postgres/overlays/extension-objects.d.ts +28 -0
- package/dist/postgres/overlays/extension-objects.d.ts.map +1 -0
- package/dist/postgres/overlays/types.d.ts +10 -0
- package/dist/postgres/overlays/types.d.ts.map +1 -1
- package/dist/rules/audit-entries.ts +7 -0
- package/dist/rules/clickhouse-type-check.ts +173 -0
- package/dist/rules/postgres-audit-entries.ts +8 -0
- package/dist/rules/postgres-names.ts +506 -0
- package/dist/rules/sql101.ts +62 -0
- package/dist/rules/sqlch121.ts +12 -0
- package/dist/rules/sqlch122.ts +12 -0
- package/dist/rules/sqlch123.ts +11 -0
- package/dist/rules/sqlch124.ts +21 -0
- package/dist/rules/sqlch125.ts +56 -0
- package/dist/rules/sqlch126.ts +57 -0
- package/dist/rules/sqlch127.ts +128 -0
- package/dist/rules/sqlpg119.ts +37 -0
- package/dist/rules/sqlpg120.ts +70 -0
- package/dist/rules/sqlpg121.ts +32 -0
- package/dist/rules/sqlpg122.ts +28 -0
- package/dist/rules/sqlpg123.ts +63 -0
- package/dist/rules/sqlpg124.ts +47 -0
- package/dist/rules/sqlpg125.ts +40 -0
- package/dist/rules/sqlpg126.ts +37 -0
- package/package.json +2 -2
- package/src/clickhouse/catalog-types.ts +9 -0
- package/src/clickhouse/overlays/codecs.ts +6 -2
- package/src/clickhouse/overlays/kinds.ts +1 -1
- package/src/clickhouse/overlays/types.ts +20 -3
- package/src/clickhouse/type-syntax.ts +82 -0
- package/src/codegen/clickhouse-module.ts +15 -1
- package/src/codegen/postgres-module.ts +43 -0
- package/src/composites/composites.test.ts +7 -6
- package/src/generated/clickhouse.ts +1905 -6
- package/src/generated/postgres.ts +2639 -0
- package/src/lint/audit-catalog.ts +8 -0
- package/src/lint/post-synth/audit-entries.ts +7 -0
- package/src/lint/post-synth/clickhouse-catalog-checks.test.ts +315 -0
- package/src/lint/post-synth/clickhouse-type-check.ts +173 -0
- package/src/lint/post-synth/index.ts +32 -0
- package/src/lint/post-synth/postgres-audit-entries.ts +8 -0
- package/src/lint/post-synth/postgres-checks.test.ts +214 -1
- package/src/lint/post-synth/postgres-names.ts +506 -0
- package/src/lint/post-synth/sql101.ts +62 -0
- package/src/lint/post-synth/sqlch121.ts +12 -0
- package/src/lint/post-synth/sqlch122.ts +12 -0
- package/src/lint/post-synth/sqlch123.ts +11 -0
- package/src/lint/post-synth/sqlch124.ts +21 -0
- package/src/lint/post-synth/sqlch125.ts +56 -0
- package/src/lint/post-synth/sqlch126.ts +57 -0
- package/src/lint/post-synth/sqlch127.ts +128 -0
- package/src/lint/post-synth/sqlpg119.ts +37 -0
- package/src/lint/post-synth/sqlpg120.ts +70 -0
- package/src/lint/post-synth/sqlpg121.ts +32 -0
- package/src/lint/post-synth/sqlpg122.ts +28 -0
- package/src/lint/post-synth/sqlpg123.ts +63 -0
- package/src/lint/post-synth/sqlpg124.ts +47 -0
- package/src/lint/post-synth/sqlpg125.ts +40 -0
- package/src/lint/post-synth/sqlpg126.ts +37 -0
- package/src/postgres/overlays/extension-objects.ts +100 -0
- package/src/postgres/overlays/types.ts +25 -0
|
@@ -66,6 +66,14 @@ export const sqlAuditCatalog: Record<string, RuleMeta> = {
|
|
|
66
66
|
"Use an engine from the pinned server's system.table_engines (or system.database_engines for a database).",
|
|
67
67
|
{ category: "correctness" },
|
|
68
68
|
),
|
|
69
|
+
SQL101: auditRule(
|
|
70
|
+
"SQL101",
|
|
71
|
+
"merge-worthy",
|
|
72
|
+
"guidance",
|
|
73
|
+
"Two exports declare the same object",
|
|
74
|
+
"Keep one declaration of the object and import it where the other was used.",
|
|
75
|
+
{ category: "correctness" },
|
|
76
|
+
),
|
|
69
77
|
...sqlPostSynthAuditEntries,
|
|
70
78
|
...postgresPostSynthAuditEntries,
|
|
71
79
|
};
|
|
@@ -26,4 +26,11 @@ export const sqlPostSynthAuditEntries: Record<string, RuleMeta> = {
|
|
|
26
26
|
SQLCH118: auditRule("SQLCH118", "merge-worthy", "guidance", "A MergeTree setting is not one the pinned server has", "Fix the setting name, or pick a setting from the pinned server's system.merge_tree_settings.", { category: "correctness" }),
|
|
27
27
|
SQLCH119: auditRule("SQLCH119", "report-only", "guidance", "An object uses a deprecated or experimental engine", "Use the engine that replaced it (Atomic for Ordinary), or accept the experimental engine knowingly.", { category: "best-practice" }),
|
|
28
28
|
SQLCH120: auditRule("SQLCH120", "report-only", "guidance", "A MergeTree engine uses the deprecated positional arguments", "Write a bare MergeTree with ORDER BY, PARTITION BY and SETTINGS.", { category: "best-practice" }),
|
|
29
|
+
SQLCH121: auditRule("SQLCH121", "merge-worthy", "guidance", "A column type names a family the pinned server does not have", "Use a type family from the pinned server's system.data_type_families, in the case it lists unless it is case-insensitive.", { category: "correctness" }),
|
|
30
|
+
SQLCH122: auditRule("SQLCH122", "merge-worthy", "guidance", "A column type's parameters do not fit its family", "Give the family the parameters it takes, each within its range.", { category: "correctness" }),
|
|
31
|
+
SQLCH123: auditRule("SQLCH123", "merge-worthy", "guidance", "A Nullable wraps a type ClickHouse does not allow inside Nullable", "Move Nullable inside (Array(Nullable(T)), LowCardinality(Nullable(T))) or drop it.", { category: "correctness" }),
|
|
32
|
+
SQLCH124: auditRule("SQLCH124", "merge-worthy", "guidance", "A table declares a column twice", "Remove or rename one of the two columns.", { category: "correctness" }),
|
|
33
|
+
SQLCH125: auditRule("SQLCH125", "merge-worthy", "guidance", "A codec parameter is outside what the codec takes", "Give the codec a parameter within its range or set, or drop the parameter for the default.", { category: "correctness" }),
|
|
34
|
+
SQLCH126: auditRule("SQLCH126", "merge-worthy", "guidance", "A GRANT column list names a column the table does not declare", "Fix the column name, or declare the column on the table.", { category: "correctness" }),
|
|
35
|
+
SQLCH127: auditRule("SQLCH127", "merge-worthy", "guidance", "An expression calls a function the pinned server does not have", "Fix the function name, use one from the pinned server's system.functions, or declare it with func.", { category: "correctness" }),
|
|
29
36
|
};
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLCH121 to SQLCH127 and SQL101 (#3750): column types, codec parameters,
|
|
3
|
+
* grant columns and function calls against the pinned catalog, and the same
|
|
4
|
+
* object declared twice. Each flagged case is one the probe found building
|
|
5
|
+
* with exit 0; each clean case is a correct declaration that must stay clean.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { makePostSynthCtx } from "@intentius/chant-test-utils";
|
|
9
|
+
import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
|
|
10
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
11
|
+
import { sqlSerializer } from "../../serializer";
|
|
12
|
+
import { database, dictionary, func, grant, role, table, view } from "../../clickhouse/entities";
|
|
13
|
+
import { schema, table as pgTable, type as pgType } from "../../postgres/entities";
|
|
14
|
+
import { sqlAuditCatalog } from "../audit-catalog";
|
|
15
|
+
import { postSynthChecks } from "./index";
|
|
16
|
+
import { calledNames, isKnownFunction } from "./sqlch127";
|
|
17
|
+
import { typeFindings } from "./clickhouse-type-check";
|
|
18
|
+
|
|
19
|
+
type Entities = Record<string, unknown>;
|
|
20
|
+
|
|
21
|
+
function run(id: string, entities: Entities) {
|
|
22
|
+
const check = postSynthChecks.find((c: PostSynthCheck) => c.id === id)!;
|
|
23
|
+
const out = sqlSerializer.serialize(new Map(Object.entries(entities)) as never) as SerializerResult;
|
|
24
|
+
return check.check(makePostSynthCtx("sql", out.primary));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const flagged = (id: string, entities: Entities, text: RegExp) => {
|
|
28
|
+
const diags = run(id, entities);
|
|
29
|
+
expect(diags.length, JSON.stringify(diags)).toBe(1);
|
|
30
|
+
expect(diags[0]).toMatchObject({ checkId: id, lexicon: "sql", severity: "error" });
|
|
31
|
+
expect(diags[0]!.message).toMatch(text);
|
|
32
|
+
return diags[0]!;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const clean = (id: string, entities: Entities) => expect(run(id, entities)).toEqual([]);
|
|
36
|
+
|
|
37
|
+
const NEW = ["SQL101", "SQLCH121", "SQLCH122", "SQLCH123", "SQLCH124", "SQLCH125", "SQLCH126", "SQLCH127"];
|
|
38
|
+
|
|
39
|
+
const shop = database`CREATE DATABASE shop ENGINE = Atomic`;
|
|
40
|
+
/** One table with the column list given, in the probe's shape. */
|
|
41
|
+
const events = (columns: string) =>
|
|
42
|
+
table`CREATE TABLE ${shop}.events (${columns as unknown as never}) ENGINE = MergeTree ORDER BY tuple()`;
|
|
43
|
+
|
|
44
|
+
describe("registration", () => {
|
|
45
|
+
test("each new check is registered with an audit entry", () => {
|
|
46
|
+
const ids = postSynthChecks.map((c) => c.id);
|
|
47
|
+
for (const id of NEW) {
|
|
48
|
+
expect(ids, id).toContain(id);
|
|
49
|
+
expect(sqlAuditCatalog[id], id).toBeDefined();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** Correct column types of every shape ClickHouse accepts. */
|
|
55
|
+
const CORRECT_TYPES = [
|
|
56
|
+
"UInt64",
|
|
57
|
+
"BIGINT UNSIGNED",
|
|
58
|
+
"bigint",
|
|
59
|
+
"Int32 UNSIGNED",
|
|
60
|
+
"INT(11)",
|
|
61
|
+
"UInt64(8)",
|
|
62
|
+
"VARCHAR(255)",
|
|
63
|
+
"String",
|
|
64
|
+
"text",
|
|
65
|
+
"datetime",
|
|
66
|
+
"DOUBLE PRECISION",
|
|
67
|
+
"Float64",
|
|
68
|
+
"bool",
|
|
69
|
+
"Bool",
|
|
70
|
+
"LowCardinality(String)",
|
|
71
|
+
"LowCardinality(Nullable(String))",
|
|
72
|
+
"Array(Nullable(String))",
|
|
73
|
+
"Array(Array(UInt8))",
|
|
74
|
+
"Nullable(String)",
|
|
75
|
+
"Map(String, UInt64)",
|
|
76
|
+
"Map(LowCardinality(String), Array(Nullable(Float64)))",
|
|
77
|
+
"Tuple(String, UInt8)",
|
|
78
|
+
"Tuple(a String, b Nullable(UInt8))",
|
|
79
|
+
"Tuple(date Date, `x y` String)",
|
|
80
|
+
"Tuple()",
|
|
81
|
+
"Nullable(Tuple(a String, b UInt8))",
|
|
82
|
+
"Nested(x UInt8, y String)",
|
|
83
|
+
"AggregateFunction(uniq, UInt64)",
|
|
84
|
+
"AggregateFunction(quantiles(0.5, 0.9), Float64)",
|
|
85
|
+
"AggregateFunction(count)",
|
|
86
|
+
"AggregateFunction(argMax, String, DateTime)",
|
|
87
|
+
"SimpleAggregateFunction(sum, UInt64)",
|
|
88
|
+
"SimpleAggregateFunction(anyLast, Nullable(String))",
|
|
89
|
+
"DateTime('UTC')",
|
|
90
|
+
"DateTime64(3, 'UTC')",
|
|
91
|
+
"DateTime64",
|
|
92
|
+
"Decimal(18, 4)",
|
|
93
|
+
"Decimal(76, 2)",
|
|
94
|
+
"DECIMAL(10, 2)",
|
|
95
|
+
"Decimal",
|
|
96
|
+
"Decimal64(4)",
|
|
97
|
+
"Enum8('a' = 1, 'b(' = 2)",
|
|
98
|
+
"Enum('a', 'b')",
|
|
99
|
+
"FixedString(16)",
|
|
100
|
+
"UUID",
|
|
101
|
+
"IPv4",
|
|
102
|
+
"Variant(String, UInt64)",
|
|
103
|
+
"JSON(max_dynamic_paths = 10, a.b UInt32)",
|
|
104
|
+
"Dynamic(max_types = 8)",
|
|
105
|
+
"String COLLATE utf8",
|
|
106
|
+
"Point",
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
describe("SQLCH121, SQLCH122, SQLCH123: column types", () => {
|
|
110
|
+
test.each(CORRECT_TYPES)("%s is clean", (type) => {
|
|
111
|
+
expect(typeFindings(type, "test")).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("UInt46 is not a family (probe type-unknown)", () => {
|
|
115
|
+
flagged("SQLCH121", { shop, events: events("id UInt46, at DateTime") }, /column id UInt46: UInt46 is not a type family ClickHouse \d/);
|
|
116
|
+
});
|
|
117
|
+
test("uint64 is case-sensitive (probe type-case)", () => {
|
|
118
|
+
flagged("SQLCH121", { shop, events: events("id uint64") }, /uint64 is not a type family .* \(it has UInt64, and that name is case-sensitive\)/);
|
|
119
|
+
});
|
|
120
|
+
test("Nullabel(String) is not a family (probe wrapper-unknown)", () => {
|
|
121
|
+
flagged("SQLCH121", { shop, events: events("id UInt64, note Nullabel(String)") }, /column note Nullabel\(String\): Nullabel is not/);
|
|
122
|
+
});
|
|
123
|
+
test.each(["Array(UInt46)", "Map(String, UInt46)", "Tuple(a UInt46)", "Nested(a UInt46)", "Nullable(UInt46)", "LowCardinality(UInt46)", "Variant(String, UInt46)", "AggregateFunction(uniq, UInt46)", "SimpleAggregateFunction(sum, UInt46)", "Array(Map(String, Array(UInt46)))"])(
|
|
124
|
+
"a family is checked at every depth: %s",
|
|
125
|
+
(type) => {
|
|
126
|
+
expect(typeFindings(type, "test")).toEqual([{ checkId: "SQLCH121", detail: expect.stringMatching(/^UInt46 is not/) }]);
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
test("FixedString with no length (probe type-params-missing)", () => {
|
|
130
|
+
flagged("SQLCH122", { shop, events: events("id UInt64, code FixedString") }, /FixedString needs its length/);
|
|
131
|
+
});
|
|
132
|
+
test("Decimal(100, 2) (probe type-params-bad)", () => {
|
|
133
|
+
flagged("SQLCH122", { shop, events: events("id UInt64, amount Decimal(100, 2)") }, /Decimal precision 100 is outside 1 to 76/);
|
|
134
|
+
});
|
|
135
|
+
test.each([
|
|
136
|
+
["UUID(4)", /UUID takes no parameters/],
|
|
137
|
+
["UInt64(8, 2)", /UInt64 takes at most 1 parameter/],
|
|
138
|
+
["Decimal(10, 2, 1)", /Decimal takes at most 2 parameters/],
|
|
139
|
+
["DateTime64(12)", /DateTime64 precision 12 is outside 0 to 9/],
|
|
140
|
+
["Map(String)", /Map needs its value/],
|
|
141
|
+
["Nullable", /Nullable needs its type/],
|
|
142
|
+
["Enum8", /Enum8 needs at least one member/],
|
|
143
|
+
["Array(FixedString)", /FixedString needs its length/],
|
|
144
|
+
])("parameters that do not fit: %s", (type, detail) => {
|
|
145
|
+
expect(typeFindings(type, "test")).toEqual([{ checkId: "SQLCH122", detail: expect.stringMatching(detail) }]);
|
|
146
|
+
});
|
|
147
|
+
test("Nullable(LowCardinality(String)) (probe wrapper-order)", () => {
|
|
148
|
+
flagged("SQLCH123", { shop, events: events("id UInt64, note Nullable(LowCardinality(String))") }, /Nullable cannot hold LowCardinality; write LowCardinality\(Nullable\(T\)\) instead/);
|
|
149
|
+
});
|
|
150
|
+
test("Nullable(Array(String)) (probe wrapper-nested-nullable)", () => {
|
|
151
|
+
flagged("SQLCH123", { shop, events: events("id UInt64, tags Nullable(Array(String))") }, /Nullable cannot hold Array/);
|
|
152
|
+
});
|
|
153
|
+
test.each(["Nullable(Map(String, UInt8))", "Nullable(Nested(a UInt8))", "Nullable(Variant(String))", "Nullable(Nullable(String))", "Array(Nullable(Array(UInt8)))"])(
|
|
154
|
+
"wrappers ClickHouse refuses: %s",
|
|
155
|
+
(type) => {
|
|
156
|
+
expect(typeFindings(type, "test").map((f) => f.checkId)).toEqual(["SQLCH123"]);
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
test("view and dictionary columns are checked too", () => {
|
|
160
|
+
const v = view`CREATE MATERIALIZED VIEW ${shop}.mv (kind Strng) ENGINE = MergeTree ORDER BY kind AS SELECT 'a' AS kind`;
|
|
161
|
+
flagged("SQLCH121", { shop, v }, /v \(mv\): column kind Strng/);
|
|
162
|
+
const d = dictionary`CREATE DICTIONARY ${shop}.d (id UInt64, name Strng) PRIMARY KEY id SOURCE(CLICKHOUSE(TABLE 't')) LAYOUT(HASHED()) LIFETIME(300)`;
|
|
163
|
+
flagged("SQLCH121", { shop, d }, /column name Strng/);
|
|
164
|
+
});
|
|
165
|
+
test("a table with every correct type is clean for all three", () => {
|
|
166
|
+
const t = events(CORRECT_TYPES.map((type, i) => `c${i} ${type}`).join(", "));
|
|
167
|
+
for (const id of ["SQLCH121", "SQLCH122", "SQLCH123"]) clean(id, { shop, t });
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("SQLCH124: a column declared twice", () => {
|
|
172
|
+
test("flags (id UInt64, id UInt32) (probe dup-column)", () => {
|
|
173
|
+
flagged("SQLCH124", { shop, events: events("id UInt64, id UInt32") }, /events \(events\) declares column id twice/);
|
|
174
|
+
});
|
|
175
|
+
test("distinct columns are clean", () => {
|
|
176
|
+
clean("SQLCH124", { shop, events: events("id UInt64, Id UInt32, n Nested(id UInt8)") });
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("SQLCH125: codec parameters", () => {
|
|
181
|
+
test("ZSTD(99) (probe codec-bad-param)", () => {
|
|
182
|
+
flagged("SQLCH125", { shop, t2: events("id UInt64 CODEC(ZSTD(99))") }, /column id CODEC\(ZSTD\(99\)\): ZSTD level 99 is outside 1 to 22/);
|
|
183
|
+
});
|
|
184
|
+
test.each([
|
|
185
|
+
["LZ4HC(13)", /LZ4HC level 13 is outside 0 to 12/],
|
|
186
|
+
["Delta(3), ZSTD", /Delta bytes 3 is not one of 1, 2, 4, 8/],
|
|
187
|
+
["DoubleDelta(16)", /DoubleDelta bytes 16 is not one of/],
|
|
188
|
+
["Gorilla(5)", /Gorilla bytes 5/],
|
|
189
|
+
["LZ4(1)", /LZ4 takes no parameters/],
|
|
190
|
+
["T64('nibble')", /T64 variant 'nibble' is not one of 'byte', 'bit'/],
|
|
191
|
+
])("flags %s", (codec, text) => {
|
|
192
|
+
flagged("SQLCH125", { shop, t: events(`id UInt64 CODEC(${codec})`) }, text);
|
|
193
|
+
});
|
|
194
|
+
test("codecs within their parameters, and unknown-parameter codecs, are clean", () => {
|
|
195
|
+
const t = events(
|
|
196
|
+
"a UInt64 CODEC(Delta, ZSTD(3)), b UInt64 CODEC(Delta(8), LZ4HC(9)), c DateTime CODEC(DoubleDelta(4), ZSTD(1, 27)), d Float64 CODEC(Gorilla, FPC(12, 8)), e UInt64 CODEC(T64('bit'), ZSTD), f String CODEC(NONE), g String CODEC(AES_128_GCM_SIV), h Float64 CODEC(ALP(1, 2, 3))",
|
|
197
|
+
);
|
|
198
|
+
clean("SQLCH125", { shop, t });
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("SQLCH126: grant column lists", () => {
|
|
203
|
+
const t = table`CREATE TABLE ${shop}.events (id UInt64, kind String, n Nested(a UInt8)) ENGINE = MergeTree ORDER BY id`;
|
|
204
|
+
const r = role`CREATE ROLE analyst`;
|
|
205
|
+
test("GRANT SELECT(kindd) ON ${events} (probe grant-bad-column)", () => {
|
|
206
|
+
const g = grant`GRANT SELECT(kindd) ON ${t} TO ${r}`;
|
|
207
|
+
flagged("SQLCH126", { shop, t, r, g }, /g: GRANT SELECT\(kindd\) ON shop.events names column kindd, which t \(events\) does not declare/);
|
|
208
|
+
});
|
|
209
|
+
test("declared columns, Nested fields, and a plain-text target are clean", () => {
|
|
210
|
+
const g = grant`GRANT SELECT(id, kind, n.a), INSERT(id) ON ${t} TO ${r}`;
|
|
211
|
+
const plain = grant`GRANT SELECT(whatever) ON shop.other TO ${r}`;
|
|
212
|
+
const all = grant`GRANT SELECT ON ${t} TO ${r}`;
|
|
213
|
+
clean("SQLCH126", { shop, t, r, g, plain, all });
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("SQLCH127: functions in expressions", () => {
|
|
218
|
+
test("DEFAULT noww() (probe default-function-unknown)", () => {
|
|
219
|
+
flagged("SQLCH127", { shop, events: events("id UInt64, at DateTime DEFAULT noww()") }, /events \(events\): column at DEFAULT calls noww\(\), which ClickHouse \S+ does not have/);
|
|
220
|
+
});
|
|
221
|
+
test("PARTITION BY toYYYYMMM(at) (probe function-unknown)", () => {
|
|
222
|
+
const t = table`CREATE TABLE ${shop}.events (id UInt64, at DateTime) ENGINE = MergeTree PARTITION BY toYYYYMMM(at) ORDER BY id`;
|
|
223
|
+
flagged("SQLCH127", { shop, t }, /PARTITION BY calls toYYYYMMM\(\)/);
|
|
224
|
+
});
|
|
225
|
+
test.each([
|
|
226
|
+
["ORDER BY", "ORDER BY (id, toStartOfHourr(at))"],
|
|
227
|
+
["PRIMARY KEY", "PRIMARY KEY lowerr(s) ORDER BY lowerr(s)"],
|
|
228
|
+
["SAMPLE BY", "ORDER BY (id, cityHash6(id)) SAMPLE BY cityHash6(id)"],
|
|
229
|
+
["TTL", "ORDER BY id TTL toDatee(at) + INTERVAL 1 DAY"],
|
|
230
|
+
])("checks %s", (where, clauses) => {
|
|
231
|
+
const t = table`CREATE TABLE ${shop}.events (id UInt64, at DateTime, s String) ENGINE = MergeTree ${clauses as unknown as never}`;
|
|
232
|
+
const messages = run("SQLCH127", { shop, t }).map((d) => d.message);
|
|
233
|
+
expect(messages).toContainEqual(expect.stringContaining(`${where} calls`));
|
|
234
|
+
});
|
|
235
|
+
test("checks MATERIALIZED, ALIAS, EPHEMERAL, a column TTL and a skip index", () => {
|
|
236
|
+
const t = events(
|
|
237
|
+
"id UInt64, a UInt64 MATERIALIZED fooo(id), b UInt64 ALIAS barr(id), c UInt64 EPHEMERAL bazz(1), at DateTime TTL quxx(at), INDEX k zapp(id) TYPE minmax GRANULARITY 1",
|
|
238
|
+
);
|
|
239
|
+
const messages = run("SQLCH127", { shop, t }).map((d) => d.message);
|
|
240
|
+
expect(messages).toEqual([
|
|
241
|
+
expect.stringContaining("column a MATERIALIZED calls fooo()"),
|
|
242
|
+
expect.stringContaining("column b ALIAS calls barr()"),
|
|
243
|
+
expect.stringContaining("column c EPHEMERAL calls bazz()"),
|
|
244
|
+
expect.stringContaining("column at TTL calls quxx()"),
|
|
245
|
+
expect.stringContaining("index k calls zapp()"),
|
|
246
|
+
]);
|
|
247
|
+
});
|
|
248
|
+
test("catalog functions, aliases, combinators, keywords, lambdas, strings and func UDFs are clean", () => {
|
|
249
|
+
const f = func`CREATE FUNCTION normalizeKind AS (k) -> lower(trim(k))`;
|
|
250
|
+
const t = table`CREATE TABLE ${shop}.events (
|
|
251
|
+
id UInt64,
|
|
252
|
+
at DateTime DEFAULT now(),
|
|
253
|
+
at64 DateTime64(3) DEFAULT now64(3),
|
|
254
|
+
d Date DEFAULT toDate(at),
|
|
255
|
+
day Date MATERIALIZED DATE_TRUNC('day', at),
|
|
256
|
+
day2 Date MATERIALIZED date_trunc('day', at),
|
|
257
|
+
u UUID DEFAULT generateUUIDv4(),
|
|
258
|
+
s String DEFAULT 'not_a_call(x)',
|
|
259
|
+
k String DEFAULT normalizeKind(s),
|
|
260
|
+
c UInt64 DEFAULT CAST(1 AS UInt64),
|
|
261
|
+
t Tuple(UInt8, UInt8) DEFAULT tuple(1, 2),
|
|
262
|
+
i UInt8 DEFAULT if(id > 0, 1, 0),
|
|
263
|
+
y UInt16 DEFAULT EXTRACT(YEAR FROM at),
|
|
264
|
+
tr String DEFAULT TRIM(BOTH ' ' FROM s),
|
|
265
|
+
sub String ALIAS SUBSTRING(s, 1, 2),
|
|
266
|
+
dd Int64 ALIAS DATE_DIFF('day', at, now()),
|
|
267
|
+
inl UInt8 ALIAS id IN (1, 2),
|
|
268
|
+
arr Array(UInt64) DEFAULT arrayMap(x -> x * 2, [id]),
|
|
269
|
+
cnt UInt64 ALIAS COUNT(),
|
|
270
|
+
q String ALIAS \`weird(name)\`,
|
|
271
|
+
m Map(String, UInt8) DEFAULT map('a', 1),
|
|
272
|
+
INDEX ix lower(s) TYPE bloom_filter GRANULARITY 1
|
|
273
|
+
) ENGINE = MergeTree PARTITION BY toYYYYMM(at) ORDER BY (id, cityHash64(id)) SAMPLE BY cityHash64(id)
|
|
274
|
+
TTL at + INTERVAL 1 DAY RECOMPRESS CODEC(ZSTD(17)), at + toIntervalMonth(1) DELETE`;
|
|
275
|
+
clean("SQLCH127", { shop, f, t });
|
|
276
|
+
});
|
|
277
|
+
test("combinators resolve to their aggregate", () => {
|
|
278
|
+
for (const name of ["countIf", "sumState", "uniqMerge", "argMaxIf", "sumIfState", "quantilesMergeState", "anyLastSimpleState", "sumForEach", "uniqExactOrNull", "COUNTIf"]) {
|
|
279
|
+
expect(isKnownFunction(name), name).toBe(true);
|
|
280
|
+
}
|
|
281
|
+
for (const name of ["noww", "toYYYYMMIf", "nowState", "State", "Merge"]) expect(isKnownFunction(name), name).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
test("only an identifier followed by ( outside strings and quoted names is a call", () => {
|
|
284
|
+
expect(calledNames("f(x) + 'g(y)' + `h(z)` + t.k(1) + x IN (1) + EXISTS(1) + ARRAY(1) + INTERVAL (1) DAY")).toEqual(["f"]);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("SQL101: an object declared twice", () => {
|
|
289
|
+
test("two exports of shop.events (probe dup-table)", () => {
|
|
290
|
+
const a = table`CREATE TABLE ${shop}.events (id UInt64) ENGINE = MergeTree ORDER BY id`;
|
|
291
|
+
const b = table`CREATE TABLE ${shop}.events (id UInt64) ENGINE = MergeTree ORDER BY id`;
|
|
292
|
+
const d = flagged("SQL101", { shop, a, b }, /^a and b both declare ClickHouse table shop.events/);
|
|
293
|
+
expect(d.entity).toBe("b");
|
|
294
|
+
});
|
|
295
|
+
test("two exports of app.orders, Postgres", () => {
|
|
296
|
+
const app = schema`CREATE SCHEMA app`;
|
|
297
|
+
const a = pgTable`CREATE TABLE ${app}.orders (id bigint PRIMARY KEY)`;
|
|
298
|
+
const b = pgTable`CREATE TABLE ${app}.orders (id bigint PRIMARY KEY)`;
|
|
299
|
+
flagged("SQL101", { app, a, b }, /a and b both declare Postgres table/);
|
|
300
|
+
});
|
|
301
|
+
test("one name across kinds or databases, and distinct objects, are clean", () => {
|
|
302
|
+
const other = database`CREATE DATABASE other ENGINE = Atomic`;
|
|
303
|
+
const a = table`CREATE TABLE ${shop}.events (id UInt64) ENGINE = MergeTree ORDER BY id`;
|
|
304
|
+
const b = table`CREATE TABLE ${other}.events (id UInt64) ENGINE = MergeTree ORDER BY id`;
|
|
305
|
+
const r = role`CREATE ROLE analyst`;
|
|
306
|
+
const g1 = grant`GRANT SELECT ON ${a} TO ${r}`;
|
|
307
|
+
const g2 = grant`GRANT SELECT ON ${b} TO ${r}`;
|
|
308
|
+
clean("SQL101", { shop, other, a, b, r, g1, g2 });
|
|
309
|
+
const app = schema`CREATE SCHEMA app`;
|
|
310
|
+
const mood = pgType`CREATE TYPE ${app}.mood AS ENUM ('a')`;
|
|
311
|
+
const orders = pgTable`CREATE TABLE ${app}.orders (id bigint PRIMARY KEY)`;
|
|
312
|
+
const lines = pgTable`CREATE TABLE ${app}.lines (id bigint PRIMARY KEY)`;
|
|
313
|
+
clean("SQL101", { app, mood, orders, lines });
|
|
314
|
+
});
|
|
315
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checking a ClickHouse column type against the pinned catalog, for SQLCH121
|
|
3
|
+
* (the family), SQLCH122 (its parameters) and SQLCH123 (a wrapper the server
|
|
4
|
+
* refuses). One walk over the type answers all three; each check keeps the
|
|
5
|
+
* findings with its own id.
|
|
6
|
+
*
|
|
7
|
+
* Families come from `system.data_type_families` (`TYPE_FAMILIES`): a name is
|
|
8
|
+
* case-sensitive unless the catalog marks it case-insensitive, and an alias
|
|
9
|
+
* resolves to its family. Parameters come from the `TYPE_PARAMETERS` overlay.
|
|
10
|
+
* The walk goes into every parameter that is itself a type, so `UInt46`
|
|
11
|
+
* inside `Array(Map(String, UInt46))` is found.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { CLICKHOUSE_VERSION, TYPE_FAMILIES } from "../../generated/clickhouse";
|
|
15
|
+
import { TYPE_PARAMETERS, type TypeParameter } from "../../clickhouse/overlays/types";
|
|
16
|
+
import { numericParam, readType, typeTokens, type TypeParam } from "../../clickhouse/type-syntax";
|
|
17
|
+
import type { Token } from "../../clickhouse/tokens";
|
|
18
|
+
import { checkOf } from "./clickhouse-helpers";
|
|
19
|
+
import { clickhouseObjects, type OutputObject } from "./sql-helpers";
|
|
20
|
+
|
|
21
|
+
export interface TypeFinding {
|
|
22
|
+
checkId: "SQLCH121" | "SQLCH122" | "SQLCH123";
|
|
23
|
+
/** What is wrong, without the column: `UInt46 is not a type family ClickHouse 26.8.15.10 has`. */
|
|
24
|
+
detail: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type FamilySpec = { canonical: string; caseInsensitive: boolean };
|
|
28
|
+
const FAMILIES = TYPE_FAMILIES as Readonly<Record<string, FamilySpec | undefined>>;
|
|
29
|
+
const FOLDED = new Map<string, FamilySpec>();
|
|
30
|
+
const BY_LOWER = new Map<string, string>();
|
|
31
|
+
for (const [name, spec] of Object.entries(FAMILIES)) {
|
|
32
|
+
if (spec?.caseInsensitive) FOLDED.set(name.toLowerCase(), spec);
|
|
33
|
+
BY_LOWER.set(name.toLowerCase(), name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A family by name as the server resolves it: exactly, or in any case when the catalog marks it case-insensitive. */
|
|
37
|
+
export function familyOf(name: string): FamilySpec | undefined {
|
|
38
|
+
return FAMILIES[name] ?? FOLDED.get(name.toLowerCase());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The MySQL-compatible words the parser keeps after a type (`INT(11) UNSIGNED`); the server ignores them. */
|
|
42
|
+
const MODIFIERS = new Set(["UNSIGNED", "SIGNED", "AUTO_INCREMENT", "ZEROFILL"]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What `Nullable(...)` cannot hold. `Tuple` is not here: the pinned server
|
|
46
|
+
* takes `Nullable(Tuple(...))` when `enable_nullable_tuple_type` is on, a
|
|
47
|
+
* setting of the server's profile that the build cannot see.
|
|
48
|
+
*/
|
|
49
|
+
const NOT_INSIDE_NULLABLE: Record<string, string> = {
|
|
50
|
+
Array: "make the elements Nullable instead, Array(Nullable(T)), or drop Nullable: an empty array stands for no value",
|
|
51
|
+
Map: "make the values Nullable instead, Map(K, Nullable(V)), or drop Nullable",
|
|
52
|
+
Nested: "make the fields Nullable instead",
|
|
53
|
+
LowCardinality: "write LowCardinality(Nullable(T)) instead",
|
|
54
|
+
Variant: "drop Nullable: a Variant already holds NULL",
|
|
55
|
+
Nullable: "drop one Nullable",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Families whose parameters are settings or expressions rather than types and literals; left to the server. */
|
|
59
|
+
const OPAQUE = new Set(["JSON", "Dynamic"]);
|
|
60
|
+
|
|
61
|
+
/** Families whose one repeated parameter needs at least one value. */
|
|
62
|
+
const NEEDS_ONE = new Set(["Enum", "Enum8", "Enum16", "Nested", "Variant"]);
|
|
63
|
+
|
|
64
|
+
const isName = (t: Token | undefined) => t?.kind === "ident" || t?.kind === "qident";
|
|
65
|
+
|
|
66
|
+
/** Every finding for one column type, in the order the walk meets them. */
|
|
67
|
+
export function typeFindings(type: string, version: string): TypeFinding[] {
|
|
68
|
+
const tokens = typeTokens(type);
|
|
69
|
+
if (!tokens) return [];
|
|
70
|
+
const out: TypeFinding[] = [];
|
|
71
|
+
walk(tokens, version, out);
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function walk(tokens: readonly Token[], version: string, out: TypeFinding[], named = false): string | undefined {
|
|
76
|
+
const syntax = readType(tokens);
|
|
77
|
+
if (!syntax) return undefined;
|
|
78
|
+
let words = syntax.words;
|
|
79
|
+
// A named tuple or Nested element, `a String`: the first word is the element's name.
|
|
80
|
+
if (named && words.length >= 2 && !familyOf(words.map((w) => w.text).join(" "))) {
|
|
81
|
+
const at = tokens.indexOf(words[0]!);
|
|
82
|
+
return walk(tokens.slice(at + 1), version, out);
|
|
83
|
+
}
|
|
84
|
+
if (words.some((w) => w.kind !== "ident")) return undefined;
|
|
85
|
+
// `INT UNSIGNED` is a family of its own; `Int32 UNSIGNED` is Int32 with a modifier the server ignores.
|
|
86
|
+
let spec = familyOf(words.map((w) => w.text).join(" "));
|
|
87
|
+
while (!spec && words.length > 1 && MODIFIERS.has(words[words.length - 1]!.text.toUpperCase())) {
|
|
88
|
+
words = words.slice(0, -1);
|
|
89
|
+
spec = familyOf(words.map((w) => w.text).join(" "));
|
|
90
|
+
}
|
|
91
|
+
const written = words.map((w) => w.text).join(" ");
|
|
92
|
+
if (!spec) {
|
|
93
|
+
const other = BY_LOWER.get(written.toLowerCase());
|
|
94
|
+
const hint = other ? ` (it has ${other}, and that name is case-sensitive)` : "";
|
|
95
|
+
out.push({ checkId: "SQLCH121", detail: `${written} is not a type family ClickHouse ${version} has${hint}` });
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
const family = spec.canonical;
|
|
99
|
+
if (OPAQUE.has(family)) return family;
|
|
100
|
+
const expected: readonly TypeParameter[] = TYPE_PARAMETERS[family] ?? [];
|
|
101
|
+
let params = syntax.params;
|
|
102
|
+
const say = (detail: string) => out.push({ checkId: "SQLCH122", detail });
|
|
103
|
+
|
|
104
|
+
if (params === undefined) {
|
|
105
|
+
const required = expected.filter((p) => !p.optional && !p.repeated);
|
|
106
|
+
if (required.length > 0) say(`${written} needs its ${required.map((p) => p.name).join(" and ")}`);
|
|
107
|
+
else if (NEEDS_ONE.has(family)) say(`${written} needs at least one ${expected[0]!.name}`);
|
|
108
|
+
return family;
|
|
109
|
+
}
|
|
110
|
+
// `AggregateFunction(1, uniq, UInt64)`: a leading number is the function's state version.
|
|
111
|
+
if (family === "AggregateFunction" && params[0] && numericParam(params[0]) !== undefined) params = params.slice(1);
|
|
112
|
+
|
|
113
|
+
if (expected.length === 0) {
|
|
114
|
+
if (params.length > 0) say(`${written} takes no parameters, and ${written}(${params.map((p) => p.text).join(", ")}) gives ${params.length}`);
|
|
115
|
+
return family;
|
|
116
|
+
}
|
|
117
|
+
const repeated = expected[expected.length - 1]!.repeated === true;
|
|
118
|
+
if (!repeated && params.length > expected.length) {
|
|
119
|
+
say(`${written} takes at most ${expected.length} parameter${expected.length === 1 ? "" : "s"} (${expected.map((p) => p.name).join(", ")}), and is given ${params.length}`);
|
|
120
|
+
return family;
|
|
121
|
+
}
|
|
122
|
+
const required = expected.filter((p) => !p.optional && !p.repeated).length;
|
|
123
|
+
if (params.length < required) {
|
|
124
|
+
say(`${written} needs its ${expected.slice(params.length, required).map((p) => p.name).join(" and ")}`);
|
|
125
|
+
return family;
|
|
126
|
+
}
|
|
127
|
+
if (params.length === 0 && NEEDS_ONE.has(family)) {
|
|
128
|
+
say(`${written} needs at least one ${expected[0]!.name}`);
|
|
129
|
+
return family;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
params.forEach((param, i) => {
|
|
133
|
+
const p = expected[Math.min(i, expected.length - 1)]!;
|
|
134
|
+
checkParam(written, family, p, param, version, out);
|
|
135
|
+
});
|
|
136
|
+
return family;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function checkParam(written: string, family: string, p: TypeParameter, param: TypeParam, version: string, out: TypeFinding[]): void {
|
|
140
|
+
if (p.kind === "number" && p.range) {
|
|
141
|
+
const value = numericParam(param);
|
|
142
|
+
if (value !== undefined && (value < p.range[0] || value > p.range[1])) {
|
|
143
|
+
out.push({ checkId: "SQLCH122", detail: `${written} ${p.name} ${param.text} is outside ${p.range[0]} to ${p.range[1]}` });
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (p.kind !== "type" && p.kind !== "named-type") return;
|
|
148
|
+
const inner = walk(param.tokens, version, out, p.kind === "named-type" && isName(param.tokens.find((t) => t.kind !== "ws" && t.kind !== "comment")));
|
|
149
|
+
if (family === "Nullable" && inner !== undefined && NOT_INSIDE_NULLABLE[inner]) {
|
|
150
|
+
out.push({ checkId: "SQLCH123", detail: `Nullable cannot hold ${inner}; ${NOT_INSIDE_NULLABLE[inner]}` });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Each declared column type in the build: tables, views that list their columns, and dictionary attributes. */
|
|
155
|
+
export function* columnTypes(objects: readonly OutputObject[]): Generator<{ object: OutputObject; column: string; type: string }> {
|
|
156
|
+
for (const o of objects) {
|
|
157
|
+
if (!/^ClickHouse::(Table|View|MaterializedView|Dictionary)$/.test(o.type)) continue;
|
|
158
|
+
const columns = (o.columns ?? []) as Array<{ name: string; type?: string }>;
|
|
159
|
+
for (const c of columns) if (c.type) yield { object: o, column: c.name, type: c.type };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The body of one of the three type checks: every column type's findings with that id. */
|
|
164
|
+
export function reportTypeFindings(id: TypeFinding["checkId"]): Parameters<typeof checkOf>[1] {
|
|
165
|
+
return (ctx, report) => {
|
|
166
|
+
for (const { object, column, type } of columnTypes(clickhouseObjects(ctx))) {
|
|
167
|
+
for (const f of typeFindings(type, CLICKHOUSE_VERSION)) {
|
|
168
|
+
if (f.checkId !== id) continue;
|
|
169
|
+
report({ severity: "error", message: `${object.export} (${object.name}): column ${column} ${type}: ${f.detail}`, entity: object.export });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Code generated by chant generate. DO NOT EDIT.
|
|
2
2
|
import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { sql101 } from "./sql101";
|
|
3
4
|
import { sqlch101 } from "./sqlch101";
|
|
4
5
|
import { sqlch102 } from "./sqlch102";
|
|
5
6
|
import { sqlch103 } from "./sqlch103";
|
|
@@ -20,6 +21,13 @@ import { sqlch117 } from "./sqlch117";
|
|
|
20
21
|
import { sqlch118 } from "./sqlch118";
|
|
21
22
|
import { sqlch119 } from "./sqlch119";
|
|
22
23
|
import { sqlch120 } from "./sqlch120";
|
|
24
|
+
import { sqlch121 } from "./sqlch121";
|
|
25
|
+
import { sqlch122 } from "./sqlch122";
|
|
26
|
+
import { sqlch123 } from "./sqlch123";
|
|
27
|
+
import { sqlch124 } from "./sqlch124";
|
|
28
|
+
import { sqlch125 } from "./sqlch125";
|
|
29
|
+
import { sqlch126 } from "./sqlch126";
|
|
30
|
+
import { sqlch127 } from "./sqlch127";
|
|
23
31
|
import { sqlpg101 } from "./sqlpg101";
|
|
24
32
|
import { sqlpg102 } from "./sqlpg102";
|
|
25
33
|
import { sqlpg103 } from "./sqlpg103";
|
|
@@ -38,8 +46,17 @@ import { sqlpg115 } from "./sqlpg115";
|
|
|
38
46
|
import { sqlpg116 } from "./sqlpg116";
|
|
39
47
|
import { sqlpg117 } from "./sqlpg117";
|
|
40
48
|
import { sqlpg118 } from "./sqlpg118";
|
|
49
|
+
import { sqlpg119 } from "./sqlpg119";
|
|
50
|
+
import { sqlpg120 } from "./sqlpg120";
|
|
51
|
+
import { sqlpg121 } from "./sqlpg121";
|
|
52
|
+
import { sqlpg122 } from "./sqlpg122";
|
|
53
|
+
import { sqlpg123 } from "./sqlpg123";
|
|
54
|
+
import { sqlpg124 } from "./sqlpg124";
|
|
55
|
+
import { sqlpg125 } from "./sqlpg125";
|
|
56
|
+
import { sqlpg126 } from "./sqlpg126";
|
|
41
57
|
|
|
42
58
|
export const postSynthChecks: PostSynthCheck[] = [
|
|
59
|
+
sql101,
|
|
43
60
|
sqlch101,
|
|
44
61
|
sqlch102,
|
|
45
62
|
sqlch103,
|
|
@@ -60,6 +77,13 @@ export const postSynthChecks: PostSynthCheck[] = [
|
|
|
60
77
|
sqlch118,
|
|
61
78
|
sqlch119,
|
|
62
79
|
sqlch120,
|
|
80
|
+
sqlch121,
|
|
81
|
+
sqlch122,
|
|
82
|
+
sqlch123,
|
|
83
|
+
sqlch124,
|
|
84
|
+
sqlch125,
|
|
85
|
+
sqlch126,
|
|
86
|
+
sqlch127,
|
|
63
87
|
sqlpg101,
|
|
64
88
|
sqlpg102,
|
|
65
89
|
sqlpg103,
|
|
@@ -78,4 +102,12 @@ export const postSynthChecks: PostSynthCheck[] = [
|
|
|
78
102
|
sqlpg116,
|
|
79
103
|
sqlpg117,
|
|
80
104
|
sqlpg118,
|
|
105
|
+
sqlpg119,
|
|
106
|
+
sqlpg120,
|
|
107
|
+
sqlpg121,
|
|
108
|
+
sqlpg122,
|
|
109
|
+
sqlpg123,
|
|
110
|
+
sqlpg124,
|
|
111
|
+
sqlpg125,
|
|
112
|
+
sqlpg126,
|
|
81
113
|
];
|
|
@@ -26,4 +26,12 @@ export const postgresPostSynthAuditEntries: Record<string, RuleMeta> = {
|
|
|
26
26
|
SQLPG116: auditRule("SQLPG116", "merge-worthy", "guidance", "An extension the target major no longer ships", "Remove the extension, or set sql.postgresMajor to an older major.", { category: "best-practice" }),
|
|
27
27
|
SQLPG117: auditRule("SQLPG117", "report-only", "guidance", "A view without security_invoker runs with its owner's privileges", "Set security_invoker = true in the view's WITH options.", { category: "security" }),
|
|
28
28
|
SQLPG118: auditRule("SQLPG118", "report-only", "guidance", "A table uses INHERITS where declarative partitioning replaced it", "Use PARTITION BY and PARTITION OF.", { category: "best-practice" }),
|
|
29
|
+
SQLPG119: auditRule("SQLPG119", "merge-worthy", "guidance", "A column, domain or sequence type the target major does not have", "Fix the type name, declare the type, or declare the extension that provides it.", { category: "correctness" }),
|
|
30
|
+
SQLPG120: auditRule("SQLPG120", "merge-worthy", "guidance", "A type modifier that does not fit the type", "Drop the modifier, or bring it into the type's range.", { category: "correctness" }),
|
|
31
|
+
SQLPG121: auditRule("SQLPG121", "merge-worthy", "guidance", "An identity column whose type is not smallint, integer or bigint", "Declare the identity column smallint, integer or bigint.", { category: "correctness" }),
|
|
32
|
+
SQLPG122: auditRule("SQLPG122", "merge-worthy", "guidance", "A table declares a column twice, or an enum lists a label twice", "Remove or rename the repeated column or label.", { category: "correctness" }),
|
|
33
|
+
SQLPG123: auditRule("SQLPG123", "merge-worthy", "guidance", "A key, index or grant column list names a column the table does not declare", "Fix the column name, or write it as ${table.columns.name}.", { category: "correctness" }),
|
|
34
|
+
SQLPG124: auditRule("SQLPG124", "merge-worthy", "guidance", "A foreign key names a column the referenced table lacks, or one of another type category", "Reference a declared column, and give the referencing column a type of the same category.", { category: "correctness" }),
|
|
35
|
+
SQLPG125: auditRule("SQLPG125", "merge-worthy", "guidance", "An index access method or operator class the target major does not have, or not for that method", "Use a method and operator class the target major has, or declare the extension that provides them.", { category: "correctness" }),
|
|
36
|
+
SQLPG126: auditRule("SQLPG126", "merge-worthy", "guidance", "An expression calls a function the target major does not have", "Fix the function name, declare the function, or declare the extension that provides it.", { category: "correctness" }),
|
|
29
37
|
};
|