@hypequery/clickhouse 1.6.2 → 2.0.0

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 (141) hide show
  1. package/README-CLI.md +43 -88
  2. package/README.md +84 -253
  3. package/dist/cli/bin.js +16 -8
  4. package/dist/core/adapters/clickhouse-adapter.d.ts.map +1 -1
  5. package/dist/core/adapters/clickhouse-adapter.js +3 -2
  6. package/dist/core/cache/cache-manager.d.ts.map +1 -1
  7. package/dist/core/cache/cache-manager.js +5 -3
  8. package/dist/core/connection.d.ts +6 -6
  9. package/dist/core/connection.js +9 -9
  10. package/dist/core/cross-filter.js +1 -1
  11. package/dist/core/dialects/clickhouse-dialect.d.ts +2 -2
  12. package/dist/core/dialects/clickhouse-dialect.d.ts.map +1 -1
  13. package/dist/core/dialects/clickhouse-dialect.js +39 -22
  14. package/dist/core/dialects/sql-dialect.d.ts +2 -2
  15. package/dist/core/dialects/sql-dialect.d.ts.map +1 -1
  16. package/dist/core/env/auto-client.d.ts.map +1 -1
  17. package/dist/core/env/auto-client.js +1 -1
  18. package/dist/core/features/aggregations.d.ts +7 -90
  19. package/dist/core/features/aggregations.d.ts.map +1 -1
  20. package/dist/core/features/aggregations.js +19 -7
  21. package/dist/core/features/analytics.d.ts +5 -870
  22. package/dist/core/features/analytics.d.ts.map +1 -1
  23. package/dist/core/features/analytics.js +15 -13
  24. package/dist/core/features/cross-filtering.d.ts +1 -1
  25. package/dist/core/features/cross-filtering.d.ts.map +1 -1
  26. package/dist/core/features/cross-filtering.js +28 -73
  27. package/dist/core/features/executor.d.ts +1 -1
  28. package/dist/core/features/executor.d.ts.map +1 -1
  29. package/dist/core/features/executor.js +9 -11
  30. package/dist/core/features/filtering.d.ts +5 -91
  31. package/dist/core/features/filtering.d.ts.map +1 -1
  32. package/dist/core/features/filtering.js +63 -77
  33. package/dist/core/features/joins.d.ts +2 -19
  34. package/dist/core/features/joins.d.ts.map +1 -1
  35. package/dist/core/features/joins.js +16 -5
  36. package/dist/core/features/query-modifiers.d.ts +10 -109
  37. package/dist/core/features/query-modifiers.d.ts.map +1 -1
  38. package/dist/core/features/query-modifiers.js +64 -18
  39. package/dist/core/formatters/sql-formatter.d.ts +16 -5
  40. package/dist/core/formatters/sql-formatter.d.ts.map +1 -1
  41. package/dist/core/formatters/sql-formatter.js +197 -93
  42. package/dist/core/join-relationships.d.ts +22 -5
  43. package/dist/core/join-relationships.d.ts.map +1 -1
  44. package/dist/core/join-relationships.js +1 -1
  45. package/dist/core/query-builder.d.ts +63 -12
  46. package/dist/core/query-builder.d.ts.map +1 -1
  47. package/dist/core/query-builder.js +210 -153
  48. package/dist/core/query-node.d.ts +7 -0
  49. package/dist/core/query-node.d.ts.map +1 -0
  50. package/dist/core/query-node.js +80 -0
  51. package/dist/core/tests/integration/setup.d.ts +1 -0
  52. package/dist/core/tests/integration/setup.d.ts.map +1 -1
  53. package/dist/core/tests/integration/setup.js +4 -2
  54. package/dist/core/types/select-types.d.ts +3 -0
  55. package/dist/core/types/select-types.d.ts.map +1 -1
  56. package/dist/core/utils/connection-endpoint.d.ts +3 -0
  57. package/dist/core/utils/connection-endpoint.d.ts.map +1 -0
  58. package/dist/core/utils/connection-endpoint.js +9 -0
  59. package/dist/core/utils/filter-application.d.ts +15 -0
  60. package/dist/core/utils/filter-application.d.ts.map +1 -0
  61. package/dist/core/utils/filter-application.js +32 -0
  62. package/dist/core/utils/query-config-compat.d.ts +48 -0
  63. package/dist/core/utils/query-config-compat.d.ts.map +1 -0
  64. package/dist/core/utils/query-config-compat.js +137 -0
  65. package/dist/core/utils/relation-application.d.ts +9 -0
  66. package/dist/core/utils/relation-application.d.ts.map +1 -0
  67. package/dist/core/utils/relation-application.js +19 -0
  68. package/dist/core/utils/relation-validation.d.ts +6 -0
  69. package/dist/core/utils/relation-validation.d.ts.map +1 -0
  70. package/dist/core/utils/relation-validation.js +29 -0
  71. package/dist/core/utils/sql-expressions.d.ts +14 -0
  72. package/dist/core/utils/sql-expressions.d.ts.map +1 -1
  73. package/dist/core/utils/sql-expressions.js +40 -0
  74. package/dist/core/utils/tuple-filter-validation.d.ts +3 -0
  75. package/dist/core/utils/tuple-filter-validation.d.ts.map +1 -0
  76. package/dist/core/utils/tuple-filter-validation.js +16 -0
  77. package/dist/index.d.ts +2 -13
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +3 -8
  80. package/dist/types/base.d.ts +88 -22
  81. package/dist/types/base.d.ts.map +1 -1
  82. package/dist/types/filters.d.ts +9 -5
  83. package/dist/types/filters.d.ts.map +1 -1
  84. package/package.json +5 -5
  85. package/dist/migrations/config/index.d.ts +0 -3
  86. package/dist/migrations/config/index.d.ts.map +0 -1
  87. package/dist/migrations/config/index.js +0 -1
  88. package/dist/migrations/config/types.d.ts +0 -45
  89. package/dist/migrations/config/types.d.ts.map +0 -1
  90. package/dist/migrations/config/types.js +0 -28
  91. package/dist/migrations/diff/diff.d.ts +0 -11
  92. package/dist/migrations/diff/diff.d.ts.map +0 -1
  93. package/dist/migrations/diff/diff.js +0 -240
  94. package/dist/migrations/diff/index.d.ts +0 -3
  95. package/dist/migrations/diff/index.d.ts.map +0 -1
  96. package/dist/migrations/diff/index.js +0 -1
  97. package/dist/migrations/diff/types.d.ts +0 -74
  98. package/dist/migrations/diff/types.d.ts.map +0 -1
  99. package/dist/migrations/diff/types.js +0 -1
  100. package/dist/migrations/plan/index.d.ts +0 -3
  101. package/dist/migrations/plan/index.d.ts.map +0 -1
  102. package/dist/migrations/plan/index.js +0 -1
  103. package/dist/migrations/plan/plan.d.ts +0 -12
  104. package/dist/migrations/plan/plan.d.ts.map +0 -1
  105. package/dist/migrations/plan/plan.js +0 -416
  106. package/dist/migrations/plan/types.d.ts +0 -93
  107. package/dist/migrations/plan/types.d.ts.map +0 -1
  108. package/dist/migrations/plan/types.js +0 -1
  109. package/dist/migrations/schema/column.d.ts +0 -71
  110. package/dist/migrations/schema/column.d.ts.map +0 -1
  111. package/dist/migrations/schema/column.js +0 -123
  112. package/dist/migrations/schema/define.d.ts +0 -24
  113. package/dist/migrations/schema/define.d.ts.map +0 -1
  114. package/dist/migrations/schema/define.js +0 -47
  115. package/dist/migrations/schema/index.d.ts +0 -4
  116. package/dist/migrations/schema/index.d.ts.map +0 -1
  117. package/dist/migrations/schema/index.js +0 -2
  118. package/dist/migrations/schema/types.d.ts +0 -74
  119. package/dist/migrations/schema/types.d.ts.map +0 -1
  120. package/dist/migrations/schema/types.js +0 -1
  121. package/dist/migrations/snapshot/index.d.ts +0 -3
  122. package/dist/migrations/snapshot/index.d.ts.map +0 -1
  123. package/dist/migrations/snapshot/index.js +0 -1
  124. package/dist/migrations/snapshot/serialize.d.ts +0 -21
  125. package/dist/migrations/snapshot/serialize.d.ts.map +0 -1
  126. package/dist/migrations/snapshot/serialize.js +0 -127
  127. package/dist/migrations/snapshot/types.d.ts +0 -47
  128. package/dist/migrations/snapshot/types.d.ts.map +0 -1
  129. package/dist/migrations/snapshot/types.js +0 -1
  130. package/dist/migrations/sql/index.d.ts +0 -4
  131. package/dist/migrations/sql/index.d.ts.map +0 -1
  132. package/dist/migrations/sql/index.js +0 -2
  133. package/dist/migrations/sql/render.d.ts +0 -10
  134. package/dist/migrations/sql/render.d.ts.map +0 -1
  135. package/dist/migrations/sql/render.js +0 -347
  136. package/dist/migrations/sql/types.d.ts +0 -53
  137. package/dist/migrations/sql/types.d.ts.map +0 -1
  138. package/dist/migrations/sql/types.js +0 -1
  139. package/dist/migrations/sql/write.d.ts +0 -10
  140. package/dist/migrations/sql/write.d.ts.map +0 -1
  141. package/dist/migrations/sql/write.js +0 -35
