Skip to content

Commit 2c838e0

Browse files
committed
Initial support for named time shifts
1 parent 9a4d13f commit 2c838e0

File tree

10 files changed

+151
-42
lines changed

10 files changed

+151
-42
lines changed

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
);

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ impl PrettyPrint for MultiStageLeafMeasure {
3030
&format!(
3131
"- {}: {}",
3232
time_shift.dimension.full_name(),
33-
time_shift.interval.to_sql()
33+
if let Some(interval) = &time_shift.interval {
34+
interval.to_sql()
35+
} else if let Some(name) = &time_shift.name {
36+
format!("{} (named)", name.to_string())
37+
} else {
38+
"None".to_string()
39+
}
3440
),
3541
&details_state,
3642
);

rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/builder.rs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,24 @@ impl PhysicalPlanBuilderContext {
4343
.iter()
4444
.partition_map(|(key, shift)| {
4545
if let Ok(dimension) = shift.dimension.as_dimension() {
46-
if let Some((dim_key, cts)) =
47-
dimension.calendar_time_shift_for_interval(&shift.interval)
48-
{
49-
return Either::Right((dim_key.clone(), cts.clone()));
50-
} else if let Some(calendar_pk) = dimension.time_shift_pk_full_name() {
51-
let mut shift = shift.clone();
52-
shift.interval = shift.interval.inverse();
53-
return Either::Left((calendar_pk, shift.clone()));
46+
if let Some(dim_shift_name) = &shift.name {
47+
if let Some((dim_key, cts)) =
48+
dimension.calendar_time_shift_for_named_interval(dim_shift_name)
49+
{
50+
return Either::Right((dim_key.clone(), cts.clone()));
51+
} else if let Some(_calendar_pk) = dimension.time_shift_pk_full_name() {
52+
// TODO: Handle case when named shift is not found
53+
}
54+
} else if let Some(dim_shift_interval) = &shift.interval {
55+
if let Some((dim_key, cts)) =
56+
dimension.calendar_time_shift_for_interval(dim_shift_interval)
57+
{
58+
return Either::Right((dim_key.clone(), cts.clone()));
59+
} else if let Some(calendar_pk) = dimension.time_shift_pk_full_name() {
60+
let mut shift = shift.clone();
61+
shift.interval = Some(dim_shift_interval.inverse());
62+
return Either::Left((calendar_pk, shift.clone()));
63+
}
5464
}
5565
}
5666
Either::Left((key.clone(), shift.clone()))

rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/applied_state.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::plan::{FilterGroup, FilterItem};
22
use crate::planner::filter::FilterOperator;
33
use crate::planner::sql_evaluator::{DimensionTimeShift, MeasureTimeShifts, MemberSymbol};
44
use crate::planner::{BaseDimension, BaseMember, BaseTimeDimension};
5+
use cubenativeutils::CubeError;
56
use itertools::Itertools;
67
use std::cmp::PartialEq;
78
use std::collections::HashMap;
@@ -72,15 +73,25 @@ impl MultiStageAppliedState {
7273
.collect_vec();
7374
}
7475

75-
pub fn add_time_shifts(&mut self, time_shifts: MeasureTimeShifts) {
76+
pub fn add_time_shifts(&mut self, time_shifts: MeasureTimeShifts) -> Result<(), CubeError> {
7677
let resolved_shifts = match time_shifts {
7778
MeasureTimeShifts::Dimensions(dimensions) => dimensions,
7879
MeasureTimeShifts::Common(interval) => self
7980
.all_time_members()
8081
.into_iter()
8182
.map(|m| DimensionTimeShift {
82-
interval: interval.clone(),
83+
interval: Some(interval.clone()),
8384
dimension: m,
85+
name: None,
86+
})
87+
.collect_vec(),
88+
MeasureTimeShifts::Named(named_shift) => self
89+
.all_time_members()
90+
.into_iter()
91+
.map(|m| DimensionTimeShift {
92+
interval: None,
93+
dimension: m,
94+
name: Some(named_shift.clone()),
8495
})
8596
.collect_vec(),
8697
};
@@ -90,13 +101,41 @@ impl MultiStageAppliedState {
90101
.dimensions_shifts
91102
.get_mut(&ts.dimension.full_name())
92103
{
93-
exists.interval += ts.interval;
104+
if let Some(interval) = exists.interval.clone() {
105+
if let Some(new_interval) = ts.interval {
106+
exists.interval = Some(interval + new_interval);
107+
} else {
108+
return Err(CubeError::internal(format!(
109+
"Cannot use both named ({}) and interval ({}) shifts for the same dimension: {}.",
110+
ts.name.clone().unwrap_or("-".to_string()),
111+
interval.to_sql(),
112+
ts.dimension.full_name(),
113+
)));
114+
}
115+
} else if let Some(named_shift) = exists.name.clone() {
116+
return if let Some(new_interval) = ts.interval {
117+
Err(CubeError::internal(format!(
118+
"Cannot use both named ({}) and interval ({}) shifts for the same dimension: {}.",
119+
named_shift,
120+
new_interval.to_sql(),
121+
ts.dimension.full_name(),
122+
)))
123+
} else {
124+
Err(CubeError::internal(format!(
125+
"Cannot use more than one named shifts ({}, {}) for the same dimension: {}.",
126+
ts.name.clone().unwrap_or("-".to_string()),
127+
named_shift,
128+
ts.dimension.full_name(),
129+
)))
130+
};
131+
}
94132
} else {
95133
self.time_shifts
96134
.dimensions_shifts
97135
.insert(ts.dimension.full_name(), ts);
98136
}
99137
}
138+
Ok(())
100139
}
101140

102141
pub fn time_shifts(&self) -> &TimeShiftState {

rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ impl MultiStageQueryPlanner {
225225
new_state.add_dimensions(dimensions_to_add);
226226
}
227227
if let Some(time_shift) = multi_stage_member.time_shift() {
228-
new_state.add_time_shifts(time_shift.clone());
228+
new_state.add_time_shifts(time_shift.clone())?;
229229
}
230230
if state.has_filters_for_member(&member_name) {
231231
new_state.remove_filter_for_member(&member_name);

rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/time_shift.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ impl SqlNode for TimeShiftSqlNode {
4343
MemberSymbol::Dimension(ev) => {
4444
if !ev.is_reference() && ev.dimension_type() == "time" {
4545
if let Some(shift) = self.shifts.dimensions_shifts.get(&ev.full_name()) {
46-
let shift = shift.interval.to_sql();
46+
let shift = shift.interval.clone().unwrap().to_sql(); // Common time shifts should always have an interval
4747
let res = templates.add_timestamp_interval(input, shift)?;
4848
format!("({})", res)
4949
} else {

rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ pub struct DimensionCaseDefinition {
2727

2828
#[derive(Clone)]
2929
pub struct CalendarDimensionTimeShift {
30-
pub interval: SqlInterval,
30+
pub interval: Option<SqlInterval>,
31+
pub name: Option<String>,
3132
pub sql: Option<Rc<SqlCall>>,
3233
}
3334

@@ -234,11 +235,31 @@ impl DimensionSymbol {
234235
&self,
235236
interval: &SqlInterval,
236237
) -> Option<(String, CalendarDimensionTimeShift)> {
237-
if let Some(ts) = self
238-
.time_shift
239-
.iter()
240-
.find(|shift| shift.interval == *interval)
241-
{
238+
if let Some(ts) = self.time_shift.iter().find(|shift| {
239+
if let Some(s_i) = &shift.interval {
240+
s_i == interval
241+
} else {
242+
false
243+
}
244+
}) {
245+
if let Some(pk) = &self.time_shift_pk {
246+
return Some((pk.full_name(), ts.clone()));
247+
}
248+
}
249+
None
250+
}
251+
252+
pub fn calendar_time_shift_for_named_interval(
253+
&self,
254+
interval_name: &String,
255+
) -> Option<(String, CalendarDimensionTimeShift)> {
256+
if let Some(ts) = self.time_shift.iter().find(|shift| {
257+
if let Some(s_n) = &shift.name {
258+
s_n == interval_name
259+
} else {
260+
false
261+
}
262+
}) {
242263
if let Some(pk) = &self.time_shift_pk_full_name {
243264
return Some((pk.clone(), ts.clone()));
244265
} else if self.is_self_time_shift_pk {
@@ -369,18 +390,28 @@ impl SymbolFactory for DimensionSymbolFactory {
369390
time_shift
370391
.iter()
371392
.map(|item| -> Result<_, CubeError> {
372-
let interval = item.static_data().interval.parse::<SqlInterval>()?;
373-
let interval = if item.static_data().timeshift_type == "next" {
374-
-interval
375-
} else {
376-
interval
393+
let interval = match &item.static_data().interval {
394+
Some(raw) => {
395+
let mut iv = raw.parse::<SqlInterval>()?;
396+
if item.static_data().timeshift_type.as_deref() == Some("next") {
397+
iv = -iv;
398+
}
399+
400+
Some(iv)
401+
}
402+
None => None,
377403
};
404+
let name = item.static_data().name.clone();
378405
let sql = if let Some(sql) = item.sql()? {
379406
Some(compiler.compile_sql_call(&cube_name, sql)?)
380407
} else {
381408
None
382409
};
383-
Ok(CalendarDimensionTimeShift { interval, sql })
410+
Ok(CalendarDimensionTimeShift {
411+
interval,
412+
name,
413+
sql,
414+
})
384415
})
385416
.collect::<Result<Vec<_>, _>>()?
386417
} else {

rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,16 @@ impl MeasureOrderBy {
3838

3939
#[derive(Clone, Debug)]
4040
pub struct DimensionTimeShift {
41-
pub interval: SqlInterval,
41+
pub interval: Option<SqlInterval>,
42+
pub name: Option<String>,
4243
pub dimension: Rc<MemberSymbol>,
4344
}
4445

4546
impl PartialEq for DimensionTimeShift {
4647
fn eq(&self, other: &Self) -> bool {
47-
self.interval == other.interval && self.dimension.full_name() == other.dimension.full_name()
48+
self.interval == other.interval
49+
&& self.dimension.full_name() == other.dimension.full_name()
50+
&& self.name == other.name
4851
}
4952
}
5053

@@ -54,6 +57,7 @@ impl Eq for DimensionTimeShift {}
5457
pub enum MeasureTimeShifts {
5558
Dimensions(Vec<DimensionTimeShift>),
5659
Common(SqlInterval),
60+
Named(String),
5761
}
5862

5963
#[derive(Clone)]
@@ -567,19 +571,29 @@ impl SymbolFactory for MeasureSymbolFactory {
567571
let mut shifts: HashMap<String, DimensionTimeShift> = HashMap::new();
568572
let mut common_shift = None;
569573
for shift_ref in time_shift_references.iter() {
570-
let interval = shift_ref.interval.parse::<SqlInterval>()?;
571-
let interval =
572-
if shift_ref.shift_type.as_ref().unwrap_or(&format!("prior")) == "next" {
573-
-interval
574-
} else {
575-
interval
576-
};
574+
let interval = match &shift_ref.interval {
575+
Some(raw) => {
576+
let mut iv = raw.parse::<SqlInterval>()?;
577+
if shift_ref
578+
.shift_type
579+
.as_deref()
580+
.unwrap_or("prior")
581+
== "next"
582+
{
583+
iv = -iv;
584+
}
585+
586+
Some(iv)
587+
}
588+
None => None,
589+
};
590+
let name = shift_ref.name.clone();
577591
if let Some(time_dimension) = &shift_ref.time_dimension {
578592
let dimension = compiler.add_dimension_evaluator(time_dimension.clone())?;
579593
let dimension = find_owned_by_cube_child(&dimension)?;
580594
let dimension_name = dimension.full_name();
581595
if let Some(exists) = shifts.get(&dimension_name) {
582-
if exists.interval != interval {
596+
if exists.interval != interval || exists.name != name {
583597
return Err(CubeError::user(format!(
584598
"Different time shifts for one dimension {} not allowed",
585599
dimension_name
@@ -590,15 +604,16 @@ impl SymbolFactory for MeasureSymbolFactory {
590604
dimension_name,
591605
DimensionTimeShift {
592606
interval: interval.clone(),
607+
name: name.clone(),
593608
dimension: dimension.clone(),
594609
},
595610
);
596611
};
597612
} else {
598613
if common_shift.is_none() {
599-
common_shift = Some(interval);
614+
common_shift = interval;
600615
} else {
601-
if common_shift != Some(interval) {
616+
if common_shift != interval {
602617
return Err(CubeError::user(format!(
603618
"Measure can contain only one common time_shift (without time_dimension).",
604619
)));

0 commit comments

Comments
 (0)