@intentius/chant-lexicon-sql 0.124.31 → 0.124.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/change-set.js +250 -0
- package/dist/clickhouse/access.js +722 -0
- package/dist/clickhouse/apply/apply.js +243 -0
- package/dist/clickhouse/apply/mutations.js +66 -0
- package/dist/clickhouse/apply/statements.js +545 -0
- package/dist/clickhouse/catalog-types.js +7 -0
- package/dist/clickhouse/container.js +152 -0
- package/dist/clickhouse/engine-syntax.js +159 -0
- package/dist/clickhouse/entities.js +593 -0
- package/dist/clickhouse/expressions.js +107 -0
- package/dist/clickhouse/http.js +55 -0
- package/dist/clickhouse/import/generator.js +192 -0
- package/dist/clickhouse/import/inlined.js +207 -0
- package/dist/clickhouse/import/ir.js +160 -0
- package/dist/clickhouse/import/live-export.js +131 -0
- package/dist/clickhouse/import/parser.js +17 -0
- package/dist/clickhouse/live/bind.js +116 -0
- package/dist/clickhouse/live/catalog.js +223 -0
- package/dist/clickhouse/live/describe-resources.js +91 -0
- package/dist/clickhouse/live/undeclarable.js +59 -0
- package/dist/clickhouse/overlays/codecs.js +57 -0
- package/dist/clickhouse/overlays/engines.js +101 -0
- package/dist/clickhouse/overlays/kinds.js +1 -0
- package/dist/clickhouse/overlays/projections.js +26 -0
- package/dist/clickhouse/overlays/skip-indexes.js +40 -0
- package/dist/clickhouse/overlays/ttl.js +34 -0
- package/dist/clickhouse/overlays/types.js +59 -0
- package/dist/clickhouse/ownership.js +83 -0
- package/dist/clickhouse/parser.js +674 -0
- package/dist/clickhouse/plan/commands.js +159 -0
- package/dist/clickhouse/plan/deep.js +345 -0
- package/dist/clickhouse/plan/diff.js +312 -0
- package/dist/clickhouse/plan/disruption.js +73 -0
- package/dist/clickhouse/plan/normalize.js +545 -0
- package/dist/clickhouse/plan/rebuild-handoff.js +57 -0
- package/dist/clickhouse/plan/report.js +26 -0
- package/dist/clickhouse/plan/rules.js +72 -0
- package/dist/clickhouse/plan/schema.js +67 -0
- package/dist/clickhouse/plan/server-format.js +35 -0
- package/dist/clickhouse/rebuild/backfill.js +265 -0
- package/dist/clickhouse/rebuild/engines.js +146 -0
- package/dist/clickhouse/rebuild/observe.js +260 -0
- package/dist/clickhouse/rebuild/op.js +152 -0
- package/dist/clickhouse/rebuild/partitions.js +65 -0
- package/dist/clickhouse/rebuild/receipts.js +120 -0
- package/dist/clickhouse/rebuild/replicas.js +121 -0
- package/dist/clickhouse/rebuild/shards.js +76 -0
- package/dist/clickhouse/rebuild/steps.js +383 -0
- package/dist/clickhouse/rebuild/verify.js +279 -0
- package/dist/clickhouse/row-types.js +151 -0
- package/dist/clickhouse/testing/fake-server.js +47 -0
- package/dist/clickhouse/testing/roundtrip-schema.js +25 -0
- package/dist/clickhouse/testing/writable-server.js +123 -0
- package/dist/clickhouse/tokens.js +23 -0
- package/dist/clickhouse/topology.js +463 -0
- package/dist/clickhouse/type-syntax.js +73 -0
- package/dist/clickhouse.js +25 -0
- package/dist/codegen/classifier-page.js +93 -0
- package/dist/codegen/clickhouse-module.js +249 -0
- package/dist/codegen/coverage-page.js +70 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +222 -0
- package/dist/codegen/generate-cli.js +13 -0
- package/dist/codegen/generate.js +97 -0
- package/dist/codegen/package.js +28 -0
- package/dist/codegen/postgres-classifier-page.js +95 -0
- package/dist/codegen/postgres-module.js +418 -0
- package/dist/codegen/registry.js +52 -0
- package/dist/composites/audit-log-table.js +35 -0
- package/dist/composites/catalog.js +524 -0
- package/dist/composites/cdc-mirror.js +32 -0
- package/dist/composites/clickhouse.js +6 -0
- package/dist/composites/events-table.js +26 -0
- package/dist/composites/index.js +8 -0
- package/dist/composites/join-table.js +25 -0
- package/dist/composites/postgres.js +6 -0
- package/dist/composites/refreshed-view.js +24 -0
- package/dist/composites/replacing-table.js +28 -0
- package/dist/composites/rollup-view.js +29 -0
- package/dist/composites/sharded-table.js +27 -0
- package/dist/composites/soft-delete-table.js +34 -0
- package/dist/composites/tenant-table.js +26 -0
- package/dist/composites/typed.js +8 -0
- package/dist/config.js +206 -0
- package/dist/core/apply.js +93 -0
- package/dist/core/classifier.js +115 -0
- package/dist/core/container.js +69 -0
- package/dist/core/cursor.js +261 -0
- package/dist/core/diff.js +48 -0
- package/dist/core/entity.js +58 -0
- package/dist/core/find-templates.js +118 -0
- package/dist/core/foreign-tables.js +27 -0
- package/dist/core/handoff.js +32 -0
- package/dist/core/index.js +64 -0
- package/dist/core/interpolation.js +191 -0
- package/dist/core/normalize.js +43 -0
- package/dist/core/ownership.js +119 -0
- package/dist/core/receipts.js +22 -0
- package/dist/core/references.js +61 -0
- package/dist/core/statements.js +42 -0
- package/dist/core/template.js +79 -0
- package/dist/core/testing/http-stub.js +21 -0
- package/dist/core/tokens.js +202 -0
- package/dist/core/typed.js +40 -0
- package/dist/core/undeclarable.js +46 -0
- package/dist/coverage.js +161 -0
- package/dist/detect.js +32 -0
- package/dist/dialects.js +18 -0
- package/dist/files/build-root.js +72 -0
- package/dist/files/clickhouse.js +138 -0
- package/dist/files/common.js +58 -0
- package/dist/files/postgres.js +389 -0
- package/dist/generated/clickhouse-types.js +5 -0
- package/dist/generated/clickhouse.js +4361 -0
- package/dist/generated/postgres-types.js +227 -0
- package/dist/generated/postgres.js +3667 -0
- package/dist/import-generator.js +14 -0
- package/dist/import-parser.js +71 -0
- package/dist/index.js +24 -0
- package/dist/init-templates.js +290 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +31 -0
- package/dist/lint/audit-lineage.js +22 -0
- package/dist/lint/post-synth/audit-entries.js +34 -0
- package/dist/lint/post-synth/clickhouse-helpers.js +79 -0
- package/dist/lint/post-synth/clickhouse-type-check.js +162 -0
- package/dist/lint/post-synth/index.js +112 -0
- package/dist/lint/post-synth/postgres-audit-entries.js +35 -0
- package/dist/lint/post-synth/postgres-helpers.js +65 -0
- package/dist/lint/post-synth/postgres-names.js +507 -0
- package/dist/lint/post-synth/sql-helpers.js +23 -0
- package/dist/lint/post-synth/sql101.js +59 -0
- package/dist/lint/post-synth/sql102.js +34 -0
- package/dist/lint/post-synth/sqlch101.js +34 -0
- package/dist/lint/post-synth/sqlch102.js +23 -0
- package/dist/lint/post-synth/sqlch103.js +45 -0
- package/dist/lint/post-synth/sqlch104.js +41 -0
- package/dist/lint/post-synth/sqlch105.js +36 -0
- package/dist/lint/post-synth/sqlch106.js +27 -0
- package/dist/lint/post-synth/sqlch107.js +39 -0
- package/dist/lint/post-synth/sqlch108.js +28 -0
- package/dist/lint/post-synth/sqlch109.js +21 -0
- package/dist/lint/post-synth/sqlch110.js +31 -0
- package/dist/lint/post-synth/sqlch111.js +41 -0
- package/dist/lint/post-synth/sqlch112.js +28 -0
- package/dist/lint/post-synth/sqlch113.js +19 -0
- package/dist/lint/post-synth/sqlch114.js +26 -0
- package/dist/lint/post-synth/sqlch115.js +30 -0
- package/dist/lint/post-synth/sqlch116.js +22 -0
- package/dist/lint/post-synth/sqlch117.js +26 -0
- package/dist/lint/post-synth/sqlch118.js +27 -0
- package/dist/lint/post-synth/sqlch119.js +32 -0
- package/dist/lint/post-synth/sqlch120.js +20 -0
- package/dist/lint/post-synth/sqlch121.js +11 -0
- package/dist/lint/post-synth/sqlch122.js +11 -0
- package/dist/lint/post-synth/sqlch123.js +10 -0
- package/dist/lint/post-synth/sqlch124.js +21 -0
- package/dist/lint/post-synth/sqlch125.js +55 -0
- package/dist/lint/post-synth/sqlch126.js +62 -0
- package/dist/lint/post-synth/sqlch127.js +132 -0
- package/dist/lint/post-synth/sqlpg101.js +25 -0
- package/dist/lint/post-synth/sqlpg102.js +35 -0
- package/dist/lint/post-synth/sqlpg103.js +23 -0
- package/dist/lint/post-synth/sqlpg104.js +23 -0
- package/dist/lint/post-synth/sqlpg105.js +22 -0
- package/dist/lint/post-synth/sqlpg106.js +22 -0
- package/dist/lint/post-synth/sqlpg107.js +22 -0
- package/dist/lint/post-synth/sqlpg108.js +28 -0
- package/dist/lint/post-synth/sqlpg109.js +39 -0
- package/dist/lint/post-synth/sqlpg110.js +38 -0
- package/dist/lint/post-synth/sqlpg111.js +23 -0
- package/dist/lint/post-synth/sqlpg112.js +25 -0
- package/dist/lint/post-synth/sqlpg113.js +29 -0
- package/dist/lint/post-synth/sqlpg114.js +39 -0
- package/dist/lint/post-synth/sqlpg115.js +48 -0
- package/dist/lint/post-synth/sqlpg116.js +26 -0
- package/dist/lint/post-synth/sqlpg117.js +24 -0
- package/dist/lint/post-synth/sqlpg118.js +22 -0
- package/dist/lint/post-synth/sqlpg119.js +41 -0
- package/dist/lint/post-synth/sqlpg120.js +83 -0
- package/dist/lint/post-synth/sqlpg121.js +33 -0
- package/dist/lint/post-synth/sqlpg122.js +27 -0
- package/dist/lint/post-synth/sqlpg123.js +69 -0
- package/dist/lint/post-synth/sqlpg124.js +50 -0
- package/dist/lint/post-synth/sqlpg125.js +41 -0
- package/dist/lint/post-synth/sqlpg126.js +38 -0
- package/dist/lint/rules/index.js +12 -0
- package/dist/lint/rules/postgres-templates.js +27 -0
- package/dist/lint/rules/sqlch001.js +79 -0
- package/dist/lint/rules/sqlch002.js +124 -0
- package/dist/lint/rules/sqlch003.js +69 -0
- package/dist/lint/rules/sqlch004.js +83 -0
- package/dist/lint/rules/sqlpg001.js +68 -0
- package/dist/lint/rules/sqlpg002.js +69 -0
- package/dist/lint/rules/sqlpg003.js +58 -0
- package/dist/lint/rules/sqlpg004.js +77 -0
- package/dist/lint/rules/sqlpg005.js +86 -0
- package/dist/lint/rules/templates.js +25 -0
- package/dist/lint/rules/typed-names.js +132 -0
- package/dist/live-dialect.js +32 -0
- package/dist/lsp/catalog.js +44 -0
- package/dist/lsp/completions.js +136 -0
- package/dist/lsp/hover.js +115 -0
- package/dist/lsp/postgres-catalog.js +176 -0
- package/dist/lsp/postgres-template.js +171 -0
- package/dist/lsp/postgres.js +266 -0
- package/dist/lsp/refs.js +60 -0
- package/dist/lsp/registry.d.ts.map +1 -1
- package/dist/lsp/registry.js +19 -0
- package/dist/lsp/template.js +143 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +293 -0
- package/dist/mcp/postgres.js +117 -0
- package/dist/migration-statements.js +243 -0
- package/dist/op/activities/clickhouse-apply.js +83 -0
- package/dist/op/activities/clickhouse-emulator.js +53 -0
- package/dist/op/activities/clickhouse-rebuild.js +119 -0
- package/dist/op/activities/index.js +26 -0
- package/dist/op/activities/postgres-apply.js +126 -0
- package/dist/op/activities/postgres-emulator.js +70 -0
- package/dist/op/activities/postgres-migration.js +156 -0
- package/dist/op/activities/sql-exec.js +57 -0
- package/dist/op/activity-contracts.js +103 -0
- package/dist/package-cli.js +14 -0
- package/dist/plugin.js +259 -0
- package/dist/postgres/access/acl.js +344 -0
- package/dist/postgres/access/changes.js +30 -0
- package/dist/postgres/access/import.js +61 -0
- package/dist/postgres/access/live.js +96 -0
- package/dist/postgres/apply/apply.js +557 -0
- package/dist/postgres/apply/statements.js +882 -0
- package/dist/postgres/catalog-types.js +18 -0
- package/dist/postgres/container.js +58 -0
- package/dist/postgres/entities.js +1390 -0
- package/dist/postgres/entity-types.js +28 -0
- package/dist/postgres/import/generator.js +283 -0
- package/dist/postgres/import/ir.js +53 -0
- package/dist/postgres/import/live-export.js +83 -0
- package/dist/postgres/keywords.js +32 -0
- package/dist/postgres/live/bind.js +130 -0
- package/dist/postgres/live/catalog-kinds.js +301 -0
- package/dist/postgres/live/catalog.js +644 -0
- package/dist/postgres/live/client.js +151 -0
- package/dist/postgres/live/describe-resources.js +135 -0
- package/dist/postgres/live/undeclarable.js +111 -0
- package/dist/postgres/migrate/backfill.js +170 -0
- package/dist/postgres/migrate/batches.js +122 -0
- package/dist/postgres/migrate/carry.js +518 -0
- package/dist/postgres/migrate/handoff.js +59 -0
- package/dist/postgres/migrate/names.js +61 -0
- package/dist/postgres/migrate/observe.js +360 -0
- package/dist/postgres/migrate/op.js +178 -0
- package/dist/postgres/migrate/receipts.js +117 -0
- package/dist/postgres/migrate/replication.js +106 -0
- package/dist/postgres/migrate/steps.js +514 -0
- package/dist/postgres/overlays/columns.js +31 -0
- package/dist/postgres/overlays/constraints.js +52 -0
- package/dist/postgres/overlays/extension-objects.js +88 -0
- package/dist/postgres/overlays/indexes.js +25 -0
- package/dist/postgres/overlays/kinds.js +2 -0
- package/dist/postgres/overlays/partitioning.js +23 -0
- package/dist/postgres/overlays/relations.js +25 -0
- package/dist/postgres/overlays/storage.js +65 -0
- package/dist/postgres/overlays/type-ddl.js +36 -0
- package/dist/postgres/overlays/types.js +74 -0
- package/dist/postgres/parser.js +1824 -0
- package/dist/postgres/plan/commands.js +137 -0
- package/dist/postgres/plan/deep.js +324 -0
- package/dist/postgres/plan/diff.js +499 -0
- package/dist/postgres/plan/disruption.js +64 -0
- package/dist/postgres/plan/normalize.js +889 -0
- package/dist/postgres/plan/report.js +21 -0
- package/dist/postgres/plan/rules.js +115 -0
- package/dist/postgres/plan/schema.js +105 -0
- package/dist/postgres/plan/server-normalize.js +166 -0
- package/dist/postgres/providers/aurora.js +44 -0
- package/dist/postgres/providers/azure.js +53 -0
- package/dist/postgres/providers/cloud-sql.js +41 -0
- package/dist/postgres/providers/index.js +73 -0
- package/dist/postgres/providers/neon.js +44 -0
- package/dist/postgres/providers/rds.js +47 -0
- package/dist/postgres/providers/supabase.js +45 -0
- package/dist/postgres/providers/types.js +21 -0
- package/dist/postgres/row-types.js +107 -0
- package/dist/postgres/testing/fake-client.js +34 -0
- package/dist/postgres/testing/migration.js +48 -0
- package/dist/postgres/testing/roundtrip-schema.js +64 -0
- package/dist/postgres/testing/server.js +47 -0
- package/dist/postgres/testing/writable-server.js +160 -0
- package/dist/postgres/tokens.js +41 -0
- package/dist/postgres.js +15 -0
- package/dist/read/clickhouse.js +82 -0
- package/dist/read/objects.js +44 -0
- package/dist/read/postgres.js +104 -0
- package/dist/read.js +52 -0
- package/dist/receipts.js +207 -0
- package/dist/serializer.js +172 -0
- package/dist/skill-defs.js +93 -0
- package/dist/spec/catalog.js +191 -0
- package/dist/spec/fetch.d.ts.map +1 -1
- package/dist/spec/fetch.js +90 -0
- package/dist/spec/pin.js +42 -0
- package/dist/spec/postgres-catalog.js +171 -0
- package/dist/spec/postgres-fetch.js +91 -0
- package/dist/spec/postgres-pin.js +82 -0
- package/dist/sql-files.js +206 -0
- package/dist/sql-types-command.js +97 -0
- package/dist/sql-types.js +323 -0
- package/dist/token-source.js +156 -0
- package/dist/validate-cli.js +5 -0
- package/dist/validate.js +151 -0
- package/dist/workspace-outputs.js +191 -0
- package/package.json +7 -7
- package/src/codegen/package.ts +2 -2
- package/src/lint/rules/typescript-7.test.ts +2 -1
- package/src/lsp/catalog.ts +2 -2
- package/src/lsp/postgres-catalog.ts +2 -2
- package/src/lsp/registry.ts +2 -1
- package/src/mcp/index.ts +4 -2
- package/src/spec/fetch.ts +2 -2
- package/src/spec/postgres-fetch.ts +2 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CODECS, CLICKHOUSE_VERSION } from "../../generated/clickhouse.js";
|
|
2
|
+
import { checkOf, splitTop, tablesOf } from "./clickhouse-helpers.js";
|
|
3
|
+
const NUMBER = /^-?\d+(\.\d+)?$/;
|
|
4
|
+
/**
|
|
5
|
+
* SQLCH125: a codec's parameters do not fit it: more than the codec takes
|
|
6
|
+
* (`LZ4(1)`), a number outside its range (`ZSTD(99)`, `LZ4HC(13)`), or a
|
|
7
|
+
* value outside its set (`Delta(3)`; the byte width is 1, 2, 4 or 8).
|
|
8
|
+
*
|
|
9
|
+
* The parameters are the `CODEC_OVERLAY`'s; a codec marked `"unknown"` there
|
|
10
|
+
* (the experimental ones with undocumented parameters) is not checked. An
|
|
11
|
+
* unknown codec name is SQLCH114's.
|
|
12
|
+
*/
|
|
13
|
+
export const sqlch125 = checkOf({ id: "SQLCH125", description: "A codec parameter is outside what the codec takes" }, (ctx, report) => {
|
|
14
|
+
const codecs = CODECS;
|
|
15
|
+
for (const t of tablesOf(ctx)) {
|
|
16
|
+
for (const c of t.columns) {
|
|
17
|
+
if (!c.codec)
|
|
18
|
+
continue;
|
|
19
|
+
for (const part of splitTop(c.codec)) {
|
|
20
|
+
const m = /^([A-Za-z_]\w*)\s*(?:\(([\s\S]*)\))?$/.exec(part.trim());
|
|
21
|
+
if (!m)
|
|
22
|
+
continue;
|
|
23
|
+
const name = m[1];
|
|
24
|
+
const spec = codecs[name] ?? Object.entries(codecs).find(([k]) => k.toLowerCase() === name.toLowerCase())?.[1];
|
|
25
|
+
if (!spec || spec.parameters === "unknown" || m[2] === undefined)
|
|
26
|
+
continue;
|
|
27
|
+
const args = splitTop(m[2]);
|
|
28
|
+
const where = `${t.export} (${t.name}): column ${c.name} CODEC(${c.codec})`;
|
|
29
|
+
const expected = spec.parameters;
|
|
30
|
+
if (args.length > expected.length) {
|
|
31
|
+
const takes = expected.length === 0 ? "takes no parameters" : `takes at most ${expected.length} (${expected.map((p) => p.name).join(", ")})`;
|
|
32
|
+
report({ severity: "error", message: `${where}: ${name} ${takes}, and is given ${args.length}`, entity: t.export });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
args.forEach((arg, i) => {
|
|
36
|
+
const p = expected[i];
|
|
37
|
+
const text = arg.trim();
|
|
38
|
+
if (p.values && !p.values.some((v) => v.toLowerCase() === text.toLowerCase())) {
|
|
39
|
+
report({ severity: "error", message: `${where}: ${name} ${p.name} ${text} is not one of ${p.values.join(", ")}`, entity: t.export });
|
|
40
|
+
}
|
|
41
|
+
else if (p.range && NUMBER.test(text)) {
|
|
42
|
+
const n = Number(text);
|
|
43
|
+
if (n < p.range[0] || n > p.range[1]) {
|
|
44
|
+
report({
|
|
45
|
+
severity: "error",
|
|
46
|
+
message: `${where}: ${name} ${p.name} ${text} is outside ${p.range[0]} to ${p.range[1]}, the range ClickHouse ${CLICKHOUSE_VERSION} takes`,
|
|
47
|
+
entity: t.export,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { bareName, checkOf, splitTop } from "./clickhouse-helpers.js";
|
|
2
|
+
import { clickhouseObjects } from "./sql-helpers.js";
|
|
3
|
+
/** The columns a grant can name on an object: each declared column, and `n.a` for a `Nested` column's fields. */
|
|
4
|
+
function grantableColumns(o) {
|
|
5
|
+
const columns = (o.columns ?? []);
|
|
6
|
+
if (columns.length === 0)
|
|
7
|
+
return undefined;
|
|
8
|
+
const out = new Set();
|
|
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)
|
|
16
|
+
out.add(`${c.name}.${bareName(name) ?? name}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
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")
|
|
37
|
+
continue;
|
|
38
|
+
const target = (g.dependsOn ?? [])
|
|
39
|
+
.map((e) => byExport.get(e))
|
|
40
|
+
.find((o) => o !== undefined && (o.sqlName === g.on || (o.database ? `${o.database}.${o.name}` : o.name) === g.on));
|
|
41
|
+
if (!target)
|
|
42
|
+
continue;
|
|
43
|
+
const columns = grantableColumns(target);
|
|
44
|
+
if (!columns)
|
|
45
|
+
continue;
|
|
46
|
+
for (const privilege of (g.privileges ?? [])) {
|
|
47
|
+
const m = /^([A-Za-z_][A-Za-z_ ]*?)\s*\(([\s\S]*)\)\s*$/.exec(privilege.trim());
|
|
48
|
+
if (!m)
|
|
49
|
+
continue;
|
|
50
|
+
for (const element of splitTop(m[2])) {
|
|
51
|
+
const name = bareName(element);
|
|
52
|
+
if (name === undefined || columns.has(name))
|
|
53
|
+
continue;
|
|
54
|
+
report({
|
|
55
|
+
severity: "error",
|
|
56
|
+
message: `${g.export}: GRANT ${m[1].toUpperCase()}(${name}) ON ${g.on} names column ${name}, which ${target.export} (${target.name}) does not declare`,
|
|
57
|
+
entity: g.export,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { AGGREGATE_COMBINATORS, CLICKHOUSE_VERSION, FUNCTIONS, TABLE_FUNCTIONS } from "../../generated/clickhouse.js";
|
|
2
|
+
import { isTrivia, tokenizeText } from "../../clickhouse/tokens.js";
|
|
3
|
+
import { checkOf, isTable } from "./clickhouse-helpers.js";
|
|
4
|
+
import { clickhouseObjects } from "./sql-helpers.js";
|
|
5
|
+
const BY_NAME = FUNCTIONS;
|
|
6
|
+
const FOLDED = new Map();
|
|
7
|
+
for (const [name, spec] of Object.entries(BY_NAME))
|
|
8
|
+
if (spec?.caseInsensitive)
|
|
9
|
+
FOLDED.set(name.toLowerCase(), spec);
|
|
10
|
+
/**
|
|
11
|
+
* Words followed by `(` that are not function calls: SQL keywords that take
|
|
12
|
+
* parentheses, and the names ClickHouse's parser reads with a syntax of their
|
|
13
|
+
* own (`EXTRACT(DAY FROM d)`, `TRIM(BOTH ' ' FROM s)`, `DATE_ADD(...)`), in
|
|
14
|
+
* any case.
|
|
15
|
+
*/
|
|
16
|
+
const NOT_CALLS = new Set(("and or not in is as on by to like ilike between case when then else end any all some global exists array interval " +
|
|
17
|
+
"extract substring position trim ltrim rtrim " +
|
|
18
|
+
"dateadd date_add timestamp_add timestampadd datesub date_sub timestamp_sub timestampsub datediff date_diff timestampdiff timestamp_diff " +
|
|
19
|
+
"grouping over filter where set delete group with select from values distinct using asc desc nulls").split(" "));
|
|
20
|
+
/** `ZSTD(3)` after `CODEC` in a TTL `RECOMPRESS` is a codec, not a call. */
|
|
21
|
+
const SKIPS_GROUP = new Set(["codec"]);
|
|
22
|
+
const lookup = (name) => BY_NAME[name] ?? FOLDED.get(name.toLowerCase());
|
|
23
|
+
const TABLE_FUNCTION_NAMES = new Set(TABLE_FUNCTIONS);
|
|
24
|
+
/**
|
|
25
|
+
* A catalog function, or an aggregate function with combinator suffixes
|
|
26
|
+
* (`countIf`, `sumState`, `uniqMerge`). A table function (`numbers`) counts
|
|
27
|
+
* too: it is a name the server has, and where it does not belong is the
|
|
28
|
+
* server's to say.
|
|
29
|
+
*/
|
|
30
|
+
export function isKnownFunction(name) {
|
|
31
|
+
if (lookup(name) || TABLE_FUNCTION_NAMES.has(name))
|
|
32
|
+
return true;
|
|
33
|
+
// Strip combinators from the end, longest first, and look for an aggregate underneath.
|
|
34
|
+
const suffixes = [...AGGREGATE_COMBINATORS].sort((a, b) => b.length - a.length);
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
const queue = [name];
|
|
37
|
+
while (queue.length > 0) {
|
|
38
|
+
const base = queue.pop();
|
|
39
|
+
for (const s of suffixes) {
|
|
40
|
+
if (base.length <= s.length || !base.endsWith(s))
|
|
41
|
+
continue;
|
|
42
|
+
const rest = base.slice(0, -s.length);
|
|
43
|
+
if (seen.has(rest))
|
|
44
|
+
continue;
|
|
45
|
+
seen.add(rest);
|
|
46
|
+
const spec = lookup(rest);
|
|
47
|
+
const aggregate = spec && (spec.aggregate || (spec.aliasOf !== undefined && lookup(spec.aliasOf)?.aggregate));
|
|
48
|
+
if (aggregate)
|
|
49
|
+
return true;
|
|
50
|
+
queue.push(rest);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
/** The names an expression calls: an identifier directly followed by `(`, outside strings and quoted names. */
|
|
56
|
+
export function calledNames(expr) {
|
|
57
|
+
let tokens;
|
|
58
|
+
try {
|
|
59
|
+
tokens = tokenizeText(expr, 0).filter((t) => !isTrivia(t));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const out = [];
|
|
65
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
66
|
+
const t = tokens[i];
|
|
67
|
+
const next = tokens[i + 1];
|
|
68
|
+
if (t.kind !== "ident" || next?.kind !== "punct" || next.text !== "(")
|
|
69
|
+
continue;
|
|
70
|
+
const prev = tokens[i - 1];
|
|
71
|
+
if (prev?.kind === "punct" && prev.text === ".")
|
|
72
|
+
continue;
|
|
73
|
+
const lower = t.text.toLowerCase();
|
|
74
|
+
if (SKIPS_GROUP.has(lower)) {
|
|
75
|
+
let depth = 0;
|
|
76
|
+
for (i = i + 1; i < tokens.length; i++) {
|
|
77
|
+
const x = tokens[i];
|
|
78
|
+
if (x.kind === "punct" && x.text === "(")
|
|
79
|
+
depth++;
|
|
80
|
+
else if (x.kind === "punct" && x.text === ")" && --depth === 0)
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (NOT_CALLS.has(lower))
|
|
86
|
+
continue;
|
|
87
|
+
out.push(t.text);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* SQLCH127: an expression the table declares calls a function the pinned
|
|
93
|
+
* server does not have: `DEFAULT noww()`, `PARTITION BY toYYYYMMM(at)`.
|
|
94
|
+
*
|
|
95
|
+
* Read in each column's `DEFAULT`, `MATERIALIZED`, `ALIAS`, `EPHEMERAL` and
|
|
96
|
+
* TTL expression, in `ORDER BY`, `PRIMARY KEY`, `PARTITION BY`, `SAMPLE BY`
|
|
97
|
+
* and the table TTL, and in each skip index expression. A name counts when it
|
|
98
|
+
* is in `system.functions` (case as the catalog says, aliases included), is an
|
|
99
|
+
* aggregate function with combinator suffixes, or is a function the project
|
|
100
|
+
* declares with `func`.
|
|
101
|
+
*/
|
|
102
|
+
export const sqlch127 = checkOf({ id: "SQLCH127", description: "An expression calls a function the pinned server does not have" }, (ctx, report) => {
|
|
103
|
+
const objects = clickhouseObjects(ctx);
|
|
104
|
+
const declared = new Set(objects.filter((o) => o.type === "ClickHouse::Function").map((o) => o.name));
|
|
105
|
+
for (const t of objects.filter(isTable)) {
|
|
106
|
+
const places = [];
|
|
107
|
+
for (const c of t.columns) {
|
|
108
|
+
if (c.default?.expr)
|
|
109
|
+
places.push([`column ${c.name} ${c.default.kind}`, c.default.expr]);
|
|
110
|
+
if (c.ttl)
|
|
111
|
+
places.push([`column ${c.name} TTL`, c.ttl]);
|
|
112
|
+
}
|
|
113
|
+
places.push(["ORDER BY", t.orderBy], ["PRIMARY KEY", t.primaryKey], ["PARTITION BY", t.partitionBy], ["SAMPLE BY", t.sampleBy], ["TTL", t.ttl]);
|
|
114
|
+
for (const ix of t.indexes ?? [])
|
|
115
|
+
places.push([`index ${ix.name}`, ix.expr]);
|
|
116
|
+
for (const [where, expr] of places) {
|
|
117
|
+
if (!expr)
|
|
118
|
+
continue;
|
|
119
|
+
const reported = new Set();
|
|
120
|
+
for (const name of calledNames(expr)) {
|
|
121
|
+
if (reported.has(name) || declared.has(name) || isKnownFunction(name))
|
|
122
|
+
continue;
|
|
123
|
+
reported.add(name);
|
|
124
|
+
report({
|
|
125
|
+
severity: "error",
|
|
126
|
+
message: `${t.export} (${t.name}): ${where} calls ${name}(), which ClickHouse ${CLICKHOUSE_VERSION} does not have and the project does not declare with func`,
|
|
127
|
+
entity: t.export,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { checkOf, ownsColumns, tablesOf } from "./postgres-helpers.js";
|
|
2
|
+
/**
|
|
3
|
+
* SQLPG101: a table with no primary key.
|
|
4
|
+
*
|
|
5
|
+
* Doc: https://www.postgresql.org/docs/18/ddl-constraints.html#DDL-CONSTRAINTS-PRIMARY-KEYS
|
|
6
|
+
* ("a primary key indicates that a column or group of columns can be used as a
|
|
7
|
+
* unique identifier for rows"). Without one, logical replication needs a
|
|
8
|
+
* REPLICA IDENTITY and rows cannot be addressed by identity. A partition, a
|
|
9
|
+
* typed table and a LIKE copy are skipped, and a UNIQUE constraint over NOT
|
|
10
|
+
* NULL columns counts as an identity.
|
|
11
|
+
*/
|
|
12
|
+
export const sqlpg101 = checkOf({ id: "SQLPG101", description: "A table declares no primary key" }, (ctx, report) => {
|
|
13
|
+
for (const t of tablesOf(ctx)) {
|
|
14
|
+
if (!ownsColumns(t) || t.primaryKey || t.like.length > 0)
|
|
15
|
+
continue;
|
|
16
|
+
const notNull = new Set(t.columns.filter((c) => c.notNull).map((c) => c.name));
|
|
17
|
+
if (t.uniques.some((u) => u.columns.length > 0 && u.columns.every((c) => notNull.has(c))))
|
|
18
|
+
continue;
|
|
19
|
+
report({
|
|
20
|
+
severity: "warning",
|
|
21
|
+
message: `${t.export} (${t.sqlName}) has no primary key; add one, or a UNIQUE constraint over NOT NULL columns`,
|
|
22
|
+
entity: t.export,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { checkOf, indexesOf, leadingColumns, ownsColumns, tablesOf } from "./postgres-helpers.js";
|
|
2
|
+
/**
|
|
3
|
+
* SQLPG102: a foreign key whose referencing columns no index leads with.
|
|
4
|
+
*
|
|
5
|
+
* Doc: https://www.postgresql.org/docs/18/ddl-constraints.html#DDL-CONSTRAINTS-FK
|
|
6
|
+
* ("it is often a good idea to index the referencing columns too ... a delete
|
|
7
|
+
* of a referenced row requires a scan of the referencing table"). The primary
|
|
8
|
+
* key, a UNIQUE constraint or an index covers the key when its leading columns
|
|
9
|
+
* are the foreign key's columns, in any order.
|
|
10
|
+
*/
|
|
11
|
+
export const sqlpg102 = checkOf({ id: "SQLPG102", description: "A foreign key's referencing columns have no index" }, (ctx, report) => {
|
|
12
|
+
const indexes = indexesOf(ctx);
|
|
13
|
+
const sameSet = (a, b) => a.length === b.length && a.every((c) => b.includes(c));
|
|
14
|
+
for (const t of tablesOf(ctx)) {
|
|
15
|
+
if (!ownsColumns(t))
|
|
16
|
+
continue;
|
|
17
|
+
const covers = [];
|
|
18
|
+
if (t.primaryKey)
|
|
19
|
+
covers.push(t.primaryKey.columns);
|
|
20
|
+
for (const u of t.uniques)
|
|
21
|
+
covers.push(u.columns);
|
|
22
|
+
for (const i of indexes)
|
|
23
|
+
if (i.tableName === t.sqlName && (i.method ?? "btree") === "btree")
|
|
24
|
+
covers.push(leadingColumns(i));
|
|
25
|
+
for (const fk of t.foreignKeys) {
|
|
26
|
+
if (covers.some((c) => c.length >= fk.columns.length && sameSet(c.slice(0, fk.columns.length), fk.columns)))
|
|
27
|
+
continue;
|
|
28
|
+
report({
|
|
29
|
+
severity: "warning",
|
|
30
|
+
message: `${t.export} (${t.sqlName}) has a foreign key on (${fk.columns.join(", ")}) to ${fk.refTable} with no index leading with those columns; deletes and updates of the referenced row scan the table`,
|
|
31
|
+
entity: t.export,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { baseType, checkOf, tablesOf } from "./postgres-helpers.js";
|
|
2
|
+
/**
|
|
3
|
+
* SQLPG103: a serial column where an identity column is preferred.
|
|
4
|
+
*
|
|
5
|
+
* Doc: https://www.postgresql.org/docs/18/datatype-numeric.html#DATATYPE-SERIAL
|
|
6
|
+
* (serial is a notational convenience that creates a sequence the column does
|
|
7
|
+
* not own as a constraint); https://www.postgresql.org/docs/18/sql-createtable.html
|
|
8
|
+
* documents GENERATED ... AS IDENTITY, the standard form. Prior art: squawk
|
|
9
|
+
* prefer-identity.
|
|
10
|
+
*/
|
|
11
|
+
export const sqlpg103 = checkOf({ id: "SQLPG103", description: "A serial column where an identity column is preferred" }, (ctx, report) => {
|
|
12
|
+
for (const t of tablesOf(ctx)) {
|
|
13
|
+
for (const c of t.columns) {
|
|
14
|
+
if (!/^(small|big)?serial[248]?$/.test(baseType(c.type)))
|
|
15
|
+
continue;
|
|
16
|
+
report({
|
|
17
|
+
severity: "warning",
|
|
18
|
+
message: `${t.export}.${c.name} is ${c.type}; declare it as an identity column (GENERATED ALWAYS AS IDENTITY)`,
|
|
19
|
+
entity: t.export,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { baseType, checkOf, tablesOf } from "./postgres-helpers.js";
|
|
2
|
+
/**
|
|
3
|
+
* SQLPG104: a timestamp column without time zone.
|
|
4
|
+
*
|
|
5
|
+
* Doc: https://www.postgresql.org/docs/18/datatype-datetime.html (timestamp
|
|
6
|
+
* without time zone ignores any zone in the input, so the value means
|
|
7
|
+
* different instants to clients in different zones); the Postgres wiki's
|
|
8
|
+
* "Don't Do This" says the same. Prior art: squawk prefer-timestamptz.
|
|
9
|
+
*/
|
|
10
|
+
export const sqlpg104 = checkOf({ id: "SQLPG104", description: "A timestamp column without time zone" }, (ctx, report) => {
|
|
11
|
+
for (const t of tablesOf(ctx)) {
|
|
12
|
+
for (const c of t.columns) {
|
|
13
|
+
const type = baseType(c.type);
|
|
14
|
+
if (type !== "timestamp" && type !== "timestamp without time zone")
|
|
15
|
+
continue;
|
|
16
|
+
report({
|
|
17
|
+
severity: "warning",
|
|
18
|
+
message: `${t.export}.${c.name} is ${c.type}; use timestamptz, which stores an instant`,
|
|
19
|
+
entity: t.export,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { baseType, checkOf, tablesOf } from "./postgres-helpers.js";
|
|
2
|
+
/**
|
|
3
|
+
* SQLPG105: a json column where jsonb fits.
|
|
4
|
+
*
|
|
5
|
+
* Doc: https://www.postgresql.org/docs/18/datatype-json.html ("the jsonb data
|
|
6
|
+
* type is faster to process ... and supports indexing"; json has no equality
|
|
7
|
+
* operator, so SELECT DISTINCT, GROUP BY and UNION fail on it). Prior art:
|
|
8
|
+
* strong_migrations "Adding a json column".
|
|
9
|
+
*/
|
|
10
|
+
export const sqlpg105 = checkOf({ id: "SQLPG105", description: "A json column where jsonb fits" }, (ctx, report) => {
|
|
11
|
+
for (const t of tablesOf(ctx)) {
|
|
12
|
+
for (const c of t.columns) {
|
|
13
|
+
if (baseType(c.type) !== "json")
|
|
14
|
+
continue;
|
|
15
|
+
report({
|
|
16
|
+
severity: "warning",
|
|
17
|
+
message: `${t.export}.${c.name} is json; use jsonb unless the exact text, key order or duplicate keys must be kept`,
|
|
18
|
+
entity: t.export,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { baseType, checkOf, tablesOf } from "./postgres-helpers.js";
|
|
2
|
+
/**
|
|
3
|
+
* SQLPG106: a char(n) column.
|
|
4
|
+
*
|
|
5
|
+
* Doc: https://www.postgresql.org/docs/18/datatype-character.html ("there is
|
|
6
|
+
* no performance advantage to character(n) ... it takes more storage because
|
|
7
|
+
* of the blank padding"). Prior art: squawk ban-char-field.
|
|
8
|
+
*/
|
|
9
|
+
export const sqlpg106 = checkOf({ id: "SQLPG106", description: "A char(n) column" }, (ctx, report) => {
|
|
10
|
+
for (const t of tablesOf(ctx)) {
|
|
11
|
+
for (const c of t.columns) {
|
|
12
|
+
const type = baseType(c.type);
|
|
13
|
+
if (type !== "char" && type !== "character" && type !== "bpchar")
|
|
14
|
+
continue;
|
|
15
|
+
report({
|
|
16
|
+
severity: "warning",
|
|
17
|
+
message: `${t.export}.${c.name} is ${c.type}, which pads with blanks; use text, or varchar(n) for a length rule`,
|
|
18
|
+
entity: t.export,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { baseType, checkOf, tablesOf } from "./postgres-helpers.js";
|
|
2
|
+
/**
|
|
3
|
+
* SQLPG107: a money column.
|
|
4
|
+
*
|
|
5
|
+
* Doc: https://www.postgresql.org/docs/18/datatype-money.html ("the output of
|
|
6
|
+
* this type depends on lc_monetary ... restoring a dump into a database with
|
|
7
|
+
* a different lc_monetary setting may not work"; it holds a fixed number of
|
|
8
|
+
* decimals and no currency). Use numeric(p, s).
|
|
9
|
+
*/
|
|
10
|
+
export const sqlpg107 = checkOf({ id: "SQLPG107", description: "A money column" }, (ctx, report) => {
|
|
11
|
+
for (const t of tablesOf(ctx)) {
|
|
12
|
+
for (const c of t.columns) {
|
|
13
|
+
if (baseType(c.type) !== "money")
|
|
14
|
+
continue;
|
|
15
|
+
report({
|
|
16
|
+
severity: "warning",
|
|
17
|
+
message: `${t.export}.${c.name} is money, whose rounding and output depend on lc_monetary; use numeric(p, s) and a currency column`,
|
|
18
|
+
entity: t.export,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { baseType, checkOf, tablesOf } from "./postgres-helpers.js";
|
|
2
|
+
/**
|
|
3
|
+
* SQLPG108: a varchar(n) column whose limit no CHECK or domain explains.
|
|
4
|
+
*
|
|
5
|
+
* Doc: https://www.postgresql.org/docs/18/datatype-character.html ("there is
|
|
6
|
+
* no performance difference among these three types"; a length limit only
|
|
7
|
+
* buys the check, and raising it later takes a table rewrite on old majors).
|
|
8
|
+
* Reported only for a limit that is a round number such as 255, the habit of
|
|
9
|
+
* other databases. Prior art: squawk prefer-text-field.
|
|
10
|
+
*/
|
|
11
|
+
export const sqlpg108 = checkOf({ id: "SQLPG108", description: "A varchar(n) column with a habitual length limit" }, (ctx, report) => {
|
|
12
|
+
for (const t of tablesOf(ctx)) {
|
|
13
|
+
for (const c of t.columns) {
|
|
14
|
+
const type = (c.type ?? "").toLowerCase();
|
|
15
|
+
const m = /^(?:varchar|character varying)\s*\(\s*(\d+)\s*\)$/.exec(type.trim());
|
|
16
|
+
if (!m || baseType(type) !== "varchar" && baseType(type) !== "character varying")
|
|
17
|
+
continue;
|
|
18
|
+
const n = Number(m[1]);
|
|
19
|
+
if (![50, 100, 128, 200, 250, 255, 256, 500, 512, 1000, 1024].includes(n))
|
|
20
|
+
continue;
|
|
21
|
+
report({
|
|
22
|
+
severity: "warning",
|
|
23
|
+
message: `${t.export}.${c.name} is varchar(${n}); text performs the same, and a CHECK states a real length rule`,
|
|
24
|
+
entity: t.export,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { checkOf, indexesOf, tablesOf } from "./postgres-helpers.js";
|
|
2
|
+
const plain = (i) => (i.method ?? "btree") === "btree" && i.elements.every((e) => e.column);
|
|
3
|
+
const keyOf = (i) => `${i.elements.map((e) => e.column).join(",")}|${i.where ?? ""}|${i.include ?? ""}`;
|
|
4
|
+
/**
|
|
5
|
+
* SQLPG109: an index that duplicates a constraint's index, or another index.
|
|
6
|
+
*
|
|
7
|
+
* Doc: https://www.postgresql.org/docs/18/indexes-unique.html ("PostgreSQL
|
|
8
|
+
* automatically creates a unique index when a unique constraint or a primary
|
|
9
|
+
* key is defined for a table"), so a second index on the same columns costs
|
|
10
|
+
* every write and serves no query. Two indexes with the same method, columns,
|
|
11
|
+
* INCLUDE and predicate are the same index.
|
|
12
|
+
*/
|
|
13
|
+
export const sqlpg109 = checkOf({ id: "SQLPG109", description: "An index duplicates a constraint or another index" }, (ctx, report) => {
|
|
14
|
+
const indexes = indexesOf(ctx).filter(plain);
|
|
15
|
+
for (const t of tablesOf(ctx)) {
|
|
16
|
+
const constraints = [];
|
|
17
|
+
if (t.primaryKey)
|
|
18
|
+
constraints.push({ label: "its primary key", cols: t.primaryKey.columns.join(",") });
|
|
19
|
+
for (const u of t.uniques)
|
|
20
|
+
constraints.push({ label: `its unique constraint${u.name ? ` ${u.name}` : ""}`, cols: u.columns.join(",") });
|
|
21
|
+
const mine = indexes.filter((i) => i.tableName === t.sqlName);
|
|
22
|
+
const seen = new Map();
|
|
23
|
+
for (const i of mine) {
|
|
24
|
+
const key = keyOf(i);
|
|
25
|
+
const cols = i.elements.map((e) => e.column).join(",");
|
|
26
|
+
const dup = constraints.find((c) => c.cols === cols && !i.where && !i.include);
|
|
27
|
+
if (dup) {
|
|
28
|
+
report({ severity: "warning", message: `${i.export} (${i.sqlName}) indexes (${cols}) on ${t.sqlName}, which ${dup.label} already indexes`, entity: i.export });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const first = seen.get(key);
|
|
32
|
+
if (first) {
|
|
33
|
+
report({ severity: "warning", message: `${i.export} (${i.sqlName}) duplicates ${first.export} (${first.sqlName}): same columns on ${t.sqlName}`, entity: i.export });
|
|
34
|
+
}
|
|
35
|
+
else
|
|
36
|
+
seen.set(key, i);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { checkOf, indexesOf, leadingColumns, tablesOf } from "./postgres-helpers.js";
|
|
2
|
+
/**
|
|
3
|
+
* SQLPG110: a btree index whose columns are a leading prefix of another index.
|
|
4
|
+
*
|
|
5
|
+
* Doc: https://www.postgresql.org/docs/18/indexes-multicolumn.html ("a
|
|
6
|
+
* multicolumn B-tree index can be used with query conditions that involve any
|
|
7
|
+
* subset of the index's columns, but the index is most efficient when there
|
|
8
|
+
* are constraints on the leading (leftmost) columns"). A plain index on (a)
|
|
9
|
+
* is served by one on (a, b) or by the primary key (a, b). A unique index
|
|
10
|
+
* enforces a rule and is kept.
|
|
11
|
+
*/
|
|
12
|
+
export const sqlpg110 = checkOf({ id: "SQLPG110", description: "A btree index is a prefix of another index" }, (ctx, report) => {
|
|
13
|
+
const indexes = indexesOf(ctx).filter((i) => (i.method ?? "btree") === "btree");
|
|
14
|
+
for (const t of tablesOf(ctx)) {
|
|
15
|
+
const wider = [];
|
|
16
|
+
if (t.primaryKey)
|
|
17
|
+
wider.push({ label: "the primary key", cols: t.primaryKey.columns });
|
|
18
|
+
for (const u of t.uniques)
|
|
19
|
+
wider.push({ label: "a unique constraint", cols: u.columns });
|
|
20
|
+
const mine = indexes.filter((i) => i.tableName === t.sqlName);
|
|
21
|
+
for (const i of mine)
|
|
22
|
+
if (!i.where)
|
|
23
|
+
wider.push({ label: i.sqlName, cols: leadingColumns(i) });
|
|
24
|
+
for (const i of mine) {
|
|
25
|
+
if (i.unique || i.where || i.include || !i.elements.every((e) => e.column))
|
|
26
|
+
continue;
|
|
27
|
+
const cols = i.elements.map((e) => e.column);
|
|
28
|
+
const cover = wider.find((w) => w.label !== i.sqlName && w.cols.length > cols.length && cols.every((c, n) => w.cols[n] === c));
|
|
29
|
+
if (!cover)
|
|
30
|
+
continue;
|
|
31
|
+
report({
|
|
32
|
+
severity: "warning",
|
|
33
|
+
message: `${i.export} (${i.sqlName}) on (${cols.join(", ")}) is a prefix of ${cover.label} (${cover.cols.join(", ")}); drop it`,
|
|
34
|
+
entity: i.export,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { checkOf, indexesOf, isMaterializedView } from "./postgres-helpers.js";
|
|
2
|
+
import { postgresObjects } from "./sql-helpers.js";
|
|
3
|
+
/**
|
|
4
|
+
* SQLPG111: a materialized view with no unique index.
|
|
5
|
+
*
|
|
6
|
+
* Doc: https://www.postgresql.org/docs/18/sql-refreshmaterializedview.html
|
|
7
|
+
* ("CONCURRENTLY ... is only allowed if there is at least one UNIQUE index on
|
|
8
|
+
* the materialized view which uses only column names and includes all rows").
|
|
9
|
+
* Without one, every refresh takes an ACCESS EXCLUSIVE lock and blocks readers.
|
|
10
|
+
*/
|
|
11
|
+
export const sqlpg111 = checkOf({ id: "SQLPG111", description: "A materialized view has no unique index, so it cannot refresh concurrently" }, (ctx, report) => {
|
|
12
|
+
const indexes = indexesOf(ctx);
|
|
13
|
+
for (const v of postgresObjects(ctx).filter(isMaterializedView)) {
|
|
14
|
+
const ok = indexes.some((i) => i.tableName === v.sqlName && i.unique && !i.where && i.elements.every((e) => e.column));
|
|
15
|
+
if (ok)
|
|
16
|
+
continue;
|
|
17
|
+
report({
|
|
18
|
+
severity: "warning",
|
|
19
|
+
message: `${v.export} (${v.sqlName}) has no unique index over plain columns; REFRESH MATERIALIZED VIEW CONCURRENTLY is refused and a plain refresh blocks readers`,
|
|
20
|
+
entity: v.export,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { checkOf, tablesOf } from "./postgres-helpers.js";
|
|
2
|
+
const SENSITIVE = /(^|_)(password|passwd|pwd|secret|token|api_?key|ssn|social_security|tax_id|credit_card|card_number|cvv|iban|passport|date_of_birth|dob|email|phone|address)(_|$)/i;
|
|
3
|
+
/**
|
|
4
|
+
* SQLPG112: a secret- or PII-named column in a table with no comment.
|
|
5
|
+
*
|
|
6
|
+
* Doc: https://www.postgresql.org/docs/18/sql-comment.html. A COMMENT on the
|
|
7
|
+
* table or the column is where the retention, encryption or masking rule is
|
|
8
|
+
* written down, and where pg_dump, anonymizers and catalog scanners read it.
|
|
9
|
+
* A column or a table comment satisfies the check.
|
|
10
|
+
*/
|
|
11
|
+
export const sqlpg112 = checkOf({ id: "SQLPG112", description: "A secret- or PII-named column with no comment on it or its table" }, (ctx, report) => {
|
|
12
|
+
for (const t of tablesOf(ctx)) {
|
|
13
|
+
if (t.comment)
|
|
14
|
+
continue;
|
|
15
|
+
for (const c of t.columns) {
|
|
16
|
+
if (c.comment || !SENSITIVE.test(c.name))
|
|
17
|
+
continue;
|
|
18
|
+
report({
|
|
19
|
+
severity: "warning",
|
|
20
|
+
message: `${t.export}.${c.name} looks like a secret or personal data and neither it nor ${t.sqlName} has a COMMENT; say what it holds and how it is protected`,
|
|
21
|
+
entity: t.export,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { checkOf } from "./postgres-helpers.js";
|
|
2
|
+
import { postgresObjects } from "./sql-helpers.js";
|
|
3
|
+
const PLACED = new Set(["Postgres::Table", "Postgres::View", "Postgres::MaterializedView", "Postgres::Sequence", "Postgres::Enum", "Postgres::CompositeType", "Postgres::RangeType", "Postgres::Domain"]);
|
|
4
|
+
/**
|
|
5
|
+
* SQLPG113: an object in the public schema while the project declares schemas.
|
|
6
|
+
*
|
|
7
|
+
* Doc: https://www.postgresql.org/docs/18/ddl-schemas.html#DDL-SCHEMAS-PUBLIC
|
|
8
|
+
* and https://www.postgresql.org/docs/18/ddl-schemas.html#DDL-SCHEMAS-PATTERNS
|
|
9
|
+
* (the usage patterns recommend that applications not share `public`). An
|
|
10
|
+
* unqualified name is created in the first schema of search_path, usually
|
|
11
|
+
* public. Silent for a project that declares no schema.
|
|
12
|
+
*/
|
|
13
|
+
export const sqlpg113 = checkOf({ id: "SQLPG113", description: "An object is in the public schema though the project declares schemas" }, (ctx, report) => {
|
|
14
|
+
const objects = postgresObjects(ctx);
|
|
15
|
+
if (!objects.some((o) => o.type === "Postgres::Schema"))
|
|
16
|
+
return;
|
|
17
|
+
for (const o of objects) {
|
|
18
|
+
if (!PLACED.has(o.type))
|
|
19
|
+
continue;
|
|
20
|
+
const schema = o.schema ?? "public";
|
|
21
|
+
if (schema !== "public")
|
|
22
|
+
continue;
|
|
23
|
+
report({
|
|
24
|
+
severity: "warning",
|
|
25
|
+
message: `${o.export} (${o.sqlName}) is in the public schema; qualify it with one of the declared schemas`,
|
|
26
|
+
entity: o.export,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|