Skip to content

fix(schema-compiler): Reject pre-agg if measure is unmultiplied in query but multiplied in pre-agg #9541

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

Open
wants to merge 2 commits into
base: chore/preaggs-2-ts
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions packages/cubejs-schema-compiler/src/adapter/BaseQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -3787,11 +3787,11 @@ export class BaseQuery {
}

// eslint-disable-next-line consistent-return
preAggregationQueryForSqlEvaluation(cube, preAggregation) {
preAggregationQueryForSqlEvaluation(cube, preAggregation, context = {}) {
if (preAggregation.type === 'autoRollup') {
return this.preAggregations.autoRollupPreAggregationQuery(cube, preAggregation);
} else if (preAggregation.type === 'rollup') {
return this.preAggregations.rollupPreAggregationQuery(cube, preAggregation);
return this.preAggregations.rollupPreAggregationQuery(cube, preAggregation, context);
} else if (preAggregation.type === 'originalSql') {
return this;
}
Expand Down
33 changes: 29 additions & 4 deletions packages/cubejs-schema-compiler/src/adapter/PreAggregations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export type PreAggregationForCube = {
references: PreAggregationReferences;
};

export type EvaluateReferencesContext = {
inPreAggEvaluation?: boolean;
};

export type BaseMember = BaseDimension | BaseMeasure | BaseFilter | BaseGroupFilter | BaseSegment;

export type CanUsePreAggregationFn = (references: PreAggregationReferences) => boolean;
Expand Down Expand Up @@ -583,12 +587,15 @@ export class PreAggregations {
const dimensionsTrimmed = references
.dimensions
.map(d => CubeSymbols.joinHintFromPath(d).path);
const multipliedMeasuresTrimmed = references
.multipliedMeasures?.map(m => CubeSymbols.joinHintFromPath(m).path);

return {
...references,
dimensions: dimensionsTrimmed,
measures: measuresTrimmed,
timeDimensions: timeDimensionsTrimmed,
multipliedMeasures: multipliedMeasuresTrimmed,
};
}

Expand Down Expand Up @@ -723,6 +730,19 @@ export class PreAggregations {
// TODO remove this in favor of matching with join path
const referencesTrimmed = trimmedReferences(references);

// Even if there are no multiplied measures in the query (because no multiplier dimensions are requested)
// but the same measures are multiplied in the pre-aggregation, we can't use pre-aggregation
// for such queries.
if (referencesTrimmed.multipliedMeasures) {
const backAliasMultipliedMeasures = backAlias(referencesTrimmed.multipliedMeasures);

if (transformedQuery.leafMeasures.some(m => referencesTrimmed.multipliedMeasures.includes(m)) ||
transformedQuery.measures.some(m => backAliasMultipliedMeasures.includes(m))
) {
return false;
}
}

const dimensionsMatch = (dimensions, doBackAlias) => R.all(
d => (
doBackAlias ?
Expand Down Expand Up @@ -1171,11 +1191,11 @@ export class PreAggregations {
);
}

public rollupPreAggregationQuery(cube: string, aggregation): BaseQuery {
public rollupPreAggregationQuery(cube: string, aggregation: PreAggregationDefinition, context: EvaluateReferencesContext = {}): BaseQuery {
// `this.evaluateAllReferences` will retain not only members, but their join path as well, and pass join hints
// to subquery. Otherwise, members in subquery would regenerate new join tree from clean state,
// and it can be different from expected by join path in pre-aggregation declaration
const references = this.evaluateAllReferences(cube, aggregation);
const references = this.evaluateAllReferences(cube, aggregation, null, context);
const cubeQuery = this.query.newSubQueryForCube(cube, {});
return this.query.newSubQueryForCube(cube, {
rowLimit: null,
Expand Down Expand Up @@ -1203,7 +1223,7 @@ export class PreAggregations {
});
}

public autoRollupPreAggregationQuery(cube: string, aggregation): BaseQuery {
public autoRollupPreAggregationQuery(cube: string, aggregation: PreAggregationDefinition): BaseQuery {
return this.query.newSubQueryForCube(
cube,
{
Expand Down Expand Up @@ -1244,13 +1264,18 @@ export class PreAggregations {
.toLowerCase();
}

private evaluateAllReferences(cube: string, aggregation: PreAggregationDefinition, preAggregationName: string | null = null): PreAggregationReferences {
private evaluateAllReferences(cube: string, aggregation: PreAggregationDefinition, preAggregationName: string | null = null, context: EvaluateReferencesOptions = {}): PreAggregationReferences {
// TODO build a join tree for all references, so they would always include full join path
// Even for preaggregation references without join path
// It is necessary to be able to match query and preaggregation based on full join tree

const evaluateReferences = () => {
const references = this.query.cubeEvaluator.evaluatePreAggregationReferences(cube, aggregation);
if (!context.inPreAggEvaluation) {
const preAggQuery = this.query.preAggregationQueryForSqlEvaluation(cube, aggregation, { inPreAggEvaluation: true });
const aggregateMeasures = preAggQuery?.fullKeyQueryAggregateMeasures({ hasMultipliedForPreAggregation: true });
references.multipliedMeasures = aggregateMeasures?.multipliedMeasures?.map(m => m.measure);
}
if (aggregation.type === 'rollupLambda') {
if (references.rollups.length > 0) {
const [firstLambdaCube] = this.query.cubeEvaluator.parsePath('preAggregations', references.rollups[0]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export type PreAggregationReferences = {
measures: Array<string>,
timeDimensions: Array<PreAggregationTimeDimensionReference>,
rollups: Array<string>,
multipliedMeasures?: Array<string>,
};

export type PreAggregationInfo = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,6 @@ describe('MSSqlPreAggregations', () => {
timeDimensionReference: createdAt,
granularity: 'day',
},
approx: {
type: 'rollup',
measureReferences: [countDistinctApprox],
timeDimensionReference: createdAt,
granularity: 'day'
},
ratioRollup: {
type: 'rollup',
measureReferences: [checkinsTotal, uniqueSourceCount],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
cubes:
- name: line_items
sql_table: public.line_items

dimensions:
- name: id
sql: id
type: number
primary_key: true

- name: product_id
sql: product_id
type: number

- name: order_id
sql: order_id
type: number

- name: quantity
sql: quantity
type: number

- name: price
sql: price
type: number

- name: orders
sql_table: public.orders
joins:
- name: line_items
sql: "{CUBE}.id = {line_items}.order_id"
relationship: one_to_many
dimensions:
- name: id
sql: id
type: number
primary_key: true
- name: user_id
sql: user_id
type: number
- name: amount
sql: amount
type: number
- name: status
sql: status
type: string
- name: status_new
sql: status || '_new'
type: string
- name: created_at
sql: created_at
type: time
measures:
- name: count
type: count
- name: total_qty
type: sum
sql: amount

pre_aggregations:
- name: pre_agg_with_multiplied_measures
dimensions:
- orders.status
- line_items.product_id
measures:
- orders.count
- orders.total_qty
timeDimension: orders.created_at
granularity: month
163 changes: 163 additions & 0 deletions packages/cubejs-schema-compiler/test/unit/pre-aggregations.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import fs from 'fs';
import path from 'path';
import { prepareJsCompiler, prepareYamlCompiler } from './PrepareCompiler';
import { createECommerceSchema, createSchemaYaml } from './utils';
import { PostgresQuery, queryClass, QueryFactory } from '../../src';
Expand Down Expand Up @@ -447,4 +449,165 @@ describe('pre-aggregations', () => {
expect(preAggregationsDescription[0].invalidateKeyQueries[0][0].includes('WHERE ((date(created_at) >= $1::timestamptz AND date(created_at) <= $2::timestamptz))')).toBeTruthy();
expect(preAggregationsDescription[0].invalidateKeyQueries[0][1]).toEqual(['__FROM_PARTITION_RANGE', '__TO_PARTITION_RANGE']);
});

describe('rollup with multiplied measure', () => {
let compiler;
let cubeEvaluator;
let joinGraph;

beforeAll(async () => {
const modelContent = fs.readFileSync(
path.join(process.cwd(), '/test/unit/fixtures/orders_and_items_multiplied_pre_agg.yml'),
'utf8'
);
({ compiler, cubeEvaluator, joinGraph } = prepareYamlCompiler(modelContent));
await compiler.compile();
});

it('measure is unmultiplied in query but multiplied in pre-agg', async () => {
const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, {
measures: [
'orders.total_qty'
],
dimensions: [],
timeDimensions: [
{
dimension: 'orders.created_at',
dateRange: [
'2017-05-01',
'2025-05-01'
]
}
]
});

const preAggregationsDescription: any = query.preAggregations?.preAggregationsDescription();
// Pre-agg should not match
expect(preAggregationsDescription).toEqual([]);
});

it('measure is unmultiplied in query but multiplied in pre-agg + granularity', async () => {
const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, {
measures: [
'orders.total_qty'
],
dimensions: [],
timeDimensions: [
{
dimension: 'orders.created_at',
dateRange: [
'2017-05-01',
'2025-05-01'
],
granularity: 'month'
}
]
});

const preAggregationsDescription: any = query.preAggregations?.preAggregationsDescription();
// Pre-agg should not match
expect(preAggregationsDescription).toEqual([]);
});

it('measure is unmultiplied in query but multiplied in pre-agg + granularity + local dimension', async () => {
const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, {
measures: [
'orders.total_qty'
],
dimensions: [
'orders.status'
],
timeDimensions: [
{
dimension: 'orders.created_at',
dateRange: [
'2017-05-01',
'2025-05-01'
],
granularity: 'month'
}
]
});

const preAggregationsDescription: any = query.preAggregations?.preAggregationsDescription();
// Pre-agg should not match
expect(preAggregationsDescription).toEqual([]);
});

it('measure is unmultiplied in query but multiplied in pre-agg + granularity + external dimension', async () => {
const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, {
measures: [
'orders.total_qty'
],
dimensions: [
'line_items.product_id'
],
timeDimensions: [
{
dimension: 'orders.created_at',
dateRange: [
'2017-05-01',
'2025-05-01'
],
granularity: 'month'
}
]
});

const preAggregationsDescription: any = query.preAggregations?.preAggregationsDescription();
// Pre-agg should not match
expect(preAggregationsDescription).toEqual([]);
});

it('partial-match of query with pre-agg: 1 measure + all dimensions, no granularity', async () => {
const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, {
measures: [
'orders.total_qty'
],
dimensions: [
'orders.status',
'line_items.product_id'
],
timeDimensions: [
{
dimension: 'orders.created_at',
dateRange: [
'2017-05-01',
'2025-05-01'
]
}
]
});

const preAggregationsDescription: any = query.preAggregations?.preAggregationsDescription();
// Pre-agg should not match
expect(preAggregationsDescription).toEqual([]);
});

it('full-match of query with pre-agg: 1 measure + granularity + all dimensions', async () => {
const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, {
measures: [
'orders.total_qty'
],
dimensions: [
'orders.status',
'line_items.product_id'
],
timeDimensions: [
{
dimension: 'orders.created_at',
dateRange: [
'2017-05-01',
'2025-05-01'
],
granularity: 'month'
}
]
});

const preAggregationsDescription: any = query.preAggregations?.preAggregationsDescription();
expect(preAggregationsDescription.length).toEqual(1);
expect(preAggregationsDescription[0].preAggregationId).toEqual('orders.pre_agg_with_multiplied_measures');
});
});
});
Loading