@cubejs-backend/query-orchestrator 1.2.13 → 1.2.15

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 (27) hide show
  1. package/dist/src/orchestrator/PreAggregationLoadCache.d.ts +42 -0
  2. package/dist/src/orchestrator/PreAggregationLoadCache.d.ts.map +1 -0
  3. package/dist/src/orchestrator/PreAggregationLoadCache.js +172 -0
  4. package/dist/src/orchestrator/PreAggregationLoadCache.js.map +1 -0
  5. package/dist/src/orchestrator/PreAggregationLoader.d.ts +105 -0
  6. package/dist/src/orchestrator/PreAggregationLoader.d.ts.map +1 -0
  7. package/dist/src/orchestrator/PreAggregationLoader.js +742 -0
  8. package/dist/src/orchestrator/PreAggregationLoader.js.map +1 -0
  9. package/dist/src/orchestrator/PreAggregationPartitionRangeLoader.d.ts +65 -0
  10. package/dist/src/orchestrator/PreAggregationPartitionRangeLoader.d.ts.map +1 -0
  11. package/dist/src/orchestrator/PreAggregationPartitionRangeLoader.js +355 -0
  12. package/dist/src/orchestrator/PreAggregationPartitionRangeLoader.js.map +1 -0
  13. package/dist/src/orchestrator/PreAggregations.d.ts +35 -223
  14. package/dist/src/orchestrator/PreAggregations.d.ts.map +1 -1
  15. package/dist/src/orchestrator/PreAggregations.js +17 -1261
  16. package/dist/src/orchestrator/PreAggregations.js.map +1 -1
  17. package/dist/src/orchestrator/QueryCache.d.ts +14 -4
  18. package/dist/src/orchestrator/QueryCache.d.ts.map +1 -1
  19. package/dist/src/orchestrator/QueryCache.js +1 -1
  20. package/dist/src/orchestrator/QueryCache.js.map +1 -1
  21. package/dist/src/orchestrator/QueryQueue.js +1 -1
  22. package/dist/src/orchestrator/QueryQueue.js.map +1 -1
  23. package/dist/src/orchestrator/index.d.ts +3 -0
  24. package/dist/src/orchestrator/index.d.ts.map +1 -1
  25. package/dist/src/orchestrator/index.js +3 -0
  26. package/dist/src/orchestrator/index.js.map +1 -1
  27. package/package.json +6 -6
