Skip to content

Commit 7189720

Browse files
authored
fix: Views with proxy dimensions and non-additive measures don't not match pre-aggregations (#7374)
Fixes #7099
1 parent ac2bf15 commit 7189720

File tree

3 files changed

+184
-24
lines changed

3 files changed

+184
-24
lines changed

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

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -266,13 +266,15 @@ export class PreAggregations {
266266
}
267267

268268
static transformQueryToCanUseForm(query) {
269-
const sortedDimensions = this.squashDimensions(query);
269+
const flattenDimensionMembers = this.flattenDimensionMembers(query);
270+
const sortedDimensions = this.squashDimensions(query, flattenDimensionMembers);
271+
const aliasedToFirstNonAliasedDimension = this.aliasedToFirstNonAliasedDimension(query, flattenDimensionMembers);
270272
const measures = query.measures.concat(query.measureFilters);
271-
const measurePaths = R.uniq(measures.map(m => m.measure));
273+
const measurePaths = R.uniq(PreAggregations.firstNonAliasMember(query, measures));
272274
const collectLeafMeasures = query.collectLeafMeasures.bind(query);
273-
const dimensionsList = query.dimensions.map(dim => dim.dimension);
274-
const segmentsList = query.segments.map(s => s.segment);
275-
const ownedDimensions = PreAggregations.ownedMembers(query, PreAggregations.concatDimensionMembers(query));
275+
const dimensionsList = PreAggregations.firstNonAliasMember(query, query.dimensions);
276+
const segmentsList = PreAggregations.firstNonAliasMember(query, query.segments);
277+
const ownedDimensions = PreAggregations.ownedMembers(query, flattenDimensionMembers);
276278
const ownedTimeDimensions = query.timeDimensions.map(td => {
277279
const owned = PreAggregations.ownedMembers(query, [td]);
278280
let { dimension } = td;
@@ -288,6 +290,19 @@ export class PreAggregations {
288290
});
289291
}).map(d => query.newTimeDimension(d));
290292

293+
const firstNonAliasTimeDimensions = query.timeDimensions.map(td => {
294+
const nonAlias = PreAggregations.firstNonAliasMember(query, [td]);
295+
let { dimension } = td;
296+
if (nonAlias.length === 1) {
297+
[dimension] = nonAlias;
298+
}
299+
return query.newTimeDimension({
300+
dimension,
301+
dateRange: td.dateRange,
302+
granularity: td.granularity,
303+
});
304+
}).map(d => query.newTimeDimension(d));
305+
291306
const measureToLeafMeasures = {};
292307

293308
const leafMeasurePaths =
@@ -319,16 +334,18 @@ export class PreAggregations {
319334
return true;
320335
}
321336

322-
const sortedTimeDimensions = PreAggregations.sortTimeDimensionsWithRollupGranularity(query.timeDimensions);
323-
const timeDimensions = PreAggregations.timeDimensionsAsIs(query.timeDimensions);
337+
const sortedTimeDimensions = PreAggregations.sortTimeDimensionsWithRollupGranularity(firstNonAliasTimeDimensions);
338+
const timeDimensions = PreAggregations.timeDimensionsAsIs(firstNonAliasTimeDimensions);
324339
const ownedTimeDimensionsWithRollupGranularity = PreAggregations.sortTimeDimensionsWithRollupGranularity(ownedTimeDimensions);
325340
const ownedTimeDimensionsAsIs = PreAggregations.timeDimensionsAsIs(ownedTimeDimensions);
326341

327342
const hasNoTimeDimensionsWithoutGranularity = !query.timeDimensions.filter(d => !d.granularity).length;
328343

329344
const allFiltersWithinSelectedDimensions =
330345
R.all(d => dimensionsList.indexOf(d) !== -1)(
331-
query.filters.map(f => f.dimension)
346+
R.flatten(
347+
query.filters.map(f => f.getMembers())
348+
).map(f => PreAggregations.firstNonAliasMember(query, [f])[0])
332349
);
333350

