Skip to content

fix(schema-compiler): Use join paths from pre-aggregation declaration instead of building join tree from scratch #9471

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 15 commits into from
Apr 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ export class PreAggregations {

return {
...queryBody,
preAggregations: expandedPreAggregations.reduce((a, b) => a.concat(b), []),
preAggregations: expandedPreAggregations.flat(),
groupedPartitionPreAggregations: expandedPreAggregations
};
}
Expand Down
11 changes: 11 additions & 0 deletions packages/cubejs-schema-compiler/src/adapter/BaseDimension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { BaseQuery } from './BaseQuery';
import type { DimensionDefinition, SegmentDefinition } from '../compiler/CubeEvaluator';
import { CubeSymbols } from "../compiler/CubeSymbols";

export class BaseDimension {
public readonly expression: any;
Expand All @@ -10,6 +11,8 @@ export class BaseDimension {

public readonly isMemberExpression: boolean = false;

public readonly joinHint: Array<string> = [];

public constructor(
protected readonly query: BaseQuery,
public readonly dimension: any
Expand All @@ -20,6 +23,14 @@ export class BaseDimension {
// In case of SQL push down expressionName doesn't contain cube name. It's just a column name.
this.expressionName = dimension.expressionName || `${dimension.cubeName}.${dimension.name}`;
this.isMemberExpression = !!dimension.definition;
} else {
// TODO move this `as` to static types
const dimensionPath = dimension as string | null;
if (dimensionPath !== null) {
const { path, joinHint } = CubeSymbols.joinHintFromPath(dimensionPath);
this.dimension = path;
this.joinHint = joinHint;
}
}
}

Expand Down
9 changes: 9 additions & 0 deletions packages/cubejs-schema-compiler/src/adapter/BaseMeasure.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { UserError } from '../compiler/UserError';
import type { BaseQuery } from './BaseQuery';
import { MeasureDefinition } from '../compiler/CubeEvaluator';
import { CubeSymbols } from "../compiler/CubeSymbols";

export class BaseMeasure {
public readonly expression: any;
Expand All @@ -13,6 +14,8 @@ export class BaseMeasure {

protected readonly patchedMeasure: MeasureDefinition | null = null;

public readonly joinHint: Array<string> = [];

protected preparePatchedMeasure(sourceMeasure: string, newMeasureType: string | null, addFilters: Array<{sql: Function}>): MeasureDefinition {
const source = this.query.cubeEvaluator.measureByPath(sourceMeasure);

Expand Down Expand Up @@ -123,6 +126,12 @@ export class BaseMeasure {
measure.expression.addFilters,
);
}
} else {
// TODO move this `as` to static types
const measurePath = measure as string;
const { path, joinHint } = CubeSymbols.joinHintFromPath(measurePath);
this.measure = path;
this.joinHint = joinHint;
}
}

Expand Down
152 changes: 80 additions & 72 deletions packages/cubejs-schema-compiler/src/adapter/BaseQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
timeSeriesFromCustomInterval
} from '@cubejs-backend/shared';

import { CubeSymbols } from "../compiler/CubeSymbols";
import { UserError } from '../compiler/UserError';
import { SqlParser } from '../parser/SqlParser';
import { BaseDimension } from './BaseDimension';
Expand Down Expand Up @@ -307,16 +308,16 @@
}

prebuildJoin() {
if (this.useNativeSqlPlanner) {
// Tesseract doesn't require join to be prebuilt and there's a case where single join can't be built for multi-fact query
// But we need this join for a fallback when using pre-aggregations. So we’ll try to obtain the join but ignore any errors (which may occur if the query is a multi-fact one).
try {
this.join = this.joinGraph.buildJoin(this.allJoinHints);
} catch (e) {
// Ignore
}
} else {
try {
// TODO allJoinHints should contain join hints form pre-agg
this.join = this.joinGraph.buildJoin(this.allJoinHints);
} catch (e) {
if (this.useNativeSqlPlanner) {

Check warning on line 315 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#L315

Added line #L315 was not covered by tests
// Tesseract doesn't require join to be prebuilt and there's a case where single join can't be built for multi-fact query
// But we need this join for a fallback when using pre-aggregations. So we’ll try to obtain the join but ignore any errors (which may occur if the query is a multi-fact one).
} else {
throw e;

Check warning on line 319 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#L319

Added line #L319 was not covered by tests
}
}
}

Expand Down Expand Up @@ -363,6 +364,10 @@
return this.collectedCubeNames;
}

