Skip to content

Commit 1deddcc

Browse files
authored
feat(tesseract): Basic pre-aggregations support (#9434)
1 parent 9bb4a2f commit 1deddcc

File tree

69 files changed

+3582
-291
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+3582
-291
lines changed

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

+97-28
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,11 @@ export class BaseQuery {
289289
}).filter(R.identity).map(this.newTimeDimension.bind(this));
290290
this.allFilters = this.timeDimensions.concat(this.segments).concat(this.filters);
291291
this.useNativeSqlPlanner = this.options.useNativeSqlPlanner ?? getEnv('nativeSqlPlanner');
292+
this.canUseNativeSqlPlannerPreAggregation = false;
293+
if (this.useNativeSqlPlanner) {
294+
const hasMultiStageMeasures = this.fullKeyQueryAggregateMeasures({ hasMultipliedForPreAggregation: true }).multiStageMembers.length > 0;
295+
this.canUseNativeSqlPlannerPreAggregation = hasMultiStageMeasures;
296+
}
292297
this.prebuildJoin();
293298

294299
this.cubeAliasPrefix = this.options.cubeAliasPrefix;
@@ -471,6 +476,19 @@ export class BaseQuery {
471476
}
472477

473478
newDimension(dimensionPath) {
479+
if (typeof dimensionPath === 'string') {
480+
const memberArr = dimensionPath.split('.');
481+
if (memberArr.length > 3 &&
482+
memberArr[memberArr.length - 2] === 'granularities' &&
483+
this.cubeEvaluator.isDimension(memberArr.slice(0, -2))) {
484+
return this.newTimeDimension(
485+
{
486+
dimension: this.cubeEvaluator.pathFromArray(memberArr.slice(0, -2)),
487+
granularity: memberArr[memberArr.length - 1]
488+
}
489+
);
490+
}
491+
}
474492
return new BaseDimension(this, dimensionPath);
475493
}
476494

