@hypequery/clickhouse 1.6.2 → 2.0.1

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 (142) 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 +27 -84
  19. package/dist/core/features/aggregations.d.ts.map +1 -1
  20. package/dist/core/features/aggregations.js +59 -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 +64 -12
  46. package/dist/core/query-builder.d.ts.map +1 -1
  47. package/dist/core/query-builder.js +213 -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 +3 -10
  52. package/dist/core/tests/integration/setup.d.ts.map +1 -1
  53. package/dist/core/tests/integration/setup.js +30 -249
  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 +89 -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/core/tests/integration/test-data.json +0 -190
  86. package/dist/migrations/config/index.d.ts +0 -3
  87. package/dist/migrations/config/index.d.ts.map +0 -1
  88. package/dist/migrations/config/index.js +0 -1
  89. package/dist/migrations/config/types.d.ts +0 -45
  90. package/dist/migrations/config/types.d.ts.map +0 -1
  91. package/dist/migrations/config/types.js +0 -28
  92. package/dist/migrations/diff/diff.d.ts +0 -11
  93. package/dist/migrations/diff/diff.d.ts.map +0 -1
  94. package/dist/migrations/diff/diff.js +0 -240
  95. package/dist/migrations/diff/index.d.ts +0 -3
  96. package/dist/migrations/diff/index.d.ts.map +0 -1
  97. package/dist/migrations/diff/index.js +0 -1
  98. package/dist/migrations/diff/types.d.ts +0 -74
  99. package/dist/migrations/diff/types.d.ts.map +0 -1
  100. package/dist/migrations/diff/types.js +0 -1
  101. package/dist/migrations/plan/index.d.ts +0 -3
  102. package/dist/migrations/plan/index.d.ts.map +0 -1
  103. package/dist/migrations/plan/index.js +0 -1
  104. package/dist/migrations/plan/plan.d.ts +0 -12
  105. package/dist/migrations/plan/plan.d.ts.map +0 -1
  106. package/dist/migrations/plan/plan.js +0 -416
  107. package/dist/migrations/plan/types.d.ts +0 -93
  108. package/dist/migrations/plan/types.d.ts.map +0 -1
  109. package/dist/migrations/plan/types.js +0 -1
  110. package/dist/migrations/schema/column.d.ts +0 -71
  111. package/dist/migrations/schema/column.d.ts.map +0 -1
  112. package/dist/migrations/schema/column.js +0 -123
  113. package/dist/migrations/schema/define.d.ts +0 -24
  114. package/dist/migrations/schema/define.d.ts.map +0 -1
  115. package/dist/migrations/schema/define.js +0 -47
  116. package/dist/migrations/schema/index.d.ts +0 -4
  117. package/dist/migrations/schema/index.d.ts.map +0 -1
  118. package/dist/migrations/schema/index.js +0 -2
  119. package/dist/migrations/schema/types.d.ts +0 -74
  120. package/dist/migrations/schema/types.d.ts.map +0 -1
  121. package/dist/migrations/schema/types.js +0 -1
  122. package/dist/migrations/snapshot/index.d.ts +0 -3
  123. package/dist/migrations/snapshot/index.d.ts.map +0 -1
  124. package/dist/migrations/snapshot/index.js +0 -1
  125. package/dist/migrations/snapshot/serialize.d.ts +0 -21
  126. package/dist/migrations/snapshot/serialize.d.ts.map +0 -1
  127. package/dist/migrations/snapshot/serialize.js +0 -127
  128. package/dist/migrations/snapshot/types.d.ts +0 -47
  129. package/dist/migrations/snapshot/types.d.ts.map +0 -1
  130. package/dist/migrations/snapshot/types.js +0 -1
  131. package/dist/migrations/sql/index.d.ts +0 -4
  132. package/dist/migrations/sql/index.d.ts.map +0 -1
  133. package/dist/migrations/sql/index.js +0 -2
  134. package/dist/migrations/sql/render.d.ts +0 -10
  135. package/dist/migrations/sql/render.d.ts.map +0 -1
  136. package/dist/migrations/sql/render.js +0 -347
  137. package/dist/migrations/sql/types.d.ts +0 -53
  138. package/dist/migrations/sql/types.d.ts.map +0 -1
  139. package/dist/migrations/sql/types.js +0 -1
  140. package/dist/migrations/sql/write.d.ts +0 -10
  141. package/dist/migrations/sql/write.d.ts.map +0 -1
  142. 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);
