@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
|
@@ -29,6 +29,7 @@ const WIDTH: ArgumentOverlay & { name: string } = {
|
|
|
29
29
|
name: "bytes",
|
|
30
30
|
kind: "number",
|
|
31
31
|
optional: true,
|
|
32
|
+
values: ["1", "2", "4", "8"],
|
|
32
33
|
note: "1, 2, 4 or 8; the column type's width when omitted.",
|
|
33
34
|
};
|
|
34
35
|
|
|
@@ -40,7 +41,10 @@ export const CODEC_OVERLAY: Record<string, CodecOverlay> = {
|
|
|
40
41
|
role: "compression",
|
|
41
42
|
},
|
|
42
43
|
ZSTD: {
|
|
43
|
-
parameters: [
|
|
44
|
+
parameters: [
|
|
45
|
+
{ name: "level", kind: "number", optional: true, range: [1, 22], note: "1 when omitted." },
|
|
46
|
+
{ name: "window_log", kind: "number", optional: true, range: [10, 31], note: "Turns on long-range matching with this window log." },
|
|
47
|
+
],
|
|
44
48
|
role: "compression",
|
|
45
49
|
},
|
|
46
50
|
Delta: { parameters: [WIDTH], role: "preprocessing" },
|
|
@@ -54,7 +58,7 @@ export const CODEC_OVERLAY: Record<string, CodecOverlay> = {
|
|
|
54
58
|
FPC: {
|
|
55
59
|
parameters: [
|
|
56
60
|
{ name: "level", kind: "number", optional: true, range: [1, 28], note: "12 when omitted." },
|
|
57
|
-
{ name: "float_size", kind: "number", optional: true, note: "4 or 8; the column type's width when omitted." },
|
|
61
|
+
{ name: "float_size", kind: "number", optional: true, values: ["4", "8"], note: "4 or 8; the column type's width when omitted." },
|
|
58
62
|
],
|
|
59
63
|
role: "compression",
|
|
60
64
|
},
|
|
@@ -29,7 +29,7 @@ export interface ArgumentOverlay {
|
|
|
29
29
|
kind: ArgumentKind;
|
|
30
30
|
/** Overrides the syntax line's optionality, with `note` saying why. */
|
|
31
31
|
optional?: boolean;
|
|
32
|
-
/** The legal values of a `keyword
|
|
32
|
+
/** The legal values of a `keyword`, or of a `number` limited to a set (a codec's byte width: 1, 2, 4 or 8). */
|
|
33
33
|
values?: readonly string[];
|
|
34
34
|
/** The inclusive range of a `number`. */
|
|
35
35
|
range?: readonly [number, number];
|
|
@@ -26,16 +26,33 @@ const P = (name: string, kind: TypeParameter["kind"], extra: Partial<TypeParamet
|
|
|
26
26
|
|
|
27
27
|
const TZ = P("timezone", "string", { optional: true });
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* The server takes and ignores a MySQL-style display width on an integer
|
|
31
|
+
* (`INT(11)`, `UInt64(8)`), a precision and scale on a float (`DOUBLE(10, 2)`)
|
|
32
|
+
* and a length on a string (`VARCHAR(255)`), so each is an optional parameter
|
|
33
|
+
* here rather than a mistake.
|
|
34
|
+
*/
|
|
35
|
+
const WIDTH = [P("display_width", "number", { optional: true })];
|
|
36
|
+
const FLOAT = [P("precision", "number", { optional: true }), P("scale", "number", { optional: true })];
|
|
37
|
+
const INTEGERS = ["Int8", "Int16", "Int32", "Int64", "Int128", "Int256", "UInt8", "UInt16", "UInt32", "UInt64", "UInt128", "UInt256"];
|
|
38
|
+
|
|
29
39
|
export const TYPE_PARAMETERS: Record<string, readonly TypeParameter[]> = {
|
|
30
|
-
|
|
40
|
+
...Object.fromEntries(INTEGERS.map((name) => [name, WIDTH])),
|
|
41
|
+
Float32: FLOAT,
|
|
42
|
+
Float64: FLOAT,
|
|
43
|
+
BFloat16: FLOAT,
|
|
44
|
+
String: [P("length", "number", { optional: true })],
|
|
45
|
+
/** A bare `Decimal` is `Decimal(10, 0)`. */
|
|
46
|
+
Decimal: [P("precision", "number", { optional: true, range: [1, 76] }), P("scale", "number", { optional: true, range: [0, 76] })],
|
|
31
47
|
Decimal32: [P("scale", "number", { range: [0, 9] })],
|
|
32
48
|
Decimal64: [P("scale", "number", { range: [0, 18] })],
|
|
33
49
|
Decimal128: [P("scale", "number", { range: [0, 38] })],
|
|
34
50
|
Decimal256: [P("scale", "number", { range: [0, 76] })],
|
|
35
51
|
DateTime: [TZ],
|
|
36
52
|
DateTime32: [TZ],
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
/** A bare `DateTime64` has precision 3. */
|
|
54
|
+
DateTime64: [P("precision", "number", { optional: true, range: [0, 9] }), TZ],
|
|
55
|
+
Time64: [P("precision", "number", { optional: true, range: [0, 9] })],
|
|
39
56
|
FixedString: [P("length", "number", { range: [1, Number.MAX_SAFE_INTEGER] })],
|
|
40
57
|
Enum: [P("member", "enum-member", { repeated: true })],
|
|
41
58
|
Enum8: [P("member", "enum-member", { repeated: true })],
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A column type, read into its family and parameters: `Nullable(String)` is
|
|
3
|
+
* the family `Nullable` with the one parameter `String`;
|
|
4
|
+
* `DateTime64(3, 'UTC')` is `DateTime64` with `3` and `'UTC'`.
|
|
5
|
+
*
|
|
6
|
+
* The reading is lexical, over the same tokenizer the parser uses, so a
|
|
7
|
+
* string literal holding a parenthesis (`Enum8('a(' = 1)`) or a comma does not
|
|
8
|
+
* split a parameter. What a parameter means (a type, a number, a name) is the
|
|
9
|
+
* caller's question, answered from `TYPE_PARAMETERS`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { isTrivia, tokenizeText, type Token } from "./tokens";
|
|
13
|
+
|
|
14
|
+
export interface TypeParam {
|
|
15
|
+
/** The parameter as written, trimmed. */
|
|
16
|
+
text: string;
|
|
17
|
+
tokens: Token[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TypeSyntax {
|
|
21
|
+
/** The words before the parameters: `UInt64`, `BIGINT UNSIGNED`, or `a String` for a named tuple element. */
|
|
22
|
+
words: Token[];
|
|
23
|
+
/** The parameters, or undefined when the type has no parentheses. */
|
|
24
|
+
params?: TypeParam[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const textOf = (tokens: readonly Token[]): string => tokens.map((t) => t.text).join("").trim();
|
|
28
|
+
|
|
29
|
+
/** The significant tokens of a type, or undefined when it does not tokenize. */
|
|
30
|
+
export function typeTokens(text: string): Token[] | undefined {
|
|
31
|
+
try {
|
|
32
|
+
return tokenizeText(text, 0);
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Read a type into its leading words and its parameters. Undefined when the
|
|
40
|
+
* text is not a type this can read: no leading name, or unbalanced parentheses.
|
|
41
|
+
* Anything after the closing parenthesis (`COLLATE utf8`) is ignored.
|
|
42
|
+
*/
|
|
43
|
+
export function readType(tokens: readonly Token[]): TypeSyntax | undefined {
|
|
44
|
+
const sig = tokens.filter((t) => !isTrivia(t));
|
|
45
|
+
const words: Token[] = [];
|
|
46
|
+
let i = 0;
|
|
47
|
+
// `String COLLATE utf8`: COLLATE ends the type's words.
|
|
48
|
+
while (i < sig.length && (sig[i]!.kind === "ident" || sig[i]!.kind === "qident") && sig[i]!.text.toUpperCase() !== "COLLATE") words.push(sig[i++]!);
|
|
49
|
+
if (words.length === 0) return undefined;
|
|
50
|
+
if (i >= sig.length || !(sig[i]!.kind === "punct" && sig[i]!.text === "(")) return { words };
|
|
51
|
+
// Parameters: the tokens up to the matching parenthesis, split at top-level commas.
|
|
52
|
+
const start = tokens.indexOf(sig[i]!);
|
|
53
|
+
const params: TypeParam[] = [];
|
|
54
|
+
let depth = 0;
|
|
55
|
+
let current: Token[] = [];
|
|
56
|
+
for (let j = start; j < tokens.length; j++) {
|
|
57
|
+
const t = tokens[j]!;
|
|
58
|
+
if (t.kind === "punct" && t.text === "(") {
|
|
59
|
+
if (depth++ === 0) continue;
|
|
60
|
+
} else if (t.kind === "punct" && t.text === ")") {
|
|
61
|
+
if (--depth === 0) {
|
|
62
|
+
if (params.length > 0 || current.some((x) => !isTrivia(x))) params.push({ text: textOf(current), tokens: current });
|
|
63
|
+
return { words, params };
|
|
64
|
+
}
|
|
65
|
+
} else if (depth === 1 && t.kind === "punct" && t.text === ",") {
|
|
66
|
+
params.push({ text: textOf(current), tokens: current });
|
|
67
|
+
current = [];
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
current.push(t);
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A parameter that is a numeric literal, possibly negative: its value. */
|
|
76
|
+
export function numericParam(param: TypeParam): number | undefined {
|
|
77
|
+
const sig = param.tokens.filter((t) => !isTrivia(t));
|
|
78
|
+
const negative = sig.length === 2 && sig[0]!.kind === "op" && sig[0]!.text === "-";
|
|
79
|
+
const n = negative ? sig[1] : sig.length === 1 ? sig[0] : undefined;
|
|
80
|
+
if (!n || n.kind !== "number" || !/^\d+(\.\d+)?([eE][+-]?\d+)?$/.test(n.text)) return undefined;
|
|
81
|
+
return negative ? -Number(n.text) : Number(n.text);
|
|
82
|
+
}
|
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";
|
|
@@ -219,7 +219,7 @@ export function renderClickHouseModule(catalog: ClickHouseCatalog): RenderedModu
|
|
|
219
219
|
|
|
220
220
|
const tables = [
|
|
221
221
|
...header("The tables lint and the LSP read, typed by ./clickhouse-types."),
|
|
222
|
-
`import type { CodecSpec, DatabaseEngineSpec, SettingSpec, SkipIndexSpec, TableEngineSpec, TypeFamilySpec } from "../clickhouse/catalog-types";`,
|
|
222
|
+
`import type { CodecSpec, DatabaseEngineSpec, FunctionSpec, SettingSpec, SkipIndexSpec, TableEngineSpec, TypeFamilySpec } from "../clickhouse/catalog-types";`,
|
|
223
223
|
`import type { CodecName, ColumnTypeName, DatabaseEngineName, MergeTreeSettings, QuerySettings, SkipIndexType, TableEngineName } from "./clickhouse-types";`,
|
|
224
224
|
"",
|
|
225
225
|
`export * from "./clickhouse-types";`,
|
|
@@ -255,6 +255,20 @@ export function renderClickHouseModule(catalog: ClickHouseCatalog): RenderedModu
|
|
|
255
255
|
}),
|
|
256
256
|
"};",
|
|
257
257
|
"",
|
|
258
|
+
`/** Every function the server has (\`system.functions\`, origin System), aliases included. */`,
|
|
259
|
+
`export const FUNCTIONS: Readonly<Record<string, FunctionSpec>> = {`,
|
|
260
|
+
...catalog.functions.map(
|
|
261
|
+
(f) =>
|
|
262
|
+
` ${key(f.name)}: ${JSON.stringify({ aggregate: f.aggregate, caseInsensitive: f.caseInsensitive, ...(f.aliasOf ? { aliasOf: f.aliasOf } : {}) })},`,
|
|
263
|
+
),
|
|
264
|
+
"};",
|
|
265
|
+
"",
|
|
266
|
+
`/** The aggregate function combinators (\`system.aggregate_function_combinators\`): the suffixes of \`countIf\`, \`sumState\`. */`,
|
|
267
|
+
`export const AGGREGATE_COMBINATORS: readonly string[] = ${JSON.stringify(catalog.aggregateCombinators.map((c) => c.name))};`,
|
|
268
|
+
"",
|
|
269
|
+
`/** The table functions (\`system.table_functions\`): \`numbers\`, \`remote\`, \`s3\`. */`,
|
|
270
|
+
`export const TABLE_FUNCTIONS: readonly string[] = ${JSON.stringify(catalog.tableFunctions)};`,
|
|
271
|
+
"",
|
|
258
272
|
`export const SKIP_INDEX_TYPES: Readonly<Record<SkipIndexType, SkipIndexSpec>> = {`,
|
|
259
273
|
...indexLines,
|
|
260
274
|
"};",
|
|
@@ -410,6 +410,43 @@ export function renderPostgresModule(catalogs: readonly PostgresCatalog[]): Rend
|
|
|
410
410
|
"};",
|
|
411
411
|
];
|
|
412
412
|
|
|
413
|
+
// Names lint checks a declaration against, each with the majors that have it
|
|
414
|
+
// (`true`: every supported major). Read straight off the catalogs, internal
|
|
415
|
+
// functions included: a column default may call `pg_current_xact_id()`.
|
|
416
|
+
const presence = (lists: ReadonlyArray<readonly string[]>): Map<string, number[]> => {
|
|
417
|
+
const out = new Map<string, number[]>();
|
|
418
|
+
lists.forEach((names, i) => {
|
|
419
|
+
for (const n of new Set(names)) out.set(n, [...(out.get(n) ?? []), majors[i]!]);
|
|
420
|
+
});
|
|
421
|
+
return out;
|
|
422
|
+
};
|
|
423
|
+
const majorsLiteral = (ms: readonly number[]): string => (ms.length === majors.length ? "true" : JSON.stringify(ms));
|
|
424
|
+
const presenceTable = (m: Map<string, number[]>, indent: string): string[] =>
|
|
425
|
+
sorted(m.keys()).map((n) => `${indent}${JSON.stringify(n)}: ${majorsLiteral(m.get(n)!)},`);
|
|
426
|
+
const functionTable = [
|
|
427
|
+
`/** Every function, aggregate and procedure name in \`pg_proc\`, to the majors that have it (\`true\`: all of them). */`,
|
|
428
|
+
`export const FUNCTIONS: Readonly<Record<string, true | readonly number[]>> = {`,
|
|
429
|
+
...presenceTable(presence(catalogs.map((c) => c.functions.map((f) => f.name))), " "),
|
|
430
|
+
"};",
|
|
431
|
+
];
|
|
432
|
+
const methodTable = [
|
|
433
|
+
`/** The index access methods, to the majors that have each (\`true\`: all of them). */`,
|
|
434
|
+
`export const INDEX_ACCESS_METHODS: Readonly<Record<string, true | readonly number[]>> = {`,
|
|
435
|
+
...presenceTable(presence(catalogs.map((c) => c.accessMethods.filter((a) => a.type === "i").map((a) => a.name))), " "),
|
|
436
|
+
"};",
|
|
437
|
+
];
|
|
438
|
+
const opclassAms = sorted(catalogs.flatMap((c) => c.opclasses.map((o) => o.am)));
|
|
439
|
+
const opclassTable = [
|
|
440
|
+
`/** The operator classes of each index access method, to the majors that have each (\`true\`: all of them). */`,
|
|
441
|
+
`export const OPERATOR_CLASSES: Readonly<Record<string, Readonly<Record<string, true | readonly number[]>>>> = {`,
|
|
442
|
+
...opclassAms.flatMap((am) => [
|
|
443
|
+
` ${key(am)}: {`,
|
|
444
|
+
...presenceTable(presence(catalogs.map((c) => c.opclasses.filter((o) => o.am === am).map((o) => o.name))), " "),
|
|
445
|
+
" },",
|
|
446
|
+
]),
|
|
447
|
+
"};",
|
|
448
|
+
];
|
|
449
|
+
|
|
413
450
|
const tables = [
|
|
414
451
|
...header("The tables lint and the LSP read, typed by ./postgres-types."),
|
|
415
452
|
`import type { ColumnTypeSpec, SettingSpec, StorageParameterSpec } from "../postgres/catalog-types";`,
|
|
@@ -425,6 +462,12 @@ export function renderPostgresModule(catalogs: readonly PostgresCatalog[]): Rend
|
|
|
425
462
|
"",
|
|
426
463
|
...keywordTable,
|
|
427
464
|
"",
|
|
465
|
+
...functionTable,
|
|
466
|
+
"",
|
|
467
|
+
...methodTable,
|
|
468
|
+
"",
|
|
469
|
+
...opclassTable,
|
|
470
|
+
"",
|
|
428
471
|
].join("\n");
|
|
429
472
|
|
|
430
473
|
notes.push(
|
|
@@ -202,7 +202,7 @@ test("a prop that is not valid SQL is refused at the interpolation that carried
|
|
|
202
202
|
// ── What the lexicon's own checks say ──────────────────────────────────
|
|
203
203
|
|
|
204
204
|
describe("the composites pass the lexicon's post-synth checks", () => {
|
|
205
|
-
test("SQLCH101-
|
|
205
|
+
test("SQL101 and SQLCH101-127 find nothing in any of them", () => {
|
|
206
206
|
const source = events();
|
|
207
207
|
const instances: Array<[string, CompositeInstance]> = [
|
|
208
208
|
["users", ReplacingTable({ name: "users", columns: "id UInt64, email String", orderBy: "id" })],
|
|
@@ -230,10 +230,11 @@ describe("the composites pass the lexicon's post-synth checks", () => {
|
|
|
230
230
|
expect(doc.applyOrder.indexOf("hitsLocal")).toBeLessThan(doc.applyOrder.indexOf("hitsDistributed"));
|
|
231
231
|
|
|
232
232
|
const ctx = makePostSynthCtx("sql", out.primary, entities);
|
|
233
|
-
const clickhouse = postSynthChecks.filter((c) => c.id.startsWith("SQLCH"));
|
|
233
|
+
const clickhouse = postSynthChecks.filter((c) => c.id.startsWith("SQLCH") || c.id === "SQL101");
|
|
234
234
|
const ids = clickhouse.map((c) => c.id).sort();
|
|
235
|
-
expect(ids[0]).toBe("
|
|
236
|
-
expect(ids
|
|
235
|
+
expect(ids[0]).toBe("SQL101");
|
|
236
|
+
expect(ids).toContain("SQLCH101");
|
|
237
|
+
expect(ids.at(-1)).toBe("SQLCH127");
|
|
237
238
|
expect(clickhouse.flatMap((check) => check.check(ctx))).toEqual([]);
|
|
238
239
|
});
|
|
239
240
|
});
|
|
@@ -559,7 +560,7 @@ describe("the Postgres composites", () => {
|
|
|
559
560
|
expect(() => TenantTable({ name: "notes", columns: "id bigint NOT NULL, (", primaryKey: "id", indexOn: "id" })).toThrow(SqlTemplateError);
|
|
560
561
|
});
|
|
561
562
|
|
|
562
|
-
test("SQLPG101 to
|
|
563
|
+
test("SQLPG101 to SQLPG126 find nothing in any of them", () => {
|
|
563
564
|
const people = users();
|
|
564
565
|
const teams = SoftDeleteTable({ name: "teams", schema: "app", columns: "id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL" });
|
|
565
566
|
const members = JoinTable({ name: "memberships", schema: "app", left: people.table, leftColumn: "user_id", right: teams.table, rightColumn: "team_id" });
|
|
@@ -588,7 +589,7 @@ describe("the Postgres composites", () => {
|
|
|
588
589
|
|
|
589
590
|
const ctx = makePostSynthCtx("sql", out.primary, entities);
|
|
590
591
|
const checks = postSynthChecks.filter((c) => c.id.startsWith("SQLPG"));
|
|
591
|
-
expect(checks).toHaveLength(
|
|
592
|
+
expect(checks).toHaveLength(26);
|
|
592
593
|
expect(checks.flatMap((check) => check.check(ctx).map((d) => `${check.id} ${d.message}`))).toEqual([]);
|
|
593
594
|
});
|
|
594
595
|
});
|
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";
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed columns (#3751): the types that carry a relation's columns from the
|
|
3
|
+
* declarations `chant sql types` generates to `tsc`, in every dialect.
|
|
4
|
+
*
|
|
5
|
+
* Each dialect declares an empty `SqlSchema` interface keyed by qualified
|
|
6
|
+
* name, and the generated `sql-types.d.ts` augments it. A tag given the name
|
|
7
|
+
* as its type argument (`` table<"shop.orders">`CREATE TABLE ...` ``) returns
|
|
8
|
+
* a relation whose `columns` are that entry's, each a {@link Column} branded
|
|
9
|
+
* with its SQL type. The tag's interpolations are typed by the entry's
|
|
10
|
+
* `slots`: the generator writes, per `${}` position, the column a foreign key
|
|
11
|
+
* or a sort key there must be ({@link KeyColumn}, {@link NotNullColumn}).
|
|
12
|
+
*
|
|
13
|
+
* A relation with no type argument, or one the generated file does not list
|
|
14
|
+
* yet, keeps unchecked columns: any name type-checks and every slot takes
|
|
15
|
+
* them, so a project adopts the type argument one declaration at a time.
|
|
16
|
+
*
|
|
17
|
+
* The runtime half is the record of those slots: a tag notes which `${}`
|
|
18
|
+
* feeds a foreign key's column list or a sort key, off the serialized output
|
|
19
|
+
* as `setInterpolationFields` keeps its record, for the generator to read.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { AttrRef } from "@intentius/chant/attrref";
|
|
23
|
+
|
|
24
|
+
declare const columnBrand: unique symbol;
|
|
25
|
+
declare const checkedBrand: unique symbol;
|
|
26
|
+
declare const relationName: unique symbol;
|
|
27
|
+
declare const unnamedBrand: unique symbol;
|
|
28
|
+
|
|
29
|
+
/** What the generator knows about a column. */
|
|
30
|
+
export interface ColumnTraits {
|
|
31
|
+
/** The row type `RowOf` gives the column, before `| null`. */
|
|
32
|
+
readonly ts: unknown;
|
|
33
|
+
readonly nullable: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* How the column compares in a key. Postgres: the catalog category letter
|
|
36
|
+
* (`N`, `S`), or the type itself for the user-defined category, an enum or
|
|
37
|
+
* a row type; a domain compares as its base. ClickHouse: the type family.
|
|
38
|
+
*/
|
|
39
|
+
readonly cmp: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A column reference branded with its SQL type, spelled as the catalog
|
|
44
|
+
* spells it. `Checked` is true for a column the generated declarations type,
|
|
45
|
+
* false for an unchecked one; it is a discriminant, so a slot's error names
|
|
46
|
+
* the checked member it wanted.
|
|
47
|
+
*/
|
|
48
|
+
export interface Column<SqlType extends string = string, T extends ColumnTraits = ColumnTraits, Checked extends boolean = true> extends AttrRef {
|
|
49
|
+
readonly [columnBrand]: { readonly sqlType: SqlType; readonly traits: T };
|
|
50
|
+
readonly [checkedBrand]: Checked;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A column of a relation the declarations do not cover: any name type-checks, and it fits every slot. */
|
|
54
|
+
export type UncheckedColumn = Column<string, ColumnTraits, false>;
|
|
55
|
+
|
|
56
|
+
/** Any column, checked or not: what a relation annotated without a name (`PostgresTable`) holds. */
|
|
57
|
+
export type AnyColumn = Column<string, ColumnTraits, boolean>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* What a slot takes besides its checked column: an unchecked relation's
|
|
61
|
+
* column, which reads as `| undefined` under `noUncheckedIndexedAccess`. A
|
|
62
|
+
* checked relation's columns are named properties, so nothing of theirs gets
|
|
63
|
+
* through here.
|
|
64
|
+
*/
|
|
65
|
+
type Unchecked = UncheckedColumn | undefined;
|
|
66
|
+
|
|
67
|
+
/** A `${}` that must be a column comparing as `Cmp`: the referenced column of a foreign key. */
|
|
68
|
+
export type KeyColumn<Cmp extends string> = Unchecked | Column<string, { readonly ts: unknown; readonly nullable: boolean; readonly cmp: Cmp }>;
|
|
69
|
+
|
|
70
|
+
/** A `${}` that must be a column that cannot be NULL: an element of a ClickHouse sort key. */
|
|
71
|
+
export type NotNullColumn = Unchecked | Column<string, { readonly ts: unknown; readonly nullable: false; readonly cmp: string }>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The name a relation's type carries when its tag has no type argument. Its
|
|
75
|
+
* columns are unchecked, and `RowOf` refuses it.
|
|
76
|
+
*/
|
|
77
|
+
export type Unnamed = string & { readonly [unnamedBrand]: true };
|
|
78
|
+
|
|
79
|
+
/** Carries a relation's declared name on its type, so `RowOf<typeof t>` can find its entry. */
|
|
80
|
+
export interface Named<N extends string> {
|
|
81
|
+
readonly [relationName]?: N;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The columns of relation `N` in `Registry` (a dialect's `SqlSchema`):
|
|
86
|
+
* unchecked for an {@link Unnamed} relation or a name the registry does not
|
|
87
|
+
* list, any column for the bare annotation (`N` is `string`), and the
|
|
88
|
+
* entry's own otherwise.
|
|
89
|
+
*/
|
|
90
|
+
export type ColumnsIn<Registry, N extends string> = [N] extends [Unnamed]
|
|
91
|
+
? Readonly<Record<string, UncheckedColumn>>
|
|
92
|
+
: string extends N
|
|
93
|
+
? Readonly<Record<string, AnyColumn>>
|
|
94
|
+
: N extends keyof Registry
|
|
95
|
+
? Registry[N] extends { readonly columns: infer C } ? C : never
|
|
96
|
+
: Readonly<Record<string, UncheckedColumn>>;
|
|
97
|
+
|
|
98
|
+
/** The interpolations a tag for relation `N` takes: the entry's slots, then anything. */
|
|
99
|
+
export type SlotsIn<Registry, N extends string> = N extends keyof Registry
|
|
100
|
+
? Registry[N] extends { readonly slots: infer S extends readonly unknown[] } ? [...S, ...unknown[]] : unknown[]
|
|
101
|
+
: unknown[];
|
|
102
|
+
|
|
103
|
+
type RowFromColumns<C> = {
|
|
104
|
+
-readonly [K in keyof C]: C[K] extends Column<string, infer T> ? (T["nullable"] extends true ? T["ts"] | null : T["ts"]) : unknown;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** A relation's row: `T` is its name or a relation's type carrying one. */
|
|
108
|
+
export type RowIn<Registry, T> = T extends string
|
|
109
|
+
? RowFromColumns<ColumnsIn<Registry, T>>
|
|
110
|
+
: T extends Named<infer N> ? RowFromColumns<ColumnsIn<Registry, N>> : never;
|
|
111
|
+
|
|
112
|
+
// ── The slot record ─────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* One `${}` the generator types. `references`: a column in a foreign key's
|
|
116
|
+
* referenced column list, paired with `column`, the referencing table's own
|
|
117
|
+
* column at that position. `sortKey`: a bare element of a ClickHouse
|
|
118
|
+
* `ORDER BY` or `PRIMARY KEY`.
|
|
119
|
+
*/
|
|
120
|
+
export type TypedSlot = { readonly index: number; readonly kind: "references"; readonly column: string } | { readonly index: number; readonly kind: "sortKey" };
|
|
121
|
+
|
|
122
|
+
const TYPED_SLOTS = Symbol.for("chant.sql.typedSlots");
|
|
123
|
+
|
|
124
|
+
/** Record the typed slots on an entity: non-enumerable and symbol-keyed, so no serializer sees it. */
|
|
125
|
+
export function setTypedSlots(entity: object, slots: readonly TypedSlot[]): void {
|
|
126
|
+
if (slots.length === 0 || !Object.isExtensible(entity)) return;
|
|
127
|
+
Object.defineProperty(entity, TYPED_SLOTS, { value: Object.freeze([...slots]), enumerable: false, writable: false, configurable: true });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** What {@link setTypedSlots} recorded on an entity; empty when nothing was. */
|
|
131
|
+
export function getTypedSlots(entity: object): readonly TypedSlot[] {
|
|
132
|
+
return ((entity as Record<symbol, unknown>)[TYPED_SLOTS] as readonly TypedSlot[] | undefined) ?? [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A relation's key in `SqlSchema`: `schema.name` (Postgres) or
|
|
137
|
+
* `database.name` (ClickHouse), unquoted and with Postgres's folding
|
|
138
|
+
* applied, or the name alone when the DDL does not qualify it.
|
|
139
|
+
*/
|
|
140
|
+
export function relationKey(props: { readonly schema?: string; readonly database?: string; readonly name: string }): string {
|
|
141
|
+
const qualifier = props.schema ?? props.database;
|
|
142
|
+
return qualifier ? `${qualifier}.${props.name}` : props.name;
|
|
143
|
+
}
|