@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,81 @@
|
|
|
1
|
+
import type { LintContext, LintDiagnostic, LintRule } from "@intentius/chant/lint/rule";
|
|
2
|
+
import { parseCreate, unquote } from "../../clickhouse/parser";
|
|
3
|
+
import { isTrivia, SqlSyntaxError } from "../../clickhouse/tokens";
|
|
4
|
+
import { findTemplates, tokensOf } from "./templates";
|
|
5
|
+
import { declaredText, generatedNamesFor, nameAgrees, normalizedName, SQL_TYPES_FILE, typeArgumentName } from "./typed-names";
|
|
6
|
+
|
|
7
|
+
const MODULE = "@intentius/chant-lexicon-sql/clickhouse";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* SQLCH004: a typed ClickHouse tag whose type argument does not name the
|
|
11
|
+
* object its template declares, or is not in the generated `sql-types.d.ts`.
|
|
12
|
+
*
|
|
13
|
+
* `table<"shop.events">\`CREATE TABLE shop.events ...\``, and the same for
|
|
14
|
+
* `view` and `dictionary`, types `.columns` from the generated file's
|
|
15
|
+
* `SqlSchema["shop.events"]`. tsc does not compare the argument with the
|
|
16
|
+
* template's text, and an argument the file does not list falls back to
|
|
17
|
+
* unchecked columns, so a typo checks nothing and says nothing. The rule reads
|
|
18
|
+
* the declared name from the template (a database supplied by an interpolation
|
|
19
|
+
* is not known to lint, and is left out of the comparison) and the listed
|
|
20
|
+
* names from the nearest `sql-types.d.ts` above the file. With no generated
|
|
21
|
+
* file, only the first check runs. A name missing from the file is a warning,
|
|
22
|
+
* since an object added since the last `chant sql types` is stale and
|
|
23
|
+
* `chant sql types --check` is what fails on it.
|
|
24
|
+
*/
|
|
25
|
+
export const sqlch004: LintRule = {
|
|
26
|
+
id: "SQLCH004",
|
|
27
|
+
severity: "error",
|
|
28
|
+
category: "correctness",
|
|
29
|
+
description: "A ClickHouse tag's type argument does not name the object its template declares, or is not in sql-types.d.ts",
|
|
30
|
+
|
|
31
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
32
|
+
const source = context.sourceFile;
|
|
33
|
+
const out: LintDiagnostic[] = [];
|
|
34
|
+
let listed: Set<string> | undefined | null = null;
|
|
35
|
+
for (const found of findTemplates(source)) {
|
|
36
|
+
if (found.tag !== "table" && found.tag !== "view" && found.tag !== "dictionary") continue;
|
|
37
|
+
const arg = typeArgumentName(found.node);
|
|
38
|
+
if (!arg) continue;
|
|
39
|
+
const at = (() => {
|
|
40
|
+
const { line, character } = source.getLineAndCharacterOfPosition(arg.node.getStart(source));
|
|
41
|
+
return { line: line + 1, column: character + 1 };
|
|
42
|
+
})();
|
|
43
|
+
let declared: string[] | undefined;
|
|
44
|
+
try {
|
|
45
|
+
const tokens = tokensOf(found);
|
|
46
|
+
const node = parseCreate(tokens);
|
|
47
|
+
if (node.statement === found.tag) {
|
|
48
|
+
declared = tokens
|
|
49
|
+
.slice(node.name.from, node.name.to)
|
|
50
|
+
.filter((t) => !isTrivia(t) && !(t.kind === "punct" && t.text === "."))
|
|
51
|
+
.map((t) => (t.kind === "ref" ? "" : unquote(t.text)));
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (err instanceof SqlSyntaxError) continue; // SQLCH001 reports it
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
if (!declared) continue; // SQLCH001: the tag holds another statement
|
|
58
|
+
if (!nameAgrees(declared, arg.name)) {
|
|
59
|
+
out.push({
|
|
60
|
+
ruleId: "SQLCH004",
|
|
61
|
+
severity: "error",
|
|
62
|
+
message: `${found.tag}<"${arg.name}"> names another object than the template declares (${declaredText(declared)}); the type argument is the qualified name the build renders`,
|
|
63
|
+
file: context.filePath,
|
|
64
|
+
...at,
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (listed === null) listed = generatedNamesFor(context.filePath, MODULE);
|
|
69
|
+
if (listed && !listed.has(normalizedName(arg.name))) {
|
|
70
|
+
out.push({
|
|
71
|
+
ruleId: "SQLCH004",
|
|
72
|
+
severity: "warning",
|
|
73
|
+
message: `"${arg.name}" is not in ${SQL_TYPES_FILE}, so its columns are unchecked; run \`chant sql types\` to add it`,
|
|
74
|
+
file: context.filePath,
|
|
75
|
+
...at,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { LintContext, LintDiagnostic, LintRule } from "@intentius/chant/lint/rule";
|
|
2
|
+
import { parseStatements, SqlSyntaxError } from "../../postgres/parser";
|
|
3
|
+
import { findPostgresTemplates, postgresTokensOf } from "./postgres-templates";
|
|
4
|
+
import { declaredText, generatedNamesFor, nameAgrees, normalizedName, SQL_TYPES_FILE, typeArgumentName } from "./typed-names";
|
|
5
|
+
|
|
6
|
+
const MODULE = "@intentius/chant-lexicon-sql/postgres";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SQLPG005: a typed Postgres tag whose type argument does not name the object
|
|
10
|
+
* its template declares, or is not in the generated `sql-types.d.ts`.
|
|
11
|
+
*
|
|
12
|
+
* `table<"shop.orders">\`CREATE TABLE shop.orders ...\`` types `.columns` from
|
|
13
|
+
* the generated file's `SqlSchema["shop.orders"]`. tsc does not compare the
|
|
14
|
+
* argument with the template's text, and an argument the file does not list
|
|
15
|
+
* falls back to unchecked columns, so a typo checks nothing and says nothing.
|
|
16
|
+
* The rule reads the declared name from the template (a schema supplied by an
|
|
17
|
+
* interpolation is not known to lint, and is left out of the comparison) and
|
|
18
|
+
* the listed names from the nearest `sql-types.d.ts` above the file. With no
|
|
19
|
+
* generated file, only the first check runs. A name missing from the file is
|
|
20
|
+
* a warning, since a table added since the last `chant sql types` is stale
|
|
21
|
+
* and `chant sql types --check` is what fails on it.
|
|
22
|
+
*/
|
|
23
|
+
export const sqlpg005: LintRule = {
|
|
24
|
+
id: "SQLPG005",
|
|
25
|
+
severity: "error",
|
|
26
|
+
category: "correctness",
|
|
27
|
+
description: "A Postgres tag's type argument does not name the object its template declares, or is not in sql-types.d.ts",
|
|
28
|
+
|
|
29
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
30
|
+
const source = context.sourceFile;
|
|
31
|
+
const out: LintDiagnostic[] = [];
|
|
32
|
+
let listed: Set<string> | undefined | null = null;
|
|
33
|
+
for (const found of findPostgresTemplates(source)) {
|
|
34
|
+
if (found.tag !== "table" && found.tag !== "view") continue;
|
|
35
|
+
const arg = typeArgumentName(found.node);
|
|
36
|
+
if (!arg) continue;
|
|
37
|
+
const at = (() => {
|
|
38
|
+
const { line, character } = source.getLineAndCharacterOfPosition(arg.node.getStart(source));
|
|
39
|
+
return { line: line + 1, column: character + 1 };
|
|
40
|
+
})();
|
|
41
|
+
let declared: string[] | undefined;
|
|
42
|
+
try {
|
|
43
|
+
const node = parseStatements(postgresTokensOf(found))[0];
|
|
44
|
+
if (node && (node.statement === "table" || node.statement === "view")) declared = node.name.pieces;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err instanceof SqlSyntaxError) continue; // SQLPG001 reports it
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
if (!declared) continue; // SQLPG001: the tag holds another statement
|
|
50
|
+
if (!nameAgrees(declared, arg.name)) {
|
|
51
|
+
out.push({
|
|
52
|
+
ruleId: "SQLPG005",
|
|
53
|
+
severity: "error",
|
|
54
|
+
message: `${found.tag}<"${arg.name}"> names another object than the template declares (${declaredText(declared)}); the type argument is the qualified name the build renders`,
|
|
55
|
+
file: context.filePath,
|
|
56
|
+
...at,
|
|
57
|
+
});
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (listed === null) listed = generatedNamesFor(context.filePath, MODULE);
|
|
61
|
+
if (listed && !listed.has(normalizedName(arg.name))) {
|
|
62
|
+
out.push({
|
|
63
|
+
ruleId: "SQLPG005",
|
|
64
|
+
severity: "warning",
|
|
65
|
+
message: `"${arg.name}" is not in ${SQL_TYPES_FILE}, so its columns are unchecked; run \`chant sql types\` to add it`,
|
|
66
|
+
file: context.filePath,
|
|
67
|
+
...at,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import { mkdirSync, mkdtempSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { describe, expect, test } from "vitest";
|
|
6
|
+
import type { LintContext } from "@intentius/chant/lint/rule";
|
|
7
|
+
import { sqlch004 } from "./sqlch004";
|
|
8
|
+
import { sqlpg005 } from "./sqlpg005";
|
|
9
|
+
import { nameAgrees, namesInGenerated, normalizedName, splitQualified } from "./typed-names";
|
|
10
|
+
|
|
11
|
+
const PG = "@intentius/chant-lexicon-sql/postgres";
|
|
12
|
+
const CH = "@intentius/chant-lexicon-sql/clickhouse";
|
|
13
|
+
|
|
14
|
+
const ctx = (code: string, filePath = "schema.ts"): LintContext => ({
|
|
15
|
+
sourceFile: ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true),
|
|
16
|
+
entities: [],
|
|
17
|
+
filePath,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const generated = (module: string, names: string[]): string =>
|
|
21
|
+
`// Generated by chant — do not edit directly.\nimport type { Column } from "${module}";\n\ndeclare module "${module}" {\n interface SqlSchema {\n` +
|
|
22
|
+
names.map((n) => ` /** src/schema.ts */\n ${JSON.stringify(n)}: { columns: { id: Column<"bigint", { ts: string; nullable: false; cmp: "N" }> } };\n`).join("") +
|
|
23
|
+
" }\n}\n";
|
|
24
|
+
|
|
25
|
+
/** A project directory with the generated file in src/ and a source file under src/db/. */
|
|
26
|
+
function project(module: string, names: string[] | undefined): string {
|
|
27
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-3751-")));
|
|
28
|
+
mkdirSync(join(root, "src", "db"), { recursive: true });
|
|
29
|
+
writeFileSync(join(root, "chant.config.json"), "{}");
|
|
30
|
+
if (names) writeFileSync(join(root, "src", "sql-types.d.ts"), generated(module, names));
|
|
31
|
+
return join(root, "src", "db", "schema.ts");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("typed-names helpers", () => {
|
|
35
|
+
test("splitQualified takes quotes off and splits outside them", () => {
|
|
36
|
+
expect(splitQualified('shop."Order.Items"')).toEqual(["shop", "Order.Items"]);
|
|
37
|
+
expect(splitQualified("orders")).toEqual(["orders"]);
|
|
38
|
+
expect(normalizedName('"shop"."orders"')).toBe(normalizedName("shop.orders"));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("nameAgrees compares from the last piece and skips interpolations", () => {
|
|
42
|
+
expect(nameAgrees(["shop", "orders"], "shop.orders")).toBe(true);
|
|
43
|
+
expect(nameAgrees(["orders"], "shop.orders")).toBe(true);
|
|
44
|
+
expect(nameAgrees(["", "orders"], "shop.orders")).toBe(true);
|
|
45
|
+
expect(nameAgrees(["shop", "orders"], "orders")).toBe(false);
|
|
46
|
+
expect(nameAgrees(["shop", "orders"], "shop.ordrs")).toBe(false);
|
|
47
|
+
expect(nameAgrees(["other", "orders"], "shop.orders")).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("namesInGenerated reads only the module's SqlSchema", () => {
|
|
51
|
+
const text = generated(PG, ["shop.orders", "shop.users"]);
|
|
52
|
+
expect(namesInGenerated(text, PG)).toEqual(new Set([normalizedName("shop.orders"), normalizedName("shop.users")]));
|
|
53
|
+
expect(namesInGenerated(text, CH)).toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const PGX = `import { table, view } from "${PG}";\n`;
|
|
58
|
+
const CHX = `import { table, view, dictionary } from "${CH}";\n`;
|
|
59
|
+
|
|
60
|
+
describe("SQLPG005: a typed Postgres tag's type argument", () => {
|
|
61
|
+
test("a name that agrees with the template, with no generated file, is clean", () => {
|
|
62
|
+
const code = `${PGX}export const o = table<"shop.orders">\`CREATE TABLE shop.orders (id bigint)\`;`;
|
|
63
|
+
expect(sqlpg005.check(ctx(code))).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("a different name is an error, at the type argument", () => {
|
|
67
|
+
const code = `${PGX}export const o = table<"shop.ordrs">\`CREATE TABLE shop.orders (id bigint)\`;`;
|
|
68
|
+
const [d, ...rest] = sqlpg005.check(ctx(code));
|
|
69
|
+
expect(rest).toEqual([]);
|
|
70
|
+
expect(d).toMatchObject({ ruleId: "SQLPG005", severity: "error", line: 2, column: 24 });
|
|
71
|
+
expect(d!.message).toContain("shop.orders");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("a schema that is an interpolation is left out of the comparison", () => {
|
|
75
|
+
const ok = `${PGX}import { shopSchema } from "./s";\nexport const o = table<"shop.orders">\`CREATE TABLE \${shopSchema}.orders (id bigint)\`;`;
|
|
76
|
+
expect(sqlpg005.check(ctx(ok))).toEqual([]);
|
|
77
|
+
const bad = ok.replace("shop.orders", "shop.items");
|
|
78
|
+
expect(sqlpg005.check(ctx(bad))).toHaveLength(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("a view is read too, and quoting and case folding follow Postgres", () => {
|
|
82
|
+
const ok = `${PGX}export const v = view<"app.order_totals">\`CREATE VIEW App."order_totals" AS SELECT 1\`;`;
|
|
83
|
+
expect(sqlpg005.check(ctx(ok))).toEqual([]);
|
|
84
|
+
const bad = `${PGX}export const v = view<"app.Order_Totals">\`CREATE VIEW app.order_totals AS SELECT 1\`;`;
|
|
85
|
+
expect(sqlpg005.check(ctx(bad))).toHaveLength(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("a tag with no type argument, or a non-literal one, is not read", () => {
|
|
89
|
+
expect(sqlpg005.check(ctx(`${PGX}export const o = table\`CREATE TABLE shop.orders (id bigint)\`;`))).toEqual([]);
|
|
90
|
+
expect(sqlpg005.check(ctx(`${PGX}export const o = table<string>\`CREATE TABLE shop.orders (id bigint)\`;`))).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("a tag from another module, or the ClickHouse tag, is not read", () => {
|
|
94
|
+
expect(sqlpg005.check(ctx(`import { table } from "./mine";\nexport const o = table<"x.y">\`not sql\`;`))).toEqual([]);
|
|
95
|
+
expect(sqlpg005.check(ctx(`${CHX}export const o = table<"a.b">\`CREATE TABLE c.d (id UInt64) ENGINE = Log\`;`))).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("a template that does not parse is SQLPG001's to report", () => {
|
|
99
|
+
expect(sqlpg005.check(ctx(`${PGX}export const o = table<"a.b">\`CREATE TABLE\`;`))).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("a name the generated file lacks is a warning; one it lists is clean", () => {
|
|
103
|
+
const file = project(PG, ["shop.users"]);
|
|
104
|
+
const code = `${PGX}export const o = table<"shop.orders">\`CREATE TABLE shop.orders (id bigint)\`;\nexport const u = table<"shop.users">\`CREATE TABLE shop.users (id bigint)\`;`;
|
|
105
|
+
const diags = sqlpg005.check(ctx(code, file));
|
|
106
|
+
expect(diags).toHaveLength(1);
|
|
107
|
+
expect(diags[0]).toMatchObject({ ruleId: "SQLPG005", severity: "warning", line: 2 });
|
|
108
|
+
expect(diags[0]!.message).toMatch(/not in sql-types\.d\.ts.*chant sql types/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("a generated file with no block for the dialect is silent", () => {
|
|
112
|
+
const file = project(CH, ["shop.events"]);
|
|
113
|
+
const code = `${PGX}export const o = table<"shop.orders">\`CREATE TABLE shop.orders (id bigint)\`;`;
|
|
114
|
+
expect(sqlpg005.check(ctx(code, file))).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("SQLCH004: a typed ClickHouse tag's type argument", () => {
|
|
119
|
+
const ddl = (db: string) => `CREATE TABLE ${db}.events (id UInt64) ENGINE = Log`;
|
|
120
|
+
|
|
121
|
+
test("a name that agrees is clean; a different one is an error", () => {
|
|
122
|
+
expect(sqlch004.check(ctx(`${CHX}export const e = table<"shop.events">\`${ddl("shop")}\`;`))).toEqual([]);
|
|
123
|
+
const [d] = sqlch004.check(ctx(`${CHX}export const e = table<"shop.event">\`${ddl("shop")}\`;`));
|
|
124
|
+
expect(d).toMatchObject({ ruleId: "SQLCH004", severity: "error", line: 2 });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("a database that is an interpolation is left out, and a name differing in case is a different name", () => {
|
|
128
|
+
expect(sqlch004.check(ctx(`${CHX}import { db } from "./d";\nexport const e = table<"shop.events">\`${ddl("${db}")}\`;`))).toEqual([]);
|
|
129
|
+
expect(sqlch004.check(ctx(`${CHX}export const e = table<"shop.Events">\`${ddl("shop")}\`;`))).toHaveLength(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("a dictionary and a view are read", () => {
|
|
133
|
+
const dict = `${CHX}export const d = dictionary<"shop.codes">\`CREATE DICTIONARY shop.codes (id UInt64) PRIMARY KEY id SOURCE(CLICKHOUSE(TABLE 'c')) LAYOUT(FLAT()) LIFETIME(0)\`;`;
|
|
134
|
+
expect(sqlch004.check(ctx(dict))).toEqual([]);
|
|
135
|
+
expect(sqlch004.check(ctx(dict.replace("shop.codes\"", "shop.cods\"")))).toHaveLength(1);
|
|
136
|
+
const view = `${CHX}export const v = view<"shop.v1">\`CREATE VIEW shop.v2 AS SELECT 1\`;`;
|
|
137
|
+
expect(sqlch004.check(ctx(view))).toHaveLength(1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("a name the generated file lacks is a warning", () => {
|
|
141
|
+
const file = project(CH, ["shop.other"]);
|
|
142
|
+
const [d] = sqlch004.check(ctx(`${CHX}export const e = table<"shop.events">\`${ddl("shop")}\`;`, file));
|
|
143
|
+
expect(d).toMatchObject({ ruleId: "SQLCH004", severity: "warning" });
|
|
144
|
+
expect(sqlch004.check(ctx(`${CHX}export const e = table<"shop.other">\`CREATE TABLE shop.other (id UInt64) ENGINE = Log\`;`, file))).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("without a generated file only the name comparison runs", () => {
|
|
148
|
+
const file = project(CH, undefined);
|
|
149
|
+
expect(sqlch004.check(ctx(`${CHX}export const e = table<"shop.events">\`${ddl("shop")}\`;`, file))).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the typed-column rules (SQLPG005, SQLCH004; chant #3751) share: reading
|
|
3
|
+
* a tag's type argument, comparing it with the name the template declares, and
|
|
4
|
+
* reading the names the generated `sql-types.d.ts` lists.
|
|
5
|
+
*
|
|
6
|
+
* The type argument is `table<"shop.orders">\`CREATE TABLE ...\``. tsc checks
|
|
7
|
+
* that it names an entry of the generated file's `SqlSchema` only when the
|
|
8
|
+
* argument is a constraint, and an unlisted name falls back to unchecked
|
|
9
|
+
* columns, so a mistyped argument is silent without these rules.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { dirname, join, resolve } from "node:path";
|
|
14
|
+
import * as ts from "typescript";
|
|
15
|
+
|
|
16
|
+
/** The generated file's name, beside the source (`chant sql types`). */
|
|
17
|
+
export const SQL_TYPES_FILE = "sql-types.d.ts";
|
|
18
|
+
|
|
19
|
+
/** A tag's type argument, when it is a string literal: `table<"shop.orders">`. */
|
|
20
|
+
export function typeArgumentName(node: ts.TaggedTemplateExpression): { name: string; node: ts.LiteralTypeNode } | undefined {
|
|
21
|
+
const arg = node.typeArguments?.[0];
|
|
22
|
+
if (!arg || node.typeArguments!.length !== 1) return undefined;
|
|
23
|
+
if (!ts.isLiteralTypeNode(arg) || !ts.isStringLiteral(arg.literal)) return undefined;
|
|
24
|
+
return { name: arg.literal.text, node: arg };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Split a dotted name outside quotes and take the quotes off: `shop."Order Items"` is `shop`, `Order Items`. */
|
|
28
|
+
export function splitQualified(name: string): string[] {
|
|
29
|
+
const pieces: string[] = [];
|
|
30
|
+
let cur = "";
|
|
31
|
+
let quote: string | undefined;
|
|
32
|
+
for (let i = 0; i < name.length; i++) {
|
|
33
|
+
const c = name[i]!;
|
|
34
|
+
if (quote) {
|
|
35
|
+
if (c === quote && name[i + 1] === quote) {
|
|
36
|
+
cur += c;
|
|
37
|
+
i++;
|
|
38
|
+
} else if (c === quote) quote = undefined;
|
|
39
|
+
else cur += c;
|
|
40
|
+
} else if (c === '"' || c === "`") quote = c;
|
|
41
|
+
else if (c === ".") {
|
|
42
|
+
pieces.push(cur);
|
|
43
|
+
cur = "";
|
|
44
|
+
} else cur += c;
|
|
45
|
+
}
|
|
46
|
+
pieces.push(cur);
|
|
47
|
+
return pieces;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether the declared name (an empty piece is an interpolation, which lint
|
|
52
|
+
* cannot read) can be the type argument's. Compared from the last piece back.
|
|
53
|
+
* A template that does not qualify its name takes the argument's schema, since
|
|
54
|
+
* the build qualifies it with the schema or database the project sets; one
|
|
55
|
+
* that does qualify it must agree with the argument.
|
|
56
|
+
*/
|
|
57
|
+
export function nameAgrees(declared: readonly string[], argument: string): boolean {
|
|
58
|
+
const given = splitQualified(argument);
|
|
59
|
+
if (declared.length === 0) return true;
|
|
60
|
+
if (declared.length > given.length) return false;
|
|
61
|
+
for (let i = 1; i <= declared.length; i++) {
|
|
62
|
+
const d = declared[declared.length - i]!;
|
|
63
|
+
if (d === "") continue;
|
|
64
|
+
if (d !== given[given.length - i]) return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The text of a declared name for a message; an interpolation is `${...}`. */
|
|
70
|
+
export const declaredText = (declared: readonly string[]): string => declared.map((p) => (p === "" ? "${...}" : p)).join(".");
|
|
71
|
+
|
|
72
|
+
/** A qualified name with quoting taken off, so `"shop"."orders"` and `shop.orders` compare equal. */
|
|
73
|
+
export const normalizedName = (name: string): string => splitQualified(name).join("\0");
|
|
74
|
+
|
|
75
|
+
/** The names (normalized, see {@link normalizedName}) a `declare module "<module>"` block in a generated file augments `SqlSchema` with. */
|
|
76
|
+
export function namesInGenerated(content: string, module: string): Set<string> | undefined {
|
|
77
|
+
const source = ts.createSourceFile(SQL_TYPES_FILE, content, ts.ScriptTarget.Latest, true);
|
|
78
|
+
let names: Set<string> | undefined;
|
|
79
|
+
for (const stmt of source.statements) {
|
|
80
|
+
if (!ts.isModuleDeclaration(stmt) || !ts.isStringLiteral(stmt.name) || stmt.name.text !== module) continue;
|
|
81
|
+
if (!stmt.body || !ts.isModuleBlock(stmt.body)) continue;
|
|
82
|
+
for (const inner of stmt.body.statements) {
|
|
83
|
+
if (!ts.isInterfaceDeclaration(inner) || inner.name.text !== "SqlSchema") continue;
|
|
84
|
+
names ??= new Set();
|
|
85
|
+
for (const member of inner.members) {
|
|
86
|
+
const n = member.name;
|
|
87
|
+
if (n && (ts.isStringLiteral(n) || ts.isIdentifier(n))) names.add(normalizedName(n.text));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return names;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The generated file nearest above a source file, up to the directory that
|
|
96
|
+
* holds the project's config. `chant sql types` writes it in `sourceDir` (or
|
|
97
|
+
* the project root), so the walk finds it from any file below.
|
|
98
|
+
*/
|
|
99
|
+
export function findGeneratedFile(filePath: string): string | undefined {
|
|
100
|
+
let dir = dirname(resolve(filePath));
|
|
101
|
+
for (;;) {
|
|
102
|
+
const candidate = join(dir, SQL_TYPES_FILE);
|
|
103
|
+
if (existsSync(candidate)) return candidate;
|
|
104
|
+
if (existsSync(join(dir, "chant.config.ts")) || existsSync(join(dir, "chant.config.json"))) return undefined;
|
|
105
|
+
const up = dirname(dir);
|
|
106
|
+
if (up === dir) return undefined;
|
|
107
|
+
dir = up;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The names the generated file lists for a dialect's module, or undefined when there is no file or it has no such block. */
|
|
112
|
+
export function generatedNamesFor(filePath: string, module: string): Set<string> | undefined {
|
|
113
|
+
const found = findGeneratedFile(filePath);
|
|
114
|
+
if (!found) return undefined;
|
|
115
|
+
try {
|
|
116
|
+
return namesInGenerated(readFileSync(found, "utf8"), module);
|
|
117
|
+
} catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -15,7 +15,7 @@ concurrency:
|
|
|
15
15
|
jobs:
|
|
16
16
|
schema-watch:
|
|
17
17
|
runs-on: docker
|
|
18
|
-
container: node:22
|
|
18
|
+
container: node:22
|
|
19
19
|
env:
|
|
20
20
|
CLICKHOUSE_READER_USER: chant_reader
|
|
21
21
|
CLICKHOUSE_READER_PASSWORD: '\${{ secrets.CLICKHOUSE_READER_PASSWORD }}'
|
|
@@ -47,7 +47,7 @@ permissions:
|
|
|
47
47
|
jobs:
|
|
48
48
|
schema-watch:
|
|
49
49
|
runs-on: ubuntu-latest
|
|
50
|
-
container: node:22
|
|
50
|
+
container: node:22
|
|
51
51
|
env:
|
|
52
52
|
CLICKHOUSE_READER_USER: chant_reader
|
|
53
53
|
CLICKHOUSE_READER_PASSWORD: '\${{ secrets.CLICKHOUSE_READER_PASSWORD }}'
|
|
@@ -78,7 +78,7 @@ stages:
|
|
|
78
78
|
|
|
79
79
|
schema-watch:
|
|
80
80
|
stage: ops
|
|
81
|
-
image: node:22
|
|
81
|
+
image: node:22
|
|
82
82
|
resource_group: schema-watch
|
|
83
83
|
variables:
|
|
84
84
|
CLICKHOUSE_READER_USER: chant_reader
|
package/src/postgres/entities.ts
CHANGED
|
@@ -62,11 +62,13 @@ import { feed, lineage, spanText as text, splice as spliceWith, templateSyntaxEr
|
|
|
62
62
|
import { SqlObject, isColumnRefOf, isSqlObjectOf, makeSqlEntity } from "../core/entity";
|
|
63
63
|
import type { LineageEdge } from "../core/references";
|
|
64
64
|
import { SqlLiteral, SqlTemplateError, describeValue as describe, stripUnset, templateParts } from "../core/template";
|
|
65
|
+
import { setTypedSlots, type ColumnsIn, type Named, type RowIn, type SlotsIn, type TypedSlot, type Unnamed } from "../core/typed";
|
|
65
66
|
|
|
66
67
|
export { SQL_LEXICON } from "../core/entity";
|
|
67
68
|
export type { LineageEdge } from "../core/references";
|
|
68
69
|
export { SqlLiteral, SqlTemplateError } from "../core/template";
|
|
69
70
|
export { quoteIdent } from "./keywords";
|
|
71
|
+
export type { AnyColumn, Column, ColumnTraits, KeyColumn, Named, NotNullColumn, UncheckedColumn, Unnamed } from "../core/typed";
|
|
70
72
|
|
|
71
73
|
export { POSTGRES_ENTITY_TYPES, type PostgresEntityType } from "./entity-types";
|
|
72
74
|
import { POSTGRES_ENTITY_TYPES, type PostgresEntityType } from "./entity-types";
|
|
@@ -431,18 +433,31 @@ export interface PostgresSchema extends PostgresObject {
|
|
|
431
433
|
readonly props: SchemaProps;
|
|
432
434
|
}
|
|
433
435
|
|
|
434
|
-
/**
|
|
435
|
-
|
|
436
|
+
/**
|
|
437
|
+
* Every Postgres table and view the project declares, keyed by its qualified
|
|
438
|
+
* name (`shop.orders`). Empty here; the `sql-types.d.ts` that `chant sql
|
|
439
|
+
* types` writes augments it, and a tag given the name as its type argument
|
|
440
|
+
* reads its columns from there.
|
|
441
|
+
*/
|
|
442
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- augmented by the generated declarations
|
|
443
|
+
export interface SqlSchema {}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* A table or a view: something with columns that SQL can reference. `N` is
|
|
447
|
+
* its qualified name when its tag was given one (`` table<"shop.orders">`...` ``):
|
|
448
|
+
* its columns are then the generated declarations' entry for that name.
|
|
449
|
+
*/
|
|
450
|
+
export interface PostgresRelation<N extends string = string> extends PostgresObject, Named<N> {
|
|
436
451
|
/** One reference per column, by its folded name: `${users.columns.email}`. */
|
|
437
|
-
readonly columns:
|
|
452
|
+
readonly columns: ColumnsIn<SqlSchema, N>;
|
|
438
453
|
}
|
|
439
454
|
|
|
440
|
-
export interface PostgresTable extends PostgresRelation {
|
|
455
|
+
export interface PostgresTable<N extends string = string> extends PostgresRelation<N> {
|
|
441
456
|
readonly entityType: "Postgres::Table";
|
|
442
457
|
readonly props: TableProps;
|
|
443
458
|
}
|
|
444
459
|
|
|
445
|
-
export interface PostgresView extends PostgresRelation {
|
|
460
|
+
export interface PostgresView<N extends string = string> extends PostgresRelation<N> {
|
|
446
461
|
readonly entityType: "Postgres::View" | "Postgres::MaterializedView";
|
|
447
462
|
readonly props: ViewProps;
|
|
448
463
|
}
|
|
@@ -502,6 +517,13 @@ export interface PostgresGrant extends PostgresObject {
|
|
|
502
517
|
readonly props: GrantProps | DefaultPrivilegesProps;
|
|
503
518
|
}
|
|
504
519
|
|
|
520
|
+
/**
|
|
521
|
+
* A declared table's or view's row, for application code: `RowOf<typeof orders>`
|
|
522
|
+
* for a declaration given its name, or `RowOf<"shop.users">` by name, which
|
|
523
|
+
* also covers an object only a `.sql` file declares.
|
|
524
|
+
*/
|
|
525
|
+
export type RowOf<T extends keyof SqlSchema | Named<keyof SqlSchema & string>> = RowIn<SqlSchema, T>;
|
|
526
|
+
|
|
505
527
|
export function isPostgresObject(value: unknown): value is PostgresObject {
|
|
506
528
|
return isSqlObjectOf(value, "Postgres::");
|
|
507
529
|
}
|
|
@@ -816,6 +838,23 @@ function foreignKeyDef(ctx: Ctx, c: ConstraintNode, columns: string[]): ForeignK
|
|
|
816
838
|
);
|
|
817
839
|
}
|
|
818
840
|
|
|
841
|
+
/**
|
|
842
|
+
* The `${}` in a foreign key's referenced column list that each name one
|
|
843
|
+
* column, paired with the referencing column at the same position (#3751).
|
|
844
|
+
* `chant sql types` types each as a column comparing like that one.
|
|
845
|
+
*/
|
|
846
|
+
function referenceSlots(ctx: Ctx, c: ConstraintNode, columns: readonly string[]): TypedSlot[] {
|
|
847
|
+
const out: TypedSlot[] = [];
|
|
848
|
+
c.references!.columns.forEach((x, n) => {
|
|
849
|
+
const column = columns[n];
|
|
850
|
+
if (x.name || column === undefined) return;
|
|
851
|
+
const sig = ctx.tokens.slice(x.span.from, x.span.to).filter((t) => !isTrivia(t));
|
|
852
|
+
const t = sig[0];
|
|
853
|
+
if (sig.length === 1 && t?.kind === "ref" && isColumnRef(ctx.values[t.part])) out.push({ index: t.part, kind: "references", column });
|
|
854
|
+
});
|
|
855
|
+
return out;
|
|
856
|
+
}
|
|
857
|
+
|
|
819
858
|
function exclusionDef(ctx: Ctx, c: ConstraintNode): ExclusionDef {
|
|
820
859
|
return stripUnset({
|
|
821
860
|
name: c.name,
|
|
@@ -1142,6 +1181,7 @@ function build(tag: PostgresTag, strings: TemplateStringsArray | readonly string
|
|
|
1142
1181
|
const ddl = untokenize(tokens, (i) => render(values[i], i)).trim().replace(/;\s*$/, "");
|
|
1143
1182
|
const source = { strings: rawParts };
|
|
1144
1183
|
const dependsOn = [...new Set([...values, ...(joined?.refs ?? [])].filter((v) => isPostgresObject(v) || isColumnRef(v)))];
|
|
1184
|
+
const slots: TypedSlot[] = [];
|
|
1145
1185
|
const make = <T extends PostgresObject>(type: PostgresEntityType, q: { schema?: string; name: string }, props: object, columnNames?: readonly string[]): T => {
|
|
1146
1186
|
const entity = makeSqlEntity(PostgresObject.prototype as PostgresObject, type, sqlNameOf(q), props, columnNames, dependsOn) as T;
|
|
1147
1187
|
const fed = ctx.fed.map((paths) => [...paths].sort());
|
|
@@ -1152,6 +1192,7 @@ function build(tag: PostgresTag, strings: TemplateStringsArray | readonly string
|
|
|
1152
1192
|
joined.original.forEach((orig, i) => (byOriginal[orig] = fed[i]!));
|
|
1153
1193
|
setInterpolationFields(entity, byOriginal);
|
|
1154
1194
|
}
|
|
1195
|
+
setTypedSlots(entity, slots);
|
|
1155
1196
|
return entity;
|
|
1156
1197
|
};
|
|
1157
1198
|
|
|
@@ -1264,7 +1305,10 @@ function build(tag: PostgresTag, strings: TemplateStringsArray | readonly string
|
|
|
1264
1305
|
const foreignKeys: ForeignKeyDef[] = [];
|
|
1265
1306
|
const exclusions: ExclusionDef[] = [];
|
|
1266
1307
|
const file = (c: ConstraintNode, cols: string[]) => {
|
|
1267
|
-
if (c.kind === "FOREIGN KEY")
|
|
1308
|
+
if (c.kind === "FOREIGN KEY") {
|
|
1309
|
+
foreignKeys.push(foreignKeyDef(ctx, c, cols));
|
|
1310
|
+
slots.push(...referenceSlots(ctx, c, cols));
|
|
1311
|
+
}
|
|
1268
1312
|
else if (c.kind === "PRIMARY KEY") {
|
|
1269
1313
|
if (primaryKey) throw new SqlTemplateError(tag, `${sqlNameOf(q)} declares more than one primary key`, 0, 0);
|
|
1270
1314
|
primaryKey = keyDef(ctx, c, cols, "primaryKey");
|
|
@@ -1554,9 +1598,14 @@ export function schema(strings: TemplateStringsArray, ...values: unknown[]): Pos
|
|
|
1554
1598
|
return build("schema", strings, values) as PostgresSchema;
|
|
1555
1599
|
}
|
|
1556
1600
|
|
|
1557
|
-
/**
|
|
1558
|
-
|
|
1559
|
-
|
|
1601
|
+
/**
|
|
1602
|
+
* `` table`CREATE TABLE ...` ``: one Postgres table with its columns and
|
|
1603
|
+
* constraints, parsed at build time. Given its qualified name as the type
|
|
1604
|
+
* argument (`` table<"shop.orders">`...` ``), its columns are the ones
|
|
1605
|
+
* `chant sql types` declared for that name.
|
|
1606
|
+
*/
|
|
1607
|
+
export function table<const N extends string = Unnamed>(strings: TemplateStringsArray, ...values: SlotsIn<SqlSchema, N>): PostgresTable<N> {
|
|
1608
|
+
return build("table", strings, values) as PostgresTable<N>;
|
|
1560
1609
|
}
|
|
1561
1610
|
|
|
1562
1611
|
/** `` index`CREATE [UNIQUE] INDEX [CONCURRENTLY] name ON ...` ``: one named index. */
|
|
@@ -1564,9 +1613,13 @@ export function index(strings: TemplateStringsArray, ...values: unknown[]): Post
|
|
|
1564
1613
|
return build("index", strings, values) as PostgresIndex;
|
|
1565
1614
|
}
|
|
1566
1615
|
|
|
1567
|
-
/**
|
|
1568
|
-
|
|
1569
|
-
|
|
1616
|
+
/**
|
|
1617
|
+
* `` view`CREATE [MATERIALIZED] VIEW ...` ``: one view or materialized view,
|
|
1618
|
+
* with what it reads and its column lineage. Takes its qualified name as the
|
|
1619
|
+
* type argument, as {@link table} does.
|
|
1620
|
+
*/
|
|
1621
|
+
export function view<const N extends string = Unnamed>(strings: TemplateStringsArray, ...values: SlotsIn<SqlSchema, N>): PostgresView<N> {
|
|
1622
|
+
return build("view", strings, values) as PostgresView<N>;
|
|
1570
1623
|
}
|
|
1571
1624
|
|
|
1572
1625
|
/** `` sequence`CREATE SEQUENCE ...` ``: one sequence. Reference it as `nextval(${seq})`. */
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* references cross schemas freely.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { relationKey } from "../../core/typed";
|
|
24
25
|
import type { GeneratedFile, TypeScriptGenerator } from "@intentius/chant/import/generator";
|
|
25
26
|
import type { TemplateIR } from "@intentius/chant/import/parser";
|
|
26
27
|
import { isTrivia, tokenizeText, type Token } from "../tokens";
|
|
@@ -233,6 +234,11 @@ const indent = (body: string) =>
|
|
|
233
234
|
.map((l, i) => (i === 0 || l.length === 0 ? l : ` ${l}`))
|
|
234
235
|
.join("\n");
|
|
235
236
|
|
|
237
|
+
const RELATIONS = new Set<string>([POSTGRES_ENTITY_TYPES.table, POSTGRES_ENTITY_TYPES.view, POSTGRES_ENTITY_TYPES.materializedView]);
|
|
238
|
+
|
|
239
|
+
/** A table's or view's qualified name as its tag's type argument (#3751), so its columns are typed once `chant sql types` has run. */
|
|
240
|
+
const typeArgument = (it: Item): string => (RELATIONS.has(it.type) ? `<${JSON.stringify(relationKey(it))}>` : "");
|
|
241
|
+
|
|
236
242
|
export class PostgresGenerator implements TypeScriptGenerator {
|
|
237
243
|
readonly ownsLayout = true;
|
|
238
244
|
|
|
@@ -256,7 +262,7 @@ export class PostgresGenerator implements TypeScriptGenerator {
|
|
|
256
262
|
const lines: string[] = [];
|
|
257
263
|
if (this.header) lines.push(...this.header.split("\n").map((l) => `// ${l}`), "");
|
|
258
264
|
lines.push(`import { ${tags.join(", ")} } from "@intentius/chant-lexicon-sql/postgres";`, "");
|
|
259
|
-
for (const it of order) lines.push(`export const ${it.exportName} = ${TAG[it.type]}\`\n ${indent(bodyText(bodies.get(it.exportName)!))}\`;`, "");
|
|
265
|
+
for (const it of order) lines.push(`export const ${it.exportName} = ${TAG[it.type]}${typeArgument(it)}\`\n ${indent(bodyText(bodies.get(it.exportName)!))}\`;`, "");
|
|
260
266
|
return [{ path: "schema.ts", content: `${lines.join("\n").trimEnd()}\n` }];
|
|
261
267
|
}
|
|
262
268
|
}
|