Skip to content

Commit 3739c78

Browse files
committed
Initial support for named time shifts
1 parent b500456 commit 3739c78

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() {
51-
let mut shift = shift.clone();
52-
shift.interval = shift.interval.inverse();
53-
return Either::Left((calendar_pk.full_name(), 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() {
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() {
60+
let mut shift = shift.clone();
61+
shift.interval = Some(dim_shift_interval.inverse());
62+
return Either::Left((calendar_pk.full_name(), 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

@@ -229,11 +230,31 @@ impl DimensionSymbol {
229230
&self,
230231
interval: &SqlInterval,
231232
) -> Option<(String, CalendarDimensionTimeShift)> {
232-
if let Some(ts) = self
233-
.time_shift
234-
.iter()
235-
.find(|shift| shift.interval == *interval)
236-
{
233+
if let Some(ts) = self.time_shift.iter().find(|shift| {
234+
if let Some(s_i) = &shift.interval {
235+
s_i == interval
236+
} else {
237+
false
238+
}
239+
}) {
240+
if let Some(pk) = &self.time_shift_pk {
241+
return Some((pk.full_name(), ts.clone()));
242+
}
243+
}
244+
None
245+
}
246+
247+
pub fn calendar_time_shift_for_named_interval(
248+
&self,
249+
interval_name: &String,
250+
) -> Option<(String, CalendarDimensionTimeShift)> {
251+
if let Some(ts) = self.time_shift.iter().find(|shift| {
252+
if let Some(s_n) = &shift.name {
253+
s_n == interval_name
254+
} else {
255+
false
256+
}
257+
}) {
237258
if let Some(pk) = &self.time_shift_pk {
238259
return Some((pk.full_name(), ts.clone()));
239260
}
@@ -362,18 +383,28 @@ impl SymbolFactory for DimensionSymbolFactory {
362383
time_shift
363384
.iter()
364385
.map(|item| -> Result<_, CubeError> {
365-
let interval = item.static_data().interval.parse::<SqlInterval>()?;
366-
let interval = if item.static_data().timeshift_type == "next" {
367-
-interval
368-
} else {
369-
interval
386+
let interval = match &item.static_data().interval {
387+
Some(raw) => {
388+
let mut iv = raw.parse::<SqlInterval>()?;
389+
if item.static_data().timeshift_type.as_deref() == Some("next") {
390+
iv = -iv;
391+
}
392+
393+
Some(iv)
394+
}
395+
None => None,
370396
};
397+
let name = item.static_data().name.clone();
371398
let sql = if let Some(sql) = item.sql()? {
372399
Some(compiler.compile_sql_call(&cube_name, sql)?)
373400
} else {
374401
None
375402
};
376-
Ok(CalendarDimensionTimeShift { interval, sql })
403+
Ok(CalendarDimensionTimeShift {
404+
interval,
405+
name,
406+
sql,
407+
})
377408
})
378409
.collect::<Result<Vec<_>, _>>()?
379410
} 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)