Skip to content

Commit 320484f

Browse files
authored
feat(schema-compiler): Allow one measure timeShift without time dimension (#9545)
* make timeDimension in timeshift measure optional * fix evaluateMultiStageReferences * optional in basequery * use shiftInterval even if timeShift is w/o timeDimension * tests
1 parent b7d4a67 commit 320484f

File tree

5 files changed

+203
-32
lines changed

5 files changed

+203
-32
lines changed

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

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,10 +1356,22 @@ export class BaseQuery {
13561356
if (memberDef.addGroupByReferences) {
13571357
queryContext = { ...queryContext, dimensions: R.uniq(queryContext.dimensions.concat(memberDef.addGroupByReferences)) };
13581358
}
1359-
if (memberDef.timeShiftReferences) {
1360-
queryContext = {
1361-
...queryContext,
1362-
timeDimensions: queryContext.timeDimensions.map(td => {
1359+
if (memberDef.timeShiftReferences?.length) {
1360+
let mapFn;
1361+
1362+
if (memberDef.timeShiftReferences.length === 1 && !memberDef.timeShiftReferences[0].timeDimension) {
1363+
const timeShift = memberDef.timeShiftReferences[0];
1364+
mapFn = (td) => {
1365+
if (td.shiftInterval) {
1366+
throw new UserError(`Hierarchical time shift is not supported but was provided for '${td.dimension}'. Parent time shift is '${td.shiftInterval}' and current is '${timeShift.interval}'`);
1367+
}
1368+
return {
1369+
...td,
1370+
shiftInterval: timeShift.type === 'next' ? this.negateInterval(timeShift.interval) : timeShift.interval
1371+
};
1372+
};
1373+
} else {
1374+
mapFn = (td) => {
13631375
const timeShift = memberDef.timeShiftReferences.find(r => r.timeDimension === td.dimension);
13641376
if (timeShift) {
13651377
if (td.shiftInterval) {
@@ -1371,7 +1383,12 @@ export class BaseQuery {
13711383
};
13721384
}
13731385
return td;
1374-
})
1386+
};
1387+
}
1388+
1389+
queryContext = {
1390+
...queryContext,
1391+
timeDimensions: queryContext.timeDimensions.map(mapFn)
13751392
};
13761393
}
13771394
queryContext = {

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ export type DimensionDefinition = {
2828
};
2929

3030
export type TimeShiftDefinition = {
31-
timeDimension: (...args: Array<unknown>) => ToString,
31+
timeDimension?: (...args: Array<unknown>) => ToString,
3232
interval: string,
3333
type: 'next' | 'prior',
3434
};
3535

3636
export type TimeShiftDefinitionReference = {
37-
timeDimension: string,
37+
timeDimension?: string,
3838
interval: string,
3939
type: 'next' | 'prior',
4040
};
@@ -353,8 +353,13 @@ export class CubeEvaluator extends CubeSymbols {
353353
member.addGroupByReferences = this.evaluateReferences(cubeName, member.addGroupBy);
354354
}
355355
if (member.timeShift) {
356-
member.timeShiftReferences = member.timeShift
357-
.map(s => ({ ...s, timeDimension: this.evaluateReferences(cubeName, s.timeDimension) }));
356+
member.timeShiftReferences = member.timeShift.map((s): TimeShiftDefinitionReference => ({
357+
interval: s.interval,
358+
type: s.type,
359+
...(typeof s.timeDimension === 'function'
360+
? { timeDimension: this.evaluateReferences(cubeName, s.timeDimension) }
361+
: {}),
362+
}));
358363
}
359364
}
360365
}

packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,18 @@ const multiStageMeasureType = Joi.string().valid(
564564
'rank'
565565
);
566566

567+
const timeShiftItemRequired = Joi.object({
568+
timeDimension: Joi.func().required(),
569+
interval: regexTimeInterval.required(),
570+
type: Joi.string().valid('next', 'prior').required(),
571+
});
572+
573+
const timeShiftItemOptional = Joi.object({
574+
timeDimension: Joi.func(), // не required
575+
interval: regexTimeInterval.required(),
576+
type: Joi.string().valid('next', 'prior').required(),
577+
});
578+
567579
const MeasuresSchema = Joi.object().pattern(identifierRegex, Joi.alternatives().conditional(Joi.ref('.multiStage'), [
568580
{
569581
is: true,
@@ -574,11 +586,10 @@ const MeasuresSchema = Joi.object().pattern(identifierRegex, Joi.alternatives().
574586
groupBy: Joi.func(),
575587
reduceBy: Joi.func(),
576588
addGroupBy: Joi.func(),
577-
timeShift: Joi.array().items(Joi.object().keys({
578-
timeDimension: Joi.func().required(),
579-
interval: regexTimeInterval.required(),
580-
type: Joi.string().valid('next', 'prior').required(),
581-
})),
589+
timeShift: Joi.alternatives().conditional(Joi.array().length(1), {
590+
then: Joi.array().items(timeShiftItemOptional),
591+
otherwise: Joi.array().items(timeShiftItemRequired)
592+
}),
582593
// TODO validate for order window functions
583594
orderBy: Joi.array().items(Joi.object().keys({
584595
sql: Joi.func().required(),

packages/cubejs-schema-compiler/test/integration/postgres/sql-generation.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,15 @@ describe('SQL Generation', () => {
129129
type: 'prior',
130130
}]
131131
},
132+
revenue_day_ago_no_td: {
133+
multi_stage: true,
134+
type: 'sum',
135+
sql: \`\${revenue}\`,
136+
time_shift: [{
137+
interval: '1 day',
138+
type: 'prior',
139+
}]
140+
},
132141
cagr_day: {
133142
multi_stage: true,
134143
sql: \`ROUND(100 * \${revenue} / NULLIF(\${revenue_day_ago}, 0))\`,
@@ -1347,6 +1356,26 @@ SELECT 1 AS revenue, cast('2024-01-01' AS timestamp) as time UNION ALL
13471356
{ visitors__created_at_day: '2017-01-06T00:00:00.000Z', visitors__cagr_day: '300', visitors__revenue: '900', visitors__revenue_day_ago: '300' }
13481357
]));
13491358

1359+
it('CAGR (no td in time_shift)', async () => runQueryTest({
1360+
measures: [
1361+
'visitors.revenue',
1362+
'visitors.revenue_day_ago_no_td',
1363+
'visitors.cagr_day'
1364+
],
1365+
timeDimensions: [{
1366+
dimension: 'visitors.created_at',
1367+
granularity: 'day',
1368+
dateRange: ['2016-12-01', '2017-01-31']
1369+
}],
1370+
order: [{
1371+
id: 'visitors.created_at'
1372+
}],
1373+
timezone: 'America/Los_Angeles'
1374+
}, [
1375+
{ visitors__created_at_day: '2017-01-05T00:00:00.000Z', visitors__cagr_day: '150', visitors__revenue: '300', visitors__revenue_day_ago_no_td: '200' },
1376+
{ visitors__created_at_day: '2017-01-06T00:00:00.000Z', visitors__cagr_day: '300', visitors__revenue: '900', visitors__revenue_day_ago_no_td: '300' }
1377+
]));
1378+
13501379
it('sql utils', async () => runQueryTest({
13511380
measures: [
13521381
'visitors.visitor_count'

packages/cubejs-schema-compiler/test/unit/cube-validator.test.ts

Lines changed: 127 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -251,27 +251,136 @@ describe('Cube Validation', () => {
251251
expect(validationResult.error).toBeTruthy();
252252
});
253253

254-
it('measures alternatives', async () => {
255-
const cubeValidator = new CubeValidator(new CubeSymbols());
256-
const cube = {
257-
name: 'name',
258-
sql: () => '',
259-
fileName: 'fileName',
260-
measures: {
261-
number: {
262-
type: 'suma'
254+
describe('Measures', () => {
255+
it('measures alternatives', async () => {
256+
const cubeValidator = new CubeValidator(new CubeSymbols());
257+
const cube = {
258+
name: 'name',
259+
sql: () => '',
260+
fileName: 'fileName',
261+
measures: {
262+
number: {
263+
type: 'suma'
264+
}
263265
}
264-
}
265-
};
266+
};
266267

267-
const validationResult = cubeValidator.validate(cube, {
268-
error: (message: any, _e: any) => {
269-
console.log(message);
270-
expect(message).toContain('must be one of [count, number,');
271-
}
272-
} as any);
268+
const validationResult = cubeValidator.validate(cube, {
269+
error: (message: any, _e: any) => {
270+
console.log(message);
271+
expect(message).toContain('must be one of [count, number,');
272+
}
273+
} as any);
273274

274-
expect(validationResult.error).toBeTruthy();
275+
expect(validationResult.error).toBeTruthy();
276+
});
277+
278+
it('2 timeShifts, 1 without timeDimension', async () => {
279+
const cubeValidator = new CubeValidator(new CubeSymbols());
280+
const cube = {
281+
name: 'name',
282+
sql: () => '',
283+
fileName: 'fileName',
284+
measures: {
285+
measure_with_time_shift: {
286+
multiStage: true,
287+
type: 'sum',
288+
sql: () => '',
289+
timeShift: [
290+
{
291+
timeDimension: () => '',
292+
interval: '1 day',
293+
type: 'prior',
294+
},
295+
{
296+
interval: '1 day',
297+
type: 'prior',
298+
}
299+
]
300+
}
301+
}
302+
};
303+
304+
const validationResult = cubeValidator.validate(cube, {
305+
error: (message: any, _e: any) => {
306+
console.log(message);
307+
expect(message).toContain('(measures.measure_with_time_shift.timeShift[1].timeDimension) is required');
308+
}
309+
} as any);
310+
311+
expect(validationResult.error).toBeTruthy();
312+
});
313+
314+
it('3 timeShifts', async () => {
315+
const cubeValidator = new CubeValidator(new CubeSymbols());
316+
const cube = {
317+
name: 'name',
318+
sql: () => '',
319+
fileName: 'fileName',
320+
measures: {
321+
measure_with_time_shift: {
322+
multiStage: true,
323+
type: 'sum',
324+
sql: () => '',
325+
timeShift: [
326+
{
327+
timeDimension: () => '',
328+
interval: '1 day',
329+
type: 'prior',
330+
},
331+
{
332+
timeDimension: () => '',
333+
interval: '1 year',
334+
type: 'next',
335+
},
336+
{
337+
timeDimension: () => '',
338+
interval: '1 week',
339+
type: 'prior',
340+
}
341+
]
342+
}
343+
}
344+
};
345+
346+
const validationResult = cubeValidator.validate(cube, {
347+
error: (message: any, _e: any) => {
348+
console.log(message);
349+
}
350+
} as any);
351+
352+
expect(validationResult.error).toBeFalsy();
353+
});
354+
355+
it('1 timeShift without timeDimension', async () => {
356+
const cubeValidator = new CubeValidator(new CubeSymbols());
357+
const cube = {
358+
name: 'name',
359+
sql: () => '',
360+
fileName: 'fileName',
361+
measures: {
362+
measure_with_time_shift: {
363+
multiStage: true,
364+
type: 'sum',
365+
sql: () => '',
366+
timeShift: [
367+
{
368+
interval: '1 day',
369+
type: 'prior',
370+
}
371+
]
372+
}
373+
}
374+
};
375+
376+
const validationResult = cubeValidator.validate(cube, {
377+
error: (message: any, _e: any) => {
378+
console.log(message);
379+
}
380+
} as any);
381+
382+
expect(validationResult.error).toBeFalsy();
383+
});
275384
});
276385

277386
it('OriginalSqlSchema', async () => {

0 commit comments

Comments
 (0)