@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.
- 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
package/dist/src/ddl.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/ddl.ts
|
|
2
|
+
import { escapeId } from "./escape-id.js";
|
|
3
|
+
import { buildIndexSQL } from "./build-index-sql.js";
|
|
4
|
+
import { quoteString, renderEngine, renderKeyList, renderKeyPart, } from "./ddl-shared.js";
|
|
5
|
+
// список для `SETTINGS` / `MODIFY SETTING`: числа как есть, строки в кавычках
|
|
6
|
+
export function renderSettings(settings) {
|
|
7
|
+
return Object.entries(settings)
|
|
8
|
+
.map(([key, value]) => typeof value === "number"
|
|
9
|
+
? `${key} = ${value}`
|
|
10
|
+
: `${key} = ${quoteString(value)}`)
|
|
11
|
+
.join(", ");
|
|
12
|
+
}
|
|
13
|
+
export function buildColumnSQL(name, col) {
|
|
14
|
+
const parts = [escapeId(name), col.clickhouseType];
|
|
15
|
+
if (col.default)
|
|
16
|
+
parts.push(`${col.default.kind} ${col.default.expr}`);
|
|
17
|
+
if (col.codec)
|
|
18
|
+
parts.push(`CODEC(${col.codec})`);
|
|
19
|
+
if (col.comment)
|
|
20
|
+
parts.push(`COMMENT ${quoteString(col.comment)}`);
|
|
21
|
+
return parts.join(" ");
|
|
22
|
+
}
|
|
23
|
+
export function buildCreateTableSQL(database, tableName, table) {
|
|
24
|
+
const body = [
|
|
25
|
+
...Object.entries(table.columns).map(([name, col]) => buildColumnSQL(name, col)),
|
|
26
|
+
...(table.indexes ?? []).map(buildIndexSQL),
|
|
27
|
+
].join(",\n");
|
|
28
|
+
const clauses = [`ENGINE = ${renderEngine(table)}`];
|
|
29
|
+
if (table.partitionBy) {
|
|
30
|
+
clauses.push(`PARTITION BY ${renderKeyPart(table.partitionBy)}`);
|
|
31
|
+
}
|
|
32
|
+
if (table.primaryKey) {
|
|
33
|
+
clauses.push(`PRIMARY KEY ${renderKeyList(table.primaryKey)}`);
|
|
34
|
+
}
|
|
35
|
+
clauses.push(`ORDER BY ${renderKeyList(table.orderBy)}`);
|
|
36
|
+
if (table.sampleBy)
|
|
37
|
+
clauses.push(`SAMPLE BY ${renderKeyPart(table.sampleBy)}`);
|
|
38
|
+
if (table.ttl)
|
|
39
|
+
clauses.push(`TTL ${table.ttl}`);
|
|
40
|
+
if (table.settings && Object.keys(table.settings).length) {
|
|
41
|
+
clauses.push(`SETTINGS ${renderSettings(table.settings)}`);
|
|
42
|
+
}
|
|
43
|
+
if (table.comment)
|
|
44
|
+
clauses.push(`COMMENT ${quoteString(table.comment)}`);
|
|
45
|
+
return `
|
|
46
|
+
CREATE TABLE IF NOT EXISTS ${escapeId(database)}.${escapeId(tableName)}
|
|
47
|
+
(
|
|
48
|
+
${body}
|
|
49
|
+
)
|
|
50
|
+
${clauses.join("\n ")}
|
|
51
|
+
`.trim();
|
|
52
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Column } from "./column.js";
|
|
2
|
+
export type DictionaryCredentials = {
|
|
3
|
+
user: string;
|
|
4
|
+
password?: string;
|
|
5
|
+
};
|
|
6
|
+
export type DictionarySource = ({
|
|
7
|
+
query: string;
|
|
8
|
+
} | {
|
|
9
|
+
table: string;
|
|
10
|
+
database?: string;
|
|
11
|
+
where?: string;
|
|
12
|
+
}) & Partial<DictionaryCredentials>;
|
|
13
|
+
export type DictionaryLayout = "flat" | "hashed" | "sparse_hashed" | "hashed_array" | "complex_key_hashed";
|
|
14
|
+
export type DictionaryDefinition<TKey extends Record<string, Column<unknown>>, TAttributes extends Record<string, Column<unknown>>> = {
|
|
15
|
+
key: TKey;
|
|
16
|
+
attributes: TAttributes;
|
|
17
|
+
source: DictionarySource;
|
|
18
|
+
layout: DictionaryLayout;
|
|
19
|
+
lifetime: number | {
|
|
20
|
+
min: number;
|
|
21
|
+
max: number;
|
|
22
|
+
};
|
|
23
|
+
comment?: string;
|
|
24
|
+
};
|
|
25
|
+
export type AnyDictionary = DictionaryDefinition<Record<string, Column<unknown>>, Record<string, Column<unknown>>>;
|
|
26
|
+
export declare function dictionary<TKey extends Record<string, Column<unknown>>, TAttributes extends Record<string, Column<unknown>>>(def: DictionaryDefinition<TKey, TAttributes>): DictionaryDefinition<TKey, TAttributes>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type EngineSpec = {
|
|
2
|
+
family: "MergeTree" | "ReplacingMergeTree" | "CollapsingMergeTree" | "VersionedCollapsingMergeTree" | "SummingMergeTree" | "AggregatingMergeTree";
|
|
3
|
+
args: string[];
|
|
4
|
+
collapsing?: {
|
|
5
|
+
sign: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export declare const engine: {
|
|
10
|
+
mergeTree: () => EngineSpec;
|
|
11
|
+
replacingMergeTree: (version?: string) => EngineSpec;
|
|
12
|
+
collapsingMergeTree: (sign: string) => EngineSpec;
|
|
13
|
+
versionedCollapsingMergeTree: (sign: string, version: string) => EngineSpec;
|
|
14
|
+
summingMergeTree: (columns?: string[]) => EngineSpec;
|
|
15
|
+
aggregatingMergeTree: () => EngineSpec;
|
|
16
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/engine.ts
|
|
2
|
+
export const engine = {
|
|
3
|
+
mergeTree: () => ({ family: "MergeTree", args: [] }),
|
|
4
|
+
replacingMergeTree: (version) => ({
|
|
5
|
+
family: "ReplacingMergeTree",
|
|
6
|
+
args: version ? [version] : [],
|
|
7
|
+
}),
|
|
8
|
+
collapsingMergeTree: (sign) => ({
|
|
9
|
+
family: "CollapsingMergeTree",
|
|
10
|
+
args: [sign],
|
|
11
|
+
collapsing: { sign },
|
|
12
|
+
}),
|
|
13
|
+
versionedCollapsingMergeTree: (sign, version) => ({
|
|
14
|
+
family: "VersionedCollapsingMergeTree",
|
|
15
|
+
args: [sign, version],
|
|
16
|
+
collapsing: { sign, version },
|
|
17
|
+
}),
|
|
18
|
+
summingMergeTree: (columns = []) => ({
|
|
19
|
+
family: "SummingMergeTree",
|
|
20
|
+
args: columns.length ? [`(${columns.join(", ")})`] : [],
|
|
21
|
+
}),
|
|
22
|
+
aggregatingMergeTree: () => ({
|
|
23
|
+
family: "AggregatingMergeTree",
|
|
24
|
+
args: [],
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { AnyDictionary, DictionaryCredentials } from "./dictionary.js";
|
|
2
|
+
import type { DdlNode, DdlStatement } from "./ensure-tables-on-nodes.js";
|
|
3
|
+
export declare function ensureDictionariesOnNodes(nodes: DdlNode[], database: string, dictionaries: Record<string, AnyDictionary>, credentials?: DictionaryCredentials, mode?: "create" | "replace"): Promise<DdlStatement[]>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/ensure-dictionaries-on-nodes.ts
|
|
2
|
+
import { buildCreateDictionarySQL } from "./build-create-dictionary-query.js";
|
|
3
|
+
// Словарь не реплицируется — это локальный кэш каждой ноды над её же
|
|
4
|
+
// данными, поэтому он создаётся из схемы на каждой ноде отдельно.
|
|
5
|
+
export async function ensureDictionariesOnNodes(nodes, database, dictionaries, credentials, mode = "create") {
|
|
6
|
+
const executed = [];
|
|
7
|
+
for (const node of nodes) {
|
|
8
|
+
for (const name in dictionaries) {
|
|
9
|
+
const sql = buildCreateDictionarySQL(database, name, dictionaries[name], credentials, mode);
|
|
10
|
+
await node.client.command({ query: sql });
|
|
11
|
+
executed.push({ node: node.url, sql });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return executed;
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ClickHouseClient } from "@clickhouse/client";
|
|
2
|
+
import type { AnyTable } from "./table.js";
|
|
3
|
+
export type DdlNode = {
|
|
4
|
+
url: string;
|
|
5
|
+
client: ClickHouseClient;
|
|
6
|
+
};
|
|
7
|
+
export type DdlStatement = {
|
|
8
|
+
node: string;
|
|
9
|
+
sql: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function ensureTablesOnNodes(nodes: DdlNode[], database: string, tables: Record<string, AnyTable>): Promise<DdlStatement[]>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/ensure-tables-on-nodes.ts
|
|
2
|
+
import { buildCreateTableSQL } from "./ddl.js";
|
|
3
|
+
import { escapeId } from "./escape-id.js";
|
|
4
|
+
async function tableExists(client, database, tableName) {
|
|
5
|
+
const result = await client.query({
|
|
6
|
+
query: `EXISTS TABLE ${escapeId(database)}.${escapeId(tableName)}`,
|
|
7
|
+
format: "JSONEachRow",
|
|
8
|
+
});
|
|
9
|
+
const rows = await result.json();
|
|
10
|
+
return Number(rows[0]?.result) === 1;
|
|
11
|
+
}
|
|
12
|
+
// Первая нода — источник истины: на ней таблица создаётся из схемы, а на
|
|
13
|
+
// остальные копируется её SHOW CREATE TABLE. Копия, а не повторный рендер —
|
|
14
|
+
// потому что Keeper сверяет метаданные реплик побайтово (порядок колонок,
|
|
15
|
+
// дефолты), и любое расхождение рендера оставило бы реплику вне набора.
|
|
16
|
+
export async function ensureTablesOnNodes(nodes, database, tables) {
|
|
17
|
+
const [primary, ...secondaries] = nodes;
|
|
18
|
+
const executed = [];
|
|
19
|
+
for (const tableName in tables) {
|
|
20
|
+
const createSQL = buildCreateTableSQL(database, tableName, tables[tableName]);
|
|
21
|
+
await primary.client.command({ query: createSQL });
|
|
22
|
+
executed.push({ node: primary.url, sql: createSQL });
|
|
23
|
+
}
|
|
24
|
+
for (const node of secondaries) {
|
|
25
|
+
for (const tableName in tables) {
|
|
26
|
+
if (await tableExists(node.client, database, tableName))
|
|
27
|
+
continue;
|
|
28
|
+
const shown = await primary.client.query({
|
|
29
|
+
query: `SHOW CREATE TABLE ${escapeId(database)}.${escapeId(tableName)}`,
|
|
30
|
+
format: "JSONEachRow",
|
|
31
|
+
});
|
|
32
|
+
const [{ statement }] = await shown.json();
|
|
33
|
+
const copySQL = statement.replace(/^CREATE TABLE /, "CREATE TABLE IF NOT EXISTS ");
|
|
34
|
+
await node.client.command({ query: copySQL });
|
|
35
|
+
executed.push({ node: node.url, sql: copySQL });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return executed;
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function escapeId(name: string): string;
|
package/dist/src/expr.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from "./column.js";
|
|
2
|
+
export * from "./expr.js";
|
|
3
|
+
export * from "./engine.js";
|
|
4
|
+
export * from "./table.js";
|
|
5
|
+
export * from "./dictionary.js";
|
|
6
|
+
export * from "./schema.js";
|
|
7
|
+
export * from "./client.js";
|
|
8
|
+
export * from "./where.js";
|
|
9
|
+
export * from "./sql.js";
|
|
10
|
+
export * from "./table-client.js";
|
|
11
|
+
export * from "./rows.js";
|
|
12
|
+
export * from "./migrate.js";
|
|
13
|
+
export * from "./ddl.js";
|
|
14
|
+
export * from "./migration-error.js";
|
|
15
|
+
export * from "./build-create-dictionary-query.js";
|
|
16
|
+
export type { QueryOptions, QueryJsonResult, QuerySettings } from "./query.js";
|
|
17
|
+
export type { DdlNode, DdlStatement } from "./ensure-tables-on-nodes.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from "./column.js";
|
|
2
|
+
export * from "./expr.js";
|
|
3
|
+
export * from "./engine.js";
|
|
4
|
+
export * from "./table.js";
|
|
5
|
+
export * from "./dictionary.js";
|
|
6
|
+
export * from "./schema.js";
|
|
7
|
+
export * from "./client.js";
|
|
8
|
+
export * from "./where.js";
|
|
9
|
+
export * from "./sql.js";
|
|
10
|
+
export * from "./table-client.js";
|
|
11
|
+
export * from "./rows.js";
|
|
12
|
+
export * from "./migrate.js";
|
|
13
|
+
export * from "./ddl.js";
|
|
14
|
+
export * from "./migration-error.js";
|
|
15
|
+
export * from "./build-create-dictionary-query.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AnyTable } from "./table.js";
|
|
2
|
+
export type DbColumn = {
|
|
3
|
+
table: string;
|
|
4
|
+
name: string;
|
|
5
|
+
type: string;
|
|
6
|
+
};
|
|
7
|
+
export type DbTableSettings = Record<string, Record<string, string>>;
|
|
8
|
+
export type MigrationPlan = {
|
|
9
|
+
statements: string[];
|
|
10
|
+
warnings: string[];
|
|
11
|
+
};
|
|
12
|
+
export declare function parseEngineSettings(engineFull: string): Record<string, string>;
|
|
13
|
+
export declare function buildMigrationPlan(database: string, tables: Record<string, AnyTable>, existing: DbColumn[]): MigrationPlan;
|
|
14
|
+
export declare function buildSettingsPlan(database: string, tables: Record<string, AnyTable>, existingSettings: DbTableSettings): string[];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/migrate.ts
|
|
2
|
+
import { buildColumnSQL, buildCreateTableSQL, renderSettings } from "./ddl.js";
|
|
3
|
+
import { escapeId } from "./escape-id.js";
|
|
4
|
+
import { keyColumns } from "./ddl-shared.js";
|
|
5
|
+
// `SETTINGS a = 1, b = 'x'` из хвоста `system.tables.engine_full`
|
|
6
|
+
export function parseEngineSettings(engineFull) {
|
|
7
|
+
const out = {};
|
|
8
|
+
const at = engineFull.lastIndexOf(" SETTINGS ");
|
|
9
|
+
if (at === -1)
|
|
10
|
+
return out;
|
|
11
|
+
const re = /(\w+)\s*=\s*('(?:[^'\\]|\\.)*'|[^,]+)/g;
|
|
12
|
+
for (const m of engineFull.slice(at + " SETTINGS ".length).matchAll(re)) {
|
|
13
|
+
const raw = m[2].trim();
|
|
14
|
+
out[m[1]] = raw.startsWith("'")
|
|
15
|
+
? raw.slice(1, -1).replace(/\\(.)/g, "$1")
|
|
16
|
+
: raw;
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
export function buildMigrationPlan(database, tables, existing) {
|
|
21
|
+
const statements = [];
|
|
22
|
+
const warnings = [];
|
|
23
|
+
const dbTables = new Map();
|
|
24
|
+
for (const col of existing) {
|
|
25
|
+
let cols = dbTables.get(col.table);
|
|
26
|
+
if (!cols) {
|
|
27
|
+
cols = new Map();
|
|
28
|
+
dbTables.set(col.table, cols);
|
|
29
|
+
}
|
|
30
|
+
cols.set(col.name, col.type);
|
|
31
|
+
}
|
|
32
|
+
for (const tableName in tables) {
|
|
33
|
+
const def = tables[tableName];
|
|
34
|
+
const dbCols = dbTables.get(tableName);
|
|
35
|
+
if (!dbCols) {
|
|
36
|
+
statements.push(buildCreateTableSQL(database, tableName, def));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const tableRef = `${escapeId(database)}.${escapeId(tableName)}`;
|
|
40
|
+
const keys = keyColumns([...def.orderBy, ...(def.primaryKey ?? [])]);
|
|
41
|
+
for (const colName in def.columns) {
|
|
42
|
+
const col = def.columns[colName];
|
|
43
|
+
const wanted = col.clickhouseType;
|
|
44
|
+
const actual = dbCols.get(colName);
|
|
45
|
+
if (actual === undefined) {
|
|
46
|
+
statements.push(`ALTER TABLE ${tableRef} ADD COLUMN ${buildColumnSQL(colName, col)}`);
|
|
47
|
+
}
|
|
48
|
+
else if (actual !== wanted) {
|
|
49
|
+
// ключевую колонку сервер менять не даст; лучше внятная ошибка
|
|
50
|
+
// до выполнения плана, чем упавшая посередине миграция
|
|
51
|
+
if (keys.includes(colName)) {
|
|
52
|
+
throw new Error(`Column "${colName}" of ${database}.${tableName} is part of the ` +
|
|
53
|
+
`sorting key and cannot change type (${actual} → ${wanted}); ` +
|
|
54
|
+
`recreate the table instead`);
|
|
55
|
+
}
|
|
56
|
+
statements.push(`ALTER TABLE ${tableRef} MODIFY COLUMN ${buildColumnSQL(colName, col)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const dbColName of dbCols.keys()) {
|
|
60
|
+
if (!(dbColName in def.columns)) {
|
|
61
|
+
warnings.push(`Column "${dbColName}" exists in ${database}.${tableName} ` +
|
|
62
|
+
`but not in the schema — not dropped, remove it manually if unneeded`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { statements, warnings };
|
|
67
|
+
}
|
|
68
|
+
// Настройки таблицы — локальные метаданные каждой ноды: MODIFY SETTING не
|
|
69
|
+
// реплицируется (в отличие от ADD/MODIFY COLUMN), поэтому план настроек
|
|
70
|
+
// строится и выполняется на каждой ноде по её `system.tables`. Только
|
|
71
|
+
// объявленные в схеме настройки и только существующие на ноде таблицы —
|
|
72
|
+
// новые получат их из CREATE (или из SHOW CREATE копии). Правка метаданных,
|
|
73
|
+
// данные не переписываются.
|
|
74
|
+
export function buildSettingsPlan(database, tables, existingSettings) {
|
|
75
|
+
const statements = [];
|
|
76
|
+
for (const tableName in tables) {
|
|
77
|
+
const declared = tables[tableName].settings;
|
|
78
|
+
const actual = existingSettings[tableName];
|
|
79
|
+
if (!declared || !actual)
|
|
80
|
+
continue;
|
|
81
|
+
const changed = {};
|
|
82
|
+
for (const [key, value] of Object.entries(declared)) {
|
|
83
|
+
if (actual[key] !== String(value))
|
|
84
|
+
changed[key] = value;
|
|
85
|
+
}
|
|
86
|
+
if (Object.keys(changed).length) {
|
|
87
|
+
statements.push(`ALTER TABLE ${escapeId(database)}.${escapeId(tableName)} MODIFY SETTING ${renderSettings(changed)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return statements;
|
|
91
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DdlStatement } from "./ensure-tables-on-nodes.js";
|
|
2
|
+
export declare class MigrationError extends Error {
|
|
3
|
+
readonly executed: DdlStatement[];
|
|
4
|
+
readonly failed?: DdlStatement;
|
|
5
|
+
constructor(args: {
|
|
6
|
+
executed: DdlStatement[];
|
|
7
|
+
failed?: DdlStatement;
|
|
8
|
+
cause: unknown;
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/migration-error.ts
|
|
2
|
+
// DDL в ClickHouse не транзакционен: упавший посередине $migrate оставляет
|
|
3
|
+
// выполненную часть на нодах. Без списка выполненного вызывающий не знает,
|
|
4
|
+
// что уже применено, поэтому ошибка несёт его вместе с упавшим оператором.
|
|
5
|
+
export class MigrationError extends Error {
|
|
6
|
+
// успешно выполненные до сбоя операторы, в порядке выполнения
|
|
7
|
+
executed;
|
|
8
|
+
// упавший оператор; нет, если сбой внутри докладки таблиц/словарей на
|
|
9
|
+
// ноды — тогда их выполненная часть видна только по самим нодам
|
|
10
|
+
failed;
|
|
11
|
+
constructor(args) {
|
|
12
|
+
const reason = args.cause instanceof Error
|
|
13
|
+
? args.cause.message
|
|
14
|
+
: String(args.cause);
|
|
15
|
+
super(`$migrate failed after ${args.executed.length} statement(s)` +
|
|
16
|
+
(args.failed
|
|
17
|
+
? ` on ${args.failed.node}: ${args.failed.sql}`
|
|
18
|
+
: "") +
|
|
19
|
+
`\n${reason}`, { cause: args.cause });
|
|
20
|
+
this.name = "MigrationError";
|
|
21
|
+
this.executed = args.executed;
|
|
22
|
+
this.failed = args.failed;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ClickHouseClient } from "@clickhouse/client";
|
|
2
|
+
import type { AnyColumns } from "./table.js";
|
|
3
|
+
export type QuerySettings = Record<string, string | number | boolean>;
|
|
4
|
+
export type QueryOptions = {
|
|
5
|
+
params?: Record<string, unknown>;
|
|
6
|
+
settings?: QuerySettings;
|
|
7
|
+
columns?: AnyColumns;
|
|
8
|
+
};
|
|
9
|
+
export type QueryJsonResult<TRow, TTotals = TRow> = {
|
|
10
|
+
data: TRow[];
|
|
11
|
+
totals?: TTotals;
|
|
12
|
+
rows: number;
|
|
13
|
+
rows_before_limit_at_least?: number;
|
|
14
|
+
meta: {
|
|
15
|
+
name: string;
|
|
16
|
+
type: string;
|
|
17
|
+
}[];
|
|
18
|
+
statistics: {
|
|
19
|
+
elapsed: number;
|
|
20
|
+
rows_read: number;
|
|
21
|
+
bytes_read: number;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export declare function runQuery<TRow>(client: ClickHouseClient, sql: string, options?: QueryOptions): Promise<TRow[]>;
|
|
25
|
+
export declare function runQueryJson<TRow, TTotals = TRow>(client: ClickHouseClient, sql: string, options?: QueryOptions): Promise<QueryJsonResult<TRow, TTotals>>;
|
|
26
|
+
export declare function runCommand(client: ClickHouseClient, sql: string, options?: Omit<QueryOptions, "columns">): Promise<void>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/query.ts
|
|
2
|
+
// Сырые запросы поверх @clickhouse/client: агрегаты, GROUP BY и DDL не
|
|
3
|
+
// выражаются Prisma-синтаксисом, а второй клиент в приложении — дубль
|
|
4
|
+
// соединения и настроек.
|
|
5
|
+
import { deserializeRows } from "./rows.js";
|
|
6
|
+
// те же настройки, что у findMany: ISO-даты без таймзонной неоднозначности и
|
|
7
|
+
// 64-битные целые строками, чтобы BigInt() был без потерь
|
|
8
|
+
const WIRE_SETTINGS = {
|
|
9
|
+
date_time_output_format: "iso",
|
|
10
|
+
output_format_json_quote_64bit_integers: 1,
|
|
11
|
+
};
|
|
12
|
+
function serializeParams(params = {}) {
|
|
13
|
+
const out = {};
|
|
14
|
+
for (const key in params) {
|
|
15
|
+
const value = params[key];
|
|
16
|
+
out[key] =
|
|
17
|
+
value instanceof Date ? Math.floor(value.getTime() / 1000) : value;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
export async function runQuery(client, sql, options = {}) {
|
|
22
|
+
const result = await client.query({
|
|
23
|
+
query: sql,
|
|
24
|
+
query_params: serializeParams(options.params),
|
|
25
|
+
format: "JSONEachRow",
|
|
26
|
+
clickhouse_settings: { ...WIRE_SETTINGS, ...options.settings },
|
|
27
|
+
});
|
|
28
|
+
const rows = await result.json();
|
|
29
|
+
return options.columns
|
|
30
|
+
? deserializeRows(rows, options.columns)
|
|
31
|
+
: rows;
|
|
32
|
+
}
|
|
33
|
+
// FORMAT JSON нужен, когда важны totals (WITH TOTALS), точный
|
|
34
|
+
// rows_before_limit_at_least и статистика чтения — JSONEachRow их не отдаёт
|
|
35
|
+
export async function runQueryJson(client, sql, options = {}) {
|
|
36
|
+
const result = await client.query({
|
|
37
|
+
query: sql,
|
|
38
|
+
query_params: serializeParams(options.params),
|
|
39
|
+
format: "JSON",
|
|
40
|
+
clickhouse_settings: { ...WIRE_SETTINGS, ...options.settings },
|
|
41
|
+
});
|
|
42
|
+
// у FORMAT JSON клиент типизирует поля как необязательные; сервер отдаёт их всегда
|
|
43
|
+
const body = (await result.json());
|
|
44
|
+
if (!options.columns)
|
|
45
|
+
return body;
|
|
46
|
+
return {
|
|
47
|
+
...body,
|
|
48
|
+
data: deserializeRows(body.data, options.columns),
|
|
49
|
+
totals: body.totals
|
|
50
|
+
? deserializeRows([body.totals], options.columns)[0]
|
|
51
|
+
: undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export async function runCommand(client, sql, options = {}) {
|
|
55
|
+
await client.command({
|
|
56
|
+
query: sql,
|
|
57
|
+
query_params: serializeParams(options.params),
|
|
58
|
+
clickhouse_settings: options.settings,
|
|
59
|
+
});
|
|
60
|
+
}
|
package/dist/src/rows.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/rows.ts
|
|
2
|
+
const BIGINT_TYPES = new Set([
|
|
3
|
+
"UInt64",
|
|
4
|
+
"Int64",
|
|
5
|
+
"UInt128",
|
|
6
|
+
"Int128",
|
|
7
|
+
"UInt256",
|
|
8
|
+
"Int256",
|
|
9
|
+
]);
|
|
10
|
+
function convertValue(value, chType) {
|
|
11
|
+
if (value === null || value === undefined)
|
|
12
|
+
return value;
|
|
13
|
+
const nullable = /^Nullable\((.+)\)$/.exec(chType);
|
|
14
|
+
if (nullable)
|
|
15
|
+
return convertValue(value, nullable[1]);
|
|
16
|
+
// LowCardinality — способ хранения, на значение не влияет
|
|
17
|
+
const lowCardinality = /^LowCardinality\((.+)\)$/.exec(chType);
|
|
18
|
+
if (lowCardinality)
|
|
19
|
+
return convertValue(value, lowCardinality[1]);
|
|
20
|
+
const array = /^Array\((.+)\)$/.exec(chType);
|
|
21
|
+
if (array) {
|
|
22
|
+
return value.map((v) => convertValue(v, array[1]));
|
|
23
|
+
}
|
|
24
|
+
if (BIGINT_TYPES.has(chType))
|
|
25
|
+
return BigInt(value);
|
|
26
|
+
// Float может прийти строкой (inf/nan или quote_64bit_integers у Float64
|
|
27
|
+
// на старых серверах) — приводим к number; Decimal, Enum, IPv4/6 и Map
|
|
28
|
+
// приходят уже в нужном виде
|
|
29
|
+
if (chType.startsWith("Float"))
|
|
30
|
+
return Number(value);
|
|
31
|
+
if (chType.startsWith("DateTime") || chType.startsWith("Date")) {
|
|
32
|
+
return new Date(value);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
export function deserializeRows(rows, columns) {
|
|
37
|
+
return rows.map((row) => {
|
|
38
|
+
const out = {};
|
|
39
|
+
for (const k in row) {
|
|
40
|
+
const col = columns[k];
|
|
41
|
+
out[k] = col ? convertValue(row[k], col.clickhouseType) : row[k];
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AnyTable } from "./table.js";
|
|
2
|
+
import type { AnyDictionary } from "./dictionary.js";
|
|
3
|
+
export type ClismaSchema<TTables extends Record<string, AnyTable>, TDictionaries extends Record<string, AnyDictionary> = Record<string, never>> = {
|
|
4
|
+
database?: string;
|
|
5
|
+
tables: TTables;
|
|
6
|
+
dictionaries?: TDictionaries;
|
|
7
|
+
};
|
|
8
|
+
export declare function createClisma<TTables extends Record<string, AnyTable>, TDictionaries extends Record<string, AnyDictionary> = Record<string, never>>(schema: ClismaSchema<TTables, TDictionaries>): ClismaSchema<TTables, TDictionaries>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AnyColumns, AnyTable } from "./table.js";
|
|
2
|
+
import { WhereInput } from "./where.js";
|
|
3
|
+
export type SQLBuildResult = {
|
|
4
|
+
sql: string;
|
|
5
|
+
params: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
export type OrderByInput<T> = {
|
|
8
|
+
[K in keyof T]?: "asc" | "desc";
|
|
9
|
+
};
|
|
10
|
+
export type FindManyArgs<T> = {
|
|
11
|
+
where?: WhereInput<T>;
|
|
12
|
+
select?: {
|
|
13
|
+
[K in keyof T]?: boolean;
|
|
14
|
+
};
|
|
15
|
+
orderBy?: OrderByInput<T> | OrderByInput<T>[];
|
|
16
|
+
take?: number;
|
|
17
|
+
skip?: number;
|
|
18
|
+
final?: boolean;
|
|
19
|
+
settings?: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
export { escapeId } from "./escape-id.js";
|
|
22
|
+
export declare const NO_LIMIT = "18446744073709551615";
|
|
23
|
+
export declare function createBuildContext(tableName: string, columns: AnyColumns): {
|
|
24
|
+
params: Record<string, unknown>;
|
|
25
|
+
colRef: (key: string) => string;
|
|
26
|
+
baseType: (key: string) => string;
|
|
27
|
+
addParam: (value: unknown, type: string) => string;
|
|
28
|
+
whereSQL: (where: Record<string, unknown>) => string;
|
|
29
|
+
};
|
|
30
|
+
export declare function tableRef(database: string, tableName: string, table: AnyTable, final?: boolean): string;
|
|
31
|
+
export declare function countExpr(table: AnyTable): string;
|
|
32
|
+
export declare function checkPagination(name: string, value: number): void;
|
|
33
|
+
export declare function validateUpdateData(table: AnyTable, tableName: string, data: Record<string, unknown>): string[];
|