@@ -636,38 +654,39 @@ export class BaseQuery {
636654
* @returns {[string, Array<unknown>]}
637655
*/
638656
buildSqlAndParams(exportAnnotatedSql) {
639-
if (!this.options.preAggregationQuery && !this.options.disableExternalPreAggregations && this.externalQueryClass) {
640-
if (this.externalPreAggregationQuery()) { // TODO performance
641-
return this.externalQuery().buildSqlAndParams(exportAnnotatedSql);
642-
}
643-
}
644-
645657
if (this.useNativeSqlPlanner) {
646658
let isRelatedToPreAggregation = false;
647-
if (this.options.preAggregationQuery) {
648-
isRelatedToPreAggregation = true;
649-
} else if (!this.options.disableExternalPreAggregations && this.externalQueryClass) {
650-
if (this.externalPreAggregationQuery()) {
659+
660+
if (!this.canUseNativeSqlPlannerPreAggregation) {
661+
if (this.options.preAggregationQuery) {
651662
isRelatedToPreAggregation = true;
652-
}
653-
} else {
654-
let preAggForQuery =
655-
this.preAggregations.findPreAggregationForQuery();
656-
if (this.options.disableExternalPreAggregations && preAggForQuery && preAggForQuery.preAggregation.external) {
657-
preAggForQuery = undefined;
658-
}
659-
if (preAggForQuery) {
663+
} else if (!this.options.disableExternalPreAggregations && this.externalQueryClass && this.externalPreAggregationQuery()) {
660664
isRelatedToPreAggregation = true;
665+
} else {
666+
let preAggForQuery =
667+
this.preAggregations.findPreAggregationForQuery();
668+
if (this.options.disableExternalPreAggregations && preAggForQuery && preAggForQuery.preAggregation.external) {
669+
preAggForQuery = undefined;
670+
}
671+
if (preAggForQuery) {
672+
isRelatedToPreAggregation = true;
673+
}
661674
}
662-
}
663675

664-
if (isRelatedToPreAggregation) {
665-
return this.newQueryWithoutNative().buildSqlAndParams(exportAnnotatedSql);
676+
if (isRelatedToPreAggregation) {
677+
return this.newQueryWithoutNative().buildSqlAndParams(exportAnnotatedSql);
678+
}
666679
}
667680

668681
return this.buildSqlAndParamsRust(exportAnnotatedSql);
669682
}
670683

684+
if (!this.options.preAggregationQuery && !this.options.disableExternalPreAggregations && this.externalQueryClass) {
685+
if (this.externalPreAggregationQuery()) { // TODO performance
686+
return this.externalQuery().buildSqlAndParams(exportAnnotatedSql);
687+
}
688+
}
689+
671690
return this.compilers.compiler.withQuery(
672691
this,
673692
() => this.cacheValue(
@@ -703,8 +722,8 @@ export class BaseQuery {
703722
offset: this.options.offset ? this.options.offset.toString() : null,
704723
baseTools: this,
705724
ungrouped: this.options.ungrouped,
706-
exportAnnotatedSql: exportAnnotatedSql === true
707-
725+
exportAnnotatedSql: exportAnnotatedSql === true,
726+
preAggregationQuery: this.options.preAggregationQuery
708727
};
709728

710729
const buildResult = nativeBuildSqlAndParams(queryParams);
@@ -718,9 +737,57 @@ export class BaseQuery {
718737
}
719738

720739
const res = buildResult.result;
740+
const [query, params, preAggregation] = res;
721741
// FIXME
722-
res[1] = [...res[1]];
723-
return res;
742+
const paramsArray = [...params];
743+
if (preAggregation) {
744+
this.preAggregations.preAggregationForQuery = preAggregation;
745+
}
746+
return [query, paramsArray];
747+
}
748+
749+
// FIXME Temporary solution
750+
findPreAggregationForQueryRust() {
751+
let optionsOrder = this.options.order;
752+
if (optionsOrder && !Array.isArray(optionsOrder)) {
753+
optionsOrder = [optionsOrder];
754+
}
755+
const order = optionsOrder ? R.pipe(
756+
R.map((hash) => ((!hash || !hash.id) ? null : hash)),
757+
R.reject(R.isNil),
758+
)(optionsOrder) : undefined;
759+
760+
const queryParams = {
761+
measures: this.options.measures,
762+
dimensions: this.options.dimensions,
763+
segments: this.options.segments,
764+
timeDimensions: this.options.timeDimensions,
765+
timezone: this.options.timezone,
766+
joinGraph: this.joinGraph,
767+
cubeEvaluator: this.cubeEvaluator,
768+
order,
769+
filters: this.options.filters,
770+
limit: this.options.limit ? this.options.limit.toString() : null,
771+
rowLimit: this.options.rowLimit ? this.options.rowLimit.toString() : null,
772+
offset: this.options.offset ? this.options.offset.toString() : null,
773+
baseTools: this,
774+
ungrouped: this.options.ungrouped,
775+
exportAnnotatedSql: false,
776+
preAggregationQuery: this.options.preAggregationQuery
777+
};
778+
779+
const buildResult = nativeBuildSqlAndParams(queryParams);
780+
781+
if (buildResult.error) {
782+
if (buildResult.error.cause === 'User') {
783+
throw new UserError(buildResult.error.message);
784+
} else {
785+
throw new Error(buildResult.error.message);
786+
}
787+
}
788+
789+
const [, , preAggregation] = buildResult.result;
790+
return preAggregation;
724791
}
725792

726793
allCubeMembers(path) {
@@ -743,6 +810,10 @@ export class BaseQuery {
743810
return timeSeriesFromCustomInterval(granularityInterval, dateRange, moment(origin), { timestampPrecision: 3 });
744811
}
745812

813+
getPreAggregationByName(cube, preAggregationName) {
814+
return this.preAggregations.getRollupPreAggregationByName(cube, preAggregationName);
815+
}
816+
746817
get shouldReuseParams() {
747818
return false;
748819
}
@@ -922,7 +993,6 @@ export class BaseQuery {
922993
const renderedWithQueries = withQueries.map(q => this.renderWithQuery(q));
923994

924995
let toJoin;
925-
926996
if (this.options.preAggregationQuery) {
927997
const allRegular = regularMeasures.concat(
928998
cumulativeMeasures
@@ -1153,7 +1223,6 @@ export class BaseQuery {
11531223

11541224
const multipliedMeasures = measuresToRender(true, false)(measureToHierarchy);
11551225
const regularMeasures = measuresToRender(false, false)(measureToHierarchy);
1156-
11571226
const cumulativeMeasures =
11581227
R.pipe(
11591228
R.map(multiplied => R.xprod([multiplied], measuresToRender(multiplied, true)(measureToHierarchy))),
@@ -3228,7 +3297,7 @@ export class BaseQuery {
32283297
}
32293298

32303299
newSubQueryForCube(cube, options) {
3231-
options = { ...options, useNativeSqlPlanner: false }; // We don't use tesseract for pre-aggregations generation yet
3300+
options = { ...options };
32323301
if (this.options.queryFactory) {
32333302
// When dealing with rollup joins, it's crucial to use the correct parameter allocator for the specific cube in use.
32343303
// By default, we'll use BaseQuery, but it's important to note that different databases (Oracle, PostgreSQL, MySQL, Druid, etc.)

packages/cubejs-schema-compiler/src/adapter/PreAggregations.js

+29-6
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ export class PreAggregations {
559559
transformedQuery.filterDimensionsSingleValueEqual || {},
560560
)
561561
));
562-
562+
563563
const backAlias = (references) => references.map(r => (
564564
Array.isArray(r) ?
565565
[transformedQuery.allBackAliasMembers[r[0]] || r[0], r[1]] :
@@ -782,11 +782,15 @@ export class PreAggregations {
782782
*/
783783
findPreAggregationForQuery() {
784784
if (!this.preAggregationForQuery) {
785-
this.preAggregationForQuery =
786-
this
787-
.rollupMatchResults()
788-
// Refresh worker can access specific pre-aggregations even in case those hidden by others
789-
.find(p => p.canUsePreAggregation && (!this.query.options.preAggregationId || p.preAggregationId === this.query.options.preAggregationId));
785+
if (this.query.useNativeSqlPlanner && this.query.canUseNativeSqlPlannerPreAggregation) {
786+
this.preAggregationForQuery = this.query.findPreAggregationForQueryRust();
787+
} else {
788+
this.preAggregationForQuery =
789+
this
790+
.rollupMatchResults()
791+
// Refresh worker can access specific pre-aggregations even in case those hidden by others
792+
.find(p => p.canUsePreAggregation && (!this.query.options.preAggregationId || p.preAggregationId === this.query.options.preAggregationId));
793+
}
790794
}
791795
return this.preAggregationForQuery;
792796
}
@@ -866,6 +870,25 @@ export class PreAggregations {
866870
)(preAggregations);
867871
}
868872

873+
getRollupPreAggregationByName(cube, preAggregationName) {
874+
const canUsePreAggregation = () => true;
875+
const preAggregation = R.pipe(
876+
R.toPairs,
877+
R.filter(([_, a]) => a.type === 'rollup' || a.type === 'rollupJoin' || a.type === 'rollupLambda'),
878+
R.find(([k, _]) => k === preAggregationName)
879+
)(this.query.cubeEvaluator.preAggregationsForCube(cube));
880+
if (preAggregation) {
881+
const tableName = this.preAggregationTableName(cube, preAggregation[0], preAggregation[1]);
882+
const preAggObj = preAggregation ? this.evaluatedPreAggregationObj(cube, preAggregation[0], preAggregation[1], canUsePreAggregation) : {};
883+
return {
884+
tableName,
885+
...preAggObj
886+
};
887+
} else {
888+
return {};
889+
}
890+
}
891+
869892
// TODO check multiplication factor didn't change
870893
buildRollupJoin(preAggObj, preAggObjsToJoin) {
871894
return this.query.cacheValue(

packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts

+7
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,13 @@ export class CubeEvaluator extends CubeSymbols {
520520
return this.cubeFromPath(path).preAggregations || {};
521521
}
522522

523+
public preAggregationsForCubeAsArray(path: string) {
524+
return Object.entries(this.cubeFromPath(path).preAggregations || {}).map(([name, preAggregation]) => ({
525+
name,
526+
...(preAggregation as Record<string, any>)
527+
}));
528+
}
529+
523530
/**
524531
* Returns pre-aggregations filtered by the specified selector.
525532
*/

packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,7 @@ const measureTypeWithCount = Joi.string().valid(
560560
);
561561

562562
const multiStageMeasureType = Joi.string().valid(
563-
'count', 'number', 'string', 'boolean', 'time', 'sum', 'avg', 'min', 'max', 'countDistinct', 'runningTotal', 'countDistinctApprox',
563+
'count', 'number', 'string', 'boolean', 'time', 'sum', 'avg', 'min', 'max', 'countDistinct', 'runningTotal', 'countDistinctApprox', 'numberAgg',
564564
'rank'
565565
);
566566

packages/cubejs-schema-compiler/test/integration/postgres/pre-aggregations-alias.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,9 @@ describe('PreAggregationsAlias', () => {
368368
});
369369

370370
const preAggregationsDescription: any = query.preAggregations?.preAggregationsDescription();
371+
const sqlAndParams = query.buildSqlAndParams();
371372
expect(preAggregationsDescription[0].tableName).toEqual('rvis_rollupalias');
372-
expect(query.buildSqlAndParams()[0]).toContain('rvis_rollupalias');
373+
expect(sqlAndParams[0]).toContain('rvis_rollupalias');
373374

374375
return dbRunner.evaluateQueryWithPreAggregations(query).then(res => {
375376
expect(res).toEqual(

0 commit comments

Comments
 (0)