@@ -185,13 +252,13 @@ export class QueryBuilder {
185
252
  max(column, alias) {
186
253
  return this.applyAggregation(column, alias, 'max', (col, finalAlias) => this.aggregations.max(col, finalAlias));
187
254
  }
255
+ countDistinct(column, alias) {
256
+ return this.applyAggregation(column, alias, 'countDistinct', (col, finalAlias) => this.aggregations.countDistinct(col, finalAlias));
257
+ }
188
258
  applyAggregation(column, alias, suffix, updater) {
189
259
  const columnName = String(column);
190
260
  const finalAlias = (alias || `${columnName}_${suffix}`);
191
- const nextState = {
192
- ...this.state,
193
- output: {}
194
- };
261
+ const nextState = this.buildSelectState({});
195
262
  const nextConfig = updater(columnName, finalAlias);
196
263
  return this.fork(nextState, nextConfig);
197
264
  }
@@ -256,11 +323,7 @@ export class QueryBuilder {
256
323
  return;
257
324
  }
258
325
  // 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)) {
326
+ if (ADVANCED_IN_OPERATORS.has(operator) || operator === 'isNull' || operator === 'isNotNull') {
264
327
  return;
265
328
  }
266
329
  const columnName = String(column);
@@ -270,46 +333,25 @@ export class QueryBuilder {
270
333
  const columnType = baseColumns[columnName];
271
334
  FilterValidator.validateFilterCondition({ column: columnName, operator, value }, columnType);
272
335
  }
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;
336
+ applyFilter(clause, conjunction, columnOrColumns, operator, value) {
337
+ const normalized = normalizeFilterApplication(clause, conjunction, columnOrColumns, operator, value, builder => builder(createPredicateBuilder()));
338
+ if (normalized.kind === 'expression') {
339
+ return this.updateQuery(() => this.filtering.addExpressionCondition(clause, conjunction, normalized.expression));
278
340
  }
279
- if (operator === undefined) {
280
- throw new Error('Operator is required when specifying a column for where()');
281
- }
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;
341
+ this.validateFilterValue(normalized.validationTarget, normalized.operator, normalized.value);
342
+ return this.updateQuery(() => this.filtering.addCondition(clause, conjunction, normalized.column, normalized.operator, normalized.value));
343
+ }
344
+ where(columnOrColumns, operator, value) {
345
+ return this.applyFilter('where', 'AND', columnOrColumns, operator, value);
293
346
  }
294
347
  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;
348
+ return this.applyFilter('where', 'OR', columnOrColumns, operator, value);
349
+ }
350
+ prewhere(columnOrColumns, operator, value) {
351
+ return this.applyFilter('prewhere', 'AND', columnOrColumns, operator, value);
352
+ }
353
+ orPrewhere(columnOrColumns, operator, value) {
354
+ return this.applyFilter('prewhere', 'OR', columnOrColumns, operator, value);
313
355
  }
314
356
  /**
315
357
  * Creates a parenthesized group of WHERE conditions joined with AND/OR operators.
@@ -323,10 +365,8 @@ export class QueryBuilder {
323
365
  * ```
324
366
  */
325
367
  whereGroup(callback) {
326
- this.config = this.filtering.startWhereGroup();
327
- callback(this);
328
- this.config = this.filtering.endWhereGroup();
329
- return this;
368
+ const groupBuilder = this.runDraftCallback(this.createDetachedBuilder(), callback);
369
+ return this.updateQuery(() => this.filtering.addGroup('where', 'AND', groupBuilder.getQueryNode().where));
330
370
  }
331
371
  /**
332
372
  * Creates a parenthesized group of WHERE conditions joined with OR operator.
@@ -340,10 +380,8 @@ export class QueryBuilder {
340
380
  * ```
341
381
  */
342
382
  orWhereGroup(callback) {
343
- this.config = this.filtering.startOrWhereGroup();
344
- callback(this);
345
- this.config = this.filtering.endWhereGroup();
346
- return this;
383
+ const groupBuilder = this.runDraftCallback(this.createDetachedBuilder(), callback);
384
+ return this.updateQuery(() => this.filtering.addGroup('where', 'OR', groupBuilder.getQueryNode().where));
347
385
  }
348
386
  /**
349
387
  * Adds a GROUP BY clause.
@@ -356,16 +394,23 @@ export class QueryBuilder {
356
394
  */
357
395
  groupBy(columns) {
358
396
  const normalized = Array.isArray(columns) ? columns.map(String) : String(columns);
359
- this.config = this.modifiers.addGroupBy(normalized);
360
- return this;
397
+ return this.updateQuery(() => this.modifiers.addGroupBy(normalized));
398
+ }
399
+ arrayJoin(column) {
400
+ return this.updateQuery(() => this.modifiers.addArrayJoin('ARRAY', String(column)));
401
+ }
402
+ leftArrayJoin(column) {
403
+ return this.updateQuery(() => this.modifiers.addArrayJoin('LEFT ARRAY', String(column)));
361
404
  }
