@intentius/chant-lexicon-sql 0.121.1 → 0.123.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/README.md +1 -1
- package/dist/clickhouse/catalog-types.d.ts +8 -0
- package/dist/clickhouse/catalog-types.d.ts.map +1 -1
- package/dist/clickhouse/entities.d.ts +37 -11
- package/dist/clickhouse/entities.d.ts.map +1 -1
- package/dist/clickhouse/import/generator.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/plan/commands.d.ts.map +1 -1
- package/dist/clickhouse/row-types.d.ts +30 -0
- package/dist/clickhouse/row-types.d.ts.map +1 -0
- package/dist/clickhouse/type-syntax.d.ts +33 -0
- package/dist/clickhouse/type-syntax.d.ts.map +1 -0
- package/dist/clickhouse.d.ts +1 -1
- package/dist/clickhouse.d.ts.map +1 -1
- package/dist/codegen/clickhouse-module.d.ts.map +1 -1
- package/dist/codegen/postgres-module.d.ts.map +1 -1
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/typed.d.ts +132 -0
- package/dist/core/typed.d.ts.map +1 -0
- 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 +27 -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 +151 -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/sql102.d.ts +16 -0
- package/dist/lint/post-synth/sql102.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/lint/rules/index.d.ts +3 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lint/rules/sqlch004.d.ts +19 -0
- package/dist/lint/rules/sqlch004.d.ts.map +1 -0
- package/dist/lint/rules/sqlpg005.d.ts +18 -0
- package/dist/lint/rules/sqlpg005.d.ts.map +1 -0
- package/dist/lint/rules/typed-names.d.ts +43 -0
- package/dist/lint/rules/typed-names.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +19 -0
- package/dist/okf/rules/SQL101.md +23 -0
- package/dist/okf/rules/SQL102.md +11 -0
- package/dist/okf/rules/SQLCH004.md +11 -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/SQLPG005.md +11 -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/entities.d.ts +38 -9
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts.map +1 -1
- 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/postgres/row-types.d.ts +17 -0
- package/dist/postgres/row-types.d.ts.map +1 -0
- package/dist/postgres/testing/migration.d.ts +3 -2
- package/dist/postgres/testing/migration.d.ts.map +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.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 +510 -0
- package/dist/rules/sql101.ts +62 -0
- package/dist/rules/sql102.ts +34 -0
- package/dist/rules/sqlch004.ts +81 -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/sqlpg005.ts +73 -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/dist/rules/typed-names.ts +120 -0
- package/dist/sql-types-command.d.ts +19 -0
- package/dist/sql-types-command.d.ts.map +1 -0
- package/dist/sql-types.d.ts +53 -0
- package/dist/sql-types.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/clickhouse/catalog-types.ts +9 -0
- package/src/clickhouse/dictionary.test.ts +1 -1
- package/src/clickhouse/entities.ts +79 -14
- package/src/clickhouse/import/generator.ts +7 -1
- package/src/clickhouse/import/roundtrip.test.ts +1 -1
- 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/plan/commands.ts +5 -0
- package/src/clickhouse/row-types.ts +158 -0
- package/src/clickhouse/type-syntax.ts +82 -0
- package/src/clickhouse.ts +10 -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/core/index.ts +4 -0
- package/src/core/typed.ts +143 -0
- package/src/generated/clickhouse.ts +1905 -6
- package/src/generated/postgres.ts +2639 -0
- package/src/lint/audit-catalog.ts +28 -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 +34 -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 +510 -0
- package/src/lint/post-synth/sql101.ts +62 -0
- package/src/lint/post-synth/sql102.ts +34 -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/lint/rules/index.ts +4 -2
- package/src/lint/rules/sqlch004.ts +81 -0
- package/src/lint/rules/sqlpg005.ts +73 -0
- package/src/lint/rules/typed-names.test.ts +151 -0
- package/src/lint/rules/typed-names.ts +120 -0
- package/src/op/__snapshots__/watch-pipeline.test.ts.snap +3 -3
- package/src/postgres/entities.ts +65 -12
- package/src/postgres/import/generator.ts +7 -1
- package/src/postgres/overlays/extension-objects.ts +100 -0
- package/src/postgres/overlays/types.ts +25 -0
- package/src/postgres/row-types.ts +111 -0
- package/src/postgres.ts +10 -0
- package/src/sql-types-command.ts +99 -0
- package/src/sql-types-staleness.test.ts +110 -0
- package/src/sql-types.test.ts +132 -0
- package/src/sql-types.ts +352 -0
- package/src/typecheck.test.ts +12 -3
- package/src/typed-columns.test.ts +92 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { CODECS, CLICKHOUSE_VERSION } from "../../generated/clickhouse";
|
|
2
|
+
import { checkOf, isTable, splitTop } from "./clickhouse-helpers";
|
|
3
|
+
import { clickhouseObjects } from "./sql-helpers";
|
|
4
|
+
|
|
5
|
+
type Param = { name: string; kind: string; optional?: boolean; range?: readonly [number, number]; values?: readonly string[] };
|
|
6
|
+
|
|
7
|
+
const NUMBER = /^-?\d+(\.\d+)?$/;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* SQLCH125: a codec's parameters do not fit it: more than the codec takes
|
|
11
|
+
* (`LZ4(1)`), a number outside its range (`ZSTD(99)`, `LZ4HC(13)`), or a
|
|
12
|
+
* value outside its set (`Delta(3)`; the byte width is 1, 2, 4 or 8).
|
|
13
|
+
*
|
|
14
|
+
* The parameters are the `CODEC_OVERLAY`'s; a codec marked `"unknown"` there
|
|
15
|
+
* (the experimental ones with undocumented parameters) is not checked. An
|
|
16
|
+
* unknown codec name is SQLCH114's.
|
|
17
|
+
*/
|
|
18
|
+
export const sqlch125 = checkOf({ id: "SQLCH125", description: "A codec parameter is outside what the codec takes" }, (ctx, report) => {
|
|
19
|
+
const codecs = CODECS as Record<string, { parameters: readonly Param[] | "unknown" } | undefined>;
|
|
20
|
+
for (const t of clickhouseObjects(ctx).filter(isTable)) {
|
|
21
|
+
for (const c of t.columns) {
|
|
22
|
+
if (!c.codec) continue;
|
|
23
|
+
for (const part of splitTop(c.codec)) {
|
|
24
|
+
const m = /^([A-Za-z_]\w*)\s*(?:\(([\s\S]*)\))?$/.exec(part.trim());
|
|
25
|
+
if (!m) continue;
|
|
26
|
+
const name = m[1]!;
|
|
27
|
+
const spec = codecs[name] ?? Object.entries(codecs).find(([k]) => k.toLowerCase() === name.toLowerCase())?.[1];
|
|
28
|
+
if (!spec || spec.parameters === "unknown" || m[2] === undefined) continue;
|
|
29
|
+
const args = splitTop(m[2]);
|
|
30
|
+
const where = `${t.export} (${t.name}): column ${c.name} CODEC(${c.codec})`;
|
|
31
|
+
const expected = spec.parameters;
|
|
32
|
+
if (args.length > expected.length) {
|
|
33
|
+
const takes = expected.length === 0 ? "takes no parameters" : `takes at most ${expected.length} (${expected.map((p) => p.name).join(", ")})`;
|
|
34
|
+
report({ severity: "error", message: `${where}: ${name} ${takes}, and is given ${args.length}`, entity: t.export });
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
args.forEach((arg, i) => {
|
|
38
|
+
const p = expected[i]!;
|
|
39
|
+
const text = arg.trim();
|
|
40
|
+
if (p.values && !p.values.some((v) => v.toLowerCase() === text.toLowerCase())) {
|
|
41
|
+
report({ severity: "error", message: `${where}: ${name} ${p.name} ${text} is not one of ${p.values.join(", ")}`, entity: t.export });
|
|
42
|
+
} else if (p.range && NUMBER.test(text)) {
|
|
43
|
+
const n = Number(text);
|
|
44
|
+
if (n < p.range[0] || n > p.range[1]) {
|
|
45
|
+
report({
|
|
46
|
+
severity: "error",
|
|
47
|
+
message: `${where}: ${name} ${p.name} ${text} is outside ${p.range[0]} to ${p.range[1]}, the range ClickHouse ${CLICKHOUSE_VERSION} takes`,
|
|
48
|
+
entity: t.export,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { bareName, checkOf, splitTop } from "./clickhouse-helpers";
|
|
2
|
+
import { clickhouseObjects, type OutputObject } from "./sql-helpers";
|
|
3
|
+
|
|
4
|
+
/** The columns a grant can name on an object: each declared column, and `n.a` for a `Nested` column's fields. */
|
|
5
|
+
function grantableColumns(o: OutputObject): Set<string> | undefined {
|
|
6
|
+
const columns = (o.columns ?? []) as Array<{ name: string; type?: string }>;
|
|
7
|
+
if (columns.length === 0) return undefined;
|
|
8
|
+
const out = new Set<string>();
|
|
9
|
+
for (const c of columns) {
|
|
10
|
+
out.add(c.name);
|
|
11
|
+
const nested = /^Nested\s*\(([\s\S]*)\)$/.exec(c.type?.trim() ?? "");
|
|
12
|
+
if (nested) {
|
|
13
|
+
for (const field of splitTop(nested[1]!)) {
|
|
14
|
+
const name = /^(`(?:[^`]|``)*`|[A-Za-z_]\w*)/.exec(field.trim())?.[1];
|
|
15
|
+
if (name) out.add(`${c.name}.${bareName(name) ?? name}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* SQLCH126: a `GRANT` column list names a column the `${}` table does not
|
|
24
|
+
* declare: `GRANT SELECT(kindd) ON ${events}`. The server refuses the grant
|
|
25
|
+
* ("There is no column ...").
|
|
26
|
+
*
|
|
27
|
+
* Only a target the grant interpolates is checked, against the columns it
|
|
28
|
+
* declares; a target written as a plain name, or a view that infers its
|
|
29
|
+
* columns from its SELECT, is left to the server. ClickHouse has no declared
|
|
30
|
+
* `REVOKE`, so a grant is the only column list.
|
|
31
|
+
*/
|
|
32
|
+
export const sqlch126 = checkOf({ id: "SQLCH126", description: "A GRANT column list names a column the table does not declare" }, (ctx, report) => {
|
|
33
|
+
const objects = clickhouseObjects(ctx);
|
|
34
|
+
const byExport = new Map(objects.map((o) => [o.export, o]));
|
|
35
|
+
for (const g of objects) {
|
|
36
|
+
if (g.type !== "ClickHouse::Grant" || typeof g.on !== "string") continue;
|
|
37
|
+
const target = ((g.dependsOn ?? []) as string[])
|
|
38
|
+
.map((e) => byExport.get(e))
|
|
39
|
+
.find((o) => o !== undefined && (o.sqlName === g.on || (o.database ? `${o.database}.${o.name}` : o.name) === g.on));
|
|
40
|
+
if (!target) continue;
|
|
41
|
+
const columns = grantableColumns(target);
|
|
42
|
+
if (!columns) continue;
|
|
43
|
+
for (const privilege of (g.privileges ?? []) as string[]) {
|
|
44
|
+
const m = /^([A-Za-z_][A-Za-z_ ]*?)\s*\(([\s\S]*)\)\s*$/.exec(privilege.trim());
|
|
45
|
+
if (!m) continue;
|
|
46
|
+
for (const element of splitTop(m[2]!)) {
|
|
47
|
+
const name = bareName(element);
|
|
48
|
+
if (name === undefined || columns.has(name)) continue;
|
|
49
|
+
report({
|
|
50
|
+
severity: "error",
|
|
51
|
+
message: `${g.export}: GRANT ${m[1]!.toUpperCase()}(${name}) ON ${g.on} names column ${name}, which ${target.export} (${target.name}) does not declare`,
|
|
52
|
+
entity: g.export,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { AGGREGATE_COMBINATORS, CLICKHOUSE_VERSION, FUNCTIONS, TABLE_FUNCTIONS } from "../../generated/clickhouse";
|
|
2
|
+
import type { FunctionSpec } from "../../clickhouse/catalog-types";
|
|
3
|
+
import { isTrivia, tokenizeText, type Token } from "../../clickhouse/tokens";
|
|
4
|
+
import { checkOf, isTable } from "./clickhouse-helpers";
|
|
5
|
+
import { clickhouseObjects } from "./sql-helpers";
|
|
6
|
+
|
|
7
|
+
const BY_NAME = FUNCTIONS as Readonly<Record<string, FunctionSpec | undefined>>;
|
|
8
|
+
const FOLDED = new Map<string, FunctionSpec>();
|
|
9
|
+
for (const [name, spec] of Object.entries(BY_NAME)) if (spec?.caseInsensitive) FOLDED.set(name.toLowerCase(), spec);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Words followed by `(` that are not function calls: SQL keywords that take
|
|
13
|
+
* parentheses, and the names ClickHouse's parser reads with a syntax of their
|
|
14
|
+
* own (`EXTRACT(DAY FROM d)`, `TRIM(BOTH ' ' FROM s)`, `DATE_ADD(...)`), in
|
|
15
|
+
* any case.
|
|
16
|
+
*/
|
|
17
|
+
const NOT_CALLS = new Set(
|
|
18
|
+
(
|
|
19
|
+
"and or not in is as on by to like ilike between case when then else end any all some global exists array interval " +
|
|
20
|
+
"extract substring position trim ltrim rtrim " +
|
|
21
|
+
"dateadd date_add timestamp_add timestampadd datesub date_sub timestamp_sub timestampsub datediff date_diff timestampdiff timestamp_diff " +
|
|
22
|
+
"grouping over filter where set delete group with select from values distinct using asc desc nulls"
|
|
23
|
+
).split(" "),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
/** `ZSTD(3)` after `CODEC` in a TTL `RECOMPRESS` is a codec, not a call. */
|
|
27
|
+
const SKIPS_GROUP = new Set(["codec"]);
|
|
28
|
+
|
|
29
|
+
const lookup = (name: string): FunctionSpec | undefined => BY_NAME[name] ?? FOLDED.get(name.toLowerCase());
|
|
30
|
+
|
|
31
|
+
const TABLE_FUNCTION_NAMES = new Set(TABLE_FUNCTIONS);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A catalog function, or an aggregate function with combinator suffixes
|
|
35
|
+
* (`countIf`, `sumState`, `uniqMerge`). A table function (`numbers`) counts
|
|
36
|
+
* too: it is a name the server has, and where it does not belong is the
|
|
37
|
+
* server's to say.
|
|
38
|
+
*/
|
|
39
|
+
export function isKnownFunction(name: string): boolean {
|
|
40
|
+
if (lookup(name) || TABLE_FUNCTION_NAMES.has(name)) return true;
|
|
41
|
+
// Strip combinators from the end, longest first, and look for an aggregate underneath.
|
|
42
|
+
const suffixes = [...AGGREGATE_COMBINATORS].sort((a, b) => b.length - a.length);
|
|
43
|
+
const seen = new Set<string>();
|
|
44
|
+
const queue = [name];
|
|
45
|
+
while (queue.length > 0) {
|
|
46
|
+
const base = queue.pop()!;
|
|
47
|
+
for (const s of suffixes) {
|
|
48
|
+
if (base.length <= s.length || !base.endsWith(s)) continue;
|
|
49
|
+
const rest = base.slice(0, -s.length);
|
|
50
|
+
if (seen.has(rest)) continue;
|
|
51
|
+
seen.add(rest);
|
|
52
|
+
const spec = lookup(rest);
|
|
53
|
+
const aggregate = spec && (spec.aggregate || (spec.aliasOf !== undefined && lookup(spec.aliasOf)?.aggregate));
|
|
54
|
+
if (aggregate) return true;
|
|
55
|
+
queue.push(rest);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The names an expression calls: an identifier directly followed by `(`, outside strings and quoted names. */
|
|
62
|
+
export function calledNames(expr: string): string[] {
|
|
63
|
+
let tokens: Token[];
|
|
64
|
+
try {
|
|
65
|
+
tokens = tokenizeText(expr, 0).filter((t) => !isTrivia(t));
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
const out: string[] = [];
|
|
70
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
71
|
+
const t = tokens[i]!;
|
|
72
|
+
const next = tokens[i + 1];
|
|
73
|
+
if (t.kind !== "ident" || next?.kind !== "punct" || next.text !== "(") continue;
|
|
74
|
+
const prev = tokens[i - 1];
|
|
75
|
+
if (prev?.kind === "punct" && prev.text === ".") continue;
|
|
76
|
+
const lower = t.text.toLowerCase();
|
|
77
|
+
if (SKIPS_GROUP.has(lower)) {
|
|
78
|
+
let depth = 0;
|
|
79
|
+
for (i = i + 1; i < tokens.length; i++) {
|
|
80
|
+
const x = tokens[i]!;
|
|
81
|
+
if (x.kind === "punct" && x.text === "(") depth++;
|
|
82
|
+
else if (x.kind === "punct" && x.text === ")" && --depth === 0) break;
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (NOT_CALLS.has(lower)) continue;
|
|
87
|
+
out.push(t.text);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* SQLCH127: an expression the table declares calls a function the pinned
|
|
94
|
+
* server does not have: `DEFAULT noww()`, `PARTITION BY toYYYYMMM(at)`.
|
|
95
|
+
*
|
|
96
|
+
* Read in each column's `DEFAULT`, `MATERIALIZED`, `ALIAS`, `EPHEMERAL` and
|
|
97
|
+
* TTL expression, in `ORDER BY`, `PRIMARY KEY`, `PARTITION BY`, `SAMPLE BY`
|
|
98
|
+
* and the table TTL, and in each skip index expression. A name counts when it
|
|
99
|
+
* is in `system.functions` (case as the catalog says, aliases included), is an
|
|
100
|
+
* aggregate function with combinator suffixes, or is a function the project
|
|
101
|
+
* declares with `func`.
|
|
102
|
+
*/
|
|
103
|
+
export const sqlch127 = checkOf({ id: "SQLCH127", description: "An expression calls a function the pinned server does not have" }, (ctx, report) => {
|
|
104
|
+
const objects = clickhouseObjects(ctx);
|
|
105
|
+
const declared = new Set(objects.filter((o) => o.type === "ClickHouse::Function").map((o) => o.name));
|
|
106
|
+
for (const t of objects.filter(isTable)) {
|
|
107
|
+
const places: Array<[string, string | undefined]> = [];
|
|
108
|
+
for (const c of t.columns) {
|
|
109
|
+
if (c.default?.expr) places.push([`column ${c.name} ${c.default.kind}`, c.default.expr]);
|
|
110
|
+
if (c.ttl) places.push([`column ${c.name} TTL`, c.ttl]);
|
|
111
|
+
}
|
|
112
|
+
places.push(["ORDER BY", t.orderBy], ["PRIMARY KEY", t.primaryKey], ["PARTITION BY", t.partitionBy], ["SAMPLE BY", t.sampleBy], ["TTL", t.ttl]);
|
|
113
|
+
for (const ix of t.indexes ?? []) places.push([`index ${ix.name}`, ix.expr]);
|
|
114
|
+
for (const [where, expr] of places) {
|
|
115
|
+
if (!expr) continue;
|
|
116
|
+
const reported = new Set<string>();
|
|
117
|
+
for (const name of calledNames(expr)) {
|
|
118
|
+
if (reported.has(name) || declared.has(name) || isKnownFunction(name)) continue;
|
|
119
|
+
reported.add(name);
|
|
120
|
+
report({
|
|
121
|
+
severity: "error",
|
|
122
|
+
message: `${t.export} (${t.name}): ${where} calls ${name}(), which ClickHouse ${CLICKHOUSE_VERSION} does not have and the project does not declare with func`,
|
|
123
|
+
entity: t.export,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { checkOf, tablesOf } from "./postgres-helpers";
|
|
2
|
+
import { parseType, projectOf, resolveType, typeLabel } from "./postgres-names";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SQLPG119: a column, array element, domain base or sequence `AS` type the
|
|
6
|
+
* target major does not have.
|
|
7
|
+
*
|
|
8
|
+
* Doc: https://www.postgresql.org/docs/18/datatype.html. The server refuses
|
|
9
|
+
* `type "numerik" does not exist` at CREATE. A built-in type counts in every
|
|
10
|
+
* spelling Postgres accepts: the catalog's `pg_type` name and `format_type()`
|
|
11
|
+
* name (`int4`, `integer`), and the grammar's own spellings (`int`, `decimal`,
|
|
12
|
+
* `double precision`, `time with time zone`), an overlay beside the catalog.
|
|
13
|
+
* So does a type the project declares (`type`, `domain`, a table's row type),
|
|
14
|
+
* by `${}` or by name. A type qualified with a schema the project does not
|
|
15
|
+
* declare is a plain-text reference and left to the server. An unknown name is
|
|
16
|
+
* a warning when the project declares an extension, since extension types are
|
|
17
|
+
* not in the core catalog; the overlay of common extension types answers for
|
|
18
|
+
* the ones it knows.
|
|
19
|
+
*/
|
|
20
|
+
export const sqlpg119 = checkOf({ id: "SQLPG119", description: "A column, domain or sequence type the target major does not have" }, (ctx, report) => {
|
|
21
|
+
const p = projectOf(ctx);
|
|
22
|
+
const check = (entity: string, sqlName: string, what: string, text: string | undefined, serial: boolean) => {
|
|
23
|
+
const t = parseType(text);
|
|
24
|
+
if (!t) return;
|
|
25
|
+
const r = resolveType(p, t, { serial });
|
|
26
|
+
if (r.kind !== "missing") return;
|
|
27
|
+
const kind = t.array ? "array element type" : "type";
|
|
28
|
+
report({ severity: r.miss.severity, message: `${entity} (${sqlName}) ${what} ${kind} ${typeLabel(t)}, which Postgres ${p.major} does not have${r.miss.note}`, entity });
|
|
29
|
+
};
|
|
30
|
+
for (const t of tablesOf(ctx)) {
|
|
31
|
+
for (const c of t.columns) check(t.export, t.sqlName, `column ${c.name} has`, c.type, true);
|
|
32
|
+
}
|
|
33
|
+
for (const o of p.objects) {
|
|
34
|
+
if (o.type === "Postgres::Domain") check(o.export, o.sqlName as string, "is a domain over", o.dataType as string | undefined, false);
|
|
35
|
+
if (o.type === "Postgres::Sequence") check(o.export, o.sqlName as string, "is a sequence of", o.dataType as string | undefined, false);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { checkOf, tablesOf } from "./postgres-helpers";
|
|
2
|
+
import { parametersOf, parseType, projectOf, resolveType, typeLabel, type ParsedType, type PgProject } from "./postgres-names";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SQLPG120: a type modifier that does not fit the type.
|
|
6
|
+
*
|
|
7
|
+
* Doc: https://www.postgresql.org/docs/18/datatype-numeric.html,
|
|
8
|
+
* https://www.postgresql.org/docs/18/datatype-character.html,
|
|
9
|
+
* https://www.postgresql.org/docs/18/datatype-datetime.html,
|
|
10
|
+
* https://www.postgresql.org/docs/18/datatype-bit.html. The server refuses a
|
|
11
|
+
* modifier on a type that takes none (`bigint(8)`), `numeric` precision
|
|
12
|
+
* outside 1 to 1000 or scale outside -1000 to 1000 (0 to the precision before
|
|
13
|
+
* 15), `varchar(n)` or `char(n)` outside 1 to 10485760, `timestamp`, `time`
|
|
14
|
+
* or `interval` precision outside 0 to 6, `bit(n)` below 1, and `float(p)`
|
|
15
|
+
* outside 1 to 53. The parameters and ranges are the lexicon's type overlay.
|
|
16
|
+
* Only built-in types are checked: a declared or extension type's modifiers
|
|
17
|
+
* are its own (`geometry(Point, 4326)`).
|
|
18
|
+
*/
|
|
19
|
+
export const sqlpg120 = checkOf({ id: "SQLPG120", description: "A type modifier that does not fit the type" }, (ctx, report) => {
|
|
20
|
+
const p = projectOf(ctx);
|
|
21
|
+
const check = (entity: string, sqlName: string, what: string, text: string | undefined, serial: boolean) => {
|
|
22
|
+
const t = parseType(text);
|
|
23
|
+
if (!t) return;
|
|
24
|
+
const problem = modifierProblem(p, t, serial);
|
|
25
|
+
if (problem) report({ severity: "error", message: `${entity} (${sqlName}) ${what} ${text!.trim()}: ${problem}`, entity });
|
|
26
|
+
};
|
|
27
|
+
for (const t of tablesOf(ctx)) for (const c of t.columns) check(t.export, t.sqlName, `column ${c.name} has type`, c.type, true);
|
|
28
|
+
for (const o of p.objects) {
|
|
29
|
+
if (o.type === "Postgres::Domain") check(o.export, o.sqlName as string, "is a domain over", o.dataType as string | undefined, false);
|
|
30
|
+
if (o.type === "Postgres::Sequence") check(o.export, o.sqlName as string, "is a sequence of", o.dataType as string | undefined, false);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const int = (s: string): number | undefined => (/^[+-]?\d+$/.test(s.trim()) ? Number(s.trim()) : undefined);
|
|
35
|
+
|
|
36
|
+
function modifierProblem(p: PgProject, t: ParsedType, serial: boolean): string | undefined {
|
|
37
|
+
if (t.schema !== undefined || t.quoted) return undefined;
|
|
38
|
+
const mods = t.modifiers;
|
|
39
|
+
if (t.name === "float" && mods) {
|
|
40
|
+
const n = mods.length === 1 ? int(mods[0]!) : undefined;
|
|
41
|
+
if (mods.length !== 1) return "float takes one precision";
|
|
42
|
+
if (n !== undefined && (n < 1 || n > 53)) return `float precision ${n} is outside 1 to 53`;
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const r = resolveType(p, t, { serial });
|
|
46
|
+
if (r.kind !== "builtin") return undefined;
|
|
47
|
+
const params = parametersOf(r.canonical);
|
|
48
|
+
if (t.fields !== undefined) {
|
|
49
|
+
const fields = params.find((a) => a.kind === "keyword")?.values ?? [];
|
|
50
|
+
if (!fields.includes(t.fields)) return `${t.fields} is not an interval field (${fields.join(", ")})`;
|
|
51
|
+
}
|
|
52
|
+
if (!mods) return undefined;
|
|
53
|
+
const numbers = params.filter((a) => a.kind === "number");
|
|
54
|
+
if (numbers.length === 0) return `${typeLabel(t)} takes no type modifier`;
|
|
55
|
+
if (mods.length > numbers.length) return `${typeLabel(t)} takes at most ${numbers.length} modifier${numbers.length === 1 ? "" : "s"}`;
|
|
56
|
+
const values = mods.map(int);
|
|
57
|
+
for (let i = 0; i < mods.length; i++) {
|
|
58
|
+
const v = values[i];
|
|
59
|
+
const spec = numbers[i]!;
|
|
60
|
+
if (v === undefined || !spec.range) continue;
|
|
61
|
+
let [lo, hi] = spec.range;
|
|
62
|
+
// numeric's scale: 0 to the precision before 15, -1000 to 1000 since.
|
|
63
|
+
if (r.canonical === "numeric" && spec.name === "scale" && p.major < 15) {
|
|
64
|
+
lo = 0;
|
|
65
|
+
hi = values[0] ?? hi;
|
|
66
|
+
}
|
|
67
|
+
if (v < lo || v > hi) return `${spec.name} ${v} is outside ${lo} to ${hi}${r.canonical === "numeric" && spec.name === "scale" && p.major < 15 ? ` at Postgres ${p.major}` : ""}`;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { checkOf, tablesOf } from "./postgres-helpers";
|
|
2
|
+
import { parseType, projectOf, resolveType } from "./postgres-names";
|
|
3
|
+
|
|
4
|
+
const IDENTITY_TYPES = new Set(["smallint", "integer", "bigint"]);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SQLPG121: an identity column whose type is not `smallint`, `integer` or
|
|
8
|
+
* `bigint`.
|
|
9
|
+
*
|
|
10
|
+
* Doc: https://www.postgresql.org/docs/18/sql-createtable.html ("GENERATED {
|
|
11
|
+
* ALWAYS | BY DEFAULT } AS IDENTITY ... The column ... must be of type
|
|
12
|
+
* smallint, integer or bigint"). The server refuses `identity column type must
|
|
13
|
+
* be smallint, integer, or bigint` at CREATE. A declared type (a domain) is
|
|
14
|
+
* left to the server.
|
|
15
|
+
*/
|
|
16
|
+
export const sqlpg121 = checkOf({ id: "SQLPG121", description: "An identity column whose type is not smallint, integer or bigint" }, (ctx, report) => {
|
|
17
|
+
const p = projectOf(ctx);
|
|
18
|
+
for (const t of tablesOf(ctx)) {
|
|
19
|
+
for (const c of t.columns) {
|
|
20
|
+
if (c.generated?.kind !== "identity") continue;
|
|
21
|
+
const parsed = parseType(c.type);
|
|
22
|
+
if (!parsed) continue;
|
|
23
|
+
const r = resolveType(p, parsed, { serial: true });
|
|
24
|
+
if (r.kind !== "builtin" || (IDENTITY_TYPES.has(r.canonical) && !parsed.array)) continue;
|
|
25
|
+
report({
|
|
26
|
+
severity: "error",
|
|
27
|
+
message: `${t.export} (${t.sqlName}) column ${c.name} is an identity column of type ${c.type}; an identity column must be smallint, integer or bigint`,
|
|
28
|
+
entity: t.export,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { checkOf, tablesOf } from "./postgres-helpers";
|
|
2
|
+
import { postgresObjects } from "./sql-helpers";
|
|
3
|
+
|
|
4
|
+
const repeated = (xs: readonly string[]): string[] => [...new Set(xs.filter((x, i) => xs.indexOf(x) !== i))];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SQLPG122: a table that declares a column twice, or an enum that lists a
|
|
8
|
+
* label twice.
|
|
9
|
+
*
|
|
10
|
+
* Doc: https://www.postgresql.org/docs/18/sql-createtable.html and
|
|
11
|
+
* https://www.postgresql.org/docs/18/sql-createtype.html. The server refuses
|
|
12
|
+
* `column "id" specified more than once` and `enum labels must be unique`.
|
|
13
|
+
* Column names are compared as Postgres folds them; labels as written, since
|
|
14
|
+
* they are case-sensitive strings.
|
|
15
|
+
*/
|
|
16
|
+
export const sqlpg122 = checkOf({ id: "SQLPG122", description: "A table declares a column twice, or an enum lists a label twice" }, (ctx, report) => {
|
|
17
|
+
for (const t of tablesOf(ctx)) {
|
|
18
|
+
for (const name of repeated(t.columns.map((c) => c.name))) {
|
|
19
|
+
report({ severity: "error", message: `${t.export} (${t.sqlName}) declares the column ${name} more than once`, entity: t.export });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
for (const o of postgresObjects(ctx)) {
|
|
23
|
+
if (o.type !== "Postgres::Enum") continue;
|
|
24
|
+
for (const label of repeated((o.labels as string[] | undefined) ?? [])) {
|
|
25
|
+
report({ severity: "error", message: `${o.export} (${o.sqlName as string}) lists the enum label '${label}' more than once`, entity: o.export });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { checkOf, indexesOf, tablesOf } from "./postgres-helpers";
|
|
2
|
+
import { columnsOf, includeColumns, interpolatedTable, projectOf } from "./postgres-names";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SQLPG123: a bare column name in a key, index or grant column list that the
|
|
6
|
+
* table does not declare.
|
|
7
|
+
*
|
|
8
|
+
* Doc: https://www.postgresql.org/docs/18/sql-createtable.html,
|
|
9
|
+
* https://www.postgresql.org/docs/18/sql-createindex.html,
|
|
10
|
+
* https://www.postgresql.org/docs/18/sql-grant.html. The server refuses
|
|
11
|
+
* `column "idd" named in key does not exist` (or `column "emial" does not
|
|
12
|
+
* exist`) at CREATE or GRANT. Checked: `PRIMARY KEY`, `UNIQUE` and their
|
|
13
|
+
* `INCLUDE`, a foreign key's own columns, an index's bare columns and
|
|
14
|
+
* `INCLUDE` on a `${}` table, and a `GRANT`/`REVOKE` column list on a `${}`
|
|
15
|
+
* table. An expression (an index expression, a CHECK, a generated column) is
|
|
16
|
+
* left to the server, as is a table whose columns come from somewhere the
|
|
17
|
+
* build cannot see (`LIKE`, `OF type`, a plain-text parent).
|
|
18
|
+
*/
|
|
19
|
+
export const sqlpg123 = checkOf({ id: "SQLPG123", description: "A key, index or grant column list names a column the table does not declare" }, (ctx, report) => {
|
|
20
|
+
const p = projectOf(ctx);
|
|
21
|
+
const verify = (entity: string, sqlName: string, where: string, tableName: string, names: readonly string[], cols: ReadonlyMap<string, unknown>) => {
|
|
22
|
+
for (const n of new Set(names)) {
|
|
23
|
+
if (cols.has(n)) continue;
|
|
24
|
+
report({ severity: "error", message: `${entity} (${sqlName}) ${where} names the column ${n}, which ${tableName} does not declare`, entity });
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
for (const t of tablesOf(ctx)) {
|
|
28
|
+
const cols = columnsOf(p, t);
|
|
29
|
+
if (!cols) continue;
|
|
30
|
+
if (t.primaryKey) {
|
|
31
|
+
verify(t.export, t.sqlName, "PRIMARY KEY", t.sqlName, t.primaryKey.columns, cols);
|
|
32
|
+
verify(t.export, t.sqlName, "PRIMARY KEY ... INCLUDE", t.sqlName, includeColumns(t.primaryKey.include), cols);
|
|
33
|
+
}
|
|
34
|
+
for (const u of t.uniques) {
|
|
35
|
+
verify(t.export, t.sqlName, "UNIQUE", t.sqlName, u.columns, cols);
|
|
36
|
+
verify(t.export, t.sqlName, "UNIQUE ... INCLUDE", t.sqlName, includeColumns(u.include), cols);
|
|
37
|
+
}
|
|
38
|
+
for (const fk of t.foreignKeys) verify(t.export, t.sqlName, `FOREIGN KEY to ${fk.refTable}`, t.sqlName, fk.columns, cols);
|
|
39
|
+
}
|
|
40
|
+
for (const i of indexesOf(ctx)) {
|
|
41
|
+
const t = interpolatedTable(p, i.table, i.tableName);
|
|
42
|
+
const cols = t && columnsOf(p, t);
|
|
43
|
+
if (!cols) continue;
|
|
44
|
+
verify(i.export, i.sqlName, `index on ${i.tableName}`, i.tableName, i.elements.flatMap((e) => (e.column ? [e.column] : [])), cols);
|
|
45
|
+
verify(i.export, i.sqlName, `index on ${i.tableName} INCLUDE`, i.tableName, includeColumns(i.include), cols);
|
|
46
|
+
}
|
|
47
|
+
for (const g of p.objects) {
|
|
48
|
+
if (g.type !== "Postgres::Grant" || g.on !== "table") continue;
|
|
49
|
+
const objects = (g.objects as unknown[]) ?? [];
|
|
50
|
+
const names = (g.objectNames as string[]) ?? [];
|
|
51
|
+
const privileges = (g.privileges as Array<{ privilege: string; columns?: string[] }>) ?? [];
|
|
52
|
+
objects.forEach((ref, n) => {
|
|
53
|
+
const t = interpolatedTable(p, ref, names[n]);
|
|
54
|
+
const cols = t && columnsOf(p, t);
|
|
55
|
+
if (!cols) return;
|
|
56
|
+
for (const priv of privileges) {
|
|
57
|
+
if (!priv.columns?.length) continue;
|
|
58
|
+
const action = g.action === "revoke" ? "REVOKE" : "GRANT";
|
|
59
|
+
verify(g.export, String(g.name), `${action} ${priv.privilege.toUpperCase()} on ${t!.sqlName}`, t!.sqlName, priv.columns, cols);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { checkOf, tablesOf } from "./postgres-helpers";
|
|
2
|
+
import { columnsOf, interpolatedTable, projectOf, typeIdentity } from "./postgres-names";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SQLPG124: a foreign key to a `${}` table that names a column the table does
|
|
6
|
+
* not declare, or pairs columns whose types do not compare.
|
|
7
|
+
*
|
|
8
|
+
* Doc: https://www.postgresql.org/docs/18/ddl-constraints.html#DDL-CONSTRAINTS-FK
|
|
9
|
+
* and https://www.postgresql.org/docs/18/catalog-pg-type.html#CATALOG-TYPCATEGORY-TABLE.
|
|
10
|
+
* The server refuses `column "idd" referenced in foreign key constraint does
|
|
11
|
+
* not exist` and `Key columns "customer_id" and "id" are of incompatible
|
|
12
|
+
* types: text and bigint`. Types compare by their catalog category: `integer`
|
|
13
|
+
* to `bigint` (both numeric) passes, `text` to `bigint` fails. The
|
|
14
|
+
* user-defined category (`uuid`, `bytea`, `jsonb`...) compares by type; a
|
|
15
|
+
* domain by its base type; an enum by itself. A key to a table named in plain
|
|
16
|
+
* text, or a type outside the project, is left to the server.
|
|
17
|
+
*/
|
|
18
|
+
export const sqlpg124 = checkOf({ id: "SQLPG124", description: "A foreign key names a column the referenced table lacks, or one of another type category" }, (ctx, report) => {
|
|
19
|
+
const p = projectOf(ctx);
|
|
20
|
+
for (const t of tablesOf(ctx)) {
|
|
21
|
+
const own = columnsOf(p, t);
|
|
22
|
+
for (const fk of t.foreignKeys) {
|
|
23
|
+
const ref = interpolatedTable(p, (fk as { references?: unknown }).references, fk.refTable);
|
|
24
|
+
const refCols = ref && columnsOf(p, ref);
|
|
25
|
+
if (!ref || !refCols) continue;
|
|
26
|
+
const targets = fk.refColumns.length > 0 ? fk.refColumns : (ref.primaryKey?.columns ?? []);
|
|
27
|
+
const missing = targets.filter((c) => !refCols.has(c));
|
|
28
|
+
for (const c of missing) {
|
|
29
|
+
report({ severity: "error", message: `${t.export} (${t.sqlName}) has a foreign key on (${fk.columns.join(", ")}) to ${ref.sqlName} (${c}), which ${ref.sqlName} does not declare`, entity: t.export });
|
|
30
|
+
}
|
|
31
|
+
if (missing.length > 0 || !own || targets.length !== fk.columns.length) continue;
|
|
32
|
+
fk.columns.forEach((c, n) => {
|
|
33
|
+
const mine = own.get(c);
|
|
34
|
+
const theirs = refCols.get(targets[n]!);
|
|
35
|
+
if (!mine || !theirs) return;
|
|
36
|
+
const a = typeIdentity(p, mine.type);
|
|
37
|
+
const b = typeIdentity(p, theirs.type);
|
|
38
|
+
if (a === undefined || b === undefined || a === b) return;
|
|
39
|
+
report({
|
|
40
|
+
severity: "error",
|
|
41
|
+
message: `${t.export} (${t.sqlName}) has a foreign key from ${c} (${mine.type}) to ${ref.sqlName} (${targets[n]}) (${theirs.type}); the types do not compare, so Postgres refuses the key`,
|
|
42
|
+
entity: t.export,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { checkOf, indexesOf } from "./postgres-helpers";
|
|
2
|
+
import { parseIndexElement, projectOf, resolveMethod, resolveOpClass } from "./postgres-names";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SQLPG125: an index access method or operator class the target major does
|
|
6
|
+
* not have, or an operator class that is not for the index's method.
|
|
7
|
+
*
|
|
8
|
+
* Doc: https://www.postgresql.org/docs/18/sql-createindex.html and
|
|
9
|
+
* https://www.postgresql.org/docs/18/indexes-opclass.html. The server refuses
|
|
10
|
+
* `access method "btreee" does not exist` and `operator class "numeric_opz"
|
|
11
|
+
* does not exist for access method "btree"` at CREATE. Methods and operator
|
|
12
|
+
* classes come from the target major's catalog (`pg_am`, `pg_opclass`). The
|
|
13
|
+
* ones an extension adds (`gin_trgm_ops` from pg_trgm, `hnsw` from vector)
|
|
14
|
+
* pass when the project declares that extension and are an error naming it
|
|
15
|
+
* when it does not; one neither the catalog nor the extension overlay knows is
|
|
16
|
+
* a warning when the project declares any extension. An operator class
|
|
17
|
+
* qualified with a schema the project does not declare is left alone.
|
|
18
|
+
*/
|
|
19
|
+
export const sqlpg125 = checkOf({ id: "SQLPG125", description: "An index access method or operator class the target major does not have, or not for that method" }, (ctx, report) => {
|
|
20
|
+
const p = projectOf(ctx);
|
|
21
|
+
for (const i of indexesOf(ctx)) {
|
|
22
|
+
const method = i.method ?? "btree";
|
|
23
|
+
const m = resolveMethod(p, method);
|
|
24
|
+
if (!m.ok) {
|
|
25
|
+
report({ severity: m.miss.severity, message: `${i.export} (${i.sqlName}) uses the index access method ${method}, which Postgres ${p.major} does not have${m.miss.note}`, entity: i.export });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
for (const e of i.elements) {
|
|
29
|
+
const opclass = parseIndexElement(e.expr)?.opclass;
|
|
30
|
+
if (!opclass) continue;
|
|
31
|
+
const r = resolveOpClass(p, method, opclass);
|
|
32
|
+
if (r.ok) continue;
|
|
33
|
+
const label = `${opclass.schema ? `${opclass.schema}.` : ""}${opclass.name}`;
|
|
34
|
+
const message = r.otherMethods
|
|
35
|
+
? `${i.export} (${i.sqlName}) uses the operator class ${label}, which is for ${r.otherMethods.join(", ")}, not ${method}`
|
|
36
|
+
: `${i.export} (${i.sqlName}) uses the operator class ${label}, which Postgres ${p.major} does not have for ${method}${r.miss.note}`;
|
|
37
|
+
report({ severity: r.miss.severity, message, entity: i.export });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { checkOf } from "./postgres-helpers";
|
|
2
|
+
import { callsIn, expressionsOf, projectOf, resolveCall } from "./postgres-names";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SQLPG126: a call to a function the target major does not have, in a column
|
|
6
|
+
* `DEFAULT`, a generated column, a `CHECK` (a table's or a domain's), or an
|
|
7
|
+
* index expression.
|
|
8
|
+
*
|
|
9
|
+
* Doc: https://www.postgresql.org/docs/18/functions.html. The server refuses
|
|
10
|
+
* `function noww() does not exist` at CREATE. A name counts when it is in
|
|
11
|
+
* the target major's `pg_proc`, a function or procedure the project declares,
|
|
12
|
+
* a type name used as a cast (`int4(x)`), or qualified with a schema the
|
|
13
|
+
* project does not declare. Only a name directly followed by `(`, outside
|
|
14
|
+
* string literals, is a call; the grammar's own constructs that read like
|
|
15
|
+
* calls (`COALESCE`, `NULLIF`, `GREATEST`, `LEAST`, `CAST`, `EXTRACT`, `ROW`,
|
|
16
|
+
* `ARRAY`, `EXISTS`, `IN`, `ANY`, `ALL`, `SOME`, and the key word forms of
|
|
17
|
+
* `OVERLAY`, `POSITION`, `SUBSTRING` and `TRIM`) are reserved or column-name
|
|
18
|
+
* key words, not `pg_proc` names, and are skipped by their key word category.
|
|
19
|
+
* An error, or a warning when the project declares an extension.
|
|
20
|
+
*/
|
|
21
|
+
export const sqlpg126 = checkOf({ id: "SQLPG126", description: "An expression calls a function the target major does not have" }, (ctx, report) => {
|
|
22
|
+
const p = projectOf(ctx);
|
|
23
|
+
for (const o of p.objects) {
|
|
24
|
+
const seen = new Set<string>();
|
|
25
|
+
for (const { where, expr } of expressionsOf(o)) {
|
|
26
|
+
for (const c of callsIn(expr)) {
|
|
27
|
+
const label = `${c.schema ? `${c.schema}.` : ""}${c.name}`;
|
|
28
|
+
if (seen.has(`${where}\0${label}`)) continue;
|
|
29
|
+
seen.add(`${where}\0${label}`);
|
|
30
|
+
const r = resolveCall(p, c);
|
|
31
|
+
if (r.ok) continue;
|
|
32
|
+
const why = r.since !== undefined ? `, which needs Postgres ${r.since}; Postgres ${p.major} does not have it` : r.until !== undefined ? `, which Postgres removed after ${r.until}` : `, which Postgres ${p.major} does not have`;
|
|
33
|
+
report({ severity: r.miss.severity, message: `${o.export} (${o.sqlName as string}) ${where} calls ${label}()${why}${r.miss.note}`, entity: o.export });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
package/src/lint/rules/index.ts
CHANGED
|
@@ -2,12 +2,14 @@ import type { LintRule } from "@intentius/chant/lint/rule";
|
|
|
2
2
|
import { sqlch001 } from "./sqlch001";
|
|
3
3
|
import { sqlch002 } from "./sqlch002";
|
|
4
4
|
import { sqlch003 } from "./sqlch003";
|
|
5
|
+
import { sqlch004 } from "./sqlch004";
|
|
5
6
|
import { sqlpg001 } from "./sqlpg001";
|
|
6
7
|
import { sqlpg002 } from "./sqlpg002";
|
|
7
8
|
import { sqlpg003 } from "./sqlpg003";
|
|
8
9
|
import { sqlpg004 } from "./sqlpg004";
|
|
10
|
+
import { sqlpg005 } from "./sqlpg005";
|
|
9
11
|
|
|
10
|
-
export { sqlch001, sqlch002, sqlch003, sqlpg001, sqlpg002, sqlpg003, sqlpg004 };
|
|
12
|
+
export { sqlch001, sqlch002, sqlch003, sqlch004, sqlpg001, sqlpg002, sqlpg003, sqlpg004, sqlpg005 };
|
|
11
13
|
|
|
12
14
|
/** The lexicon's source-level lint rules, returned by `lintRules()`. */
|
|
13
|
-
export const rules: LintRule[] = [sqlch001, sqlch002, sqlch003, sqlpg001, sqlpg002, sqlpg003, sqlpg004];
|
|
15
|
+
export const rules: LintRule[] = [sqlch001, sqlch002, sqlch003, sqlch004, sqlpg001, sqlpg002, sqlpg003, sqlpg004, sqlpg005];
|