334351
const isAdditive = R.all(m => m.isAdditive(), query.measures);
@@ -345,7 +362,8 @@ export class PreAggregations {
345362

346363
let filterDimensionsSingleValueEqual = this.collectFilterDimensionsWithSingleValueEqual(
347364
query.filters,
348-
dimensionsList.concat(segmentsList).reduce((map, d) => map.set(d, 1), new Map())
365+
dimensionsList.concat(segmentsList).reduce((map, d) => map.set(d, 1), new Map()),
366+
aliasedToFirstNonAliasedDimension
349367
);
350368

351369
filterDimensionsSingleValueEqual =
@@ -381,6 +399,14 @@ export class PreAggregations {
381399
);
382400
}
383401

402+
static firstNonAliasMember(query, members) {
403+
return members.map(
404+
member => query
405+
.collectFrom([member], query.collectMemberNamesFor.bind(query), 'collectMemberNamesFor')
406+
.find(d => !query.cubeEvaluator.byPathAnyType(d).aliasMember)
407+
);
408+
}
409+
384410
static sortTimeDimensionsWithRollupGranularity(timeDimensions) {
385411
return timeDimensions && R.sortBy(
386412
R.prop(0),
@@ -395,13 +421,14 @@ export class PreAggregations {
395421
) || [];
396422
}
397423

398-
static collectFilterDimensionsWithSingleValueEqual(filters, map) {
424+
static collectFilterDimensionsWithSingleValueEqual(filters, map, aliasedToFirstNonAliasedDimension) {
399425
// eslint-disable-next-line no-restricted-syntax
400426
for (const f of filters) {
401427
if (f.operator === 'equals') {
402-
map.set(f.dimension, Math.min(map.get(f.dimension) || 2, f.values.length));
428+
const nonAliasedDimension = aliasedToFirstNonAliasedDimension[f.dimension] || f.dimension;
429+
map.set(nonAliasedDimension, Math.min(map.get(nonAliasedDimension) || 2, f.values.length));
403430
} else if (f.operator === 'and') {
404-
const res = this.collectFilterDimensionsWithSingleValueEqual(f.values, map);
431+
const res = this.collectFilterDimensionsWithSingleValueEqual(f.values, map, aliasedToFirstNonAliasedDimension);
405432
if (res == null) return null;
406433
} else {
407434
return null;
@@ -632,14 +659,29 @@ export class PreAggregations {
632659
}
633660
}
634661

635-
static squashDimensions(query) {
662+
static squashDimensions(query, flattenDimensionMembers) {
636663
return R.pipe(R.uniq, R.sortBy(R.identity))(
637-
query.dimensions.concat(query.filters).map(d => d.dimension).concat(query.segments.map(s => s.segment))
664+
PreAggregations.firstNonAliasMember(
665+
query,
666+
flattenDimensionMembers
667+
)
638668
);
639669
}
640670

641-
static concatDimensionMembers(query) {
642-
return query.dimensions.concat(query.filters).concat(query.segments);
671+
static aliasedToFirstNonAliasedDimension(query, flattenDimensionMembers) {
672+
return flattenDimensionMembers
673+
.map(member => (
674+
{ [member.dimension]: PreAggregations.firstNonAliasMember(query, [member])[0] }
675+
));
676+
}
677+
678+
static flattenDimensionMembers(query) {
679+
return R.flatten(
680+
query.dimensions
681+
.concat(query.filters)
682+
.concat(query.segments)
683+
.map(m => m.getMembers()),
684+
);
643685
}
644686

645687
// eslint-disable-next-line no-unused-vars

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

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,17 +138,23 @@ export class CubeEvaluator extends CubeSymbols {
138138

139139
for (const memberName of Object.keys(members)) {
140140
let ownedByCube = true;
141+
let aliasMember = false;
141142

142143
const member = members[memberName];
143144
if (member.sql && !member.subQuery) {
144145
const funcArgs = this.funcArguments(member.sql);
145-
const cubeReferences = this.collectUsedCubeReferences(cube.name, member.sql);
146+
const { cubeReferencesUsed, evaluatedSql, pathReferencesUsed } = this.collectUsedCubeReferences(cube.name, member.sql);
146147
// We won't check for FILTER_PARAMS here as it shouldn't affect ownership and it should obey the same reference rules.
147148
// To affect ownership FILTER_PARAMS can be declared as `${FILTER_PARAMS.Foo.bar.filter(`${Foo.bar}`)}`.
148-
if (funcArgs.length > 0 && cubeReferences.length === 0) {
149+
// It isn't owned if there are non {CUBE} references
150+
if (funcArgs.length > 0 && cubeReferencesUsed.length === 0) {
149151
ownedByCube = false;
150152
}
151-
const foreignCubes = cubeReferences.filter(usedCube => usedCube !== cube.name);
153+
// Aliases one to one some another member as in case of views
154+
if (!ownedByCube && pathReferencesUsed.length === 1 && this.pathFromArray(pathReferencesUsed[0]) === evaluatedSql) {
155+
aliasMember = true;
156+
}
157+
const foreignCubes = cubeReferencesUsed.filter(usedCube => usedCube !== cube.name);
152158
if (foreignCubes.length > 0) {
153159
errorReporter.error(`Member '${cube.name}.${memberName}' references foreign cubes: ${foreignCubes.join(', ')}. Please split and move this definition to corresponding cubes.`);
154160
}
@@ -158,7 +164,7 @@ export class CubeEvaluator extends CubeSymbols {
158164
errorReporter.error(`View '${cube.name}' defines own member '${cube.name}.${memberName}'. Please move this member definition to one of the cubes.`);
159165
}
160166

161-
members[memberName] = { ...members[memberName], ownedByCube };
167+
members[memberName] = { ...members[memberName], ownedByCube, aliasMember };
162168
}
163169
}
164170

@@ -357,8 +363,9 @@ export class CubeEvaluator extends CubeSymbols {
357363
const cubeEvaluator = this;
358364

359365
const cubeReferencesUsed = [];
366+
const pathReferencesUsed = [];
360367

361-
cubeEvaluator.resolveSymbolsCall(sqlFn, (name) => {
368+
const evaluatedSql = cubeEvaluator.resolveSymbolsCall(sqlFn, (name) => {
362369
const referencedCube = cubeEvaluator.symbols[name] && name || cube;
363370
const resolvedSymbol =
364371
cubeEvaluator.resolveSymbol(
@@ -369,14 +376,20 @@ export class CubeEvaluator extends CubeSymbols {
369376
if (resolvedSymbol._objectWithResolvedProperties) {
370377
return resolvedSymbol;
371378
}
372-
return cubeEvaluator.pathFromArray([referencedCube, name]);
379+
const path = [referencedCube, name];
380+
pathReferencesUsed.push(path);
381+
return cubeEvaluator.pathFromArray(path);
373382
}, {
374383
// eslint-disable-next-line no-shadow
375-
sqlResolveFn: (symbol, cube, n) => cubeEvaluator.pathFromArray([cube, n]),
384+
sqlResolveFn: (symbol, cube, n) => {
385+
const path = [cube, n];
386+
pathReferencesUsed.push(path);
387+
return cubeEvaluator.pathFromArray(path);
388+
},
376389
contextSymbols: BaseQuery.emptyParametrizedContextSymbols(this, () => '$empty_param$'),
377390
cubeReferencesUsed,
378391
});
379-
return cubeReferencesUsed;
392+
return { cubeReferencesUsed, pathReferencesUsed, evaluatedSql };
380393
}
381394

382395
evaluatePreAggregationReferences(cube, aggregation) {

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

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ describe('PreAggregations', () => {
8282
type: 'time',
8383
sql: 'created_at'
8484
},
85+
signedUpAt: {
86+
type: 'time',
87+
sql: \`\${createdAt}\`
88+
},
8589
checkinsCount: {
8690
type: 'number',
8791
sql: \`\${visitor_checkins.count}\`,
@@ -450,6 +454,13 @@ describe('PreAggregations', () => {
450454
}
451455
}
452456
});
457+
458+
view('visitors_view', {
459+
cubes: [{
460+
join_path: visitors,
461+
includes: ['uniqueSourceCount', 'ratio', 'createdAt', 'signedUpAt']
462+
}]
463+
});
453464
`);
454465

455466
it('simple pre-aggregation', () => compiler.compile().then(() => {
@@ -540,6 +551,100 @@ describe('PreAggregations', () => {
540551
});
541552
}));
542553

554+
it('leaf measure view pre-aggregation', () => compiler.compile().then(() => {
555+
const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, {
556+
measures: [
557+
'visitors_view.ratio'
558+
],
559+
timeDimensions: [{
560+
dimension: 'visitors_view.createdAt',
561+
granularity: 'day',
562+
dateRange: ['2017-01-01', '2017-01-30']
563+
}],
564+
timezone: 'America/Los_Angeles',
565+
order: [{
566+
id: 'visitors_view.createdAt'
567+
}],
568+
preAggregationsSchema: ''
569+
});
570+
571+
const queryAndParams = query.buildSqlAndParams();
572+
console.log(queryAndParams);
573+
const preAggregationsDescription = query.preAggregations?.preAggregationsDescription();
574+
console.log(preAggregationsDescription);
575+
expect((<any>preAggregationsDescription)[0].loadSql[0]).toMatch(/visitors_ratio/);
576+
577+
return dbRunner.evaluateQueryWithPreAggregations(query).then(res => {
578+
expect(res).toEqual(
579+
[
580+
{
581+
visitors_view__created_at_day: '2017-01-02T00:00:00.000Z',
582+
visitors_view__ratio: '0.33333333333333333333'
583+
},
584+
{
585+
visitors_view__created_at_day: '2017-01-04T00:00:00.000Z',
586+
visitors_view__ratio: '0.50000000000000000000'
587+
},
588+
{
589+
visitors_view__created_at_day: '2017-01-05T00:00:00.000Z',
590+
visitors_view__ratio: '1.00000000000000000000'
591+
},
592+
{
593+
visitors_view__created_at_day: '2017-01-06T00:00:00.000Z',
594+
visitors_view__ratio: null
595+
}
596+
]
597+
);
598+
});
599+
}));
600+
601+
it('non-additive measure view pre-aggregation', () => compiler.compile().then(() => {
602+
const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, {
603+
measures: [
604+
'visitors_view.uniqueSourceCount'
605+
],
606+
timeDimensions: [{
607+
dimension: 'visitors_view.signedUpAt',
608+
granularity: 'day',
609+
dateRange: ['2017-01-01', '2017-01-30']
610+
}],
611+
timezone: 'America/Los_Angeles',
612+
order: [{
613+
id: 'visitors_view.createdAt'
614+
}],
615+
preAggregationsSchema: ''
616+
});
617+
618+
const queryAndParams = query.buildSqlAndParams();
619+
console.log(queryAndParams);
620+
const preAggregationsDescription = query.preAggregations?.preAggregationsDescription();
621+
console.log(preAggregationsDescription);
622+
expect((<any>preAggregationsDescription)[0].loadSql[0]).toMatch(/visitors_ratio/);
623+
624+
return dbRunner.evaluateQueryWithPreAggregations(query).then(res => {
625+
expect(res).toEqual(
626+
[
627+
{
628+
visitors_view__signed_up_at_day: '2017-01-02T00:00:00.000Z',
629+
visitors_view__unique_source_count: '1'
630+
},
631+
{
632+
visitors_view__signed_up_at_day: '2017-01-04T00:00:00.000Z',
633+
visitors_view__unique_source_count: '1'
634+
},
635+
{
636+
visitors_view__signed_up_at_day: '2017-01-05T00:00:00.000Z',
637+
visitors_view__unique_source_count: '1'
638+
},
639+
{
640+
visitors_view__signed_up_at_day: '2017-01-06T00:00:00.000Z',
641+
visitors_view__unique_source_count: '0'
642+
}
643+
]
644+
);
645+
});
646+
}));
647+
543648
it('inherited original sql', () => compiler.compile().then(() => {
544649
const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, {
545650
measures: [

0 commit comments

Comments
 (0)