@hypequery/clickhouse 0.2.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.
- package/README-CLI.md +123 -0
- package/README.md +276 -0
- package/dist/cli/bin.js +151 -0
- package/dist/cli/generate-types.d.ts +5 -0
- package/dist/cli/generate-types.js +91 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +2 -0
- package/dist/core/connection.d.ts.map +1 -0
- package/dist/core/connection.js +34 -0
- package/dist/core/cross-filter.d.ts.map +1 -0
- package/dist/core/cross-filter.js +218 -0
- package/dist/core/features/aggregations.d.ts.map +1 -0
- package/dist/core/features/aggregations.js +35 -0
- package/dist/core/features/analytics.d.ts.map +1 -0
- package/dist/core/features/analytics.js +35 -0
- package/dist/core/features/executor.d.ts.map +1 -0
- package/dist/core/features/executor.js +136 -0
- package/dist/core/features/filtering.d.ts.map +1 -0
- package/dist/core/features/filtering.js +30 -0
- package/dist/core/features/joins.d.ts.map +1 -0
- package/dist/core/features/joins.js +16 -0
- package/dist/core/features/pagination.d.ts.map +1 -0
- package/dist/core/features/pagination.js +190 -0
- package/dist/core/features/query-modifiers.d.ts.map +1 -0
- package/dist/core/features/query-modifiers.js +50 -0
- package/dist/core/formatters/sql-formatter.d.ts.map +1 -0
- package/dist/core/formatters/sql-formatter.js +69 -0
- package/dist/core/join-relationships.d.ts.map +1 -0
- package/dist/core/join-relationships.js +56 -0
- package/dist/core/query-builder.d.ts.map +1 -0
- package/dist/core/query-builder.js +372 -0
- package/dist/core/tests/index.d.ts.map +1 -0
- package/dist/core/tests/index.js +1 -0
- package/dist/core/tests/integration/setup.d.ts.map +1 -0
- package/dist/core/tests/integration/setup.js +274 -0
- package/dist/core/tests/test-utils.d.ts.map +1 -0
- package/dist/core/tests/test-utils.js +32 -0
- package/dist/core/utils/logger.d.ts.map +1 -0
- package/dist/core/utils/logger.js +98 -0
- package/dist/core/utils/sql-expressions.d.ts.map +1 -0
- package/dist/core/utils/sql-expressions.js +73 -0
- package/dist/core/utils.d.ts.map +1 -0
- package/dist/core/utils.js +29 -0
- package/dist/core/validators/filter-validator.d.ts.map +1 -0
- package/dist/core/validators/filter-validator.js +19 -0
- package/dist/core/validators/value-validator.d.ts.map +1 -0
- package/dist/core/validators/value-validator.js +47 -0
- package/dist/formatters/index.d.ts.map +1 -0
- package/dist/formatters/index.js +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/types/base.d.ts.map +1 -0
- package/dist/types/base.js +1 -0
- package/dist/types/clickhouse-types.d.ts.map +1 -0
- package/dist/types/clickhouse-types.js +1 -0
- package/dist/types/filters.d.ts.map +1 -0
- package/dist/types/filters.js +1 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/package.json +67 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { ClickHouseConnection } from './connection';
|
|
2
|
+
import { SQLFormatter } from './formatters/sql-formatter';
|
|
3
|
+
import { AggregationFeature } from './features/aggregations';
|
|
4
|
+
import { JoinFeature } from './features/joins';
|
|
5
|
+
import { FilteringFeature } from './features/filtering';
|
|
6
|
+
import { AnalyticsFeature } from './features/analytics';
|
|
7
|
+
import { ExecutorFeature } from './features/executor';
|
|
8
|
+
import { QueryModifiersFeature } from './features/query-modifiers';
|
|
9
|
+
import { FilterValidator } from './validators/filter-validator';
|
|
10
|
+
import { PaginationFeature } from './features/pagination';
|
|
11
|
+
/**
|
|
12
|
+
* A type-safe query builder for ClickHouse databases.
|
|
13
|
+
* @template Schema - The full database schema
|
|
14
|
+
* @template T - The schema type of the current table
|
|
15
|
+
* @template HasSelect - Whether a SELECT clause has been applied
|
|
16
|
+
* @template Aggregations - The type of any aggregation functions applied
|
|
17
|
+
*/
|
|
18
|
+
export class QueryBuilder {
|
|
19
|
+
constructor(tableName, schema, originalSchema) {
|
|
20
|
+
this.config = {};
|
|
21
|
+
this.formatter = new SQLFormatter();
|
|
22
|
+
this.tableName = tableName;
|
|
23
|
+
this.schema = schema;
|
|
24
|
+
this.originalSchema = originalSchema;
|
|
25
|
+
this.aggregations = new AggregationFeature(this);
|
|
26
|
+
this.joins = new JoinFeature(this);
|
|
27
|
+
this.filtering = new FilteringFeature(this);
|
|
28
|
+
this.analytics = new AnalyticsFeature(this);
|
|
29
|
+
this.executor = new ExecutorFeature(this);
|
|
30
|
+
this.modifiers = new QueryModifiersFeature(this);
|
|
31
|
+
this.pagination = new PaginationFeature(this);
|
|
32
|
+
}
|
|
33
|
+
debug() {
|
|
34
|
+
console.log('Current Type:', {
|
|
35
|
+
schema: this.schema,
|
|
36
|
+
originalSchema: this.originalSchema,
|
|
37
|
+
config: this.config
|
|
38
|
+
});
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
clone() {
|
|
42
|
+
const newBuilder = new QueryBuilder(this.tableName, this.schema, this.originalSchema);
|
|
43
|
+
newBuilder.config = { ...this.config };
|
|
44
|
+
// Initialize features with the new builder
|
|
45
|
+
newBuilder.aggregations = new AggregationFeature(newBuilder);
|
|
46
|
+
newBuilder.joins = new JoinFeature(newBuilder);
|
|
47
|
+
newBuilder.filtering = new FilteringFeature(newBuilder);
|
|
48
|
+
newBuilder.analytics = new AnalyticsFeature(newBuilder);
|
|
49
|
+
newBuilder.executor = new ExecutorFeature(newBuilder);
|
|
50
|
+
newBuilder.modifiers = new QueryModifiersFeature(newBuilder);
|
|
51
|
+
newBuilder.pagination = new PaginationFeature(newBuilder);
|
|
52
|
+
return newBuilder;
|
|
53
|
+
}
|
|
54
|
+
// --- Analytics Helper: Add a CTE.
|
|
55
|
+
withCTE(alias, subquery) {
|
|
56
|
+
this.config = this.analytics.addCTE(alias, subquery);
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Groups results by a time interval using a specified ClickHouse function.
|
|
61
|
+
*
|
|
62
|
+
* @param column - The column containing the date or timestamp.
|
|
63
|
+
* @param interval - The interval value. For example, "1 day" or "15 minute".
|
|
64
|
+
* This is only used when the method is 'toStartOfInterval'.
|
|
65
|
+
* @param method - The time bucketing function to use.
|
|
66
|
+
* Defaults to 'toStartOfInterval'.
|
|
67
|
+
* Other valid values include 'toStartOfMinute', 'toStartOfHour',
|
|
68
|
+
* 'toStartOfDay', 'toStartOfWeek', 'toStartOfMonth', 'toStartOfQuarter', and 'toStartOfYear'.
|
|
69
|
+
* @returns The current QueryBuilder instance.
|
|
70
|
+
*/
|
|
71
|
+
groupByTimeInterval(column, interval, method = 'toStartOfInterval') {
|
|
72
|
+
this.config = this.analytics.addTimeInterval(column, interval, method);
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
// --- Analytics Helper: Add a raw SQL fragment.
|
|
76
|
+
raw(sql) {
|
|
77
|
+
// Use raw() to inject SQL that isn't supported by the builder.
|
|
78
|
+
// Use with caution.
|
|
79
|
+
this.config.having = this.config.having || [];
|
|
80
|
+
this.config.having.push(sql);
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
// --- Analytics Helper: Add query settings.
|
|
84
|
+
settings(opts) {
|
|
85
|
+
this.config = this.analytics.addSettings(opts);
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Applies a set of cross filters to the current query.
|
|
90
|
+
* All filter conditions from the provided CrossFilter are added to the query.
|
|
91
|
+
* @param crossFilter - An instance of CrossFilter containing shared filter conditions.
|
|
92
|
+
* @returns The current QueryBuilder instance.
|
|
93
|
+
*/
|
|
94
|
+
applyCrossFilters(crossFilter) {
|
|
95
|
+
const filterGroup = crossFilter.getConditions();
|
|
96
|
+
filterGroup.conditions.forEach((item) => {
|
|
97
|
+
if ('column' in item) {
|
|
98
|
+
this.where(item.column, item.operator, item.value);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Selects specific columns from the table.
|
|
105
|
+
* @template K - The keys/columns to select
|
|
106
|
+
* @param {K[]} columns - Array of column names to select
|
|
107
|
+
* @returns {QueryBuilder} A new QueryBuilder instance with updated types
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* builder.select(['id', 'name'])
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
select(columns) {
|
|
114
|
+
// Create a new builder with the appropriate type parameters
|
|
115
|
+
const newBuilder = new QueryBuilder(this.tableName, {
|
|
116
|
+
name: this.schema.name,
|
|
117
|
+
columns: {} // We need this cast because we only know the shape at runtime
|
|
118
|
+
}, this.originalSchema);
|
|
119
|
+
// Process columns array to handle SqlExpressions and convert to strings
|
|
120
|
+
const processedColumns = columns.map(col => {
|
|
121
|
+
if (typeof col === 'object' && col !== null && '__type' in col) {
|
|
122
|
+
return col.toSql();
|
|
123
|
+
}
|
|
124
|
+
return String(col);
|
|
125
|
+
});
|
|
126
|
+
newBuilder.config = {
|
|
127
|
+
...this.config,
|
|
128
|
+
select: processedColumns,
|
|
129
|
+
orderBy: this.config.orderBy?.map(({ column, direction }) => ({
|
|
130
|
+
column: String(column),
|
|
131
|
+
direction
|
|
132
|
+
}))
|
|
133
|
+
};
|
|
134
|
+
return newBuilder;
|
|
135
|
+
}
|
|
136
|
+
sum(column, alias) {
|
|
137
|
+
const newBuilder = this.clone();
|
|
138
|
+
newBuilder.config = this.aggregations.sum(column, alias);
|
|
139
|
+
return newBuilder;
|
|
140
|
+
}
|
|
141
|
+
count(column, alias) {
|
|
142
|
+
const newBuilder = this.clone();
|
|
143
|
+
newBuilder.config = this.aggregations.count(column, alias);
|
|
144
|
+
return newBuilder;
|
|
145
|
+
}
|
|
146
|
+
avg(column, alias) {
|
|
147
|
+
const newBuilder = this.clone();
|
|
148
|
+
newBuilder.config = this.aggregations.avg(column, alias);
|
|
149
|
+
return newBuilder;
|
|
150
|
+
}
|
|
151
|
+
min(column, alias) {
|
|
152
|
+
const newBuilder = this.clone();
|
|
153
|
+
newBuilder.config = this.aggregations.min(column, alias);
|
|
154
|
+
return newBuilder;
|
|
155
|
+
}
|
|
156
|
+
max(column, alias) {
|
|
157
|
+
const newBuilder = this.clone();
|
|
158
|
+
newBuilder.config = this.aggregations.max(column, alias);
|
|
159
|
+
return newBuilder;
|
|
160
|
+
}
|
|
161
|
+
// Make needed properties accessible to features
|
|
162
|
+
getTableName() {
|
|
163
|
+
return this.tableName;
|
|
164
|
+
}
|
|
165
|
+
getFormatter() {
|
|
166
|
+
return this.formatter;
|
|
167
|
+
}
|
|
168
|
+
// Delegate execution methods to feature
|
|
169
|
+
toSQL() {
|
|
170
|
+
return this.executor.toSQL();
|
|
171
|
+
}
|
|
172
|
+
toSQLWithParams() {
|
|
173
|
+
return this.executor.toSQLWithParams();
|
|
174
|
+
}
|
|
175
|
+
execute() {
|
|
176
|
+
return this.executor.execute();
|
|
177
|
+
}
|
|
178
|
+
async stream() {
|
|
179
|
+
return this.executor.stream();
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Processes each row in a stream with the provided callback function
|
|
183
|
+
* @param callback Function to call for each row in the stream
|
|
184
|
+
*/
|
|
185
|
+
async streamForEach(callback) {
|
|
186
|
+
const stream = await this.stream();
|
|
187
|
+
const reader = stream.getReader();
|
|
188
|
+
try {
|
|
189
|
+
while (true) {
|
|
190
|
+
const { done, value: rows } = await reader.read();
|
|
191
|
+
if (done)
|
|
192
|
+
break;
|
|
193
|
+
for (const row of rows) {
|
|
194
|
+
await callback(row);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
reader.releaseLock();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
validateFilterValue(column, operator, value) {
|
|
203
|
+
if (FilterValidator.validateJoinedColumn(String(column)))
|
|
204
|
+
return;
|
|
205
|
+
const columnType = this.schema.columns[column];
|
|
206
|
+
FilterValidator.validateFilterCondition({ column: String(column), operator, value }, columnType);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Adds a WHERE clause to filter results.
|
|
210
|
+
* @template K - The column key type
|
|
211
|
+
* @param {K} column - The column to filter on
|
|
212
|
+
* @param {FilterOperator} operator - The comparison operator
|
|
213
|
+
* @param {any} value - The value to compare against
|
|
214
|
+
* @returns {this} The current QueryBuilder instance
|
|
215
|
+
* @example
|
|
216
|
+
* ```ts
|
|
217
|
+
* builder.where('age', 'gt', 18)
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
where(column, operator, value) {
|
|
221
|
+
this.validateFilterValue(column, operator, value);
|
|
222
|
+
this.config = this.filtering.addCondition('AND', column, operator, value);
|
|
223
|
+
return this;
|
|
224
|
+
}
|
|
225
|
+
orWhere(column, operator, value) {
|
|
226
|
+
this.config = this.filtering.addCondition('OR', column, operator, value);
|
|
227
|
+
return this;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Adds a GROUP BY clause.
|
|
231
|
+
* @param {keyof T | Array<keyof T>} columns - Column(s) to group by
|
|
232
|
+
* @returns {this} The current QueryBuilder instance
|
|
233
|
+
* @example
|
|
234
|
+
* ```ts
|
|
235
|
+
* builder.groupBy(['category', 'status'])
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
groupBy(columns) {
|
|
239
|
+
this.config = this.modifiers.addGroupBy(columns);
|
|
240
|
+
return this;
|
|
241
|
+
}
|
|
242
|
+
limit(count) {
|
|
243
|
+
this.config = this.modifiers.addLimit(count);
|
|
244
|
+
return this;
|
|
245
|
+
}
|
|
246
|
+
offset(count) {
|
|
247
|
+
this.config = this.modifiers.addOffset(count);
|
|
248
|
+
return this;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Adds an ORDER BY clause.
|
|
252
|
+
* @param {keyof T} column - The column to order by
|
|
253
|
+
* @param {OrderDirection} [direction='ASC'] - The sort direction
|
|
254
|
+
* @returns {this} The current QueryBuilder instance
|
|
255
|
+
* @example
|
|
256
|
+
* ```ts
|
|
257
|
+
* builder.orderBy('created_at', 'DESC')
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
orderBy(column, direction = 'ASC') {
|
|
261
|
+
this.config = this.modifiers.addOrderBy(column, direction);
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Adds a HAVING clause for filtering grouped results.
|
|
266
|
+
* @param {string} condition - The HAVING condition
|
|
267
|
+
* @returns {this} The current QueryBuilder instance
|
|
268
|
+
* @example
|
|
269
|
+
* ```ts
|
|
270
|
+
* builder.having('COUNT(*) > 5')
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
having(condition, parameters) {
|
|
274
|
+
this.config = this.modifiers.addHaving(condition, parameters);
|
|
275
|
+
return this;
|
|
276
|
+
}
|
|
277
|
+
distinct() {
|
|
278
|
+
this.config = this.modifiers.setDistinct();
|
|
279
|
+
return this;
|
|
280
|
+
}
|
|
281
|
+
whereBetween(column, [min, max]) {
|
|
282
|
+
if (min === null || max === null) {
|
|
283
|
+
throw new Error('BETWEEN values cannot be null');
|
|
284
|
+
}
|
|
285
|
+
return this.where(column, 'between', [min, max]);
|
|
286
|
+
}
|
|
287
|
+
innerJoin(table, leftColumn, rightColumn, alias) {
|
|
288
|
+
const newBuilder = this.clone();
|
|
289
|
+
newBuilder.config = this.joins.addJoin('INNER', table, leftColumn, rightColumn, alias);
|
|
290
|
+
return newBuilder;
|
|
291
|
+
}
|
|
292
|
+
leftJoin(table, leftColumn, rightColumn, alias) {
|
|
293
|
+
const newBuilder = this.clone();
|
|
294
|
+
newBuilder.config = this.joins.addJoin('LEFT', table, leftColumn, rightColumn, alias);
|
|
295
|
+
return newBuilder;
|
|
296
|
+
}
|
|
297
|
+
rightJoin(table, leftColumn, rightColumn, alias) {
|
|
298
|
+
const newBuilder = this.clone();
|
|
299
|
+
newBuilder.config = this.joins.addJoin('RIGHT', table, leftColumn, rightColumn, alias);
|
|
300
|
+
return newBuilder;
|
|
301
|
+
}
|
|
302
|
+
fullJoin(table, leftColumn, rightColumn, alias) {
|
|
303
|
+
const newBuilder = this.clone();
|
|
304
|
+
newBuilder.config = this.joins.addJoin('FULL', table, leftColumn, rightColumn, alias);
|
|
305
|
+
return newBuilder;
|
|
306
|
+
}
|
|
307
|
+
// Make config accessible to features
|
|
308
|
+
getConfig() {
|
|
309
|
+
return this.config;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Paginates the query results using cursor-based pagination
|
|
313
|
+
*/
|
|
314
|
+
async paginate(options) {
|
|
315
|
+
return this.pagination.paginate(options);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Gets the first page of results
|
|
319
|
+
*/
|
|
320
|
+
async firstPage(pageSize) {
|
|
321
|
+
return this.pagination.firstPage(pageSize);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Returns an async iterator that yields all pages
|
|
325
|
+
*/
|
|
326
|
+
iteratePages(pageSize) {
|
|
327
|
+
return this.pagination.iteratePages(pageSize);
|
|
328
|
+
}
|
|
329
|
+
static setJoinRelationships(relationships) {
|
|
330
|
+
this.relationships = relationships;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Apply a predefined join relationship
|
|
334
|
+
*/
|
|
335
|
+
withRelation(name, options) {
|
|
336
|
+
if (!QueryBuilder.relationships) {
|
|
337
|
+
throw new Error('Join relationships have not been initialized. Call QueryBuilder.setJoinRelationships first.');
|
|
338
|
+
}
|
|
339
|
+
const path = QueryBuilder.relationships.get(name);
|
|
340
|
+
if (!path) {
|
|
341
|
+
throw new Error(`Join relationship '${name}' not found`);
|
|
342
|
+
}
|
|
343
|
+
if (Array.isArray(path)) {
|
|
344
|
+
// Handle join chain
|
|
345
|
+
path.forEach(joinPath => {
|
|
346
|
+
const type = options?.type || joinPath.type || 'INNER';
|
|
347
|
+
const alias = options?.alias || joinPath.alias;
|
|
348
|
+
const table = String(joinPath.to);
|
|
349
|
+
this.config = this.joins.addJoin(type, table, joinPath.leftColumn, `${table}.${joinPath.rightColumn}`, alias);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
// Handle single join
|
|
354
|
+
const type = options?.type || path.type || 'INNER';
|
|
355
|
+
const alias = options?.alias || path.alias;
|
|
356
|
+
const table = String(path.to);
|
|
357
|
+
this.config = this.joins.addJoin(type, table, path.leftColumn, `${table}.${path.rightColumn}`, alias);
|
|
358
|
+
}
|
|
359
|
+
return this;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
export function createQueryBuilder(config) {
|
|
363
|
+
ClickHouseConnection.initialize(config);
|
|
364
|
+
return {
|
|
365
|
+
table(tableName) {
|
|
366
|
+
return new QueryBuilder(tableName, {
|
|
367
|
+
name: tableName,
|
|
368
|
+
columns: {}
|
|
369
|
+
}, {});
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/tests/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './test-utils';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../../../src/core/tests/integration/setup.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE;QACV,EAAE,EAAE,OAAO,CAAC;QACZ,IAAI,EAAE,QAAQ,CAAC;QACf,KAAK,EAAE,SAAS,CAAC;QACjB,UAAU,EAAE,UAAU,CAAC;QACvB,QAAQ,EAAE,QAAQ,CAAC;QACnB,MAAM,EAAE,OAAO,CAAC;KACjB,CAAC;IACF,KAAK,EAAE;QACL,EAAE,EAAE,OAAO,CAAC;QACZ,SAAS,EAAE,QAAQ,CAAC;QACpB,KAAK,EAAE,QAAQ,CAAC;QAChB,UAAU,EAAE,UAAU,CAAC;QACvB,MAAM,EAAE,QAAQ,CAAC;KAClB,CAAC;IACF,MAAM,EAAE;QACN,EAAE,EAAE,OAAO,CAAC;QACZ,OAAO,EAAE,OAAO,CAAC;QACjB,UAAU,EAAE,OAAO,CAAC;QACpB,QAAQ,EAAE,OAAO,CAAC;QAClB,KAAK,EAAE,SAAS,CAAC;QACjB,MAAM,EAAE,QAAQ,CAAC;QACjB,UAAU,EAAE,UAAU,CAAC;KACxB,CAAC;IACF,QAAQ,EAAE;QACR,EAAE,EAAE,OAAO,CAAC;QACZ,IAAI,EAAE,QAAQ,CAAC;QACf,KAAK,EAAE,SAAS,CAAC;QACjB,QAAQ,EAAE,QAAQ,CAAC;QACnB,WAAW,EAAE,QAAQ,CAAC;KACvB,CAAC;CACH;AAGD,wBAAsB,wBAAwB;;GAwB7C;AAmDD,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCrB,CAAC;AAGF,wBAAsB,iBAAiB,kBAuFtC;AAGD,wBAAgB,iBAAiB,IAAI,OAAO,CAO3C;AAGD,wBAAgB,wBAAwB,SAsDvC;AAGD,wBAAgB,uBAAuB,SAYtC"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { createQueryBuilder } from '../../../index';
|
|
3
|
+
import { ClickHouseConnection } from '../../connection';
|
|
4
|
+
// Configuration for the test ClickHouse instance
|
|
5
|
+
const CLICKHOUSE_HOST = process.env.CLICKHOUSE_TEST_HOST || 'http://localhost:8123';
|
|
6
|
+
const CLICKHOUSE_USER = process.env.CLICKHOUSE_TEST_USER || 'hypequery';
|
|
7
|
+
const CLICKHOUSE_PASSWORD = process.env.CLICKHOUSE_TEST_PASSWORD || 'hypequery_test';
|
|
8
|
+
const CLICKHOUSE_DB = process.env.CLICKHOUSE_TEST_DB || 'test_db';
|
|
9
|
+
// Log connection details for debugging
|
|
10
|
+
console.log('Initializing ClickHouse connection with:', {
|
|
11
|
+
host: CLICKHOUSE_HOST,
|
|
12
|
+
user: CLICKHOUSE_USER,
|
|
13
|
+
password: CLICKHOUSE_PASSWORD ? 'PROVIDED' : 'NOT_PROVIDED',
|
|
14
|
+
database: CLICKHOUSE_DB
|
|
15
|
+
});
|
|
16
|
+
// Helper to initialize the connection
|
|
17
|
+
export async function initializeTestConnection() {
|
|
18
|
+
try {
|
|
19
|
+
ClickHouseConnection.initialize({
|
|
20
|
+
host: CLICKHOUSE_HOST,
|
|
21
|
+
username: CLICKHOUSE_USER,
|
|
22
|
+
password: CLICKHOUSE_PASSWORD,
|
|
23
|
+
database: CLICKHOUSE_DB
|
|
24
|
+
});
|
|
25
|
+
// Test the connection
|
|
26
|
+
const client = ClickHouseConnection.getClient();
|
|
27
|
+
await client.ping();
|
|
28
|
+
console.log('ClickHouse connection successfully established');
|
|
29
|
+
return createQueryBuilder({
|
|
30
|
+
host: CLICKHOUSE_HOST,
|
|
31
|
+
username: CLICKHOUSE_USER,
|
|
32
|
+
password: CLICKHOUSE_PASSWORD,
|
|
33
|
+
database: CLICKHOUSE_DB
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error('Failed to initialize ClickHouse connection:', error);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// SQL to create test tables
|
|
42
|
+
const CREATE_TEST_TABLE = `
|
|
43
|
+
CREATE TABLE IF NOT EXISTS test_table (
|
|
44
|
+
id Int32,
|
|
45
|
+
name String,
|
|
46
|
+
price Float64,
|
|
47
|
+
created_at DateTime,
|
|
48
|
+
category String,
|
|
49
|
+
active UInt8
|
|
50
|
+
) ENGINE = MergeTree()
|
|
51
|
+
ORDER BY id
|
|
52
|
+
`;
|
|
53
|
+
const CREATE_USERS_TABLE = `
|
|
54
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
55
|
+
id Int32,
|
|
56
|
+
user_name String,
|
|
57
|
+
email String,
|
|
58
|
+
created_at DateTime,
|
|
59
|
+
status String
|
|
60
|
+
) ENGINE = MergeTree()
|
|
61
|
+
ORDER BY id
|
|
62
|
+
`;
|
|
63
|
+
const CREATE_ORDERS_TABLE = `
|
|
64
|
+
CREATE TABLE IF NOT EXISTS orders (
|
|
65
|
+
id Int32,
|
|
66
|
+
user_id Int32,
|
|
67
|
+
product_id Int32,
|
|
68
|
+
quantity Int32,
|
|
69
|
+
total Float64,
|
|
70
|
+
status String,
|
|
71
|
+
created_at DateTime
|
|
72
|
+
) ENGINE = MergeTree()
|
|
73
|
+
ORDER BY id
|
|
74
|
+
`;
|
|
75
|
+
const CREATE_PRODUCTS_TABLE = `
|
|
76
|
+
CREATE TABLE IF NOT EXISTS products (
|
|
77
|
+
id Int32,
|
|
78
|
+
name String,
|
|
79
|
+
price Float64,
|
|
80
|
+
category String,
|
|
81
|
+
description String
|
|
82
|
+
) ENGINE = MergeTree()
|
|
83
|
+
ORDER BY id
|
|
84
|
+
`;
|
|
85
|
+
// Sample data for tests
|
|
86
|
+
export const TEST_DATA = {
|
|
87
|
+
test_table: [
|
|
88
|
+
{ id: 1, name: 'Product 1', price: 10.99, created_at: '2023-01-01 00:00:00', category: 'A', active: 1 },
|
|
89
|
+
{ id: 2, name: 'Product 2', price: 20.50, created_at: '2023-01-02 00:00:00', category: 'B', active: 1 },
|
|
90
|
+
{ id: 3, name: 'Product 3', price: 15.75, created_at: '2023-01-03 00:00:00', category: 'A', active: 0 },
|
|
91
|
+
{ id: 4, name: 'Product 4', price: 25.00, created_at: '2023-01-04 00:00:00', category: 'C', active: 1 },
|
|
92
|
+
{ id: 5, name: 'Product 5', price: 30.25, created_at: '2023-01-05 00:00:00', category: 'B', active: 0 },
|
|
93
|
+
{ id: 6, name: 'Product 6', price: 12.99, created_at: '2023-01-06 00:00:00', category: 'A', active: 1 },
|
|
94
|
+
{ id: 7, name: 'Product 7', price: 22.50, created_at: '2023-01-07 00:00:00', category: 'B', active: 1 },
|
|
95
|
+
{ id: 8, name: 'Product 8', price: 18.75, created_at: '2023-01-08 00:00:00', category: 'C', active: 0 }
|
|
96
|
+
],
|
|
97
|
+
users: [
|
|
98
|
+
{ id: 1, user_name: 'user1', email: 'user1@example.com', created_at: '2023-01-01 00:00:00', status: 'active' },
|
|
99
|
+
{ id: 2, user_name: 'user2', email: 'user2@example.com', created_at: '2023-01-02 00:00:00', status: 'active' },
|
|
100
|
+
{ id: 3, user_name: 'user3', email: 'user3@example.com', created_at: '2023-01-03 00:00:00', status: 'inactive' },
|
|
101
|
+
{ id: 4, user_name: 'user4', email: 'user4@example.com', created_at: '2023-01-04 00:00:00', status: 'active' },
|
|
102
|
+
{ id: 5, user_name: 'user5', email: 'user5@example.com', created_at: '2023-01-05 00:00:00', status: 'pending' }
|
|
103
|
+
],
|
|
104
|
+
orders: [
|
|
105
|
+
{ id: 1, user_id: 1, product_id: 1, quantity: 2, total: 21.98, status: 'completed', created_at: '2023-01-10 10:00:00' },
|
|
106
|
+
{ id: 2, user_id: 1, product_id: 3, quantity: 1, total: 15.75, status: 'completed', created_at: '2023-01-11 11:00:00' },
|
|
107
|
+
{ id: 3, user_id: 2, product_id: 2, quantity: 3, total: 61.50, status: 'completed', created_at: '2023-01-12 12:00:00' },
|
|
108
|
+
{ id: 4, user_id: 3, product_id: 5, quantity: 1, total: 30.25, status: 'pending', created_at: '2023-01-13 13:00:00' },
|
|
109
|
+
{ id: 5, user_id: 4, product_id: 4, quantity: 2, total: 50.00, status: 'completed', created_at: '2023-01-14 14:00:00' },
|
|
110
|
+
{ id: 6, user_id: 2, product_id: 6, quantity: 1, total: 12.99, status: 'cancelled', created_at: '2023-01-15 15:00:00' },
|
|
111
|
+
{ id: 7, user_id: 5, product_id: 7, quantity: 4, total: 90.00, status: 'pending', created_at: '2023-01-16 16:00:00' },
|
|
112
|
+
{ id: 8, user_id: 1, product_id: 8, quantity: 1, total: 18.75, status: 'completed', created_at: '2023-01-17 17:00:00' }
|
|
113
|
+
],
|
|
114
|
+
products: [
|
|
115
|
+
{ id: 1, name: 'Product A', price: 10.99, category: 'Electronics', description: 'A great electronic device' },
|
|
116
|
+
{ id: 2, name: 'Product B', price: 20.50, category: 'Clothing', description: 'Comfortable clothing item' },
|
|
117
|
+
{ id: 3, name: 'Product C', price: 15.75, category: 'Electronics', description: 'Another electronic gadget' },
|
|
118
|
+
{ id: 4, name: 'Product D', price: 25.00, category: 'Home', description: 'Home decoration item' },
|
|
119
|
+
{ id: 5, name: 'Product E', price: 30.25, category: 'Kitchen', description: 'Useful kitchen tool' },
|
|
120
|
+
{ id: 6, name: 'Product F', price: 12.99, category: 'Office', description: 'Office supplies' },
|
|
121
|
+
{ id: 7, name: 'Product G', price: 22.50, category: 'Electronics', description: 'Premium electronic device' },
|
|
122
|
+
{ id: 8, name: 'Product H', price: 18.75, category: 'Clothing', description: 'Stylish clothing piece' }
|
|
123
|
+
]
|
|
124
|
+
};
|
|
125
|
+
// Helper to set up the test database
|
|
126
|
+
export async function setupTestDatabase() {
|
|
127
|
+
const client = ClickHouseConnection.getClient();
|
|
128
|
+
// Create database if it doesn't exist
|
|
129
|
+
await client.command({
|
|
130
|
+
query: `CREATE DATABASE IF NOT EXISTS ${CLICKHOUSE_DB}`
|
|
131
|
+
});
|
|
132
|
+
// Use the test database
|
|
133
|
+
await client.command({
|
|
134
|
+
query: `USE ${CLICKHOUSE_DB}`
|
|
135
|
+
});
|
|
136
|
+
// Create test tables
|
|
137
|
+
await client.command({
|
|
138
|
+
query: CREATE_TEST_TABLE
|
|
139
|
+
});
|
|
140
|
+
await client.command({
|
|
141
|
+
query: CREATE_USERS_TABLE
|
|
142
|
+
});
|
|
143
|
+
await client.command({
|
|
144
|
+
query: CREATE_ORDERS_TABLE
|
|
145
|
+
});
|
|
146
|
+
await client.command({
|
|
147
|
+
query: CREATE_PRODUCTS_TABLE
|
|
148
|
+
});
|
|
149
|
+
// Truncate tables if they exist
|
|
150
|
+
await client.command({
|
|
151
|
+
query: `TRUNCATE TABLE IF EXISTS test_table`
|
|
152
|
+
});
|
|
153
|
+
await client.command({
|
|
154
|
+
query: `TRUNCATE TABLE IF EXISTS users`
|
|
155
|
+
});
|
|
156
|
+
await client.command({
|
|
157
|
+
query: `TRUNCATE TABLE IF EXISTS orders`
|
|
158
|
+
});
|
|
159
|
+
await client.command({
|
|
160
|
+
query: `TRUNCATE TABLE IF EXISTS products`
|
|
161
|
+
});
|
|
162
|
+
// Insert test data
|
|
163
|
+
// For test_table
|
|
164
|
+
for (const item of TEST_DATA.test_table) {
|
|
165
|
+
await client.command({
|
|
166
|
+
query: `
|
|
167
|
+
INSERT INTO test_table (id, name, price, created_at, category, active)
|
|
168
|
+
VALUES (${item.id}, '${item.name}', ${item.price}, '${item.created_at}', '${item.category}', ${item.active})
|
|
169
|
+
`
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// For users
|
|
173
|
+
for (const user of TEST_DATA.users) {
|
|
174
|
+
await client.command({
|
|
175
|
+
query: `
|
|
176
|
+
INSERT INTO users (id, user_name, email, created_at, status)
|
|
177
|
+
VALUES (${user.id}, '${user.user_name}', '${user.email}', '${user.created_at}', '${user.status}')
|
|
178
|
+
`
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
// For orders
|
|
182
|
+
for (const order of TEST_DATA.orders) {
|
|
183
|
+
await client.command({
|
|
184
|
+
query: `
|
|
185
|
+
INSERT INTO orders (id, user_id, product_id, quantity, total, status, created_at)
|
|
186
|
+
VALUES (${order.id}, ${order.user_id}, ${order.product_id}, ${order.quantity}, ${order.total}, '${order.status}', '${order.created_at}')
|
|
187
|
+
`
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
// For products
|
|
191
|
+
for (const product of TEST_DATA.products) {
|
|
192
|
+
await client.command({
|
|
193
|
+
query: `
|
|
194
|
+
INSERT INTO products (id, name, price, category, description)
|
|
195
|
+
VALUES (${product.id}, '${product.name}', ${product.price}, '${product.category}', '${product.description}')
|
|
196
|
+
`
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Helper to check if Docker is available
|
|
201
|
+
export function isDockerAvailable() {
|
|
202
|
+
try {
|
|
203
|
+
execSync('docker --version', { stdio: 'ignore' });
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Helper to start a ClickHouse Docker container for testing
|
|
211
|
+
export function startClickHouseContainer() {
|
|
212
|
+
if (!isDockerAvailable()) {
|
|
213
|
+
console.warn('Docker is not available. Integration tests will use the configured ClickHouse instance.');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
// Check if container is already running
|
|
218
|
+
const containerId = execSync('docker ps -q -f name=hypequery-test-clickhouse').toString().trim();
|
|
219
|
+
if (containerId) {
|
|
220
|
+
console.log('ClickHouse test container is already running.');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// Start a new container with the hypequery user already configured
|
|
224
|
+
execSync(`docker run -d --name hypequery-test-clickhouse -p 8123:8123 -p 9000:9000 --ulimit nofile=262144:262144 -e CLICKHOUSE_USER=${CLICKHOUSE_USER} -e CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD} -e CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 clickhouse/clickhouse-server:latest`, { stdio: 'inherit' });
|
|
225
|
+
console.log('Started ClickHouse test container with user:', CLICKHOUSE_USER);
|
|
226
|
+
// Wait for ClickHouse to be ready
|
|
227
|
+
let attempts = 0;
|
|
228
|
+
const maxAttempts = 30;
|
|
229
|
+
while (attempts < maxAttempts) {
|
|
230
|
+
try {
|
|
231
|
+
execSync('curl -s http://localhost:8123/ping', { stdio: 'ignore' });
|
|
232
|
+
console.log('ClickHouse is ready.');
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
attempts++;
|
|
237
|
+
if (attempts >= maxAttempts) {
|
|
238
|
+
throw new Error('ClickHouse failed to start in time.');
|
|
239
|
+
}
|
|
240
|
+
console.log(`Waiting for ClickHouse to be ready... (${attempts}/${maxAttempts})`);
|
|
241
|
+
execSync('sleep 1');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Create the test database
|
|
245
|
+
try {
|
|
246
|
+
execSync(`
|
|
247
|
+
docker exec hypequery-test-clickhouse clickhouse-client -u ${CLICKHOUSE_USER} --password ${CLICKHOUSE_PASSWORD} --query "CREATE DATABASE IF NOT EXISTS ${CLICKHOUSE_DB}"
|
|
248
|
+
`, { stdio: 'inherit' });
|
|
249
|
+
console.log(`Created database '${CLICKHOUSE_DB}'.`);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
console.error('Failed to create database:', error);
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
console.error('Failed to start ClickHouse container:', error);
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Helper to stop the ClickHouse Docker container
|
|
262
|
+
export function stopClickHouseContainer() {
|
|
263
|
+
if (!isDockerAvailable()) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
execSync('docker stop hypequery-test-clickhouse', { stdio: 'ignore' });
|
|
268
|
+
execSync('docker rm hypequery-test-clickhouse', { stdio: 'ignore' });
|
|
269
|
+
console.log('Stopped and removed ClickHouse test container.');
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
console.error('Failed to stop ClickHouse container:', error);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../../../src/core/tests/test-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD,KAAK,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAA;AAEnE,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,SAAS,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,OAAO,CAAC;IACZ,SAAS,EAAE,QAAQ,CAAC;IACpB,KAAK,EAAE,QAAQ,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAGF,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,eAAe,CAAC;IAC5B,KAAK,EAAE,WAAW,CAAC;IACnB,CAAC,SAAS,EAAE,MAAM,GAAG;QAAE,CAAC,UAAU,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAC;CAC3D;AAGD,eAAO,MAAM,YAAY,EAAE,UAiB1B,CAAC;AAEF,wBAAgB,iBAAiB,IAAI,YAAY,CAAC,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,CASjH;AAED,wBAAgB,gBAAgB,IAAI,YAAY,CAAC,UAAU,EAAE,UAAU,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,UAAU,CAAC,YAAY,CAAC,CAAC,CAS1H"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { QueryBuilder } from '../query-builder';
|
|
2
|
+
// Test data
|
|
3
|
+
export const TEST_SCHEMAS = {
|
|
4
|
+
test_table: {
|
|
5
|
+
id: 'Int32',
|
|
6
|
+
name: 'String',
|
|
7
|
+
price: 'Float64',
|
|
8
|
+
created_at: 'Date',
|
|
9
|
+
category: 'String',
|
|
10
|
+
active: 'UInt8',
|
|
11
|
+
created_by: 'Int32',
|
|
12
|
+
updated_by: 'Int32'
|
|
13
|
+
},
|
|
14
|
+
users: {
|
|
15
|
+
id: 'Int32',
|
|
16
|
+
user_name: 'String',
|
|
17
|
+
email: 'String',
|
|
18
|
+
created_at: 'Date'
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export function setupUsersBuilder() {
|
|
22
|
+
return new QueryBuilder('users', {
|
|
23
|
+
name: 'users',
|
|
24
|
+
columns: TEST_SCHEMAS.users
|
|
25
|
+
}, TEST_SCHEMAS);
|
|
26
|
+
}
|
|
27
|
+
export function setupTestBuilder() {
|
|
28
|
+
return new QueryBuilder('test_table', {
|
|
29
|
+
name: 'test_table',
|
|
30
|
+
columns: TEST_SCHEMAS.test_table
|
|
31
|
+
}, TEST_SCHEMAS);
|
|
32
|
+
}
|