@hypequery/clickhouse 2.0.1 → 2.0.2

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 (73) hide show
  1. package/dist/core/tests/integration/test-data.json +190 -0
  2. package/dist/core/utils.d.ts.map +1 -1
  3. package/dist/core/utils.js +2 -1
  4. package/dist/datasets.d.ts +41 -0
  5. package/dist/datasets.d.ts.map +1 -0
  6. package/dist/datasets.js +387 -0
  7. package/dist/migrations/config/index.d.ts +3 -0
  8. package/dist/migrations/config/index.d.ts.map +1 -0
  9. package/dist/migrations/config/index.js +1 -0
  10. package/dist/migrations/config/types.d.ts +45 -0
  11. package/dist/migrations/config/types.d.ts.map +1 -0
  12. package/dist/migrations/config/types.js +28 -0
  13. package/dist/migrations/diff/diff.d.ts +11 -0
  14. package/dist/migrations/diff/diff.d.ts.map +1 -0
  15. package/dist/migrations/diff/diff.js +240 -0
  16. package/dist/migrations/diff/index.d.ts +3 -0
  17. package/dist/migrations/diff/index.d.ts.map +1 -0
  18. package/dist/migrations/diff/index.js +1 -0
  19. package/dist/migrations/diff/types.d.ts +74 -0
  20. package/dist/migrations/diff/types.d.ts.map +1 -0
  21. package/dist/migrations/diff/types.js +1 -0
  22. package/dist/migrations/introspect/index.d.ts +3 -0
  23. package/dist/migrations/introspect/index.d.ts.map +1 -0
  24. package/dist/migrations/introspect/index.js +1 -0
  25. package/dist/migrations/introspect/pull-schema.d.ts +23 -0
  26. package/dist/migrations/introspect/pull-schema.d.ts.map +1 -0
  27. package/dist/migrations/introspect/pull-schema.js +135 -0
  28. package/dist/migrations/plan/index.d.ts +3 -0
  29. package/dist/migrations/plan/index.d.ts.map +1 -0
  30. package/dist/migrations/plan/index.js +1 -0
  31. package/dist/migrations/plan/plan.d.ts +12 -0
  32. package/dist/migrations/plan/plan.d.ts.map +1 -0
  33. package/dist/migrations/plan/plan.js +416 -0
  34. package/dist/migrations/plan/types.d.ts +93 -0
  35. package/dist/migrations/plan/types.d.ts.map +1 -0
  36. package/dist/migrations/plan/types.js +1 -0
  37. package/dist/migrations/schema/column.d.ts +71 -0
  38. package/dist/migrations/schema/column.d.ts.map +1 -0
  39. package/dist/migrations/schema/column.js +123 -0
  40. package/dist/migrations/schema/define.d.ts +24 -0
  41. package/dist/migrations/schema/define.d.ts.map +1 -0
  42. package/dist/migrations/schema/define.js +47 -0
  43. package/dist/migrations/schema/index.d.ts +4 -0
  44. package/dist/migrations/schema/index.d.ts.map +1 -0
  45. package/dist/migrations/schema/index.js +2 -0
  46. package/dist/migrations/schema/types.d.ts +74 -0
  47. package/dist/migrations/schema/types.d.ts.map +1 -0
  48. package/dist/migrations/schema/types.js +1 -0
  49. package/dist/migrations/snapshot/index.d.ts +3 -0
  50. package/dist/migrations/snapshot/index.d.ts.map +1 -0
  51. package/dist/migrations/snapshot/index.js +1 -0
  52. package/dist/migrations/snapshot/serialize.d.ts +21 -0
  53. package/dist/migrations/snapshot/serialize.d.ts.map +1 -0
  54. package/dist/migrations/snapshot/serialize.js +127 -0
  55. package/dist/migrations/snapshot/types.d.ts +47 -0
  56. package/dist/migrations/snapshot/types.d.ts.map +1 -0
  57. package/dist/migrations/snapshot/types.js +1 -0
  58. package/dist/migrations/sql/index.d.ts +4 -0
  59. package/dist/migrations/sql/index.d.ts.map +1 -0
  60. package/dist/migrations/sql/index.js +2 -0
  61. package/dist/migrations/sql/render.d.ts +10 -0
  62. package/dist/migrations/sql/render.d.ts.map +1 -0
  63. package/dist/migrations/sql/render.js +347 -0
  64. package/dist/migrations/sql/types.d.ts +53 -0
  65. package/dist/migrations/sql/types.d.ts.map +1 -0
  66. package/dist/migrations/sql/types.js +1 -0
  67. package/dist/migrations/sql/write.d.ts +10 -0
  68. package/dist/migrations/sql/write.d.ts.map +1 -0
  69. package/dist/migrations/sql/write.js +35 -0
  70. package/dist/semantic-backend.d.ts +7 -0
  71. package/dist/semantic-backend.d.ts.map +1 -0
  72. package/dist/semantic-backend.js +208 -0
  73. package/package.json +1 -1