362
405
  limit(count) {
363
- this.config = this.modifiers.addLimit(count);
364
- return this;
406
+ return this.updateQuery(() => this.modifiers.addLimit(count));
407
+ }
408
+ limitBy(count, by) {
409
+ const normalized = Array.isArray(by) ? by.map(String) : String(by);
410
+ return this.updateQuery(() => this.modifiers.addLimitBy(count, normalized));
365
411
  }
366
412
  offset(count) {
367
- this.config = this.modifiers.addOffset(count);
368
- return this;
413
+ return this.updateQuery(() => this.modifiers.addOffset(count));
369
414
  }
370
415
  /**
371
416
  * Adds an ORDER BY clause.
@@ -378,8 +423,7 @@ export class QueryBuilder {
378
423
  * ```
379
424
  */
380
425
  orderBy(column, direction = 'ASC') {
381
- this.config = this.modifiers.addOrderBy(String(column), direction);
382
- return this;
426
+ return this.updateQuery(() => this.modifiers.addOrderBy(String(column), direction));
383
427
  }
384
428
  /**
385
429
  * Adds a HAVING clause for filtering grouped results.
@@ -391,12 +435,31 @@ export class QueryBuilder {
391
435
  * ```
392
436
  */
393
437
  having(condition, parameters) {
394
- this.config = this.modifiers.addHaving(condition, parameters);
395
- return this;
438
+ return this.updateQuery(() => this.modifiers.addHaving(condition, parameters));
396
439
  }
397
440
  distinct() {
398
- this.config = this.modifiers.setDistinct();
399
- return this;
441
+ return this.updateQuery(() => this.modifiers.setDistinct());
442
+ }
443
+ withTotals() {
444
+ return this.updateQuery(() => this.modifiers.setWithTotals());
445
+ }
446
+ whereNull(column) {
447
+ return this.updateQuery(() => this.filtering.addCondition('where', 'AND', String(column), 'isNull', null));
448
+ }
449
+ whereNotNull(column) {
450
+ return this.updateQuery(() => this.filtering.addCondition('where', 'AND', String(column), 'isNotNull', null));
451
+ }
452
+ orWhereNull(column) {
453
+ return this.updateQuery(() => this.filtering.addCondition('where', 'OR', String(column), 'isNull', null));
454
+ }
455
+ orWhereNotNull(column) {
456
+ return this.updateQuery(() => this.filtering.addCondition('where', 'OR', String(column), 'isNotNull', null));
457
+ }
458
+ prewhereNull(column) {
459
+ return this.updateQuery(() => this.filtering.addCondition('prewhere', 'AND', String(column), 'isNull', null));
460
+ }
461
+ prewhereNotNull(column) {
462
+ return this.updateQuery(() => this.filtering.addCondition('prewhere', 'AND', String(column), 'isNotNull', null));
400
463
  }
