@quark-fw/clisma 0.1.15 → 0.1.16

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/client.js CHANGED
@@ -5,6 +5,7 @@ import { buildMigrationPlan, buildSettingsPlan, parseEngineSettings, } from "./m
5
5
  import { assertReplicatedSchema } from "./assert-replicated-schema.js";
6
6
  import { ensureTablesOnNodes, } from "./ensure-tables-on-nodes.js";
7
7
  import { ensureDictionariesOnNodes } from "./ensure-dictionaries-on-nodes.js";
8
+ import { MigrationError } from "./migration-error.js";
8
9
  import { runCommand, runQuery, runQueryJson, } from "./query.js";
9
10
  export function createClient(schema, options) {
10
11
  const database = options.database ?? schema.database ?? "default";
@@ -14,7 +15,9 @@ export function createClient(schema, options) {
14
15
  username: options.username,
15
16
  password: options.password,
16
17
  ...(withDatabase ? { database } : {}),
17
- ...(options.requestTimeoutMs ? { request_timeout: options.requestTimeoutMs } : {}),
18
+ ...(options.requestTimeoutMs
19
+ ? { request_timeout: options.requestTimeoutMs }
20
+ : {}),
18
21
  });
19
22
  const ch = connect(options.url, true);
20
23
  // клиенты DDL-нод создаются один раз; основная нода переиспользует `ch`
@@ -23,6 +26,9 @@ export function createClient(schema, options) {
23
26
  url,
24
27
  client: url === options.url ? ch : connect(url, true),
25
28
  }));
29
+ // источник истины схемы: `url` может быть балансировщиком или нодой вне
30
+ // ddlNodes, и план с него разошёлся бы с тем, куда пишется DDL
31
+ const primary = nodes[0];
26
32
  const db = {};
27
33
  for (const tableName in schema.tables) {
28
34
  db[tableName] = new TableClient(database, tableName, schema.tables[tableName], ch);
@@ -69,7 +75,7 @@ export function createClient(schema, options) {
69
75
  assertReplicatedSchema(nodes.length, schema.tables);
70
76
  if (!opts?.dryRun)
71
77
  await ensureDatabase();
72
- const result = await ch.query({
78
+ const result = await primary.client.query({
73
79
  query: `
74
80
  SELECT table, name, type
75
81
  FROM system.columns
@@ -92,17 +98,33 @@ export function createClient(schema, options) {
92
98
  return { ...plan, settings, executed: [] };
93
99
  // план (CREATE недостающих, ALTER ADD/MODIFY) выполняется на первой
94
100
  // ноде: реплицируемый ALTER сам разъезжается по репликам, а недостающие
95
- // на репликах таблицы докладывает ensureTablesOnNodes копией SHOW CREATE
101
+ // на репликах таблицы докладывает ensureTablesOnNodes копией SHOW CREATE.
102
+ // Сбой посередине — MigrationError со списком уже выполненного
96
103
  const executed = [];
97
- for (const statement of plan.statements) {
98
- await ch.command({ query: statement });
99
- executed.push({ node: options.url, sql: statement });
104
+ const run = async (node, sql) => {
105
+ try {
106
+ await node.client.command({ query: sql });
107
+ }
108
+ catch (cause) {
109
+ throw new MigrationError({
110
+ executed,
111
+ failed: { node: node.url, sql },
112
+ cause,
113
+ });
114
+ }
115
+ executed.push({ node: node.url, sql });
116
+ };
117
+ for (const statement of plan.statements)
118
+ await run(primary, statement);
119
+ try {
120
+ executed.push(...(await ensureEverywhere("replace")));
121
+ }
122
+ catch (cause) {
123
+ throw new MigrationError({ executed, cause });
100
124
  }
101
- executed.push(...(await ensureEverywhere("replace")));
102
125
  for (const statement of settings) {
103
126
  const node = nodes.find((n) => n.url === statement.node);
104
- await node.client.command({ query: statement.sql });
105
- executed.push(statement);
127
+ await run(node, statement.sql);
106
128
  }
107
129
  return { ...plan, settings, executed };
108
130
  };
package/dist/ddl.js CHANGED
@@ -5,7 +5,9 @@ import { quoteString, renderEngine, renderKeyList, renderKeyPart, } from "./ddl-
5
5
  // список для `SETTINGS` / `MODIFY SETTING`: числа как есть, строки в кавычках
6
6
  export function renderSettings(settings) {
7
7
  return Object.entries(settings)
8
- .map(([key, value]) => typeof value === "number" ? `${key} = ${value}` : `${key} = ${quoteString(value)}`)
8
+ .map(([key, value]) => typeof value === "number"
9
+ ? `${key} = ${value}`
10
+ : `${key} = ${quoteString(value)}`)
9
11
  .join(", ");
10
12
  }
11
13
  export function buildColumnSQL(name, col) {
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export * from "./table-client.js";
11
11
  export * from "./rows.js";
12
12
  export * from "./migrate.js";
13
13
  export * from "./ddl.js";
14
+ export * from "./migration-error.js";
14
15
  export * from "./build-create-dictionary-query.js";
15
16
  export type { QueryOptions, QueryJsonResult, QuerySettings } from "./query.js";
16
17
  export type { DdlNode, DdlStatement } from "./ensure-tables-on-nodes.js";
package/dist/index.js CHANGED
@@ -11,4 +11,5 @@ export * from "./table-client.js";
11
11
  export * from "./rows.js";
12
12
  export * from "./migrate.js";
13
13
  export * from "./ddl.js";
14
+ export * from "./migration-error.js";
14
15
  export * from "./build-create-dictionary-query.js";
package/dist/migrate.js CHANGED
@@ -11,7 +11,9 @@ export function parseEngineSettings(engineFull) {
11
11
  const re = /(\w+)\s*=\s*('(?:[^'\\]|\\.)*'|[^,]+)/g;
12
12
  for (const m of engineFull.slice(at + " SETTINGS ".length).matchAll(re)) {
13
13
  const raw = m[2].trim();
14
- out[m[1]] = raw.startsWith("'") ? raw.slice(1, -1).replace(/\\(.)/g, "$1") : raw;
14
+ out[m[1]] = raw.startsWith("'")
15
+ ? raw.slice(1, -1).replace(/\\(.)/g, "$1")
16
+ : raw;
15
17
  }
16
18
  return out;
17
19
  }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quark-fw/clisma",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "Prisma-like typed client and schema migrator for ClickHouse",
5
5
  "keywords": [
6
6
  "clickhouse",
@@ -31,9 +31,9 @@
31
31
  "scripts": {
32
32
  "build": "tsc -p tsconfig.build.json",
33
33
  "typecheck": "tsc --noEmit",
34
- "test": "tsx --env-file=./src/test/.env ./src/test/index.ts",
35
- "test:unit": "tsx --test src/test/unit.test.ts src/test/ddl.test.ts",
36
- "prepublishOnly": "npm run typecheck && npm run test:unit && npm run build"
34
+ "test:integration": "tsx --env-file=./src/test/.env ./src/test/index.ts",
35
+ "test": "tsx --test src/test/unit.test.ts src/test/ddl.test.ts src/test/migrate-nodes.test.ts",
36
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
37
37
  },
38
38
  "dependencies": {
39
39
  "@clickhouse/client": "^1.17.0"