@@ -9,8 +9,22 @@ import { QueryModifiersFeature } from './features/query-modifiers.js';
9
9
  import { FilterValidator } from './validators/filter-validator.js';
10
10
  import { createPredicateBuilder, } from './utils/predicate-builder.js';
11
11
  import { CrossFilteringFeature } from './features/cross-filtering.js';
12
+ import { cloneSelectQueryNode, createSelectQueryNode, transformSelectQueryNode, } from './query-node.js';
12
13
  import { executeWithCache } from './cache/cache-manager.js';
13
14
  import { mergeCacheOptionsPartial, initializeCacheRuntime } from './cache/utils.js';
15
+ import { normalizeFilterApplication } from './utils/filter-application.js';
16
+ import { toLegacyQueryConfig } from './utils/query-config-compat.js';
17
+ import { applyRelationPath, resolveRelationPath } from './utils/relation-application.js';
18
+ const ADVANCED_IN_OPERATORS = new Set([
19
+ 'globalIn',
20
+ 'globalNotIn',
21
+ 'inSubquery',
22
+ 'globalInSubquery',
23
+ 'inTable',
24
+ 'globalInTable',
25
+ 'inTuple',
26
+ 'globalInTuple',
27
+ ]);
14
28
  /**
15
29
  * Type guard to check if a config is a client-based configuration.
16
30
  */
