@quark-fw/clisma 0.1.26 → 0.1.28

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.
Files changed (61) hide show
  1. package/dist/src/assert-replicated-schema.d.ts +2 -0
  2. package/dist/src/assert-replicated-schema.js +16 -0
  3. package/dist/src/build-count-query.d.ts +4 -0
  4. package/dist/src/build-count-query.js +13 -0
  5. package/dist/src/build-create-dictionary-query.d.ts +2 -0
  6. package/dist/src/build-create-dictionary-query.js +51 -0
  7. package/dist/src/build-delete-query.d.ts +4 -0
  8. package/dist/src/build-delete-query.js +11 -0
  9. package/dist/src/build-find-many-query.d.ts +3 -0
  10. package/dist/src/build-find-many-query.js +58 -0
  11. package/dist/src/build-index-sql.d.ts +2 -0
  12. package/dist/src/build-index-sql.js +11 -0
  13. package/dist/src/build-lightweight-delete-query.d.ts +4 -0
  14. package/dist/src/build-lightweight-delete-query.js +11 -0
  15. package/dist/src/build-update-query.d.ts +4 -0
  16. package/dist/src/build-update-query.js +13 -0
  17. package/dist/src/client.d.ts +36 -0
  18. package/dist/src/client.js +139 -0
  19. package/dist/src/column.d.ts +46 -0
  20. package/dist/src/column.js +79 -0
  21. package/dist/src/ddl-shared.d.ts +13 -0
  22. package/dist/src/ddl-shared.js +54 -0
  23. package/dist/src/ddl.d.ts +5 -0
  24. package/dist/src/ddl.js +52 -0
  25. package/dist/src/dictionary.d.ts +26 -0
  26. package/dist/src/dictionary.js +7 -0
  27. package/dist/src/engine.d.ts +16 -0
  28. package/dist/src/engine.js +26 -0
  29. package/dist/src/ensure-dictionaries-on-nodes.d.ts +3 -0
  30. package/dist/src/ensure-dictionaries-on-nodes.js +15 -0
  31. package/dist/src/ensure-tables-on-nodes.d.ts +11 -0
  32. package/dist/src/ensure-tables-on-nodes.js +39 -0
  33. package/dist/src/escape-id.d.ts +1 -0
  34. package/dist/src/escape-id.js +4 -0
  35. package/dist/src/expr.d.ts +5 -0
  36. package/dist/src/expr.js +4 -0
  37. package/dist/src/index.d.ts +17 -0
  38. package/dist/src/index.js +15 -0
  39. package/dist/src/migrate.d.ts +14 -0
  40. package/dist/src/migrate.js +91 -0
  41. package/dist/src/migration-error.d.ts +10 -0
  42. package/dist/src/migration-error.js +24 -0
  43. package/dist/src/query.d.ts +26 -0
  44. package/dist/src/query.js +60 -0
  45. package/dist/src/rows.d.ts +2 -0
  46. package/dist/src/rows.js +45 -0
  47. package/dist/src/schema.d.ts +8 -0
  48. package/dist/src/schema.js +4 -0
  49. package/dist/src/sql-shared.d.ts +33 -0
  50. package/dist/src/sql-shared.js +171 -0
  51. package/dist/src/sql.d.ts +6 -0
  52. package/dist/src/sql.js +9 -0
  53. package/dist/src/table-client.d.ts +55 -0
  54. package/dist/src/table-client.js +198 -0
  55. package/dist/src/table.d.ts +37 -0
  56. package/dist/src/table.js +30 -0
  57. package/dist/src/types.d.ts +4 -0
  58. package/dist/src/types.js +2 -0
  59. package/dist/src/where.d.ts +24 -0
  60. package/dist/src/where.js +2 -0
  61. package/package.json +3 -3
