@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.
- package/dist/core/tests/integration/test-data.json +190 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/utils.js +2 -1
- package/dist/datasets.d.ts +41 -0
- package/dist/datasets.d.ts.map +1 -0
- package/dist/datasets.js +387 -0
- package/dist/migrations/config/index.d.ts +3 -0
- package/dist/migrations/config/index.d.ts.map +1 -0
- package/dist/migrations/config/index.js +1 -0
- package/dist/migrations/config/types.d.ts +45 -0
- package/dist/migrations/config/types.d.ts.map +1 -0
- package/dist/migrations/config/types.js +28 -0
- package/dist/migrations/diff/diff.d.ts +11 -0
- package/dist/migrations/diff/diff.d.ts.map +1 -0
- package/dist/migrations/diff/diff.js +240 -0
- package/dist/migrations/diff/index.d.ts +3 -0
- package/dist/migrations/diff/index.d.ts.map +1 -0
- package/dist/migrations/diff/index.js +1 -0
- package/dist/migrations/diff/types.d.ts +74 -0
- package/dist/migrations/diff/types.d.ts.map +1 -0
- package/dist/migrations/diff/types.js +1 -0
- package/dist/migrations/introspect/index.d.ts +3 -0
- package/dist/migrations/introspect/index.d.ts.map +1 -0
- package/dist/migrations/introspect/index.js +1 -0
- package/dist/migrations/introspect/pull-schema.d.ts +23 -0
- package/dist/migrations/introspect/pull-schema.d.ts.map +1 -0
- package/dist/migrations/introspect/pull-schema.js +135 -0
- package/dist/migrations/plan/index.d.ts +3 -0
- package/dist/migrations/plan/index.d.ts.map +1 -0
- package/dist/migrations/plan/index.js +1 -0
- package/dist/migrations/plan/plan.d.ts +12 -0
- package/dist/migrations/plan/plan.d.ts.map +1 -0
- package/dist/migrations/plan/plan.js +416 -0
- package/dist/migrations/plan/types.d.ts +93 -0
- package/dist/migrations/plan/types.d.ts.map +1 -0
- package/dist/migrations/plan/types.js +1 -0
- package/dist/migrations/schema/column.d.ts +71 -0
- package/dist/migrations/schema/column.d.ts.map +1 -0
- package/dist/migrations/schema/column.js +123 -0
- package/dist/migrations/schema/define.d.ts +24 -0
- package/dist/migrations/schema/define.d.ts.map +1 -0
- package/dist/migrations/schema/define.js +47 -0
- package/dist/migrations/schema/index.d.ts +4 -0
- package/dist/migrations/schema/index.d.ts.map +1 -0
- package/dist/migrations/schema/index.js +2 -0
- package/dist/migrations/schema/types.d.ts +74 -0
- package/dist/migrations/schema/types.d.ts.map +1 -0
- package/dist/migrations/schema/types.js +1 -0
- package/dist/migrations/snapshot/index.d.ts +3 -0
- package/dist/migrations/snapshot/index.d.ts.map +1 -0
- package/dist/migrations/snapshot/index.js +1 -0
- package/dist/migrations/snapshot/serialize.d.ts +21 -0
- package/dist/migrations/snapshot/serialize.d.ts.map +1 -0
- package/dist/migrations/snapshot/serialize.js +127 -0
- package/dist/migrations/snapshot/types.d.ts +47 -0
- package/dist/migrations/snapshot/types.d.ts.map +1 -0
- package/dist/migrations/snapshot/types.js +1 -0
- package/dist/migrations/sql/index.d.ts +4 -0
- package/dist/migrations/sql/index.d.ts.map +1 -0
- package/dist/migrations/sql/index.js +2 -0
- package/dist/migrations/sql/render.d.ts +10 -0
- package/dist/migrations/sql/render.d.ts.map +1 -0
- package/dist/migrations/sql/render.js +347 -0
- package/dist/migrations/sql/types.d.ts +53 -0
- package/dist/migrations/sql/types.d.ts.map +1 -0
- package/dist/migrations/sql/types.js +1 -0
- package/dist/migrations/sql/write.d.ts +10 -0
- package/dist/migrations/sql/write.d.ts.map +1 -0
- package/dist/migrations/sql/write.js +35 -0
- package/dist/semantic-backend.d.ts +7 -0
- package/dist/semantic-backend.d.ts.map +1 -0
- package/dist/semantic-backend.js +208 -0
- package/package.json +1 -1
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { createMigrationPlan, isMigrationPlan } from '../plan/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Renders a snapshot diff into reviewable migration artifacts.
|
|
4
|
+
*
|
|
5
|
+
* The renderer produces forward SQL, best-effort reverse SQL, and metadata. It
|
|
6
|
+
* also sequences dependent materialized views around table mutations so stored
|
|
7
|
+
* view SELECT definitions are dropped and recreated with the target snapshot.
|
|
8
|
+
*/
|
|
9
|
+
export function renderMigrationArtifacts(input, options) {
|
|
10
|
+
const plan = isMigrationPlan(input) ? input : createMigrationPlan(input);
|
|
11
|
+
assertNoPlanBlockers(plan);
|
|
12
|
+
const upStatements = [];
|
|
13
|
+
const downStatements = [];
|
|
14
|
+
const consumedViewNames = new Set();
|
|
15
|
+
const renderedOperations = [];
|
|
16
|
+
let containsManualSteps = false;
|
|
17
|
+
for (const plannedOperation of normalizeOperationsForRendering(plan.operations)) {
|
|
18
|
+
const { operation } = plannedOperation;
|
|
19
|
+
if (isMaterializedViewOperation(operation) && consumedViewNames.has(getOperationViewName(operation))) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
renderedOperations.push({
|
|
23
|
+
kind: operation.kind,
|
|
24
|
+
classification: plannedOperation.classification,
|
|
25
|
+
});
|
|
26
|
+
upStatements.push(...renderOperationUp(operation, { plan, cluster: options.cluster }, consumedViewNames));
|
|
27
|
+
const renderedDown = renderOperationDown(operation, {
|
|
28
|
+
previousSnapshot: plan.previousSnapshot,
|
|
29
|
+
nextSnapshot: plan.nextSnapshot,
|
|
30
|
+
cluster: options.cluster,
|
|
31
|
+
}, consumedViewNames);
|
|
32
|
+
downStatements.unshift(...renderedDown.statements);
|
|
33
|
+
containsManualSteps = containsManualSteps || renderedDown.manual;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
upSql: joinStatements(upStatements),
|
|
37
|
+
downSql: joinStatements(downStatements),
|
|
38
|
+
meta: {
|
|
39
|
+
name: options.name,
|
|
40
|
+
timestamp: options.timestamp,
|
|
41
|
+
operations: renderedOperations,
|
|
42
|
+
sourceSnapshotHash: plan.sourceSnapshotHash,
|
|
43
|
+
targetSnapshotHash: plan.targetSnapshotHash,
|
|
44
|
+
custom: false,
|
|
45
|
+
unsafe: plan.diagnostics.some(diagnostic => diagnostic.level === 'warning') ||
|
|
46
|
+
plan.blockers.length > 0 ||
|
|
47
|
+
containsManualSteps,
|
|
48
|
+
containsManualSteps,
|
|
49
|
+
},
|
|
50
|
+
plan,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function renderOperationUp(operation, context, consumedViewNames) {
|
|
54
|
+
switch (operation.kind) {
|
|
55
|
+
case 'CreateTable':
|
|
56
|
+
return [renderCreateTable(operation.table, context.cluster)];
|
|
57
|
+
case 'DropTable':
|
|
58
|
+
return [renderDropTable(operation.tableName, context.cluster)];
|
|
59
|
+
case 'AddColumn':
|
|
60
|
+
return [renderAlterTableAddColumn(operation.tableName, operation.column, context.cluster)];
|
|
61
|
+
case 'DropColumn':
|
|
62
|
+
return [renderAlterTableDropColumn(operation.tableName, operation.columnName, context.cluster)];
|
|
63
|
+
case 'ModifyColumnDefault':
|
|
64
|
+
case 'ModifyColumnType': {
|
|
65
|
+
const nextColumn = requireColumn(context.plan.nextSnapshot, operation.tableName, operation.columnName);
|
|
66
|
+
return [renderAlterTableModifyColumn(operation.tableName, nextColumn, context.cluster)];
|
|
67
|
+
}
|
|
68
|
+
case 'CreateMaterializedView':
|
|
69
|
+
return [renderCreateMaterializedView({ view: operation.view, cluster: context.cluster })];
|
|
70
|
+
case 'DropMaterializedView':
|
|
71
|
+
return [renderDropMaterializedView(operation.viewName, context.cluster)];
|
|
72
|
+
case 'RecreateMaterializedView':
|
|
73
|
+
return [
|
|
74
|
+
renderDropMaterializedView(operation.previousView.name, context.cluster),
|
|
75
|
+
renderCreateMaterializedView({ view: operation.nextView, cluster: context.cluster }),
|
|
76
|
+
];
|
|
77
|
+
case 'AlterTableWithDependentViews':
|
|
78
|
+
return renderAlterTableWithDependentViewsUp(operation, context, consumedViewNames);
|
|
79
|
+
default: {
|
|
80
|
+
const exhaustiveCheck = operation;
|
|
81
|
+
return exhaustiveCheck;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function renderOperationDown(operation, lookup, consumedViewNames) {
|
|
86
|
+
switch (operation.kind) {
|
|
87
|
+
case 'CreateTable':
|
|
88
|
+
return { statements: [renderDropTable(operation.table.name, lookup.cluster)], manual: false };
|
|
89
|
+
case 'DropTable':
|
|
90
|
+
return { statements: [manualDownComment(`recreate dropped table "${operation.tableName}" manually`)], manual: true };
|
|
91
|
+
case 'AddColumn':
|
|
92
|
+
return {
|
|
93
|
+
statements: [renderAlterTableDropColumn(operation.tableName, operation.column.name, lookup.cluster)],
|
|
94
|
+
manual: false,
|
|
95
|
+
};
|
|
96
|
+
case 'DropColumn':
|
|
97
|
+
return {
|
|
98
|
+
statements: [manualDownComment(`recreate dropped column "${operation.tableName}.${operation.columnName}" manually`)],
|
|
99
|
+
manual: true,
|
|
100
|
+
};
|
|
101
|
+
case 'ModifyColumnDefault': {
|
|
102
|
+
const previousColumn = requireColumn(lookup.previousSnapshot, operation.tableName, operation.columnName);
|
|
103
|
+
return {
|
|
104
|
+
statements: [renderAlterTableModifyColumn(operation.tableName, previousColumn, lookup.cluster)],
|
|
105
|
+
manual: false,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
case 'ModifyColumnType':
|
|
109
|
+
return {
|
|
110
|
+
statements: [manualDownComment(`revert type change for "${operation.tableName}.${operation.columnName}" manually`)],
|
|
111
|
+
manual: true,
|
|
112
|
+
};
|
|
113
|
+
case 'CreateMaterializedView':
|
|
114
|
+
return { statements: [renderDropMaterializedView(operation.view.name, lookup.cluster)], manual: false };
|
|
115
|
+
case 'DropMaterializedView': {
|
|
116
|
+
const previousView = requireMaterializedView(lookup.previousSnapshot, operation.viewName);
|
|
117
|
+
return {
|
|
118
|
+
statements: [renderCreateMaterializedView({ view: previousView, cluster: lookup.cluster })],
|
|
119
|
+
manual: false,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
case 'RecreateMaterializedView':
|
|
123
|
+
return {
|
|
124
|
+
statements: [
|
|
125
|
+
renderDropMaterializedView(operation.nextView.name, lookup.cluster),
|
|
126
|
+
renderCreateMaterializedView({ view: operation.previousView, cluster: lookup.cluster }),
|
|
127
|
+
],
|
|
128
|
+
manual: false,
|
|
129
|
+
};
|
|
130
|
+
case 'AlterTableWithDependentViews':
|
|
131
|
+
return renderAlterTableWithDependentViewsDown(operation, lookup, consumedViewNames);
|
|
132
|
+
default: {
|
|
133
|
+
const exhaustiveCheck = operation;
|
|
134
|
+
return exhaustiveCheck;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function renderAlterTableWithDependentViewsUp(operation, context, consumedViewNames) {
|
|
139
|
+
const statements = [];
|
|
140
|
+
for (const viewName of operation.dependentViewNames) {
|
|
141
|
+
statements.push(renderDropMaterializedView(viewName, context.cluster));
|
|
142
|
+
consumedViewNames.add(viewName);
|
|
143
|
+
}
|
|
144
|
+
for (const nestedOperation of normalizeTableMutationOperations(operation.operations)) {
|
|
145
|
+
statements.push(...renderOperationUp(nestedOperation, context, consumedViewNames));
|
|
146
|
+
}
|
|
147
|
+
for (const viewName of operation.dependentViewNames) {
|
|
148
|
+
statements.push(renderCreateMaterializedView({
|
|
149
|
+
view: requireMaterializedView(context.plan.nextSnapshot, viewName),
|
|
150
|
+
cluster: context.cluster,
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
return statements;
|
|
154
|
+
}
|
|
155
|
+
function renderAlterTableWithDependentViewsDown(operation, lookup, consumedViewNames) {
|
|
156
|
+
const statements = [];
|
|
157
|
+
let manual = false;
|
|
158
|
+
for (const viewName of operation.dependentViewNames) {
|
|
159
|
+
statements.push(renderDropMaterializedView(viewName, lookup.cluster));
|
|
160
|
+
consumedViewNames.add(viewName);
|
|
161
|
+
}
|
|
162
|
+
for (const nestedOperation of [...normalizeTableMutationOperations(operation.operations)].reverse()) {
|
|
163
|
+
const rendered = renderOperationDown(nestedOperation, lookup, consumedViewNames);
|
|
164
|
+
statements.push(...rendered.statements);
|
|
165
|
+
manual = manual || rendered.manual;
|
|
166
|
+
}
|
|
167
|
+
for (const viewName of operation.dependentViewNames) {
|
|
168
|
+
statements.push(renderCreateMaterializedView({
|
|
169
|
+
view: requireMaterializedView(lookup.previousSnapshot, viewName),
|
|
170
|
+
cluster: lookup.cluster,
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
return { statements, manual };
|
|
174
|
+
}
|
|
175
|
+
function renderCreateTable(table, cluster) {
|
|
176
|
+
const columns = table.columns
|
|
177
|
+
.map(column => ` ${quoteIdentifier(column.name)} ${column.type}${renderColumnDefault(column)}`)
|
|
178
|
+
.join(',\n');
|
|
179
|
+
const settings = Object.keys(table.settings).length > 0
|
|
180
|
+
? `\nSETTINGS ${Object.entries(table.settings).map(([key, value]) => `${key} = ${value}`).join(', ')}`
|
|
181
|
+
: '';
|
|
182
|
+
return [
|
|
183
|
+
`CREATE TABLE ${quoteIdentifier(table.name)}${renderClusterClause(cluster)} (`,
|
|
184
|
+
columns,
|
|
185
|
+
`) ENGINE = ${renderTableEngine(table)}${settings};`,
|
|
186
|
+
].join('\n');
|
|
187
|
+
}
|
|
188
|
+
function renderDropTable(tableName, cluster) {
|
|
189
|
+
return `DROP TABLE ${quoteIdentifier(tableName)}${renderClusterClause(cluster)};`;
|
|
190
|
+
}
|
|
191
|
+
function renderAlterTableAddColumn(tableName, column, cluster) {
|
|
192
|
+
return `ALTER TABLE ${quoteIdentifier(tableName)}${renderClusterClause(cluster)} ADD COLUMN ` +
|
|
193
|
+
`${quoteIdentifier(column.name)} ${column.type}${renderColumnDefault(column)};`;
|
|
194
|
+
}
|
|
195
|
+
function renderAlterTableDropColumn(tableName, columnName, cluster) {
|
|
196
|
+
return `ALTER TABLE ${quoteIdentifier(tableName)}${renderClusterClause(cluster)} DROP COLUMN ${quoteIdentifier(columnName)};`;
|
|
197
|
+
}
|
|
198
|
+
function renderAlterTableModifyColumn(tableName, column, cluster) {
|
|
199
|
+
return `ALTER TABLE ${quoteIdentifier(tableName)}${renderClusterClause(cluster)} MODIFY COLUMN ` +
|
|
200
|
+
`${quoteIdentifier(column.name)} ${column.type}${renderColumnDefault(column)};`;
|
|
201
|
+
}
|
|
202
|
+
function renderCreateMaterializedView(context) {
|
|
203
|
+
const toClause = context.view.to ? `\nTO ${quoteIdentifier(context.view.to)}` : '';
|
|
204
|
+
return [
|
|
205
|
+
`CREATE MATERIALIZED VIEW ${quoteIdentifier(context.view.name)}${renderClusterClause(context.cluster)}${toClause} AS`,
|
|
206
|
+
context.view.select,
|
|
207
|
+
';',
|
|
208
|
+
].join('\n');
|
|
209
|
+
}
|
|
210
|
+
function renderDropMaterializedView(viewName, cluster) {
|
|
211
|
+
return `DROP TABLE ${quoteIdentifier(viewName)}${renderClusterClause(cluster)};`;
|
|
212
|
+
}
|
|
213
|
+
function renderTableEngine(table) {
|
|
214
|
+
const parts = [table.engine.type, `ORDER BY (${table.engine.orderBy.join(', ')})`];
|
|
215
|
+
if (table.engine.partitionBy) {
|
|
216
|
+
parts.push(`PARTITION BY ${table.engine.partitionBy}`);
|
|
217
|
+
}
|
|
218
|
+
if (table.engine.primaryKey.length > 0) {
|
|
219
|
+
parts.push(`PRIMARY KEY (${table.engine.primaryKey.join(', ')})`);
|
|
220
|
+
}
|
|
221
|
+
if (table.engine.sampleBy) {
|
|
222
|
+
parts.push(`SAMPLE BY ${table.engine.sampleBy}`);
|
|
223
|
+
}
|
|
224
|
+
return parts.join('\n');
|
|
225
|
+
}
|
|
226
|
+
function renderColumnDefault(column) {
|
|
227
|
+
return column.default !== undefined ? ` DEFAULT ${renderDefaultValue(column.default)}` : '';
|
|
228
|
+
}
|
|
229
|
+
function renderClusterClause(cluster) {
|
|
230
|
+
return cluster ? ` ON CLUSTER ${quoteIdentifier(cluster)}` : '';
|
|
231
|
+
}
|
|
232
|
+
function quoteIdentifier(identifier) {
|
|
233
|
+
if (identifier.trim() === '') {
|
|
234
|
+
throw new Error('Invalid ClickHouse identifier: identifiers must not be empty.');
|
|
235
|
+
}
|
|
236
|
+
return `\`${identifier.replace(/`/g, '``')}\``;
|
|
237
|
+
}
|
|
238
|
+
function requireColumn(snapshot, tableName, columnName) {
|
|
239
|
+
const table = snapshot.tables.find(candidate => candidate.name === tableName);
|
|
240
|
+
if (!table) {
|
|
241
|
+
throw new Error(`Table "${tableName}" not found in snapshot.`);
|
|
242
|
+
}
|
|
243
|
+
const column = table.columns.find(candidate => candidate.name === columnName);
|
|
244
|
+
if (!column) {
|
|
245
|
+
throw new Error(`Column "${tableName}.${columnName}" not found in snapshot.`);
|
|
246
|
+
}
|
|
247
|
+
return column;
|
|
248
|
+
}
|
|
249
|
+
function requireMaterializedView(snapshot, viewName) {
|
|
250
|
+
const view = snapshot.materializedViews.find(candidate => candidate.name === viewName);
|
|
251
|
+
if (!view) {
|
|
252
|
+
throw new Error(`Materialized view "${viewName}" not found in snapshot.`);
|
|
253
|
+
}
|
|
254
|
+
return view;
|
|
255
|
+
}
|
|
256
|
+
function isMaterializedViewOperation(operation) {
|
|
257
|
+
return operation.kind === 'DropMaterializedView' ||
|
|
258
|
+
operation.kind === 'CreateMaterializedView' ||
|
|
259
|
+
operation.kind === 'RecreateMaterializedView';
|
|
260
|
+
}
|
|
261
|
+
function getOperationViewName(operation) {
|
|
262
|
+
switch (operation.kind) {
|
|
263
|
+
case 'DropMaterializedView':
|
|
264
|
+
return operation.viewName;
|
|
265
|
+
case 'CreateMaterializedView':
|
|
266
|
+
return operation.view.name;
|
|
267
|
+
case 'RecreateMaterializedView':
|
|
268
|
+
return operation.nextView.name;
|
|
269
|
+
default: {
|
|
270
|
+
const exhaustiveCheck = operation;
|
|
271
|
+
return exhaustiveCheck;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function manualDownComment(message) {
|
|
276
|
+
return `-- MANUAL STEP REQUIRED: ${message}`;
|
|
277
|
+
}
|
|
278
|
+
function joinStatements(statements) {
|
|
279
|
+
return statements.join('\n\n').trim();
|
|
280
|
+
}
|
|
281
|
+
function renderDefaultValue(defaultValue) {
|
|
282
|
+
if (defaultValue.kind === 'sql') {
|
|
283
|
+
return defaultValue.value;
|
|
284
|
+
}
|
|
285
|
+
if (defaultValue.value === null) {
|
|
286
|
+
return 'NULL';
|
|
287
|
+
}
|
|
288
|
+
if (typeof defaultValue.value === 'number') {
|
|
289
|
+
return String(defaultValue.value);
|
|
290
|
+
}
|
|
291
|
+
if (typeof defaultValue.value === 'boolean') {
|
|
292
|
+
return defaultValue.value ? 'true' : 'false';
|
|
293
|
+
}
|
|
294
|
+
return `'${escapeStringLiteral(defaultValue.value)}'`;
|
|
295
|
+
}
|
|
296
|
+
function escapeStringLiteral(value) {
|
|
297
|
+
return value
|
|
298
|
+
.replace(/\\/g, '\\\\')
|
|
299
|
+
.replace(/\0/g, '\\0')
|
|
300
|
+
.replace(/\x08/g, '\\b')
|
|
301
|
+
.replace(/\f/g, '\\f')
|
|
302
|
+
.replace(/\n/g, '\\n')
|
|
303
|
+
.replace(/\r/g, '\\r')
|
|
304
|
+
.replace(/\t/g, '\\t')
|
|
305
|
+
.replace(/\v/g, '\\v')
|
|
306
|
+
.replace(/'/g, "\\'");
|
|
307
|
+
}
|
|
308
|
+
function normalizeOperationsForRendering(operations) {
|
|
309
|
+
return operations.map(plannedOperation => {
|
|
310
|
+
if (plannedOperation.operation.kind !== 'AlterTableWithDependentViews') {
|
|
311
|
+
return plannedOperation;
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
...plannedOperation,
|
|
315
|
+
operation: {
|
|
316
|
+
...plannedOperation.operation,
|
|
317
|
+
operations: normalizeTableMutationOperations(plannedOperation.operation.operations),
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}).filter(plannedOperation => {
|
|
321
|
+
if (plannedOperation.operation.kind !== 'ModifyColumnDefault') {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
const operation = plannedOperation.operation;
|
|
325
|
+
return !operations.some(candidate => candidate.operation.kind === 'ModifyColumnType' &&
|
|
326
|
+
candidate.operation.tableName === operation.tableName &&
|
|
327
|
+
candidate.operation.columnName === operation.columnName);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
function normalizeTableMutationOperations(operations) {
|
|
331
|
+
return operations.filter(operation => {
|
|
332
|
+
if (operation.kind !== 'ModifyColumnDefault') {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
return !operations.some(candidate => candidate.kind === 'ModifyColumnType' &&
|
|
336
|
+
candidate.tableName === operation.tableName &&
|
|
337
|
+
candidate.columnName === operation.columnName);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function assertNoPlanBlockers(plan) {
|
|
341
|
+
if (plan.blockers.length === 0) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
throw new Error('Cannot render migration with unsupported changes:\n' +
|
|
345
|
+
plan.blockers.map(blocker => `- ${blocker.message}`).join('\n') +
|
|
346
|
+
'\n\nUse a custom SQL migration for this change, or split the schema evolution into supported steps.');
|
|
347
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { MigrationOperation, SnapshotDiffResult } from '../diff/types.js';
|
|
2
|
+
import type { MigrationOperationClassification, MigrationPlan, MigrationPlanInput } from '../plan/types.js';
|
|
3
|
+
import type { Snapshot, SnapshotMaterializedView } from '../snapshot/types.js';
|
|
4
|
+
export interface MigrationMeta {
|
|
5
|
+
name: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
operations: Array<{
|
|
8
|
+
kind: MigrationOperation['kind'];
|
|
9
|
+
classification: MigrationOperationClassification;
|
|
10
|
+
}>;
|
|
11
|
+
sourceSnapshotHash: string;
|
|
12
|
+
targetSnapshotHash: string;
|
|
13
|
+
custom: boolean;
|
|
14
|
+
unsafe: boolean;
|
|
15
|
+
containsManualSteps: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface RenderMigrationArtifactsOptions {
|
|
18
|
+
name: string;
|
|
19
|
+
timestamp: string;
|
|
20
|
+
cluster?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface RenderMigrationArtifactsResult {
|
|
23
|
+
upSql: string;
|
|
24
|
+
downSql: string;
|
|
25
|
+
meta: MigrationMeta;
|
|
26
|
+
plan: MigrationPlan;
|
|
27
|
+
}
|
|
28
|
+
export interface RenderSqlContext {
|
|
29
|
+
plan: MigrationPlan;
|
|
30
|
+
cluster?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface MaterializedViewRenderContext {
|
|
33
|
+
view: SnapshotMaterializedView;
|
|
34
|
+
cluster?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface WriteMigrationArtifactsOptions {
|
|
37
|
+
outDir: string;
|
|
38
|
+
migrationName: string;
|
|
39
|
+
artifacts: RenderMigrationArtifactsResult;
|
|
40
|
+
}
|
|
41
|
+
export interface WriteMigrationArtifactsResult {
|
|
42
|
+
migrationDir: string;
|
|
43
|
+
upPath: string;
|
|
44
|
+
downPath: string;
|
|
45
|
+
metaPath: string;
|
|
46
|
+
planPath: string;
|
|
47
|
+
}
|
|
48
|
+
export interface SnapshotLookup {
|
|
49
|
+
previousSnapshot: Snapshot;
|
|
50
|
+
nextSnapshot: Snapshot;
|
|
51
|
+
}
|
|
52
|
+
export type RenderMigrationArtifactsInput = MigrationPlanInput | SnapshotDiffResult;
|
|
53
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/migrations/sql/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC/E,OAAO,KAAK,EACV,gCAAgC,EAChC,aAAa,EACb,kBAAkB,EACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,QAAQ,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAE/E,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;QACjC,cAAc,EAAE,gCAAgC,CAAC;KAClD,CAAC,CAAC;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,+BAA+B;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,aAAa,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,wBAAwB,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,8BAA8B;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,8BAA8B,CAAC;CAC3C;AAED,MAAM,WAAW,6BAA6B;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,gBAAgB,EAAE,QAAQ,CAAC;IAC3B,YAAY,EAAE,QAAQ,CAAC;CACxB;AAED,MAAM,MAAM,6BAA6B,GAAG,kBAAkB,GAAG,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { WriteMigrationArtifactsOptions, WriteMigrationArtifactsResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Writes rendered migration artifacts to a migration directory.
|
|
4
|
+
*
|
|
5
|
+
* The migration name must be a single safe path segment. The writer creates
|
|
6
|
+
* `up.sql`, `down.sql`, `meta.json`, and `plan.json` files under
|
|
7
|
+
* `outDir/migrationName`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function writeMigrationArtifacts(options: WriteMigrationArtifactsOptions): Promise<WriteMigrationArtifactsResult>;
|
|
10
|
+
//# sourceMappingURL=write.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../../src/migrations/sql/write.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,8BAA8B,EAC9B,6BAA6B,EAC9B,MAAM,YAAY,CAAC;AAEpB;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,6BAA6B,CAAC,CAuBxC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Writes rendered migration artifacts to a migration directory.
|
|
5
|
+
*
|
|
6
|
+
* The migration name must be a single safe path segment. The writer creates
|
|
7
|
+
* `up.sql`, `down.sql`, `meta.json`, and `plan.json` files under
|
|
8
|
+
* `outDir/migrationName`.
|
|
9
|
+
*/
|
|
10
|
+
export async function writeMigrationArtifacts(options) {
|
|
11
|
+
assertValidMigrationName(options.migrationName);
|
|
12
|
+
const migrationDir = path.join(options.outDir, options.migrationName);
|
|
13
|
+
await mkdir(migrationDir, { recursive: true });
|
|
14
|
+
const upPath = path.join(migrationDir, 'up.sql');
|
|
15
|
+
const downPath = path.join(migrationDir, 'down.sql');
|
|
16
|
+
const metaPath = path.join(migrationDir, 'meta.json');
|
|
17
|
+
const planPath = path.join(migrationDir, 'plan.json');
|
|
18
|
+
await writeFile(upPath, `${options.artifacts.upSql}\n`, 'utf8');
|
|
19
|
+
await writeFile(downPath, `${options.artifacts.downSql}\n`, 'utf8');
|
|
20
|
+
await writeFile(metaPath, `${JSON.stringify(options.artifacts.meta, null, 2)}\n`, 'utf8');
|
|
21
|
+
await writeFile(planPath, `${JSON.stringify(options.artifacts.plan, null, 2)}\n`, 'utf8');
|
|
22
|
+
return {
|
|
23
|
+
migrationDir,
|
|
24
|
+
upPath,
|
|
25
|
+
downPath,
|
|
26
|
+
metaPath,
|
|
27
|
+
planPath,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function assertValidMigrationName(migrationName) {
|
|
31
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(migrationName)) {
|
|
32
|
+
throw new Error(`Invalid migration name "${migrationName}". ` +
|
|
33
|
+
'Migration names must be a single path segment containing only letters, numbers, underscores, and hyphens.');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type SemanticBackend } from '@hypequery/datasets';
|
|
2
|
+
export interface ClickHouseSemanticQueryBuilder {
|
|
3
|
+
table(tableName: string): any;
|
|
4
|
+
rawQuery<TResult = any>(sql: string, params?: unknown[]): Promise<TResult[]>;
|
|
5
|
+
}
|
|
6
|
+
export declare function createClickHouseSemanticBackendFromQueryBuilder(queryBuilder: ClickHouseSemanticQueryBuilder): SemanticBackend;
|
|
7
|
+
//# sourceMappingURL=semantic-backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantic-backend.d.ts","sourceRoot":"","sources":["../src/semantic-backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,eAAe,EAGrB,MAAM,qBAAqB,CAAC;AAU7B,MAAM,WAAW,8BAA8B;IAC7C,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,GAAG,CAAC;IAC9B,QAAQ,CAAC,OAAO,GAAG,GAAG,EACpB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;CACvB;AAsMD,wBAAgB,+CAA+C,CAC7D,YAAY,EAAE,8BAA8B,GAC3C,eAAe,CAuBjB"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
const GRAIN_FUNCTIONS = {
|
|
2
|
+
day: 'toStartOfDay',
|
|
3
|
+
week: 'toStartOfWeek',
|
|
4
|
+
month: 'toStartOfMonth',
|
|
5
|
+
quarter: 'toStartOfQuarter',
|
|
6
|
+
year: 'toStartOfYear',
|
|
7
|
+
};
|
|
8
|
+
function renderGrain(field, unit) {
|
|
9
|
+
return `${GRAIN_FUNCTIONS[unit]}(${field})`;
|
|
10
|
+
}
|
|
11
|
+
function applyFilters(builder, filters) {
|
|
12
|
+
let qb = builder;
|
|
13
|
+
for (const filter of filters) {
|
|
14
|
+
qb = qb.where(filter.field, filter.operator, filter.value);
|
|
15
|
+
}
|
|
16
|
+
return qb;
|
|
17
|
+
}
|
|
18
|
+
function renderLiteral(value) {
|
|
19
|
+
if (value === null)
|
|
20
|
+
return 'NULL';
|
|
21
|
+
if (typeof value === 'number')
|
|
22
|
+
return String(value);
|
|
23
|
+
if (typeof value === 'boolean')
|
|
24
|
+
return value ? '1' : '0';
|
|
25
|
+
if (value instanceof Date)
|
|
26
|
+
return `'${value.toISOString().replace(/'/g, "''")}'`;
|
|
27
|
+
if (typeof value === 'string')
|
|
28
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
29
|
+
throw new Error(`Unsupported filter literal value: ${String(value)}.`);
|
|
30
|
+
}
|
|
31
|
+
function renderFilterCondition(filter) {
|
|
32
|
+
switch (filter.operator) {
|
|
33
|
+
case 'eq':
|
|
34
|
+
return `${filter.field} = ${renderLiteral(filter.value)}`;
|
|
35
|
+
case 'neq':
|
|
36
|
+
return `${filter.field} != ${renderLiteral(filter.value)}`;
|
|
37
|
+
case 'gt':
|
|
38
|
+
return `${filter.field} > ${renderLiteral(filter.value)}`;
|
|
39
|
+
case 'gte':
|
|
40
|
+
return `${filter.field} >= ${renderLiteral(filter.value)}`;
|
|
41
|
+
case 'lt':
|
|
42
|
+
return `${filter.field} < ${renderLiteral(filter.value)}`;
|
|
43
|
+
case 'lte':
|
|
44
|
+
return `${filter.field} <= ${renderLiteral(filter.value)}`;
|
|
45
|
+
case 'like':
|
|
46
|
+
return `${filter.field} LIKE ${renderLiteral(filter.value)}`;
|
|
47
|
+
case 'in':
|
|
48
|
+
case 'notIn': {
|
|
49
|
+
if (!Array.isArray(filter.value) || filter.value.length === 0) {
|
|
50
|
+
throw new Error(`"${filter.operator}" filters require a non-empty array.`);
|
|
51
|
+
}
|
|
52
|
+
const values = filter.value.map((value) => renderLiteral(value)).join(', ');
|
|
53
|
+
return `${filter.field} ${filter.operator === 'in' ? 'IN' : 'NOT IN'} (${values})`;
|
|
54
|
+
}
|
|
55
|
+
case 'between': {
|
|
56
|
+
if (!Array.isArray(filter.value) || filter.value.length !== 2) {
|
|
57
|
+
throw new Error('"between" filters require a two-item array.');
|
|
58
|
+
}
|
|
59
|
+
return `${filter.field} BETWEEN ${renderLiteral(filter.value[0])} AND ${renderLiteral(filter.value[1])}`;
|
|
60
|
+
}
|
|
61
|
+
default:
|
|
62
|
+
throw new Error(`Unsupported filter operator "${filter.operator}".`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function renderFilteredAggregationField(aggregation) {
|
|
66
|
+
if (!aggregation.filters?.length) {
|
|
67
|
+
return aggregation.field;
|
|
68
|
+
}
|
|
69
|
+
const condition = aggregation.filters
|
|
70
|
+
.map(renderFilterCondition)
|
|
71
|
+
.map((part) => `(${part})`)
|
|
72
|
+
.join(' AND ');
|
|
73
|
+
const fallback = aggregation.aggregation === 'sum' ? '0' : 'NULL';
|
|
74
|
+
return `if(${condition}, ${aggregation.field}, ${fallback})`;
|
|
75
|
+
}
|
|
76
|
+
function renderExpression(expression) {
|
|
77
|
+
switch (expression.kind) {
|
|
78
|
+
case 'ref':
|
|
79
|
+
return expression.name;
|
|
80
|
+
case 'literal':
|
|
81
|
+
return renderLiteral(expression.value);
|
|
82
|
+
case 'binary': {
|
|
83
|
+
const operator = {
|
|
84
|
+
add: '+',
|
|
85
|
+
subtract: '-',
|
|
86
|
+
multiply: '*',
|
|
87
|
+
divide: '/',
|
|
88
|
+
}[expression.operator];
|
|
89
|
+
return `(${renderExpression(expression.left)}) ${operator} (${renderExpression(expression.right)})`;
|
|
90
|
+
}
|
|
91
|
+
case 'function': {
|
|
92
|
+
if (expression.name === 'nullIfZero') {
|
|
93
|
+
return `NULLIF(${renderExpression(expression.args[0])}, 0)`;
|
|
94
|
+
}
|
|
95
|
+
if (expression.name === 'coalesce') {
|
|
96
|
+
return `COALESCE(${expression.args.map(renderExpression).join(', ')})`;
|
|
97
|
+
}
|
|
98
|
+
const fn = {
|
|
99
|
+
round: 'ROUND',
|
|
100
|
+
floor: 'FLOOR',
|
|
101
|
+
ceil: 'CEIL',
|
|
102
|
+
}[expression.name];
|
|
103
|
+
return `${fn}(${expression.args.map(renderExpression).join(', ')})`;
|
|
104
|
+
}
|
|
105
|
+
default:
|
|
106
|
+
throw new Error('Unsupported semantic expression.');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function applyAggregations(builder, plan) {
|
|
110
|
+
let qb = builder;
|
|
111
|
+
for (const aggregation of plan.aggregations) {
|
|
112
|
+
const field = renderFilteredAggregationField(aggregation);
|
|
113
|
+
switch (aggregation.aggregation) {
|
|
114
|
+
case 'sum':
|
|
115
|
+
qb = qb.sum(field, aggregation.name);
|
|
116
|
+
break;
|
|
117
|
+
case 'count':
|
|
118
|
+
qb = qb.count(field, aggregation.name);
|
|
119
|
+
break;
|
|
120
|
+
case 'countDistinct':
|
|
121
|
+
qb = qb.countDistinct(field, aggregation.name);
|
|
122
|
+
break;
|
|
123
|
+
case 'avg':
|
|
124
|
+
qb = qb.avg(field, aggregation.name);
|
|
125
|
+
break;
|
|
126
|
+
case 'min':
|
|
127
|
+
qb = qb.min(field, aggregation.name);
|
|
128
|
+
break;
|
|
129
|
+
case 'max':
|
|
130
|
+
qb = qb.max(field, aggregation.name);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return qb;
|
|
135
|
+
}
|
|
136
|
+
function appendOrderLimitOffset(builder, plan) {
|
|
137
|
+
let qb = builder;
|
|
138
|
+
for (const order of plan.orderBy ?? []) {
|
|
139
|
+
qb = qb.orderBy(order.field, order.direction.toUpperCase());
|
|
140
|
+
}
|
|
141
|
+
if (plan.limit != null)
|
|
142
|
+
qb = qb.limit(plan.limit);
|
|
143
|
+
if (plan.offset != null)
|
|
144
|
+
qb = qb.offset(plan.offset);
|
|
145
|
+
return qb;
|
|
146
|
+
}
|
|
147
|
+
function buildAggregateQuery(queryBuilder, plan) {
|
|
148
|
+
let qb = queryBuilder.table(plan.source);
|
|
149
|
+
const selectParts = plan.dimensions.map((dimension) => (dimension.field === dimension.name ? dimension.name : `${dimension.field} AS ${dimension.name}`));
|
|
150
|
+
const groupByParts = plan.dimensions.map((dimension) => dimension.name);
|
|
151
|
+
if (plan.grain) {
|
|
152
|
+
selectParts.unshift(`${renderGrain(plan.grain.field, plan.grain.unit)} AS ${plan.grain.output}`);
|
|
153
|
+
groupByParts.unshift(plan.grain.output);
|
|
154
|
+
}
|
|
155
|
+
if (selectParts.length > 0)
|
|
156
|
+
qb = qb.select(selectParts);
|
|
157
|
+
qb = applyAggregations(qb, plan);
|
|
158
|
+
if (groupByParts.length > 0)
|
|
159
|
+
qb = qb.groupBy(groupByParts);
|
|
160
|
+
if (plan.tenant)
|
|
161
|
+
qb = qb.where(plan.tenant.field, 'eq', plan.tenant.value);
|
|
162
|
+
qb = applyFilters(qb, plan.filters);
|
|
163
|
+
return appendOrderLimitOffset(qb, plan);
|
|
164
|
+
}
|
|
165
|
+
function buildDerivedSQL(queryBuilder, plan) {
|
|
166
|
+
if (plan.input.kind !== 'aggregate') {
|
|
167
|
+
throw new Error('ClickHouse datasets currently supports derived metrics over aggregate input plans only.');
|
|
168
|
+
}
|
|
169
|
+
const inputQuery = buildAggregateQuery(queryBuilder, plan.input);
|
|
170
|
+
const { sql, parameters } = inputQuery.toSQLWithParams();
|
|
171
|
+
const passthrough = [
|
|
172
|
+
...(plan.input.grain ? [plan.input.grain.output] : []),
|
|
173
|
+
...plan.input.dimensions.map((dimension) => dimension.name),
|
|
174
|
+
];
|
|
175
|
+
const metricSelects = plan.metrics.map((metric) => (`${renderExpression(metric.expression)} AS ${metric.name}`));
|
|
176
|
+
let outerSql = `WITH base AS (${sql}) SELECT ${[...passthrough, ...metricSelects].join(', ')} FROM base`;
|
|
177
|
+
if (plan.orderBy?.length) {
|
|
178
|
+
outerSql += ` ORDER BY ${plan.orderBy.map((order) => (`${order.field} ${order.direction.toUpperCase()}`)).join(', ')}`;
|
|
179
|
+
}
|
|
180
|
+
if (plan.limit != null)
|
|
181
|
+
outerSql += ` LIMIT ${plan.limit}`;
|
|
182
|
+
if (plan.offset != null)
|
|
183
|
+
outerSql += ` OFFSET ${plan.offset}`;
|
|
184
|
+
return { sql: outerSql, parameters };
|
|
185
|
+
}
|
|
186
|
+
export function createClickHouseSemanticBackendFromQueryBuilder(queryBuilder) {
|
|
187
|
+
return {
|
|
188
|
+
async execute(plan) {
|
|
189
|
+
const start = Date.now();
|
|
190
|
+
if (plan.kind === 'aggregate') {
|
|
191
|
+
const query = buildAggregateQuery(queryBuilder, plan);
|
|
192
|
+
const { sql } = query.toSQLWithParams();
|
|
193
|
+
const data = await query.execute();
|
|
194
|
+
return { data, meta: { sql, timingMs: Date.now() - start, tenant: plan.tenant?.value } };
|
|
195
|
+
}
|
|
196
|
+
const { sql, parameters } = buildDerivedSQL(queryBuilder, plan);
|
|
197
|
+
const data = await queryBuilder.rawQuery(sql, parameters);
|
|
198
|
+
const tenant = plan.input.kind === 'aggregate' ? plan.input.tenant?.value : undefined;
|
|
199
|
+
return { data, meta: { sql, timingMs: Date.now() - start, tenant } };
|
|
200
|
+
},
|
|
201
|
+
async explain(plan) {
|
|
202
|
+
if (plan.kind === 'aggregate') {
|
|
203
|
+
return { sql: buildAggregateQuery(queryBuilder, plan).toSQLWithParams().sql };
|
|
204
|
+
}
|
|
205
|
+
return { sql: buildDerivedSQL(queryBuilder, plan).sql };
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|