Skip to content

feat(tesseract): Basic pre-aggregations support #9434

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
May 6, 2025
125 changes: 97 additions & 28 deletions packages/cubejs-schema-compiler/src/adapter/BaseQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,11 @@
}).filter(R.identity).map(this.newTimeDimension.bind(this));
this.allFilters = this.timeDimensions.concat(this.segments).concat(this.filters);
this.useNativeSqlPlanner = this.options.useNativeSqlPlanner ?? getEnv('nativeSqlPlanner');
this.canUseNativeSqlPlannerPreAggregation = false;
if (this.useNativeSqlPlanner) {
const hasMultiStageMeasures = this.fullKeyQueryAggregateMeasures({ hasMultipliedForPreAggregation: true }).multiStageMembers.length > 0;
this.canUseNativeSqlPlannerPreAggregation = hasMultiStageMeasures;

Check warning on line 295 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js#L294-L295

Added lines #L294 - L295 were not covered by tests
}
this.prebuildJoin();

this.cubeAliasPrefix = this.options.cubeAliasPrefix;
Expand Down Expand Up @@ -471,6 +476,19 @@
}

newDimension(dimensionPath) {
if (typeof dimensionPath === 'string') {
const memberArr = dimensionPath.split('.');
if (memberArr.length > 3 &&
memberArr[memberArr.length - 2] === 'granularities' &&
this.cubeEvaluator.isDimension(memberArr.slice(0, -2))) {
return this.newTimeDimension(

Check warning on line 484 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

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

Added line #L484 was not covered by tests
{
dimension: this.cubeEvaluator.pathFromArray(memberArr.slice(0, -2)),
granularity: memberArr[memberArr.length - 1]
}
);
}
}
return new BaseDimension(this, dimensionPath);
}

