@housekit/orm 0.1.47 → 0.1.48

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 (79) hide show
  1. package/README.md +34 -0
  2. package/dist/builders/delete.js +112 -0
  3. package/dist/builders/insert.d.ts +0 -91
  4. package/dist/builders/insert.js +393 -0
  5. package/dist/builders/prepared.d.ts +1 -2
  6. package/dist/builders/prepared.js +30 -0
  7. package/dist/builders/select.d.ts +0 -161
  8. package/dist/builders/select.js +562 -0
  9. package/dist/builders/select.types.js +1 -0
  10. package/dist/builders/update.js +136 -0
  11. package/dist/client.d.ts +0 -6
  12. package/dist/client.js +140 -0
  13. package/dist/codegen/zod.js +107 -0
  14. package/dist/column.d.ts +1 -25
  15. package/dist/column.js +133 -0
  16. package/dist/compiler.d.ts +0 -7
  17. package/dist/compiler.js +513 -0
  18. package/dist/core.js +6 -0
  19. package/dist/data-types.d.ts +0 -61
  20. package/dist/data-types.js +127 -0
  21. package/dist/dictionary.d.ts +0 -149
  22. package/dist/dictionary.js +158 -0
  23. package/dist/engines.d.ts +0 -385
  24. package/dist/engines.js +292 -0
  25. package/dist/expressions.d.ts +0 -10
  26. package/dist/expressions.js +268 -0
  27. package/dist/external.d.ts +0 -112
  28. package/dist/external.js +224 -0
  29. package/dist/index.d.ts +0 -51
  30. package/dist/index.js +139 -6853
  31. package/dist/logger.js +36 -0
  32. package/dist/materialized-views.d.ts +0 -188
  33. package/dist/materialized-views.js +380 -0
  34. package/dist/metadata.js +59 -0
  35. package/dist/modules/aggregates.d.ts +0 -164
  36. package/dist/modules/aggregates.js +121 -0
  37. package/dist/modules/array.d.ts +0 -98
  38. package/dist/modules/array.js +71 -0
  39. package/dist/modules/conditional.d.ts +0 -84
  40. package/dist/modules/conditional.js +138 -0
  41. package/dist/modules/conversion.d.ts +0 -147
  42. package/dist/modules/conversion.js +109 -0
  43. package/dist/modules/geo.d.ts +0 -164
  44. package/dist/modules/geo.js +112 -0
  45. package/dist/modules/hash.js +4 -0
  46. package/dist/modules/index.js +12 -0
  47. package/dist/modules/json.d.ts +0 -106
  48. package/dist/modules/json.js +76 -0
  49. package/dist/modules/math.d.ts +0 -16
  50. package/dist/modules/math.js +16 -0
  51. package/dist/modules/string.d.ts +0 -136
  52. package/dist/modules/string.js +89 -0
  53. package/dist/modules/time.d.ts +0 -123
  54. package/dist/modules/time.js +91 -0
  55. package/dist/modules/types.d.ts +0 -133
  56. package/dist/modules/types.js +114 -0
  57. package/dist/modules/window.js +140 -0
  58. package/dist/relational.d.ts +0 -82
  59. package/dist/relational.js +290 -0
  60. package/dist/relations.js +21 -0
  61. package/dist/schema-builder.d.ts +0 -90
  62. package/dist/schema-builder.js +140 -0
  63. package/dist/table.d.ts +0 -42
  64. package/dist/table.js +406 -0
  65. package/dist/utils/background-batcher.js +75 -0
  66. package/dist/utils/batch-transform.js +51 -0
  67. package/dist/utils/binary-reader.d.ts +0 -6
  68. package/dist/utils/binary-reader.js +334 -0
  69. package/dist/utils/binary-serializer.d.ts +0 -125
  70. package/dist/utils/binary-serializer.js +637 -0
  71. package/dist/utils/binary-worker-code.js +1 -0
  72. package/dist/utils/binary-worker-pool.d.ts +0 -34
  73. package/dist/utils/binary-worker-pool.js +206 -0
  74. package/dist/utils/binary-worker.d.ts +0 -11
  75. package/dist/utils/binary-worker.js +63 -0
  76. package/dist/utils/insert-processing.d.ts +0 -2
  77. package/dist/utils/insert-processing.js +163 -0
  78. package/dist/utils/lru-cache.js +30 -0
  79. package/package.json +68 -3
