@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.
Files changed (61) hide show
  1. package/README-CLI.md +123 -0
  2. package/README.md +276 -0
  3. package/dist/cli/bin.js +151 -0
  4. package/dist/cli/generate-types.d.ts +5 -0
  5. package/dist/cli/generate-types.js +91 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.js +2 -0
  8. package/dist/core/connection.d.ts.map +1 -0
  9. package/dist/core/connection.js +34 -0
  10. package/dist/core/cross-filter.d.ts.map +1 -0
  11. package/dist/core/cross-filter.js +218 -0
  12. package/dist/core/features/aggregations.d.ts.map +1 -0
  13. package/dist/core/features/aggregations.js +35 -0
  14. package/dist/core/features/analytics.d.ts.map +1 -0
  15. package/dist/core/features/analytics.js +35 -0
  16. package/dist/core/features/executor.d.ts.map +1 -0
  17. package/dist/core/features/executor.js +136 -0
  18. package/dist/core/features/filtering.d.ts.map +1 -0
  19. package/dist/core/features/filtering.js +30 -0
  20. package/dist/core/features/joins.d.ts.map +1 -0
  21. package/dist/core/features/joins.js +16 -0
  22. package/dist/core/features/pagination.d.ts.map +1 -0
  23. package/dist/core/features/pagination.js +190 -0
  24. package/dist/core/features/query-modifiers.d.ts.map +1 -0
  25. package/dist/core/features/query-modifiers.js +50 -0
  26. package/dist/core/formatters/sql-formatter.d.ts.map +1 -0
  27. package/dist/core/formatters/sql-formatter.js +69 -0
  28. package/dist/core/join-relationships.d.ts.map +1 -0
  29. package/dist/core/join-relationships.js +56 -0
  30. package/dist/core/query-builder.d.ts.map +1 -0
  31. package/dist/core/query-builder.js +372 -0
  32. package/dist/core/tests/index.d.ts.map +1 -0
  33. package/dist/core/tests/index.js +1 -0
  34. package/dist/core/tests/integration/setup.d.ts.map +1 -0
  35. package/dist/core/tests/integration/setup.js +274 -0
  36. package/dist/core/tests/test-utils.d.ts.map +1 -0
  37. package/dist/core/tests/test-utils.js +32 -0
  38. package/dist/core/utils/logger.d.ts.map +1 -0
  39. package/dist/core/utils/logger.js +98 -0
  40. package/dist/core/utils/sql-expressions.d.ts.map +1 -0
  41. package/dist/core/utils/sql-expressions.js +73 -0
  42. package/dist/core/utils.d.ts.map +1 -0
  43. package/dist/core/utils.js +29 -0
  44. package/dist/core/validators/filter-validator.d.ts.map +1 -0
  45. package/dist/core/validators/filter-validator.js +19 -0
  46. package/dist/core/validators/value-validator.d.ts.map +1 -0
  47. package/dist/core/validators/value-validator.js +47 -0
  48. package/dist/formatters/index.d.ts.map +1 -0
  49. package/dist/formatters/index.js +1 -0
  50. package/dist/index.d.ts +33 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +5 -0
  53. package/dist/types/base.d.ts.map +1 -0
  54. package/dist/types/base.js +1 -0
  55. package/dist/types/clickhouse-types.d.ts.map +1 -0
  56. package/dist/types/clickhouse-types.js +1 -0
  57. package/dist/types/filters.d.ts.map +1 -0
  58. package/dist/types/filters.js +1 -0
  59. package/dist/types/index.d.ts.map +1 -0
  60. package/dist/types/index.js +2 -0
  61. 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
+ }