Skip to content

Commit 0576b21

Browse files
committed
fix(schema-compiler): Handle measures with dimension-only member expressions in fullKeyQueryAggregateMeasures
`fullKeyQueryAggregate` is using measure.cube() a lot, like for grouping measures to key cube This can be wrong for measure targeting view: we want to attach measure to subquery for its definition cube, and there should be no subquery for view itself It's expected that `fullKeyQueryAggregateMeasures` would resolve and prepare all measures from its original form in query to actual leaf measures in cubes, then build subqueries with those, and then `joinFullKeyQueryAggregate` would use `renderedReference` to point to these leaf measures Hard case is a measure that: * targeting view * is a member expression * references only dimensions from that view Measure like that are "leaf" - there's nowhere to push it, member expression has to be directly in leaf subquery If measure references dimension from a single cube it can be multiplied (think `SUM(${view.deep_dimension})`) So, three points: * it must be accounted for in `multipliedMeasures` * it must be attached to a proper cube subquery * it must use renderedReference correctly `collectRootMeasureToHieararchy` will not drop such measure completely. Now it will check is there's 0 measures collected, try to collect all referenced members to gather used cubes and detect multiplication, and add new measure in hierarchy. Because new returned measure is patched, it will be attached to correct cube subquery in `fullKeyQueryAggregate` Then `outerMeasuresJoinFullKeyQueryAggregate` needs to generate proper alias for it, so outer unpatched measure would pick up alias from inner patched one
1 parent 9003448 commit 0576b21

File tree

1 file changed

+54
-2
lines changed

1 file changed

+54
-2
lines changed

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

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,7 +1058,7 @@ export class BaseQuery {
10581058
const member = m.measure ? m.measure : m.dimension;
10591059
const memberPath = typeof member === 'string'
10601060
? member
1061-
: this.cubeEvaluator.pathFromArray([m.expressionCubeName, m.expressionName]);
1061+
: this.cubeEvaluator.pathFromArray([m.measure?.originalCubeName ?? m.expressionCubeName, m.expressionName]);
10621062
return [memberPath, m.aliasName()];
10631063
}),
10641064
R.fromPairs,
@@ -1701,6 +1701,8 @@ export class BaseQuery {
17011701
.filter(f => R.none(m => m.measure === f.measure, this.measures));
17021702

17031703
return R.fromPairs(this.measures.concat(notAddedMeasureFilters).map(m => {
1704+
const measureName = typeof m.measure === 'string' ? m.measure : `${m.measure.cubeName}.${m.measure.name}`;
1705+
17041706
const collectedMeasures = this.collectFrom(
17051707
[m],
17061708
this.collectMultipliedMeasures(context),
@@ -1714,7 +1716,57 @@ export class BaseQuery {
17141716
const cubeName = m.expressionCubeName ? `\`${m.expressionCubeName}\` ` : '';
17151717
throw new UserError(`The query contains \`COUNT(*)\` expression but cube/view ${cubeName}is missing \`count\` measure`);
17161718
}
1717-
return [typeof m.measure === 'string' ? m.measure : `${m.measure.cubeName}.${m.measure.name}`, collectedMeasures];
1719+
if (collectedMeasures.length === 0 && m.isMemberExpression) {
1720+
// `m` is member expression measure, but does not reference any other measure
1721+
// Consider this dimensions-only measure. This can happen at least in 2 cases:
1722+
// 1. Ad-hoc aggregation over dimension: SELECT MAX(dim) FROM cube
1723+
// 2. Ungrouped query with SQL pushdown will render every column as measure: SELECT dim1 FROM cube WHERE LOWER(dim2) = 'foo';
1724+
// Measures like this needs a special treatment to attach them to cube and decide if they are multiplied or not
1725+
// This would return measure object in `measure`, not path
1726+
// TODO return measure object for every measure
1727+
1728+
const memberNamesForMeasure = this.collectFrom(
1729+
[m],
1730+
this.collectMemberNamesFor.bind(this),
1731+
context ? ['collectMemberNamesFor', JSON.stringify(context)] : 'collectMemberNamesFor',
1732+
this.queryCache
1733+
);
1734+
const cubeNamesForMeasure = R.pipe(
1735+
R.map(mem => this.cubeEvaluator.parsePathAnyType(mem)[0]),
1736+
// Filtering views, because collectMemberNamesFor can return both view.dim and cube.dim
1737+
R.filter(cubeName => {
1738+
const cubeDef = this.cubeEvaluator.getCubeDefinition(cubeName);
1739+
return !cubeDef.isView;
1740+
}),
1741+
// Single member expression can reference multiple dimensions from same cube
1742+
R.uniq,
1743+
)(
1744+
memberNamesForMeasure
1745+
);
1746+
1747+
let cubeNameToAttach;
1748+
switch (cubeNamesForMeasure.length) {
1749+
case 1:
1750+
cubeNameToAttach = cubeNamesForMeasure[0];
1751+
break;
1752+
default:
1753+
throw new Error(`Expected single cube for dimension-only measure ${mName}, got ${cubeNamesForMeasure}`);
1754+
}
1755+
1756+
const multiplied = this.multipliedJoinRowResult(cubeNameToAttach) || false;
1757+
1758+
const attachedMeasure = {
1759+
...m.measure,
1760+
originalCubeName: m.measure.cubeName,
1761+
cubeName: cubeNameToAttach
1762+
};
1763+
1764+
return [measureName, [{
1765+
multiplied,
1766+
measure: attachedMeasure,
1767+
}]];
1768+
}
1769+
return [measureName, collectedMeasures];
17181770
}));
17191771
}
17201772

0 commit comments

Comments
 (0)