Expand Down Expand Up @@ -636,38 +654,39 @@
* @returns {[string, Array<unknown>]}
*/
buildSqlAndParams(exportAnnotatedSql) {
if (!this.options.preAggregationQuery && !this.options.disableExternalPreAggregations && this.externalQueryClass) {
if (this.externalPreAggregationQuery()) { // TODO performance
return this.externalQuery().buildSqlAndParams(exportAnnotatedSql);
}
}

if (this.useNativeSqlPlanner) {
let isRelatedToPreAggregation = false;
if (this.options.preAggregationQuery) {
isRelatedToPreAggregation = true;
} else if (!this.options.disableExternalPreAggregations && this.externalQueryClass) {
if (this.externalPreAggregationQuery()) {

if (!this.canUseNativeSqlPlannerPreAggregation) {
if (this.options.preAggregationQuery) {

Check warning on line 661 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js#L660-L661

Added lines #L660 - L661 were not covered by tests
isRelatedToPreAggregation = true;
}
} else {
let preAggForQuery =
this.preAggregations.findPreAggregationForQuery();
if (this.options.disableExternalPreAggregations && preAggForQuery && preAggForQuery.preAggregation.external) {
preAggForQuery = undefined;
}
if (preAggForQuery) {
} else if (!this.options.disableExternalPreAggregations && this.externalQueryClass && this.externalPreAggregationQuery()) {

Check warning on line 663 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

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

Added line #L663 was not covered by tests
isRelatedToPreAggregation = true;
} else {
let preAggForQuery =
this.preAggregations.findPreAggregationForQuery();
if (this.options.disableExternalPreAggregations && preAggForQuery && preAggForQuery.preAggregation.external) {
preAggForQuery = undefined;

Check warning on line 669 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js#L667-L669

Added lines #L667 - L669 were not covered by tests
}
if (preAggForQuery) {
isRelatedToPreAggregation = true;

Check warning on line 672 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js#L671-L672

Added lines #L671 - L672 were not covered by tests
}
}
}

if (isRelatedToPreAggregation) {
return this.newQueryWithoutNative().buildSqlAndParams(exportAnnotatedSql);
if (isRelatedToPreAggregation) {
return this.newQueryWithoutNative().buildSqlAndParams(exportAnnotatedSql);

Check warning on line 677 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js#L676-L677

Added lines #L676 - L677 were not covered by tests
}
}

return this.buildSqlAndParamsRust(exportAnnotatedSql);
}

if (!this.options.preAggregationQuery && !this.options.disableExternalPreAggregations && this.externalQueryClass) {
if (this.externalPreAggregationQuery()) { // TODO performance
return this.externalQuery().buildSqlAndParams(exportAnnotatedSql);

Check warning on line 686 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js#L685-L686

Added lines #L685 - L686 were not covered by tests
}
}

return this.compilers.compiler.withQuery(
this,
() => this.cacheValue(
Expand Down Expand Up @@ -703,8 +722,8 @@
offset: this.options.offset ? this.options.offset.toString() : null,
baseTools: this,
ungrouped: this.options.ungrouped,
exportAnnotatedSql: exportAnnotatedSql === true

exportAnnotatedSql: exportAnnotatedSql === true,
preAggregationQuery: this.options.preAggregationQuery
};

const buildResult = nativeBuildSqlAndParams(queryParams);
Expand All @@ -718,9 +737,57 @@
}

const res = buildResult.result;
const [query, params, preAggregation] = res;

Check warning on line 740 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

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

Added line #L740 was not covered by tests
// FIXME
res[1] = [...res[1]];
return res;
const paramsArray = [...params];
if (preAggregation) {
this.preAggregations.preAggregationForQuery = preAggregation;

Check warning on line 744 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js#L742-L744

Added lines #L742 - L744 were not covered by tests
}
return [query, paramsArray];

Check warning on line 746 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

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

Added line #L746 was not covered by tests
}

// FIXME Temporary solution
findPreAggregationForQueryRust() {
let optionsOrder = this.options.order;
if (optionsOrder && !Array.isArray(optionsOrder)) {
optionsOrder = [optionsOrder];

Check warning on line 753 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js#L751-L753

Added lines #L751 - L753 were not covered by tests
}
const order = optionsOrder ? R.pipe(
R.map((hash) => ((!hash || !hash.id) ? null : hash)),

Check warning on line 756 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js#L755-L756

Added lines #L755 - L756 were not covered by tests
R.reject(R.isNil),
)(optionsOrder) : undefined;

const queryParams = {

Check warning on line 760 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

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

Added line #L760 was not covered by tests
measures: this.options.measures,
dimensions: this.options.dimensions,
segments: this.options.segments,
timeDimensions: this.options.timeDimensions,
timezone: this.options.timezone,
joinGraph: this.joinGraph,
cubeEvaluator: this.cubeEvaluator,
order,
filters: this.options.filters,
limit: this.options.limit ? this.options.limit.toString() : null,
rowLimit: this.options.rowLimit ? this.options.rowLimit.toString() : null,
offset: this.options.offset ? this.options.offset.toString() : null,
baseTools: this,
ungrouped: this.options.ungrouped,
exportAnnotatedSql: false,
preAggregationQuery: this.options.preAggregationQuery
};

const buildResult = nativeBuildSqlAndParams(queryParams);

Check warning on line 779 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

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

Added line #L779 was not covered by tests

if (buildResult.error) {
if (buildResult.error.cause === 'User') {
throw new UserError(buildResult.error.message);

Check warning on line 783 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js#L781-L783

Added lines #L781 - L783 were not covered by tests
} else {
throw new Error(buildResult.error.message);

Check warning on line 785 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

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

Added line #L785 was not covered by tests
}
}

const [, , preAggregation] = buildResult.result;
return preAggregation;

Check warning on line 790 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js#L789-L790

Added lines #L789 - L790 were not covered by tests
}

allCubeMembers(path) {
Expand All @@ -743,6 +810,10 @@
return timeSeriesFromCustomInterval(granularityInterval, dateRange, moment(origin), { timestampPrecision: 3 });
}

getPreAggregationByName(cube, preAggregationName) {
return this.preAggregations.getRollupPreAggregationByName(cube, preAggregationName);

Check warning on line 814 in packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

View check run for this annotation

Codecov / codecov/patch

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

Added line #L814 was not covered by tests
}

get shouldReuseParams() {
return false;
}
Expand Down Expand Up @@ -922,7 +993,6 @@
const renderedWithQueries = withQueries.map(q => this.renderWithQuery(q));

let toJoin;

if (this.options.preAggregationQuery) {
const allRegular = regularMeasures.concat(
cumulativeMeasures
Expand Down Expand Up @@ -1153,7 +1223,6 @@

const multipliedMeasures = measuresToRender(true, false)(measureToHierarchy);
const regularMeasures = measuresToRender(false, false)(measureToHierarchy);

const cumulativeMeasures =
R.pipe(
R.map(multiplied => R.xprod([multiplied], measuresToRender(multiplied, true)(measureToHierarchy))),
Expand Down Expand Up @@ -3228,7 +3297,7 @@
}

newSubQueryForCube(cube, options) {
options = { ...options, useNativeSqlPlanner: false }; // We don't use tesseract for pre-aggregations generation yet
options = { ...options };
if (this.options.queryFactory) {
// When dealing with rollup joins, it's crucial to use the correct parameter allocator for the specific cube in use.
// By default, we'll use BaseQuery, but it's important to note that different databases (Oracle, PostgreSQL, MySQL, Druid, etc.)
Expand Down
35 changes: 29 additions & 6 deletions packages/cubejs-schema-compiler/src/adapter/PreAggregations.js
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@
transformedQuery.filterDimensionsSingleValueEqual || {},
)
));

const backAlias = (references) => references.map(r => (
Array.isArray(r) ?
[transformedQuery.allBackAliasMembers[r[0]] || r[0], r[1]] :
Expand Down Expand Up @@ -782,11 +782,15 @@
*/
findPreAggregationForQuery() {
if (!this.preAggregationForQuery) {
this.preAggregationForQuery =
this
.rollupMatchResults()
// Refresh worker can access specific pre-aggregations even in case those hidden by others
.find(p => p.canUsePreAggregation && (!this.query.options.preAggregationId || p.preAggregationId === this.query.options.preAggregationId));
if (this.query.useNativeSqlPlanner && this.query.canUseNativeSqlPlannerPreAggregation) {
this.preAggregationForQuery = this.query.findPreAggregationForQueryRust();

Check warning on line 786 in packages/cubejs-schema-compiler/src/adapter/PreAggregations.js

View check run for this annotation

Codecov / codecov/patch

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

Added line #L786 was not covered by tests
} else {
this.preAggregationForQuery =
this
.rollupMatchResults()
// Refresh worker can access specific pre-aggregations even in case those hidden by others
.find(p => p.canUsePreAggregation && (!this.query.options.preAggregationId || p.preAggregationId === this.query.options.preAggregationId));
}
}
return this.preAggregationForQuery;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case of useNativeSqlPlanner this will always be undefined.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. From this.query.findPreAggregationForQueryRust() code

      const [, , preAggregation] = buildResult.result;
      if (preAggregation) {
        this.preAggregations.preAggregationForQuery = preAggregation;
      }
    }
    return this.preAggregations.preAggregationForQuery;

Yeah, I know it’s not very elegant, but it’s pretty much the only fast way to splice the logic together until the whole PreAggregation part moves into Tesseract.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not return it instead of setting? Thus you can keep the logic similar and clean.

}
Expand Down Expand Up @@ -866,6 +870,25 @@
)(preAggregations);
}

getRollupPreAggregationByName(cube, preAggregationName) {
const canUsePreAggregation = () => true;
const preAggregation = R.pipe(

Check warning on line 875 in packages/cubejs-schema-compiler/src/adapter/PreAggregations.js

View check run for this annotation

Codecov / codecov/patch

packages/cubejs-schema-compiler/src/adapter/PreAggregations.js#L874-L875

Added lines #L874 - L875 were not covered by tests
R.toPairs,
R.filter(([_, a]) => a.type === 'rollup' || a.type === 'rollupJoin' || a.type === 'rollupLambda'),
R.find(([k, _]) => k === preAggregationName)

Check warning on line 878 in packages/cubejs-schema-compiler/src/adapter/PreAggregations.js

View check run for this annotation

Codecov / codecov/patch

packages/cubejs-schema-compiler/src/adapter/PreAggregations.js#L877-L878

Added lines #L877 - L878 were not covered by tests
)(this.query.cubeEvaluator.preAggregationsForCube(cube));
if (preAggregation) {
const tableName = this.preAggregationTableName(cube, preAggregation[0], preAggregation[1]);
const preAggObj = preAggregation ? this.evaluatedPreAggregationObj(cube, preAggregation[0], preAggregation[1], canUsePreAggregation) : {};
return {

Check warning on line 883 in packages/cubejs-schema-compiler/src/adapter/PreAggregations.js

View check run for this annotation

Codecov / codecov/patch

packages/cubejs-schema-compiler/src/adapter/PreAggregations.js#L880-L883

Added lines #L880 - L883 were not covered by tests
tableName,
...preAggObj
};
} else {
return {};

Check warning on line 888 in packages/cubejs-schema-compiler/src/adapter/PreAggregations.js

View check run for this annotation

Codecov / codecov/patch

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

Added line #L888 was not covered by tests
}
}

// TODO check multiplication factor didn't change
buildRollupJoin(preAggObj, preAggObjsToJoin) {
return this.query.cacheValue(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,13 @@ export class CubeEvaluator extends CubeSymbols {
return this.cubeFromPath(path).preAggregations || {};
}

public preAggregationsForCubeAsArray(path: string) {
return Object.entries(this.cubeFromPath(path).preAggregations || {}).map(([name, preAggregation]) => ({
name,
...(preAggregation as Record<string, any>)
}));
}

/**
* Returns pre-aggregations filtered by the specified selector.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ const measureTypeWithCount = Joi.string().valid(
);

const multiStageMeasureType = Joi.string().valid(
'count', 'number', 'string', 'boolean', 'time', 'sum', 'avg', 'min', 'max', 'countDistinct', 'runningTotal', 'countDistinctApprox',
'count', 'number', 'string', 'boolean', 'time', 'sum', 'avg', 'min', 'max', 'countDistinct', 'runningTotal', 'countDistinctApprox', 'numberAgg',
'rank'
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,9 @@ describe('PreAggregationsAlias', () => {
});

const preAggregationsDescription: any = query.preAggregations?.preAggregationsDescription();
const sqlAndParams = query.buildSqlAndParams();
expect(preAggregationsDescription[0].tableName).toEqual('rvis_rollupalias');
expect(query.buildSqlAndParams()[0]).toContain('rvis_rollupalias');
expect(sqlAndParams[0]).toContain('rvis_rollupalias');

return dbRunner.evaluateQueryWithPreAggregations(query).then(res => {
expect(res).toEqual(
Expand Down
Loading
Loading