@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,63 @@
|
|
|
1
|
+
import { checkOf, indexesOf, tablesOf } from "./postgres-helpers";
|
|
2
|
+
import { columnsOf, includeColumns, interpolatedTable, projectOf } from "./postgres-names";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SQLPG123: a bare column name in a key, index or grant column list that the
|
|
6
|
+
* table does not declare.
|
|
7
|
+
*
|
|
8
|
+
* Doc: https://www.postgresql.org/docs/18/sql-createtable.html,
|
|
9
|
+
* https://www.postgresql.org/docs/18/sql-createindex.html,
|
|
10
|
+
* https://www.postgresql.org/docs/18/sql-grant.html. The server refuses
|
|
11
|
+
* `column "idd" named in key does not exist` (or `column "emial" does not
|
|
12
|
+
* exist`) at CREATE or GRANT. Checked: `PRIMARY KEY`, `UNIQUE` and their
|
|
13
|
+
* `INCLUDE`, a foreign key's own columns, an index's bare columns and
|
|
14
|
+
* `INCLUDE` on a `${}` table, and a `GRANT`/`REVOKE` column list on a `${}`
|
|
15
|
+
* table. An expression (an index expression, a CHECK, a generated column) is
|
|
16
|
+
* left to the server, as is a table whose columns come from somewhere the
|
|
17
|
+
* build cannot see (`LIKE`, `OF type`, a plain-text parent).
|
|
18
|
+
*/
|
|
19
|
+
export const sqlpg123 = checkOf({ id: "SQLPG123", description: "A key, index or grant column list names a column the table does not declare" }, (ctx, report) => {
|
|
20
|
+
const p = projectOf(ctx);
|
|
21
|
+
const verify = (entity: string, sqlName: string, where: string, tableName: string, names: readonly string[], cols: ReadonlyMap<string, unknown>) => {
|
|
22
|
+
for (const n of new Set(names)) {
|
|
23
|
+
if (cols.has(n)) continue;
|
|
24
|
+
report({ severity: "error", message: `${entity} (${sqlName}) ${where} names the column ${n}, which ${tableName} does not declare`, entity });
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
for (const t of tablesOf(ctx)) {
|
|
28
|
+
const cols = columnsOf(p, t);
|
|
29
|
+
if (!cols) continue;
|
|
30
|
+
if (t.primaryKey) {
|
|
31
|
+
verify(t.export, t.sqlName, "PRIMARY KEY", t.sqlName, t.primaryKey.columns, cols);
|
|
32
|
+
verify(t.export, t.sqlName, "PRIMARY KEY ... INCLUDE", t.sqlName, includeColumns(t.primaryKey.include), cols);
|
|
33
|
+
}
|
|
34
|
+
for (const u of t.uniques) {
|
|
35
|
+
verify(t.export, t.sqlName, "UNIQUE", t.sqlName, u.columns, cols);
|
|
36
|
+
verify(t.export, t.sqlName, "UNIQUE ... INCLUDE", t.sqlName, includeColumns(u.include), cols);
|
|
37
|
+
}
|
|
38
|
+
for (const fk of t.foreignKeys) verify(t.export, t.sqlName, `FOREIGN KEY to ${fk.refTable}`, t.sqlName, fk.columns, cols);
|
|
39
|
+
}
|
|
40
|
+
for (const i of indexesOf(ctx)) {
|
|
41
|
+
const t = interpolatedTable(p, i.table, i.tableName);
|
|
42
|
+
const cols = t && columnsOf(p, t);
|
|
43
|
+
if (!cols) continue;
|
|
44
|
+
verify(i.export, i.sqlName, `index on ${i.tableName}`, i.tableName, i.elements.flatMap((e) => (e.column ? [e.column] : [])), cols);
|
|
45
|
+
verify(i.export, i.sqlName, `index on ${i.tableName} INCLUDE`, i.tableName, includeColumns(i.include), cols);
|
|
46
|
+
}
|
|
47
|
+
for (const g of p.objects) {
|
|
48
|
+
if (g.type !== "Postgres::Grant" || g.on !== "table") continue;
|
|
49
|
+
const objects = (g.objects as unknown[]) ?? [];
|
|
50
|
+
const names = (g.objectNames as string[]) ?? [];
|
|
51
|
+
const privileges = (g.privileges as Array<{ privilege: string; columns?: string[] }>) ?? [];
|
|
52
|
+
objects.forEach((ref, n) => {
|
|
53
|
+
const t = interpolatedTable(p, ref, names[n]);
|
|
54
|
+
const cols = t && columnsOf(p, t);
|
|
55
|
+
if (!cols) return;
|
|
56
|
+
for (const priv of privileges) {
|
|
57
|
+
if (!priv.columns?.length) continue;
|
|
58
|
+
const action = g.action === "revoke" ? "REVOKE" : "GRANT";
|
|
59
|
+
verify(g.export, String(g.name), `${action} ${priv.privilege.toUpperCase()} on ${t!.sqlName}`, t!.sqlName, priv.columns, cols);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { checkOf, tablesOf } from "./postgres-helpers";
|
|
2
|
+
import { columnsOf, interpolatedTable, projectOf, typeIdentity } from "./postgres-names";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SQLPG124: a foreign key to a `${}` table that names a column the table does
|
|
6
|
+
* not declare, or pairs columns whose types do not compare.
|
|
7
|
+
*
|
|
8
|
+
* Doc: https://www.postgresql.org/docs/18/ddl-constraints.html#DDL-CONSTRAINTS-FK
|
|
9
|
+
* and https://www.postgresql.org/docs/18/catalog-pg-type.html#CATALOG-TYPCATEGORY-TABLE.
|
|
10
|
+
* The server refuses `column "idd" referenced in foreign key constraint does
|
|
11
|
+
* not exist` and `Key columns "customer_id" and "id" are of incompatible
|
|
12
|
+
* types: text and bigint`. Types compare by their catalog category: `integer`
|
|
13
|
+
* to `bigint` (both numeric) passes, `text` to `bigint` fails. The
|
|
14
|
+
* user-defined category (`uuid`, `bytea`, `jsonb`...) compares by type; a
|
|
15
|
+
* domain by its base type; an enum by itself. A key to a table named in plain
|
|
16
|
+
* text, or a type outside the project, is left to the server.
|
|
17
|
+
*/
|
|
18
|
+
export const sqlpg124 = checkOf({ id: "SQLPG124", description: "A foreign key names a column the referenced table lacks, or one of another type category" }, (ctx, report) => {
|
|
19
|
+
const p = projectOf(ctx);
|
|
20
|
+
for (const t of tablesOf(ctx)) {
|
|
21
|
+
const own = columnsOf(p, t);
|
|
22
|
+
for (const fk of t.foreignKeys) {
|
|
23
|
+
const ref = interpolatedTable(p, (fk as { references?: unknown }).references, fk.refTable);
|
|
24
|
+
const refCols = ref && columnsOf(p, ref);
|
|
25
|
+
if (!ref || !refCols) continue;
|
|
26
|
+
const targets = fk.refColumns.length > 0 ? fk.refColumns : (ref.primaryKey?.columns ?? []);
|
|
27
|
+
const missing = targets.filter((c) => !refCols.has(c));
|
|
28
|
+
for (const c of missing) {
|
|
29
|
+
report({ severity: "error", message: `${t.export} (${t.sqlName}) has a foreign key on (${fk.columns.join(", ")}) to ${ref.sqlName} (${c}), which ${ref.sqlName} does not declare`, entity: t.export });
|
|
30
|
+
}
|
|
31
|
+
if (missing.length > 0 || !own || targets.length !== fk.columns.length) continue;
|
|
32
|
+
fk.columns.forEach((c, n) => {
|
|
33
|
+
const mine = own.get(c);
|
|
34
|
+
const theirs = refCols.get(targets[n]!);
|
|
35
|
+
if (!mine || !theirs) return;
|
|
36
|
+
const a = typeIdentity(p, mine.type);
|
|
37
|
+
const b = typeIdentity(p, theirs.type);
|
|
38
|
+
if (a === undefined || b === undefined || a === b) return;
|
|
39
|
+
report({
|
|
40
|
+
severity: "error",
|
|
41
|
+
message: `${t.export} (${t.sqlName}) has a foreign key from ${c} (${mine.type}) to ${ref.sqlName} (${targets[n]}) (${theirs.type}); the types do not compare, so Postgres refuses the key`,
|
|
42
|
+
entity: t.export,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { checkOf, indexesOf } from "./postgres-helpers";
|
|
2
|
+
import { parseIndexElement, projectOf, resolveMethod, resolveOpClass } from "./postgres-names";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SQLPG125: an index access method or operator class the target major does
|
|
6
|
+
* not have, or an operator class that is not for the index's method.
|
|
7
|
+
*
|
|
8
|
+
* Doc: https://www.postgresql.org/docs/18/sql-createindex.html and
|
|
9
|
+
* https://www.postgresql.org/docs/18/indexes-opclass.html. The server refuses
|
|
10
|
+
* `access method "btreee" does not exist` and `operator class "numeric_opz"
|
|
11
|
+
* does not exist for access method "btree"` at CREATE. Methods and operator
|
|
12
|
+
* classes come from the target major's catalog (`pg_am`, `pg_opclass`). The
|
|
13
|
+
* ones an extension adds (`gin_trgm_ops` from pg_trgm, `hnsw` from vector)
|
|
14
|
+
* pass when the project declares that extension and are an error naming it
|
|
15
|
+
* when it does not; one neither the catalog nor the extension overlay knows is
|
|
16
|
+
* a warning when the project declares any extension. An operator class
|
|
17
|
+
* qualified with a schema the project does not declare is left alone.
|
|
18
|
+
*/
|
|
19
|
+
export const sqlpg125 = checkOf({ id: "SQLPG125", description: "An index access method or operator class the target major does not have, or not for that method" }, (ctx, report) => {
|
|
20
|
+
const p = projectOf(ctx);
|
|
21
|
+
for (const i of indexesOf(ctx)) {
|
|
22
|
+
const method = i.method ?? "btree";
|
|
23
|
+
const m = resolveMethod(p, method);
|
|
24
|
+
if (!m.ok) {
|
|
25
|
+
report({ severity: m.miss.severity, message: `${i.export} (${i.sqlName}) uses the index access method ${method}, which Postgres ${p.major} does not have${m.miss.note}`, entity: i.export });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
for (const e of i.elements) {
|
|
29
|
+
const opclass = parseIndexElement(e.expr)?.opclass;
|
|
30
|
+
if (!opclass) continue;
|
|
31
|
+
const r = resolveOpClass(p, method, opclass);
|
|
32
|
+
if (r.ok) continue;
|
|
33
|
+
const label = `${opclass.schema ? `${opclass.schema}.` : ""}${opclass.name}`;
|
|
34
|
+
const message = r.otherMethods
|
|
35
|
+
? `${i.export} (${i.sqlName}) uses the operator class ${label}, which is for ${r.otherMethods.join(", ")}, not ${method}`
|
|
36
|
+
: `${i.export} (${i.sqlName}) uses the operator class ${label}, which Postgres ${p.major} does not have for ${method}${r.miss.note}`;
|
|
37
|
+
report({ severity: r.miss.severity, message, entity: i.export });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { checkOf } from "./postgres-helpers";
|
|
2
|
+
import { callsIn, expressionsOf, projectOf, resolveCall } from "./postgres-names";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SQLPG126: a call to a function the target major does not have, in a column
|
|
6
|
+
* `DEFAULT`, a generated column, a `CHECK` (a table's or a domain's), or an
|
|
7
|
+
* index expression.
|
|
8
|
+
*
|
|
9
|
+
* Doc: https://www.postgresql.org/docs/18/functions.html. The server refuses
|
|
10
|
+
* `function noww() does not exist` at CREATE. A name counts when it is in
|
|
11
|
+
* the target major's `pg_proc`, a function or procedure the project declares,
|
|
12
|
+
* a type name used as a cast (`int4(x)`), or qualified with a schema the
|
|
13
|
+
* project does not declare. Only a name directly followed by `(`, outside
|
|
14
|
+
* string literals, is a call; the grammar's own constructs that read like
|
|
15
|
+
* calls (`COALESCE`, `NULLIF`, `GREATEST`, `LEAST`, `CAST`, `EXTRACT`, `ROW`,
|
|
16
|
+
* `ARRAY`, `EXISTS`, `IN`, `ANY`, `ALL`, `SOME`, and the key word forms of
|
|
17
|
+
* `OVERLAY`, `POSITION`, `SUBSTRING` and `TRIM`) are reserved or column-name
|
|
18
|
+
* key words, not `pg_proc` names, and are skipped by their key word category.
|
|
19
|
+
* An error, or a warning when the project declares an extension.
|
|
20
|
+
*/
|
|
21
|
+
export const sqlpg126 = checkOf({ id: "SQLPG126", description: "An expression calls a function the target major does not have" }, (ctx, report) => {
|
|
22
|
+
const p = projectOf(ctx);
|
|
23
|
+
for (const o of p.objects) {
|
|
24
|
+
const seen = new Set<string>();
|
|
25
|
+
for (const { where, expr } of expressionsOf(o)) {
|
|
26
|
+
for (const c of callsIn(expr)) {
|
|
27
|
+
const label = `${c.schema ? `${c.schema}.` : ""}${c.name}`;
|
|
28
|
+
if (seen.has(`${where}\0${label}`)) continue;
|
|
29
|
+
seen.add(`${where}\0${label}`);
|
|
30
|
+
const r = resolveCall(p, c);
|
|
31
|
+
if (r.ok) continue;
|
|
32
|
+
const why = r.since !== undefined ? `, which needs Postgres ${r.since}; Postgres ${p.major} does not have it` : r.until !== undefined ? `, which Postgres removed after ${r.until}` : `, which Postgres ${p.major} does not have`;
|
|
33
|
+
report({ severity: r.miss.severity, message: `${o.export} (${o.sqlName as string}) ${where} calls ${label}()${why}${r.miss.note}`, entity: o.export });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant sql types [--output <path>] [--check]` (#3751): build the project the way
|
|
3
|
+
* `chant build` does, folded, and write the declarations
|
|
4
|
+
* {@link renderSqlTypes} renders from its entities, so a tag given its
|
|
5
|
+
* qualified name as the type argument has typed columns.
|
|
6
|
+
*
|
|
7
|
+
* The file goes to `<sourceDir>/sql-types.d.ts` (the project root when
|
|
8
|
+
* `sourceDir` is unset), where any tsconfig that includes the schema includes
|
|
9
|
+
* it too. `--output` puts it elsewhere, as a workspace `generated` entry's
|
|
10
|
+
* generator does. The command refuses to write beside a `sql-types.ts`: tsc
|
|
11
|
+
* resolves the `.ts` first and would drop the declarations. `chant build`
|
|
12
|
+
* never writes the file. `--check` renders the same text and compares it with
|
|
13
|
+
* the file instead of writing it: exit 1, naming the file, when they differ.
|
|
14
|
+
*/
|
|
15
|
+
import type { CommandGroupContext } from "@intentius/chant/cli/command-group";
|
|
16
|
+
/** The file tsc would read instead of `path`: `sql-types.ts` beside `sql-types.d.ts`. */
|
|
17
|
+
export declare function shadowingSource(path: string): string | undefined;
|
|
18
|
+
export declare function runTypes(ctx: CommandGroupContext): Promise<number>;
|
|
19
|
+
//# sourceMappingURL=sql-types-command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-types-command.d.ts","sourceRoot":"","sources":["../src/sql-types-command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAqB9E,yFAAyF;AACzF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIhE;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAqDxE"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The declarations `chant sql types` writes (#3751): one `sql-types.d.ts`
|
|
3
|
+
* that augments the dialect's `SqlSchema` with every table, view and
|
|
4
|
+
* dictionary the build declares, keyed by qualified name.
|
|
5
|
+
*
|
|
6
|
+
* {@link renderSqlTypes} is pure: the entities of a build in, the file's text
|
|
7
|
+
* out. It reads the objects as the serializer writes them, so a `.sql`
|
|
8
|
+
* file's objects and a composite's members are covered exactly as the build
|
|
9
|
+
* covers them, and it reads each template's slot record (`core/typed.ts`) for
|
|
10
|
+
* the `${}` positions a foreign key or a sort key consumes.
|
|
11
|
+
*
|
|
12
|
+
* Each column is a `Column<sqlType, { ts, nullable, cmp }>`:
|
|
13
|
+
*
|
|
14
|
+
* - `sqlType` is the type as the catalog spells it (`bigint`,
|
|
15
|
+
* `timestamp(3) with time zone`, `Nullable(String)`);
|
|
16
|
+
* - `ts` is its value in application code (`./postgres/row-types.ts`,
|
|
17
|
+
* `./clickhouse/row-types.ts`);
|
|
18
|
+
* - `nullable` is whether the value can be NULL. A Postgres view's columns
|
|
19
|
+
* always can, as the server's own catalog says; a column whose type the
|
|
20
|
+
* generator cannot tell is `never`, which fits every slot;
|
|
21
|
+
* - `cmp` is how it compares in a key: SQLPG124's type identity in Postgres
|
|
22
|
+
* (`./lint/post-synth/postgres-names.ts`'s `typeIdentity`), the type
|
|
23
|
+
* family in ClickHouse; `never` when it cannot be told.
|
|
24
|
+
*
|
|
25
|
+
* A view's column that is one column reference (`${t.columns.c}`, `o.${t.columns.c}`)
|
|
26
|
+
* is that column's type; any other expression is `unknown`.
|
|
27
|
+
*/
|
|
28
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
29
|
+
import type { SqlDialect } from "./dialects.js";
|
|
30
|
+
/** The file `chant sql types` writes, in the project's source directory. */
|
|
31
|
+
export declare const SQL_TYPES_FILE = "sql-types.d.ts";
|
|
32
|
+
/**
|
|
33
|
+
* Where `chant sql types` writes the file by default: the project's
|
|
34
|
+
* `sourceDir`, or the project root when it is unset.
|
|
35
|
+
*/
|
|
36
|
+
export declare function sqlTypesPath(projectDir: string, config: Record<string, unknown> | undefined): string;
|
|
37
|
+
/** The file on disk against what the build's entities render: `missing`, `stale` or `current`. */
|
|
38
|
+
export declare function sqlTypesState(entities: ReadonlyMap<string, Declarable>, file: string, opts?: RenderSqlTypesOptions): "missing" | "stale" | "current";
|
|
39
|
+
/** The module each dialect's `SqlSchema` is augmented in. */
|
|
40
|
+
export declare const SQL_TYPES_MODULE: Readonly<Record<SqlDialect, string>>;
|
|
41
|
+
export interface RenderSqlTypesOptions {
|
|
42
|
+
/** The directory each declaration's source file is named relative to. Default: the working directory. */
|
|
43
|
+
projectDir?: string;
|
|
44
|
+
/** The project's config: `sql.postgresMajor` for the catalog, `sql.dialect` when nothing is declared. */
|
|
45
|
+
config?: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The `sql-types.d.ts` for a build's entities: one module that augments the
|
|
49
|
+
* dialect's `SqlSchema` with each table, view and dictionary, in key order.
|
|
50
|
+
* Pure, so a check can render it in memory and compare it with the file.
|
|
51
|
+
*/
|
|
52
|
+
export declare function renderSqlTypes(entities: ReadonlyMap<string, Declarable>, opts?: RenderSqlTypesOptions): string;
|
|
53
|
+
//# sourceMappingURL=sql-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-types.d.ts","sourceRoot":"","sources":["../src/sql-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAW9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAQ7C,4EAA4E;AAC5E,eAAO,MAAM,cAAc,mBAAmB,CAAC;AAE/C;;;GAGG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,MAAM,CAGpG;AAED,kGAAkG;AAClG,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,EACzC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,qBAA0B,GAC/B,SAAS,GAAG,OAAO,GAAG,SAAS,CASjC;AAED,6DAA6D;AAC7D,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAGjE,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,yGAAyG;IACzG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yGAAyG;IACzG,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAiOD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,IAAI,GAAE,qBAA0B,GAAG,MAAM,CAiClH"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-sql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.123.0",
|
|
4
4
|
"description": "SQL lexicon for chant — database schema as typed declarations, one dialect at a time; ClickHouse first, with types read from a pinned server's catalog",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"typescript": "^5.9.3"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"@intentius/chant": "^0.
|
|
73
|
+
"@intentius/chant": "^0.123.0",
|
|
74
74
|
"typescript": "^5.9.3"
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -56,6 +56,15 @@ export interface TypeFamilySpec {
|
|
|
56
56
|
caseInsensitive: boolean;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/** A function the server has: \`system.functions\`. */
|
|
60
|
+
export interface FunctionSpec {
|
|
61
|
+
aggregate: boolean;
|
|
62
|
+
/** The server resolves the name in any case. */
|
|
63
|
+
caseInsensitive: boolean;
|
|
64
|
+
/** The function this name is an alias of. */
|
|
65
|
+
aliasOf?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
export interface CodecSpec {
|
|
60
69
|
compression: boolean;
|
|
61
70
|
generic: boolean;
|
|
@@ -171,7 +171,7 @@ describe("importing a dictionary", () => {
|
|
|
171
171
|
]);
|
|
172
172
|
const [file] = new ClickHouseGenerator().generate(ir);
|
|
173
173
|
expect(file!.content).toContain('import { database, dictionary } from "@intentius/chant-lexicon-sql/clickhouse";');
|
|
174
|
-
expect(file!.content).toContain(
|
|
174
|
+
expect(file!.content).toContain(`export const ratesDict = dictionary<"shop.rates_dict">\``);
|
|
175
175
|
expect(file!.content).toContain("CREATE DICTIONARY ${shopDb}.rates_dict");
|
|
176
176
|
});
|
|
177
177
|
});
|
|
@@ -46,10 +46,12 @@ import { feed, lineage, spanText as text, splice as spliceWith, templateSyntaxEr
|
|
|
46
46
|
import { SQL_LEXICON, SqlObject, isColumnRefOf, isSqlObjectOf, makeSqlEntity } from "../core/entity";
|
|
47
47
|
import type { LineageEdge } from "../core/references";
|
|
48
48
|
import { SqlLiteral, SqlTemplateError, describeValue as describe, stripUnset as strip, templateParts } from "../core/template";
|
|
49
|
+
import { setTypedSlots, type ColumnsIn, type Named, type RowIn, type SlotsIn, type TypedSlot, type Unnamed } from "../core/typed";
|
|
49
50
|
|
|
50
51
|
export { SQL_LEXICON } from "../core/entity";
|
|
51
52
|
export type { LineageEdge } from "../core/references";
|
|
52
53
|
export { SqlLiteral, SqlTemplateError, templateParts, unescapeTemplateDelimiters } from "../core/template";
|
|
54
|
+
export type { AnyColumn, Column, ColumnTraits, KeyColumn, Named, NotNullColumn, UncheckedColumn, Unnamed } from "../core/typed";
|
|
53
55
|
|
|
54
56
|
export const CLICKHOUSE_ENTITY_TYPES = {
|
|
55
57
|
database: "ClickHouse::Database",
|
|
@@ -198,18 +200,31 @@ export interface ClickHouseDatabase extends ClickHouseObject {
|
|
|
198
200
|
readonly props: DatabaseProps;
|
|
199
201
|
}
|
|
200
202
|
|
|
201
|
-
/**
|
|
202
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Every ClickHouse table, view and dictionary the project declares, keyed by
|
|
205
|
+
* its qualified name (`shop.events`). Empty here; the `sql-types.d.ts` that
|
|
206
|
+
* `chant sql types` writes augments it, and a tag given the name as its type
|
|
207
|
+
* argument reads its columns from there.
|
|
208
|
+
*/
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- augmented by the generated declarations
|
|
210
|
+
export interface SqlSchema {}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A table or a view: something with columns that SQL can reference. `N` is
|
|
214
|
+
* its qualified name when its tag was given one (`` table<"shop.events">`...` ``):
|
|
215
|
+
* its columns are then the generated declarations' entry for that name.
|
|
216
|
+
*/
|
|
217
|
+
export interface ClickHouseRelation<N extends string = string> extends ClickHouseObject, Named<N> {
|
|
203
218
|
/** One reference per column, by name: `${events.columns.user_id}`. */
|
|
204
|
-
readonly columns:
|
|
219
|
+
readonly columns: ColumnsIn<SqlSchema, N>;
|
|
205
220
|
}
|
|
206
221
|
|
|
207
|
-
export interface ClickHouseTable extends ClickHouseRelation {
|
|
222
|
+
export interface ClickHouseTable<N extends string = string> extends ClickHouseRelation<N> {
|
|
208
223
|
readonly entityType: "ClickHouse::Table";
|
|
209
224
|
readonly props: TableProps;
|
|
210
225
|
}
|
|
211
226
|
|
|
212
|
-
export interface ClickHouseView extends ClickHouseRelation {
|
|
227
|
+
export interface ClickHouseView<N extends string = string> extends ClickHouseRelation<N> {
|
|
213
228
|
readonly entityType: "ClickHouse::View" | "ClickHouse::MaterializedView";
|
|
214
229
|
readonly props: ViewProps;
|
|
215
230
|
}
|
|
@@ -312,7 +327,7 @@ export interface ClickHouseGrant extends ClickHouseObject {
|
|
|
312
327
|
}
|
|
313
328
|
|
|
314
329
|
/** A dictionary: its attributes are columns `dictGet` reads, so SQL can reference them. */
|
|
315
|
-
export interface ClickHouseDictionary extends ClickHouseRelation {
|
|
330
|
+
export interface ClickHouseDictionary<N extends string = string> extends ClickHouseRelation<N> {
|
|
316
331
|
readonly entityType: "ClickHouse::Dictionary";
|
|
317
332
|
readonly props: DictionaryProps;
|
|
318
333
|
}
|
|
@@ -327,6 +342,14 @@ function makeEntity(
|
|
|
327
342
|
return makeSqlEntity(ClickHouseObject.prototype as ClickHouseObject, entityType, sqlName, props, columnNames, dependsOn);
|
|
328
343
|
}
|
|
329
344
|
|
|
345
|
+
/**
|
|
346
|
+
* A declared table's, view's or dictionary's row, for application code:
|
|
347
|
+
* `RowOf<typeof events>` for a declaration given its name, or
|
|
348
|
+
* `RowOf<"shop.users">` by name, which also covers an object only a `.sql`
|
|
349
|
+
* file declares.
|
|
350
|
+
*/
|
|
351
|
+
export type RowOf<T extends keyof SqlSchema | Named<keyof SqlSchema & string>> = RowIn<SqlSchema, T>;
|
|
352
|
+
|
|
330
353
|
export function isClickHouseObject(value: unknown): value is ClickHouseObject {
|
|
331
354
|
return isSqlObjectOf(value, "ClickHouse::");
|
|
332
355
|
}
|
|
@@ -444,6 +467,42 @@ function qualified(ctx: Ctx, span: Span): { database?: string; name: string } {
|
|
|
444
467
|
return pieces.length >= 2 ? { database: pieces[pieces.length - 2], name: pieces[pieces.length - 1]! } : { name: pieces[0] ?? "" };
|
|
445
468
|
}
|
|
446
469
|
|
|
470
|
+
/**
|
|
471
|
+
* The `${}` that stand alone as an element of a sort key (`ORDER BY ${t.columns.a}`,
|
|
472
|
+
* `ORDER BY (${t.columns.a}, b)`), each a column that must not be Nullable
|
|
473
|
+
* (#3751). An element inside an expression (`ifNull(${...}, '')`) is not one.
|
|
474
|
+
*/
|
|
475
|
+
function sortKeySlots(ctx: Ctx, span: Span | undefined): TypedSlot[] {
|
|
476
|
+
if (!span) return [];
|
|
477
|
+
let sig = ctx.tokens.slice(span.from, span.to).filter((t) => !isTrivia(t));
|
|
478
|
+
const isPunct = (i: number, p: string) => sig[i]?.kind === "punct" && sig[i]!.text === p;
|
|
479
|
+
if (isPunct(0, "(") && isPunct(sig.length - 1, ")")) {
|
|
480
|
+
let depth = 0;
|
|
481
|
+
let closesAt = -1;
|
|
482
|
+
for (let i = 0; i < sig.length && closesAt < 0; i++) {
|
|
483
|
+
if (isPunct(i, "(")) depth++;
|
|
484
|
+
else if (isPunct(i, ")") && --depth === 0) closesAt = i;
|
|
485
|
+
}
|
|
486
|
+
if (closesAt === sig.length - 1) sig = sig.slice(1, -1);
|
|
487
|
+
}
|
|
488
|
+
const out: TypedSlot[] = [];
|
|
489
|
+
let depth = 0;
|
|
490
|
+
let element: typeof sig = [];
|
|
491
|
+
const flush = () => {
|
|
492
|
+
const t = element[0];
|
|
493
|
+
if (element.length === 1 && t?.kind === "ref" && isColumnRef(ctx.values[t.part])) out.push({ index: t.part, kind: "sortKey" });
|
|
494
|
+
element = [];
|
|
495
|
+
};
|
|
496
|
+
for (const t of sig) {
|
|
497
|
+
if (t.kind === "punct" && t.text === "(") depth++;
|
|
498
|
+
else if (t.kind === "punct" && t.text === ")") depth--;
|
|
499
|
+
if (depth === 0 && t.kind === "punct" && t.text === ",") flush();
|
|
500
|
+
else element.push(t);
|
|
501
|
+
}
|
|
502
|
+
flush();
|
|
503
|
+
return out;
|
|
504
|
+
}
|
|
505
|
+
|
|
447
506
|
type Expect = "database" | "table" | "view" | "dictionary" | "func";
|
|
448
507
|
|
|
449
508
|
function build(tag: Expect, strings: TemplateStringsArray | readonly string[], values: readonly unknown[]): ClickHouseObject {
|
|
@@ -484,6 +543,7 @@ function build(tag: Expect, strings: TemplateStringsArray | readonly string[], v
|
|
|
484
543
|
};
|
|
485
544
|
const done = <T extends ClickHouseObject>(entity: T): T => {
|
|
486
545
|
setInterpolationFields(entity, ctx.fed.map((paths) => [...paths].sort()));
|
|
546
|
+
if (node.statement === "table" || node.statement === "view") setTypedSlots(entity, [...sortKeySlots(ctx, node.orderBy), ...sortKeySlots(ctx, node.primaryKey)]);
|
|
487
547
|
return entity;
|
|
488
548
|
};
|
|
489
549
|
|
|
@@ -625,17 +685,22 @@ export function database(strings: TemplateStringsArray, ...values: unknown[]): C
|
|
|
625
685
|
return build("database", strings, values) as ClickHouseDatabase;
|
|
626
686
|
}
|
|
627
687
|
|
|
628
|
-
/**
|
|
629
|
-
|
|
630
|
-
|
|
688
|
+
/**
|
|
689
|
+
* `` table`CREATE TABLE ...` ``: one ClickHouse table, parsed at build time.
|
|
690
|
+
* Given its qualified name as the type argument (`` table<"shop.events">`...` ``),
|
|
691
|
+
* its columns are the ones `chant sql types` declared for that name.
|
|
692
|
+
*/
|
|
693
|
+
export function table<const N extends string = Unnamed>(strings: TemplateStringsArray, ...values: SlotsIn<SqlSchema, N>): ClickHouseTable<N> {
|
|
694
|
+
return build("table", strings, values) as ClickHouseTable<N>;
|
|
631
695
|
}
|
|
632
696
|
|
|
633
697
|
/**
|
|
634
698
|
* `` view`CREATE [MATERIALIZED] VIEW ...` ``: one view or materialized view,
|
|
635
|
-
* with the tables it reads and its column lineage.
|
|
699
|
+
* with the tables it reads and its column lineage. Takes its qualified name
|
|
700
|
+
* as the type argument, as {@link table} does.
|
|
636
701
|
*/
|
|
637
|
-
export function view(strings: TemplateStringsArray, ...values:
|
|
638
|
-
return build("view", strings, values) as ClickHouseView
|
|
702
|
+
export function view<const N extends string = Unnamed>(strings: TemplateStringsArray, ...values: SlotsIn<SqlSchema, N>): ClickHouseView<N> {
|
|
703
|
+
return build("view", strings, values) as ClickHouseView<N>;
|
|
639
704
|
}
|
|
640
705
|
|
|
641
706
|
/**
|
|
@@ -643,8 +708,8 @@ export function view(strings: TemplateStringsArray, ...values: unknown[]): Click
|
|
|
643
708
|
* key, source, layout and lifetime, parsed at build time (#3682). Its
|
|
644
709
|
* attributes are columns: `${rates.columns.rate}`.
|
|
645
710
|
*/
|
|
646
|
-
export function dictionary(strings: TemplateStringsArray, ...values:
|
|
647
|
-
return build("dictionary", strings, values) as ClickHouseDictionary
|
|
711
|
+
export function dictionary<const N extends string = Unnamed>(strings: TemplateStringsArray, ...values: SlotsIn<SqlSchema, N>): ClickHouseDictionary<N> {
|
|
712
|
+
return build("dictionary", strings, values) as ClickHouseDictionary<N>;
|
|
648
713
|
}
|
|
649
714
|
|
|
650
715
|
/**
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* without import cycles.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { relationKey } from "../../core/typed";
|
|
9
10
|
import type { GeneratedFile, TypeScriptGenerator } from "@intentius/chant/import/generator";
|
|
10
11
|
import type { TemplateIR } from "@intentius/chant/import/parser";
|
|
11
12
|
import { isTrivia, tokenizeText, type Token } from "../tokens";
|
|
@@ -162,6 +163,11 @@ function indent(body: string): string {
|
|
|
162
163
|
return lines.map((l, i) => (i === 0 || l.length === 0 ? l : ` ${l}`)).join("\n");
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
const RELATIONS = new Set<string>([CLICKHOUSE_ENTITY_TYPES.table, CLICKHOUSE_ENTITY_TYPES.view, CLICKHOUSE_ENTITY_TYPES.materializedView, CLICKHOUSE_ENTITY_TYPES.dictionary]);
|
|
167
|
+
|
|
168
|
+
/** A table's, view's or dictionary's qualified name as its tag's type argument (#3751), so its columns are typed once `chant sql types` has run. */
|
|
169
|
+
const typeArgument = (it: Item): string => (RELATIONS.has(it.type) ? `<${JSON.stringify(relationKey(it))}>` : "");
|
|
170
|
+
|
|
165
171
|
export class ClickHouseGenerator implements TypeScriptGenerator {
|
|
166
172
|
readonly ownsLayout = true;
|
|
167
173
|
|
|
@@ -186,7 +192,7 @@ export class ClickHouseGenerator implements TypeScriptGenerator {
|
|
|
186
192
|
if (this.header) lines.push(...this.header.split("\n").map((l) => `// ${l}`), "");
|
|
187
193
|
lines.push(`import { ${tags.join(", ")} } from "@intentius/chant-lexicon-sql/clickhouse";`, "");
|
|
188
194
|
for (const it of order) {
|
|
189
|
-
lines.push(`export const ${it.exportName} = ${CLICKHOUSE_TAG_OF[it.type]}\`\n ${indent(bodyText(bodies.get(it.exportName)!))}\`;`, "");
|
|
195
|
+
lines.push(`export const ${it.exportName} = ${CLICKHOUSE_TAG_OF[it.type]}${typeArgument(it)}\`\n ${indent(bodyText(bodies.get(it.exportName)!))}\`;`, "");
|
|
190
196
|
}
|
|
191
197
|
return [{ path: "schema.ts", content: `${lines.join("\n").trimEnd()}\n` }];
|
|
192
198
|
}
|
|
@@ -64,7 +64,7 @@ describe("import, build, import again", () => {
|
|
|
64
64
|
"ClickHouse::View",
|
|
65
65
|
"ClickHouse::Table",
|
|
66
66
|
]);
|
|
67
|
-
expect(file!.content).toContain(
|
|
67
|
+
expect(file!.content).toContain(`export const events = table<"analytics.events">\``);
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
test("the declarations build back to one statement per object, dependencies first", async () => {
|