diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/member-expression.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/member-expression.test.ts index 89249f52aeb0f..df16364c3e15d 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/member-expression.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/member-expression.test.ts @@ -203,7 +203,6 @@ cubes: views: - name: customers_view - cubes: - join_path: customers includes: @@ -384,4 +383,26 @@ views: // Skipping because it works only in Tesseract }); } + + if (getEnv('nativeSqlPlanner')) { + it('multi stage duplicated time shift over view and origin cube', async () => runQueryTest({ + measures: [ + 'orders_view.cagr_1_y' + ], + timeDimensions: [ + { + dimension: 'orders_view.date', + dateRange: ['2022-01-01', '2022-10-31'], + }, + ], + + timezone: 'America/Los_Angeles' + }, + + [{ orders_view__cagr_1_y: '-0.90000000000000000000' }])); + } else { + it.skip('member expression multi stage with time dimension segment', () => { + // Skipping because it works only in Tesseract + }); + } }); diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/base_measure.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/base_measure.rs index d903e5ab49e83..00591bce13405 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/base_measure.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/base_measure.rs @@ -1,68 +1,14 @@ use super::query_tools::QueryTools; -use super::sql_evaluator::{MemberExpressionSymbol, MemberSymbol, SqlCall}; +use super::sql_evaluator::{MeasureTimeShift, MemberExpressionSymbol, MemberSymbol, SqlCall}; use super::{evaluate_with_context, BaseMember, BaseMemberHelper, VisitorContext}; use crate::cube_bridge::measure_definition::{ MeasureDefinition, RollingWindow, TimeShiftReference, }; use crate::planner::sql_templates::PlanSqlTemplates; use cubenativeutils::CubeError; -use lazy_static::lazy_static; -use regex::Regex; use std::fmt::{Debug, Formatter}; use std::rc::Rc; -#[derive(Clone, Debug)] -pub struct MeasureTimeShift { - pub interval: String, - pub time_dimension: String, -} - -lazy_static! { - static ref INTERVAL_MATCH_RE: Regex = - Regex::new(r"^(-?\d+) (second|minute|hour|day|week|month|quarter|year)s?$").unwrap(); -} -impl MeasureTimeShift { - pub fn try_from_reference(reference: &TimeShiftReference) -> Result { - let parsed_interval = - if let Some(captures) = INTERVAL_MATCH_RE.captures(&reference.interval) { - let duration = if let Some(duration) = captures.get(1) { - duration.as_str().parse::().ok() - } else { - None - }; - let granularity = if let Some(granularity) = captures.get(2) { - Some(granularity.as_str().to_owned()) - } else { - None - }; - if let Some((duration, granularity)) = duration.zip(granularity) { - Some((duration, granularity)) - } else { - None - } - } else { - None - }; - if let Some((duration, granularity)) = parsed_interval { - let duration = if reference.shift_type.as_ref().unwrap_or(&format!("prior")) == "next" { - duration * (-1) - } else { - duration - }; - - Ok(Self { - interval: format!("{duration} {granularity}"), - time_dimension: reference.time_dimension.clone(), - }) - } else { - Err(CubeError::user(format!( - "Invalid interval: {}", - reference.interval - ))) - } - } -} - pub struct BaseMeasure { measure: String, query_tools: Rc, @@ -70,7 +16,6 @@ pub struct BaseMeasure { definition: Option>, #[allow(dead_code)] member_expression_definition: Option, - time_shifts: Vec, cube_name: String, name: String, default_alias: String, @@ -80,7 +25,6 @@ impl Debug for BaseMeasure { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("BaseMeasure") .field("measure", &self.measure) - .field("time_shifts", &self.time_shifts) .field("default_alias", &self.default_alias) .finish() } @@ -132,7 +76,6 @@ impl BaseMeasure { ) -> Result>, CubeError> { let res = match evaluation_node.as_ref() { MemberSymbol::Measure(s) => { - let time_shifts = Self::parse_time_shifts(&s.definition())?; let default_alias = BaseMemberHelper::default_alias( &s.cube_name(), &s.name(), @@ -147,7 +90,6 @@ impl BaseMeasure { member_expression_definition: None, cube_name: s.cube_name().clone(), name: s.name().clone(), - time_shifts, default_alias, })) } @@ -166,7 +108,6 @@ impl BaseMeasure { name, member_expression_definition, default_alias, - time_shifts: vec![], })) } _ => None, @@ -212,7 +153,6 @@ impl BaseMeasure { name, member_expression_definition, default_alias, - time_shifts: vec![], })) } @@ -232,19 +172,6 @@ impl BaseMeasure { Ok(res) } - fn parse_time_shifts( - definition: &Rc, - ) -> Result, CubeError> { - if let Some(time_shifts) = &definition.static_data().time_shift_references { - time_shifts - .iter() - .map(|t| MeasureTimeShift::try_from_reference(t)) - .collect::, _>>() - } else { - Ok(vec![]) - } - } - pub fn member_evaluator(&self) -> &Rc { &self.member_evaluator } @@ -289,8 +216,11 @@ impl BaseMeasure { .map_or(None, |d| d.static_data().time_shift_references.clone()) } - pub fn time_shifts(&self) -> &Vec { - &self.time_shifts + pub fn time_shifts(&self) -> Vec { + match self.member_evaluator.as_ref() { + MemberSymbol::Measure(measure_symbol) => measure_symbol.time_shifts().clone(), + _ => vec![], + } } pub fn is_multi_stage(&self) -> bool { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/applied_state.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/applied_state.rs index e39347de43f89..80fe8bfdc1d2d 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/applied_state.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/applied_state.rs @@ -1,6 +1,6 @@ use crate::plan::{FilterGroup, FilterItem}; use crate::planner::filter::FilterOperator; -use crate::planner::planners::multi_stage::MultiStageTimeShift; +use crate::planner::sql_evaluator::MeasureTimeShift; use crate::planner::{BaseDimension, BaseMember, BaseTimeDimension}; use cubenativeutils::CubeError; use itertools::Itertools; @@ -17,7 +17,7 @@ pub struct MultiStageAppliedState { dimensions_filters: Vec, measures_filters: Vec, segments: Vec, - time_shifts: HashMap, + time_shifts: HashMap, } impl MultiStageAppliedState { @@ -62,14 +62,17 @@ impl MultiStageAppliedState { .collect_vec(); } - pub fn add_time_shifts(&mut self, time_shifts: Vec) { + pub fn add_time_shifts(&mut self, time_shifts: Vec) { for ts in time_shifts.into_iter() { - self.time_shifts - .insert(ts.time_dimension.clone(), ts.interval.clone()); + if let Some(exists) = self.time_shifts.get_mut(&ts.dimension.full_name()) { + exists.interval += ts.interval; + } else { + self.time_shifts.insert(ts.dimension.full_name(), ts); + } } } - pub fn time_shifts(&self) -> &HashMap { + pub fn time_shifts(&self) -> &HashMap { &self.time_shifts } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member.rs index 2731bdde32618..ec882404c7275 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member.rs @@ -1,63 +1,7 @@ -use crate::cube_bridge::measure_definition::TimeShiftReference; -use crate::planner::sql_evaluator::MemberSymbol; +use crate::planner::sql_evaluator::{MeasureTimeShift, MemberSymbol}; use crate::planner::BaseTimeDimension; -use cubenativeutils::CubeError; -use lazy_static::lazy_static; -use regex::Regex; use std::rc::Rc; -#[derive(Clone, Debug)] -pub struct MultiStageTimeShift { - pub interval: String, - pub time_dimension: String, -} - -lazy_static! { - static ref INTERVAL_MATCH_RE: Regex = - Regex::new(r"^(-?\d+) (second|minute|hour|day|week|month|quarter|year)s?$").unwrap(); -} -impl MultiStageTimeShift { - pub fn try_from_reference(reference: &TimeShiftReference) -> Result { - let parsed_interval = - if let Some(captures) = INTERVAL_MATCH_RE.captures(&reference.interval) { - let duration = if let Some(duration) = captures.get(1) { - duration.as_str().parse::().ok() - } else { - None - }; - let granularity = if let Some(granularity) = captures.get(2) { - Some(granularity.as_str().to_owned()) - } else { - None - }; - if let Some((duration, granularity)) = duration.zip(granularity) { - Some((duration, granularity)) - } else { - None - } - } else { - None - }; - if let Some((duration, granularity)) = parsed_interval { - let duration = if reference.shift_type.as_ref().unwrap_or(&format!("prior")) == "next" { - duration * (-1) - } else { - duration - }; - - Ok(Self { - interval: format!("{duration} {granularity}"), - time_dimension: reference.time_dimension.clone(), - }) - } else { - Err(CubeError::user(format!( - "Invalid interval: {}", - reference.interval - ))) - } - } -} - #[derive(Clone)] pub struct TimeSeriesDescription { pub time_dimension: Rc, @@ -143,7 +87,7 @@ pub struct MultiStageInodeMember { reduce_by: Vec, add_group_by: Vec, group_by: Option>, - time_shifts: Vec, + time_shifts: Vec, } impl MultiStageInodeMember { @@ -152,7 +96,7 @@ impl MultiStageInodeMember { reduce_by: Vec, add_group_by: Vec, group_by: Option>, - time_shifts: Vec, + time_shifts: Vec, ) -> Self { Self { inode_type, @@ -179,7 +123,7 @@ impl MultiStageInodeMember { &self.group_by } - pub fn time_shifts(&self) -> &Vec { + pub fn time_shifts(&self) -> &Vec { &self.time_shifts } } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs index 4e0f0fb48ed45..ccc6a6ad36669 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs @@ -1,7 +1,7 @@ use super::{ MultiStageAppliedState, MultiStageInodeMember, MultiStageInodeMemberType, MultiStageLeafMemberType, MultiStageMember, MultiStageMemberQueryPlanner, MultiStageMemberType, - MultiStageQueryDescription, MultiStageTimeShift, RollingWindowPlanner, + MultiStageQueryDescription, RollingWindowPlanner, }; use crate::plan::{Cte, From, Schema, Select, SelectBuilder}; use crate::planner::query_tools::QueryTools; @@ -124,15 +124,8 @@ impl MultiStageQueryPlanner { MultiStageInodeMemberType::Calculate }; - let time_shifts = if let Some(refs) = measure.time_shift_references() { - let time_shifts = refs - .iter() - .map(|r| MultiStageTimeShift::try_from_reference(r)) - .collect::, _>>()?; - time_shifts - } else { - vec![] - }; + let time_shifts = measure.time_shifts(); + let is_ungrupped = match &member_type { MultiStageInodeMemberType::Rank | MultiStageInodeMemberType::Calculate => true, _ => self.query_properties.ungrouped(), diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/find_owned_by_cube.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/find_owned_by_cube.rs new file mode 100644 index 0000000000000..87e81c85a56cd --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/find_owned_by_cube.rs @@ -0,0 +1,66 @@ +use crate::planner::sql_evaluator::MemberSymbol; +use cubenativeutils::CubeError; +use std::rc::Rc; + +pub struct FindOwnedByCubeChildCollector {} + +impl FindOwnedByCubeChildCollector { + pub fn new() -> Self { + Self {} + } + + pub fn find(&self, node: &Rc) -> Result, CubeError> { + self.find_impl(node, &node.full_name()) + } + + fn find_impl( + &self, + node: &Rc, + origin_node_name: &String, + ) -> Result, CubeError> { + match node.as_ref() { + MemberSymbol::Dimension(dimension_symbol) => { + if dimension_symbol.owned_by_cube() { + Ok(node.clone()) + } else { + self.process_deps(&node, origin_node_name) + } + } + MemberSymbol::TimeDimension(time_dimension_symbol) => { + self.find_impl(time_dimension_symbol.base_symbol(), origin_node_name) + } + MemberSymbol::Measure(measure_symbol) => { + if measure_symbol.owned_by_cube() { + Ok(node.clone()) + } else { + self.process_deps(&node, origin_node_name) + } + } + _ => Err(CubeError::internal(format!( + "FindOwnedByCubeChild cannot be processed on node {}", + node.full_name() + ))), + } + } + + fn process_deps( + &self, + node: &Rc, + origin_node_name: &String, + ) -> Result, CubeError> { + let deps = node.get_dependencies(); + if deps.len() == 1 { + self.find_impl(&deps[0], origin_node_name) + } else { + Err(CubeError::internal(format!( + "Cannot find owned by cube child for {}", + origin_node_name + ))) + } + } +} + +pub fn find_owned_by_cube_child(node: &Rc) -> Result, CubeError> { + let visitor = FindOwnedByCubeChildCollector::new(); + visitor.find(node) +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/mod.rs index 3315b5dfa2b79..a5fe4c0531624 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/mod.rs @@ -1,4 +1,5 @@ mod cube_names_collector; +mod find_owned_by_cube; mod has_cumulative_members; mod has_multi_stage_members; mod join_hints_collector; @@ -7,6 +8,7 @@ mod multiplied_measures_collector; mod sub_query_dimensions; pub use cube_names_collector::collect_cube_names; +pub use find_owned_by_cube::*; pub use has_cumulative_members::{has_cumulative_members, HasCumulativeMembersCollector}; pub use has_multi_stage_members::{has_multi_stage_members, HasMultiStageMembersCollector}; pub use join_hints_collector::{ diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/mod.rs index 8aeaaafa89cd2..0a9e167c0746c 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/mod.rs @@ -16,7 +16,7 @@ pub use sql_visitor::SqlEvaluatorVisitor; pub use symbols::{ CubeNameSymbol, CubeNameSymbolFactory, CubeTableSymbol, CubeTableSymbolFactory, DimensionCaseDefinition, DimensionCaseWhenItem, DimensionSymbol, DimensionSymbolFactory, - DimenstionCaseLabel, MeasureSymbol, MeasureSymbolFactory, MemberExpressionSymbol, MemberSymbol, - SymbolFactory, TimeDimensionSymbol, + DimenstionCaseLabel, MeasureSymbol, MeasureSymbolFactory, MeasureTimeShift, + MemberExpressionSymbol, MemberSymbol, SymbolFactory, TimeDimensionSymbol, }; pub use visitor::TraversalVisitor; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/factory.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/factory.rs index 3ffcc61e0ed5d..ecb6062a15d4c 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/factory.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/factory.rs @@ -5,12 +5,13 @@ use super::{ TimeShiftSqlNode, UngroupedMeasureSqlNode, UngroupedQueryFinalMeasureSqlNode, }; use crate::plan::schema::QualifiedColumnName; +use crate::planner::sql_evaluator::MeasureTimeShift; use std::collections::{HashMap, HashSet}; use std::rc::Rc; #[derive(Clone)] pub struct SqlNodesFactory { - time_shifts: HashMap, + time_shifts: HashMap, ungrouped: bool, ungrouped_measure: bool, count_approx_as_state: bool, @@ -42,7 +43,7 @@ impl SqlNodesFactory { } } - pub fn set_time_shifts(&mut self, time_shifts: HashMap) { + pub fn set_time_shifts(&mut self, time_shifts: HashMap) { self.time_shifts = time_shifts; } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/time_shift.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/time_shift.rs index e0777823fe5be..5a29cff9b43b3 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/time_shift.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/time_shift.rs @@ -1,7 +1,7 @@ use super::SqlNode; use crate::planner::query_tools::QueryTools; -use crate::planner::sql_evaluator::MemberSymbol; use crate::planner::sql_evaluator::SqlEvaluatorVisitor; +use crate::planner::sql_evaluator::{MeasureTimeShift, MemberSymbol}; use crate::planner::sql_templates::PlanSqlTemplates; use cubenativeutils::CubeError; use std::any::Any; @@ -9,16 +9,16 @@ use std::collections::HashMap; use std::rc::Rc; pub struct TimeShiftSqlNode { - shifts: HashMap, + shifts: HashMap, input: Rc, } impl TimeShiftSqlNode { - pub fn new(shifts: HashMap, input: Rc) -> Rc { + pub fn new(shifts: HashMap, input: Rc) -> Rc { Rc::new(Self { shifts, input }) } - pub fn shifts(&self) -> &HashMap { + pub fn shifts(&self) -> &HashMap { &self.shifts } @@ -46,6 +46,7 @@ impl SqlNode for TimeShiftSqlNode { let res = match node.as_ref() { MemberSymbol::Dimension(ev) => { if let Some(shift) = self.shifts.get(&ev.full_name()) { + let shift = shift.interval.to_sql(); format!("({input} + interval '{shift}')") } else { input diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs index 0b4818df43f01..4e8e33bd0b5cc 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs @@ -5,9 +5,14 @@ use crate::cube_bridge::measure_definition::{ }; use crate::cube_bridge::member_sql::MemberSql; use crate::planner::query_tools::QueryTools; +use crate::planner::sql_evaluator::collectors::find_owned_by_cube_child; use crate::planner::sql_evaluator::{sql_nodes::SqlNode, Compiler, SqlCall, SqlEvaluatorVisitor}; use crate::planner::sql_templates::PlanSqlTemplates; +use crate::planner::SqlInterval; use cubenativeutils::CubeError; +use itertools::Itertools; +use std::cmp::{Eq, PartialEq}; +use std::collections::HashMap; use std::rc::Rc; #[derive(Clone)] @@ -33,6 +38,20 @@ impl MeasureOrderBy { } } +#[derive(Clone, Debug)] +pub struct MeasureTimeShift { + pub interval: SqlInterval, + pub dimension: Rc, +} + +impl PartialEq for MeasureTimeShift { + fn eq(&self, other: &Self) -> bool { + self.interval == other.interval && self.dimension.full_name() == other.dimension.full_name() + } +} + +impl Eq for MeasureTimeShift {} + #[derive(Clone)] pub struct MeasureSymbol { cube_name: String, @@ -40,6 +59,7 @@ pub struct MeasureSymbol { definition: Rc, measure_filters: Vec>, measure_drill_filters: Vec>, + time_shifts: Vec, measure_order_by: Vec, member_sql: Option>, pk_sqls: Vec>, @@ -55,6 +75,7 @@ impl MeasureSymbol { definition: Rc, measure_filters: Vec>, measure_drill_filters: Vec>, + time_shifts: Vec, measure_order_by: Vec, ) -> Self { Self { @@ -66,6 +87,7 @@ impl MeasureSymbol { measure_filters, measure_drill_filters, measure_order_by, + time_shifts, is_splitted_source: false, } } @@ -82,6 +104,10 @@ impl MeasureSymbol { &self.pk_sqls } + pub fn time_shifts(&self) -> &Vec { + &self.time_shifts + } + pub fn is_calculated(&self) -> bool { match self.definition.static_data().measure_type.as_str() { "number" | "string" | "time" | "boolean" => true, @@ -354,6 +380,43 @@ impl SymbolFactory for MeasureSymbolFactory { None }; + let time_shifts = + if let Some(time_shift_references) = &definition.static_data().time_shift_references { + let mut shifts: HashMap = HashMap::new(); + for shift_ref in time_shift_references.iter() { + let interval = shift_ref.interval.parse::()?; + let interval = + if shift_ref.shift_type.as_ref().unwrap_or(&format!("prior")) == "next" { + -interval + } else { + interval + }; + let dimension = + compiler.add_dimension_evaluator(shift_ref.time_dimension.clone())?; + let dimension = find_owned_by_cube_child(&dimension)?; + let dimension_name = dimension.full_name(); + if let Some(exists) = shifts.get(&dimension_name) { + if exists.interval != interval { + return Err(CubeError::user(format!( + "Different time shifts for one dimension {} not allowed", + dimension_name + ))); + } + } else { + shifts.insert( + dimension_name, + MeasureTimeShift { + interval: interval.clone(), + dimension: dimension.clone(), + }, + ); + }; + } + shifts.into_values().collect_vec() + } else { + vec![] + }; + Ok(MemberSymbol::new_measure(MeasureSymbol::new( cube_name, name, @@ -362,6 +425,7 @@ impl SymbolFactory for MeasureSymbolFactory { definition, measure_filters, measure_drill_filters, + time_shifts, measure_order_by, ))) } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/mod.rs index c05e9d160b911..c29d900d2ed6c 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/mod.rs @@ -13,7 +13,7 @@ pub use dimension_symbol::{ DimensionCaseDefinition, DimensionCaseWhenItem, DimensionSymbol, DimensionSymbolFactory, DimenstionCaseLabel, }; -pub use measure_symbol::{MeasureSymbol, MeasureSymbolFactory}; +pub use measure_symbol::{MeasureSymbol, MeasureSymbolFactory, MeasureTimeShift}; pub use member_expression_symbol::MemberExpressionSymbol; pub use member_symbol::MemberSymbol; pub use symbol_factory::SymbolFactory; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/time_dimension/sql_interval.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/time_dimension/sql_interval.rs index cda53985fdbc3..fbe0568b9e5e4 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/time_dimension/sql_interval.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/time_dimension/sql_interval.rs @@ -1,8 +1,9 @@ use cubenativeutils::CubeError; use itertools::Itertools; +use std::ops::{Add, AddAssign, Neg, Sub}; use std::str::FromStr; -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Hash, Eq)] pub struct SqlInterval { pub year: i32, pub month: i32, @@ -57,6 +58,32 @@ impl SqlInterval { Ok(res.to_string()) } + pub fn to_sql(&self) -> String { + let mut res = vec![]; + if self.year != 0 { + res.push(format!("{} YEAR", self.year)); + } + if self.month != 0 { + res.push(format!("{} MONTH", self.month)); + } + if self.week != 0 { + res.push(format!("{} WEEK", self.week)); + } + if self.day != 0 { + res.push(format!("{} DAY", self.day)); + } + if self.hour != 0 { + res.push(format!("{} HOUR", self.hour)); + } + if self.minute != 0 { + res.push(format!("{} MINUTE", self.minute)); + } + if self.second != 0 { + res.push(format!("{} SECOND", self.second)); + } + res.join(" ") + } + pub fn inverse(&self) -> Self { Self::new( -self.year, @@ -70,6 +97,63 @@ impl SqlInterval { } } +impl Add for SqlInterval { + type Output = SqlInterval; + fn add(self, other: SqlInterval) -> SqlInterval { + SqlInterval::new( + self.year + other.year, + self.month + other.month, + self.week + other.week, + self.day + other.day, + self.hour + other.hour, + self.minute + other.minute, + self.second + other.second, + ) + } +} + +impl AddAssign for SqlInterval { + fn add_assign(&mut self, other: SqlInterval) { + self.year += other.year; + self.month += other.month; + self.week += other.week; + self.day += other.day; + self.hour += other.hour; + self.minute += other.minute; + self.second += other.second; + } +} + +impl Sub for SqlInterval { + type Output = SqlInterval; + fn sub(self, other: SqlInterval) -> SqlInterval { + SqlInterval::new( + self.year - other.year, + self.month - other.month, + self.week - other.week, + self.day - other.day, + self.hour - other.hour, + self.minute - other.minute, + self.second - other.second, + ) + } +} + +impl Neg for SqlInterval { + type Output = SqlInterval; + fn neg(self) -> SqlInterval { + SqlInterval::new( + -self.year, + -self.month, + -self.week, + -self.day, + -self.hour, + -self.minute, + -self.second, + ) + } +} + impl Default for SqlInterval { fn default() -> Self { Self { @@ -124,4 +208,19 @@ mod tests { SqlInterval::new(1, 3, 4, 2, 4, 2, 1) ); } + #[test] + fn test_arithmetic() { + assert_eq!( + SqlInterval::new(1, 3, 4, 2, 4, 2, 1) + SqlInterval::new(1, 3, 4, 2, 4, 2, 1), + SqlInterval::new(2, 6, 8, 4, 8, 4, 2) + ); + assert_eq!( + SqlInterval::new(1, 3, 4, 2, 4, 2, 1) - SqlInterval::new(1, 4, 4, 2, 2, 2, 1), + SqlInterval::new(0, -1, 0, 0, 2, 0, 0) + ); + assert_eq!( + -SqlInterval::new(1, 3, -4, 2, 4, 2, 1), + SqlInterval::new(-1, -3, 4, -2, -4, -2, -1) + ); + } }