@@ -0,0 +1,742 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PreAggregationLoader = void 0;
7
+ const ramda_1 = __importDefault(require("ramda"));
8
+ const shared_1 = require("@cubejs-backend/shared");
9
+ const base_driver_1 = require("@cubejs-backend/base-driver");
10
+ const QueryCache_1 = require("./QueryCache");
11
+ const ContinueWaitError_1 = require("./ContinueWaitError");
12
+ const StreamObjectsCounter_1 = require("./StreamObjectsCounter");
13
+ const PreAggregations_1 = require("./PreAggregations");
14
+ // There are community developed and custom drivers which not always up-to-date with latest BaseDriver.
15
+ // Extra defence for drivers that don't expose now() yet.
16
+ function nowTimestamp(client) {
17
+ return client.nowTimestamp?.() ?? new Date().getTime();
18
+ }
19
+ class PreAggregationLoader {
20
+ driverFactory;
21
+ logger;
22
+ queryCache;
23
+ loadCache;
24
+ preAggregations;
25
+ preAggregation;
26
+ preAggregationsTablesToTempTables;
27
+ /**
28
+ * Determines whether current instance instantiated for a jobbed build query
29
+ * (initialized by the /cubejs-system/v1/pre-aggregations/jobs endpoint) or
30
+ * not.
31
+ */
32
+ isJob;
33
+ waitForRenew;
34
+ forceBuild;
35
+ orphanedTimeout;
36
+ externalDriverFactory;
37
+ requestId;
38
+ metadata;
39
+ structureVersionPersistTime;
40
+ externalRefresh;
41
+ constructor(driverFactory, logger, queryCache, preAggregations, preAggregation, preAggregationsTablesToTempTables, loadCache, options = {}) {
42
+ this.driverFactory = driverFactory;
43
+ this.logger = logger;
44
+ this.queryCache = queryCache;
45
+ this.loadCache = loadCache;
46
+ this.preAggregations = preAggregations;
47
+ this.preAggregation = preAggregation;
48
+ this.preAggregationsTablesToTempTables = preAggregationsTablesToTempTables;
49
+ this.isJob = !!options.isJob;
50
+ this.waitForRenew = options.waitForRenew;
51
+ this.forceBuild = options.forceBuild;
52
+ this.orphanedTimeout = options.orphanedTimeout;
53
+ this.externalDriverFactory = preAggregations.externalDriverFactory;
54
+ this.requestId = options.requestId;
55
+ this.metadata = options.metadata;
56
+ this.structureVersionPersistTime = preAggregations.structureVersionPersistTime;
57
+ this.externalRefresh = options.externalRefresh;
58
+ if (this.externalRefresh && this.waitForRenew) {
59
+ const message = 'Invalid configuration - when externalRefresh is true, it will not perform a renew, therefore you cannot wait for it using waitForRenew.';
60
+ if (['production', 'test'].includes((0, shared_1.getEnv)('nodeEnv'))) {
61
+ throw new Error(message);
62
+ }
63
+ else {
64
+ this.logger('Invalid Configuration', {
65
+ requestId: this.requestId,
66
+ warning: message,
67
+ });
68
+ this.waitForRenew = false;
69
+ }
70
+ }
71
+ }
72
+ async loadPreAggregation(throwOnMissingPartition) {
73
+ const notLoadedKey = (this.preAggregation.invalidateKeyQueries || [])
74
+ .find(keyQuery => !this.loadCache.hasKeyQueryResult(keyQuery));
75
+ if (this.isJob || !(notLoadedKey && !this.waitForRenew)) {
76
+ // Case 1: pre-agg build job processing.
77
+ // Case 2: either we have no data cached for this rollup or waitForRenew
78
+ // is true, either way, synchronously renew what data is needed so that
79
+ // the most current data will be returned fo the current request.
80
+ const result = await this.loadPreAggregationWithKeys();
81
+ const refreshKeyValues = await this.getInvalidationKeyValues();
82
+ return {
83
+ ...result,
84
+ refreshKeyValues,
85
+ queryKey: this.isJob
86
+ // We need to return a queryKey value for the jobed build query
87
+ // (initialized by the /cubejs-system/v1/pre-aggregations/jobs
88
+ // endpoint) as a part of the response to make it possible to get a
89
+ // query result from the cache by the other API call.
90
+ ? this.preAggregationQueryKey(refreshKeyValues)
91
+ : undefined,
92
+ };
93
+ }
94
+ else {
95
+ // Case 3: pre-agg exists
96
+ const structureVersion = (0, PreAggregations_1.getStructureVersion)(this.preAggregation);
97
+ const getVersionsStarted = new Date();
98
+ const { byStructure } = await this.loadCache.getVersionEntries(this.preAggregation);
99
+ this.logger('Load PreAggregations Tables', {
100
+ preAggregation: this.preAggregation,
101
+ requestId: this.requestId,
102
+ duration: (new Date().getTime() - getVersionsStarted.getTime())
103
+ });
104
+ const versionEntryByStructureVersion = byStructure[`${this.preAggregation.tableName}_${structureVersion}`];
105
+ if (this.externalRefresh) {
106
+ if (!versionEntryByStructureVersion && throwOnMissingPartition) {
107
+ // eslint-disable-next-line no-use-before-define
108
+ throw new Error(PreAggregations_1.PreAggregations.noPreAggregationPartitionsBuiltMessage([this.preAggregation]));
109
+ }
110
+ if (!versionEntryByStructureVersion) {
111
+ return null;
112
+ }
113
+ else {
114
+ // the rollups are being maintained independently of this instance of cube.js
115
+ // immediately return the latest rollup data that instance already has
116
+ return {
117
+ targetTableName: this.targetTableName(versionEntryByStructureVersion),
118
+ refreshKeyValues: [],
119
+ lastUpdatedAt: versionEntryByStructureVersion.last_updated_at,
120
+ buildRangeEnd: versionEntryByStructureVersion.build_range_end,
121
+ };
122
+ }
123
+ }
124
+ if (versionEntryByStructureVersion) {
125
+ // this triggers an asynchronous/background load of the pre-aggregation but immediately
126
+ // returns the latest data it already has
127
+ this.loadPreAggregationWithKeys().catch(e => {
128
+ if (!(e instanceof ContinueWaitError_1.ContinueWaitError)) {
129
+ this.logger('Error loading pre-aggregation', {
130
+ error: (e.stack || e),
131
+ preAggregation: this.preAggregation,
132
+ requestId: this.requestId
133
+ });
134
+ }
135
+ });
136
+ return {
137
+ targetTableName: this.targetTableName(versionEntryByStructureVersion),
138
+ refreshKeyValues: [],
139
+ lastUpdatedAt: versionEntryByStructureVersion.last_updated_at,
140
+ buildRangeEnd: versionEntryByStructureVersion.build_range_end,
141
+ };
142
+ }
143
+ else {
144
+ // no rollup has been built yet - build it synchronously as part of responding to this request
145
+ return this.loadPreAggregationWithKeys();
146
+ }
147
+ }
148
+ }
149
+ async loadPreAggregationWithKeys() {
150
+ const invalidationKeys = await this.getPartitionInvalidationKeyValues();
151
+ const contentVersion = this.contentVersion(invalidationKeys);
152
+ const structureVersion = (0, PreAggregations_1.getStructureVersion)(this.preAggregation);
153
+ const versionEntries = await this.loadCache.getVersionEntries(this.preAggregation);
154
+ const getVersionEntryByContentVersion = ({ byContent }) => byContent[`${this.preAggregation.tableName}_${contentVersion}`];
155
+ const versionEntryByContentVersion = getVersionEntryByContentVersion(versionEntries);
156
+ if (versionEntryByContentVersion && !this.forceBuild) {
157
+ const targetTableName = this.targetTableName(versionEntryByContentVersion);
158
+ // No need to block here
159
+ this.updateLastTouch(targetTableName);
160
+ return {
161
+ targetTableName,
162
+ refreshKeyValues: [],
163
+ lastUpdatedAt: versionEntryByContentVersion.last_updated_at,
164
+ buildRangeEnd: versionEntryByContentVersion.build_range_end,
165
+ };
166
+ }
167
+ if (!this.waitForRenew && !this.forceBuild) {
168
+ const versionEntryByStructureVersion = versionEntries.byStructure[`${this.preAggregation.tableName}_${structureVersion}`];
169
+ if (versionEntryByStructureVersion) {
170
+ const targetTableName = this.targetTableName(versionEntryByStructureVersion);
171
+ // No need to block here
172
+ this.updateLastTouch(targetTableName);
173
+ return {
174
+ targetTableName,
175
+ refreshKeyValues: [],
176
+ lastUpdatedAt: versionEntryByStructureVersion.last_updated_at,
177
+ buildRangeEnd: versionEntryByStructureVersion.build_range_end,
178
+ };
179
+ }
180
+ }
181
+ const client = this.preAggregation.external ?
182
+ await this.externalDriverFactory() :
183
+ await this.driverFactory();
184
+ if (!versionEntries.versionEntries.length) {
185
+ await client.createSchemaIfNotExists(this.preAggregation.preAggregationsSchema);
186
+ }
187
+ // ensure we find appropriate structure version before invalidating anything
188
+ const versionEntry = versionEntries.byStructure[`${this.preAggregation.tableName}_${structureVersion}`] ||
189
+ versionEntries.byTableName[this.preAggregation.tableName];
190
+ const newVersionEntry = {
191
+ table_name: this.preAggregation.tableName,
192
+ structure_version: structureVersion,
193
+ content_version: contentVersion,
194
+ last_updated_at: nowTimestamp(client),
195
+ naming_version: 2,
196
+ };
197
+ const mostRecentResult = async () => {
198
+ await this.loadCache.reset(this.preAggregation);
199
+ const lastVersion = getVersionEntryByContentVersion(await this.loadCache.getVersionEntries(this.preAggregation));
200
+ if (!lastVersion) {
201
+ throw new Error(`Pre-aggregation table is not found for ${this.preAggregation.tableName} after it was successfully created`);
202
+ }
203
+ const targetTableName = this.targetTableName(lastVersion);
204
+ this.updateLastTouch(targetTableName);
205
+ return {
206
+ targetTableName,
207
+ refreshKeyValues: [],
208
+ lastUpdatedAt: lastVersion.last_updated_at,
209
+ buildRangeEnd: lastVersion.build_range_end,
210
+ };
211
+ };
212
+ if (this.forceBuild) {
213
+ this.logger('Force build pre-aggregation', {
214
+ preAggregation: this.preAggregation,
215
+ requestId: this.requestId,
216
+ metadata: this.metadata,
217
+ queryKey: this.preAggregationQueryKey(invalidationKeys),
218
+ newVersionEntry
219
+ });
220
+ if (this.isJob) {
221
+ // We don't want to wait for the jobed build query result. So we run the
222
+ // executeInQueue method and immediately return the LoadPreAggregationResult object.
223
+ this
224
+ .executeInQueue(invalidationKeys, this.priority(10), newVersionEntry)
225
+ .catch((e) => {
226
+ this.logger('Pre-aggregations build job error', {
227
+ preAggregation: this.preAggregation,
228
+ requestId: this.requestId,
229
+ newVersionEntry,
230
+ error: (e.stack || e),
231
+ });
232
+ });
233
+ const targetTableName = this.targetTableName(newVersionEntry);
234
+ this.updateLastTouch(targetTableName);
235
+ return {
236
+ targetTableName,
237
+ refreshKeyValues: [],
238
+ lastUpdatedAt: newVersionEntry.last_updated_at,
239
+ buildRangeEnd: this.preAggregation.buildRangeEnd,
240
+ };
241
+ }
242
+ else {
243
+ await this.executeInQueue(invalidationKeys, this.priority(10), newVersionEntry);
244
+ return mostRecentResult();
245
+ }
246
+ }
247
+ if (versionEntry) {
248
+ if (versionEntry.structure_version !== newVersionEntry.structure_version) {
249
+ this.logger('Invalidating pre-aggregation structure', {
250
+ preAggregation: this.preAggregation,
251
+ requestId: this.requestId,
252
+ queryKey: this.preAggregationQueryKey(invalidationKeys),
253
+ newVersionEntry
254
+ });
255
+ await this.executeInQueue(invalidationKeys, this.priority(10), newVersionEntry);
256
+ return mostRecentResult();
257
+ }
258
+ else if (versionEntry.content_version !== newVersionEntry.content_version) {
259
+ if (this.waitForRenew) {
260
+ this.logger('Waiting for pre-aggregation renew', {
261
+ preAggregation: this.preAggregation,
262
+ requestId: this.requestId,
263
+ queryKey: this.preAggregationQueryKey(invalidationKeys),
264
+ newVersionEntry
265
+ });
266
+ await this.executeInQueue(invalidationKeys, this.priority(0), newVersionEntry);
267
+ return mostRecentResult();
268
+ }
269
+ else {
270
+ this.scheduleRefresh(invalidationKeys, newVersionEntry);
271
+ }
272
+ }
273
+ }
274
+ else {
275
+ this.logger('Creating pre-aggregation from scratch', {
276
+ preAggregation: this.preAggregation,
277
+ requestId: this.requestId,
278
+ queryKey: this.preAggregationQueryKey(invalidationKeys),
279
+ newVersionEntry
280
+ });
281
+ await this.executeInQueue(invalidationKeys, this.priority(10), newVersionEntry);
282
+ return mostRecentResult();
283
+ }
284
+ const targetTableName = this.targetTableName(versionEntry);
285
+ this.updateLastTouch(targetTableName);
286
+ return {
287
+ targetTableName,
288
+ refreshKeyValues: [],
289
+ lastUpdatedAt: versionEntry.last_updated_at,
290
+ buildRangeEnd: versionEntry.build_range_end,
291
+ };
292
+ }
293
+ updateLastTouch(tableName) {
294
+ this.preAggregations.updateLastTouch(tableName).catch(e => {
295
+ this.logger('Error on pre-aggregation touch', {
296
+ error: (e.stack || e), preAggregation: this.preAggregation, requestId: this.requestId,
297
+ });
298
+ });
299
+ }
300
+ contentVersion(invalidationKeys) {
301
+ const versionArray = [this.preAggregation.structureVersionLoadSql || this.preAggregation.loadSql];
302
+ if (this.preAggregation.indexesSql && this.preAggregation.indexesSql.length) {
303
+ versionArray.push(this.preAggregation.indexesSql);
304
+ }
305
+ if (this.preAggregation.streamOffset) {
306
+ versionArray.push(this.preAggregation.streamOffset);
307
+ }
308
+ if (this.preAggregation.outputColumnTypes) {
309
+ versionArray.push(this.preAggregation.outputColumnTypes);
310
+ }
311
+ versionArray.push(invalidationKeys);
312
+ return (0, PreAggregations_1.version)(versionArray);
313
+ }
314
+ priority(defaultValue) {
315
+ return this.preAggregation.priority != null ? this.preAggregation.priority : defaultValue;
316
+ }
317
+ getInvalidationKeyValues() {
318
+ return Promise.all((this.preAggregation.invalidateKeyQueries || []).map((sqlQuery) => this.loadCache.keyQueryResult(sqlQuery, this.waitForRenew, this.priority(10))));
319
+ }
320
+ getPartitionInvalidationKeyValues() {
321
+ if (this.preAggregation.partitionInvalidateKeyQueries) {
322
+ return Promise.all((this.preAggregation.partitionInvalidateKeyQueries || []).map((sqlQuery) => this.loadCache.keyQueryResult(sqlQuery, this.waitForRenew, this.priority(10))));
323
+ }
324
+ else {
325
+ return this.getInvalidationKeyValues();
326
+ }
327
+ }
328
+ scheduleRefresh(invalidationKeys, newVersionEntry) {
329
+ this.logger('Refreshing pre-aggregation content', {
330
+ preAggregation: this.preAggregation,
331
+ requestId: this.requestId,
332
+ queryKey: this.preAggregationQueryKey(invalidationKeys),
333
+ newVersionEntry
334
+ });
335
+ this.executeInQueue(invalidationKeys, this.priority(0), newVersionEntry)
336
+ .catch(e => {
337
+ if (!(e instanceof ContinueWaitError_1.ContinueWaitError)) {
338
+ this.logger('Error refreshing pre-aggregation', {
339
+ error: (e.stack || e), preAggregation: this.preAggregation, requestId: this.requestId
340
+ });
341
+ }
342
+ });
343
+ }
344
+ async executeInQueue(invalidationKeys, priority, newVersionEntry) {
345
+ const queue = await this.preAggregations.getQueue(this.preAggregation.dataSource);
346
+ return queue.executeInQueue('query', this.preAggregationQueryKey(invalidationKeys), {
347
+ preAggregation: this.preAggregation,
348
+ preAggregationsTablesToTempTables: this.preAggregationsTablesToTempTables,
349
+ newVersionEntry,
350
+ requestId: this.requestId,
351
+ invalidationKeys,
352
+ forceBuild: this.forceBuild,
353
+ isJob: this.isJob,
354
+ metadata: this.metadata,
355
+ orphanedTimeout: this.orphanedTimeout,
356
+ }, priority,
357
+ // eslint-disable-next-line no-use-before-define
358
+ { stageQueryKey: PreAggregations_1.PreAggregations.preAggregationQueryCacheKey(this.preAggregation), requestId: this.requestId });
359
+ }
360
+ preAggregationQueryKey(invalidationKeys) {
361
+ return this.preAggregation.indexesSql && this.preAggregation.indexesSql.length ?
362
+ [this.preAggregation.loadSql, this.preAggregation.indexesSql, invalidationKeys] :
363
+ [this.preAggregation.loadSql, invalidationKeys];
364
+ }
365
+ targetTableName(versionEntry) {
366
+ // eslint-disable-next-line no-use-before-define
367
+ return PreAggregations_1.PreAggregations.targetTableName(versionEntry);
368
+ }
369
+ refresh(newVersionEntry, invalidationKeys, client) {
370
+ this.updateLastTouch(this.targetTableName(newVersionEntry));
371
+ let refreshStrategy = this.refreshStoreInSourceStrategy;
372
+ if (this.preAggregation.external) {
373
+ const readOnly = this.preAggregation.readOnly ||
374
+ client.config && client.config.readOnly ||
375
+ client.readOnly && (typeof client.readOnly === 'boolean' ? client.readOnly : client.readOnly());
376
+ if (readOnly) {
377
+ refreshStrategy = this.refreshReadOnlyExternalStrategy;
378
+ }
379
+ else {
380
+ refreshStrategy = this.refreshWriteStrategy;
381
+ }
382
+ }
383
+ return (0, base_driver_1.cancelCombinator)(saveCancelFn => refreshStrategy.bind(this)(client, newVersionEntry, saveCancelFn, invalidationKeys));
384
+ }
385
+ logExecutingSql(payload) {
386
+ this.logger('Executing Load Pre Aggregation SQL', payload);
387
+ }
388
+ queryOptions(invalidationKeys, query, params, targetTableName, newVersionEntry) {
389
+ return {
390
+ queryKey: this.preAggregationQueryKey(invalidationKeys),
391
+ query,
392
+ values: params,
393
+ targetTableName,
394
+ requestId: this.requestId,
395
+ newVersionEntry,
396
+ buildRangeEnd: this.preAggregation.buildRangeEnd,
397
+ };
398
+ }
399
+ async refreshStoreInSourceStrategy(client, newVersionEntry, saveCancelFn, invalidationKeys) {
400
+ const [loadSql, params] = Array.isArray(this.preAggregation.loadSql) ? this.preAggregation.loadSql : [this.preAggregation.loadSql, []];
401
+ const targetTableName = this.targetTableName(newVersionEntry);
402
+ const query = QueryCache_1.QueryCache.replacePreAggregationTableNames(loadSql, this.preAggregationsTablesToTempTables).replace(this.preAggregation.tableName, targetTableName);
403
+ const queryOptions = this.queryOptions(invalidationKeys, query, params, targetTableName, newVersionEntry);
404
+ this.logExecutingSql(queryOptions);
405
+ try {
406
+ // TODO move index creation to the driver
407
+ await saveCancelFn(client.loadPreAggregationIntoTable(targetTableName, query, params, {
408
+ streamOffset: this.preAggregation.streamOffset,
409
+ outputColumnTypes: this.preAggregation.outputColumnTypes,
410
+ ...queryOptions
411
+ }));
412
+ await this.createIndexes(client, newVersionEntry, saveCancelFn, queryOptions);
413
+ await this.loadCache.fetchTables(this.preAggregation);
414
+ }
415
+ finally {
416
+ // We must clean orphaned in any cases: success or exception
417
+ await this.dropOrphanedTables(client, targetTableName, saveCancelFn, false, queryOptions);
418
+ await this.loadCache.fetchTables(this.preAggregation);
419
+ }
420
+ }
421
+ async refreshWriteStrategy(client, newVersionEntry, saveCancelFn, invalidationKeys) {
422
+ const capabilities = client?.capabilities();
423
+ const withTempTable = !(capabilities?.unloadWithoutTempTable);
424
+ const dropSourceTempTable = !capabilities?.streamingSource;
425
+ return this.runWriteStrategy(client, newVersionEntry, saveCancelFn, invalidationKeys, withTempTable, dropSourceTempTable);
426
+ }
427
+ /**
428
+ * Runs export strategy with write access in data source
429
+ */
430
+ async runWriteStrategy(client, newVersionEntry, saveCancelFn, invalidationKeys, withTempTable, dropSourceTempTable) {
431
+ if (withTempTable) {
432
+ await client.createSchemaIfNotExists(this.preAggregation.preAggregationsSchema);
433
+ }
434
+ const targetTableName = this.targetTableName(newVersionEntry);
435
+ const queryOptions = await this.prepareWriteStrategy(client, targetTableName, newVersionEntry, saveCancelFn, invalidationKeys, withTempTable);
436
+ try {
437
+ const tableData = await this.downloadExternalPreAggregation(client, newVersionEntry, saveCancelFn, queryOptions, withTempTable);
438
+ try {
439
+ await this.uploadExternalPreAggregation(tableData, newVersionEntry, saveCancelFn, queryOptions);
440
+ }
441
+ finally {
442
+ if (tableData && tableData.release) {
443
+ await tableData.release();
444
+ }
445
+ }
446
+ }
447
+ finally {
448
+ await this.cleanupWriteStrategy(client, targetTableName, queryOptions, saveCancelFn, withTempTable, dropSourceTempTable);
449
+ }
450
+ }
451
+ /**
452
+ * Cleanup tables after write strategy
453
+ */
454
+ async cleanupWriteStrategy(client, targetTableName, queryOptions, saveCancelFn, withTempTable, dropSourceTempTable) {
455
+ if (withTempTable && dropSourceTempTable) {
456
+ await this.withDropLock(false, async () => {
457
+ this.logger('Dropping source temp table', queryOptions);
458
+ const actualTables = await client.getTablesQuery(this.preAggregation.preAggregationsSchema);
459
+ const mappedActualTables = actualTables.map(t => `${this.preAggregation.preAggregationsSchema}.${t.table_name || t.TABLE_NAME}`);
460
+ if (mappedActualTables.includes(targetTableName)) {
461
+ await client.dropTable(targetTableName);
462
+ }
463
+ });
464
+ }
465
+ // We must clean orphaned in any cases: success or exception
466
+ await this.loadCache.fetchTables(this.preAggregation);
467
+ await this.dropOrphanedTables(client, targetTableName, saveCancelFn, false, queryOptions);
468
+ }
469
+ /**
470
+ * Create table (if required) and prepares query options object
471
+ */
472
+ async prepareWriteStrategy(client, targetTableName, newVersionEntry, saveCancelFn, invalidationKeys, withTempTable) {
473
+ if (withTempTable) {
474
+ const [loadSql, params] = Array.isArray(this.preAggregation.loadSql) ? this.preAggregation.loadSql : [this.preAggregation.loadSql, []];
475
+ const query = QueryCache_1.QueryCache.replacePreAggregationTableNames(loadSql, this.preAggregationsTablesToTempTables).replace(this.preAggregation.tableName, targetTableName);
476
+ const queryOptions = this.queryOptions(invalidationKeys, query, params, targetTableName, newVersionEntry);
477
+ this.logExecutingSql(queryOptions);
478
+ await saveCancelFn(client.loadPreAggregationIntoTable(targetTableName, query, params, {
479
+ streamOffset: this.preAggregation.streamOffset,
480
+ outputColumnTypes: this.preAggregation.outputColumnTypes,
481
+ ...queryOptions
482
+ }));
483
+ return queryOptions;
484
+ }
485
+ else {
486
+ const [sql, params] = Array.isArray(this.preAggregation.sql) ? this.preAggregation.sql : [this.preAggregation.sql, []];
487
+ const queryOptions = this.queryOptions(invalidationKeys, sql, params, targetTableName, newVersionEntry);
488
+ this.logExecutingSql(queryOptions);
489
+ return queryOptions;
490
+ }
491
+ }
492
+ /**
493
+ * Strategy to copy pre-aggregation from source db (for read-only permissions) to external data
494
+ */
495
+ async refreshReadOnlyExternalStrategy(client, newVersionEntry, saveCancelFn, invalidationKeys) {
496
+ const [sql, params] = Array.isArray(this.preAggregation.sql) ? this.preAggregation.sql : [this.preAggregation.sql, []];
497
+ const queryOptions = this.queryOptions(invalidationKeys, sql, params, this.targetTableName(newVersionEntry), newVersionEntry);
498
+ this.logExecutingSql(queryOptions);
499
+ this.logger('Downloading external pre-aggregation via query', queryOptions);
500
+ const externalDriver = await this.externalDriverFactory();
501
+ const capabilities = externalDriver.capabilities && externalDriver.capabilities();
502
+ let tableData;
503
+ if (capabilities.csvImport && client.unloadFromQuery && await client.isUnloadSupported(this.getUnloadOptions())) {
504
+ tableData = await saveCancelFn(client.unloadFromQuery(sql, params, this.getUnloadOptions())).catch((error) => {
505
+ this.logger('Downloading external pre-aggregation via query error', {
506
+ ...queryOptions,
507
+ error: error.stack || error.message
508
+ });
509
+ throw error;
510
+ });
511
+ }
512
+ else {
513
+ tableData = await saveCancelFn(client.downloadQueryResults(sql, params, {
514
+ streamOffset: this.preAggregation.streamOffset,
515
+ outputColumnTypes: this.preAggregation.outputColumnTypes,
516
+ ...queryOptions,
517
+ ...capabilities,
518
+ ...this.getStreamingOptions(),
519
+ })).catch((error) => {
520
+ this.logger('Downloading external pre-aggregation via query error', {
521
+ ...queryOptions,
522
+ error: error.stack || error.message
523
+ });
524
+ throw error;
525
+ });
526
+ }
527
+ this.logger('Downloading external pre-aggregation via query completed', {
528
+ ...queryOptions,
529
+ isUnloadSupported: (0, base_driver_1.isDownloadTableCSVData)(tableData)
530
+ });
531
+ try {
532
+ await this.uploadExternalPreAggregation(tableData, newVersionEntry, saveCancelFn, queryOptions);
533
+ }
534
+ finally {
535
+ if (tableData.release) {
536
+ await tableData.release();
537
+ }
538
+ }
539
+ await this.loadCache.fetchTables(this.preAggregation);
540
+ }
541
+ getUnloadOptions() {
542
+ return {
543
+ // Default: 16mb for Snowflake, Should be specified in MBs, because drivers convert it
544
+ maxFileSize: 64
545
+ };
546
+ }
547
+ getStreamingOptions() {
548
+ return {
549
+ // Default: 16384 (16KB), or 16 for objectMode streams. PostgreSQL/MySQL use object streams
550
+ highWaterMark: 10000
551
+ };
552
+ }
553
+ /**
554
+ * prepares download data for future cube store usage
555
+ */
556
+ async downloadExternalPreAggregation(client, newVersionEntry, saveCancelFn, queryOptions, withTempTable) {
557
+ const table = this.targetTableName(newVersionEntry);
558
+ this.logger('Downloading external pre-aggregation', queryOptions);
559
+ try {
560
+ const externalDriver = await this.externalDriverFactory();
561
+ const capabilities = externalDriver.capabilities && externalDriver.capabilities();
562
+ let tableData;
563
+ if (withTempTable) {
564
+ tableData = await this.getTableDataWithTempTable(client, table, saveCancelFn, queryOptions, capabilities);
565
+ }
566
+ else {
567
+ tableData = await this.getTableDataWithoutTempTable(client, table, saveCancelFn, queryOptions, capabilities);
568
+ }
569
+ this.logger('Downloading external pre-aggregation completed', {
570
+ ...queryOptions,
571
+ isUnloadSupported: (0, base_driver_1.isDownloadTableCSVData)(tableData)
572
+ });
573
+ return tableData;
574
+ }
575
+ catch (error) {
576
+ this.logger('Downloading external pre-aggregation error', {
577
+ ...queryOptions,
578
+ error: error?.stack || error?.message
579
+ });
580
+ throw error;
581
+ }
582
+ }
583
+ /**
584
+ * prepares download data when temp table = true
585
+ */
586
+ async getTableDataWithTempTable(client, table, saveCancelFn, queryOptions, externalDriverCapabilities) {
587
+ let tableData;
588
+ if (externalDriverCapabilities.csvImport && client.unload && await client.isUnloadSupported(this.getUnloadOptions())) {
589
+ tableData = await saveCancelFn(client.unload(table, this.getUnloadOptions()));
590
+ }
591
+ else if (externalDriverCapabilities.streamImport && client.stream) {
592
+ tableData = await saveCancelFn(client.stream(`SELECT * FROM ${table}`, [], this.getStreamingOptions()));
593
+ if (client.unload) {
594
+ const stream = new StreamObjectsCounter_1.LargeStreamWarning(this.preAggregation.preAggregationId, (msg) => {
595
+ this.logger('Downloading external pre-aggregation warning', {
596
+ ...queryOptions,
597
+ error: msg
598
+ });
599
+ });
600
+ tableData.rowStream.pipe(stream);
601
+ tableData.rowStream = stream;
602
+ }
603
+ }
604
+ else {
605
+ tableData = await saveCancelFn(client.downloadTable(table, {
606
+ streamOffset: this.preAggregation.streamOffset,
607
+ outputColumnTypes: this.preAggregation.outputColumnTypes,
608
+ ...externalDriverCapabilities
609
+ }));
610
+ }
611
+ if (!tableData.types) {
612
+ tableData.types = await saveCancelFn(client.tableColumnTypes(table));
613
+ }
614
+ return tableData;
615
+ }
616
+ /**
617
+ * prepares download data when temp table = false
618
+ */
619
+ async getTableDataWithoutTempTable(client, table, saveCancelFn, queryOptions, externalDriverCapabilities) {
620
+ const [sql, params] = Array.isArray(this.preAggregation.sql) ? this.preAggregation.sql : [this.preAggregation.sql, []];
621
+ let tableData;
622
+ if (externalDriverCapabilities.csvImport && client.unload && await client.isUnloadSupported(this.getUnloadOptions())) {
623
+ return saveCancelFn(client.unload(table, { ...this.getUnloadOptions(), query: { sql, params } }));
624
+ }
625
+ else if (externalDriverCapabilities.streamImport && client.stream) {
626
+ tableData = await saveCancelFn(client.stream(sql, params, this.getStreamingOptions()));
627
+ if (client.unload) {
628
+ const stream = new StreamObjectsCounter_1.LargeStreamWarning(this.preAggregation.preAggregationId, (msg) => {
629
+ this.logger('Downloading external pre-aggregation warning', {
630
+ ...queryOptions,
631
+ error: msg
632
+ });
633
+ });
634
+ tableData.rowStream.pipe(stream);
635
+ tableData.rowStream = stream;
636
+ }
637
+ }
638
+ else {
639
+ tableData = { rows: await saveCancelFn(client.query(sql, params)) };
640
+ }
641
+ if (!tableData.types && client.queryColumnTypes) {
642
+ tableData.types = await saveCancelFn(client.queryColumnTypes(sql, params));
643
+ }
644
+ return tableData;
645
+ }
646
+ async uploadExternalPreAggregation(tableData, newVersionEntry, saveCancelFn, queryOptions) {
647
+ const externalDriver = await this.externalDriverFactory();
648
+ const table = this.targetTableName(newVersionEntry);
649
+ this.logger('Uploading external pre-aggregation', queryOptions);
650
+ await saveCancelFn(externalDriver.uploadTableWithIndexes(table, tableData.types, tableData, this.prepareIndexesSql(newVersionEntry, queryOptions), this.preAggregation.uniqueKeyColumns, queryOptions, {
651
+ aggregationsColumns: this.preAggregation.aggregationsColumns,
652
+ createTableIndexes: this.prepareCreateTableIndexes(newVersionEntry),
653
+ sealAt: this.preAggregation.sealAt
654
+ })).catch((error) => {
655
+ this.logger('Uploading external pre-aggregation error', {
656
+ ...queryOptions,
657
+ error: error?.stack || error?.message
658
+ });
659
+ throw error;
660
+ });
661
+ this.logger('Uploading external pre-aggregation completed', queryOptions);
662
+ await this.loadCache.fetchTables(this.preAggregation);
663
+ await this.dropOrphanedTables(externalDriver, table, saveCancelFn, true, queryOptions);
664
+ }
665
+ async createIndexes(driver, newVersionEntry, saveCancelFn, queryOptions) {
666
+ const indexesSql = this.prepareIndexesSql(newVersionEntry, queryOptions);
667
+ for (let i = 0; i < indexesSql.length; i++) {
668
+ const [query, params] = indexesSql[i].sql;
669
+ await saveCancelFn(driver.query(query, params));
670
+ }
671
+ }
672
+ prepareIndexesSql(newVersionEntry, queryOptions) {
673
+ if (!this.preAggregation.indexesSql || !this.preAggregation.indexesSql.length) {
674
+ return [];
675
+ }
676
+ return this.preAggregation.indexesSql.map(({ sql, indexName }) => {
677
+ const [query, params] = sql;
678
+ const indexVersionEntry = {
679
+ ...newVersionEntry,
680
+ table_name: indexName
681
+ };
682
+ this.logger('Creating pre-aggregation index', queryOptions);
683
+ const resultingSql = QueryCache_1.QueryCache.replacePreAggregationTableNames(query, this.preAggregationsTablesToTempTables.concat([
684
+ [this.preAggregation.tableName, { targetTableName: this.targetTableName(newVersionEntry) }],
685
+ [indexName, { targetTableName: this.targetTableName(indexVersionEntry) }]
686
+ ]));
687
+ return { sql: [resultingSql, params] };
688
+ });
689
+ }
690
+ prepareCreateTableIndexes(newVersionEntry) {
691
+ if (!this.preAggregation.createTableIndexes || !this.preAggregation.createTableIndexes.length) {
692
+ return [];
693
+ }
694
+ return this.preAggregation.createTableIndexes.map(({ indexName, type, columns }) => {
695
+ const indexVersionEntry = {
696
+ ...newVersionEntry,
697
+ table_name: indexName
698
+ };
699
+ return { indexName: this.targetTableName(indexVersionEntry), type, columns };
700
+ });
701
+ }
702
+ async withDropLock(external, lockFn) {
703
+ const lockKey = this.dropLockKey(external);
704
+ return this.queryCache.withLock(lockKey, 60 * 5, lockFn);
705
+ }
706
+ async dropOrphanedTables(client, justCreatedTable, saveCancelFn, external, queryOptions) {
707
+ await this.preAggregations.addTableUsed(justCreatedTable);
708
+ return this.withDropLock(external, async () => {
709
+ this.logger('Dropping orphaned tables', { ...queryOptions, external });
710
+ const actualTables = await client.getTablesQuery(this.preAggregation.preAggregationsSchema);
711
+ const versionEntries = (0, PreAggregations_1.tablesToVersionEntries)(this.preAggregation.preAggregationsSchema, actualTables);
712
+ const versionEntriesToSave = ramda_1.default.pipe(ramda_1.default.groupBy(v => v.table_name), ramda_1.default.toPairs, ramda_1.default.map(p => p[1][0]))(versionEntries);
713
+ const structureVersionsToSave = ramda_1.default.pipe(ramda_1.default.filter((v) => (new Date().getTime() - v.last_updated_at <
714
+ this.structureVersionPersistTime * 1000)), ramda_1.default.groupBy(v => `${v.table_name}_${v.structure_version}`), ramda_1.default.toPairs, ramda_1.default.map(p => p[1][0]))(versionEntries);
715
+ const refreshEndReached = await this.preAggregations.getRefreshEndReached();
716
+ const toSave = this.preAggregations.dropPreAggregationsWithoutTouch && refreshEndReached
717
+ ? (await this.preAggregations.tablesUsed())
718
+ .concat(await this.preAggregations.tablesTouched())
719
+ .concat([justCreatedTable])
720
+ : (await this.preAggregations.tablesUsed())
721
+ .concat(structureVersionsToSave.map(v => this.targetTableName(v)))
722
+ .concat(versionEntriesToSave.map(v => this.targetTableName(v)))
723
+ .concat([justCreatedTable]);
724
+ const toDrop = actualTables
725
+ .map(t => `${this.preAggregation.preAggregationsSchema}.${t.table_name || t.TABLE_NAME}`)
726
+ .filter(t => toSave.indexOf(t) === -1);
727
+ await Promise.all(toDrop.map(table => saveCancelFn(client.dropTable(table))));
728
+ this.logger('Dropping orphaned tables completed', {
729
+ ...queryOptions,
730
+ external,
731
+ tablesToDrop: JSON.stringify(toDrop),
732
+ });
733
+ });
734
+ }
735
+ dropLockKey(external) {
736
+ return external
737
+ ? 'drop-orphaned-tables-external'
738
+ : `drop-orphaned-tables:${this.preAggregation.dataSource}`;
739
+ }
740
+ }
741
+ exports.PreAggregationLoader = PreAggregationLoader;
742
+ //# sourceMappingURL=PreAggregationLoader.js.map