401
464
  whereBetween(column, [min, max]) {
402
465
  if (min === null || max === null) {
@@ -417,46 +480,43 @@ export class QueryBuilder {
417
480
  return this.applyJoin('FULL', table, leftColumn, rightColumn, alias);
418
481
  }
419
482
  applyJoin(type, table, leftColumn, rightColumn, alias) {
420
- const nextState = {
421
- ...this.state,
422
- aliases: alias ? { ...this.state.aliases, [alias]: table } : this.state.aliases
423
- };
483
+ const nextAliases = (alias
484
+ ? { ...this.state.aliases, [alias]: table }
485
+ : this.state.aliases);
486
+ const nextState = this.withAliasesState(nextAliases);
424
487
  const nextConfig = this.joins.addJoin(type, table, String(leftColumn), rightColumn, alias);
425
- return this.fork(nextState, nextConfig);
488
+ return this.transition(nextState, nextConfig);
426
489
  }
427
- // Make config accessible to features
490
+ /**
491
+ * @deprecated Prefer `getQueryNode()` for inspection or `toQueryNode()` for the
492
+ * transformed query tree used during compilation.
493
+ */
428
494
  getConfig() {
429
- return this.config;
495
+ return toLegacyQueryConfig(this.getQueryNode());
496
+ }
497
+ toQueryNode() {
498
+ return transformSelectQueryNode(this.query, this.queryTransforms);
499
+ }
500
+ getQueryNode() {
501
+ return cloneSelectQueryNode(this.query);
430
502
  }
431
503
  static setJoinRelationships(relationships) {
432
504
  this.relationships = relationships;
433
505
  }
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) => {
506
+ withRelation(nameOrPath, options) {
507
+ const next = this.cloneMutable();
508
+ const { path, label } = resolveRelationPath(nameOrPath, QueryBuilder.relationships);
509
+ next.query = applyRelationPath(next.query, path, options, (currentQuery, joinPath, relationOptions) => {
510
+ next.query = currentQuery;
447
511
  const type = options?.type || joinPath.type || 'INNER';
448
- const alias = options?.alias || joinPath.alias;
512
+ const alias = relationOptions?.alias || joinPath.alias;
513
+ const leftColumn = String(joinPath.leftColumn);
514
+ const leftSource = String(joinPath.from);
449
515
  const table = String(joinPath.to);
450
516
  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;
517
+ return next.joins.addJoin(type, table, leftColumn, rightColumn, alias, leftSource);
518
+ }, label);
519
+ return next;
460
520
  }
461
521
  }
462
522
  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
+ }
@@ -1,9 +1,4 @@
1
- export declare const TEST_CONNECTION_CONFIG: {
2
- host: string;
3
- user: string;
4
- password: string;
5
- database: string;
6
- };
1
+ import { CLICKHOUSE_CONTAINER_NAME, TEST_CONNECTION_CONFIG, TEST_DATA } from '../../../../../../testing/clickhouse/harness.mjs';
7
2
  export declare const initializeTestConnection: () => Promise<{
8
3
  cache: import("../../cache/controller.js").CacheController;
9
4
  adapter: import("../../adapters/database-adapter.js").DatabaseAdapter;
@@ -12,10 +7,7 @@ export declare const initializeTestConnection: () => Promise<{
12
7
  table<TableName extends never>(tableName: TableName): import("../../query-builder.js").SelectQB<import("../../types/builder-state.js").SchemaDefinition<unknown>, TableName, import("../../../index.js").TableRecord<import("../../types/builder-state.js").SchemaDefinition<unknown>[TableName]>, TableName>;
13
8
  }>;
14
9
  export declare const ensureConnectionInitialized: () => import("@clickhouse/client").ClickHouseClient | import("@clickhouse/client-web").ClickHouseClient;
15
- export declare const isDockerAvailable: () => Promise<boolean>;
16
- export declare const isDockerComposeAvailable: () => Promise<boolean>;
17
10
  export declare const isContainerRunning: (containerName: string) => Promise<boolean>;
18
- export declare const isClickHouseReady: () => Promise<boolean>;
19
11
  export declare const startClickHouseContainer: () => Promise<void>;
20
12
  export declare const waitForClickHouse: (maxAttempts?: number, retryInterval?: number) => Promise<void>;
21
13
  export declare const stopClickHouseContainer: () => Promise<void>;
@@ -27,6 +19,7 @@ export interface TestSchemaType {
27
19
  price: number;
28
20
  created_at: string;
29
21
  is_active: boolean;
22
+ tags: string[];
30
23
  }>;
31
24
  users: Array<{
32
25
  id: number;
@@ -45,6 +38,6 @@ export interface TestSchemaType {
45
38
  created_at: string;
46
39
  }>;
47
40
  }
48
- export declare const TEST_DATA: TestSchemaType;
41
+ export { CLICKHOUSE_CONTAINER_NAME, TEST_CONNECTION_CONFIG, TEST_DATA };
49
42
  export declare const setupTestDatabase: () => Promise<void>;
50
43
  //# sourceMappingURL=setup.d.ts.map
@@ -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":"AAEA,OAAO,EACL,yBAAyB,EACzB,sBAAsB,EACtB,SAAS,EASV,MAAM,kDAAkD,CAAC;AA4B1D,eAAO,MAAM,wBAAwB;;;;;;EAyBpC,CAAC;AAGF,eAAO,MAAM,2BAA2B,yGAevC,CAAC;AAGF,eAAO,MAAM,kBAAkB,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,OAAO,CAE/E,CAAC;AAGF,eAAO,MAAM,wBAAwB,QAAa,OAAO,CAAC,IAAI,CAO7D,CAAC;AAGF,eAAO,MAAM,iBAAiB,GAC5B,oBAAgB,EAChB,sBAAoB,KACnB,OAAO,CAAC,IAAI,CAOd,CAAC;AAGF,eAAO,MAAM,uBAAuB,QAAa,OAAO,CAAC,IAAI,CAU5D,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;AAED,OAAO,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,SAAS,EAAE,CAAC;AAKxE,eAAO,MAAM,iBAAiB,QAAa,OAAO,CAAC,IAAI,CAuBtD,CAAC"}