@@ -23,7 +37,7 @@ export function isClientConfig(config) {
23
37
  */
24
38
  export class QueryBuilder {
25
39
  static relationships;
26
- config = {};
40
+ query;
27
41
  tableName;
28
42
  state;
29
43
  aggregations;
@@ -37,8 +51,15 @@ export class QueryBuilder {
37
51
  adapter;
38
52
  dialect;
39
53
  cacheOptions;
54
+ queryTransforms = [];
40
55
  constructor(tableName, state, runtime, adapter, dialect) {
41
56
  this.tableName = tableName;
57
+ this.query = createSelectQueryNode({
58
+ from: {
59
+ kind: 'table',
60
+ name: tableName,
61
+ },
62
+ });
42
63
  this.state = state;
43
64
  this.runtime = runtime;
44
65
  this.adapter = adapter;
@@ -51,31 +72,98 @@ export class QueryBuilder {
51
72
  this.modifiers = new QueryModifiersFeature(this);
52
73
  this.crossFiltering = new CrossFilteringFeature(this);
53
74
  }
54
- fork(state, config) {
75
+ fork(state, query) {
76
+ return this.transition(state, query);
77
+ }
78
+ transition(state, query) {
55
79
  const builder = new QueryBuilder(this.tableName, state, this.runtime, this.adapter, this.dialect);
56
- builder.config = { ...config };
57
- builder.cacheOptions = this.cacheOptions;
80
+ builder.query = cloneSelectQueryNode(query);
81
+ builder.cacheOptions = this.cacheOptions ? { ...this.cacheOptions } : undefined;
82
+ builder.queryTransforms = [...this.queryTransforms];
83
+ return builder;
84
+ }
85
+ cloneMutable() {
86
+ return this.fork(this.state, this.query);
87
+ }
88
+ assignQuery(builder, query) {
89
+ builder.query = cloneSelectQueryNode(query);
58
90
  return builder;
59
91
  }
92
+ updateQuery(updater) {
93
+ return this.assignQuery(this.cloneMutable(), updater(this.query));
94
+ }
95
+ withAliasesState(aliases) {
96
+ return {
97
+ ...this.state,
98
+ aliases,
99
+ };
100
+ }
101
+ buildSelectState(output) {
102
+ return {
103
+ ...this.state,
104
+ output,
105
+ };
106
+ }
107
+ buildSelectQuery(selections) {
108
+ return {
109
+ ...this.query,
110
+ select: selections.map(selection => ({ kind: 'selection', selection })),
111
+ orderBy: this.query.orderBy?.map(({ column, direction }) => ({
112
+ kind: 'order-by-item',
113
+ column: String(column),
114
+ direction,
115
+ })),
116
+ };
117
+ }
118
+ createDetachedBuilder() {
119
+ const builder = new QueryBuilder(this.tableName, this.state, this.runtime, this.adapter, this.dialect);
120
+ builder.query = createSelectQueryNode();
121
+ builder.cacheOptions = this.cacheOptions ? { ...this.cacheOptions } : undefined;
122
+ builder.queryTransforms = [...this.queryTransforms];
123
+ return builder;
124
+ }
125
+ runDraftCallback(seed, callback) {
126
+ let current = seed;
127
+ // Group callbacks expect fluent chaining, so the draft keeps rebinding
128
+ // method calls to the latest immutable builder instance produced so far.
129
+ const draft = new Proxy(seed, {
130
+ get: (_target, prop, receiver) => {
131
+ const value = Reflect.get(current, prop, receiver);
132
+ if (typeof value !== 'function') {
133
+ return value;
134
+ }
135
+ return (...args) => {
136
+ const result = value.apply(current, args);
137
+ if (result instanceof QueryBuilder) {
138
+ current = result;
139
+ return draft;
140
+ }
141
+ return result;
142
+ };
143
+ },
144
+ });
145
+ callback(draft);
146
+ return current;
147
+ }
60
148
  debug() {
61
149
  console.log('Current Type:', {
62
150
  state: this.state,
63
- config: this.config
151
+ query: this.query
64
152
  });
65
153
  return this;
66
154
  }
67
155
  cache(options) {
156
+ const next = this.cloneMutable();
68
157
  if (options === false) {
69
- this.cacheOptions = { mode: 'no-store', ttlMs: 0, staleTtlMs: 0, cacheTimeMs: 0 };
70
- return this;
158
+ next.cacheOptions = { mode: 'no-store', ttlMs: 0, staleTtlMs: 0, cacheTimeMs: 0 };
159
+ return next;
71
160
  }
72
- this.cacheOptions = mergeCacheOptionsPartial(this.cacheOptions, options);
73
- return this;
161
+ next.cacheOptions = mergeCacheOptionsPartial(next.cacheOptions, options);
162
+ return next;
74
163
  }
75
164
  // --- Analytics Helper: Add a CTE.
76
165
  withCTE(alias, subquery) {
77
- this.config = this.analytics.addCTE(alias, subquery);
78
- return this;
166
+ return this.updateQuery(() => this.analytics.addCTE(alias, subquery));
79
167
  }
80
168
  // --- Analytics Helper: Add a scalar WITH alias.
81
169
  withScalar(alias, expressionBuilder) {
@@ -84,17 +172,15 @@ export class QueryBuilder {
84
172
  }
85
173
  const expression = expressionBuilder(createPredicateBuilder());
86
174
  const nextConfig = this.analytics.addScalar(alias, expression);
175
+ const nextScalars = {
176
+ ...this.state.scalars,
177
+ [alias]: undefined,
178
+ };
87
179
  const nextState = {
88
180
  ...this.state,
89
- scalars: {
90
- ...this.state.scalars,
91
- [alias]: undefined,
92
- },
181
+ scalars: nextScalars,
93
182
  };
94
- const builder = new QueryBuilder(this.tableName, nextState, this.runtime, this.adapter, this.dialect);
95
- builder.config = { ...nextConfig };
96
- builder.cacheOptions = this.cacheOptions;
97
- return builder;
183
+ return this.transition(nextState, nextConfig);
98
184
  }
99
185
  /**
100
186
  * Groups results by a time interval using a specified ClickHouse function.
@@ -109,42 +195,35 @@ export class QueryBuilder {
109
195
  * @returns The current QueryBuilder instance.
110
196
  */
111
197
  groupByTimeInterval(column, interval, method = 'toStartOfInterval') {
112
- this.config = this.analytics.addTimeInterval(String(column), interval, method, this.dialect);
113
- return this;
198
+ return this.updateQuery(() => this.analytics.addTimeInterval(String(column), interval, method, this.dialect));
114
199
  }
115
200
  // --- Analytics Helper: Add a raw SQL fragment.
116
201
  raw(sql) {
117
- // Use raw() to inject SQL that isn't supported by the builder.
118
- // Use with caution.
119
- this.config.having = this.config.having || [];
120
- this.config.having.push(sql);
121
- return this;
202
+ return this.updateQuery(query => ({
203
+ ...query,
204
+ having: [...(query.having || []), { kind: 'having', expression: sql }],
205
+ }));
122
206
  }
123
207
  // --- Analytics Helper: Add query settings.
124
208
  settings(opts) {
125
- this.config = this.analytics.addSettings(opts);
126
- return this;
209
+ return this.updateQuery(() => this.analytics.addSettings(opts));
210
+ }
211
+ final() {
212
+ return this.updateQuery(query => ({
213
+ ...query,
214
+ from: {
215
+ kind: 'table',
216
+ name: this.tableName,
217
+ final: true,
218
+ },
219
+ }));
127
220
  }
128
221
  applyCrossFilters(crossFilter) {
129
- const normalized = crossFilter;
130
- this.config = this.crossFiltering.applyCrossFilters(normalized);
131
- return this;
222
+ return this.crossFiltering.applyCrossFilters(crossFilter);
132
223
  }
133
224
  select(columnsOrAsterisk) {
134
225
  if (columnsOrAsterisk === '*') {
135
- const nextState = {
136
- ...this.state,
137
- output: {}
138
- };
139
- const nextConfig = {
140
- ...this.config,
141
- select: ['*'],
142
- orderBy: this.config.orderBy?.map(({ column, direction }) => ({
143
- column: String(column),
144
- direction
145
- }))
146
- };
147
- return this.fork(nextState, nextConfig);
226
+ return this.fork(this.buildSelectState({}), this.buildSelectQuery(['*']));
148
227
  }
149
228
  const columns = columnsOrAsterisk;
150
229
  const processedColumns = columns.map(col => {
@@ -153,19 +232,7 @@ export class QueryBuilder {
153
232
  }
154
233
  return String(col);
155
234
  });
156
- const nextState = {
157
- ...this.state,
158
- output: {}
159
- };
160
- const nextConfig = {
161
- ...this.config,
162
- select: processedColumns,
163
- orderBy: this.config.orderBy?.map(({ column, direction }) => ({
164
- column: String(column),
165
- direction
166
- }))
167
- };
168
- return this.fork(nextState, nextConfig);
235
+ return this.fork(this.buildSelectState({}), this.buildSelectQuery(processedColumns));
169
236
  }
170
237
  selectConst(...columns) {
171
238
  return this.select(columns);
@@ -188,10 +255,7 @@ export class QueryBuilder {
188
255
  applyAggregation(column, alias, suffix, updater) {
189
256
  const columnName = String(column);
190
257
  const finalAlias = (alias || `${columnName}_${suffix}`);
191
- const nextState = {
192
- ...this.state,
193
- output: {}
194
- };
258
+ const nextState = this.buildSelectState({});
195
259
  const nextConfig = updater(columnName, finalAlias);
196
260
  return this.fork(nextState, nextConfig);
197
261
  }
@@ -256,11 +320,7 @@ export class QueryBuilder {
256
320
  return;
257
321
  }
258
322
  // Skip validation for advanced IN operators - they handle their own validation
259
- const advancedInOperators = [
260
- 'globalIn', 'globalNotIn', 'inSubquery', 'globalInSubquery',
261
- 'inTable', 'globalInTable', 'inTuple', 'globalInTuple'
262
- ];
263
- if (advancedInOperators.includes(operator)) {
323
+ if (ADVANCED_IN_OPERATORS.has(operator) || operator === 'isNull' || operator === 'isNotNull') {
264
324
  return;
265
325
  }
266
326
  const columnName = String(column);
@@ -270,46 +330,25 @@ export class QueryBuilder {
270
330
  const columnType = baseColumns[columnName];
271
331
  FilterValidator.validateFilterCondition({ column: columnName, operator, value }, columnType);
272
332
  }
273
- where(columnOrColumns, operator, value) {
274
- if (typeof columnOrColumns === 'function') {
275
- const expression = columnOrColumns(createPredicateBuilder());
276
- this.config = this.filtering.addExpressionCondition('AND', expression);
277
- return this;
278
- }
279
- if (operator === undefined) {
280
- throw new Error('Operator is required when specifying a column for where()');
333
+ applyFilter(clause, conjunction, columnOrColumns, operator, value) {
334
+ const normalized = normalizeFilterApplication(clause, conjunction, columnOrColumns, operator, value, builder => builder(createPredicateBuilder()));
335
+ if (normalized.kind === 'expression') {
336
+ return this.updateQuery(() => this.filtering.addExpressionCondition(clause, conjunction, normalized.expression));
281
337
  }
282
- // Handle tuple operations
283
- if (Array.isArray(columnOrColumns) && (operator === 'inTuple' || operator === 'globalInTuple')) {
284
- const columns = columnOrColumns;
285
- this.validateFilterValue(columns, operator, value);
286
- this.config = this.filtering.addCondition('AND', columns.map(String), operator, value);
287
- return this;
288
- }
289
- const column = columnOrColumns;
290
- this.validateFilterValue(column, operator, value);
291
- this.config = this.filtering.addCondition('AND', String(column), operator, value);
292
- return this;
338
+ this.validateFilterValue(normalized.validationTarget, normalized.operator, normalized.value);
339
+ return this.updateQuery(() => this.filtering.addCondition(clause, conjunction, normalized.column, normalized.operator, normalized.value));
340
+ }
341
+ where(columnOrColumns, operator, value) {
342
+ return this.applyFilter('where', 'AND', columnOrColumns, operator, value);
293
343
  }
294
344
  orWhere(columnOrColumns, operator, value) {
295
- if (typeof columnOrColumns === 'function') {
296
- const expression = columnOrColumns(createPredicateBuilder());
297
- this.config = this.filtering.addExpressionCondition('OR', expression);
298
- return this;
299
- }
300
- if (operator === undefined) {
301
- throw new Error('Operator is required when specifying a column for orWhere()');
302
- }
303
- if (Array.isArray(columnOrColumns) && (operator === 'inTuple' || operator === 'globalInTuple')) {
304
- const columns = columnOrColumns;
305
- this.validateFilterValue(columns, operator, value);
306
- this.config = this.filtering.addCondition('OR', columns.map(String), operator, value);
307
- return this;
308
- }
309
- const column = columnOrColumns;
310
- this.validateFilterValue(column, operator, value);
311
- this.config = this.filtering.addCondition('OR', String(column), operator, value);
312
- return this;
345
+ return this.applyFilter('where', 'OR', columnOrColumns, operator, value);
346
+ }
347
+ prewhere(columnOrColumns, operator, value) {
348
+ return this.applyFilter('prewhere', 'AND', columnOrColumns, operator, value);
349
+ }
350
+ orPrewhere(columnOrColumns, operator, value) {
351
+ return this.applyFilter('prewhere', 'OR', columnOrColumns, operator, value);
313
352
  }
314
353
  /**
315
354
  * Creates a parenthesized group of WHERE conditions joined with AND/OR operators.
@@ -323,10 +362,8 @@ export class QueryBuilder {
323
362
  * ```
324
363
  */
325
364
  whereGroup(callback) {
326
- this.config = this.filtering.startWhereGroup();
327
- callback(this);
328
- this.config = this.filtering.endWhereGroup();
329
- return this;
365
+ const groupBuilder = this.runDraftCallback(this.createDetachedBuilder(), callback);
366
+ return this.updateQuery(() => this.filtering.addGroup('where', 'AND', groupBuilder.getQueryNode().where));
330
367
  }
331
368
  /**
332
369
  * Creates a parenthesized group of WHERE conditions joined with OR operator.
@@ -340,10 +377,8 @@ export class QueryBuilder {
340
377
  * ```
341
378
  */
342
379
  orWhereGroup(callback) {
343
- this.config = this.filtering.startOrWhereGroup();
344
- callback(this);
345
- this.config = this.filtering.endWhereGroup();
346
- return this;
380
+ const groupBuilder = this.runDraftCallback(this.createDetachedBuilder(), callback);
381
+ return this.updateQuery(() => this.filtering.addGroup('where', 'OR', groupBuilder.getQueryNode().where));
347
382
  }
348
383
  /**
349
384
  * Adds a GROUP BY clause.
@@ -356,16 +391,23 @@ export class QueryBuilder {
356
391
  */
357
392
  groupBy(columns) {
358
393
  const normalized = Array.isArray(columns) ? columns.map(String) : String(columns);
359
- this.config = this.modifiers.addGroupBy(normalized);
360
- return this;
394
+ return this.updateQuery(() => this.modifiers.addGroupBy(normalized));
395
+ }
396
+ arrayJoin(column) {
397
+ return this.updateQuery(() => this.modifiers.addArrayJoin('ARRAY', String(column)));
398
+ }
399
+ leftArrayJoin(column) {
400
+ return this.updateQuery(() => this.modifiers.addArrayJoin('LEFT ARRAY', String(column)));
361
401
  }
362
402
  limit(count) {
363
- this.config = this.modifiers.addLimit(count);
364
- return this;
403
+ return this.updateQuery(() => this.modifiers.addLimit(count));
404
+ }
405
+ limitBy(count, by) {
406
+ const normalized = Array.isArray(by) ? by.map(String) : String(by);
407
+ return this.updateQuery(() => this.modifiers.addLimitBy(count, normalized));
365
408
  }
366
409
  offset(count) {
367
- this.config = this.modifiers.addOffset(count);
368
- return this;
410
+ return this.updateQuery(() => this.modifiers.addOffset(count));
369
411
  }
370
412
  /**
371
413
  * Adds an ORDER BY clause.
@@ -378,8 +420,7 @@ export class QueryBuilder {
378
420
  * ```
379
421
  */
380
422
  orderBy(column, direction = 'ASC') {
381
- this.config = this.modifiers.addOrderBy(String(column), direction);
382
- return this;
423
+ return this.updateQuery(() => this.modifiers.addOrderBy(String(column), direction));
383
424
  }
384
425
  /**
385
426
  * Adds a HAVING clause for filtering grouped results.
@@ -391,12 +432,31 @@ export class QueryBuilder {
391
432
  * ```
392
433
  */
393
434
  having(condition, parameters) {
394
- this.config = this.modifiers.addHaving(condition, parameters);
395
- return this;
435
+ return this.updateQuery(() => this.modifiers.addHaving(condition, parameters));
396
436
  }
397
437
  distinct() {
398
- this.config = this.modifiers.setDistinct();
399
- return this;
438
+ return this.updateQuery(() => this.modifiers.setDistinct());
439
+ }
440
+ withTotals() {
441
+ return this.updateQuery(() => this.modifiers.setWithTotals());
442
+ }
443
+ whereNull(column) {
444
+ return this.updateQuery(() => this.filtering.addCondition('where', 'AND', String(column), 'isNull', null));
445
+ }
446
+ whereNotNull(column) {
447
+ return this.updateQuery(() => this.filtering.addCondition('where', 'AND', String(column), 'isNotNull', null));
448
+ }
449
+ orWhereNull(column) {
450
+ return this.updateQuery(() => this.filtering.addCondition('where', 'OR', String(column), 'isNull', null));
451
+ }
452
+ orWhereNotNull(column) {
453
+ return this.updateQuery(() => this.filtering.addCondition('where', 'OR', String(column), 'isNotNull', null));
454
+ }
455
+ prewhereNull(column) {
456
+ return this.updateQuery(() => this.filtering.addCondition('prewhere', 'AND', String(column), 'isNull', null));
457
+ }
458
+ prewhereNotNull(column) {
459
+ return this.updateQuery(() => this.filtering.addCondition('prewhere', 'AND', String(column), 'isNotNull', null));
400
460
  }
401
461
  whereBetween(column, [min, max]) {
402
462
  if (min === null || max === null) {
@@ -417,46 +477,43 @@ export class QueryBuilder {
417
477
  return this.applyJoin('FULL', table, leftColumn, rightColumn, alias);
418
478
  }
419
479
  applyJoin(type, table, leftColumn, rightColumn, alias) {
420
- const nextState = {
421
- ...this.state,
422
- aliases: alias ? { ...this.state.aliases, [alias]: table } : this.state.aliases
423
- };
480
+ const nextAliases = (alias
481
+ ? { ...this.state.aliases, [alias]: table }
482
+ : this.state.aliases);
483
+ const nextState = this.withAliasesState(nextAliases);
424
484
  const nextConfig = this.joins.addJoin(type, table, String(leftColumn), rightColumn, alias);
425
- return this.fork(nextState, nextConfig);
485
+ return this.transition(nextState, nextConfig);
426
486
  }
427
- // Make config accessible to features
487
+ /**
488
+ * @deprecated Prefer `getQueryNode()` for inspection or `toQueryNode()` for the
489
+ * transformed query tree used during compilation.
490
+ */
428
491
  getConfig() {
429
- return this.config;
492
+ return toLegacyQueryConfig(this.getQueryNode());
493
+ }
494
+ toQueryNode() {
495
+ return transformSelectQueryNode(this.query, this.queryTransforms);
496
+ }
497
+ getQueryNode() {
498
+ return cloneSelectQueryNode(this.query);
430
499
  }
431
500
  static setJoinRelationships(relationships) {
432
501
  this.relationships = relationships;
433
502
  }
434
- /**
435
- * Apply a predefined join relationship
436
- */
437
- withRelation(name, options) {
438
- const relationships = QueryBuilder.relationships;
439
- if (!relationships) {
440
- throw new Error('Join relationships have not been initialized. Call QueryBuilder.setJoinRelationships first.');
441
- }
442
- const path = relationships.get(name);
443
- if (!path) {
444
- throw new Error(`Join relationship '${name}' not found`);
445
- }
446
- const applyJoin = (joinPath) => {
503
+ withRelation(nameOrPath, options) {
504
+ const next = this.cloneMutable();
505
+ const { path, label } = resolveRelationPath(nameOrPath, QueryBuilder.relationships);
506
+ next.query = applyRelationPath(next.query, path, options, (currentQuery, joinPath, relationOptions) => {
507
+ next.query = currentQuery;
447
508
  const type = options?.type || joinPath.type || 'INNER';
448
- const alias = options?.alias || joinPath.alias;
509
+ const alias = relationOptions?.alias || joinPath.alias;
510
+ const leftColumn = String(joinPath.leftColumn);
511
+ const leftSource = String(joinPath.from);
449
512
  const table = String(joinPath.to);
450
513
  const rightColumn = `${table}.${joinPath.rightColumn}`;
451
- this.config = this.joins.addJoin(type, table, joinPath.leftColumn, rightColumn, alias);
452
- };
453
- if (Array.isArray(path)) {
454
- path.forEach(applyJoin);
455
- }
456
- else {
457
- applyJoin(path);
458
- }
459
- return this;
514
+ return next.joins.addJoin(type, table, leftColumn, rightColumn, alias, leftSource);
515
+ }, label);
516
+ return next;
460
517
  }
461
518
  }
462
519
  export function createQueryBuilder(config) {
@@ -0,0 +1,7 @@
1
+ import type { ExprNode, QueryConfig, SelectQueryNode } from '../types/index.js';
2
+ export declare function cloneExprNode(expr?: ExprNode): ExprNode | undefined;
3
+ export declare function createSelectQueryNode<TOutput, TSchema>(config?: QueryConfig<TOutput, TSchema>): SelectQueryNode<TOutput, TSchema>;
4
+ export declare function cloneSelectQueryNode<TOutput, TSchema>(query: SelectQueryNode<TOutput, TSchema>): SelectQueryNode<TOutput, TSchema>;
5
+ export type QueryNodeTransform<TOutput, TSchema> = (query: SelectQueryNode<TOutput, TSchema>) => SelectQueryNode<TOutput, TSchema>;
6
+ export declare function transformSelectQueryNode<TOutput, TSchema>(query: SelectQueryNode<TOutput, TSchema>, transforms: ReadonlyArray<QueryNodeTransform<TOutput, TSchema>>): SelectQueryNode<TOutput, TSchema>;
7
+ //# sourceMappingURL=query-node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-node.d.ts","sourceRoot":"","sources":["../../src/core/query-node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,QAAQ,EACR,WAAW,EACX,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAY3B,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAgCnE;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EACpD,MAAM,GAAE,WAAW,CAAC,OAAO,EAAE,OAAO,CAAM,GACzC,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,CA+BnC;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EACnD,KAAK,EAAE,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,GACvC,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,CAEnC;AAED,MAAM,MAAM,kBAAkB,CAAC,OAAO,EAAE,OAAO,IAAI,CACjD,KAAK,EAAE,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,KACrC,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAEvC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,EACvD,KAAK,EAAE,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,EACxC,UAAU,EAAE,aAAa,CAAC,kBAAkB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,GAC9D,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,CAKnC"}
@@ -0,0 +1,80 @@
1
+ function cloneConditionValue(value) {
2
+ if (typeof value === 'string') {
3
+ return value;
4
+ }
5
+ if (Array.isArray(value)) {
6
+ return value.map(item => Array.isArray(item) ? item.map(inner => ({ ...inner })) : { ...item });
7
+ }
8
+ return { ...value };
9
+ }
10
+ export function cloneExprNode(expr) {
11
+ if (!expr)
12
+ return undefined;
13
+ switch (expr.kind) {
14
+ case 'condition':
15
+ return { ...expr, value: cloneConditionValue(expr.value) };
16
+ case 'raw':
17
+ return {
18
+ ...expr,
19
+ parameters: expr.parameters.map(parameter => ({ ...parameter })),
20
+ };
21
+ case 'group':
22
+ return {
23
+ ...expr,
24
+ expression: cloneExprNode(expr.expression),
25
+ };
26
+ case 'logical':
27
+ return {
28
+ ...expr,
29
+ conditions: expr.conditions.map(condition => cloneExprNode(condition)),
30
+ };
31
+ case 'sequence':
32
+ return {
33
+ ...expr,
34
+ items: expr.items.map(item => ({
35
+ ...item,
36
+ expression: cloneExprNode(item.expression),
37
+ })),
38
+ };
39
+ default:
40
+ throw new Error(`Unsupported expression kind: ${String(expr.kind)}`);
41
+ }
42
+ }
43
+ export function createSelectQueryNode(config = {}) {
44
+ return {
45
+ kind: 'select-query',
46
+ from: config.from ? { ...config.from } : undefined,
47
+ select: config.select ? config.select.map(item => ({ ...item })) : undefined,
48
+ arrayJoins: config.arrayJoins ? config.arrayJoins.map(item => ({ ...item })) : undefined,
49
+ prewhere: cloneExprNode(config.prewhere),
50
+ where: cloneExprNode(config.where),
51
+ groupBy: config.groupBy ? config.groupBy.map(item => ({ ...item })) : undefined,
52
+ withTotals: config.withTotals,
53
+ having: config.having
54
+ ? config.having.map(item => ({
55
+ ...item,
56
+ parameters: item.parameters?.map(parameter => ({ ...parameter })),
57
+ }))
58
+ : undefined,
59
+ limitBy: config.limitBy
60
+ ? {
61
+ ...config.limitBy,
62
+ by: [...config.limitBy.by],
63
+ }
64
+ : undefined,
65
+ limit: config.limit,
66
+ offset: config.offset,
67
+ distinct: config.distinct,
68
+ orderBy: config.orderBy ? config.orderBy.map(item => ({ ...item })) : undefined,
69
+ joins: config.joins ? config.joins.map(item => ({ ...item })) : undefined,
70
+ ctes: config.ctes ? config.ctes.map(item => ({ ...item })) : undefined,
71
+ unionQueries: config.unionQueries ? [...config.unionQueries] : undefined,
72
+ settings: config.settings ? { ...config.settings } : undefined,
73
+ };
74
+ }
75
+ export function cloneSelectQueryNode(query) {
76
+ return createSelectQueryNode(query);
77
+ }
78
+ export function transformSelectQueryNode(query, transforms) {
79
+ return transforms.reduce((current, transform) => transform(current), cloneSelectQueryNode(query));
80
+ }
@@ -27,6 +27,7 @@ export interface TestSchemaType {
27
27
  price: number;
28
28
  created_at: string;
29
29
  is_active: boolean;
30
+ tags: string[];
30
31
  }>;
31
32
  users: Array<{
32
33
  id: number;
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../../../src/core/tests/integration/setup.ts"],"names":[],"mappings":"AAgDA,eAAO,MAAM,sBAAsB;;;;;CAAS,CAAC;AAG7C,eAAO,MAAM,wBAAwB;;;;;;EAyBpC,CAAC;AAGF,eAAO,MAAM,2BAA2B,yGAevC,CAAC;AAGF,eAAO,MAAM,iBAAiB,QAAa,OAAO,CAAC,OAAO,CAOzD,CAAC;AAGF,eAAO,MAAM,wBAAwB,QAAa,OAAO,CAAC,OAAO,CAahE,CAAC;AAGF,eAAO,MAAM,kBAAkB,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,OAAO,CAO/E,CAAC;AAGF,eAAO,MAAM,iBAAiB,QAAa,OAAO,CAAC,OAAO,CAQzD,CAAC;AAGF,eAAO,MAAM,wBAAwB,QAAa,OAAO,CAAC,IAAI,CAwC7D,CAAC;AAGF,eAAO,MAAM,iBAAiB,GAC5B,oBAAgB,EAChB,sBAAoB,KACnB,OAAO,CAAC,IAAI,CAad,CAAC;AAGF,eAAO,MAAM,uBAAuB,QAAa,OAAO,CAAC,IAAI,CA0B5D,CAAC;AAGF,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,KAAK,CAAC;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,OAAO,CAAC;KACpB,CAAC,CAAC;IACH,KAAK,EAAE,KAAK,CAAC;QACX,EAAE,EAAE,MAAM,CAAC;QACX,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;IACH,MAAM,EAAE,KAAK,CAAC;QACZ,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;CACJ;AAkDD,eAAO,MAAM,SAAS,EAAE,cAAoC,CAAC;AAK7D,eAAO,MAAM,iBAAiB,QAAa,OAAO,CAAC,IAAI,CAkGtD,CAAC"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../../../src/core/tests/integration/setup.ts"],"names":[],"mappings":"AAgDA,eAAO,MAAM,sBAAsB;;;;;CAAS,CAAC;AAG7C,eAAO,MAAM,wBAAwB;;;;;;EAyBpC,CAAC;AAGF,eAAO,MAAM,2BAA2B,yGAevC,CAAC;AAGF,eAAO,MAAM,iBAAiB,QAAa,OAAO,CAAC,OAAO,CAOzD,CAAC;AAGF,eAAO,MAAM,wBAAwB,QAAa,OAAO,CAAC,OAAO,CAahE,CAAC;AAGF,eAAO,MAAM,kBAAkB,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,OAAO,CAO/E,CAAC;AAGF,eAAO,MAAM,iBAAiB,QAAa,OAAO,CAAC,OAAO,CAQzD,CAAC;AAGF,eAAO,MAAM,wBAAwB,QAAa,OAAO,CAAC,IAAI,CAwC7D,CAAC;AAGF,eAAO,MAAM,iBAAiB,GAC5B,oBAAgB,EAChB,sBAAoB,KACnB,OAAO,CAAC,IAAI,CAad,CAAC;AAGF,eAAO,MAAM,uBAAuB,QAAa,OAAO,CAAC,IAAI,CA0B5D,CAAC;AAGF,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,KAAK,CAAC;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,OAAO,CAAC;QACnB,IAAI,EAAE,MAAM,EAAE,CAAC;KAChB,CAAC,CAAC;IACH,KAAK,EAAE,KAAK,CAAC;QACX,EAAE,EAAE,MAAM,CAAC;QACX,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;IACH,MAAM,EAAE,KAAK,CAAC;QACZ,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;CACJ;AAmDD,eAAO,MAAM,SAAS,EAAE,cAAoC,CAAC;AAK7D,eAAO,MAAM,iBAAiB,QAAa,OAAO,CAAC,IAAI,CAmGtD,CAAC"}
@@ -234,7 +234,8 @@ function normalizeTestData() {
234
234
  category: row.category,
235
235
  price: row.price,
236
236
  created_at: normalizeDateValue(row.created_at),
237
- is_active: row.is_active
237
+ is_active: row.is_active,
238
+ tags: row.tags ?? [],
238
239
  }));
239
240
  const users = (rawTestData.users ?? []).map(row => ({
240
241
  id: row.id,
@@ -285,7 +286,8 @@ export const setupTestDatabase = async () => {
285
286
  category String,
286
287
  price Float64,
287
288
  created_at Date,
288
- is_active Boolean
289
+ is_active Boolean,
290
+ tags Array(String)
289
291
  ) ENGINE = MergeTree()
290
292
  ORDER BY id
291
293
  `
@@ -18,6 +18,9 @@ export type SelectableItem<State extends AnyBuilderState> = SelectableColumn<Sta
18
18
  export type ColumnSelectionKey<P> = P extends `${string}.${infer C}` ? C : P;
19
19
  type QualifiedColumnValue<State extends AnyBuilderState, P> = P extends `${infer Table}.${infer Column}` ? ResolveTableSchema<State, Table> extends Record<string, ColumnType> ? Column extends keyof ResolveTableSchema<State, Table> ? ResolveTableSchema<State, Table>[Column] extends ColumnType ? InferColumnType<ResolveTableSchema<State, Table>[Column]> : never : never : never : never;
20
20
  export type ColumnSelectionValue<State extends AnyBuilderState, P> = P extends OutputColumnKeys<State> ? State['output'][P] : P extends ScalarColumnKeys<State> ? State['scalars'][P] : P extends BaseColumnKeys<State> ? BaseRow<State>[P] : QualifiedColumnValue<State, P>;
21
+ export type ArraySelectableColumn<State extends AnyBuilderState> = {
22
+ [P in SelectableColumn<State>]: ColumnSelectionValue<State, P> extends readonly unknown[] | null ? P : never;
23
+ }[SelectableColumn<State>];
21
24
  export type ColumnSelectionRecord<State extends AnyBuilderState, K> = {
22
25
  [P in Extract<K, SelectableColumn<State>> as ColumnSelectionKey<P>]: ColumnSelectionValue<State, P>;
23
26
  };