@quark-fw/clisma 0.1.12 → 0.1.14
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 +126 -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 +17 -3
- package/dist/client.js +45 -18
- 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 +2 -0
- package/dist/ddl.js +41 -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.js +15 -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
package/dist/ddl.js
CHANGED
|
@@ -1,15 +1,49 @@
|
|
|
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
|
+
export function buildColumnSQL(name, col) {
|
|
6
|
+
const parts = [escapeId(name), col.clickhouseType];
|
|
7
|
+
if (col.default)
|
|
8
|
+
parts.push(`${col.default.kind} ${col.default.expr}`);
|
|
9
|
+
if (col.codec)
|
|
10
|
+
parts.push(`CODEC(${col.codec})`);
|
|
11
|
+
if (col.comment)
|
|
12
|
+
parts.push(`COMMENT ${quoteString(col.comment)}`);
|
|
13
|
+
return parts.join(" ");
|
|
14
|
+
}
|
|
3
15
|
export function buildCreateTableSQL(database, tableName, table) {
|
|
4
|
-
const
|
|
5
|
-
.map(([name, col]) =>
|
|
6
|
-
.
|
|
16
|
+
const body = [
|
|
17
|
+
...Object.entries(table.columns).map(([name, col]) => buildColumnSQL(name, col)),
|
|
18
|
+
...(table.indexes ?? []).map(buildIndexSQL),
|
|
19
|
+
].join(",\n");
|
|
20
|
+
const clauses = [`ENGINE = ${renderEngine(table)}`];
|
|
21
|
+
if (table.partitionBy) {
|
|
22
|
+
clauses.push(`PARTITION BY ${renderKeyPart(table.partitionBy)}`);
|
|
23
|
+
}
|
|
24
|
+
if (table.primaryKey) {
|
|
25
|
+
clauses.push(`PRIMARY KEY ${renderKeyList(table.primaryKey)}`);
|
|
26
|
+
}
|
|
27
|
+
clauses.push(`ORDER BY ${renderKeyList(table.orderBy)}`);
|
|
28
|
+
if (table.sampleBy)
|
|
29
|
+
clauses.push(`SAMPLE BY ${renderKeyPart(table.sampleBy)}`);
|
|
30
|
+
if (table.ttl)
|
|
31
|
+
clauses.push(`TTL ${table.ttl}`);
|
|
32
|
+
if (table.settings && Object.keys(table.settings).length) {
|
|
33
|
+
const settings = Object.entries(table.settings)
|
|
34
|
+
.map(([key, value]) => typeof value === "number"
|
|
35
|
+
? `${key} = ${value}`
|
|
36
|
+
: `${key} = ${quoteString(value)}`)
|
|
37
|
+
.join(", ");
|
|
38
|
+
clauses.push(`SETTINGS ${settings}`);
|
|
39
|
+
}
|
|
40
|
+
if (table.comment)
|
|
41
|
+
clauses.push(`COMMENT ${quoteString(table.comment)}`);
|
|
7
42
|
return `
|
|
8
43
|
CREATE TABLE IF NOT EXISTS ${escapeId(database)}.${escapeId(tableName)}
|
|
9
44
|
(
|
|
10
|
-
${
|
|
45
|
+
${body}
|
|
11
46
|
)
|
|
12
|
-
|
|
13
|
-
ORDER BY (${table.orderBy.map(escapeId).join(", ")})
|
|
47
|
+
${clauses.join("\n ")}
|
|
14
48
|
`.trim();
|
|
15
49
|
}
|
|
@@ -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.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/migrate.ts
|
|
2
|
-
import { buildCreateTableSQL } from "./ddl.js";
|
|
3
|
-
import { escapeId } from "./
|
|
2
|
+
import { buildColumnSQL, buildCreateTableSQL } from "./ddl.js";
|
|
3
|
+
import { escapeId } from "./escape-id.js";
|
|
4
|
+
import { keyColumns } from "./ddl-shared.js";
|
|
4
5
|
export function buildMigrationPlan(database, tables, existing) {
|
|
5
6
|
const statements = [];
|
|
6
7
|
const warnings = [];
|
|
@@ -21,14 +22,23 @@ export function buildMigrationPlan(database, tables, existing) {
|
|
|
21
22
|
continue;
|
|
22
23
|
}
|
|
23
24
|
const tableRef = `${escapeId(database)}.${escapeId(tableName)}`;
|
|
25
|
+
const keys = keyColumns([...def.orderBy, ...(def.primaryKey ?? [])]);
|
|
24
26
|
for (const colName in def.columns) {
|
|
25
|
-
const
|
|
27
|
+
const col = def.columns[colName];
|
|
28
|
+
const wanted = col.clickhouseType;
|
|
26
29
|
const actual = dbCols.get(colName);
|
|
27
30
|
if (actual === undefined) {
|
|
28
|
-
statements.push(`ALTER TABLE ${tableRef} ADD COLUMN ${
|
|
31
|
+
statements.push(`ALTER TABLE ${tableRef} ADD COLUMN ${buildColumnSQL(colName, col)}`);
|
|
29
32
|
}
|
|
30
33
|
else if (actual !== wanted) {
|
|
31
|
-
|
|
34
|
+
// ключевую колонку сервер менять не даст; лучше внятная ошибка
|
|
35
|
+
// до выполнения плана, чем упавшая посередине миграция
|
|
36
|
+
if (keys.includes(colName)) {
|
|
37
|
+
throw new Error(`Column "${colName}" of ${database}.${tableName} is part of the ` +
|
|
38
|
+
`sorting key and cannot change type (${actual} → ${wanted}); ` +
|
|
39
|
+
`recreate the table instead`);
|
|
40
|
+
}
|
|
41
|
+
statements.push(`ALTER TABLE ${tableRef} MODIFY COLUMN ${buildColumnSQL(colName, col)}`);
|
|
32
42
|
}
|
|
33
43
|
}
|
|
34
44
|
for (const dbColName of dbCols.keys()) {
|
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) {
|
package/dist/table.d.ts
CHANGED
|
@@ -1,15 +1,37 @@
|
|
|
1
1
|
import { Column } from "./column.js";
|
|
2
|
+
import type { EngineSpec } from "./engine.js";
|
|
3
|
+
import type { KeyPart } from "./expr.js";
|
|
2
4
|
import { Simplify } from "./types.js";
|
|
3
5
|
export type MutationStrategy = "alter" | "lightweight" | "replacing";
|
|
6
|
+
export type IndexType = "bloom_filter" | "set" | "minmax" | "tokenbf_v1" | "ngrambf_v1";
|
|
7
|
+
export type IndexDefinition = {
|
|
8
|
+
name: string;
|
|
9
|
+
expr: KeyPart;
|
|
10
|
+
type: IndexType;
|
|
11
|
+
args?: (number | string)[];
|
|
12
|
+
granularity?: number;
|
|
13
|
+
};
|
|
4
14
|
export type TableDefinition<TColumns extends Record<string, Column<unknown>>> = {
|
|
5
|
-
engine: string;
|
|
6
|
-
|
|
15
|
+
engine: string | EngineSpec;
|
|
16
|
+
replication?: {
|
|
17
|
+
path: string;
|
|
18
|
+
replica: string;
|
|
19
|
+
};
|
|
20
|
+
orderBy: KeyPart[];
|
|
21
|
+
primaryKey?: KeyPart[];
|
|
22
|
+
partitionBy?: KeyPart;
|
|
23
|
+
sampleBy?: KeyPart;
|
|
24
|
+
indexes?: IndexDefinition[];
|
|
25
|
+
ttl?: string;
|
|
26
|
+
settings?: Record<string, string | number>;
|
|
27
|
+
comment?: string;
|
|
7
28
|
mutations?: MutationStrategy;
|
|
8
29
|
columns: TColumns;
|
|
9
30
|
};
|
|
10
31
|
export type AnyColumns = Record<string, Column<unknown>>;
|
|
11
32
|
export type AnyTable = TableDefinition<AnyColumns>;
|
|
12
33
|
export declare function table<TColumns extends Record<string, Column<unknown>>>(def: TableDefinition<TColumns>): TableDefinition<TColumns>;
|
|
13
|
-
export type
|
|
34
|
+
export type InferColumns<TColumns extends Record<string, Column<unknown>>> = Simplify<{
|
|
14
35
|
[K in keyof TColumns]: TColumns[K] extends Column<infer T> ? T : never;
|
|
15
|
-
}
|
|
36
|
+
}>;
|
|
37
|
+
export type InferRow<TTable> = TTable extends TableDefinition<infer TColumns> ? InferColumns<TColumns> : never;
|
package/dist/table.js
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
// src/table.ts
|
|
2
|
+
import { collapsingOf, isReplacingEngine, keyColumns } from "./ddl-shared.js";
|
|
3
|
+
const INTEGER_TYPE = /^(U?Int(8|16|32|64))$/;
|
|
2
4
|
export function table(def) {
|
|
3
|
-
if (def.mutations === "replacing" &&
|
|
4
|
-
!def.engine.includes("ReplacingMergeTree")) {
|
|
5
|
+
if (def.mutations === "replacing" && !isReplacingEngine(def.engine)) {
|
|
5
6
|
throw new Error(`mutations: "replacing" requires a ReplacingMergeTree engine ` +
|
|
6
|
-
`(got "${def.engine}")`);
|
|
7
|
+
`(got "${typeof def.engine === "string" ? def.engine : def.engine.family}")`);
|
|
8
|
+
}
|
|
9
|
+
// коллапс работает только с целочисленным знаком и версией — ошибка
|
|
10
|
+
// схемы должна всплыть при объявлении, а не при первом INSERT
|
|
11
|
+
const collapsing = collapsingOf(def.engine);
|
|
12
|
+
if (collapsing) {
|
|
13
|
+
const sign = def.columns[collapsing.sign];
|
|
14
|
+
if (!sign || sign.clickhouseType !== "Int8") {
|
|
15
|
+
throw new Error(`Collapsing engine requires sign column "${collapsing.sign}" of type Int8`);
|
|
16
|
+
}
|
|
17
|
+
if (collapsing.version) {
|
|
18
|
+
const version = def.columns[collapsing.version];
|
|
19
|
+
if (!version || !INTEGER_TYPE.test(version.clickhouseType)) {
|
|
20
|
+
throw new Error(`VersionedCollapsingMergeTree requires integer version column "${collapsing.version}"`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
for (const key of keyColumns([...def.orderBy, ...(def.primaryKey ?? [])])) {
|
|
25
|
+
if (!def.columns[key]) {
|
|
26
|
+
throw new Error(`Key column "${key}" is not declared in columns`);
|
|
27
|
+
}
|
|
7
28
|
}
|
|
8
29
|
return def;
|
|
9
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quark-fw/clisma",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Prisma-like typed client and schema migrator for ClickHouse",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"clickhouse",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"build": "tsc -p tsconfig.build.json",
|
|
33
33
|
"typecheck": "tsc --noEmit",
|
|
34
34
|
"test": "tsx --env-file=./src/test/.env ./src/test/index.ts",
|
|
35
|
-
"test:unit": "tsx --test src/test/unit.test.ts",
|
|
35
|
+
"test:unit": "tsx --test src/test/unit.test.ts src/test/ddl.test.ts",
|
|
36
36
|
"prepublishOnly": "npm run typecheck && npm run test:unit && npm run build"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|