@@ -0,0 +1,2 @@
1
+ import type { AnyTable } from "./table.js";
2
+ export declare function assertReplicatedSchema(nodeCount: number, tables: Record<string, AnyTable>): void;
@@ -0,0 +1,16 @@
1
+ // src/assert-replicated-schema.ts
2
+ import { isReplicatedEngine } from "./ddl-shared.js";
3
+ // Без кластера в remote_servers DDL не разъезжается сам: таблицу создают на
4
+ // каждой ноде руками. Обычный MergeTree на нескольких нодах — это две
5
+ // независимые таблицы, которые тихо разойдутся по данным, поэтому при
6
+ // нескольких DDL-нодах принимаем только Replicated-движки.
7
+ export function assertReplicatedSchema(nodeCount, tables) {
8
+ if (nodeCount < 2)
9
+ return;
10
+ for (const name in tables) {
11
+ if (!isReplicatedEngine(tables[name])) {
12
+ throw new Error(`Table "${name}" must use a Replicated* engine (set \`replication\`) ` +
13
+ `when DDL targets ${nodeCount} nodes`);
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,4 @@
1
+ import { AnyTable } from "./table.js";
2
+ import { WhereInput } from "./where.js";
3
+ import { SQLBuildResult } from "./sql-shared.js";
4
+ export declare function buildCountQuery<T>(database: string, tableName: string, table: AnyTable, where?: WhereInput<T>): SQLBuildResult;
@@ -0,0 +1,13 @@
1
+ // src/build-count-query.ts
2
+ import { countExpr } from "./sql-shared.js";
3
+ import { buildFindManyQuery } from "./build-find-many-query.js";
4
+ export function buildCountQuery(database, tableName, table, where) {
5
+ const inner = buildFindManyQuery(database, tableName, table, { where });
6
+ // with no select arg the query starts with "SELECT * FROM ..." —
7
+ // swap the projection for count()
8
+ const from = inner.sql.indexOf(" FROM ");
9
+ return {
10
+ sql: `SELECT ${countExpr(table)} AS count` + inner.sql.slice(from),
11
+ params: inner.params,
12
+ };
13
+ }
@@ -0,0 +1,2 @@
1
+ import type { AnyDictionary, DictionaryCredentials } from "./dictionary.js";
2
+ export declare function buildCreateDictionarySQL(database: string, name: string, dict: AnyDictionary, credentials?: DictionaryCredentials, mode?: "create" | "replace"): string;
@@ -0,0 +1,51 @@
1
+ // src/build-create-dictionary-query.ts
2
+ import { escapeId } from "./escape-id.js";
3
+ import { quoteString } from "./ddl-shared.js";
4
+ export function buildCreateDictionarySQL(database, name, dict, credentials,
5
+ // $migrate пересоздаёт словарь: IF NOT EXISTS оставил бы старое
6
+ // определение (источник, учётку, атрибуты) навсегда
7
+ mode = "create") {
8
+ const columns = [
9
+ ...Object.entries(dict.key),
10
+ ...Object.entries(dict.attributes),
11
+ ]
12
+ .map(([colName, col]) => `${escapeId(colName)} ${col.clickhouseType}`)
13
+ .join(",\n");
14
+ const keys = Object.keys(dict.key).map(escapeId).join(", ");
15
+ const auth = dict.source.user
16
+ ? { user: dict.source.user, password: dict.source.password }
17
+ : credentials;
18
+ const authSQL = auth
19
+ ? `USER ${quoteString(auth.user)} PASSWORD ${quoteString(auth.password ?? "")} `
20
+ : "";
21
+ // источник — тот же ClickHouse: запрос целиком, либо таблица с фильтром
22
+ const source = "query" in dict.source
23
+ ? `CLICKHOUSE(${authSQL}QUERY ${quoteString(dict.source.query)})`
24
+ : `CLICKHOUSE(${authSQL}DB ${quoteString(dict.source.database ?? database)} ` +
25
+ `TABLE ${quoteString(dict.source.table)}` +
26
+ (dict.source.where
27
+ ? ` WHERE ${quoteString(dict.source.where)}`
28
+ : "") +
29
+ ")";
30
+ const lifetime = typeof dict.lifetime === "number"
31
+ ? `LIFETIME(${dict.lifetime})`
32
+ : `LIFETIME(MIN ${dict.lifetime.min} MAX ${dict.lifetime.max})`;
33
+ const clauses = [
34
+ `PRIMARY KEY ${keys}`,
35
+ `SOURCE(${source})`,
36
+ `LAYOUT(${dict.layout.toUpperCase()}())`,
37
+ lifetime,
38
+ ];
39
+ if (dict.comment)
40
+ clauses.push(`COMMENT ${quoteString(dict.comment)}`);
41
+ const verb = mode === "replace"
42
+ ? "CREATE OR REPLACE DICTIONARY"
43
+ : "CREATE DICTIONARY IF NOT EXISTS";
44
+ return `
45
+ ${verb} ${escapeId(database)}.${escapeId(name)}
46
+ (
47
+ ${columns}
48
+ )
49
+ ${clauses.join("\n ")}
50
+ `.trim();
51
+ }
@@ -0,0 +1,4 @@
1
+ import { AnyTable } from "./table.js";
2
+ import { WhereInput } from "./where.js";
3
+ import { SQLBuildResult } from "./sql-shared.js";
4
+ export declare function buildDeleteQuery<T>(database: string, tableName: string, table: AnyTable, where?: WhereInput<T>): SQLBuildResult;
@@ -0,0 +1,11 @@
1
+ // src/build-delete-query.ts
2
+ import { createBuildContext, escapeId } from "./sql-shared.js";
3
+ export function buildDeleteQuery(database, tableName, table, where) {
4
+ const ctx = createBuildContext(tableName, table.columns);
5
+ const whereSQL = where ? ctx.whereSQL(where) : "";
6
+ return {
7
+ sql: `ALTER TABLE ${escapeId(database)}.${escapeId(tableName)} ` +
8
+ `DELETE WHERE ${whereSQL || "1"}`,
9
+ params: ctx.params,
10
+ };
11
+ }
@@ -0,0 +1,3 @@
1
+ import { AnyTable } from "./table.js";
2
+ import { FindManyArgs, SQLBuildResult } from "./sql-shared.js";
3
+ export declare function buildFindManyQuery<T>(database: string, tableName: string, table: AnyTable, args?: FindManyArgs<T>): SQLBuildResult;
@@ -0,0 +1,58 @@
1
+ // src/build-find-many-query.ts
2
+ import { checkPagination, createBuildContext, NO_LIMIT, tableRef, } from "./sql-shared.js";
3
+ export function buildFindManyQuery(database, tableName, table, args = {}) {
4
+ const ctx = createBuildContext(tableName, table.columns);
5
+ // SELECT
6
+ let selectSQL = "*";
7
+ if (args.select) {
8
+ const select = args.select;
9
+ const keys = Object.keys(select).filter((k) => select[k] === true);
10
+ if (keys.length) {
11
+ selectSQL = keys.map(ctx.colRef).join(", ");
12
+ }
13
+ }
14
+ const clauses = [
15
+ `SELECT ${selectSQL} FROM ${tableRef(database, tableName, table, args.final)}`,
16
+ ];
17
+ // WHERE
18
+ if (args.where) {
19
+ const where = ctx.whereSQL(args.where);
20
+ if (where)
21
+ clauses.push(`WHERE ${where}`);
22
+ }
23
+ // ORDER BY
24
+ if (args.orderBy) {
25
+ const list = Array.isArray(args.orderBy)
26
+ ? args.orderBy
27
+ : [args.orderBy];
28
+ const orders = [];
29
+ for (const entry of list) {
30
+ for (const key in entry) {
31
+ const dir = entry[key];
32
+ if (dir === undefined)
33
+ continue;
34
+ if (dir !== "asc" && dir !== "desc") {
35
+ throw new Error(`Invalid sort direction "${dir}" for column "${key}"`);
36
+ }
37
+ orders.push(`${ctx.colRef(key)} ${dir.toUpperCase()}`);
38
+ }
39
+ }
40
+ if (orders.length)
41
+ clauses.push(`ORDER BY ${orders.join(", ")}`);
42
+ }
43
+ // LIMIT / OFFSET
44
+ if (args.take !== undefined)
45
+ checkPagination("take", args.take);
46
+ if (args.skip !== undefined)
47
+ checkPagination("skip", args.skip);
48
+ if (args.take !== undefined && args.skip !== undefined) {
49
+ clauses.push(`LIMIT ${args.take} OFFSET ${args.skip}`);
50
+ }
51
+ else if (args.take !== undefined) {
52
+ clauses.push(`LIMIT ${args.take}`);
53
+ }
54
+ else if (args.skip !== undefined) {
55
+ clauses.push(`LIMIT ${NO_LIMIT} OFFSET ${args.skip}`);
56
+ }
57
+ return { sql: clauses.join(" "), params: ctx.params };
58
+ }
@@ -0,0 +1,2 @@
1
+ import type { IndexDefinition } from "./table.js";
2
+ export declare function buildIndexSQL(index: IndexDefinition): string;
@@ -0,0 +1,11 @@
1
+ // src/build-index-sql.ts
2
+ import { escapeId } from "./escape-id.js";
3
+ import { renderKeyPart } from "./ddl-shared.js";
4
+ export function buildIndexSQL(index) {
5
+ const args = index.args?.length ? `(${index.args.join(", ")})` : "";
6
+ const granularity = index.granularity !== undefined
7
+ ? ` GRANULARITY ${index.granularity}`
8
+ : "";
9
+ return (`INDEX ${escapeId(index.name)} ${renderKeyPart(index.expr)} ` +
10
+ `TYPE ${index.type}${args}${granularity}`);
11
+ }
@@ -0,0 +1,4 @@
1
+ import { AnyTable } from "./table.js";
2
+ import { WhereInput } from "./where.js";
3
+ import { SQLBuildResult } from "./sql-shared.js";
4
+ export declare function buildLightweightDeleteQuery<T>(database: string, tableName: string, table: AnyTable, where?: WhereInput<T>): SQLBuildResult;
@@ -0,0 +1,11 @@
1
+ // src/build-lightweight-delete-query.ts
2
+ import { createBuildContext, escapeId } from "./sql-shared.js";
3
+ export function buildLightweightDeleteQuery(database, tableName, table, where) {
4
+ const ctx = createBuildContext(tableName, table.columns);
5
+ const whereSQL = where ? ctx.whereSQL(where) : "";
6
+ return {
7
+ sql: `DELETE FROM ${escapeId(database)}.${escapeId(tableName)} ` +
8
+ `WHERE ${whereSQL || "1"}`,
9
+ params: ctx.params,
10
+ };
11
+ }
@@ -0,0 +1,4 @@
1
+ import { AnyTable } from "./table.js";
2
+ import { WhereInput } from "./where.js";
3
+ import { SQLBuildResult } from "./sql-shared.js";
4
+ export declare function buildUpdateQuery<T>(database: string, tableName: string, table: AnyTable, where: WhereInput<T> | undefined, data: Record<string, unknown>): SQLBuildResult;
@@ -0,0 +1,13 @@
1
+ // src/build-update-query.ts
2
+ import { createBuildContext, escapeId, validateUpdateData, } from "./sql-shared.js";
3
+ export function buildUpdateQuery(database, tableName, table, where, data) {
4
+ const keys = validateUpdateData(table, tableName, data);
5
+ const ctx = createBuildContext(tableName, table.columns);
6
+ const sets = keys.map((k) => `${ctx.colRef(k)} = ${ctx.addParam(data[k], ctx.baseType(k))}`);
7
+ const whereSQL = where ? ctx.whereSQL(where) : "";
8
+ return {
9
+ sql: `ALTER TABLE ${escapeId(database)}.${escapeId(tableName)} ` +
10
+ `UPDATE ${sets.join(", ")} WHERE ${whereSQL || "1"}`,
11
+ params: ctx.params,
12
+ };
13
+ }
@@ -0,0 +1,36 @@
1
+ import { type ClickHouseClient } from "@clickhouse/client";
2
+ import { TableClient } from "./table-client.js";
3
+ import { AnyTable, InferRow } from "./table.js";
4
+ import type { AnyDictionary } from "./dictionary.js";
5
+ import { MigrationPlan } from "./migrate.js";
6
+ import { type DdlStatement } from "./ensure-tables-on-nodes.js";
7
+ import { type QueryJsonResult, type QueryOptions } from "./query.js";
8
+ type ClientOptions = {
9
+ url: string;
10
+ username?: string;
11
+ password?: string;
12
+ database?: string;
13
+ autoCreateDatabase?: boolean;
14
+ ddlNodes?: string[];
15
+ requestTimeoutMs?: number;
16
+ };
17
+ export type DdlPlan = MigrationPlan & {
18
+ settings: DdlStatement[];
19
+ executed: DdlStatement[];
20
+ };
21
+ export type MigrateOptions = {
22
+ dryRun?: boolean;
23
+ };
24
+ export declare function createClient<TSchema extends {
25
+ database?: string;
26
+ tables: Record<string, AnyTable>;
27
+ dictionaries?: Record<string, AnyDictionary>;
28
+ }>(schema: TSchema, options: ClientOptions): { [K in keyof TSchema["tables"]]: TableClient<InferRow<TSchema["tables"][K]>>; } & {
29
+ $init(): Promise<DdlStatement[]>;
30
+ $migrate(opts?: MigrateOptions): Promise<DdlPlan>;
31
+ $query<TRow>(sql: string, opts?: QueryOptions): Promise<TRow[]>;
32
+ $queryJson<TRow, TTotals = TRow>(sql: string, opts?: QueryOptions): Promise<QueryJsonResult<TRow, TTotals>>;
33
+ $command(sql: string, opts?: Omit<QueryOptions, "columns">): Promise<void>;
34
+ $close(): Promise<void>;
35
+ };
36
+ export type { ClickHouseClient };
@@ -0,0 +1,139 @@
1
+ import { createClient as createCHClient, } from "@clickhouse/client";
2
+ import { TableClient } from "./table-client.js";
3
+ import { escapeId } from "./escape-id.js";
4
+ import { buildMigrationPlan, buildSettingsPlan, parseEngineSettings, } from "./migrate.js";
5
+ import { assertReplicatedSchema } from "./assert-replicated-schema.js";
6
+ import { ensureTablesOnNodes, } from "./ensure-tables-on-nodes.js";
7
+ import { ensureDictionariesOnNodes } from "./ensure-dictionaries-on-nodes.js";
8
+ import { MigrationError } from "./migration-error.js";
9
+ import { runCommand, runQuery, runQueryJson, } from "./query.js";
10
+ export function createClient(schema, options) {
11
+ const database = options.database ?? schema.database ?? "default";
12
+ const dictionaries = schema.dictionaries ?? {};
13
+ const connect = (url, withDatabase) => createCHClient({
14
+ url,
15
+ username: options.username,
16
+ password: options.password,
17
+ ...(withDatabase ? { database } : {}),
18
+ ...(options.requestTimeoutMs
19
+ ? { request_timeout: options.requestTimeoutMs }
20
+ : {}),
21
+ });
22
+ const ch = connect(options.url, true);
23
+ // клиенты DDL-нод создаются один раз; основная нода переиспользует `ch`
24
+ const ddlUrls = options.ddlNodes?.length ? options.ddlNodes : [options.url];
25
+ const nodes = ddlUrls.map((url) => ({
26
+ url,
27
+ client: url === options.url ? ch : connect(url, true),
28
+ }));
29
+ // источник истины схемы: `url` может быть балансировщиком или нодой вне
30
+ // ddlNodes, и план с него разошёлся бы с тем, куда пишется DDL
31
+ const primary = nodes[0];
32
+ const db = {};
33
+ for (const tableName in schema.tables) {
34
+ db[tableName] = new TableClient(database, tableName, schema.tables[tableName], ch);
35
+ }
36
+ const ensureDatabase = async () => {
37
+ if (!options.autoCreateDatabase)
38
+ return;
39
+ for (const node of nodes) {
40
+ const rootClient = connect(node.url, false);
41
+ try {
42
+ await rootClient.command({
43
+ query: `CREATE DATABASE IF NOT EXISTS ${escapeId(database)}`,
44
+ });
45
+ }
46
+ finally {
47
+ await rootClient.close();
48
+ }
49
+ }
50
+ };
51
+ const ensureEverywhere = async (dictionaryMode) => [
52
+ ...(await ensureTablesOnNodes(nodes, database, schema.tables)),
53
+ ...(await ensureDictionariesOnNodes(nodes, database, dictionaries, options.username
54
+ ? { user: options.username, password: options.password }
55
+ : undefined, dictionaryMode)),
56
+ ];
57
+ const readTableSettings = async (client) => {
58
+ const result = await client.query({
59
+ query: `SELECT name, engine_full FROM system.tables WHERE database = {db:String}`,
60
+ query_params: { db: database },
61
+ format: "JSONEachRow",
62
+ });
63
+ const out = {};
64
+ for (const row of await result.json()) {
65
+ out[row.name] = parseEngineSettings(row.engine_full);
66
+ }
67
+ return out;
68
+ };
69
+ db.$init = async () => {
70
+ assertReplicatedSchema(nodes.length, schema.tables);
71
+ await ensureDatabase();
72
+ return ensureEverywhere("create");
73
+ };
74
+ db.$migrate = async (opts) => {
75
+ assertReplicatedSchema(nodes.length, schema.tables);
76
+ if (!opts?.dryRun)
77
+ await ensureDatabase();
78
+ const result = await primary.client.query({
79
+ query: `
80
+ SELECT table, name, type
81
+ FROM system.columns
82
+ WHERE database = {db:String}
83
+ ORDER BY table, position
84
+ `,
85
+ query_params: { db: database },
86
+ format: "JSONEachRow",
87
+ });
88
+ const existing = await result.json();
89
+ const plan = buildMigrationPlan(database, schema.tables, existing);
90
+ const settings = [];
91
+ for (const node of nodes) {
92
+ for (const sql of buildSettingsPlan(database, schema.tables, await readTableSettings(node.client))) {
93
+ settings.push({ node: node.url, sql });
94
+ }
95
+ }
96
+ // dryRun: только план — ни CREATE DATABASE, ни DDL на нодах
97
+ if (opts?.dryRun)
98
+ return { ...plan, settings, executed: [] };
99
+ // план (CREATE недостающих, ALTER ADD/MODIFY) выполняется на первой
100
+ // ноде: реплицируемый ALTER сам разъезжается по репликам, а недостающие
101
+ // на репликах таблицы докладывает ensureTablesOnNodes копией SHOW CREATE.
102
+ // Сбой посередине — MigrationError со списком уже выполненного
103
+ const executed = [];
104
+ const run = async (node, sql) => {
105
+ try {
106
+ await node.client.command({ query: sql });
107
+ }
108
+ catch (cause) {
109
+ throw new MigrationError({
110
+ executed,
111
+ failed: { node: node.url, sql },
112
+ cause,
113
+ });
114
+ }
115
+ executed.push({ node: node.url, sql });
116
+ };
117
+ for (const statement of plan.statements)
118
+ await run(primary, statement);
119
+ try {
120
+ executed.push(...(await ensureEverywhere("replace")));
121
+ }
122
+ catch (cause) {
123
+ throw new MigrationError({ executed, cause });
124
+ }
125
+ for (const statement of settings) {
126
+ const node = nodes.find((n) => n.url === statement.node);
127
+ await run(node, statement.sql);
128
+ }
129
+ return { ...plan, settings, executed };
130
+ };
131
+ db.$query = (sql, opts) => runQuery(ch, sql, opts);
132
+ db.$queryJson = (sql, opts) => runQueryJson(ch, sql, opts);
133
+ db.$command = (sql, opts) => runCommand(ch, sql, opts);
134
+ db.$close = async () => {
135
+ for (const node of nodes)
136
+ await node.client.close();
137
+ };
138
+ return db;
139
+ }
@@ -0,0 +1,46 @@
1
+ export type ColumnDefault = {
2
+ kind: "DEFAULT" | "MATERIALIZED" | "ALIAS";
3
+ expr: string;
4
+ };
5
+ export type Column<T> = {
6
+ clickhouseType: string;
7
+ nullable: boolean;
8
+ default?: ColumnDefault;
9
+ codec?: string;
10
+ comment?: string;
11
+ __type?: T;
12
+ };
13
+ type NullableColumn<C extends Column<unknown>> = C extends Column<infer T> ? Column<T | null> : never;
14
+ type ColumnType<C extends Column<unknown>> = C extends Column<infer T> ? T : never;
15
+ export declare const t: {
16
+ string: () => Column<string>;
17
+ uint8: () => Column<number>;
18
+ uint16: () => Column<number>;
19
+ uint32: () => Column<number>;
20
+ uint64: () => Column<bigint>;
21
+ int8: () => Column<number>;
22
+ int16: () => Column<number>;
23
+ int32: () => Column<number>;
24
+ int64: () => Column<bigint>;
25
+ float32: () => Column<number>;
26
+ float64: () => Column<number>;
27
+ decimal: (precision: number, scale: number) => Column<string>;
28
+ boolean: () => Column<boolean>;
29
+ date: () => Column<Date>;
30
+ date32: () => Column<Date>;
31
+ datetime: () => Column<Date>;
32
+ datetime64: (precision?: number, timezone?: string) => Column<Date>;
33
+ ipv4: () => Column<string>;
34
+ ipv6: () => Column<string>;
35
+ enum8<TValues extends Record<string, number>>(values: TValues): Column<keyof TValues & string>;
36
+ lowCardinality<C extends Column<unknown>>(col: C): C;
37
+ map<K extends Column<unknown>, V extends Column<unknown>>(key: K, value: V): Column<Record<string, ColumnType<V>>>;
38
+ nullable<C extends Column<unknown>>(col: C): NullableColumn<C>;
39
+ array<C extends Column<unknown>>(col: C): Column<ColumnType<C>[]>;
40
+ default<C extends Column<unknown>>(col: C, expr: string): C;
41
+ materialized<C extends Column<unknown>>(col: C, expr: string): C;
42
+ alias<C extends Column<unknown>>(col: C, expr: string): C;
43
+ codec<C extends Column<unknown>>(col: C, codec: string): C;
44
+ comment<C extends Column<unknown>>(col: C, comment: string): C;
45
+ };
46
+ export {};
@@ -0,0 +1,79 @@
1
+ // src/column.ts
2
+ function makeColumn(clickhouseType, nullable = false) {
3
+ return {
4
+ clickhouseType,
5
+ nullable,
6
+ };
7
+ }
8
+ export const t = {
9
+ string: () => makeColumn("String"),
10
+ uint8: () => makeColumn("UInt8"),
11
+ uint16: () => makeColumn("UInt16"),
12
+ uint32: () => makeColumn("UInt32"),
13
+ // 64-битные целые не помещаются в number без потерь — только bigint
14
+ uint64: () => makeColumn("UInt64"),
15
+ int8: () => makeColumn("Int8"),
16
+ int16: () => makeColumn("Int16"),
17
+ int32: () => makeColumn("Int32"),
18
+ int64: () => makeColumn("Int64"),
19
+ float32: () => makeColumn("Float32"),
20
+ float64: () => makeColumn("Float64"),
21
+ // Decimal отдаётся строкой: number потерял бы точность, а bigint не знает
22
+ // про дробную часть — арифметику ведёт вызывающий код
23
+ decimal: (precision, scale) => makeColumn(`Decimal(${precision}, ${scale})`),
24
+ boolean: () => makeColumn("Bool"),
25
+ date: () => makeColumn("Date"),
26
+ date32: () => makeColumn("Date32"),
27
+ datetime: () => makeColumn("DateTime"),
28
+ datetime64: (precision = 3, timezone) => makeColumn(timezone
29
+ ? `DateTime64(${precision}, '${timezone}')`
30
+ : `DateTime64(${precision})`),
31
+ ipv4: () => makeColumn("IPv4"),
32
+ ipv6: () => makeColumn("IPv6"),
33
+ // Enum8: TS-тип — объединение имён значений, на проводе те же строки
34
+ enum8(values) {
35
+ const items = Object.entries(values)
36
+ .map(([name, code]) => `'${name.replace(/'/g, "''")}' = ${code}`)
37
+ .join(", ");
38
+ return makeColumn(`Enum8(${items})`);
39
+ },
40
+ // LowCardinality не меняет TS-тип и признак nullable: это только способ
41
+ // хранения словарём, который для строк с малым числом значений даёт
42
+ // и сжатие, и быстрый GROUP BY
43
+ lowCardinality(col) {
44
+ return {
45
+ ...col,
46
+ clickhouseType: `LowCardinality(${col.clickhouseType})`,
47
+ };
48
+ },
49
+ map(key, value) {
50
+ return makeColumn(`Map(${key.clickhouseType}, ${value.clickhouseType})`);
51
+ },
52
+ nullable(col) {
53
+ return {
54
+ ...col,
55
+ clickhouseType: `Nullable(${col.clickhouseType})`,
56
+ nullable: true,
57
+ };
58
+ },
59
+ array(col) {
60
+ return makeColumn(`Array(${col.clickhouseType})`);
61
+ },
62
+ // модификаторы возвращают копию: одна фабрика может описывать несколько
63
+ // колонок разных таблиц, общий объект нельзя мутировать
64
+ default(col, expr) {
65
+ return { ...col, default: { kind: "DEFAULT", expr } };
66
+ },
67
+ materialized(col, expr) {
68
+ return { ...col, default: { kind: "MATERIALIZED", expr } };
69
+ },
70
+ alias(col, expr) {
71
+ return { ...col, default: { kind: "ALIAS", expr } };
72
+ },
73
+ codec(col, codec) {
74
+ return { ...col, codec };
75
+ },
76
+ comment(col, comment) {
77
+ return { ...col, comment };
78
+ },
79
+ };
@@ -0,0 +1,13 @@
1
+ import type { EngineSpec } from "./engine.js";
2
+ import type { Expr, KeyPart } from "./expr.js";
3
+ import type { AnyTable } from "./table.js";
4
+ export declare function isExpr(part: KeyPart): part is Expr;
5
+ export declare function renderKeyPart(part: KeyPart): string;
6
+ export declare function renderKeyList(parts: KeyPart[]): string;
7
+ export declare function keyColumns(parts: KeyPart[]): string[];
8
+ export declare function quoteString(value: string): string;
9
+ export declare function engineFamily(engine: string | EngineSpec): string;
10
+ export declare function isReplicatedEngine(table: AnyTable): boolean;
11
+ export declare function isReplacingEngine(engine: string | EngineSpec): boolean;
12
+ export declare function collapsingOf(engine: string | EngineSpec): EngineSpec["collapsing"] | undefined;
13
+ export declare function renderEngine(table: AnyTable): string;
@@ -0,0 +1,54 @@
1
+ // src/ddl-shared.ts
2
+ // Общие хелперы рендера DDL: разбор движка и ключей, экранирование.
3
+ import { escapeId } from "./escape-id.js";
4
+ export function isExpr(part) {
5
+ return typeof part === "object" && part !== null && "__expr" in part;
6
+ }
7
+ export function renderKeyPart(part) {
8
+ return isExpr(part) ? part.__expr : escapeId(part);
9
+ }
10
+ export function renderKeyList(parts) {
11
+ return `(${parts.map(renderKeyPart).join(", ")})`;
12
+ }
13
+ // имена колонок ключа без выражений — для проверок «ключ нельзя менять»
14
+ export function keyColumns(parts) {
15
+ return parts.filter((p) => typeof p === "string");
16
+ }
17
+ export function quoteString(value) {
18
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
19
+ }
20
+ export function engineFamily(engine) {
21
+ if (typeof engine !== "string")
22
+ return engine.family;
23
+ // у сырой строки семейство — имя до скобки, без префикса Replicated
24
+ return engine
25
+ .replace(/^Replicated/, "")
26
+ .replace(/\(.*$/s, "")
27
+ .trim();
28
+ }
29
+ export function isReplicatedEngine(table) {
30
+ if (typeof table.engine === "string") {
31
+ return table.engine.startsWith("Replicated");
32
+ }
33
+ return table.replication !== undefined;
34
+ }
35
+ export function isReplacingEngine(engine) {
36
+ return engineFamily(engine).includes("ReplacingMergeTree");
37
+ }
38
+ export function collapsingOf(engine) {
39
+ return typeof engine === "string" ? undefined : engine.collapsing;
40
+ }
41
+ export function renderEngine(table) {
42
+ if (typeof table.engine === "string")
43
+ return table.engine;
44
+ const { family, args } = table.engine;
45
+ if (!table.replication) {
46
+ return args.length ? `${family}(${args.join(", ")})` : family;
47
+ }
48
+ const replicated = [
49
+ quoteString(table.replication.path),
50
+ quoteString(table.replication.replica),
51
+ ...args,
52
+ ];
53
+ return `Replicated${family}(${replicated.join(", ")})`;
54
+ }
@@ -0,0 +1,5 @@
1
+ import { AnyTable } from "./table.js";
2
+ import type { Column } from "./column.js";
3
+ export declare function renderSettings(settings: Record<string, string | number>): string;
4
+ export declare function buildColumnSQL(name: string, col: Column<unknown>): string;
5
+ export declare function buildCreateTableSQL(database: string, tableName: string, table: AnyTable): string;