@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.
Files changed (42) hide show
  1. package/README.md +126 -19
  2. package/dist/assert-replicated-schema.d.ts +2 -0
  3. package/dist/assert-replicated-schema.js +16 -0
  4. package/dist/build-count-query.js +2 -1
  5. package/dist/build-create-dictionary-query.d.ts +2 -0
  6. package/dist/build-create-dictionary-query.js +51 -0
  7. package/dist/build-find-many-query.js +1 -1
  8. package/dist/build-index-sql.d.ts +2 -0
  9. package/dist/build-index-sql.js +11 -0
  10. package/dist/client.d.ts +17 -3
  11. package/dist/client.js +45 -18
  12. package/dist/column.d.ts +30 -1
  13. package/dist/column.js +55 -0
  14. package/dist/ddl-shared.d.ts +13 -0
  15. package/dist/ddl-shared.js +54 -0
  16. package/dist/ddl.d.ts +2 -0
  17. package/dist/ddl.js +41 -7
  18. package/dist/dictionary.d.ts +26 -0
  19. package/dist/dictionary.js +7 -0
  20. package/dist/engine.d.ts +16 -0
  21. package/dist/engine.js +26 -0
  22. package/dist/ensure-dictionaries-on-nodes.d.ts +3 -0
  23. package/dist/ensure-dictionaries-on-nodes.js +15 -0
  24. package/dist/ensure-tables-on-nodes.d.ts +11 -0
  25. package/dist/ensure-tables-on-nodes.js +39 -0
  26. package/dist/escape-id.d.ts +1 -0
  27. package/dist/escape-id.js +4 -0
  28. package/dist/expr.d.ts +5 -0
  29. package/dist/expr.js +4 -0
  30. package/dist/index.d.ts +7 -0
  31. package/dist/index.js +5 -0
  32. package/dist/migrate.js +15 -5
  33. package/dist/query.d.ts +26 -0
  34. package/dist/query.js +60 -0
  35. package/dist/rows.js +9 -0
  36. package/dist/schema.d.ts +4 -2
  37. package/dist/sql-shared.d.ts +5 -2
  38. package/dist/sql-shared.js +16 -7
  39. package/dist/table-client.js +9 -2
  40. package/dist/table.d.ts +26 -4
  41. package/dist/table.js +24 -3
  42. package/package.json +2 -2
package/dist/ddl.js CHANGED
@@ -1,15 +1,49 @@
1
1
  // src/ddl.ts
2
- import { escapeId } from "./sql.js";
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 columnsSQL = Object.entries(table.columns)
5
- .map(([name, col]) => `${escapeId(name)} ${col.clickhouseType}`)
6
- .join(",\n");
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
- ${columnsSQL}
45
+ ${body}
11
46
  )
12
- ENGINE = ${table.engine}
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>;
@@ -0,0 +1,7 @@
1
+ // src/dictionary.ts
2
+ export function dictionary(def) {
3
+ if (Object.keys(def.key).length === 0) {
4
+ throw new Error("Dictionary requires at least one key column");
5
+ }
6
+ return def;
7
+ }
@@ -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;
@@ -0,0 +1,4 @@
1
+ // src/escape-id.ts
2
+ export function escapeId(name) {
3
+ return "`" + name.replace(/\\/g, "\\\\").replace(/`/g, "\\`") + "`";
4
+ }
package/dist/expr.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export type Expr = {
2
+ readonly __expr: string;
3
+ };
4
+ export type KeyPart = string | Expr;
5
+ export declare function expr(sql: string): Expr;
package/dist/expr.js ADDED
@@ -0,0 +1,4 @@
1
+ // src/expr.ts
2
+ export function expr(sql) {
3
+ return { __expr: sql };
4
+ }
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 "./sql.js";
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 wanted = def.columns[colName].clickhouseType;
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 ${escapeId(colName)} ${wanted}`);
31
+ statements.push(`ALTER TABLE ${tableRef} ADD COLUMN ${buildColumnSQL(colName, col)}`);
29
32
  }
30
33
  else if (actual !== wanted) {
31
- statements.push(`ALTER TABLE ${tableRef} MODIFY COLUMN ${escapeId(colName)} ${wanted}`);
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()) {
@@ -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
- export type ClismaSchema<TTables extends Record<string, AnyTable>> = {
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>;
@@ -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 declare function escapeId(name: string): string;
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[];
@@ -1,8 +1,10 @@
1
1
  // src/sql-shared.ts
2
2
  // Shared helpers and types for the SQL builders (build-*.ts).
3
- export function escapeId(name) {
4
- return "`" + name.replace(/\\/g, "\\\\").replace(/`/g, "\\`") + "`";
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 tables must read the latest row versions
142
- return table.mutations === "replacing" ? `${ref} FINAL` : ref;
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
  }
@@ -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
- orderBy: string[];
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 InferRow<TTable> = TTable extends TableDefinition<infer TColumns> ? Simplify<{
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
- }> : never;
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.12",
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": {