/**
*
* @returns {Array<Array<string>>}
*/
get allJoinHints() {
if (!this.collectedJoinHints) {
this.collectedJoinHints = this.collectJoinHints();
Expand Down Expand Up @@ -1203,7 +1208,16 @@

collectAllMultiStageMembers(allMemberChildren) {
const allMembers = R.uniq(R.flatten(Object.keys(allMemberChildren).map(k => [k].concat(allMemberChildren[k]))));
return R.fromPairs(allMembers.map(m => ([m, this.memberInstanceByPath(m).isMultiStage()])));
return R.fromPairs(allMembers.map(m => {
// When `m` is coming from `collectAllMemberChildren`, it can contain `granularities.customGranularityName` in path
// And it would mess up with join hints detection
const trimmedPath = this
.cubeEvaluator
.parsePathAnyType(m)
.slice(0, 2)
.join('.');
return [m, this.memberInstanceByPath(trimmedPath).isMultiStage()];
}));
}

memberInstanceByPath(m) {
Expand Down Expand Up @@ -1398,7 +1412,7 @@
// TODO condition should something else instead of rank
multiStageQuery: !!withQuery.measures.find(d => {
const { type } = this.newMeasure(d).definition();
return type === 'rank' || BaseQuery.isCalculatedMeasureType(type);
return type === 'rank' || CubeSymbols.isCalculatedMeasureType(type);

Check warning on line 1415 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#L1415

Added line #L1415 was not covered by tests
}),
disableExternalPreAggregations: true,
};
Expand Down Expand Up @@ -1992,7 +2006,7 @@
);

if (shouldBuildJoinForMeasureSelect) {
const joinHints = this.collectFrom(measures, this.collectJoinHintsFor.bind(this), 'collectJoinHintsFor');
const joinHints = this.collectJoinHintsFromMembers(measures);

Check warning on line 2009 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#L2009

Added line #L2009 was not covered by tests
const measuresJoin = this.joinGraph.buildJoin(joinHints);
if (measuresJoin.multiplicationFactor[keyCubeName]) {
throw new UserError(
Expand Down Expand Up @@ -2046,6 +2060,11 @@
(!this.safeEvaluateSymbolContext().ungrouped && this.aggregateSubQueryGroupByClause() || '');
}

/**
* @param {Array<BaseMeasure>} measures
* @param {string} keyCubeName
* @returns {boolean}
*/
checkShouldBuildJoinForMeasureSelect(measures, keyCubeName) {
// When member expression references view, it would have to collect join hints from view
// Consider join A->B, as many-to-one, so B is multiplied and A is not, and member expression like SUM(AB_view.dimB)
Expand All @@ -2067,7 +2086,11 @@
.filter(member => member.definition().ownedByCube);

const cubes = this.collectFrom(nonViewMembers, this.collectCubeNamesFor.bind(this), 'collectCubeNamesFor');
const joinHints = this.collectFrom(nonViewMembers, this.collectJoinHintsFor.bind(this), 'collectJoinHintsFor');
// Not using `collectJoinHintsFromMembers([measure])` because it would collect too many join hints from view
const joinHints = [
measure.joinHint,
...this.collectJoinHintsFromMembers(nonViewMembers),
];
if (R.any(cubeName => keyCubeName !== cubeName, cubes)) {
const measuresJoin = this.joinGraph.buildJoin(joinHints);
if (measuresJoin.multiplicationFactor[keyCubeName]) {
Expand Down Expand Up @@ -2186,12 +2209,29 @@
);
}

/**
*
* @param {boolean} [excludeTimeDimensions=false]
* @returns {Array<Array<string>>}
*/
collectJoinHints(excludeTimeDimensions = false) {
return this.collectFromMembers(
excludeTimeDimensions,
this.collectJoinHintsFor.bind(this),
'collectJoinHintsFor'
);
const membersToCollectFrom = this.allMembersConcat(excludeTimeDimensions)
.concat(this.join ? this.join.joins.map(j => ({
getMembers: () => [{
path: () => null,
cube: () => this.cubeEvaluator.cubeFromPath(j.originalFrom),
definition: () => j.join,

Check warning on line 2223 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#L2219-L2223

Added lines #L2219 - L2223 were not covered by tests
}]
})) : []);

return this.collectJoinHintsFromMembers(membersToCollectFrom);
}

collectJoinHintsFromMembers(members) {
return [
...members.map(m => m.joinHint).filter(h => h?.length > 0),
...this.collectFrom(members, this.collectJoinHintsFor.bind(this), 'collectJoinHintsFromMembers'),
];
}

collectFromMembers(excludeTimeDimensions, fn, methodName) {
Expand All @@ -2206,6 +2246,11 @@
return this.collectFrom(membersToCollectFrom, fn, methodName);
}

/**
*
* @param {boolean} excludeTimeDimensions
* @returns {Array<BaseMeasure | BaseDimension | BaseSegment>}
*/
allMembersConcat(excludeTimeDimensions) {
return this.measures
.concat(this.dimensions)
Expand Down Expand Up @@ -2902,7 +2947,7 @@
funDef = this.countDistinctApprox(evaluateSql);
} else if (symbol.type === 'countDistinct' || symbol.type === 'count' && !symbol.sql && multiplied) {
funDef = `count(distinct ${evaluateSql})`;
} else if (BaseQuery.isCalculatedMeasureType(symbol.type)) {
} else if (CubeSymbols.isCalculatedMeasureType(symbol.type)) {

Check warning on line 2950 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#L2950

Added line #L2950 was not covered by tests
// TODO calculated measure type will be ungrouped
// if (this.multiStageDimensions.length !== this.dimensions.length) {
// throw new UserError(`Calculated measure '${measurePath}' uses group_by or reduce_by context modifiers while it isn't allowed`);
Expand All @@ -2928,23 +2973,12 @@
return this.primaryKeyCount(cubeName, true);
}
}
if (BaseQuery.isCalculatedMeasureType(symbol.type)) {
if (CubeSymbols.isCalculatedMeasureType(symbol.type)) {
return evaluateSql;
}
return `${symbol.type}(${evaluateSql})`;
}

static isCalculatedMeasureType(type) {
return type === 'number' || type === 'string' || type === 'time' || type === 'boolean';
}

/**
TODO: support type qualifiers on min and max
*/
static toMemberDataType(type) {
return this.isCalculatedMeasureType(type) ? type : 'number';
}

aggregateOnGroupedColumn(symbol, evaluateSql, topLevelMerge, measurePath) {
const cumulativeMeasureFilters = (this.safeEvaluateSymbolContext().cumulativeMeasureFilters || {})[measurePath];
if (cumulativeMeasureFilters) {
Expand Down Expand Up @@ -4051,7 +4085,7 @@
sqlUtils: {
convertTz: (field) => field,
},
securityContext: BaseQuery.contextSymbolsProxyFrom({}, allocateParam),
securityContext: CubeSymbols.contextSymbolsProxyFrom({}, allocateParam),
};
}

Expand All @@ -4066,41 +4100,7 @@
}

contextSymbolsProxy(symbols) {
return BaseQuery.contextSymbolsProxyFrom(symbols, this.paramAllocator.allocateParam.bind(this.paramAllocator));
}

static contextSymbolsProxyFrom(symbols, allocateParam) {
return new Proxy(symbols, {
get: (target, name) => {
const propValue = target[name];
const methods = (paramValue) => ({
filter: (column) => {
if (paramValue) {
const value = Array.isArray(paramValue) ?
paramValue.map(allocateParam) :
allocateParam(paramValue);
if (typeof column === 'function') {
return column(value);
} else {
return `${column} = ${value}`;
}
} else {
return '1 = 1';
}
},
requiredFilter: (column) => {
if (!paramValue) {
throw new UserError(`Filter for ${column} is required`);
}
return methods(paramValue).filter(column);
},
unsafeValue: () => paramValue
});
return methods(target)[name] ||
typeof propValue === 'object' && propValue !== null && BaseQuery.contextSymbolsProxyFrom(propValue, allocateParam) ||
methods(propValue);
}
});
return CubeSymbols.contextSymbolsProxyFrom(symbols, this.paramAllocator.allocateParam.bind(this.paramAllocator));
}

static extractFilterMembers(filter) {
Expand Down Expand Up @@ -4306,6 +4306,11 @@
});
}

