@intentius/chant-lexicon-sql 0.122.0 → 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/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/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.d.ts +1 -1
- package/dist/clickhouse.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/integrity.json +7 -3
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/postgres-names.d.ts +2 -0
- package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
- package/dist/lint/post-synth/sql102.d.ts +16 -0
- package/dist/lint/post-synth/sql102.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 +3 -0
- package/dist/okf/rules/SQL102.md +11 -0
- package/dist/okf/rules/SQLCH004.md +11 -0
- package/dist/okf/rules/SQLPG005.md +11 -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/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/postgres-names.ts +6 -2
- package/dist/rules/sql102.ts +34 -0
- package/dist/rules/sqlch004.ts +81 -0
- package/dist/rules/sqlpg005.ts +73 -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/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/plan/commands.ts +5 -0
- package/src/clickhouse/row-types.ts +158 -0
- package/src/clickhouse.ts +10 -0
- package/src/core/index.ts +4 -0
- package/src/core/typed.ts +143 -0
- package/src/lint/audit-catalog.ts +20 -0
- package/src/lint/post-synth/index.ts +2 -0
- package/src/lint/post-synth/postgres-names.ts +6 -2
- package/src/lint/post-synth/sql102.ts +34 -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/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,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
|
}
|
|
@@ -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 () => {
|
|
@@ -165,5 +165,10 @@ export const sqlCommands: CommandGroup = {
|
|
|
165
165
|
commands: [
|
|
166
166
|
{ name: "diff", description: "Classify the change between two chant build outputs (offline); --statements prints the statements it takes, --topology renders them for single, cluster:<name>, replicated or cloud", handler: runDiff },
|
|
167
167
|
{ name: "plan", description: "Classify the change from an environment's server to a chant build output", handler: runPlan },
|
|
168
|
+
{
|
|
169
|
+
name: "types",
|
|
170
|
+
description: "Write sql-types.d.ts: typed columns for each table, view and dictionary the build declares, for tags given their name (table<\"app.users\">) and RowOf; --output <path> writes it elsewhere; --check compares instead of writing and exits 1 when the file differs",
|
|
171
|
+
handler: async (ctx) => (await import("../../sql-types-command")).runTypes(ctx),
|
|
172
|
+
},
|
|
168
173
|
],
|
|
169
174
|
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a ClickHouse column's value is in TypeScript, for `RowOf` (#3751): the
|
|
3
|
+
* value a row has in ClickHouse's JSON formats (`JSONEachRow`, what the HTTP
|
|
4
|
+
* clients read) with the server's default settings. 64-bit and wider
|
|
5
|
+
* integers are quoted (`output_format_json_quote_64bit_integers`), so they
|
|
6
|
+
* are strings; narrower integers, floats and decimals are numbers; dates,
|
|
7
|
+
* times, UUIDs and addresses are their text; a named tuple is an object
|
|
8
|
+
* (`output_format_json_named_tuples_as_objects`) and an unnamed one an array.
|
|
9
|
+
* `Nullable(T)` adds `| null` and `LowCardinality(T)` is `T`.
|
|
10
|
+
*
|
|
11
|
+
* The table names every type family of the pinned server's catalog
|
|
12
|
+
* (`TYPE_FAMILIES`, aliases resolved), and a test fails on one it does not,
|
|
13
|
+
* so a family a new release adds is a decision rather than a silent
|
|
14
|
+
* `unknown`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ColumnTypeFamily } from "../generated/clickhouse-types";
|
|
18
|
+
import { familyOf } from "../lint/post-synth/clickhouse-type-check";
|
|
19
|
+
import { readType, typeTokens, type TypeParam } from "./type-syntax";
|
|
20
|
+
import { isTrivia } from "./tokens";
|
|
21
|
+
|
|
22
|
+
/** How a family's parameters make its row type: `inner` maps a parameter that is itself a type. */
|
|
23
|
+
type Composed = (params: readonly TypeParam[], inner: (type: string) => string) => string;
|
|
24
|
+
|
|
25
|
+
const NUMBER = "number";
|
|
26
|
+
const STRING = "string";
|
|
27
|
+
const UNKNOWN = "unknown";
|
|
28
|
+
const POINT = "[number, number]";
|
|
29
|
+
|
|
30
|
+
/** A union needs parentheses before `[]`. */
|
|
31
|
+
const element = (ts: string): string => (/[|&]/.test(ts) && !/^\{.*\}$/s.test(ts) ? `(${ts})` : ts);
|
|
32
|
+
|
|
33
|
+
/** An enum's labels as a union of string literals; its text when none read. */
|
|
34
|
+
const enumLabels: Composed = (params) => {
|
|
35
|
+
const labels = params.flatMap((p) => {
|
|
36
|
+
const first = p.tokens.find((t) => !isTrivia(t));
|
|
37
|
+
return first?.kind === "string" ? [first.text.slice(1, -1).replace(/\\(.)/g, "$1").replace(/''/g, "'")] : [];
|
|
38
|
+
});
|
|
39
|
+
return labels.length > 0 ? labels.map((l) => JSON.stringify(l)).join(" | ") : STRING;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** The TypeScript type of each type family's value, or how its parameters make it. */
|
|
43
|
+
export const CLICKHOUSE_ROW_TYPES: Readonly<Record<ColumnTypeFamily, string | Composed>> = {
|
|
44
|
+
UInt8: NUMBER,
|
|
45
|
+
UInt16: NUMBER,
|
|
46
|
+
UInt32: NUMBER,
|
|
47
|
+
Int8: NUMBER,
|
|
48
|
+
Int16: NUMBER,
|
|
49
|
+
Int32: NUMBER,
|
|
50
|
+
Float32: NUMBER,
|
|
51
|
+
Float64: NUMBER,
|
|
52
|
+
BFloat16: NUMBER,
|
|
53
|
+
Decimal: NUMBER,
|
|
54
|
+
Decimal32: NUMBER,
|
|
55
|
+
Decimal64: NUMBER,
|
|
56
|
+
Decimal128: NUMBER,
|
|
57
|
+
Decimal256: NUMBER,
|
|
58
|
+
// Quoted in JSON, so text.
|
|
59
|
+
UInt64: STRING,
|
|
60
|
+
UInt128: STRING,
|
|
61
|
+
UInt256: STRING,
|
|
62
|
+
Int64: STRING,
|
|
63
|
+
Int128: STRING,
|
|
64
|
+
Int256: STRING,
|
|
65
|
+
IntervalNanosecond: STRING,
|
|
66
|
+
IntervalMicrosecond: STRING,
|
|
67
|
+
IntervalMillisecond: STRING,
|
|
68
|
+
IntervalSecond: STRING,
|
|
69
|
+
IntervalMinute: STRING,
|
|
70
|
+
IntervalHour: STRING,
|
|
71
|
+
IntervalDay: STRING,
|
|
72
|
+
IntervalWeek: STRING,
|
|
73
|
+
IntervalMonth: STRING,
|
|
74
|
+
IntervalQuarter: STRING,
|
|
75
|
+
IntervalYear: STRING,
|
|
76
|
+
Bool: "boolean",
|
|
77
|
+
String: STRING,
|
|
78
|
+
FixedString: STRING,
|
|
79
|
+
UUID: STRING,
|
|
80
|
+
IPv4: STRING,
|
|
81
|
+
IPv6: STRING,
|
|
82
|
+
Date: STRING,
|
|
83
|
+
Date32: STRING,
|
|
84
|
+
DateTime: STRING,
|
|
85
|
+
DateTime32: STRING,
|
|
86
|
+
DateTime64: STRING,
|
|
87
|
+
Time: STRING,
|
|
88
|
+
Time64: STRING,
|
|
89
|
+
Enum: enumLabels,
|
|
90
|
+
Enum8: enumLabels,
|
|
91
|
+
Enum16: enumLabels,
|
|
92
|
+
// Wrappers and containers.
|
|
93
|
+
Nullable: (p, inner) => `${inner(p[0]?.text ?? "")} | null`,
|
|
94
|
+
LowCardinality: (p, inner) => inner(p[0]?.text ?? ""),
|
|
95
|
+
Array: (p, inner) => `${element(inner(p[0]?.text ?? ""))}[]`,
|
|
96
|
+
Map: (p, inner) => `Record<string, ${inner(p[1]?.text ?? "")}>`,
|
|
97
|
+
Tuple: (p, inner) => {
|
|
98
|
+
const elements = p.map((x) => named(x));
|
|
99
|
+
if (elements.length > 0 && elements.every((e) => e.name !== undefined)) {
|
|
100
|
+
return `{ ${elements.map((e) => `${propertyName(e.name!)}: ${inner(e.type)}`).join("; ")} }`;
|
|
101
|
+
}
|
|
102
|
+
return `[${elements.map((e) => inner(e.type)).join(", ")}]`;
|
|
103
|
+
},
|
|
104
|
+
SimpleAggregateFunction: (p, inner) => inner(p[1]?.text ?? ""),
|
|
105
|
+
AggregateFunction: UNKNOWN,
|
|
106
|
+
Nested: UNKNOWN,
|
|
107
|
+
// Self-describing: the application's own shape.
|
|
108
|
+
JSON: UNKNOWN,
|
|
109
|
+
Dynamic: UNKNOWN,
|
|
110
|
+
Variant: UNKNOWN,
|
|
111
|
+
QBit: UNKNOWN,
|
|
112
|
+
Nothing: "null",
|
|
113
|
+
// Geometry, as the JSON formats write it: points as pairs, nested in arrays.
|
|
114
|
+
Point: POINT,
|
|
115
|
+
Ring: `${POINT}[]`,
|
|
116
|
+
LineString: `${POINT}[]`,
|
|
117
|
+
Polygon: `${POINT}[][]`,
|
|
118
|
+
MultiLineString: `${POINT}[][]`,
|
|
119
|
+
MultiPoint: `${POINT}[]`,
|
|
120
|
+
MultiPolygon: `${POINT}[][][]`,
|
|
121
|
+
Geometry: UNKNOWN,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** A tuple element: `a String` is named `a`, `String` is not. */
|
|
125
|
+
function named(param: TypeParam): { name?: string; type: string } {
|
|
126
|
+
const syn = readType(param.tokens);
|
|
127
|
+
if (!syn || syn.words.length < 2) return { type: param.text };
|
|
128
|
+
if (familyOf(syn.words.map((w) => w.text).join(" "))) return { type: param.text };
|
|
129
|
+
const first = syn.words[0]!;
|
|
130
|
+
const rest = param.text.slice(param.text.indexOf(first.text) + first.text.length).trim();
|
|
131
|
+
return { name: first.kind === "qident" ? first.text.slice(1, -1) : first.text, type: rest };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const propertyName = (name: string): string => (/^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name));
|
|
135
|
+
|
|
136
|
+
/** A type's row type, wrappers and all; `unknown` for a type this cannot read. */
|
|
137
|
+
export function clickhouseTsType(type: string): string {
|
|
138
|
+
const tokens = typeTokens(type);
|
|
139
|
+
const syn = tokens && readType(tokens);
|
|
140
|
+
if (!syn) return UNKNOWN;
|
|
141
|
+
const spec = familyOf(syn.words.map((w) => w.text).join(" "));
|
|
142
|
+
const entry = spec ? (CLICKHOUSE_ROW_TYPES as Readonly<Record<string, string | Composed | undefined>>)[spec.canonical] : undefined;
|
|
143
|
+
if (entry === undefined) return UNKNOWN;
|
|
144
|
+
return typeof entry === "string" ? entry : entry(syn.params ?? [], clickhouseTsType);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A column's row type and whether it can be NULL: `Nullable(...)` at the top, under any `LowCardinality(...)`. */
|
|
148
|
+
export function clickhouseRowType(type: string): { ts: string; nullable: boolean } {
|
|
149
|
+
let t = type.trim();
|
|
150
|
+
let nullable = false;
|
|
151
|
+
for (;;) {
|
|
152
|
+
const m = /^(Nullable|LowCardinality)\s*\(([\s\S]*)\)$/i.exec(t);
|
|
153
|
+
if (!m) break;
|
|
154
|
+
if (m[1]!.toLowerCase() === "nullable") nullable = true;
|
|
155
|
+
t = m[2]!.trim();
|
|
156
|
+
}
|
|
157
|
+
return { ts: clickhouseTsType(t), nullable };
|
|
158
|
+
}
|
package/src/clickhouse.ts
CHANGED
|
@@ -50,6 +50,16 @@ export {
|
|
|
50
50
|
type LineageEdge,
|
|
51
51
|
type TableProps,
|
|
52
52
|
type ViewProps,
|
|
53
|
+
type SqlSchema,
|
|
54
|
+
type RowOf,
|
|
55
|
+
type Column,
|
|
56
|
+
type ColumnTraits,
|
|
57
|
+
type AnyColumn,
|
|
58
|
+
type UncheckedColumn,
|
|
59
|
+
type KeyColumn,
|
|
60
|
+
type NotNullColumn,
|
|
61
|
+
type Named,
|
|
62
|
+
type Unnamed,
|
|
53
63
|
} from "./clickhouse/entities";
|
|
54
64
|
export * from "./generated/clickhouse";
|
|
55
65
|
export * from "./composites/clickhouse";
|
package/src/core/index.ts
CHANGED
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
* (`classifier.ts`);
|
|
26
26
|
* - normalization helpers that are not a database's grammar, such as the
|
|
27
27
|
* `-- previously:` hint (`normalize.ts`);
|
|
28
|
+
* - typed columns: the brands, `ColumnsIn`, `SlotsIn` and `RowIn` each
|
|
29
|
+
* dialect's `SqlSchema` is read through, and the slot record a tag leaves
|
|
30
|
+
* for `chant sql types` (`typed.ts`);
|
|
28
31
|
* - the ownership trailer on an object's comment (`ownership.ts`);
|
|
29
32
|
* - receipts kept on the server (`receipts.ts`);
|
|
30
33
|
* - the applier's tri-state outcome, its envelope projection and the build
|
|
@@ -54,6 +57,7 @@ export * from "./references";
|
|
|
54
57
|
export * from "./diff";
|
|
55
58
|
export * from "./classifier";
|
|
56
59
|
export * from "./normalize";
|
|
60
|
+
export * from "./typed";
|
|
57
61
|
export * from "./ownership";
|
|
58
62
|
export * from "./receipts";
|
|
59
63
|
export * from "./apply";
|