@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,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Names in a Postgres declaration, read against the target major's catalog:
|
|
3
|
+
* column types, function calls, index access methods and operator classes.
|
|
4
|
+
* SQLPG119 to SQLPG126 share it.
|
|
5
|
+
*
|
|
6
|
+
* The line every check here keeps: a name the catalog or the project can
|
|
7
|
+
* answer for is checked; a name qualified with a schema the project does not
|
|
8
|
+
* declare (`billing.money`, `pg_catalog.int4`) is a plain-text reference and
|
|
9
|
+
* left to the server; a name the core catalog lacks is a warning, not an
|
|
10
|
+
* error, when the project declares an extension, since extension objects are
|
|
11
|
+
* not in the catalog (`overlays/extension-objects.ts` covers the common ones).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
15
|
+
import { COLUMN_TYPES, FUNCTIONS, INDEX_ACCESS_METHODS, KEYWORDS, OPERATOR_CLASSES } from "../../generated/postgres";
|
|
16
|
+
import { EXTENSION_OBJECTS } from "../../postgres/overlays/extension-objects";
|
|
17
|
+
import { GRAMMAR_TYPE_SPELLINGS, SERIAL_TYPES, SERIAL_WIDTH_SPELLINGS, TYPE_SPELLINGS } from "../../postgres/overlays/types";
|
|
18
|
+
import { isTrivia, tokenizeText, type Token } from "../../postgres/tokens";
|
|
19
|
+
import type { ColumnTypeSpec } from "../../postgres/catalog-types";
|
|
20
|
+
import { targetMajor, type PgTable } from "./postgres-helpers";
|
|
21
|
+
import { postgresObjects, type OutputObject } from "./sql-helpers";
|
|
22
|
+
|
|
23
|
+
// ── Catalog lookups ────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
type Presence = true | readonly number[] | undefined;
|
|
26
|
+
const presentAt = (p: Presence, major: number): boolean => p === true || (Array.isArray(p) && p.includes(major));
|
|
27
|
+
const majorsOf = (p: Presence): readonly number[] => (Array.isArray(p) ? p : []);
|
|
28
|
+
|
|
29
|
+
const columnTypes = COLUMN_TYPES as Readonly<Record<string, ColumnTypeSpec>>;
|
|
30
|
+
|
|
31
|
+
/** Every spelling of a built-in type (lower case, single spaces) to its `format_type()` name. */
|
|
32
|
+
const BUILTIN_SPELLINGS: ReadonlyMap<string, string> = (() => {
|
|
33
|
+
const m = new Map<string, string>();
|
|
34
|
+
for (const [sql, spec] of Object.entries(columnTypes)) {
|
|
35
|
+
m.set(sql, sql);
|
|
36
|
+
if (spec.catalogName) m.set(spec.catalogName, sql);
|
|
37
|
+
for (const a of spec.aliases) if (!m.has(a)) m.set(a, sql);
|
|
38
|
+
}
|
|
39
|
+
for (const [a, sql] of [...Object.entries(TYPE_SPELLINGS), ...Object.entries(GRAMMAR_TYPE_SPELLINGS)]) if (!m.has(a)) m.set(a, sql);
|
|
40
|
+
// `char` alone is `character` (bpchar); quoted, `"char"` is the one-byte type.
|
|
41
|
+
m.set("char", "character");
|
|
42
|
+
return m;
|
|
43
|
+
})();
|
|
44
|
+
|
|
45
|
+
const SERIALS: Readonly<Record<string, string>> = { ...SERIAL_TYPES, ...SERIAL_WIDTH_SPELLINGS };
|
|
46
|
+
|
|
47
|
+
/** The words that may follow `interval`: its fields. */
|
|
48
|
+
const INTERVAL_FIELDS = new Set(["year", "month", "day", "hour", "minute", "second", "to"]);
|
|
49
|
+
|
|
50
|
+
/** The catalog category of a built-in type (`N` numeric, `S` string...). */
|
|
51
|
+
export const categoryOf = (canonical: string): string | undefined => columnTypes[canonical]?.category;
|
|
52
|
+
|
|
53
|
+
/** The parameters a built-in type takes, from the overlay. */
|
|
54
|
+
export const parametersOf = (canonical: string) => columnTypes[canonical]?.parameters ?? [];
|
|
55
|
+
|
|
56
|
+
// ── Identifiers ────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/** An identifier's value: a quoted one as written, a bare one folded to lower case. */
|
|
59
|
+
export function identValue(text: string): string {
|
|
60
|
+
const t = text.trim();
|
|
61
|
+
if (t.startsWith('"') && t.endsWith('"') && t.length >= 2) return t.slice(1, -1).replace(/""/g, '"');
|
|
62
|
+
return t.toLowerCase();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Split on commas at depth 0. */
|
|
66
|
+
export function splitTopLevel(text: string): string[] {
|
|
67
|
+
const out: string[] = [];
|
|
68
|
+
let depth = 0;
|
|
69
|
+
let quote: string | undefined;
|
|
70
|
+
let cur = "";
|
|
71
|
+
for (const ch of text) {
|
|
72
|
+
if (quote) {
|
|
73
|
+
cur += ch;
|
|
74
|
+
if (ch === quote) quote = undefined;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (ch === '"' || ch === "'") quote = ch;
|
|
78
|
+
else if (ch === "(" || ch === "[") depth++;
|
|
79
|
+
else if (ch === ")" || ch === "]") depth--;
|
|
80
|
+
else if (ch === "," && depth === 0) {
|
|
81
|
+
out.push(cur.trim());
|
|
82
|
+
cur = "";
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
cur += ch;
|
|
86
|
+
}
|
|
87
|
+
if (cur.trim()) out.push(cur.trim());
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The bare column names of an `INCLUDE` list as the output keeps it (`b, "X"`, or `(b, c)`). */
|
|
92
|
+
export function includeColumns(text: string | undefined): string[] {
|
|
93
|
+
if (!text) return [];
|
|
94
|
+
const inner = text.trim().replace(/^\(/, "").replace(/\)$/, "");
|
|
95
|
+
return splitTopLevel(inner).filter((s) => /^("([^"]|"")+"|[A-Za-z_][\w$]*)$/.test(s)).map(identValue);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function significant(text: string): Token[] | undefined {
|
|
99
|
+
try {
|
|
100
|
+
return tokenizeText(text, 0).filter((t) => !isTrivia(t));
|
|
101
|
+
} catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const isName = (t: Token | undefined): boolean => t?.kind === "ident" || t?.kind === "qident";
|
|
107
|
+
const isPunct = (t: Token | undefined, p: string): boolean => t?.kind === "punct" && t.text === p;
|
|
108
|
+
const nameOf = (t: Token): string => (t.kind === "qident" ? identValue(t.text) : t.text.toLowerCase());
|
|
109
|
+
|
|
110
|
+
// ── The project ────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/** What the build declares, for resolving a name to it. */
|
|
113
|
+
export interface PgProject {
|
|
114
|
+
major: number;
|
|
115
|
+
objects: OutputObject[];
|
|
116
|
+
byExport: Map<string, OutputObject>;
|
|
117
|
+
schemas: Set<string>;
|
|
118
|
+
/** Declared extension names. */
|
|
119
|
+
extensions: Set<string>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function projectOf(ctx: PostSynthContext): PgProject {
|
|
123
|
+
return projectFromObjects(postgresObjects(ctx), targetMajor(ctx));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The project a set of output objects declares, at `major`: for a reader that holds the build output itself (`chant sql types`). */
|
|
127
|
+
export function projectFromObjects(objects: OutputObject[], major: number): PgProject {
|
|
128
|
+
const byExport = new Map(objects.map((o) => [o.export, o]));
|
|
129
|
+
const schemas = new Set(objects.filter((o) => o.type === "Postgres::Schema").map((o) => o.name));
|
|
130
|
+
const exts = objects.filter((o) => o.type === "Postgres::Extension");
|
|
131
|
+
return {
|
|
132
|
+
major,
|
|
133
|
+
objects,
|
|
134
|
+
byExport,
|
|
135
|
+
schemas,
|
|
136
|
+
extensions: new Set(exts.map((o) => o.name)),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const TYPE_OBJECTS = new Set(["Postgres::Enum", "Postgres::Domain", "Postgres::Table", "Postgres::View", "Postgres::MaterializedView"]);
|
|
141
|
+
const ROUTINES = new Set(["Postgres::Function", "Postgres::Procedure"]);
|
|
142
|
+
|
|
143
|
+
function declared(p: PgProject, kinds: ReadonlySet<string>, schema: string | undefined, name: string): OutputObject | undefined {
|
|
144
|
+
return p.objects.find((o) => kinds.has(o.type) && o.name === name && (schema === undefined || o.schema === schema));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The extension that provides `name` in `section`, if the overlay knows one; declared ones first. */
|
|
148
|
+
function extensionFor(p: PgProject, test: (e: (typeof EXTENSION_OBJECTS)[string]) => boolean): { name: string; declared: boolean } | undefined {
|
|
149
|
+
let found: { name: string; declared: boolean } | undefined;
|
|
150
|
+
for (const [name, e] of Object.entries(EXTENSION_OBJECTS)) {
|
|
151
|
+
if (!test(e)) continue;
|
|
152
|
+
if (p.extensions.has(name)) return { name, declared: true };
|
|
153
|
+
found ??= { name, declared: false };
|
|
154
|
+
}
|
|
155
|
+
return found;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A name the core catalog does not have: the finding's severity and a note, or nothing when an extension accounts for it. */
|
|
159
|
+
export interface Miss {
|
|
160
|
+
severity: "error" | "warning";
|
|
161
|
+
/** `, which comes from the pg_trgm extension the project does not declare` and the like. */
|
|
162
|
+
note: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function miss(p: PgProject, ext: { name: string; declared: boolean } | undefined): Miss | undefined {
|
|
166
|
+
if (ext?.declared) return undefined;
|
|
167
|
+
if (ext) return { severity: "error", note: `; it comes from the ${ext.name} extension, which the project does not declare` };
|
|
168
|
+
if (p.extensions.size > 0) return { severity: "warning", note: `; if an extension (${[...p.extensions].sort().join(", ")}) provides it, ignore this` };
|
|
169
|
+
return { severity: "error", note: "" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** A schema-qualified name the project cannot answer for: one in a schema it does not declare. */
|
|
173
|
+
const outsideProject = (p: PgProject, schema: string): boolean => !p.schemas.has(schema);
|
|
174
|
+
|
|
175
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
export interface ParsedType {
|
|
178
|
+
schema?: string;
|
|
179
|
+
/** The name: words joined by one space, bare words lower case. */
|
|
180
|
+
name: string;
|
|
181
|
+
quoted: boolean;
|
|
182
|
+
/** The modifiers of the first `(...)`, as written. */
|
|
183
|
+
modifiers?: string[];
|
|
184
|
+
array: boolean;
|
|
185
|
+
/** `interval`'s fields, upper case (`DAY TO SECOND`). */
|
|
186
|
+
fields?: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** A type as written in a column, domain or sequence: `numeric(12,2)`, `time(3) with time zone`, `app.mood[]`. Undefined when it is not a plain type name. */
|
|
190
|
+
export function parseType(text: string | undefined): ParsedType | undefined {
|
|
191
|
+
if (!text) return undefined;
|
|
192
|
+
const toks = significant(text);
|
|
193
|
+
if (!toks || toks.length === 0) return undefined;
|
|
194
|
+
const words: string[] = [];
|
|
195
|
+
let schema: string | undefined;
|
|
196
|
+
let quoted = false;
|
|
197
|
+
let modifiers: string[] | undefined;
|
|
198
|
+
let array = false;
|
|
199
|
+
for (let i = 0; i < toks.length; i++) {
|
|
200
|
+
const t = toks[i]!;
|
|
201
|
+
if (isName(t)) {
|
|
202
|
+
if (t.kind === "ident" && t.text.toLowerCase() === "array" && words.length > 0) {
|
|
203
|
+
array = true;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (array) return undefined;
|
|
207
|
+
if (isPunct(toks[i + 1], ".") && words.length === 0 && schema === undefined && isName(toks[i + 2])) {
|
|
208
|
+
schema = nameOf(t);
|
|
209
|
+
i++;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (t.kind === "qident") quoted = true;
|
|
213
|
+
words.push(nameOf(t));
|
|
214
|
+
} else if (isPunct(t, "(")) {
|
|
215
|
+
let depth = 1;
|
|
216
|
+
let j = i + 1;
|
|
217
|
+
for (; j < toks.length && depth > 0; j++) {
|
|
218
|
+
if (isPunct(toks[j], "(")) depth++;
|
|
219
|
+
else if (isPunct(toks[j], ")")) depth--;
|
|
220
|
+
}
|
|
221
|
+
if (depth !== 0) return undefined;
|
|
222
|
+
if (modifiers === undefined) {
|
|
223
|
+
const start = t.end;
|
|
224
|
+
const end = toks[j - 1]!.start;
|
|
225
|
+
modifiers = splitTopLevel(text.slice(start, end));
|
|
226
|
+
}
|
|
227
|
+
i = j - 1;
|
|
228
|
+
} else if (isPunct(t, "[")) {
|
|
229
|
+
array = true;
|
|
230
|
+
let j = i + 1;
|
|
231
|
+
while (j < toks.length && !isPunct(toks[j], "]")) j++;
|
|
232
|
+
i = j;
|
|
233
|
+
} else {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (words.length === 0) return undefined;
|
|
238
|
+
if (!quoted && words[0] === "interval" && words.length > 1 && words.slice(1).every((w) => INTERVAL_FIELDS.has(w))) {
|
|
239
|
+
return { schema, name: "interval", quoted, modifiers, array, fields: words.slice(1).join(" ").toUpperCase() };
|
|
240
|
+
}
|
|
241
|
+
return { schema, name: words.join(" "), quoted, modifiers, array };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export type TypeResolution =
|
|
245
|
+
| { kind: "builtin"; canonical: string; serial?: boolean }
|
|
246
|
+
| { kind: "declared"; object: OutputObject }
|
|
247
|
+
/** Qualified with a schema the project does not declare, or provided by a declared extension: not checked. */
|
|
248
|
+
| { kind: "outside" }
|
|
249
|
+
| { kind: "missing"; miss: Miss }
|
|
250
|
+
/** A built-in type the target major does not have. */
|
|
251
|
+
| { kind: "absent"; canonical: string };
|
|
252
|
+
|
|
253
|
+
/** Resolve a parsed type at the target major. `serial` admits `serial` and friends (a column). */
|
|
254
|
+
export function resolveType(p: PgProject, t: ParsedType, opts: { serial?: boolean } = {}): TypeResolution {
|
|
255
|
+
if (t.schema !== undefined) {
|
|
256
|
+
if (outsideProject(p, t.schema)) return { kind: "outside" };
|
|
257
|
+
const d = declared(p, TYPE_OBJECTS, t.schema, t.name);
|
|
258
|
+
if (d) return { kind: "declared", object: d };
|
|
259
|
+
const m = miss(p, extensionFor(p, (e) => e.types?.includes(t.name) ?? false));
|
|
260
|
+
return m ? { kind: "missing", miss: m } : { kind: "outside" };
|
|
261
|
+
}
|
|
262
|
+
if (!t.quoted) {
|
|
263
|
+
if (opts.serial && SERIALS[t.name] && !t.modifiers) return { kind: "builtin", canonical: SERIALS[t.name]!, serial: true };
|
|
264
|
+
if (t.name === "float" && t.modifiers?.length === 1 && /^\d+$/.test(t.modifiers[0]!)) {
|
|
265
|
+
return { kind: "builtin", canonical: Number(t.modifiers[0]) <= 24 ? "real" : "double precision" };
|
|
266
|
+
}
|
|
267
|
+
const canonical = BUILTIN_SPELLINGS.get(t.name) ?? (t.name.startsWith("_") ? BUILTIN_SPELLINGS.get(t.name.slice(1)) : undefined);
|
|
268
|
+
if (canonical) return { kind: "builtin", canonical };
|
|
269
|
+
} else {
|
|
270
|
+
if (t.name === "char") return { kind: "builtin", canonical: '"char"' };
|
|
271
|
+
const byCatalog = Object.entries(columnTypes).find(([sql, s]) => (s.catalogName ?? sql) === t.name);
|
|
272
|
+
if (byCatalog) return { kind: "builtin", canonical: byCatalog[0] };
|
|
273
|
+
}
|
|
274
|
+
const d = declared(p, TYPE_OBJECTS, undefined, t.name);
|
|
275
|
+
if (d) return { kind: "declared", object: d };
|
|
276
|
+
const m = miss(p, extensionFor(p, (e) => e.types?.includes(t.name) ?? false));
|
|
277
|
+
return m ? { kind: "missing", miss: m } : { kind: "outside" };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** A type as the user wrote it, for a message. */
|
|
281
|
+
export const typeLabel = (t: ParsedType): string => `${t.schema ? `${t.schema}.` : ""}${t.name}`;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* A comparable identity for a column's type, for a foreign key: a built-in
|
|
285
|
+
* type's catalog category (`U`, the catch-all, by name), a domain's base type,
|
|
286
|
+
* an enum or row type by name; `[]` for an array. Undefined when it cannot be
|
|
287
|
+
* told (a type outside the project).
|
|
288
|
+
*/
|
|
289
|
+
export function typeIdentity(p: PgProject, text: string | undefined, depth = 0): string | undefined {
|
|
290
|
+
const t = parseType(text);
|
|
291
|
+
if (!t || depth > 5) return undefined;
|
|
292
|
+
const r = resolveType(p, t, { serial: true });
|
|
293
|
+
const suffix = t.array ? "[]" : "";
|
|
294
|
+
if (r.kind === "builtin") {
|
|
295
|
+
const cat = categoryOf(r.canonical);
|
|
296
|
+
if (!cat) return undefined;
|
|
297
|
+
return (cat === "U" ? `U:${r.canonical}` : cat) + suffix;
|
|
298
|
+
}
|
|
299
|
+
if (r.kind === "declared") {
|
|
300
|
+
if (r.object.type === "Postgres::Domain") {
|
|
301
|
+
const base = typeIdentity(p, r.object.dataType as string | undefined, depth + 1);
|
|
302
|
+
return base === undefined ? undefined : base + suffix;
|
|
303
|
+
}
|
|
304
|
+
return `${r.object.type}:${r.object.sqlName as string}${suffix}`;
|
|
305
|
+
}
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Calls ──────────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Words that take a parenthesis in an expression without being a call to a
|
|
313
|
+
* function in `pg_proc`. Reserved and column-name key words (`COALESCE`,
|
|
314
|
+
* `CAST`, `EXTRACT`, `ROW`, `ARRAY`, `EXISTS`, `IN`, `ANY`, `SUBSTRING`...)
|
|
315
|
+
* are not function names to the grammar and are skipped by their category;
|
|
316
|
+
* these are the unreserved ones that read the same way.
|
|
317
|
+
*/
|
|
318
|
+
const NOT_CALLS = new Set(["filter", "over", "within", "zone", "varying", "precision", "value", "and", "or", "not", "as", "then", "else", "when"]);
|
|
319
|
+
|
|
320
|
+
export interface Call {
|
|
321
|
+
schema?: string;
|
|
322
|
+
name: string;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** The function calls in an expression: a name directly followed by `(`, outside string literals, not a key word construct. */
|
|
326
|
+
export function callsIn(expr: string | undefined): Call[] {
|
|
327
|
+
if (!expr) return [];
|
|
328
|
+
const toks = significant(expr);
|
|
329
|
+
if (!toks) return [];
|
|
330
|
+
const out: Call[] = [];
|
|
331
|
+
for (let i = 0; i < toks.length; i++) {
|
|
332
|
+
const t = toks[i]!;
|
|
333
|
+
if (!isName(t)) continue;
|
|
334
|
+
const prev = toks[i - 1];
|
|
335
|
+
if (isPunct(prev, ".")) continue;
|
|
336
|
+
// A type's modifiers after a cast: `x::numeric(10, 2)`, `CAST(x AS varchar(3))`.
|
|
337
|
+
if (prev?.kind === "op" && prev.text === "::") continue;
|
|
338
|
+
if (prev?.kind === "ident" && prev.text.toLowerCase() === "as") continue;
|
|
339
|
+
if (isPunct(toks[i + 1], ".") && isName(toks[i + 2]) && isPunct(toks[i + 3], "(")) {
|
|
340
|
+
out.push({ schema: nameOf(t), name: nameOf(toks[i + 2]!) });
|
|
341
|
+
i += 2;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (!isPunct(toks[i + 1], "(")) continue;
|
|
345
|
+
if (t.kind === "ident") {
|
|
346
|
+
const w = t.text.toLowerCase();
|
|
347
|
+
const cat = KEYWORDS[w];
|
|
348
|
+
if (cat === "R" || cat === "C" || cat === "T") continue;
|
|
349
|
+
if (NOT_CALLS.has(w)) continue;
|
|
350
|
+
}
|
|
351
|
+
out.push({ name: nameOf(t) });
|
|
352
|
+
}
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export type CallResolution = { ok: true } | { ok: false; miss: Miss; since?: number; until?: number };
|
|
357
|
+
|
|
358
|
+
/** Resolve a call at the target major. */
|
|
359
|
+
export function resolveCall(p: PgProject, c: Call): CallResolution {
|
|
360
|
+
if (c.schema !== undefined) {
|
|
361
|
+
if (outsideProject(p, c.schema)) return { ok: true };
|
|
362
|
+
if (declared(p, ROUTINES, c.schema, c.name)) return { ok: true };
|
|
363
|
+
const m = miss(p, extensionFor(p, (e) => providesFunction(e, c.name)));
|
|
364
|
+
return m ? { ok: false, miss: m } : { ok: true };
|
|
365
|
+
}
|
|
366
|
+
const presence = (FUNCTIONS as Readonly<Record<string, Presence>>)[c.name];
|
|
367
|
+
if (presentAt(presence, p.major)) return { ok: true };
|
|
368
|
+
if (declared(p, ROUTINES, undefined, c.name)) return { ok: true };
|
|
369
|
+
// A function-style cast: `int4(x)`, `text(x)`, or a declared type's name.
|
|
370
|
+
if (BUILTIN_SPELLINGS.has(c.name) || declared(p, TYPE_OBJECTS, undefined, c.name)) return { ok: true };
|
|
371
|
+
if (presence !== undefined) {
|
|
372
|
+
const ms = majorsOf(presence);
|
|
373
|
+
const since = ms[0]! > p.major ? ms[0] : undefined;
|
|
374
|
+
const until = ms[ms.length - 1]! < p.major ? ms[ms.length - 1] : undefined;
|
|
375
|
+
return { ok: false, miss: { severity: "error", note: "" }, since, until };
|
|
376
|
+
}
|
|
377
|
+
const m = miss(p, extensionFor(p, (e) => providesFunction(e, c.name)));
|
|
378
|
+
return m ? { ok: false, miss: m } : { ok: true };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function providesFunction(e: (typeof EXTENSION_OBJECTS)[string], name: string): boolean {
|
|
382
|
+
return (e.functions?.includes(name) ?? false) || (e.functionPrefixes?.some((pre) => name.startsWith(pre)) ?? false);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** The expressions of a table, domain or index that may call functions, each with where it sits. */
|
|
386
|
+
export function expressionsOf(o: OutputObject): Array<{ where: string; expr: string }> {
|
|
387
|
+
const out: Array<{ where: string; expr: string }> = [];
|
|
388
|
+
if (o.type === "Postgres::Table") {
|
|
389
|
+
const t = o as PgTable;
|
|
390
|
+
for (const c of t.columns) {
|
|
391
|
+
if (c.default) out.push({ where: `column ${c.name}'s DEFAULT`, expr: c.default });
|
|
392
|
+
if (c.generated?.expr) out.push({ where: `generated column ${c.name}`, expr: c.generated.expr });
|
|
393
|
+
}
|
|
394
|
+
for (const k of t.checks) out.push({ where: k.name ? `CHECK ${k.name}` : "a CHECK", expr: k.expr });
|
|
395
|
+
} else if (o.type === "Postgres::Domain") {
|
|
396
|
+
if (o.default) out.push({ where: "its DEFAULT", expr: o.default as string });
|
|
397
|
+
for (const k of (o.checks as Array<{ name?: string; expr: string }> | undefined) ?? []) out.push({ where: k.name ? `CHECK ${k.name}` : "a CHECK", expr: k.expr });
|
|
398
|
+
} else if (o.type === "Postgres::Index") {
|
|
399
|
+
for (const e of (o.elements as Array<{ expr: string; column?: string }>) ?? []) {
|
|
400
|
+
if (e.column) continue;
|
|
401
|
+
const el = parseIndexElement(e.expr);
|
|
402
|
+
out.push({ where: "an index expression", expr: el?.expr ?? e.expr });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ── Index elements ─────────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
export interface IndexElement {
|
|
411
|
+
/** The column or expression, without its collation, operator class or ordering. */
|
|
412
|
+
expr: string;
|
|
413
|
+
opclass?: { schema?: string; name: string };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const ORDERING = new Set(["asc", "desc", "nulls", "first", "last"]);
|
|
417
|
+
|
|
418
|
+
/** An index element (`(lower(x)) text_pattern_ops DESC`, `x gin_trgm_ops`) split into its expression and operator class. */
|
|
419
|
+
export function parseIndexElement(text: string): IndexElement | undefined {
|
|
420
|
+
const toks = significant(text);
|
|
421
|
+
if (!toks || toks.length === 0) return undefined;
|
|
422
|
+
let i = 0;
|
|
423
|
+
const skipGroup = (at: number): number => {
|
|
424
|
+
let depth = 0;
|
|
425
|
+
for (let j = at; j < toks.length; j++) {
|
|
426
|
+
if (isPunct(toks[j], "(")) depth++;
|
|
427
|
+
else if (isPunct(toks[j], ")") && --depth === 0) return j + 1;
|
|
428
|
+
}
|
|
429
|
+
return toks.length;
|
|
430
|
+
};
|
|
431
|
+
if (isPunct(toks[0], "(")) i = skipGroup(0);
|
|
432
|
+
else if (isName(toks[0])) {
|
|
433
|
+
i = 1;
|
|
434
|
+
while (isPunct(toks[i], ".") && isName(toks[i + 1])) i += 2;
|
|
435
|
+
if (isPunct(toks[i], "(")) i = skipGroup(i);
|
|
436
|
+
} else return undefined;
|
|
437
|
+
const expr = text.slice(toks[0]!.start, toks[i - 1]!.end);
|
|
438
|
+
if (toks[i]?.kind === "ident" && toks[i]!.text.toLowerCase() === "collate") {
|
|
439
|
+
i += 2;
|
|
440
|
+
while (isPunct(toks[i], ".") && isName(toks[i + 1])) i += 2;
|
|
441
|
+
}
|
|
442
|
+
const t = toks[i];
|
|
443
|
+
if (!isName(t) || (t!.kind === "ident" && ORDERING.has(t!.text.toLowerCase()))) return { expr };
|
|
444
|
+
if (isPunct(toks[i + 1], ".") && isName(toks[i + 2])) return { expr, opclass: { schema: nameOf(t!), name: nameOf(toks[i + 2]!) } };
|
|
445
|
+
return { expr, opclass: { name: nameOf(t!) } };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export type MethodResolution = { ok: true } | { ok: false; miss: Miss; since?: number; until?: number };
|
|
449
|
+
|
|
450
|
+
/** An index access method at the target major. */
|
|
451
|
+
export function resolveMethod(p: PgProject, method: string): MethodResolution {
|
|
452
|
+
const presence = (INDEX_ACCESS_METHODS as Readonly<Record<string, Presence>>)[method];
|
|
453
|
+
if (presentAt(presence, p.major)) return { ok: true };
|
|
454
|
+
if (presence !== undefined) return { ok: false, miss: { severity: "error", note: "" } };
|
|
455
|
+
const m = miss(p, extensionFor(p, (e) => e.accessMethods?.includes(method) ?? false));
|
|
456
|
+
return m ? { ok: false, miss: m } : { ok: true };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export type OpClassResolution = { ok: true } | { ok: false; miss: Miss; otherMethods?: string[] };
|
|
460
|
+
|
|
461
|
+
/** An operator class for `method` at the target major. */
|
|
462
|
+
export function resolveOpClass(p: PgProject, method: string, opclass: { schema?: string; name: string }): OpClassResolution {
|
|
463
|
+
if (opclass.schema !== undefined && outsideProject(p, opclass.schema)) return { ok: true };
|
|
464
|
+
const classes = OPERATOR_CLASSES as Readonly<Record<string, Readonly<Record<string, Presence>>>>;
|
|
465
|
+
if (presentAt(classes[method]?.[opclass.name], p.major)) return { ok: true };
|
|
466
|
+
const ext = extensionFor(p, (e) => e.opclasses?.[method]?.includes(opclass.name) ?? false);
|
|
467
|
+
if (ext?.declared) return { ok: true };
|
|
468
|
+
if (!ext) {
|
|
469
|
+
const others = Object.keys(classes).filter((am) => am !== method && presentAt(classes[am]![opclass.name], p.major));
|
|
470
|
+
const extOthers = Object.entries(EXTENSION_OBJECTS).flatMap(([, e]) => Object.entries(e.opclasses ?? {}).filter(([am, names]) => am !== method && names.includes(opclass.name)).map(([am]) => am));
|
|
471
|
+
const all = [...new Set([...others, ...extOthers])].sort();
|
|
472
|
+
if (all.length > 0) return { ok: false, miss: { severity: "error", note: "" }, otherMethods: all };
|
|
473
|
+
}
|
|
474
|
+
const m = miss(p, ext);
|
|
475
|
+
return m ? { ok: false, miss: m } : { ok: true };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ── Tables ─────────────────────────────────────────────────────────────
|
|
479
|
+
|
|
480
|
+
/** The table an interpolated reference names (`references`, `table`, a grant's object): the entity when it is one, else undefined. */
|
|
481
|
+
export function interpolatedTable(p: PgProject, ref: unknown, sqlName: string | undefined): PgTable | undefined {
|
|
482
|
+
if (typeof ref !== "string") return undefined;
|
|
483
|
+
const o = p.byExport.get(ref);
|
|
484
|
+
if (!o || o.type !== "Postgres::Table") return undefined;
|
|
485
|
+
if (sqlName !== undefined && o.sqlName !== sqlName) return undefined;
|
|
486
|
+
return o as PgTable;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Every column a table has, its own and those it inherits or copies from a
|
|
491
|
+
* declared parent; undefined when some come from somewhere the build cannot
|
|
492
|
+
* see (a plain-text parent, `LIKE`, `OF type`).
|
|
493
|
+
*/
|
|
494
|
+
export function columnsOf(p: PgProject, t: PgTable, seen = new Set<string>()): Map<string, PgTable["columns"][number]> | undefined {
|
|
495
|
+
if (seen.has(t.export)) return undefined;
|
|
496
|
+
seen.add(t.export);
|
|
497
|
+
if (t.ofType !== undefined || t.like.length > 0) return undefined;
|
|
498
|
+
const out = new Map<string, PgTable["columns"][number]>();
|
|
499
|
+
const parents: unknown[] = [...(t.inherits ?? []), ...(t.partitionOf !== undefined ? [t.partitionOf] : [])];
|
|
500
|
+
for (const parent of parents) {
|
|
501
|
+
const pt = interpolatedTable(p, parent, undefined);
|
|
502
|
+
if (!pt) return undefined;
|
|
503
|
+
const pc = columnsOf(p, pt, seen);
|
|
504
|
+
if (!pc) return undefined;
|
|
505
|
+
for (const [k, v] of pc) out.set(k, v);
|
|
506
|
+
}
|
|
507
|
+
// A partition's `col WITH OPTIONS ...` carries no type; the parent's column stands.
|
|
508
|
+
for (const c of t.columns) if (c.type !== undefined || !out.has(c.name)) out.set(c.name, c);
|
|
509
|
+
return out;
|
|
510
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { clickhouseObjects, postgresObjects, type OutputObject } from "./sql-helpers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Kinds whose name is not the object's identity on the server, so two
|
|
6
|
+
* exports may share it: grants; Postgres functions and procedures, which
|
|
7
|
+
* overload; triggers and policies, named per table; ClickHouse row policies,
|
|
8
|
+
* also per table.
|
|
9
|
+
*/
|
|
10
|
+
const NOT_UNIQUE = new Set([
|
|
11
|
+
"ClickHouse::Grant",
|
|
12
|
+
"ClickHouse::RowPolicy",
|
|
13
|
+
"Postgres::Grant",
|
|
14
|
+
"Postgres::DefaultPrivileges",
|
|
15
|
+
"Postgres::Function",
|
|
16
|
+
"Postgres::Procedure",
|
|
17
|
+
"Postgres::Trigger",
|
|
18
|
+
"Postgres::Policy",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const kindOf = (type: string): string =>
|
|
22
|
+
type
|
|
23
|
+
.replace(/^(ClickHouse|Postgres)::/, "")
|
|
24
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
25
|
+
.toLowerCase();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SQL101: two exports declare the same object, the same kind under the same
|
|
29
|
+
* qualified name (`shop.events` twice). The build writes both `CREATE`
|
|
30
|
+
* statements, and the second fails on the server, or replaces the first
|
|
31
|
+
* under `OR REPLACE`.
|
|
32
|
+
*/
|
|
33
|
+
export const sql101: PostSynthCheck = {
|
|
34
|
+
id: "SQL101",
|
|
35
|
+
description: "Two exports declare the same object",
|
|
36
|
+
check(ctx) {
|
|
37
|
+
const out: PostSynthDiagnostic[] = [];
|
|
38
|
+
const scan = (dialect: string, objects: readonly OutputObject[]) => {
|
|
39
|
+
const first = new Map<string, OutputObject>();
|
|
40
|
+
for (const o of objects) {
|
|
41
|
+
const name = typeof o.sqlName === "string" && o.sqlName !== "" ? o.sqlName : undefined;
|
|
42
|
+
if (!name || NOT_UNIQUE.has(o.type)) continue;
|
|
43
|
+
const key = `${o.type}\u0000${name}`;
|
|
44
|
+
const prior = first.get(key);
|
|
45
|
+
if (!prior) {
|
|
46
|
+
first.set(key, o);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
out.push({
|
|
50
|
+
checkId: "SQL101",
|
|
51
|
+
severity: "error",
|
|
52
|
+
message: `${prior.export} and ${o.export} both declare ${dialect} ${kindOf(o.type)} ${name}; the build would create it twice`,
|
|
53
|
+
entity: o.export,
|
|
54
|
+
lexicon: "sql",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
scan("ClickHouse", clickhouseObjects(ctx));
|
|
59
|
+
scan("Postgres", postgresObjects(ctx));
|
|
60
|
+
return out;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { sqlTypesPath, sqlTypesState } from "../../sql-types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SQL102: `sql-types.d.ts` no longer matches what the build declares.
|
|
6
|
+
*
|
|
7
|
+
* tsc checks a typed tag's columns, and the foreign-key and sort-key slots,
|
|
8
|
+
* against the generated file, never against the template text, so a stale file
|
|
9
|
+
* keeps checking the old types. The check renders the declarations in memory
|
|
10
|
+
* (the render `chant sql types` writes) and compares them with the file in the
|
|
11
|
+
* project's source directory. Silent when the context carries no project
|
|
12
|
+
* directory (a hand-built one) or the file does not exist, so a project that
|
|
13
|
+
* has not adopted typed columns, or keeps the file elsewhere, sees nothing.
|
|
14
|
+
* `chant sql types --check` is the same comparison without a build, and a
|
|
15
|
+
* workspace `generated` entry runs it under `chant workspace check --generated`.
|
|
16
|
+
*/
|
|
17
|
+
export const sql102: PostSynthCheck = {
|
|
18
|
+
id: "SQL102",
|
|
19
|
+
description: "sql-types.d.ts differs from what the build declares",
|
|
20
|
+
check(ctx) {
|
|
21
|
+
if (!ctx.projectDir) return [];
|
|
22
|
+
const file = sqlTypesPath(ctx.projectDir, ctx.projectConfig);
|
|
23
|
+
const state = sqlTypesState(ctx.entities, file, { projectDir: ctx.projectDir, config: ctx.projectConfig });
|
|
24
|
+
if (state !== "stale") return [];
|
|
25
|
+
return [
|
|
26
|
+
{
|
|
27
|
+
checkId: "SQL102",
|
|
28
|
+
severity: "error",
|
|
29
|
+
message: `${file} differs from what the build declares, so tsc checks columns against out-of-date types; run \`chant sql types\` to rewrite it`,
|
|
30
|
+
lexicon: "sql",
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
},
|
|
34
|
+
};
|