@quark-fw/clisma 0.1.26 → 0.1.27
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/dist/src/assert-replicated-schema.d.ts +2 -0
- package/dist/src/assert-replicated-schema.js +16 -0
- package/dist/src/build-count-query.d.ts +4 -0
- package/dist/src/build-count-query.js +13 -0
- package/dist/src/build-create-dictionary-query.d.ts +2 -0
- package/dist/src/build-create-dictionary-query.js +51 -0
- package/dist/src/build-delete-query.d.ts +4 -0
- package/dist/src/build-delete-query.js +11 -0
- package/dist/src/build-find-many-query.d.ts +3 -0
- package/dist/src/build-find-many-query.js +58 -0
- package/dist/src/build-index-sql.d.ts +2 -0
- package/dist/src/build-index-sql.js +11 -0
- package/dist/src/build-lightweight-delete-query.d.ts +4 -0
- package/dist/src/build-lightweight-delete-query.js +11 -0
- package/dist/src/build-update-query.d.ts +4 -0
- package/dist/src/build-update-query.js +13 -0
- package/dist/src/client.d.ts +36 -0
- package/dist/src/client.js +139 -0
- package/dist/src/column.d.ts +46 -0
- package/dist/src/column.js +79 -0
- package/dist/src/ddl-shared.d.ts +13 -0
- package/dist/src/ddl-shared.js +54 -0
- package/dist/src/ddl.d.ts +5 -0
- package/dist/src/ddl.js +52 -0
- package/dist/src/dictionary.d.ts +26 -0
- package/dist/src/dictionary.js +7 -0
- package/dist/src/engine.d.ts +16 -0
- package/dist/src/engine.js +26 -0
- package/dist/src/ensure-dictionaries-on-nodes.d.ts +3 -0
- package/dist/src/ensure-dictionaries-on-nodes.js +15 -0
- package/dist/src/ensure-tables-on-nodes.d.ts +11 -0
- package/dist/src/ensure-tables-on-nodes.js +39 -0
- package/dist/src/escape-id.d.ts +1 -0
- package/dist/src/escape-id.js +4 -0
- package/dist/src/expr.d.ts +5 -0
- package/dist/src/expr.js +4 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +15 -0
- package/dist/src/migrate.d.ts +14 -0
- package/dist/src/migrate.js +91 -0
- package/dist/src/migration-error.d.ts +10 -0
- package/dist/src/migration-error.js +24 -0
- package/dist/src/query.d.ts +26 -0
- package/dist/src/query.js +60 -0
- package/dist/src/rows.d.ts +2 -0
- package/dist/src/rows.js +45 -0
- package/dist/src/schema.d.ts +8 -0
- package/dist/src/schema.js +4 -0
- package/dist/src/sql-shared.d.ts +33 -0
- package/dist/src/sql-shared.js +171 -0
- package/dist/src/sql.d.ts +6 -0
- package/dist/src/sql.js +9 -0
- package/dist/src/table-client.d.ts +55 -0
- package/dist/src/table-client.js +198 -0
- package/dist/src/table.d.ts +37 -0
- package/dist/src/table.js +30 -0
- package/dist/src/types.d.ts +4 -0
- package/dist/src/types.js +2 -0
- package/dist/src/where.d.ts +24 -0
- package/dist/src/where.js +2 -0
- package/package.json +3 -3
|
@@ -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,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,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,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,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,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;
|