Skip to content

Commit e033aaf

Browse files
authored
feat(tesseract): Introduce named multistage time shifts for use with calendars (#9773)
* extend cube validator to support named timeshifts * Initial support for named time shifts * Resolve conficts * fix Evaluator to support named time shifts * update measure symbol to support named time shifts * cargo clippy * update snapshots * add tests for named time shifts
1 parent 3f16a39 commit e033aaf

File tree

17 files changed

+297
-67
lines changed

17 files changed

+297
-67
lines changed

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

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

3030
export type TimeShiftDefinition = {
31-
timeDimension?: (...args: Array<unknown>) => ToString,
32-
interval: string,
33-
type: 'next' | 'prior',
31+
timeDimension?: (...args: Array<unknown>) => ToString;
32+
name?: string;
33+
interval?: string;
34+
type?: 'next' | 'prior';
3435
};
3536

3637
export type TimeShiftDefinitionReference = {
37-
timeDimension?: string,
38-
interval: string,
39-
type: 'next' | 'prior',
38+
timeDimension?: string;
39+
name?: string;
40+
interval?: string;
41+
type?: 'next' | 'prior';
4042
};
4143

4244
export type MeasureDefinition = {
@@ -393,6 +395,7 @@ export class CubeEvaluator extends CubeSymbols {
393395
}
394396
if (member.timeShift) {
395397
member.timeShiftReferences = member.timeShift.map((s): TimeShiftDefinitionReference => ({
398+
name: s.name,
396399
interval: s.interval,
397400
type: s.type,
398401
...(typeof s.timeDimension === 'function'

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -576,9 +576,12 @@ const timeShiftItemRequired = Joi.object({
576576

577577
const timeShiftItemOptional = Joi.object({
578578
timeDimension: Joi.func(), // not required
579-
interval: regexTimeInterval.required(),
580-
type: Joi.string().valid('next', 'prior').required(),
581-
});
579+
interval: regexTimeInterval,
580+
name: identifier,
581+
type: Joi.string().valid('next', 'prior'),
582+
})
583+
.xor('name', 'interval')
584+
.and('interval', 'type');
582585

583586
const MeasuresSchema = Joi.object().pattern(identifierRegex, Joi.alternatives().conditional(Joi.ref('.multiStage'), [
584587
{
@@ -623,6 +626,16 @@ const MeasuresSchema = Joi.object().pattern(identifierRegex, Joi.alternatives().
623626
]
624627
));
625628

629+
const CalendarTimeShiftItem = Joi.object({
630+
name: identifier,
631+
interval: regexTimeInterval,
632+
type: Joi.string().valid('next', 'prior'),
633+
sql: Joi.func().required(),
634+
})
635+
.or('name', 'interval')
636+
.with('interval', 'type')
637+
.with('type', 'interval');
638+
626639
const DimensionsSchema = Joi.object().pattern(identifierRegex, Joi.alternatives().try(
627640
inherit(BaseDimensionWithoutSubQuery, {
628641
case: Joi.object().keys({
@@ -667,11 +680,7 @@ const DimensionsSchema = Joi.object().pattern(identifierRegex, Joi.alternatives(
667680
inherit(BaseDimensionWithoutSubQuery, {
668681
type: Joi.any().valid('time').required(),
669682
sql: Joi.func().required(),
670-
timeShift: Joi.array().items(Joi.object({
671-
interval: regexTimeInterval.required(),
672-
type: Joi.string().valid('next', 'prior').required(),
673-
sql: Joi.func().required(),
674-
})),
683+
timeShift: Joi.array().items(CalendarTimeShiftItem),
675684
})
676685
));
677686

packages/cubejs-schema-compiler/test/integration/postgres/calendars.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,20 @@ cubes:
7373
- interval: 1 year
7474
type: prior
7575
76+
- name: count_shifted_y_named
77+
type: number
78+
multi_stage: true
79+
sql: "{count}"
80+
time_shift:
81+
- name: one_year
82+
83+
- name: count_shifted_y1d_named
84+
type: number
85+
multi_stage: true
86+
sql: "{count}"
87+
time_shift:
88+
- name: one_year_and_one_day
89+
7690
- name: count_shifted_calendar_m
7791
type: number
7892
multi_stage: true
@@ -244,6 +258,12 @@ cubes:
244258
type: prior
245259
sql: "{CUBE}.retail_date_prev_year"
246260
261+
- name: one_year
262+
sql: "{CUBE}.retail_date_prev_year"
263+
264+
- name: one_year_and_one_day
265+
sql: "({CUBE}.retail_date_prev_year + interval '1 day')"
266+
247267
##### Retail Dates ####
248268
- name: retail_date
249269
sql: retail_date
@@ -282,6 +302,12 @@ cubes:
282302
type: prior
283303
sql: "{CUBE}.retail_date_prev_year"
284304
305+
- name: one_year
306+
sql: "{CUBE}.retail_date_prev_year"
307+
308+
- name: one_year_and_one_day
309+
sql: "({CUBE}.retail_date_prev_year + interval '1 day')"
310+
285311
- name: retail_year
286312
sql: "{CUBE}.retail_year_name"
287313
type: string
@@ -576,6 +602,22 @@ cubes:
576602
},
577603
]));
578604

605+
it('Count shifted by retail year (custom named shift + custom granularity)', async () => runQueryTest({
606+
measures: ['calendar_orders.count', 'calendar_orders.count_shifted_y_named'],
607+
timeDimensions: [{
608+
dimension: 'custom_calendar.retail_date',
609+
granularity: 'year',
610+
dateRange: ['2025-02-02', '2026-02-01']
611+
}],
612+
order: [{ id: 'custom_calendar.retail_date' }]
613+
}, [
614+
{
615+
calendar_orders__count: '37',
616+
calendar_orders__count_shifted_y_named: '39',
617+
custom_calendar__retail_date_year: '2025-02-02T00:00:00.000Z',
618+
},
619+
]));
620+
579621
it('Count shifted by retail month (custom shift + common granularity)', async () => runQueryTest({
580622
measures: ['calendar_orders.count', 'calendar_orders.count_shifted_calendar_m'],
581623
timeDimensions: [{
@@ -677,6 +719,23 @@ cubes:
677719
custom_calendar__retail_date_week: '2025-04-06T00:00:00.000Z',
678720
},
679721
]));
722+
723+
it('Count shifted by retail year and another custom calendar year (2 custom named shifts + custom granularity)', async () => runQueryTest({
724+
measures: ['calendar_orders.count', 'calendar_orders.count_shifted_y_named', 'calendar_orders.count_shifted_y1d_named'],
725+
timeDimensions: [{
726+
dimension: 'custom_calendar.retail_date',
727+
granularity: 'year',
728+
dateRange: ['2025-02-02', '2026-02-01']
729+
}],
730+
order: [{ id: 'custom_calendar.retail_date' }]
731+
}, [
732+
{
733+
calendar_orders__count: '37',
734+
calendar_orders__count_shifted_y_named: '39',
735+
calendar_orders__count_shifted_y1d_named: '39',
736+
custom_calendar__retail_date_year: '2025-02-02T00:00:00.000Z',
737+
},
738+
]));
680739
});
681740

682741
describe('PK dimension time-shifts', () => {
@@ -696,6 +755,22 @@ cubes:
696755
},
697756
]));
698757

758+
it.skip('Count shifted by retail year (custom named shift + custom granularity)1', async () => runQueryTest({
759+
measures: ['calendar_orders.count', 'calendar_orders.count_shifted_y_named'],
760+
timeDimensions: [{
761+
dimension: 'custom_calendar.date_val',
762+
granularity: 'year',
763+
dateRange: ['2025-02-02', '2026-02-01']
764+
}],
765+
order: [{ id: 'custom_calendar.date_val' }]
766+
}, [
767+
{
768+
calendar_orders__count: '37',
769+
calendar_orders__count_shifted_y_named: '39',
770+
custom_calendar__date_val_year: '2025-02-02T00:00:00.000Z',
771+
},
772+
]));
773+
699774
it.skip('Count shifted by retail month (custom shift + common granularity)', async () => runQueryTest({
700775
measures: ['calendar_orders.count', 'calendar_orders.count_shifted_calendar_m'],
701776
timeDimensions: [{
@@ -797,6 +872,23 @@ cubes:
797872
custom_calendar__date_val_week: '2025-04-06T00:00:00.000Z',
798873
},
799874
]));
875+
876+
it.skip('Count shifted by retail year and another custom calendar year (2 custom named shifts + custom granularity)', async () => runQueryTest({
877+
measures: ['calendar_orders.count', 'calendar_orders.count_shifted_y_named', 'calendar_orders.count_shifted_y1d_named'],
878+
timeDimensions: [{
879+
dimension: 'custom_calendar.date_val',
880+
granularity: 'year',
881+
dateRange: ['2025-02-02', '2026-02-01']
882+
}],
883+
order: [{ id: 'custom_calendar.date_val' }]
884+
}, [
885+
{
886+
calendar_orders__count: '37',
887+
calendar_orders__count_shifted_y_named: '39',
888+
calendar_orders__count_shifted_y1d_named: '39',
889+
custom_calendar__date_val_year: '2025-02-02T00:00:00.000Z',
890+
},
891+
]));
800892
});
801893
});
802894
});

packages/cubejs-schema-compiler/test/unit/__snapshots__/schema.test.ts.snap

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,8 @@ Object {
8787
"type": "prior",
8888
},
8989
Object {
90-
"interval": "1 year",
90+
"name": "retail_date_prev_year",
9191
"sql": [Function],
92-
"type": "prior",
9392
},
9493
Object {
9594
"interval": "2 year",
@@ -273,9 +272,8 @@ Object {
273272
"type": "prior",
274273
},
275274
Object {
276-
"interval": "1 year",
275+
"name": "retail_date_prev_year",
277276
"sql": [Function],
278-
"type": "prior",
279277
},
280278
Object {
281279
"interval": "2 year",
@@ -1894,6 +1892,7 @@ Object {
18941892
"timeShiftReferences": Array [
18951893
Object {
18961894
"interval": "1 year",
1895+
"name": undefined,
18971896
"timeDimension": "createdAt",
18981897
"type": "prior",
18991898
},

packages/cubejs-schema-compiler/test/unit/fixtures/calendar_orders.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ cubes:
4646
interval: 1 year
4747
type: prior
4848

49+
- name: count_shifted_named
50+
type: count
51+
multi_stage: true
52+
sql: "{count}"
53+
time_shift:
54+
- name: retail_date_prev_year
55+
4956
- name: completed_count
5057
type: count
5158
filters:

packages/cubejs-schema-compiler/test/unit/fixtures/custom_calendar.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ cube(`custom_calendar_js`, {
4141
sql: `{CUBE.retail_date_prev_month}`,
4242
},
4343
{
44-
interval: '1 year',
45-
type: 'prior',
44+
name: 'retail_date_prev_year',
4645
sql: `{CUBE.retail_date_prev_year}`,
4746
},
4847
{

packages/cubejs-schema-compiler/test/unit/fixtures/custom_calendar.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ cubes:
4545
type: prior
4646
sql: "{CUBE.retail_date_prev_month}"
4747

48-
- interval: 1 year
49-
type: prior
48+
- name: retail_date_prev_year
5049
sql: "{CUBE.retail_date_prev_year}"
5150

5251
# All needed intervals should be defined explicitly

rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/measure_definition.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ use std::rc::Rc;
1515

1616
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
1717
pub struct TimeShiftReference {
18-
pub interval: String,
18+
pub interval: Option<String>,
19+
pub name: Option<String>,
1920
#[serde(rename = "type")]
2021
pub shift_type: Option<String>,
2122
#[serde(rename = "timeDimension")]

rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/timeshift_definition.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ use std::rc::Rc;
1111

1212
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, Hash)]
1313
pub struct TimeShiftDefinitionStatic {
14-
pub interval: String,
14+
pub interval: Option<String>,
1515
#[serde(rename = "type")]
16-
pub timeshift_type: String,
16+
pub timeshift_type: Option<String>,
17+
pub name: Option<String>,
1718
}
1819

1920
#[nativebridge::native_bridge(TimeShiftDefinitionStatic)]

rust/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/common.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,13 @@ impl PrettyPrint for MultiStageAppliedState {
5858
&format!(
5959
"- {}: {}",
6060
time_shift.dimension.full_name(),
61-
time_shift.interval.to_sql()
61+
if let Some(interval) = &time_shift.interval {
62+
interval.to_sql()
63+
} else if let Some(name) = &time_shift.name {
64+
format!("{} (named)", name.to_string())
65+
} else {
66+
"None".to_string()
67+
}
6268
),
6369
&details_state,
6470
);

0 commit comments

Comments
 (0)