/**
*
* @param {boolean} excludeSegments
* @returns {Array<BaseMeasure | BaseDimension | BaseSegment>}
*/
flattenAllMembers(excludeSegments = false) {
return R.flatten(
this.measures
Expand All @@ -4326,9 +4331,14 @@
return this.backAliasMembers(this.flattenAllMembers());
}

/**
*
* @param {Array<BaseMeasure | BaseDimension | BaseSegment>} members
* @returns {Record<string, string>}
*/
backAliasMembers(members) {
const query = this;
return members.map(
return Object.fromEntries(members.flatMap(
member => {
const collectedMembers = query.evaluateSymbolSqlWithContext(
() => query.collectFrom([member], query.collectMemberNamesFor.bind(query), 'collectMemberNamesFor'),
Expand All @@ -4343,10 +4353,8 @@
}
return !nonAliasSeen;
})
.map(d => (
{ [query.cubeEvaluator.byPathAnyType(d).aliasMember]: memberPath }
)).reduce((a, b) => ({ ...a, ...b }), {});
.map(d => [query.cubeEvaluator.byPathAnyType(d).aliasMember, memberPath]);
}
).reduce((a, b) => ({ ...a, ...b }), {});
));
}
}
9 changes: 9 additions & 0 deletions packages/cubejs-schema-compiler/src/adapter/BaseSegment.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { BaseQuery } from './BaseQuery';
import { CubeSymbols } from "../compiler/CubeSymbols";

export class BaseSegment {
public readonly expression: any;
Expand All @@ -9,6 +10,8 @@ export class BaseSegment {

public readonly isMemberExpression: boolean = false;

public readonly joinHint: Array<string> = [];

public constructor(
protected readonly query: BaseQuery,
public readonly segment: string | any
Expand All @@ -19,6 +22,12 @@ export class BaseSegment {
// In case of SQL push down expressionName doesn't contain cube name. It's just a column name.
this.expressionName = segment.expressionName || `${segment.cubeName}.${segment.name}`;
this.isMemberExpression = !!segment.definition;
} else {
// TODO move this `as` to static types
const segmentPath = segment as string;
const { path, joinHint } = CubeSymbols.joinHintFromPath(segmentPath);
this.segment = path;
this.joinHint = joinHint;
}
}

Expand Down
Loading
Loading