@quark-fw/clisma 0.1.13 → 0.1.15
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 +135 -19
- package/dist/assert-replicated-schema.d.ts +2 -0
- package/dist/assert-replicated-schema.js +16 -0
- package/dist/build-count-query.js +2 -1
- package/dist/build-create-dictionary-query.d.ts +2 -0
- package/dist/build-create-dictionary-query.js +51 -0
- package/dist/build-find-many-query.js +1 -1
- package/dist/build-index-sql.d.ts +2 -0
- package/dist/build-index-sql.js +11 -0
- package/dist/client.d.ts +21 -3
- package/dist/client.js +75 -21
- package/dist/column.d.ts +30 -1
- package/dist/column.js +55 -0
- package/dist/ddl-shared.d.ts +13 -0
- package/dist/ddl-shared.js +54 -0
- package/dist/ddl.d.ts +3 -0
- package/dist/ddl.js +42 -7
- package/dist/dictionary.d.ts +26 -0
- package/dist/dictionary.js +7 -0
- package/dist/engine.d.ts +16 -0
- package/dist/engine.js +26 -0
- package/dist/ensure-dictionaries-on-nodes.d.ts +3 -0
- package/dist/ensure-dictionaries-on-nodes.js +15 -0
- package/dist/ensure-tables-on-nodes.d.ts +11 -0
- package/dist/ensure-tables-on-nodes.js +39 -0
- package/dist/escape-id.d.ts +1 -0
- package/dist/escape-id.js +4 -0
- package/dist/expr.d.ts +5 -0
- package/dist/expr.js +4 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/migrate.d.ts +3 -0
- package/dist/migrate.js +52 -5
- package/dist/query.d.ts +26 -0
- package/dist/query.js +60 -0
- package/dist/rows.js +9 -0
- package/dist/schema.d.ts +4 -2
- package/dist/sql-shared.d.ts +5 -2
- package/dist/sql-shared.js +16 -7
- package/dist/table-client.js +9 -2
- package/dist/table.d.ts +26 -4
- package/dist/table.js +24 -3
- package/package.json +2 -2
|
@@ -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
|
+
}
|
package/dist/ddl.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
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;
|
|
2
5
|
export declare function buildCreateTableSQL(database: string, tableName: string, table: AnyTable): string;
|
package/dist/ddl.js
CHANGED
|
@@ -1,15 +1,50 @@
|
|
|
1
1
|
// src/ddl.ts
|
|
2
|
-
import { escapeId } from "./
|
|
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" ? `${key} = ${value}` : `${key} = ${quoteString(value)}`)
|
|
9
|
+
.join(", ");
|
|
10
|
+
}
|
|
11
|
+
export function buildColumnSQL(name, col) {
|
|
12
|
+
const parts = [escapeId(name), col.clickhouseType];
|
|
13
|
+
if (col.default)
|
|
14
|
+
parts.push(`${col.default.kind} ${col.default.expr}`);
|
|
15
|
+
if (col.codec)
|
|
16
|
+
parts.push(`CODEC(${col.codec})`);
|
|
17
|
+
if (col.comment)
|
|
18
|
+
parts.push(`COMMENT ${quoteString(col.comment)}`);
|
|
19
|
+
return parts.join(" ");
|
|
20
|
+
}
|
|
3
21
|
export function buildCreateTableSQL(database, tableName, table) {
|
|
4
|
-
const
|
|
5
|
-
.map(([name, col]) =>
|
|
6
|
-
.
|
|
22
|
+
const body = [
|
|
23
|
+
...Object.entries(table.columns).map(([name, col]) => buildColumnSQL(name, col)),
|
|
24
|
+
...(table.indexes ?? []).map(buildIndexSQL),
|
|
25
|
+
].join(",\n");
|
|
26
|
+
const clauses = [`ENGINE = ${renderEngine(table)}`];
|
|
27
|
+
if (table.partitionBy) {
|
|
28
|
+
clauses.push(`PARTITION BY ${renderKeyPart(table.partitionBy)}`);
|
|
29
|
+
}
|
|
30
|
+
if (table.primaryKey) {
|
|
31
|
+
clauses.push(`PRIMARY KEY ${renderKeyList(table.primaryKey)}`);
|
|
32
|
+
}
|
|
33
|
+
clauses.push(`ORDER BY ${renderKeyList(table.orderBy)}`);
|
|
34
|
+
if (table.sampleBy)
|
|
35
|
+
clauses.push(`SAMPLE BY ${renderKeyPart(table.sampleBy)}`);
|
|
36
|
+
if (table.ttl)
|
|
37
|
+
clauses.push(`TTL ${table.ttl}`);
|
|
38
|
+
if (table.settings && Object.keys(table.settings).length) {
|
|
39
|
+
clauses.push(`SETTINGS ${renderSettings(table.settings)}`);
|
|
40
|
+
}
|
|
41
|
+
if (table.comment)
|
|
42
|
+
clauses.push(`COMMENT ${quoteString(table.comment)}`);
|
|
7
43
|
return `
|
|
8
44
|
CREATE TABLE IF NOT EXISTS ${escapeId(database)}.${escapeId(tableName)}
|
|
9
45
|
(
|
|
10
|
-
${
|
|
46
|
+
${body}
|
|
11
47
|
)
|
|
12
|
-
|
|
13
|
-
ORDER BY (${table.orderBy.map(escapeId).join(", ")})
|
|
48
|
+
${clauses.join("\n ")}
|
|
14
49
|
`.trim();
|
|
15
50
|
}
|
|
@@ -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>;
|
package/dist/engine.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/engine.js
ADDED
|
@@ -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/expr.d.ts
ADDED
package/dist/expr.js
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export * from "./column.js";
|
|
2
|
+
export * from "./expr.js";
|
|
3
|
+
export * from "./engine.js";
|
|
2
4
|
export * from "./table.js";
|
|
5
|
+
export * from "./dictionary.js";
|
|
3
6
|
export * from "./schema.js";
|
|
4
7
|
export * from "./client.js";
|
|
5
8
|
export * from "./where.js";
|
|
@@ -7,3 +10,7 @@ export * from "./sql.js";
|
|
|
7
10
|
export * from "./table-client.js";
|
|
8
11
|
export * from "./rows.js";
|
|
9
12
|
export * from "./migrate.js";
|
|
13
|
+
export * from "./ddl.js";
|
|
14
|
+
export * from "./build-create-dictionary-query.js";
|
|
15
|
+
export type { QueryOptions, QueryJsonResult, QuerySettings } from "./query.js";
|
|
16
|
+
export type { DdlNode, DdlStatement } from "./ensure-tables-on-nodes.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export * from "./column.js";
|
|
2
|
+
export * from "./expr.js";
|
|
3
|
+
export * from "./engine.js";
|
|
2
4
|
export * from "./table.js";
|
|
5
|
+
export * from "./dictionary.js";
|
|
3
6
|
export * from "./schema.js";
|
|
4
7
|
export * from "./client.js";
|
|
5
8
|
export * from "./where.js";
|
|
@@ -7,3 +10,5 @@ export * from "./sql.js";
|
|
|
7
10
|
export * from "./table-client.js";
|
|
8
11
|
export * from "./rows.js";
|
|
9
12
|
export * from "./migrate.js";
|
|
13
|
+
export * from "./ddl.js";
|
|
14
|
+
export * from "./build-create-dictionary-query.js";
|
package/dist/migrate.d.ts
CHANGED
|
@@ -4,8 +4,11 @@ export type DbColumn = {
|
|
|
4
4
|
name: string;
|
|
5
5
|
type: string;
|
|
6
6
|
};
|
|
7
|
+
export type DbTableSettings = Record<string, Record<string, string>>;
|
|
7
8
|
export type MigrationPlan = {
|
|
8
9
|
statements: string[];
|
|
9
10
|
warnings: string[];
|
|
10
11
|
};
|
|
12
|
+
export declare function parseEngineSettings(engineFull: string): Record<string, string>;
|
|
11
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[];
|
package/dist/migrate.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
// src/migrate.ts
|
|
2
|
-
import { buildCreateTableSQL } from "./ddl.js";
|
|
3
|
-
import { escapeId } from "./
|
|
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("'") ? raw.slice(1, -1).replace(/\\(.)/g, "$1") : raw;
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
4
18
|
export function buildMigrationPlan(database, tables, existing) {
|
|
5
19
|
const statements = [];
|
|
6
20
|
const warnings = [];
|
|
@@ -21,14 +35,23 @@ export function buildMigrationPlan(database, tables, existing) {
|
|
|
21
35
|
continue;
|
|
22
36
|
}
|
|
23
37
|
const tableRef = `${escapeId(database)}.${escapeId(tableName)}`;
|
|
38
|
+
const keys = keyColumns([...def.orderBy, ...(def.primaryKey ?? [])]);
|
|
24
39
|
for (const colName in def.columns) {
|
|
25
|
-
const
|
|
40
|
+
const col = def.columns[colName];
|
|
41
|
+
const wanted = col.clickhouseType;
|
|
26
42
|
const actual = dbCols.get(colName);
|
|
27
43
|
if (actual === undefined) {
|
|
28
|
-
statements.push(`ALTER TABLE ${tableRef} ADD COLUMN ${
|
|
44
|
+
statements.push(`ALTER TABLE ${tableRef} ADD COLUMN ${buildColumnSQL(colName, col)}`);
|
|
29
45
|
}
|
|
30
46
|
else if (actual !== wanted) {
|
|
31
|
-
|
|
47
|
+
// ключевую колонку сервер менять не даст; лучше внятная ошибка
|
|
48
|
+
// до выполнения плана, чем упавшая посередине миграция
|
|
49
|
+
if (keys.includes(colName)) {
|
|
50
|
+
throw new Error(`Column "${colName}" of ${database}.${tableName} is part of the ` +
|
|
51
|
+
`sorting key and cannot change type (${actual} → ${wanted}); ` +
|
|
52
|
+
`recreate the table instead`);
|
|
53
|
+
}
|
|
54
|
+
statements.push(`ALTER TABLE ${tableRef} MODIFY COLUMN ${buildColumnSQL(colName, col)}`);
|
|
32
55
|
}
|
|
33
56
|
}
|
|
34
57
|
for (const dbColName of dbCols.keys()) {
|
|
@@ -40,3 +63,27 @@ export function buildMigrationPlan(database, tables, existing) {
|
|
|
40
63
|
}
|
|
41
64
|
return { statements, warnings };
|
|
42
65
|
}
|
|
66
|
+
// Настройки таблицы — локальные метаданные каждой ноды: MODIFY SETTING не
|
|
67
|
+
// реплицируется (в отличие от ADD/MODIFY COLUMN), поэтому план настроек
|
|
68
|
+
// строится и выполняется на каждой ноде по её `system.tables`. Только
|
|
69
|
+
// объявленные в схеме настройки и только существующие на ноде таблицы —
|
|
70
|
+
// новые получат их из CREATE (или из SHOW CREATE копии). Правка метаданных,
|
|
71
|
+
// данные не переписываются.
|
|
72
|
+
export function buildSettingsPlan(database, tables, existingSettings) {
|
|
73
|
+
const statements = [];
|
|
74
|
+
for (const tableName in tables) {
|
|
75
|
+
const declared = tables[tableName].settings;
|
|
76
|
+
const actual = existingSettings[tableName];
|
|
77
|
+
if (!declared || !actual)
|
|
78
|
+
continue;
|
|
79
|
+
const changed = {};
|
|
80
|
+
for (const [key, value] of Object.entries(declared)) {
|
|
81
|
+
if (actual[key] !== String(value))
|
|
82
|
+
changed[key] = value;
|
|
83
|
+
}
|
|
84
|
+
if (Object.keys(changed).length) {
|
|
85
|
+
statements.push(`ALTER TABLE ${escapeId(database)}.${escapeId(tableName)} MODIFY SETTING ${renderSettings(changed)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return statements;
|
|
89
|
+
}
|
package/dist/query.d.ts
ADDED
|
@@ -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>;
|
package/dist/query.js
ADDED
|
@@ -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/rows.js
CHANGED
|
@@ -13,12 +13,21 @@ function convertValue(value, chType) {
|
|
|
13
13
|
const nullable = /^Nullable\((.+)\)$/.exec(chType);
|
|
14
14
|
if (nullable)
|
|
15
15
|
return convertValue(value, nullable[1]);
|
|
16
|
+
// LowCardinality — способ хранения, на значение не влияет
|
|
17
|
+
const lowCardinality = /^LowCardinality\((.+)\)$/.exec(chType);
|
|
18
|
+
if (lowCardinality)
|
|
19
|
+
return convertValue(value, lowCardinality[1]);
|
|
16
20
|
const array = /^Array\((.+)\)$/.exec(chType);
|
|
17
21
|
if (array) {
|
|
18
22
|
return value.map((v) => convertValue(v, array[1]));
|
|
19
23
|
}
|
|
20
24
|
if (BIGINT_TYPES.has(chType))
|
|
21
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);
|
|
22
31
|
if (chType.startsWith("DateTime") || chType.startsWith("Date")) {
|
|
23
32
|
return new Date(value);
|
|
24
33
|
}
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { AnyTable } from "./table.js";
|
|
2
|
-
|
|
2
|
+
import type { AnyDictionary } from "./dictionary.js";
|
|
3
|
+
export type ClismaSchema<TTables extends Record<string, AnyTable>, TDictionaries extends Record<string, AnyDictionary> = Record<string, never>> = {
|
|
3
4
|
database?: string;
|
|
4
5
|
tables: TTables;
|
|
6
|
+
dictionaries?: TDictionaries;
|
|
5
7
|
};
|
|
6
|
-
export declare function createClisma<TTables extends Record<string, AnyTable>>(schema: ClismaSchema<TTables>): ClismaSchema<TTables>;
|
|
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>;
|
package/dist/sql-shared.d.ts
CHANGED
|
@@ -15,8 +15,10 @@ export type FindManyArgs<T> = {
|
|
|
15
15
|
orderBy?: OrderByInput<T> | OrderByInput<T>[];
|
|
16
16
|
take?: number;
|
|
17
17
|
skip?: number;
|
|
18
|
+
final?: boolean;
|
|
19
|
+
settings?: Record<string, unknown>;
|
|
18
20
|
};
|
|
19
|
-
export
|
|
21
|
+
export { escapeId } from "./escape-id.js";
|
|
20
22
|
export declare const NO_LIMIT = "18446744073709551615";
|
|
21
23
|
export declare function createBuildContext(tableName: string, columns: AnyColumns): {
|
|
22
24
|
params: Record<string, unknown>;
|
|
@@ -25,6 +27,7 @@ export declare function createBuildContext(tableName: string, columns: AnyColumn
|
|
|
25
27
|
addParam: (value: unknown, type: string) => string;
|
|
26
28
|
whereSQL: (where: Record<string, unknown>) => string;
|
|
27
29
|
};
|
|
28
|
-
export declare function tableRef(database: string, tableName: string, table: AnyTable): string;
|
|
30
|
+
export declare function tableRef(database: string, tableName: string, table: AnyTable, final?: boolean): string;
|
|
31
|
+
export declare function countExpr(table: AnyTable): string;
|
|
29
32
|
export declare function checkPagination(name: string, value: number): void;
|
|
30
33
|
export declare function validateUpdateData(table: AnyTable, tableName: string, data: Record<string, unknown>): string[];
|
package/dist/sql-shared.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// src/sql-shared.ts
|
|
2
2
|
// Shared helpers and types for the SQL builders (build-*.ts).
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { escapeId } from "./escape-id.js";
|
|
4
|
+
import { collapsingOf, keyColumns } from "./ddl-shared.js";
|
|
5
|
+
// escapeId живёт отдельным файлом: он нужен и рендеру DDL, и билдерам
|
|
6
|
+
// запросов, а взаимный импорт между ними создал бы цикл модулей
|
|
7
|
+
export { escapeId } from "./escape-id.js";
|
|
6
8
|
// UInt64 max — used to emulate a bare OFFSET (skip without take)
|
|
7
9
|
export const NO_LIMIT = "18446744073709551615";
|
|
8
10
|
// DateTime is second-precision; the client would send Date with
|
|
@@ -136,10 +138,17 @@ export function createBuildContext(tableName, columns) {
|
|
|
136
138
|
}
|
|
137
139
|
return { params, colRef, baseType, addParam, whereSQL };
|
|
138
140
|
}
|
|
139
|
-
export function tableRef(database, tableName, table) {
|
|
141
|
+
export function tableRef(database, tableName, table, final = false) {
|
|
140
142
|
const ref = `${escapeId(database)}.${escapeId(tableName)}`;
|
|
141
|
-
// replacing
|
|
142
|
-
|
|
143
|
+
// replacing-таблицы обязаны читать последние версии строк; для остальных
|
|
144
|
+
// FINAL включается явно (например, чтобы прочитать коллапсирующую таблицу)
|
|
145
|
+
return table.mutations === "replacing" || final ? `${ref} FINAL` : ref;
|
|
146
|
+
}
|
|
147
|
+
// у коллапсирующих таблиц строка «существует», только если её знак не
|
|
148
|
+
// погашен парой -1 — count() посчитал бы обе строки пары
|
|
149
|
+
export function countExpr(table) {
|
|
150
|
+
const collapsing = collapsingOf(table.engine);
|
|
151
|
+
return collapsing ? `sum(${escapeId(collapsing.sign)})` : "count()";
|
|
143
152
|
}
|
|
144
153
|
export function checkPagination(name, value) {
|
|
145
154
|
if (!Number.isInteger(value) || value < 0) {
|
|
@@ -153,7 +162,7 @@ export function validateUpdateData(table, tableName, data) {
|
|
|
153
162
|
throw new Error(`updateMany requires non-empty data`);
|
|
154
163
|
}
|
|
155
164
|
for (const key of keys) {
|
|
156
|
-
if (table.orderBy.includes(key)) {
|
|
165
|
+
if (keyColumns(table.orderBy).includes(key)) {
|
|
157
166
|
throw new Error(`Cannot update ORDER BY key column "${key}" ` +
|
|
158
167
|
`of table "${tableName}"`);
|
|
159
168
|
}
|
package/dist/table-client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/table-client.ts
|
|
2
2
|
import { buildFindManyQuery, buildCountQuery, buildUpdateQuery, buildDeleteQuery, buildLightweightDeleteQuery, validateUpdateData, escapeId, } from "./sql.js";
|
|
3
3
|
import { deserializeRows } from "./rows.js";
|
|
4
|
+
import { collapsingOf } from "./ddl-shared.js";
|
|
4
5
|
// process-unique id: ms timestamp in the high bits + 20-bit counter
|
|
5
6
|
let idCounter = 0n;
|
|
6
7
|
function generateId() {
|
|
@@ -20,12 +21,13 @@ export class TableClient {
|
|
|
20
21
|
get tableRef() {
|
|
21
22
|
return `${escapeId(this.database)}.${escapeId(this.tableName)}`;
|
|
22
23
|
}
|
|
23
|
-
async runQuery(sql, params) {
|
|
24
|
+
async runQuery(sql, params, settings = {}) {
|
|
24
25
|
const result = await this.client.query({
|
|
25
26
|
query: sql,
|
|
26
27
|
query_params: params,
|
|
27
28
|
format: "JSONEachRow",
|
|
28
29
|
clickhouse_settings: {
|
|
30
|
+
...settings,
|
|
29
31
|
// ISO with Z: the default DateTime rendering is in server
|
|
30
32
|
// timezone with no offset, which is unparseable correctly
|
|
31
33
|
date_time_output_format: "iso",
|
|
@@ -37,8 +39,13 @@ export class TableClient {
|
|
|
37
39
|
return result.json();
|
|
38
40
|
}
|
|
39
41
|
async findMany(args) {
|
|
42
|
+
// сырые строки коллапсирующей таблицы содержат и погашенные версии —
|
|
43
|
+
// без FINAL такой результат вводит в заблуждение
|
|
44
|
+
if (collapsingOf(this.table.engine) && !args?.final) {
|
|
45
|
+
throw new Error(`Table "${this.tableName}" uses a collapsing engine: pass final: true to findMany`);
|
|
46
|
+
}
|
|
40
47
|
const { sql, params } = buildFindManyQuery(this.database, this.tableName, this.table, args);
|
|
41
|
-
const rows = await this.runQuery(sql, params);
|
|
48
|
+
const rows = await this.runQuery(sql, params, args?.settings);
|
|
42
49
|
return deserializeRows(rows, this.table.columns);
|
|
43
50
|
}
|
|
44
51
|
async findFirst(args) {
|