@@ -0,0 +1,190 @@
1
+ {
2
+ "test_table": [
3
+ {
4
+ "id": 1,
5
+ "name": "Product A",
6
+ "category": "A",
7
+ "price": 10.5,
8
+ "created_at": "2023-01-01 10:00:00",
9
+ "is_active": true,
10
+ "tags": [
11
+ "new",
12
+ "sale"
13
+ ],
14
+ "attributes": {
15
+ "color": "red",
16
+ "size": "M"
17
+ },
18
+ "optional_note": "Popular item",
19
+ "sku": "A-100",
20
+ "delivery_dates": [
21
+ "2023-01-05",
22
+ "2023-01-10"
23
+ ]
24
+ },
25
+ {
26
+ "id": 2,
27
+ "name": "Product B",
28
+ "category": "B",
29
+ "price": 20.75,
30
+ "created_at": "2023-01-02 12:30:00",
31
+ "is_active": true,
32
+ "tags": [
33
+ "featured"
34
+ ],
35
+ "attributes": {
36
+ "color": "blue",
37
+ "size": "L"
38
+ },
39
+ "optional_note": null,
40
+ "sku": "B-200",
41
+ "delivery_dates": [
42
+ "2023-01-06"
43
+ ]
44
+ },
45
+ {
46
+ "id": 3,
47
+ "name": "Product C",
48
+ "category": "A",
49
+ "price": 15.0,
50
+ "created_at": "2023-01-03 09:45:00",
51
+ "is_active": false,
52
+ "tags": [],
53
+ "attributes": {
54
+ "color": "green",
55
+ "size": "S"
56
+ },
57
+ "optional_note": "Backordered",
58
+ "sku": "C-300",
59
+ "delivery_dates": []
60
+ },
61
+ {
62
+ "id": 4,
63
+ "name": "Product D",
64
+ "category": "C",
65
+ "price": 8.25,
66
+ "created_at": "2023-01-04 15:15:00",
67
+ "is_active": true,
68
+ "tags": [
69
+ "clearance"
70
+ ],
71
+ "attributes": {
72
+ "color": "yellow",
73
+ "size": "XL"
74
+ },
75
+ "optional_note": null,
76
+ "sku": "D-400",
77
+ "delivery_dates": [
78
+ "2023-01-08"
79
+ ]
80
+ },
81
+ {
82
+ "id": 5,
83
+ "name": "Product E",
84
+ "category": "B",
85
+ "price": 30.0,
86
+ "created_at": "2023-01-05 11:20:00",
87
+ "is_active": true,
88
+ "tags": [
89
+ "premium",
90
+ "gift"
91
+ ],
92
+ "attributes": {
93
+ "color": "black",
94
+ "size": "M"
95
+ },
96
+ "optional_note": "Limited stock",
97
+ "sku": "E-500",
98
+ "delivery_dates": [
99
+ "2023-01-07",
100
+ "2023-01-09"
101
+ ]
102
+ },
103
+ {
104
+ "id": 6,
105
+ "name": "Product F",
106
+ "category": "D",
107
+ "price": 12.5,
108
+ "created_at": "2023-01-06 14:40:00",
109
+ "is_active": false,
110
+ "tags": [],
111
+ "attributes": {
112
+ "color": "white",
113
+ "size": "M"
114
+ },
115
+ "optional_note": null,
116
+ "sku": "F-600",
117
+ "delivery_dates": []
118
+ }
119
+ ],
120
+ "users": [
121
+ {
122
+ "id": 1,
123
+ "user_name": "john_doe",
124
+ "email": "john@example.com",
125
+ "status": "active",
126
+ "created_at": "2023-01-01"
127
+ },
128
+ {
129
+ "id": 2,
130
+ "user_name": "jane_smith",
131
+ "email": "jane@example.com",
132
+ "status": "active",
133
+ "created_at": "2023-01-02"
134
+ },
135
+ {
136
+ "id": 3,
137
+ "user_name": "bob_jones",
138
+ "email": "bob@example.com",
139
+ "status": "inactive",
140
+ "created_at": "2023-01-03"
141
+ }
142
+ ],
143
+ "orders": [
144
+ {
145
+ "id": 1,
146
+ "user_id": 1,
147
+ "product_id": 1,
148
+ "quantity": 2,
149
+ "total": 21.0,
150
+ "status": "completed",
151
+ "created_at": "2023-01-10"
152
+ },
153
+ {
154
+ "id": 2,
155
+ "user_id": 1,
156
+ "product_id": 3,
157
+ "quantity": 1,
158
+ "total": 15.0,
159
+ "status": "completed",
160
+ "created_at": "2023-01-11"
161
+ },
162
+ {
163
+ "id": 3,
164
+ "user_id": 2,
165
+ "product_id": 2,
166
+ "quantity": 3,
167
+ "total": 62.25,
168
+ "status": "pending",
169
+ "created_at": "2023-01-12"
170
+ },
171
+ {
172
+ "id": 4,
173
+ "user_id": 2,
174
+ "product_id": 5,
175
+ "quantity": 1,
176
+ "total": 30.0,
177
+ "status": "completed",
178
+ "created_at": "2023-01-13"
179
+ },
180
+ {
181
+ "id": 5,
182
+ "user_id": 3,
183
+ "product_id": 4,
184
+ "quantity": 2,
185
+ "total": 16.5,
186
+ "status": "cancelled",
187
+ "created_at": "2023-01-14"
188
+ }
189
+ ]
190
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/core/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,CAa9C;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,CAgBvE"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/core/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,CAc9C;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,CAgBvE"}
@@ -6,7 +6,8 @@ export function escapeValue(value) {
6
6
  return value.toString();
7
7
  }