@@ -0,0 +1,562 @@
1
+ import { ClickHouseColumn } from '../core';
2
+ import { sql, eq } from '../expressions';
3
+ import { and } from '../modules/conditional';
4
+ import { QueryCompiler } from '../compiler';
5
+ export class ClickHouseQueryBuilder {
6
+ client;
7
+ _select = null;
8
+ _selectResolver = null;
9
+ _table = null;
10
+ _prewhere = null;
11
+ _sample = null;
12
+ _settings = null;
13
+ _distinct = false;
14
+ _joins = [];
15
+ _arrayJoins = [];
16
+ _ctes = [];
17
+ _where = null;
18
+ _limit = null;
19
+ _offset = null;
20
+ _orderBy = [];
21
+ _groupBy = [];
22
+ _having = null;
23
+ _final = false;
24
+ _windows = {};
25
+ _suggestions = [];
26
+ constructor(client) {
27
+ this.client = client;
28
+ }
29
+ select(fields) {
30
+ if (fields) {
31
+ if (typeof fields === 'function') {
32
+ if (!this._table) {
33
+ this._selectResolver = fields;
34
+ return this;
35
+ }
36
+ this._select = fields(this._table.$columns);
37
+ this._selectResolver = null;
38
+ return this;
39
+ }
40
+ this._select = fields;
41
+ this._selectResolver = null;
42
+ return this;
43
+ }
44
+ return this;
45
+ }
46
+ from(tableOrSubquery, alias = 'subquery') {
47
+ if (tableOrSubquery instanceof ClickHouseQueryBuilder) {
48
+ return this.fromSubquery(tableOrSubquery, alias);
49
+ }
50
+ this._table = tableOrSubquery;
51
+ const defaultFinal = tableOrSubquery.$options?.defaultFinal;
52
+ if (defaultFinal !== undefined) {
53
+ this._final = Boolean(defaultFinal);
54
+ }
55
+ this.resolveSelect();
56
+ return this;
57
+ }
58
+ fromSubquery(subquery, alias) {
59
+ this._table = this.createSubqueryTable(alias, subquery);
60
+ this._final = false;
61
+ this.resolveSelect();
62
+ return this;
63
+ }
64
+ resolveSelect() {
65
+ if (!this._selectResolver)
66
+ return;
67
+ if (!this._table) {
68
+ throw new Error('Call .from() before using callback select');
69
+ }
70
+ this._select = this._selectResolver(this._table.$columns);
71
+ this._selectResolver = null;
72
+ }
73
+ where(expression) {
74
+ if (!expression)
75
+ return this;
76
+ if (typeof expression === 'object' && 'toSQL' in expression) {
77
+ this._where = expression;
78
+ return this;
79
+ }
80
+ if (typeof expression === 'object') {
81
+ const table = this._table;
82
+ if (!table)
83
+ return this;
84
+ const chunks = [];
85
+ const columns = table.$columns || table;
86
+ for (const [key, value] of Object.entries(expression)) {
87
+ const column = table[key] || columns?.[key];
88
+ if (column && value !== undefined) {
89
+ chunks.push(eq(column, value));
90
+ }
91
+ }
92
+ if (chunks.length > 0) {
93
+ const combined = chunks.length === 1 ? chunks[0] : and(...chunks);
94
+ if (combined)
95
+ this._where = combined;
96
+ }
97
+ }
98
+ return this;
99
+ }
100
+ orderBy(col, dir = 'ASC') {
101
+ if (col && typeof col === 'object' && 'col' in col && 'dir' in col) {
102
+ this._orderBy.push(col);
103
+ }
104
+ else {
105
+ this._orderBy.push({ col: col, dir });
106
+ }
107
+ if (this._table && col instanceof ClickHouseColumn) {
108
+ const opts = this._table.$options;
109
+ const orderByOpts = opts?.orderBy;
110
+ if (orderByOpts) {
111
+ const target = (Array.isArray(orderByOpts) ? orderByOpts : [orderByOpts]).map((c) => c.toString().trim());
112
+ if (!target.includes(col.name)) {
113
+ this._suggestions.push(`Ordering by ${col.name} not in physical ORDER BY; may be inefficient.`);
114
+ }
115
+ }
116
+ }
117
+ return this;
118
+ }
119
+ groupBy(...cols) {
120
+ this._groupBy = cols;
121
+ return this;
122
+ }
123
+ having(expression) {
124
+ this._having = expression;
125
+ return this;
126
+ }
127
+ limit(val) {
128
+ this._limit = val;
129
+ return this;
130
+ }
131
+ offset(val) {
132
+ this._offset = val;
133
+ return this;
134
+ }
135
+ distinct() {
136
+ this._distinct = true;
137
+ return this;
138
+ }
139
+ innerJoin(table, on) {
140
+ this._joins.push({ type: 'INNER', table: table.$table, on });
141
+ return this;
142
+ }
143
+ leftJoin(table, on) {
144
+ this._joins.push({ type: 'LEFT', table: table.$table, on });
145
+ return this;
146
+ }
147
+ rightJoin(table, on) {
148
+ this._joins.push({ type: 'RIGHT', table: table.$table, on });
149
+ return this;
150
+ }
151
+ fullJoin(table, on) {
152
+ this._joins.push({ type: 'FULL', table: table.$table, on });
153
+ return this;
154
+ }
155
+ crossJoin(table) {
156
+ this._joins.push({ type: 'CROSS', table: table.$table, on: null });
157
+ return this;
158
+ }
159
+ globalJoin(type, table, on) {
160
+ this._joins.push({ type: `GLOBAL ${type}`, table: table.$table, on });
161
+ return this;
162
+ }
163
+ globalInnerJoin(table, on) {
164
+ return this.globalJoin('INNER', table, on);
165
+ }
166
+ globalLeftJoin(table, on) {
167
+ return this.globalJoin('LEFT', table, on);
168
+ }
169
+ anyJoin(type, table, on) {
170
+ this._joins.push({ type: `ANY ${type}`, table: table.$table, on });
171
+ return this;
172
+ }
173
+ anyInnerJoin(table, on) {
174
+ return this.anyJoin('INNER', table, on);
175
+ }
176
+ anyLeftJoin(table, on) {
177
+ return this.anyJoin('LEFT', table, on);
178
+ }
179
+ allJoin(type, table, on) {
180
+ this._joins.push({ type: `ALL ${type}`, table: table.$table, on });
181
+ return this;
182
+ }
183
+ asofJoin(table, on) {
184
+ this._joins.push({ type: 'LEFT ASOF', table: table.$table, on });
185
+ return this;
186
+ }
187
+ asofInnerJoin(table, on) {
188
+ this._joins.push({ type: 'INNER ASOF', table: table.$table, on });
189
+ return this;
190
+ }
191
+ semiJoin(table, on) {
192
+ this._joins.push({ type: 'LEFT SEMI', table: table.$table, on });
193
+ return this;
194
+ }
195
+ antiJoin(table, on) {
196
+ this._joins.push({ type: 'LEFT ANTI', table: table.$table, on });
197
+ return this;
198
+ }
199
+ globalAsofJoin(table, on) {
200
+ this._joins.push({ type: 'GLOBAL LEFT ASOF', table: table.$table, on });
201
+ return this;
202
+ }
203
+ globalAnyJoin(type, table, on) {
204
+ this._joins.push({ type: `GLOBAL ANY ${type}`, table: table.$table, on });
205
+ return this;
206
+ }
207
+ arrayJoin(column, ...additionalColumns) {
208
+ this._arrayJoins.push({ column });
209
+ for (const col of additionalColumns) {
210
+ this._arrayJoins.push({ column: col });
211
+ }
212
+ return this;
213
+ }
214
+ arrayJoinMultiple(...columns) {
215
+ for (const col of columns) {
216
+ this._arrayJoins.push({ column: col });
217
+ }
218
+ return this;
219
+ }
220
+ arrayJoinAs(column, alias) {
221
+ this._arrayJoins.push({ column, alias });
222
+ return this;
223
+ }
224
+ with(name, query) {
225
+ this._ctes.push({ name, query });
226
+ return this;
227
+ }
228
+ prewhere(expression) {
229
+ this._prewhere = expression;
230
+ return this;
231
+ }
232
+ sample(ratio, offset) {
233
+ this._sample = { ratio, offset };
234
+ return this;
235
+ }
236
+ $with(alias, queryBuilder) {
237
+ const virtualColumns = {};
238
+ const selection = queryBuilder._select;
239
+ const sourceTable = queryBuilder._table;
240
+ if (selection && Object.keys(selection).length > 0) {
241
+ for (const key of Object.keys(selection)) {
242
+ const val = selection[key];
243
+ if (val instanceof ClickHouseColumn) {
244
+ const col = new ClickHouseColumn(key, val.type, val.isNull, val.meta);
245
+ col.tableName = alias;
246
+ virtualColumns[key] = col;
247
+ }
248
+ else {
249
+ const col = new ClickHouseColumn(key, 'String');
250
+ col.tableName = alias;
251
+ virtualColumns[key] = col;
252
+ }
253
+ }
254
+ }
255
+ else if (sourceTable) {
256
+ for (const [key, col] of Object.entries(sourceTable.$columns)) {
257
+ const column = col;
258
+ const clone = new ClickHouseColumn(column.name, column.type, column.isNull, column.meta);
259
+ clone.tableName = alias;
260
+ virtualColumns[key] = clone;
261
+ }
262
+ }
263
+ const cteOptions = { kind: 'cte' };
264
+ const cteTable = {
265
+ $table: alias,
266
+ $columns: virtualColumns,
267
+ $options: cteOptions,
268
+ toSQL: () => ''
269
+ };
270
+ Object.assign(cteTable, virtualColumns);
271
+ this.with(alias, queryBuilder);
272
+ const register = (mainQuery) => {
273
+ mainQuery.with(alias, queryBuilder);
274
+ return mainQuery;
275
+ };
276
+ return { cteTable, register };
277
+ }
278
+ final() {
279
+ this._final = true;
280
+ return this;
281
+ }
282
+ window(name, definition) {
283
+ this._windows[name] = definition;
284
+ return this;
285
+ }
286
+ settings(settings) {
287
+ this._settings = settings;
288
+ return this;
289
+ }
290
+ createSubqueryTable(alias, subquery) {
291
+ const buildVirtualColumns = (aliasName) => {
292
+ const virtualColumns = {};
293
+ const subState = subquery.getState();
294
+ const selection = subState.select;
295
+ const sourceTable = subState.table;
296
+ if (selection && Object.keys(selection).length > 0) {
297
+ for (const [key, val] of Object.entries(selection)) {
298
+ if (val instanceof ClickHouseColumn) {
299
+ const col = new ClickHouseColumn(key, val.type, val.isNull, val.meta);
300
+ col.tableName = aliasName;
301
+ virtualColumns[key] = col;
302
+ }
303
+ else {
304
+ const col = new ClickHouseColumn(key, 'String');
305
+ col.tableName = aliasName;
306
+ virtualColumns[key] = col;
307
+ }
308
+ }
309
+ }
310
+ else if (sourceTable) {
311
+ for (const [key, col] of Object.entries(sourceTable.$columns)) {
312
+ const column = col;
313
+ const clone = new ClickHouseColumn(key, column.type, column.isNull, column.meta);
314
+ clone.tableName = aliasName;
315
+ virtualColumns[key] = clone;
316
+ }
317
+ }
318
+ return virtualColumns;
319
+ };
320
+ const buildTable = (aliasName) => {
321
+ const columns = buildVirtualColumns(aliasName);
322
+ const table = {
323
+ $table: aliasName,
324
+ $columns: columns,
325
+ $options: { kind: 'subquery', subquery },
326
+ toSQL: () => '',
327
+ as: (nextAlias) => buildTable(nextAlias)
328
+ };
329
+ Object.entries(columns).forEach(([key, col]) => { table[key] = col; });
330
+ return table;
331
+ };
332
+ return buildTable(alias);
333
+ }
334
+ getState() {
335
+ this.resolveSelect();
336
+ return {
337
+ select: this._select,
338
+ table: this._table,
339
+ prewhere: this._prewhere,
340
+ sample: this._sample,
341
+ settings: this._settings,
342
+ distinct: this._distinct,
343
+ joins: this._joins,
344
+ arrayJoins: this._arrayJoins,
345
+ ctes: this._ctes,
346
+ where: this._where,
347
+ limit: this._limit,
348
+ offset: this._offset,
349
+ orderBy: this._orderBy,
350
+ groupBy: this._groupBy,
351
+ having: this._having,
352
+ final: this._final,
353
+ windows: this._windows,
354
+ suggestions: this._suggestions
355
+ };
356
+ }
357
+ toSQL() {
358
+ const compiler = new QueryCompiler();
359
+ const { sql, params, suggestions } = compiler.compileSelect(this.getState());
360
+ return { query: sql, params, suggestions };
361
+ }
362
+ async then(onfulfilled, onrejected) {
363
+ const compiler = new QueryCompiler();
364
+ try {
365
+ const { cachedQuery, values } = compiler.compileWithCache(this.getState(), this.client);
366
+ const data = (await cachedQuery.execute(values));
367
+ if (onfulfilled) {
368
+ return Promise.resolve(onfulfilled(data));
369
+ }
370
+ return Promise.resolve(data);
371
+ }
372
+ catch (error) {
373
+ if (onrejected) {
374
+ return Promise.resolve(onrejected(error));
375
+ }
376
+ return Promise.reject(error);
377
+ }
378
+ }
379
+ async *[Symbol.asyncIterator]() {
380
+ const compiler = new QueryCompiler();
381
+ const { cachedQuery, values } = compiler.compileWithCache(this.getState(), this.client);
382
+ const query_params = {};
383
+ for (let i = 0; i < values.length; i++) {
384
+ query_params[`p_${i + 1}`] = values[i];
385
+ }
386
+ const resultSet = await this.client.query({
387
+ query: cachedQuery.sql,
388
+ query_params,
389
+ format: 'JSONEachRow',
390
+ });
391
+ const reader = resultSet.stream();
392
+ let buffer = '';
393
+ for await (const chunk of reader) {
394
+ buffer += chunk.toString();
395
+ const lines = buffer.split('\n');
396
+ buffer = lines.pop() ?? '';
397
+ for (const line of lines) {
398
+ if (line.trim()) {
399
+ try {
400
+ yield JSON.parse(line);
401
+ }
402
+ catch (e) {
403
+ }
404
+ }
405
+ }
406
+ }
407
+ if (buffer.trim()) {
408
+ try {
409
+ yield JSON.parse(buffer);
410
+ }
411
+ catch (e) {
412
+ }
413
+ }
414
+ }
415
+ stream() {
416
+ return this[Symbol.asyncIterator]();
417
+ }
418
+ async native() {
419
+ console.warn('[HouseKit] native() is deprecated. RowBinary streaming is not supported by @clickhouse/client 1.15+. Using JSONEachRow instead.');
420
+ return this.then(data => data);
421
+ }
422
+ async vector() {
423
+ console.warn('[HouseKit] vector() is deprecated. RowBinary streaming is not supported by @clickhouse/client 1.15+. Using JSONEachRow instead.');
424
+ const rows = await this.then(data => data);
425
+ if (rows.length === 0)
426
+ return {};
427
+ const result = {};
428
+ const keys = Object.keys(rows[0]);
429
+ for (const key of keys) {
430
+ result[key] = rows.map((row) => row[key]);
431
+ }
432
+ return result;
433
+ }
434
+ async explain() {
435
+ const { query, params } = this.toSQL();
436
+ const resultSet = await this.client.query({
437
+ query: `EXPLAIN ${query}`,
438
+ query_params: params
439
+ });
440
+ return resultSet.json();
441
+ }
442
+ async explainPipeline() {
443
+ const { query, params } = this.toSQL();
444
+ const resultSet = await this.client.query({
445
+ query: `EXPLAIN PIPELINE ${query}`,
446
+ query_params: params
447
+ });
448
+ return resultSet.json();
449
+ }
450
+ async findFirst() {
451
+ const result = await this.limit(1).then();
452
+ return result.length > 0 ? result[0] : null;
453
+ }
454
+ async findUnique() {
455
+ const result = await this.limit(1).then();
456
+ return result.length > 0 ? result[0] : null;
457
+ }
458
+ async findMany(options) {
459
+ let query = this;
460
+ if (options?.limit) {
461
+ query = query.limit(options.limit);
462
+ }
463
+ if (options?.offset) {
464
+ query = query.offset(options.offset);
465
+ }
466
+ return query.then();
467
+ }
468
+ async findManyCursor(options) {
469
+ let query = this;
470
+ if (options?.limit) {
471
+ query = query.limit(options.limit);
472
+ }
473
+ if (options?.cursor) {
474
+ const { column, value, direction = 'ASC' } = options.cursor;
475
+ const operator = direction === 'ASC' ? '>' : '<';
476
+ query = query.where(sql `${column} ${operator} ${value}`);
477
+ query = query.orderBy(column, direction);
478
+ }
479
+ return query.then();
480
+ }
481
+ async count(table, options) {
482
+ let query;
483
+ if (table) {
484
+ query = new ClickHouseQueryBuilder(this.client)
485
+ .from(table)
486
+ .select({ count: sql `count(*)` });
487
+ }
488
+ else {
489
+ const countQuery = new ClickHouseQueryBuilder(this.client);
490
+ countQuery._table = this._table;
491
+ countQuery._where = this._where;
492
+ countQuery._joins = [...this._joins];
493
+ countQuery._ctes = [...this._ctes];
494
+ countQuery._prewhere = this._prewhere;
495
+ countQuery._sample = this._sample;
496
+ countQuery._distinct = this._distinct;
497
+ countQuery._groupBy = [...this._groupBy];
498
+ countQuery._having = this._having;
499
+ countQuery._final = this._final;
500
+ countQuery._windows = { ...this._windows };
501
+ countQuery._settings = this._settings ? { ...this._settings } : null;
502
+ countQuery._suggestions = [...this._suggestions];
503
+ countQuery._select = { count: sql `count(*)` };
504
+ query = countQuery;
505
+ }
506
+ if (options?.where) {
507
+ query = query.where(options.where);
508
+ }
509
+ const result = await query.limit(1).then();
510
+ return result[0]?.count || 0;
511
+ }
512
+ async exists(options) {
513
+ let query = this.limit(1);
514
+ if (options?.where) {
515
+ query = query.where(options.where);
516
+ }
517
+ const result = await query.then();
518
+ return result.length > 0;
519
+ }
520
+ async findManyWithMeta(options) {
521
+ const limit = options?.limit || 50;
522
+ const offset = options?.offset || 0;
523
+ let query = this.limit(limit + 1).offset(offset);
524
+ const results = await query.then();
525
+ const hasMore = results.length > limit;
526
+ const data = hasMore ? results.slice(0, -1) : results;
527
+ const result = {
528
+ data,
529
+ hasMore
530
+ };
531
+ if (options?.includeTotal) {
532
+ const total = await this.count();
533
+ result.total = total;
534
+ }
535
+ return result;
536
+ }
537
+ async findAsMap(keyColumn, options) {
538
+ const results = await this.findMany(options);
539
+ const map = new Map();
540
+ for (const record of results) {
541
+ const key = record[keyColumn instanceof ClickHouseColumn ? keyColumn.name : 'key'];
542
+ if (key !== undefined && key !== null) {
543
+ map.set(key, record);
544
+ }
545
+ }
546
+ return map;
547
+ }
548
+ async findGrouped(groupColumn, options) {
549
+ const results = await this.findMany(options);
550
+ const grouped = {};
551
+ for (const record of results) {
552
+ const key = record[groupColumn instanceof ClickHouseColumn ? groupColumn.name : 'group'];
553
+ if (key !== undefined && key !== null) {
554
+ if (!grouped[key]) {
555
+ grouped[key] = [];
556
+ }
557
+ grouped[key].push(record);
558
+ }
559
+ }
560
+ return grouped;
561
+ }
562
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,136 @@
1
+ import { eq } from '../expressions';
2
+ import { and } from '../modules/conditional';
3
+ export class ClickHouseUpdateBuilder {
4
+ client;
5
+ table;
6
+ _set = {};
7
+ _where = null;
8
+ _lastMutationId = null;
9
+ constructor(client, table) {
10
+ this.client = client;
11
+ this.table = table;
12
+ }
13
+ set(values) {
14
+ this._set = values;
15
+ return this;
16
+ }
17
+ where(expression) {
18
+ if (!expression)
19
+ return this;
20
+ if (typeof expression === 'object' && 'toSQL' in expression) {
21
+ this._where = expression;
22
+ return this;
23
+ }
24
+ if (typeof expression === 'object') {
25
+ const chunks = [];
26
+ for (const [key, value] of Object.entries(expression)) {
27
+ const column = this.table.$columns[key];
28
+ if (column && value !== undefined) {
29
+ chunks.push(eq(column, value));
30
+ }
31
+ }
32
+ if (chunks.length > 0) {
33
+ const combined = chunks.length === 1 ? chunks[0] : and(...chunks);
34
+ if (combined)
35
+ this._where = combined;
36
+ }
37
+ }
38
+ return this;
39
+ }
40
+ toSQL() {
41
+ if (this.table.$options.appendOnly !== false) {
42
+ throw new Error(`UPDATE is blocked for append-only table ${this.table.$table}. Set appendOnly: false to allow.`);
43
+ }
44
+ if (!this._where) {
45
+ throw new Error("❌ UPDATE requires a WHERE clause in ClickHouse (safety first!)");
46
+ }
47
+ if (Object.keys(this._set).length === 0) {
48
+ throw new Error("❌ UPDATE requires at least one field to set");
49
+ }
50
+ const params = {};
51
+ const setParts = [];
52
+ let paramCounter = 0;
53
+ for (const [key, value] of Object.entries(this._set)) {
54
+ const col = this.table.$columns[key];
55
+ if (!col)
56
+ continue;
57
+ let finalValue = value;
58
+ if (col.meta?.isJson && typeof value === 'object' && value !== null) {
59
+ finalValue = JSON.stringify(value);
60
+ }
61
+ const paramName = `p_${key}_${++paramCounter}`;
62
+ params[paramName] = finalValue;
63
+ setParts.push(`\`${key}\` = {${paramName}:String}`);
64
+ }
65
+ const whereRes = this._where.toSQL({ ignoreTablePrefix: true });
66
+ Object.assign(params, whereRes.params);
67
+ const tableName = this.table.$table;
68
+ return {
69
+ query: `ALTER TABLE \`${tableName}\` UPDATE ${setParts.join(', ')} WHERE ${whereRes.sql}`,
70
+ params
71
+ };
72
+ }
73
+ async execute() {
74
+ const { query, params } = this.toSQL();
75
+ await this.client.command({
76
+ query,
77
+ query_params: params,
78
+ });
79
+ this._lastMutationId = await fetchLatestMutationId(this.client, this.table.$table);
80
+ }
81
+ async wait(options) {
82
+ if (!this._lastMutationId) {
83
+ await this.execute();
84
+ }
85
+ if (!this._lastMutationId)
86
+ return;
87
+ await waitForMutationCompletion(this.client, this.table.$table, this._lastMutationId, options);
88
+ }
89
+ async then(onfulfilled, onrejected) {
90
+ try {
91
+ await this.execute();
92
+ if (onfulfilled) {
93
+ return Promise.resolve(onfulfilled());
94
+ }
95
+ return Promise.resolve();
96
+ }
97
+ catch (error) {
98
+ if (onrejected) {
99
+ return Promise.resolve(onrejected(error));
100
+ }
101
+ return Promise.reject(error);
102
+ }
103
+ }
104
+ }
105
+ async function fetchLatestMutationId(client, tableName) {
106
+ const result = await client.query({
107
+ query: `SELECT mutation_id FROM system.mutations WHERE database = currentDatabase() AND table = {table:String} ORDER BY create_time DESC LIMIT 1 FORMAT JSONEachRow`,
108
+ query_params: { table: tableName }
109
+ });
110
+ const rows = await result.json();
111
+ return rows[0]?.mutation_id ?? null;
112
+ }
113
+ async function waitForMutationCompletion(client, tableName, mutationId, options) {
114
+ const pollInterval = options?.pollIntervalMs ?? 500;
115
+ const timeout = options?.timeoutMs ?? 60_000;
116
+ const start = Date.now();
117
+ while (true) {
118
+ const result = await client.query({
119
+ query: `SELECT is_done, latest_failed_part, latest_fail_reason FROM system.mutations WHERE database = currentDatabase() AND table = {table:String} AND mutation_id = {mid:String} FORMAT JSONEachRow`,
120
+ query_params: { table: tableName, mid: mutationId }
121
+ });
122
+ const rows = await result.json();
123
+ const row = rows[0];
124
+ if (!row)
125
+ return;
126
+ if (row.latest_fail_reason) {
127
+ throw new Error(`Mutation ${mutationId} failed: ${row.latest_fail_reason}`);
128
+ }
129
+ if (row.is_done === 1)
130
+ return;
131
+ if (Date.now() - start > timeout) {
132
+ throw new Error(`Mutation ${mutationId} not completed after ${timeout}ms`);
133
+ }
134
+ await new Promise(res => setTimeout(res, pollInterval));
135
+ }
136
+ }