@quark-fw/clisma 0.1.14 → 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 CHANGED
@@ -255,6 +255,15 @@ const db = createClient(schema, {
255
255
  - таблицы нет → `CREATE TABLE`
256
256
  - колонки нет → `ALTER TABLE ... ADD COLUMN`
257
257
  - тип отличается → `ALTER TABLE ... MODIFY COLUMN`
258
+ - объявленная в `settings` настройка отличается от `system.tables.engine_full` →
259
+ `ALTER TABLE ... MODIFY SETTING` (правка метаданных, данные не переписываются);
260
+ настройки, которых нет в схеме, не трогаются. Настройки — локальные метаданные
261
+ ноды и **не реплицируются**, поэтому сравниваются и меняются на каждой ноде из
262
+ `ddlNodes` отдельно (`plan.settings`: `{ node, sql }`)
263
+
264
+ `db.$migrate({ dryRun: true })` только строит план: ничего не выполняет, не
265
+ создаёт БД и таблицы на нодах (`executed` пустой) — проверка перед прогоном на
266
+ живой базе.
258
267
 
259
268
  Возвращает `{ statements, warnings, executed }` — план, предупреждения и
260
269
  фактически выполненные SQL с нодой (`executed[].node`). Повторный запуск на
@@ -266,7 +275,7 @@ const db = createClient(schema, {
266
275
  - лишние колонки в БД **никогда не удаляются** — только warning;
267
276
  - сравнивается только тип колонки: кодеки, `DEFAULT`, комментарии в диффе не
268
277
  участвуют (в `ADD COLUMN` они рендерятся);
269
- - `ENGINE`, `ORDER BY`, `PARTITION BY`, индексы, TTL не диффятся — их
278
+ - `ENGINE`, `ORDER BY`, `PARTITION BY`, индексы, TTL не диффятся (настройки — диффятся) — их
270
279
  изменение требует пересоздания таблицы вручную;
271
280
  - смена типа ключевой колонки (из `ORDER BY`/`PRIMARY KEY`) — ошибка до
272
281
  выполнения плана: сервер её всё равно отклонит, лучше упасть целиком, чем
package/dist/client.d.ts CHANGED
@@ -15,15 +15,19 @@ type ClientOptions = {
15
15
  requestTimeoutMs?: number;
16
16
  };
17
17
  export type DdlPlan = MigrationPlan & {
18
+ settings: DdlStatement[];
18
19
  executed: DdlStatement[];
19
20
  };
21
+ export type MigrateOptions = {
22
+ dryRun?: boolean;
23
+ };
20
24
  export declare function createClient<TSchema extends {
21
25
  database?: string;
22
26
  tables: Record<string, AnyTable>;
23
27
  dictionaries?: Record<string, AnyDictionary>;
24
28
  }>(schema: TSchema, options: ClientOptions): { [K in keyof TSchema["tables"]]: TableClient<InferRow<TSchema["tables"][K]>>; } & {
25
29
  $init(): Promise<DdlStatement[]>;
26
- $migrate(): Promise<DdlPlan>;
30
+ $migrate(opts?: MigrateOptions): Promise<DdlPlan>;
27
31
  $query<TRow>(sql: string, opts?: QueryOptions): Promise<TRow[]>;
28
32
  $queryJson<TRow, TTotals = TRow>(sql: string, opts?: QueryOptions): Promise<QueryJsonResult<TRow, TTotals>>;
29
33
  $command(sql: string, opts?: Omit<QueryOptions, "columns">): Promise<void>;
package/dist/client.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createClient as createCHClient, } from "@clickhouse/client";
2
2
  import { TableClient } from "./table-client.js";
3
3
  import { escapeId } from "./escape-id.js";
4
- import { buildMigrationPlan } from "./migrate.js";
4
+ import { buildMigrationPlan, buildSettingsPlan, parseEngineSettings, } from "./migrate.js";
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";
@@ -48,14 +48,27 @@ export function createClient(schema, options) {
48
48
  ? { user: options.username, password: options.password }
49
49
  : undefined, dictionaryMode)),
50
50
  ];
51
+ const readTableSettings = async (client) => {
52
+ const result = await client.query({
53
+ query: `SELECT name, engine_full FROM system.tables WHERE database = {db:String}`,
54
+ query_params: { db: database },
55
+ format: "JSONEachRow",
56
+ });
57
+ const out = {};
58
+ for (const row of await result.json()) {
59
+ out[row.name] = parseEngineSettings(row.engine_full);
60
+ }
61
+ return out;
62
+ };
51
63
  db.$init = async () => {
52
64
  assertReplicatedSchema(nodes.length, schema.tables);
53
65
  await ensureDatabase();
54
66
  return ensureEverywhere("create");
55
67
  };
56
- db.$migrate = async () => {
68
+ db.$migrate = async (opts) => {
57
69
  assertReplicatedSchema(nodes.length, schema.tables);
58
- await ensureDatabase();
70
+ if (!opts?.dryRun)
71
+ await ensureDatabase();
59
72
  const result = await ch.query({
60
73
  query: `
61
74
  SELECT table, name, type
@@ -68,6 +81,15 @@ export function createClient(schema, options) {
68
81
  });
69
82
  const existing = await result.json();
70
83
  const plan = buildMigrationPlan(database, schema.tables, existing);
84
+ const settings = [];
85
+ for (const node of nodes) {
86
+ for (const sql of buildSettingsPlan(database, schema.tables, await readTableSettings(node.client))) {
87
+ settings.push({ node: node.url, sql });
88
+ }
89
+ }
90
+ // dryRun: только план — ни CREATE DATABASE, ни DDL на нодах
91
+ if (opts?.dryRun)
92
+ return { ...plan, settings, executed: [] };
71
93
  // план (CREATE недостающих, ALTER ADD/MODIFY) выполняется на первой
72
94
  // ноде: реплицируемый ALTER сам разъезжается по репликам, а недостающие
73
95
  // на репликах таблицы докладывает ensureTablesOnNodes копией SHOW CREATE
@@ -77,7 +99,12 @@ export function createClient(schema, options) {
77
99
  executed.push({ node: options.url, sql: statement });
78
100
  }
79
101
  executed.push(...(await ensureEverywhere("replace")));
80
- return { ...plan, executed };
102
+ for (const statement of settings) {
103
+ const node = nodes.find((n) => n.url === statement.node);
104
+ await node.client.command({ query: statement.sql });
105
+ executed.push(statement);
106
+ }
107
+ return { ...plan, settings, executed };
81
108
  };
82
109
  db.$query = (sql, opts) => runQuery(ch, sql, opts);
83
110
  db.$queryJson = (sql, opts) => runQueryJson(ch, sql, opts);
package/dist/ddl.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { AnyTable } from "./table.js";
2
2
  import type { Column } from "./column.js";
3
+ export declare function renderSettings(settings: Record<string, string | number>): string;
3
4
  export declare function buildColumnSQL(name: string, col: Column<unknown>): string;
4
5
  export declare function buildCreateTableSQL(database: string, tableName: string, table: AnyTable): string;
package/dist/ddl.js CHANGED
@@ -2,6 +2,12 @@
2
2
  import { escapeId } from "./escape-id.js";
3
3
  import { buildIndexSQL } from "./build-index-sql.js";
4
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
+ }
5
11
  export function buildColumnSQL(name, col) {
6
12
  const parts = [escapeId(name), col.clickhouseType];
7
13
  if (col.default)
@@ -30,12 +36,7 @@ export function buildCreateTableSQL(database, tableName, table) {
30
36
  if (table.ttl)
31
37
  clauses.push(`TTL ${table.ttl}`);
32
38
  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
+ clauses.push(`SETTINGS ${renderSettings(table.settings)}`);
39
40
  }
40
41
  if (table.comment)
41
42
  clauses.push(`COMMENT ${quoteString(table.comment)}`);
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,7 +1,20 @@
1
1
  // src/migrate.ts
2
- import { buildColumnSQL, buildCreateTableSQL } from "./ddl.js";
2
+ import { buildColumnSQL, buildCreateTableSQL, renderSettings } from "./ddl.js";
3
3
  import { escapeId } from "./escape-id.js";
4
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
+ }
5
18
  export function buildMigrationPlan(database, tables, existing) {
6
19
  const statements = [];
7
20
  const warnings = [];
@@ -50,3 +63,27 @@ export function buildMigrationPlan(database, tables, existing) {
50
63
  }
51
64
  return { statements, warnings };
52
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quark-fw/clisma",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Prisma-like typed client and schema migrator for ClickHouse",
5
5
  "keywords": [
6
6
  "clickhouse",