8
8
  else if (typeof value === 'string') {
9
- return `'${value.replace(/'/g, "''")}'`;
9
+ const escaped = value.replace(/\\/g, '\\\\').replace(/'/g, "''");
10
+ return `'${escaped}'`;
10
11
  }
11
12
  else if (value instanceof Date) {
12
13
  return `'${value.toISOString()}'`;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * ClickHouse Semantic Backend
3
+ *
4
+ * This module implements the SemanticBackend interface from @hypequery/datasets
5
+ * for ClickHouse databases. It translates database-agnostic semantic plans
6
+ * (PlanNode) into ClickHouse-specific SQL and executes them.
7
+ *
8
+ * Architecture:
9
+ * - @hypequery/datasets: Owns semantic planning, validation, and execution protocol
10
+ * - @hypequery/clickhouse: Implements SQL translation and execution for ClickHouse
11
+ *
12
+ * Usage:
13
+ * ```ts
14
+ * import { createDatasetClient } from '@hypequery/datasets';
15
+ * import { createBackend } from '@hypequery/clickhouse';
16
+ *
17
+ * const analytics = createDatasetClient({
18
+ * backend: createBackend({
19
+ * url: process.env.CLICKHOUSE_URL,
20
+ * username: process.env.CLICKHOUSE_USER,
21
+ * password: process.env.CLICKHOUSE_PASSWORD,
22
+ * database: process.env.CLICKHOUSE_DATABASE,
23
+ * })
24
+ * });
25
+ * ```
26
+ */
27
+ import { type SemanticBackend } from '@hypequery/datasets';
28
+ import type { CreateQueryBuilderConfig } from './core/query-builder.js';
29
+ import type { SchemaDefinition } from './core/types/builder-state.js';
30
+ export type CreateBackendConfig = CreateQueryBuilderConfig;
31
+ /**
32
+ * Create ClickHouse Semantic Backend
33
+ *
34
+ * Creates a SemanticBackend implementation that translates database-agnostic
35
+ * semantic plans into ClickHouse SQL and executes them.
36
+ *
37
+ * @param config - ClickHouse connection configuration
38
+ * @returns SemanticBackend interface for executing semantic queries
39
+ */
40
+ export declare function createBackend<Schema extends SchemaDefinition<Schema>>(config: CreateBackendConfig): SemanticBackend;
41
+ //# sourceMappingURL=datasets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"datasets.d.ts","sourceRoot":"","sources":["../src/datasets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAGL,KAAK,eAAe,EAGrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEtE,MAAM,MAAM,mBAAmB,GAAG,wBAAwB,CAAC;AAuW3D;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,MAAM,SAAS,gBAAgB,CAAC,MAAM,CAAC,EACnE,MAAM,EAAE,mBAAmB,GAC1B,eAAe,CAmDjB"}
@@ -0,0 +1,387 @@
1
+ /**
2
+ * ClickHouse Semantic Backend
3
+ *
4
+ * This module implements the SemanticBackend interface from @hypequery/datasets
5
+ * for ClickHouse databases. It translates database-agnostic semantic plans
6
+ * (PlanNode) into ClickHouse-specific SQL and executes them.
7
+ *
8
+ * Architecture:
9
+ * - @hypequery/datasets: Owns semantic planning, validation, and execution protocol
10
+ * - @hypequery/clickhouse: Implements SQL translation and execution for ClickHouse
11
+ *
12
+ * Usage:
13
+ * ```ts
14
+ * import { createDatasetClient } from '@hypequery/datasets';
15
+ * import { createBackend } from '@hypequery/clickhouse';
16
+ *
17
+ * const analytics = createDatasetClient({
18
+ * backend: createBackend({
19
+ * url: process.env.CLICKHOUSE_URL,
20
+ * username: process.env.CLICKHOUSE_USER,
21
+ * password: process.env.CLICKHOUSE_PASSWORD,
22
+ * database: process.env.CLICKHOUSE_DATABASE,
23
+ * })
24
+ * });
25
+ * ```
26
+ */
27
+ import { createQueryBuilder } from './core/query-builder.js';
28
+ // =============================================================================
29
+ // ClickHouse SQL Generation Utilities
30
+ // =============================================================================
31
+ const GRAIN_FUNCTIONS = {
32
+ day: 'toStartOfDay',
33
+ week: 'toStartOfWeek',
34
+ month: 'toStartOfMonth',
35
+ quarter: 'toStartOfQuarter',
36
+ year: 'toStartOfYear',
37
+ };
38
+ /**
39
+ * SQL Literal Rendering
40
+ * Safely escapes values for direct SQL inclusion
41
+ */
42
+ function renderLiteral(value) {
43
+ if (value === null)
44
+ return 'NULL';
45
+ if (typeof value === 'number')
46
+ return String(value);
47
+ if (typeof value === 'boolean')
48
+ return value ? '1' : '0';
49
+ // SQL string escaping: single quotes are escaped by doubling them
50
+ return `'${String(value).replace(/'/g, "''")}'`;
51
+ }
52
+ /**
53
+ * Time Grain Rendering
54
+ * Converts semantic grain to ClickHouse function
55
+ */
56
+ function renderGrain(field, unit) {
57
+ return `${GRAIN_FUNCTIONS[unit]}(${field})`;
58
+ }
59
+ /**
60
+ * Filter Rendering - Applies filters using query builder WHERE clauses
61
+ * This is the preferred method when working with the query builder
62
+ */
63
+ function applyFilters(builder, filters) {
64
+ let qb = builder;
65
+ for (const filter of filters) {
66
+ qb = qb.where(filter.field, filter.operator, filter.value);
67
+ }
68
+ return qb;
69
+ }
70
+ /**
71
+ * Type guard to check if value is a valid literal for SQL rendering
72
+ */
73
+ function isLiteralValue(value) {
74
+ return (typeof value === 'string' ||
75
+ typeof value === 'number' ||
76
+ typeof value === 'boolean' ||
77
+ value === null);
78
+ }
79
+ /**
80
+ * Safely render a filter value as a SQL literal
81
+ * Throws if value is not a valid literal type
82
+ */
83
+ function renderFilterValue(value) {
84
+ if (!isLiteralValue(value)) {
85
+ throw new Error(`Invalid filter value type: ${typeof value}`);
86
+ }
87
+ return renderLiteral(value);
88
+ }
89
+ /**
90
+ * Filter Condition Rendering - Converts filter to SQL WHERE clause string
91
+ * Used for filtered aggregations (IF conditions) where query builder can't be used
92
+ */
93
+ function renderFilterCondition(filter) {
94
+ const { field, operator, value } = filter;
95
+ switch (operator) {
96
+ case 'eq':
97
+ return `${field} = ${renderFilterValue(value)}`;
98
+ case 'neq':
99
+ return `${field} != ${renderFilterValue(value)}`;
100
+ case 'gt':
101
+ return `${field} > ${renderFilterValue(value)}`;
102
+ case 'gte':
103
+ return `${field} >= ${renderFilterValue(value)}`;
104
+ case 'lt':
105
+ return `${field} < ${renderFilterValue(value)}`;
106
+ case 'lte':
107
+ return `${field} <= ${renderFilterValue(value)}`;
108
+ case 'like':
109
+ return `${field} LIKE ${renderFilterValue(value)}`;
110
+ case 'in':
111
+ case 'notIn': {
112
+ if (!Array.isArray(value) || value.length === 0) {
113
+ throw new Error(`"${operator}" filters require a non-empty array.`);
114
+ }
115
+ const values = value.map(renderFilterValue).join(', ');
116
+ const op = operator === 'in' ? 'IN' : 'NOT IN';
117
+ return `${field} ${op} (${values})`;
118
+ }
119
+ case 'between': {
120
+ if (!Array.isArray(value) || value.length !== 2) {
121
+ throw new Error('"between" filters require a two-item array.');
122
+ }
123
+ return `${field} BETWEEN ${renderFilterValue(value[0])} AND ${renderFilterValue(value[1])}`;
124
+ }
125
+ default:
126
+ throw new Error(`Unsupported filter operator "${operator}".`);
127
+ }
128
+ }
129
+ /**
130
+ * Filtered Aggregation Field Rendering
131
+ * Generates ClickHouse IF() expressions for conditional aggregations
132
+ * Example: SUM(if(status = 'completed', amount, 0))
133
+ */
134
+ function renderFilteredAggregationField(aggregation) {
135
+ if (!aggregation.filters?.length) {
136
+ return aggregation.field;
137
+ }
138
+ // Combine multiple filters with AND
139
+ const condition = aggregation.filters
140
+ .map(renderFilterCondition)
141
+ .map((part) => `(${part})`)
142
+ .join(' AND ');
143
+ // Use appropriate fallback: 0 for SUM, NULL for others
144
+ const fallback = aggregation.aggregation === 'sum' ? '0' : 'NULL';
145
+ return `if(${condition}, ${aggregation.field}, ${fallback})`;
146
+ }
147
+ /**
148
+ * Expression Rendering
149
+ * Converts semantic expressions (formulas) to SQL
150
+ * Used for derived metrics
151
+ */
152
+ function renderExpression(expression) {
153
+ switch (expression.kind) {
154
+ case 'ref':
155
+ return expression.name;
156
+ case 'literal':
157
+ return renderLiteral(expression.value);
158
+ case 'binary': {
159
+ const operators = {
160
+ add: '+',
161
+ subtract: '-',
162
+ multiply: '*',
163
+ divide: '/',
164
+ };
165
+ const op = operators[expression.operator];
166
+ const left = renderExpression(expression.left);
167
+ const right = renderExpression(expression.right);
168
+ return `(${left}) ${op} (${right})`;
169
+ }
170
+ case 'function': {
171
+ const args = expression.args.map(renderExpression);
172
+ // Special case functions with custom SQL
173
+ if (expression.name === 'nullIfZero') {
174
+ return `NULLIF(${args[0]}, 0)`;
175
+ }
176
+ if (expression.name === 'coalesce') {
177
+ return `COALESCE(${args.join(', ')})`;
178
+ }
179
+ // Standard functions
180
+ const functionMap = {
181
+ round: 'ROUND',
182
+ floor: 'FLOOR',
183
+ ceil: 'CEIL',
184
+ };
185
+ const fn = functionMap[expression.name];
186
+ if (!fn) {
187
+ throw new Error(`Unsupported function: ${expression.name}`);
188
+ }
189
+ return `${fn}(${args.join(', ')})`;
190
+ }
191
+ default:
192
+ throw new Error('Unsupported semantic expression.');
193
+ }
194
+ }
195
+ // =============================================================================
196
+ // Query Builder Integration
197
+ // =============================================================================
198
+ /**
199
+ * Apply Aggregations
200
+ * Translates semantic aggregations to query builder method calls
201
+ */
202
+ function applyAggregations(builder, plan) {
203
+ let qb = builder;
204
+ for (const aggregation of plan.aggregations) {
205
+ const field = renderFilteredAggregationField(aggregation);
206
+ const { name, aggregation: aggType } = aggregation;
207
+ switch (aggType) {
208
+ case 'sum':
209
+ qb = qb.sum(field, name);
210
+ break;
211
+ case 'count':
212
+ qb = qb.count(field, name);
213
+ break;
214
+ case 'countDistinct':
215
+ qb = qb.countDistinct(field, name);
216
+ break;
217
+ case 'avg':
218
+ qb = qb.avg(field, name);
219
+ break;
220
+ case 'min':
221
+ qb = qb.min(field, name);
222
+ break;
223
+ case 'max':
224
+ qb = qb.max(field, name);
225
+ break;
226
+ }
227
+ }
228
+ return qb;
229
+ }
230
+ /**
231
+ * Append Order/Limit/Offset
232
+ * Applies result modifiers to query builder
233
+ */
234
+ function appendOrderLimitOffset(builder, plan) {
235
+ let qb = builder;
236
+ // Order by
237
+ for (const order of plan.orderBy ?? []) {
238
+ qb = qb.orderBy(order.field, order.direction.toUpperCase());
239
+ }
240
+ // Pagination
241
+ if (plan.limit != null)
242
+ qb = qb.limit(plan.limit);
243
+ if (plan.offset != null)
244
+ qb = qb.offset(plan.offset);
245
+ return qb;
246
+ }
247
+ // =============================================================================
248
+ // Plan Translation to SQL
249
+ // =============================================================================
250
+ /**
251
+ * Build Aggregate Query
252
+ * Translates semantic aggregate plan to ClickHouse query builder
253
+ */
254
+ function buildAggregateQuery(queryBuilder, plan) {
255
+ let qb = queryBuilder.table(plan.source);
256
+ // Build SELECT and GROUP BY for dimensions
257
+ const selectParts = [];
258
+ const groupByParts = [];
259
+ // Time grain (period column)
260
+ if (plan.grain) {
261
+ const grainSql = renderGrain(plan.grain.field, plan.grain.unit);
262
+ selectParts.push(`${grainSql} AS ${plan.grain.output}`);
263
+ groupByParts.push(plan.grain.output);
264
+ }
265
+ // Dimensions
266
+ for (const dimension of plan.dimensions) {
267
+ const columnSql = dimension.field === dimension.name
268
+ ? dimension.name
269
+ : `${dimension.field} AS ${dimension.name}`;
270
+ selectParts.push(columnSql);
271
+ groupByParts.push(dimension.name);
272
+ }
273
+ // Apply SELECT clause
274
+ if (selectParts.length > 0) {
275
+ qb = qb.select(selectParts);
276
+ }
277
+ // Apply aggregations (measures)
278
+ qb = applyAggregations(qb, plan);
279
+ // Apply GROUP BY
280
+ if (groupByParts.length > 0) {
281
+ qb = qb.groupBy(groupByParts);
282
+ }
283
+ // Apply tenant filter (auto-injected)
284
+ if (plan.tenant) {
285
+ qb = qb.where(plan.tenant.field, 'eq', plan.tenant.value);
286
+ }
287
+ // Apply user filters
288
+ qb = applyFilters(qb, plan.filters);
289
+ // Apply order/limit/offset
290
+ return appendOrderLimitOffset(qb, plan);
291
+ }
292
+ /**
293
+ * Build Derived Metric SQL
294
+ * Generates CTE-based query for derived metrics (formulas over base metrics)
295
+ *
296
+ * Note: Uses string concatenation for outer query since query builder
297
+ * doesn't support CTEs natively. Inner query uses query builder for safety.
298
+ */
299
+ function buildDerivedSQL(queryBuilder, plan) {
300
+ if (plan.input.kind !== 'aggregate') {
301
+ throw new Error('ClickHouse datasets currently supports derived metrics over aggregate input plans only.');
302
+ }
303
+ // Build inner aggregate query using query builder
304
+ const inputQuery = buildAggregateQuery(queryBuilder, plan.input);
305
+ const { sql, parameters } = inputQuery.toSQLWithParams();
306
+ // Passthrough columns (grain + dimensions)
307
+ const passthrough = [
308
+ ...(plan.input.grain ? [plan.input.grain.output] : []),
309
+ ...plan.input.dimensions.map((dim) => dim.name),
310
+ ];
311
+ // Derived metric calculations
312
+ const metricSelects = plan.metrics.map((metric) => `${renderExpression(metric.expression)} AS ${metric.name}`);
313
+ // Build outer query with CTE
314
+ const allSelects = [...passthrough, ...metricSelects];
315
+ let outerSql = `WITH base AS (${sql}) SELECT ${allSelects.join(', ')} FROM base`;
316
+ // ORDER BY
317
+ if (plan.orderBy?.length) {
318
+ const orderClauses = plan.orderBy.map((order) => `${order.field} ${order.direction.toUpperCase()}`);
319
+ outerSql += ` ORDER BY ${orderClauses.join(', ')}`;
320
+ }
321
+ // LIMIT and OFFSET
322
+ if (plan.limit != null) {
323
+ outerSql += ` LIMIT ${plan.limit}`;
324
+ }
325
+ if (plan.offset != null) {
326
+ outerSql += ` OFFSET ${plan.offset}`;
327
+ }
328
+ return { sql: outerSql, parameters };
329
+ }
330
+ // =============================================================================
331
+ // Semantic Backend Implementation
332
+ // =============================================================================
333
+ /**
334
+ * Create ClickHouse Semantic Backend
335
+ *
336
+ * Creates a SemanticBackend implementation that translates database-agnostic
337
+ * semantic plans into ClickHouse SQL and executes them.
338
+ *
339
+ * @param config - ClickHouse connection configuration
340
+ * @returns SemanticBackend interface for executing semantic queries
341
+ */
342
+ export function createBackend(config) {
343
+ const queryBuilder = createQueryBuilder(config);
344
+ return {
345
+ /**
346
+ * Execute a semantic plan and return results
347
+ */
348
+ async execute(plan) {
349
+ const start = Date.now();
350
+ if (plan.kind === 'aggregate') {
351
+ // Base metrics: use query builder for full safety
352
+ const query = buildAggregateQuery(queryBuilder, plan);
353
+ const { sql } = query.toSQLWithParams();
354
+ const data = await query.execute();
355
+ return {
356
+ data,
357
+ meta: {
358
+ sql,
359
+ timingMs: Date.now() - start,
360
+ tenant: plan.tenant?.value,
361
+ },
362
+ };
363
+ }
364
+ // Derived metrics: CTE query with formulas
365
+ const { sql, parameters } = buildDerivedSQL(queryBuilder, plan);
366
+ const data = await queryBuilder.rawQuery(sql, parameters);
367
+ const tenant = plan.input.kind === 'aggregate' ? plan.input.tenant?.value : undefined;
368
+ return {
369
+ data,
370
+ meta: {
371
+ sql,
372
+ timingMs: Date.now() - start,
373
+ tenant,
374
+ },
375
+ };
376
+ },
377
+ /**
378
+ * Generate SQL without executing
379
+ */
380
+ async explain(plan) {
381
+ if (plan.kind === 'aggregate') {
382
+ return { sql: buildAggregateQuery(queryBuilder, plan).toSQLWithParams().sql };
383
+ }
384
+ return { sql: buildDerivedSQL(queryBuilder, plan).sql };
385
+ },
386
+ };
387
+ }
@@ -0,0 +1,3 @@
1
+ export { DEFAULT_MIGRATIONS_OUT_DIR, DEFAULT_MIGRATIONS_PREFIX, DEFAULT_MIGRATIONS_TABLE, defineConfig, resolveClickHouseConfig, } from './types.js';
2
+ export type { ClickHouseClusterConfig, ClickHouseMigrationDbCredentials, ClickHouseMigrationDirectoryConfig, HypequeryClickHouseConfig, MigrationFilePrefix, ResolvedHypequeryClickHouseConfig, } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/migrations/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,yBAAyB,EACzB,wBAAwB,EACxB,YAAY,EACZ,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,uBAAuB,EACvB,gCAAgC,EAChC,kCAAkC,EAClC,yBAAyB,EACzB,mBAAmB,EACnB,iCAAiC,GAClC,MAAM,YAAY,CAAC"}
@@ -0,0 +1 @@
1
+ export { DEFAULT_MIGRATIONS_OUT_DIR, DEFAULT_MIGRATIONS_PREFIX, DEFAULT_MIGRATIONS_TABLE, defineConfig, resolveClickHouseConfig, } from './types.js';