From a18f779c41c3f61d56d1ab0910a9cd62750887b0 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 15:00:26 +0100 Subject: [PATCH 01/42] chore(tesseract): Symbols refactoring --- .../src/planner/sql_evaluator/symbols/mod.rs | 4 + .../sql_evaluator/symbols/primitive_type.rs | 108 ++++++++++++++++++ .../sql_evaluator/symbols/simple_dimension.rs | 28 +++++ 3 files changed, 140 insertions(+) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/primitive_type.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/simple_dimension.rs 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 84d02f0d1997c..3b34868ecc9bf 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/mod.rs @@ -4,6 +4,8 @@ mod dimension_symbol; mod measure_symbol; mod member_expression_symbol; mod member_symbol; +mod primitive_type; +mod simple_dimension; mod symbol_factory; mod time_dimension_symbol; @@ -17,5 +19,7 @@ pub use measure_symbol::{ }; pub use member_expression_symbol::{MemberExpressionExpression, MemberExpressionSymbol}; pub use member_symbol::MemberSymbol; +pub use primitive_type::*; +pub use simple_dimension::SimpleDimension; pub use symbol_factory::SymbolFactory; pub use time_dimension_symbol::TimeDimensionSymbol; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/primitive_type.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/primitive_type.rs new file mode 100644 index 0000000000000..b437588511262 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/primitive_type.rs @@ -0,0 +1,108 @@ +use std::fmt; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum PrimitiveType { + Number, + String, + Boolean, + Time, +} + +impl PrimitiveType { + /// Try to construct PrimitiveType from string + pub fn try_from_str(s: &str) -> Option { + match s { + "number" => Some(Self::Number), + "string" => Some(Self::String), + "boolean" => Some(Self::Boolean), + "time" => Some(Self::Time), + _ => None, + } + } + + /// Get string representation of the type + pub fn as_str(&self) -> &'static str { + match self { + Self::Number => "number", + Self::String => "string", + Self::Boolean => "boolean", + Self::Time => "time", + } + } +} + +impl fmt::Display for PrimitiveType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl TryFrom<&str> for PrimitiveType { + type Error = (); + + fn try_from(value: &str) -> Result { + Self::try_from_str(value).ok_or(()) + } +} + +impl TryFrom for PrimitiveType { + type Error = (); + + fn try_from(value: String) -> Result { + Self::try_from_str(&value).ok_or(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_try_from_str() { + assert_eq!( + PrimitiveType::try_from_str("number"), + Some(PrimitiveType::Number) + ); + assert_eq!( + PrimitiveType::try_from_str("string"), + Some(PrimitiveType::String) + ); + assert_eq!( + PrimitiveType::try_from_str("boolean"), + Some(PrimitiveType::Boolean) + ); + assert_eq!( + PrimitiveType::try_from_str("time"), + Some(PrimitiveType::Time) + ); + assert_eq!(PrimitiveType::try_from_str("unknown"), None); + } + + #[test] + fn test_as_str() { + assert_eq!(PrimitiveType::Number.as_str(), "number"); + assert_eq!(PrimitiveType::String.as_str(), "string"); + assert_eq!(PrimitiveType::Boolean.as_str(), "boolean"); + assert_eq!(PrimitiveType::Time.as_str(), "time"); + } + + #[test] + fn test_try_from() { + assert_eq!(PrimitiveType::try_from("number"), Ok(PrimitiveType::Number)); + assert_eq!(PrimitiveType::try_from("invalid"), Err(())); + + assert_eq!( + PrimitiveType::try_from("string".to_string()), + Ok(PrimitiveType::String) + ); + assert_eq!(PrimitiveType::try_from("invalid".to_string()), Err(())); + } + + #[test] + fn test_display() { + assert_eq!(format!("{}", PrimitiveType::Number), "number"); + assert_eq!(format!("{}", PrimitiveType::String), "string"); + assert_eq!(format!("{}", PrimitiveType::Boolean), "boolean"); + assert_eq!(format!("{}", PrimitiveType::Time), "time"); + } +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/simple_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/simple_dimension.rs new file mode 100644 index 0000000000000..595915489c53c --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/simple_dimension.rs @@ -0,0 +1,28 @@ +use super::PrimitiveType; +use crate::planner::sql_evaluator::SqlCall; +use std::rc::Rc; + +/// Represents a simple dimension with a primitive type +#[derive(Clone)] +pub struct SimpleDimension { + primitive_type: PrimitiveType, + member_sql: Rc, +} + +impl SimpleDimension { + pub fn new(primitive_type: PrimitiveType, member_sql: Rc) -> Self { + Self { + primitive_type, + member_sql, + } + } + + pub fn primitive_type(&self) -> PrimitiveType { + self.primitive_type + } + + pub fn member_sql(&self) -> &Rc { + &self.member_sql + } +} + From d784e30113d153fc8581ef845b5c98e7bf2dc3c8 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 15:02:52 +0100 Subject: [PATCH 02/42] in work --- .../sql_evaluator/symbols/geo_dimension.rs | 26 +++++++++++++++++++ .../src/planner/sql_evaluator/symbols/mod.rs | 2 ++ 2 files changed, 28 insertions(+) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/geo_dimension.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/geo_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/geo_dimension.rs new file mode 100644 index 0000000000000..56803a6ad1684 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/geo_dimension.rs @@ -0,0 +1,26 @@ +use crate::planner::sql_evaluator::SqlCall; +use std::rc::Rc; + +/// Represents a geo dimension with latitude and longitude +#[derive(Clone)] +pub struct GeoDimension { + latitude: Rc, + longitude: Rc, +} + +impl GeoDimension { + pub fn new(latitude: Rc, longitude: Rc) -> Self { + Self { + latitude, + longitude, + } + } + + pub fn latitude(&self) -> &Rc { + &self.latitude + } + + pub fn longitude(&self) -> &Rc { + &self.longitude + } +} \ No newline at end of file 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 3b34868ecc9bf..286bc95a1e374 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/mod.rs @@ -1,6 +1,7 @@ mod common; mod cube_symbol; mod dimension_symbol; +mod geo_dimension; mod measure_symbol; mod member_expression_symbol; mod member_symbol; @@ -14,6 +15,7 @@ pub use cube_symbol::{ CubeNameSymbol, CubeNameSymbolFactory, CubeTableSymbol, CubeTableSymbolFactory, }; pub use dimension_symbol::*; +pub use geo_dimension::GeoDimension; pub use measure_symbol::{ DimensionTimeShift, MeasureSymbol, MeasureSymbolFactory, MeasureTimeShifts, }; From 49b4a7903768997b3841bf53ef7bd0ed6392f733 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 15:13:16 +0100 Subject: [PATCH 03/42] in work --- .../symbols/calendar_dimension.rs | 35 +++++++++++++++++++ .../sql_evaluator/symbols/case_dimension.rs | 17 +++++++++ .../src/planner/sql_evaluator/symbols/mod.rs | 6 ++++ .../sql_evaluator/symbols/switch_dimension.rs | 15 ++++++++ 4 files changed, 73 insertions(+) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/calendar_dimension.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/case_dimension.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/switch_dimension.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/calendar_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/calendar_dimension.rs new file mode 100644 index 0000000000000..7d19c1529c7a2 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/calendar_dimension.rs @@ -0,0 +1,35 @@ +use super::CalendarDimensionTimeShift; + +/// Represents a calendar dimension with time shift capabilities +#[derive(Clone)] +pub struct CalendarDimension { + time_shift: Vec, + time_shift_pk_full_name: Option, + is_self_time_shift_pk: bool, +} + +impl CalendarDimension { + pub fn new( + time_shift: Vec, + time_shift_pk_full_name: Option, + is_self_time_shift_pk: bool, + ) -> Self { + Self { + time_shift, + time_shift_pk_full_name, + is_self_time_shift_pk, + } + } + + pub fn time_shift(&self) -> &Vec { + &self.time_shift + } + + pub fn time_shift_pk_full_name(&self) -> Option { + self.time_shift_pk_full_name.clone() + } + + pub fn is_self_time_shift_pk(&self) -> bool { + self.is_self_time_shift_pk + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/case_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/case_dimension.rs new file mode 100644 index 0000000000000..c9647e95211d9 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/case_dimension.rs @@ -0,0 +1,17 @@ +use super::Case; + +/// Represents a case dimension with conditional logic +#[derive(Clone)] +pub struct CaseDimension { + case: Case, +} + +impl CaseDimension { + pub fn new(case: Case) -> Self { + Self { case } + } + + pub fn case(&self) -> &Case { + &self.case + } +} \ No newline at end of file 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 286bc95a1e374..e78a6a8a5eae1 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/mod.rs @@ -1,3 +1,5 @@ +mod calendar_dimension; +mod case_dimension; mod common; mod cube_symbol; mod dimension_symbol; @@ -7,9 +9,12 @@ mod member_expression_symbol; mod member_symbol; mod primitive_type; mod simple_dimension; +mod switch_dimension; mod symbol_factory; mod time_dimension_symbol; +pub use calendar_dimension::CalendarDimension; +pub use case_dimension::CaseDimension; pub use common::*; pub use cube_symbol::{ CubeNameSymbol, CubeNameSymbolFactory, CubeTableSymbol, CubeTableSymbolFactory, @@ -23,5 +28,6 @@ pub use member_expression_symbol::{MemberExpressionExpression, MemberExpressionS pub use member_symbol::MemberSymbol; pub use primitive_type::*; pub use simple_dimension::SimpleDimension; +pub use switch_dimension::SwitchDimension; pub use symbol_factory::SymbolFactory; pub use time_dimension_symbol::TimeDimensionSymbol; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/switch_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/switch_dimension.rs new file mode 100644 index 0000000000000..e87ef6ac5f535 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/switch_dimension.rs @@ -0,0 +1,15 @@ +/// Represents a switch dimension with predefined values +#[derive(Clone)] +pub struct SwitchDimension { + values: Vec, +} + +impl SwitchDimension { + pub fn new(values: Vec) -> Self { + Self { values } + } + + pub fn values(&self) -> &Vec { + &self.values + } +} \ No newline at end of file From 6771878e4fdddfb41ea89fc8a1faab93e68d170d Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 15:45:50 +0100 Subject: [PATCH 04/42] in work --- .../src/cube_bridge/base_tools.rs | 11 ----- .../symbols/calendar_dimension.rs | 47 ++++++++++++++++++- .../sql_evaluator/symbols/case_dimension.rs | 21 ++++++++- .../sql_evaluator/symbols/geo_dimension.rs | 22 +++++++++ .../sql_evaluator/symbols/simple_dimension.rs | 21 ++++++++- .../sql_evaluator/symbols/switch_dimension.rs | 20 ++++++++ 6 files changed, 128 insertions(+), 14 deletions(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_tools.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_tools.rs index 37ff0e395892f..1633461e3f890 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_tools.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_tools.rs @@ -1,7 +1,4 @@ -use super::base_query_options::FilterItem; use super::driver_tools::{DriverTools, NativeDriverTools}; -use super::filter_group::{FilterGroup, NativeFilterGroup}; -use super::filter_params::{FilterParams, NativeFilterParams}; use super::join_definition::{JoinDefinition, NativeJoinDefinition}; use super::pre_aggregation_obj::{NativePreAggregationObj, PreAggregationObj}; use super::security_context::{NativeSecurityContext, SecurityContext}; @@ -23,14 +20,6 @@ pub trait BaseTools { fn sql_templates(&self) -> Result, CubeError>; fn security_context_for_rust(&self) -> Result, CubeError>; fn sql_utils_for_rust(&self) -> Result, CubeError>; - fn filters_proxy_for_rust( - &self, - used_filters: Option>, - ) -> Result, CubeError>; - fn filter_group_function_for_rust( - &self, - used_filters: Option>, - ) -> Result, CubeError>; fn generate_time_series( &self, granularity: String, diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/calendar_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/calendar_dimension.rs index 7d19c1529c7a2..710b7a1592fb2 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/calendar_dimension.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/calendar_dimension.rs @@ -1,4 +1,6 @@ -use super::CalendarDimensionTimeShift; +use super::{CalendarDimensionTimeShift, MemberSymbol}; +use cubenativeutils::CubeError; +use std::rc::Rc; /// Represents a calendar dimension with time shift capabilities #[derive(Clone)] @@ -32,4 +34,47 @@ impl CalendarDimension { pub fn is_self_time_shift_pk(&self) -> bool { self.is_self_time_shift_pk } + + pub fn get_dependencies(&self, deps: &mut Vec>) { + for shift in &self.time_shift { + if let Some(sql) = &shift.sql { + sql.extract_symbol_deps(deps); + } + } + } + + pub fn get_dependencies_with_path(&self, deps: &mut Vec<(Rc, Vec)>) { + for shift in &self.time_shift { + if let Some(sql) = &shift.sql { + sql.extract_symbol_deps_with_path(deps); + } + } + } + + pub fn apply_to_deps) -> Result, CubeError>>( + &self, + f: &F, + ) -> Result { + let time_shift = self + .time_shift + .iter() + .map(|shift| -> Result<_, CubeError> { + Ok(CalendarDimensionTimeShift { + interval: shift.interval.clone(), + name: shift.name.clone(), + sql: if let Some(sql) = &shift.sql { + Some(sql.apply_recursive(f)?) + } else { + None + }, + }) + }) + .collect::, _>>()?; + + Ok(Self { + time_shift, + time_shift_pk_full_name: self.time_shift_pk_full_name.clone(), + is_self_time_shift_pk: self.is_self_time_shift_pk, + }) + } } \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/case_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/case_dimension.rs index c9647e95211d9..3d8c206318d7f 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/case_dimension.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/case_dimension.rs @@ -1,4 +1,6 @@ -use super::Case; +use super::{Case, MemberSymbol}; +use cubenativeutils::CubeError; +use std::rc::Rc; /// Represents a case dimension with conditional logic #[derive(Clone)] @@ -14,4 +16,21 @@ impl CaseDimension { pub fn case(&self) -> &Case { &self.case } + + pub fn get_dependencies(&self, deps: &mut Vec>) { + self.case.extract_symbol_deps(deps); + } + + pub fn get_dependencies_with_path(&self, deps: &mut Vec<(Rc, Vec)>) { + self.case.extract_symbol_deps_with_path(deps); + } + + pub fn apply_to_deps) -> Result, CubeError>>( + &self, + f: &F, + ) -> Result { + Ok(Self { + case: self.case.apply_to_deps(f)?, + }) + } } \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/geo_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/geo_dimension.rs index 56803a6ad1684..8d4e82de35f3a 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/geo_dimension.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/geo_dimension.rs @@ -1,4 +1,6 @@ +use super::MemberSymbol; use crate::planner::sql_evaluator::SqlCall; +use cubenativeutils::CubeError; use std::rc::Rc; /// Represents a geo dimension with latitude and longitude @@ -23,4 +25,24 @@ impl GeoDimension { pub fn longitude(&self) -> &Rc { &self.longitude } + + pub fn get_dependencies(&self, deps: &mut Vec>) { + self.latitude.extract_symbol_deps(deps); + self.longitude.extract_symbol_deps(deps); + } + + pub fn get_dependencies_with_path(&self, deps: &mut Vec<(Rc, Vec)>) { + self.latitude.extract_symbol_deps_with_path(deps); + self.longitude.extract_symbol_deps_with_path(deps); + } + + pub fn apply_to_deps) -> Result, CubeError>>( + &self, + f: &F, + ) -> Result { + Ok(Self { + latitude: self.latitude.apply_recursive(f)?, + longitude: self.longitude.apply_recursive(f)?, + }) + } } \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/simple_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/simple_dimension.rs index 595915489c53c..dc1907661e1be 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/simple_dimension.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/simple_dimension.rs @@ -1,5 +1,6 @@ -use super::PrimitiveType; +use super::{MemberSymbol, PrimitiveType}; use crate::planner::sql_evaluator::SqlCall; +use cubenativeutils::CubeError; use std::rc::Rc; /// Represents a simple dimension with a primitive type @@ -24,5 +25,23 @@ impl SimpleDimension { pub fn member_sql(&self) -> &Rc { &self.member_sql } + + pub fn get_dependencies(&self, deps: &mut Vec>) { + self.member_sql.extract_symbol_deps(deps); + } + + pub fn get_dependencies_with_path(&self, deps: &mut Vec<(Rc, Vec)>) { + self.member_sql.extract_symbol_deps_with_path(deps); + } + + pub fn apply_to_deps) -> Result, CubeError>>( + &self, + f: &F, + ) -> Result { + Ok(Self { + primitive_type: self.primitive_type, + member_sql: self.member_sql.apply_recursive(f)?, + }) + } } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/switch_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/switch_dimension.rs index e87ef6ac5f535..3e5e59f46527b 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/switch_dimension.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/switch_dimension.rs @@ -1,3 +1,7 @@ +use super::MemberSymbol; +use cubenativeutils::CubeError; +use std::rc::Rc; + /// Represents a switch dimension with predefined values #[derive(Clone)] pub struct SwitchDimension { @@ -12,4 +16,20 @@ impl SwitchDimension { pub fn values(&self) -> &Vec { &self.values } + + pub fn get_dependencies(&self, _deps: &mut Vec>) { + // Switch dimension has no SQL dependencies + } + + pub fn get_dependencies_with_path(&self, _deps: &mut Vec<(Rc, Vec)>) { + // Switch dimension has no SQL dependencies + } + + pub fn apply_to_deps) -> Result, CubeError>>( + &self, + _f: &F, + ) -> Result { + // Switch dimension has no SQL dependencies, return clone + Ok(self.clone()) + } } \ No newline at end of file From 896b3f3247f53f3f4600aad1d6549264346af3b5 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 16:23:03 +0100 Subject: [PATCH 05/42] member_sql mock --- .../src/cube_bridge/member_sql.rs | 11 +- rust/cubesqlplanner/cubesqlplanner/src/lib.rs | 2 + .../cubesqlplanner/src/planner/query_tools.rs | 2 +- .../src/planner/sql_evaluator/compiler.rs | 13 +- .../planner/sql_evaluator/sql_call_builder.rs | 6 +- .../cube_bridge/mock_member_sql.rs | 249 ++++++++++++++++++ .../cube_bridge/mock_security_context.rs | 12 + .../cube_bridge/mock_sql_utils.rs | 12 + .../src/test_fixtures/cube_bridge/mod.rs | 7 + .../cubesqlplanner/src/test_fixtures/mod.rs | 1 + 10 files changed, 300 insertions(+), 15 deletions(-) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_sql.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_security_context.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_sql_utils.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/mod.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/member_sql.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/member_sql.rs index 09b076bb46d50..77589f9354956 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/member_sql.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/member_sql.rs @@ -3,8 +3,9 @@ use super::{ security_context::{NativeSecurityContext, SecurityContext}, sql_utils::NativeSqlUtils, }; +use crate::cube_bridge::sql_utils::SqlUtils; +use crate::planner::sql_evaluator::SqlCallArg; use crate::utils::UniqueVector; -use crate::{cube_bridge::base_tools::BaseTools, planner::sql_evaluator::SqlCallArg}; use cubenativeutils::wrappers::object::{NativeFunction, NativeStruct, NativeType}; use cubenativeutils::wrappers::serializer::{NativeDeserialize, NativeSerialize}; use cubenativeutils::wrappers::NativeContextHolderRef; @@ -300,7 +301,7 @@ pub trait MemberSql { fn as_any(self: Rc) -> Rc; fn compile_template_sql( &self, - base_tools: Rc, + sql_utils: Rc, security_context: Rc, ) -> Result<(SqlTemplate, SqlTemplateArgs), CubeError>; } @@ -633,7 +634,7 @@ impl MemberSql for NativeMemberSql { fn compile_template_sql( &self, - base_tools: Rc, + sql_utils: Rc, security_context: Rc, ) -> Result<(SqlTemplate, SqlTemplateArgs), CubeError> { let state = ProxyState::new(); @@ -666,8 +667,8 @@ impl MemberSql for NativeMemberSql { context_obj, )? } else if arg == "SQL_UTILS" { - base_tools - .sql_utils_for_rust()? + sql_utils + .clone() .as_any() .downcast::>() .unwrap() diff --git a/rust/cubesqlplanner/cubesqlplanner/src/lib.rs b/rust/cubesqlplanner/cubesqlplanner/src/lib.rs index 1eb5448bfa9f1..b872291090504 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/lib.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/lib.rs @@ -3,4 +3,6 @@ pub mod logical_plan; pub mod physical_plan_builder; pub mod plan; pub mod planner; +#[cfg(test)] +pub(crate) mod test_fixtures; pub(crate) mod utils; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/query_tools.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/query_tools.rs index a32a4be8b465e..4a4f7364c65a9 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/query_tools.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/query_tools.rs @@ -135,7 +135,7 @@ impl QueryTools { }; let evaluator_compiler = Rc::new(RefCell::new(Compiler::new( cube_evaluator.clone(), - base_tools.clone(), + base_tools.sql_utils_for_rust()?, security_context.clone(), timezone.clone(), ))); diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs index b5d7667f65709..938a9891f15c6 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs @@ -4,11 +4,11 @@ use super::{ CubeNameSymbolFactory, CubeTableSymbolFactory, DimensionSymbolFactory, MeasureSymbolFactory, SqlCall, SymbolFactory, TraversalVisitor, }; -use crate::cube_bridge::base_tools::BaseTools; use crate::cube_bridge::evaluator::CubeEvaluator; use crate::cube_bridge::join_hints::JoinHintItem; use crate::cube_bridge::member_sql::MemberSql; use crate::cube_bridge::security_context::SecurityContext; +use crate::cube_bridge::sql_utils::SqlUtils; use crate::planner::sql_evaluator::sql_call_builder::SqlCallBuilder; use chrono_tz::Tz; use cubenativeutils::CubeError; @@ -16,7 +16,7 @@ use std::collections::HashMap; use std::rc::Rc; pub struct Compiler { cube_evaluator: Rc, - base_tools: Rc, + sql_utils: Rc, security_context: Rc, timezone: Tz, /* (type, name) */ @@ -26,14 +26,14 @@ pub struct Compiler { impl Compiler { pub fn new( cube_evaluator: Rc, - base_tools: Rc, + sql_utils: Rc, security_context: Rc, timezone: Tz, ) -> Self { Self { cube_evaluator, security_context, - base_tools, + sql_utils, timezone, members: HashMap::new(), } @@ -56,10 +56,6 @@ impl Compiler { } } - pub fn base_tools(&self) -> Rc { - self.base_tools.clone() - } - pub fn add_measure_evaluator( &mut self, measure: String, @@ -136,6 +132,7 @@ impl Compiler { let call_builder = SqlCallBuilder::new( self, self.cube_evaluator.clone(), + self.sql_utils.clone(), self.security_context.clone(), ); let sql_call = call_builder.build(&cube_name, member_sql.clone())?; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call_builder.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call_builder.rs index 655064426c832..23300f81c67c7 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call_builder.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call_builder.rs @@ -4,6 +4,7 @@ use super::{SqlCall, SqlCallDependency, SqlCallFilterGroupItem, SqlCallFilterPar use crate::cube_bridge::evaluator::CubeEvaluator; use crate::cube_bridge::member_sql::*; use crate::cube_bridge::security_context::SecurityContext; +use crate::cube_bridge::sql_utils::SqlUtils; use crate::planner::sql_evaluator::TimeDimensionSymbol; use crate::planner::GranularityHelper; use cubenativeutils::CubeError; @@ -12,6 +13,7 @@ use std::rc::Rc; pub struct SqlCallBuilder<'a> { compiler: &'a mut Compiler, cube_evaluator: Rc, + sql_utils: Rc, security_context: Rc, } @@ -19,11 +21,13 @@ impl<'a> SqlCallBuilder<'a> { pub fn new( compiler: &'a mut Compiler, cube_evaluator: Rc, + sql_utils: Rc, security_context: Rc, ) -> Self { Self { compiler, cube_evaluator, + sql_utils, security_context, } } @@ -34,7 +38,7 @@ impl<'a> SqlCallBuilder<'a> { member_sql: Rc, ) -> Result { let (template, template_args) = member_sql - .compile_template_sql(self.compiler.base_tools(), self.security_context.clone())?; + .compile_template_sql(self.sql_utils.clone(), self.security_context.clone())?; let deps = template_args .symbol_paths diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_sql.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_sql.rs new file mode 100644 index 0000000000000..744c1555248f2 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_sql.rs @@ -0,0 +1,249 @@ +use crate::cube_bridge::member_sql::{MemberSql, SqlTemplate, SqlTemplateArgs}; +use crate::cube_bridge::security_context::SecurityContext; +use crate::cube_bridge::sql_utils::SqlUtils; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; + +/// Mock implementation of MemberSql for testing +/// Parses template strings like "{CUBE.field} / {other_cube.field} + {revenue}" +/// and converts them to "{arg:0} / {arg:1} + {arg:2}" with extracted symbol paths +#[derive(Debug)] +pub struct MockMemberSql { + template: String, + args: SqlTemplateArgs, + args_names: Vec, +} + +impl MockMemberSql { + /// Create a new MockMemberSql from a template string + /// Example: "{CUBE.field} / {other_cube.cube2.field} + {revenue}" + pub fn new(template: impl Into) -> Result { + let template_str = template.into(); + let (parsed_template, args, args_names) = Self::parse_template(&template_str)?; + + Ok(Self { + template: parsed_template, + args, + args_names, + }) + } + + /// Parse the template string and extract symbol paths + /// Converts "{path.to.symbol}" to "{arg:N}" and collects paths + fn parse_template(template: &str) -> Result<(String, SqlTemplateArgs, Vec), CubeError> { + let mut result = String::new(); + let mut args = SqlTemplateArgs::default(); + let mut args_names = Vec::new(); + + let mut chars = template.chars().peekable(); + + while let Some(ch) = chars.next() { + if ch == '{' { + // Check if it's an escaped brace + if chars.peek() == Some(&'{') { + chars.next(); // consume second '{' + result.push_str("{{"); + continue; + } + + // Extract content until closing '}' + let mut path = String::new(); + let mut found_closing = false; + + while let Some(ch) = chars.next() { + if ch == '}' { + found_closing = true; + break; + } + path.push(ch); + } + + if !found_closing { + return Err(CubeError::user(format!( + "Unclosed brace in template: {}", + template + ))); + } + + // Parse the path and add to symbol_paths + let path_parts: Vec = path.split('.').map(|s| s.to_string()).collect(); + + if path_parts.is_empty() || path_parts.iter().any(|p| p.is_empty()) { + return Err(CubeError::user(format!( + "Invalid path in template: {}", + path + ))); + } + + // Extract top-level arg name + let arg_name = path_parts[0].clone(); + if !args_names.contains(&arg_name) { + args_names.push(arg_name); + } + + let index = args.insert_symbol_path(path_parts); + result.push_str(&format!("{{arg:{}}}", index)); + } else if ch == '}' { + // Check if it's an escaped brace + if chars.peek() == Some(&'}') { + chars.next(); // consume second '}' + result.push_str("}}"); + continue; + } + result.push(ch); + } else { + result.push(ch); + } + } + + Ok((result, args, args_names)) + } +} + +impl MemberSql for MockMemberSql { + fn args_names(&self) -> &Vec { + &self.args_names + } + + fn as_any(self: Rc) -> Rc { + self + } + + fn compile_template_sql( + &self, + _sql_utils: Rc, + _security_context: Rc, + ) -> Result<(SqlTemplate, SqlTemplateArgs), CubeError> { + Ok((SqlTemplate::String(self.template.clone()), self.args.clone())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_simple_path() { + let mock = MockMemberSql::new("{CUBE.field}").unwrap(); + + assert_eq!(mock.template, "{arg:0}"); + assert_eq!(mock.args.symbol_paths.len(), 1); + assert_eq!(mock.args.symbol_paths[0], vec!["CUBE", "field"]); + assert_eq!(mock.args_names, vec!["CUBE"]); + } + + #[test] + fn test_multiple_paths() { + let mock = MockMemberSql::new("{CUBE.field} / {other_cube.field}").unwrap(); + + assert_eq!(mock.template, "{arg:0} / {arg:1}"); + assert_eq!(mock.args.symbol_paths.len(), 2); + assert_eq!(mock.args.symbol_paths[0], vec!["CUBE", "field"]); + assert_eq!(mock.args.symbol_paths[1], vec!["other_cube", "field"]); + assert_eq!(mock.args_names, vec!["CUBE", "other_cube"]); + } + + #[test] + fn test_nested_path() { + let mock = MockMemberSql::new("{other_cube.cube2.field}").unwrap(); + + assert_eq!(mock.template, "{arg:0}"); + assert_eq!(mock.args.symbol_paths.len(), 1); + assert_eq!(mock.args.symbol_paths[0], vec!["other_cube", "cube2", "field"]); + assert_eq!(mock.args_names, vec!["other_cube"]); + } + + #[test] + fn test_complex_expression() { + let mock = MockMemberSql::new("{CUBE.field} / {other_cube.cube2.field} + {revenue}").unwrap(); + + assert_eq!(mock.template, "{arg:0} / {arg:1} + {arg:2}"); + assert_eq!(mock.args.symbol_paths.len(), 3); + assert_eq!(mock.args.symbol_paths[0], vec!["CUBE", "field"]); + assert_eq!(mock.args.symbol_paths[1], vec!["other_cube", "cube2", "field"]); + assert_eq!(mock.args.symbol_paths[2], vec!["revenue"]); + assert_eq!(mock.args_names, vec!["CUBE", "other_cube", "revenue"]); + } + + #[test] + fn test_duplicate_paths() { + let mock = MockMemberSql::new("{CUBE.field} + {CUBE.field}").unwrap(); + + // UniqueVector should deduplicate, so both references use arg:0 + assert_eq!(mock.template, "{arg:0} + {arg:0}"); + assert_eq!(mock.args.symbol_paths.len(), 1); + assert_eq!(mock.args.symbol_paths[0], vec!["CUBE", "field"]); + assert_eq!(mock.args_names, vec!["CUBE"]); + } + + #[test] + fn test_same_top_level_different_paths() { + let mock = MockMemberSql::new("{CUBE.field1} + {CUBE.field2}").unwrap(); + + assert_eq!(mock.template, "{arg:0} + {arg:1}"); + assert_eq!(mock.args.symbol_paths.len(), 2); + assert_eq!(mock.args.symbol_paths[0], vec!["CUBE", "field1"]); + assert_eq!(mock.args.symbol_paths[1], vec!["CUBE", "field2"]); + // Top-level arg name appears only once + assert_eq!(mock.args_names, vec!["CUBE"]); + } + + #[test] + fn test_with_text() { + let mock = MockMemberSql::new("SUM({CUBE.amount}) * 100").unwrap(); + + assert_eq!(mock.template, "SUM({arg:0}) * 100"); + assert_eq!(mock.args.symbol_paths.len(), 1); + assert_eq!(mock.args.symbol_paths[0], vec!["CUBE", "amount"]); + assert_eq!(mock.args_names, vec!["CUBE"]); + } + + #[test] + fn test_escaped_braces() { + let mock = MockMemberSql::new("{{literal}} {CUBE.field}").unwrap(); + + assert_eq!(mock.template, "{{literal}} {arg:0}"); + assert_eq!(mock.args.symbol_paths.len(), 1); + assert_eq!(mock.args.symbol_paths[0], vec!["CUBE", "field"]); + assert_eq!(mock.args_names, vec!["CUBE"]); + } + + #[test] + fn test_unclosed_brace_error() { + let result = MockMemberSql::new("{CUBE.field"); + + assert!(result.is_err()); + assert!(result.unwrap_err().message.contains("Unclosed brace")); + } + + #[test] + fn test_empty_path_error() { + let result = MockMemberSql::new("{}"); + + assert!(result.is_err()); + assert!(result.unwrap_err().message.contains("Invalid path")); + } + + #[test] + fn test_compile_template_sql() { + let mock = Rc::new(MockMemberSql::new("{CUBE.field} / {other.field}").unwrap()); + let (template, args) = mock + .compile_template_sql( + Rc::new(crate::test_fixtures::cube_bridge::MockSqlUtils), + Rc::new(crate::test_fixtures::cube_bridge::MockSecurityContext), + ) + .unwrap(); + + match template { + SqlTemplate::String(s) => { + assert_eq!(s, "{arg:0} / {arg:1}"); + } + _ => panic!("Expected String template"), + } + + assert_eq!(args.symbol_paths.len(), 2); + assert_eq!(args.symbol_paths[0], vec!["CUBE", "field"]); + assert_eq!(args.symbol_paths[1], vec!["other", "field"]); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_security_context.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_security_context.rs new file mode 100644 index 0000000000000..28ddf1bd595f4 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_security_context.rs @@ -0,0 +1,12 @@ +use crate::cube_bridge::security_context::SecurityContext; +use std::any::Any; +use std::rc::Rc; + +/// Mock implementation of SecurityContext for testing +pub struct MockSecurityContext; + +impl SecurityContext for MockSecurityContext { + fn as_any(self: Rc) -> Rc { + self + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_sql_utils.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_sql_utils.rs new file mode 100644 index 0000000000000..1d101fb0baacf --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_sql_utils.rs @@ -0,0 +1,12 @@ +use crate::cube_bridge::sql_utils::SqlUtils; +use std::any::Any; +use std::rc::Rc; + +/// Mock implementation of SqlUtils for testing +pub struct MockSqlUtils; + +impl SqlUtils for MockSqlUtils { + fn as_any(self: Rc) -> Rc { + self + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs new file mode 100644 index 0000000000000..19d12e3edce08 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -0,0 +1,7 @@ +mod mock_member_sql; +mod mock_security_context; +mod mock_sql_utils; + +pub use mock_member_sql::MockMemberSql; +pub use mock_security_context::MockSecurityContext; +pub use mock_sql_utils::MockSqlUtils; \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/mod.rs new file mode 100644 index 0000000000000..226c08aca4dc3 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/mod.rs @@ -0,0 +1 @@ +pub mod cube_bridge; From 7a091416ac8504a833f389a7a17668a8d0666008 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 16:50:36 +0100 Subject: [PATCH 06/42] in work --- .../src/test_fixtures/cube_bridge/macros.rs | 70 ++++++ .../cube_bridge/mock_dimension_definition.rs | 234 ++++++++++++++++++ .../cube_bridge/mock_geo_item.rs | 37 +++ .../cube_bridge/mock_timeshift_definition.rs | 83 +++++++ .../src/test_fixtures/cube_bridge/mod.rs | 11 +- 5 files changed, 434 insertions(+), 1 deletion(-) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/macros.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_geo_item.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_timeshift_definition.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/macros.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/macros.rs new file mode 100644 index 0000000000000..6bfd6f72451d9 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/macros.rs @@ -0,0 +1,70 @@ +/// Macro to implement static_data() method for mock bridge types +/// +/// # Usage +/// ```ignore +/// impl_static_data!( +/// MockDimensionDefinition, +/// DimensionDefinitionStatic, +/// dimension_type, +/// owned_by_cube, +/// multi_stage +/// ); +/// ``` +/// +/// This generates a method that creates StaticData struct on the fly from struct fields +#[macro_export] +macro_rules! impl_static_data { + // Pattern: impl_static_data!(MockType, StaticType, field1, field2, ...) + ($mock_type:ty, $static_type:path, $($field:ident),* $(,)?) => { + impl $mock_type { + pub fn static_data(&self) -> $static_type { + $static_type { + $($field: self.$field.clone()),* + } + } + } + }; +} + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + use typed_builder::TypedBuilder; + + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] + pub struct TestStatic { + pub name: String, + pub value: Option, + } + + #[derive(TypedBuilder)] + pub struct MockTest { + #[builder(default = "test".to_string())] + name: String, + #[builder(default)] + value: Option, + } + + impl_static_data!(MockTest, TestStatic, name, value); + + #[test] + fn test_static_data_macro() { + let mock = MockTest::builder() + .name("hello".to_string()) + .value(Some(42)) + .build(); + + let static_data = mock.static_data(); + assert_eq!(static_data.name, "hello"); + assert_eq!(static_data.value, Some(42)); + } + + #[test] + fn test_static_data_macro_with_defaults() { + let mock = MockTest::builder().build(); + + let static_data = mock.static_data(); + assert_eq!(static_data.name, "test"); + assert_eq!(static_data.value, None); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs new file mode 100644 index 0000000000000..bb02d7b704673 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs @@ -0,0 +1,234 @@ +use crate::cube_bridge::case_variant::CaseVariant; +use crate::cube_bridge::dimension_definition::{DimensionDefinition, DimensionDefinitionStatic}; +use crate::cube_bridge::geo_item::GeoItem; +use crate::cube_bridge::member_sql::MemberSql; +use crate::cube_bridge::timeshift_definition::TimeShiftDefinition; +use crate::impl_static_data; +use crate::test_fixtures::cube_bridge::{MockGeoItem, MockMemberSql, MockTimeShiftDefinition}; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of DimensionDefinition for testing +#[derive(TypedBuilder)] +pub struct MockDimensionDefinition { + // Fields from DimensionDefinitionStatic + #[builder(default = "string".to_string())] + dimension_type: String, + #[builder(default)] + owned_by_cube: Option, + #[builder(default)] + multi_stage: Option, + #[builder(default)] + add_group_by_references: Option>, + #[builder(default)] + sub_query: Option, + #[builder(default)] + propagate_filters_to_sub_query: Option, + #[builder(default)] + values: Option>, + + // Optional trait fields + #[builder(default, setter(strip_option))] + sql: Option, + #[builder(default)] + case: Option>, + #[builder(default, setter(strip_option))] + latitude: Option, + #[builder(default, setter(strip_option))] + longitude: Option, + #[builder(default)] + time_shift: Option>>, +} + +impl_static_data!( + MockDimensionDefinition, + DimensionDefinitionStatic, + dimension_type, + owned_by_cube, + multi_stage, + add_group_by_references, + sub_query, + propagate_filters_to_sub_query, + values +); + +impl DimensionDefinition for MockDimensionDefinition { + fn static_data(&self) -> &DimensionDefinitionStatic { + Box::leak(Box::new(Self::static_data(self))) + } + + fn has_sql(&self) -> Result { + Ok(self.sql.is_some()) + } + + fn sql(&self) -> Result>, CubeError> { + match &self.sql { + Some(sql_str) => Ok(Some(Rc::new(MockMemberSql::new(sql_str)?))), + None => Ok(None), + } + } + + fn has_case(&self) -> Result { + Ok(self.case.is_some()) + } + + fn case(&self) -> Result, CubeError> { + Ok(self.case.as_ref().map(|c| match &**c { + CaseVariant::Case(def) => CaseVariant::Case(def.clone()), + CaseVariant::CaseSwitch(def) => CaseVariant::CaseSwitch(def.clone()), + })) + } + + fn has_latitude(&self) -> Result { + Ok(self.latitude.is_some()) + } + + fn latitude(&self) -> Result>, CubeError> { + match &self.latitude { + Some(lat_str) => Ok(Some(Rc::new( + MockGeoItem::builder().sql(lat_str.clone()).build(), + ))), + None => Ok(None), + } + } + + fn has_longitude(&self) -> Result { + Ok(self.longitude.is_some()) + } + + fn longitude(&self) -> Result>, CubeError> { + match &self.longitude { + Some(lon_str) => Ok(Some(Rc::new( + MockGeoItem::builder().sql(lon_str.clone()).build(), + ))), + None => Ok(None), + } + } + + fn has_time_shift(&self) -> Result { + Ok(self.time_shift.is_some()) + } + + fn time_shift(&self) -> Result>>, CubeError> { + match &self.time_shift { + Some(shifts) => { + let result: Vec> = shifts + .iter() + .map(|s| s.clone() as Rc) + .collect(); + Ok(Some(result)) + } + None => Ok(None), + } + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_string_dimension() { + let dim = MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("{CUBE.name}".to_string()) + .build(); + + assert_eq!(dim.static_data().dimension_type, "string"); + assert!(dim.has_sql().unwrap()); + assert!(dim.sql().unwrap().is_some()); + } + + #[test] + fn test_number_dimension() { + let dim = MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("{CUBE.count}".to_string()) + .build(); + + assert_eq!(dim.static_data().dimension_type, "number"); + assert!(dim.has_sql().unwrap()); + } + + #[test] + fn test_time_dimension() { + let dim = MockDimensionDefinition::builder() + .dimension_type("time".to_string()) + .sql("{CUBE.created_at}".to_string()) + .build(); + + assert_eq!(dim.static_data().dimension_type, "time"); + assert!(dim.has_sql().unwrap()); + } + + #[test] + fn test_geo_dimension() { + let dim = MockDimensionDefinition::builder() + .dimension_type("geo".to_string()) + .latitude("{CUBE.lat}".to_string()) + .longitude("{CUBE.lon}".to_string()) + .build(); + + assert_eq!(dim.static_data().dimension_type, "geo"); + assert!(dim.has_latitude().unwrap()); + assert!(dim.has_longitude().unwrap()); + assert!(!dim.has_sql().unwrap()); + } + + #[test] + fn test_switch_dimension() { + let dim = MockDimensionDefinition::builder() + .dimension_type("switch".to_string()) + .values(Some(vec!["active".to_string(), "inactive".to_string()])) + .build(); + + assert_eq!(dim.static_data().dimension_type, "switch"); + assert_eq!( + dim.static_data().values, + Some(vec!["active".to_string(), "inactive".to_string()]) + ); + assert!(!dim.has_sql().unwrap()); + } + + #[test] + fn test_dimension_with_time_shift() { + let time_shift = Rc::new( + MockTimeShiftDefinition::builder() + .interval(Some("1 day".to_string())) + .name(Some("yesterday".to_string())) + .build(), + ); + + let dim = MockDimensionDefinition::builder() + .dimension_type("time".to_string()) + .sql("{CUBE.date}".to_string()) + .time_shift(Some(vec![time_shift])) + .build(); + + assert!(dim.has_time_shift().unwrap()); + let shifts = dim.time_shift().unwrap().unwrap(); + assert_eq!(shifts.len(), 1); + } + + #[test] + fn test_dimension_with_flags() { + let dim = MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("{CUBE.field}".to_string()) + .multi_stage(Some(true)) + .sub_query(Some(true)) + .owned_by_cube(Some(false)) + .build(); + + assert_eq!(dim.static_data().multi_stage, Some(true)); + assert_eq!(dim.static_data().sub_query, Some(true)); + assert_eq!(dim.static_data().owned_by_cube, Some(false)); + } +} + diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_geo_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_geo_item.rs new file mode 100644 index 0000000000000..5ebf5d577c049 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_geo_item.rs @@ -0,0 +1,37 @@ +use crate::cube_bridge::geo_item::GeoItem; +use crate::cube_bridge::member_sql::MemberSql; +use crate::test_fixtures::cube_bridge::MockMemberSql; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of GeoItem for testing +#[derive(Debug, TypedBuilder)] +pub struct MockGeoItem { + sql: String, +} + +impl GeoItem for MockGeoItem { + fn sql(&self) -> Result, CubeError> { + Ok(Rc::new(MockMemberSql::new(&self.sql)?)) + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mock_geo_item() { + let geo_item = MockGeoItem::builder() + .sql("{CUBE.latitude}".to_string()) + .build(); + let sql = geo_item.sql().unwrap(); + assert_eq!(sql.args_names(), &vec!["CUBE"]); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_timeshift_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_timeshift_definition.rs new file mode 100644 index 0000000000000..0d7582d4e70a5 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_timeshift_definition.rs @@ -0,0 +1,83 @@ +use crate::cube_bridge::member_sql::MemberSql; +use crate::cube_bridge::timeshift_definition::{TimeShiftDefinition, TimeShiftDefinitionStatic}; +use crate::impl_static_data; +use crate::test_fixtures::cube_bridge::MockMemberSql; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of TimeShiftDefinition for testing +#[derive(Debug, Clone, TypedBuilder)] +pub struct MockTimeShiftDefinition { + #[builder(default)] + interval: Option, + #[builder(default)] + timeshift_type: Option, + #[builder(default)] + name: Option, + #[builder(default, setter(strip_option))] + sql: Option, +} + +impl_static_data!( + MockTimeShiftDefinition, + TimeShiftDefinitionStatic, + interval, + timeshift_type, + name +); + +impl TimeShiftDefinition for MockTimeShiftDefinition { + fn static_data(&self) -> &TimeShiftDefinitionStatic { + // Store static data in a thread-local or use lazy_static + // For now, we'll use a simpler approach - Box::leak for tests + Box::leak(Box::new(Self::static_data(self))) + } + + fn has_sql(&self) -> Result { + Ok(self.sql.is_some()) + } + + fn sql(&self) -> Result>, CubeError> { + match &self.sql { + Some(sql_str) => Ok(Some(Rc::new(MockMemberSql::new(sql_str)?))), + None => Ok(None), + } + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mock_timeshift_with_sql() { + let timeshift = MockTimeShiftDefinition::builder() + .interval(Some("1 day".to_string())) + .timeshift_type(Some("prior".to_string())) + .name(Some("yesterday".to_string())) + .sql("{CUBE.date_field}".to_string()) + .build(); + + assert_eq!(timeshift.static_data().interval, Some("1 day".to_string())); + assert!(timeshift.has_sql().unwrap()); + assert!(timeshift.sql().unwrap().is_some()); + } + + #[test] + fn test_mock_timeshift_without_sql() { + let timeshift = MockTimeShiftDefinition::builder() + .interval(Some("1 week".to_string())) + .build(); + + assert_eq!(timeshift.static_data().interval, Some("1 week".to_string())); + assert!(!timeshift.has_sql().unwrap()); + assert!(timeshift.sql().unwrap().is_none()); + } +} + diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index 19d12e3edce08..79f89596316db 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -1,7 +1,16 @@ +#[macro_use] +mod macros; + +mod mock_dimension_definition; +mod mock_geo_item; mod mock_member_sql; mod mock_security_context; mod mock_sql_utils; +mod mock_timeshift_definition; +pub use mock_dimension_definition::MockDimensionDefinition; +pub use mock_geo_item::MockGeoItem; pub use mock_member_sql::MockMemberSql; pub use mock_security_context::MockSecurityContext; -pub use mock_sql_utils::MockSqlUtils; \ No newline at end of file +pub use mock_sql_utils::MockSqlUtils; +pub use mock_timeshift_definition::MockTimeShiftDefinition; \ No newline at end of file From 94faed9dff3b02d29a2ebdc52b4cb426efeb657a Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 17:14:57 +0100 Subject: [PATCH 07/42] dimension mock --- .../src/cube_bridge/string_or_sql.rs | 11 + .../cube_bridge/mock_case_definition.rs | 74 ++++++ .../cube_bridge/mock_case_else_item.rs | 36 +++ .../cube_bridge/mock_case_item.rs | 45 ++++ .../mock_case_switch_definition.rs | 84 +++++++ .../cube_bridge/mock_case_switch_else_item.rs | 37 +++ .../cube_bridge/mock_case_switch_item.rs | 47 ++++ .../cube_bridge/mock_dimension_definition.rs | 218 ++++++++++++++++++ .../src/test_fixtures/cube_bridge/mod.rs | 13 +- 9 files changed, 564 insertions(+), 1 deletion(-) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_definition.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_else_item.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_item.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_definition.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_else_item.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_item.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/string_or_sql.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/string_or_sql.rs index 0bbd10fe24dd4..af2a85b466e17 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/string_or_sql.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/string_or_sql.rs @@ -3,13 +3,24 @@ use cubenativeutils::wrappers::inner_types::InnerTypes; use cubenativeutils::wrappers::serializer::NativeDeserialize; use cubenativeutils::wrappers::NativeObjectHandle; use cubenativeutils::CubeError; +use std::fmt; use std::rc::Rc; +#[derive(Clone)] pub enum StringOrSql { String(String), MemberSql(Rc), } +impl fmt::Debug for StringOrSql { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StringOrSql::String(s) => write!(f, "String({:?})", s), + StringOrSql::MemberSql(_) => write!(f, "MemberSql()"), + } + } +} + impl NativeDeserialize for StringOrSql { fn from_native(native_object: NativeObjectHandle) -> Result { match String::from_native(native_object.clone()) { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_definition.rs new file mode 100644 index 0000000000000..8947eda608e98 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_definition.rs @@ -0,0 +1,74 @@ +use crate::cube_bridge::case_definition::CaseDefinition; +use crate::cube_bridge::case_else_item::CaseElseItem; +use crate::cube_bridge::case_item::CaseItem; +use crate::test_fixtures::cube_bridge::{MockCaseElseItem, MockCaseItem}; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of CaseDefinition for testing +#[derive(TypedBuilder)] +pub struct MockCaseDefinition { + when: Vec>, + else_label: Rc, +} + +impl CaseDefinition for MockCaseDefinition { + fn when(&self) -> Result>, CubeError> { + Ok(self + .when + .iter() + .map(|item| item.clone() as Rc) + .collect()) + } + + fn else_label(&self) -> Result, CubeError> { + Ok(self.else_label.clone() as Rc) + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cube_bridge::string_or_sql::StringOrSql; + + #[test] + fn test_mock_case_definition() { + let when_items = vec![ + Rc::new( + MockCaseItem::builder() + .sql("{CUBE.status} = 'active'".to_string()) + .label(StringOrSql::String("Active".to_string())) + .build(), + ), + Rc::new( + MockCaseItem::builder() + .sql("{CUBE.status} = 'inactive'".to_string()) + .label(StringOrSql::String("Inactive".to_string())) + .build(), + ), + ]; + + let else_item = Rc::new( + MockCaseElseItem::builder() + .label(StringOrSql::String("Unknown".to_string())) + .build(), + ); + + let case_def = MockCaseDefinition::builder() + .when(when_items) + .else_label(else_item) + .build(); + + let when_result = case_def.when().unwrap(); + assert_eq!(when_result.len(), 2); + + let else_result = case_def.else_label().unwrap(); + assert!(else_result.label().is_ok()); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_else_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_else_item.rs new file mode 100644 index 0000000000000..8c1dd7110bc2a --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_else_item.rs @@ -0,0 +1,36 @@ +use crate::cube_bridge::case_else_item::CaseElseItem; +use crate::cube_bridge::string_or_sql::StringOrSql; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of CaseElseItem for testing +#[derive(Debug, Clone, TypedBuilder)] +pub struct MockCaseElseItem { + label: StringOrSql, +} + +impl CaseElseItem for MockCaseElseItem { + fn label(&self) -> Result { + Ok(self.label.clone()) + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mock_case_else_item() { + let item = MockCaseElseItem::builder() + .label(StringOrSql::String("Unknown".to_string())) + .build(); + + assert!(matches!(item.label().unwrap(), StringOrSql::String(_))); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_item.rs new file mode 100644 index 0000000000000..35daa266ad896 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_item.rs @@ -0,0 +1,45 @@ +use crate::cube_bridge::case_item::CaseItem; +use crate::cube_bridge::member_sql::MemberSql; +use crate::cube_bridge::string_or_sql::StringOrSql; +use crate::test_fixtures::cube_bridge::MockMemberSql; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of CaseItem for testing +#[derive(Debug, Clone, TypedBuilder)] +pub struct MockCaseItem { + sql: String, + label: StringOrSql, +} + +impl CaseItem for MockCaseItem { + fn sql(&self) -> Result, CubeError> { + Ok(Rc::new(MockMemberSql::new(&self.sql)?)) + } + + fn label(&self) -> Result { + Ok(self.label.clone()) + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mock_case_item() { + let item = MockCaseItem::builder() + .sql("{CUBE.status} = 'active'".to_string()) + .label(StringOrSql::String("Active".to_string())) + .build(); + + assert!(item.sql().is_ok()); + assert!(matches!(item.label().unwrap(), StringOrSql::String(_))); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_definition.rs new file mode 100644 index 0000000000000..5234d0b13727f --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_definition.rs @@ -0,0 +1,84 @@ +use crate::cube_bridge::case_switch_definition::CaseSwitchDefinition; +use crate::cube_bridge::case_switch_else_item::CaseSwitchElseItem; +use crate::cube_bridge::case_switch_item::CaseSwitchItem; +use crate::cube_bridge::member_sql::MemberSql; +use crate::test_fixtures::cube_bridge::{ + MockCaseSwitchElseItem, MockCaseSwitchItem, MockMemberSql, +}; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of CaseSwitchDefinition for testing +#[derive(TypedBuilder)] +pub struct MockCaseSwitchDefinition { + switch: String, + when: Vec>, + else_sql: Rc, +} + +impl CaseSwitchDefinition for MockCaseSwitchDefinition { + fn switch(&self) -> Result, CubeError> { + Ok(Rc::new(MockMemberSql::new(&self.switch)?)) + } + + fn when(&self) -> Result>, CubeError> { + Ok(self + .when + .iter() + .map(|item| item.clone() as Rc) + .collect()) + } + + fn else_sql(&self) -> Result, CubeError> { + Ok(self.else_sql.clone() as Rc) + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mock_case_switch_definition() { + let when_items = vec![ + Rc::new( + MockCaseSwitchItem::builder() + .value("1".to_string()) + .sql("{CUBE.active_sql}".to_string()) + .build(), + ), + Rc::new( + MockCaseSwitchItem::builder() + .value("0".to_string()) + .sql("{CUBE.inactive_sql}".to_string()) + .build(), + ), + ]; + + let else_item = Rc::new( + MockCaseSwitchElseItem::builder() + .sql("{CUBE.default_sql}".to_string()) + .build(), + ); + + let case_switch = MockCaseSwitchDefinition::builder() + .switch("{CUBE.status_code}".to_string()) + .when(when_items) + .else_sql(else_item) + .build(); + + assert!(case_switch.switch().is_ok()); + + let when_result = case_switch.when().unwrap(); + assert_eq!(when_result.len(), 2); + + let else_result = case_switch.else_sql().unwrap(); + assert!(else_result.sql().is_ok()); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_else_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_else_item.rs new file mode 100644 index 0000000000000..7677e9badb342 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_else_item.rs @@ -0,0 +1,37 @@ +use crate::cube_bridge::case_switch_else_item::CaseSwitchElseItem; +use crate::cube_bridge::member_sql::MemberSql; +use crate::test_fixtures::cube_bridge::MockMemberSql; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of CaseSwitchElseItem for testing +#[derive(Debug, Clone, TypedBuilder)] +pub struct MockCaseSwitchElseItem { + sql: String, +} + +impl CaseSwitchElseItem for MockCaseSwitchElseItem { + fn sql(&self) -> Result, CubeError> { + Ok(Rc::new(MockMemberSql::new(&self.sql)?)) + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mock_case_switch_else_item() { + let item = MockCaseSwitchElseItem::builder() + .sql("{CUBE.default_value}".to_string()) + .build(); + + assert!(item.sql().is_ok()); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_item.rs new file mode 100644 index 0000000000000..792999239210b --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_item.rs @@ -0,0 +1,47 @@ +use crate::cube_bridge::case_switch_item::{CaseSwitchItem, CaseSwitchItemStatic}; +use crate::cube_bridge::member_sql::MemberSql; +use crate::impl_static_data; +use crate::test_fixtures::cube_bridge::MockMemberSql; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of CaseSwitchItem for testing +#[derive(Debug, Clone, TypedBuilder)] +pub struct MockCaseSwitchItem { + value: String, + sql: String, +} + +impl_static_data!(MockCaseSwitchItem, CaseSwitchItemStatic, value); + +impl CaseSwitchItem for MockCaseSwitchItem { + fn static_data(&self) -> &CaseSwitchItemStatic { + Box::leak(Box::new(Self::static_data(self))) + } + + fn sql(&self) -> Result, CubeError> { + Ok(Rc::new(MockMemberSql::new(&self.sql)?)) + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mock_case_switch_item() { + let item = MockCaseSwitchItem::builder() + .value("1".to_string()) + .sql("{CUBE.active_sql}".to_string()) + .build(); + + assert_eq!(item.static_data().value, "1"); + assert!(item.sql().is_ok()); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs index bb02d7b704673..7531ec08d4f90 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs @@ -230,5 +230,223 @@ mod tests { assert_eq!(dim.static_data().sub_query, Some(true)); assert_eq!(dim.static_data().owned_by_cube, Some(false)); } + + #[test] + fn test_sql_parsing_simple() { + let dim = MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("{CUBE.field}".to_string()) + .build(); + + let sql = dim.sql().unwrap().unwrap(); + assert_eq!(sql.args_names(), &vec!["CUBE"]); + + // Check compiled template + use crate::test_fixtures::cube_bridge::{MockSecurityContext, MockSqlUtils}; + let (template, args) = sql + .compile_template_sql(Rc::new(MockSqlUtils), Rc::new(MockSecurityContext)) + .unwrap(); + + match template { + crate::cube_bridge::member_sql::SqlTemplate::String(s) => { + assert_eq!(s, "{arg:0}"); + } + _ => panic!("Expected String template"), + } + + assert_eq!(args.symbol_paths.len(), 1); + assert_eq!(args.symbol_paths[0], vec!["CUBE", "field"]); + } + + #[test] + fn test_sql_parsing_multiple_refs() { + let dim = MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("{CUBE.first_name} || ' ' || {CUBE.last_name}".to_string()) + .build(); + + let sql = dim.sql().unwrap().unwrap(); + assert_eq!(sql.args_names(), &vec!["CUBE"]); + + // Check compiled template + use crate::test_fixtures::cube_bridge::{MockSecurityContext, MockSqlUtils}; + let (template, args) = sql + .compile_template_sql(Rc::new(MockSqlUtils), Rc::new(MockSecurityContext)) + .unwrap(); + + match template { + crate::cube_bridge::member_sql::SqlTemplate::String(s) => { + assert_eq!(s, "{arg:0} || ' ' || {arg:1}"); + } + _ => panic!("Expected String template"), + } + + assert_eq!(args.symbol_paths.len(), 2); + assert_eq!(args.symbol_paths[0], vec!["CUBE", "first_name"]); + assert_eq!(args.symbol_paths[1], vec!["CUBE", "last_name"]); + } + + #[test] + fn test_sql_parsing_cross_cube_refs() { + let dim = MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("{CUBE.amount} / {other_cube.total}".to_string()) + .build(); + + let sql = dim.sql().unwrap().unwrap(); + assert_eq!(sql.args_names(), &vec!["CUBE", "other_cube"]); + + // Check compiled template + use crate::test_fixtures::cube_bridge::{MockSecurityContext, MockSqlUtils}; + let (template, args) = sql + .compile_template_sql(Rc::new(MockSqlUtils), Rc::new(MockSecurityContext)) + .unwrap(); + + match template { + crate::cube_bridge::member_sql::SqlTemplate::String(s) => { + assert_eq!(s, "{arg:0} / {arg:1}"); + } + _ => panic!("Expected String template"), + } + + assert_eq!(args.symbol_paths.len(), 2); + assert_eq!(args.symbol_paths[0], vec!["CUBE", "amount"]); + assert_eq!(args.symbol_paths[1], vec!["other_cube", "total"]); + } + + #[test] + fn test_geo_sql_parsing() { + let dim = MockDimensionDefinition::builder() + .dimension_type("geo".to_string()) + .latitude("{CUBE.latitude}".to_string()) + .longitude("{CUBE.longitude}".to_string()) + .build(); + + assert!(!dim.has_sql().unwrap()); + + let lat = dim.latitude().unwrap().unwrap(); + let lat_sql = lat.sql().unwrap(); + + use crate::test_fixtures::cube_bridge::{MockSecurityContext, MockSqlUtils}; + let (template, args) = lat_sql + .compile_template_sql(Rc::new(MockSqlUtils), Rc::new(MockSecurityContext)) + .unwrap(); + + match template { + crate::cube_bridge::member_sql::SqlTemplate::String(s) => { + assert_eq!(s, "{arg:0}"); + } + _ => panic!("Expected String template"), + } + + assert_eq!(args.symbol_paths[0], vec!["CUBE", "latitude"]); + } + + #[test] + fn test_case_dimension() { + use crate::cube_bridge::case_variant::CaseVariant; + use crate::cube_bridge::string_or_sql::StringOrSql; + use crate::test_fixtures::cube_bridge::{ + MockCaseDefinition, MockCaseElseItem, MockCaseItem, + }; + + let when_items = vec![ + Rc::new( + MockCaseItem::builder() + .sql("{CUBE.status} = 'active'".to_string()) + .label(StringOrSql::String("Active".to_string())) + .build(), + ), + Rc::new( + MockCaseItem::builder() + .sql("{CUBE.status} = 'inactive'".to_string()) + .label(StringOrSql::String("Inactive".to_string())) + .build(), + ), + ]; + + let else_item = Rc::new( + MockCaseElseItem::builder() + .label(StringOrSql::String("Unknown".to_string())) + .build(), + ); + + let case_def = Rc::new( + MockCaseDefinition::builder() + .when(when_items) + .else_label(else_item) + .build(), + ); + + let dim = MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .case(Some(Rc::new(CaseVariant::Case(case_def)))) + .build(); + + assert!(dim.has_case().unwrap()); + let case_result = dim.case().unwrap(); + assert!(case_result.is_some()); + + if let Some(CaseVariant::Case(case)) = case_result { + let when = case.when().unwrap(); + assert_eq!(when.len(), 2); + } else { + panic!("Expected Case variant"); + } + } + + #[test] + fn test_case_switch_dimension() { + use crate::cube_bridge::case_variant::CaseVariant; + use crate::test_fixtures::cube_bridge::{ + MockCaseSwitchDefinition, MockCaseSwitchElseItem, MockCaseSwitchItem, + }; + + let when_items = vec![ + Rc::new( + MockCaseSwitchItem::builder() + .value("1".to_string()) + .sql("{CUBE.active_value}".to_string()) + .build(), + ), + Rc::new( + MockCaseSwitchItem::builder() + .value("0".to_string()) + .sql("{CUBE.inactive_value}".to_string()) + .build(), + ), + ]; + + let else_item = Rc::new( + MockCaseSwitchElseItem::builder() + .sql("{CUBE.default_value}".to_string()) + .build(), + ); + + let case_switch = Rc::new( + MockCaseSwitchDefinition::builder() + .switch("{CUBE.status_code}".to_string()) + .when(when_items) + .else_sql(else_item) + .build(), + ); + + let dim = MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .case(Some(Rc::new(CaseVariant::CaseSwitch(case_switch)))) + .build(); + + assert!(dim.has_case().unwrap()); + let case_result = dim.case().unwrap(); + assert!(case_result.is_some()); + + if let Some(CaseVariant::CaseSwitch(case_switch)) = case_result { + assert!(case_switch.switch().is_ok()); + let when = case_switch.when().unwrap(); + assert_eq!(when.len(), 2); + } else { + panic!("Expected CaseSwitch variant"); + } + } } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index 79f89596316db..3b1575a9b2f48 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -1,6 +1,12 @@ #[macro_use] mod macros; +mod mock_case_definition; +mod mock_case_else_item; +mod mock_case_item; +mod mock_case_switch_definition; +mod mock_case_switch_else_item; +mod mock_case_switch_item; mod mock_dimension_definition; mod mock_geo_item; mod mock_member_sql; @@ -8,7 +14,12 @@ mod mock_security_context; mod mock_sql_utils; mod mock_timeshift_definition; -pub use mock_dimension_definition::MockDimensionDefinition; +pub use mock_case_definition::MockCaseDefinition; +pub use mock_case_else_item::MockCaseElseItem; +pub use mock_case_item::MockCaseItem; +pub use mock_case_switch_definition::MockCaseSwitchDefinition; +pub use mock_case_switch_else_item::MockCaseSwitchElseItem; +pub use mock_case_switch_item::MockCaseSwitchItem; pub use mock_geo_item::MockGeoItem; pub use mock_member_sql::MockMemberSql; pub use mock_security_context::MockSecurityContext; From b692926c3a4af31c09576587f4ec3a001232bfb6 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 17:20:26 +0100 Subject: [PATCH 08/42] measure mock --- .../cube_bridge/mock_measure_definition.rs | 376 ++++++++++++++++++ .../cube_bridge/mock_member_order_by.rs | 54 +++ .../mock_struct_with_sql_member.rs | 39 ++ .../src/test_fixtures/cube_bridge/mod.rs | 5 + 4 files changed, 474 insertions(+) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_measure_definition.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_order_by.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_struct_with_sql_member.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_measure_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_measure_definition.rs new file mode 100644 index 0000000000000..3884bbe9240ed --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_measure_definition.rs @@ -0,0 +1,376 @@ +use crate::cube_bridge::case_variant::CaseVariant; +use crate::cube_bridge::measure_definition::{ + MeasureDefinition, MeasureDefinitionStatic, RollingWindow, TimeShiftReference, +}; +use crate::cube_bridge::member_order_by::MemberOrderBy; +use crate::cube_bridge::member_sql::MemberSql; +use crate::cube_bridge::struct_with_sql_member::StructWithSqlMember; +use crate::impl_static_data; +use crate::test_fixtures::cube_bridge::{ + MockMemberOrderBy, MockMemberSql, MockStructWithSqlMember, +}; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of MeasureDefinition for testing +#[derive(TypedBuilder)] +pub struct MockMeasureDefinition { + // Fields from MeasureDefinitionStatic + #[builder(default = "number".to_string())] + measure_type: String, + #[builder(default)] + owned_by_cube: Option, + #[builder(default)] + multi_stage: Option, + #[builder(default)] + reduce_by_references: Option>, + #[builder(default)] + add_group_by_references: Option>, + #[builder(default)] + group_by_references: Option>, + #[builder(default)] + time_shift_references: Option>, + #[builder(default)] + rolling_window: Option, + + // Optional trait fields + #[builder(default, setter(strip_option))] + sql: Option, + #[builder(default)] + case: Option>, + #[builder(default)] + filters: Option>>, + #[builder(default)] + drill_filters: Option>>, + #[builder(default)] + order_by: Option>>, +} + +impl_static_data!( + MockMeasureDefinition, + MeasureDefinitionStatic, + measure_type, + owned_by_cube, + multi_stage, + reduce_by_references, + add_group_by_references, + group_by_references, + time_shift_references, + rolling_window +); + +impl MeasureDefinition for MockMeasureDefinition { + fn static_data(&self) -> &MeasureDefinitionStatic { + Box::leak(Box::new(Self::static_data(self))) + } + + fn has_sql(&self) -> Result { + Ok(self.sql.is_some()) + } + + fn sql(&self) -> Result>, CubeError> { + match &self.sql { + Some(sql_str) => Ok(Some(Rc::new(MockMemberSql::new(sql_str)?))), + None => Ok(None), + } + } + + fn has_case(&self) -> Result { + Ok(self.case.is_some()) + } + + fn case(&self) -> Result, CubeError> { + Ok(self.case.as_ref().map(|c| match &**c { + CaseVariant::Case(def) => CaseVariant::Case(def.clone()), + CaseVariant::CaseSwitch(def) => CaseVariant::CaseSwitch(def.clone()), + })) + } + + fn has_filters(&self) -> Result { + Ok(self.filters.is_some()) + } + + fn filters(&self) -> Result>>, CubeError> { + match &self.filters { + Some(filters) => { + let result: Vec> = filters + .iter() + .map(|f| f.clone() as Rc) + .collect(); + Ok(Some(result)) + } + None => Ok(None), + } + } + + fn has_drill_filters(&self) -> Result { + Ok(self.drill_filters.is_some()) + } + + fn drill_filters(&self) -> Result>>, CubeError> { + match &self.drill_filters { + Some(filters) => { + let result: Vec> = filters + .iter() + .map(|f| f.clone() as Rc) + .collect(); + Ok(Some(result)) + } + None => Ok(None), + } + } + + fn has_order_by(&self) -> Result { + Ok(self.order_by.is_some()) + } + + fn order_by(&self) -> Result>>, CubeError> { + match &self.order_by { + Some(order_by) => { + let result: Vec> = order_by + .iter() + .map(|o| o.clone() as Rc) + .collect(); + Ok(Some(result)) + } + None => Ok(None), + } + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_count_measure() { + let measure = MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .build(); + + assert_eq!(measure.static_data().measure_type, "count"); + assert!(measure.sql().unwrap().is_some()); + } + + #[test] + fn test_sum_measure() { + let measure = MockMeasureDefinition::builder() + .measure_type("sum".to_string()) + .sql("{CUBE.amount}".to_string()) + .build(); + + assert_eq!(measure.static_data().measure_type, "sum"); + let sql = measure.sql().unwrap().unwrap(); + assert_eq!(sql.args_names(), &vec!["CUBE"]); + } + + #[test] + fn test_measure_with_filters() { + let filters = vec![ + Rc::new( + MockStructWithSqlMember::builder() + .sql("{CUBE.status} = 'active'".to_string()) + .build(), + ), + Rc::new( + MockStructWithSqlMember::builder() + .sql("{CUBE.amount} > 0".to_string()) + .build(), + ), + ]; + + let measure = MockMeasureDefinition::builder() + .measure_type("sum".to_string()) + .sql("{CUBE.amount}".to_string()) + .filters(Some(filters)) + .build(); + + let result_filters = measure.filters().unwrap().unwrap(); + assert_eq!(result_filters.len(), 2); + } + + #[test] + fn test_measure_with_order_by() { + let order_by = vec![ + Rc::new( + MockMemberOrderBy::builder() + .sql("{CUBE.created_at}".to_string()) + .dir("desc".to_string()) + .build(), + ), + Rc::new( + MockMemberOrderBy::builder() + .sql("{CUBE.name}".to_string()) + .dir("asc".to_string()) + .build(), + ), + ]; + + let measure = MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .order_by(Some(order_by)) + .build(); + + let result_order_by = measure.order_by().unwrap().unwrap(); + assert_eq!(result_order_by.len(), 2); + } + + #[test] + fn test_measure_with_time_shift() { + let time_shift_refs = vec![ + TimeShiftReference { + interval: Some("1 day".to_string()), + name: Some("yesterday".to_string()), + shift_type: Some("prior".to_string()), + time_dimension: Some("created_at".to_string()), + }, + TimeShiftReference { + interval: Some("1 week".to_string()), + name: Some("last_week".to_string()), + shift_type: Some("prior".to_string()), + time_dimension: Some("created_at".to_string()), + }, + ]; + + let measure = MockMeasureDefinition::builder() + .measure_type("sum".to_string()) + .sql("{CUBE.amount}".to_string()) + .time_shift_references(Some(time_shift_refs)) + .build(); + + let static_data = measure.static_data(); + let refs = static_data.time_shift_references.as_ref().unwrap(); + assert_eq!(refs.len(), 2); + assert_eq!(refs[0].name, Some("yesterday".to_string())); + } + + #[test] + fn test_measure_with_rolling_window() { + let rolling_window = RollingWindow { + trailing: Some("7 day".to_string()), + leading: Some("0 day".to_string()), + offset: Some("start".to_string()), + rolling_type: Some("trailing".to_string()), + granularity: Some("day".to_string()), + }; + + let measure = MockMeasureDefinition::builder() + .measure_type("sum".to_string()) + .sql("{CUBE.amount}".to_string()) + .rolling_window(Some(rolling_window)) + .build(); + + let static_data = measure.static_data(); + let window = static_data.rolling_window.as_ref().unwrap(); + assert_eq!(window.trailing, Some("7 day".to_string())); + assert_eq!(window.granularity, Some("day".to_string())); + } + + #[test] + fn test_measure_with_case() { + use crate::cube_bridge::case_variant::CaseVariant; + use crate::cube_bridge::string_or_sql::StringOrSql; + use crate::test_fixtures::cube_bridge::{ + MockCaseDefinition, MockCaseElseItem, MockCaseItem, + }; + + let when_items = vec![ + Rc::new( + MockCaseItem::builder() + .sql("{CUBE.status} = 'active'".to_string()) + .label(StringOrSql::String("1".to_string())) + .build(), + ), + Rc::new( + MockCaseItem::builder() + .sql("{CUBE.status} = 'inactive'".to_string()) + .label(StringOrSql::String("0".to_string())) + .build(), + ), + ]; + + let else_item = Rc::new( + MockCaseElseItem::builder() + .label(StringOrSql::String("0".to_string())) + .build(), + ); + + let case_def = Rc::new( + MockCaseDefinition::builder() + .when(when_items) + .else_label(else_item) + .build(), + ); + + let measure = MockMeasureDefinition::builder() + .measure_type("number".to_string()) + .case(Some(Rc::new(CaseVariant::Case(case_def)))) + .build(); + + let case_result = measure.case().unwrap(); + assert!(case_result.is_some()); + } + + #[test] + fn test_measure_with_references() { + let measure = MockMeasureDefinition::builder() + .measure_type("sum".to_string()) + .sql("{CUBE.amount}".to_string()) + .reduce_by_references(Some(vec!["user_id".to_string(), "order_id".to_string()])) + .add_group_by_references(Some(vec!["status".to_string()])) + .group_by_references(Some(vec!["category".to_string()])) + .build(); + + assert_eq!( + measure.static_data().reduce_by_references, + Some(vec!["user_id".to_string(), "order_id".to_string()]) + ); + assert_eq!( + measure.static_data().add_group_by_references, + Some(vec!["status".to_string()]) + ); + assert_eq!( + measure.static_data().group_by_references, + Some(vec!["category".to_string()]) + ); + } + + #[test] + fn test_measure_with_drill_filters() { + let drill_filters = vec![Rc::new( + MockStructWithSqlMember::builder() + .sql("{CUBE.is_drillable} = true".to_string()) + .build(), + )]; + + let measure = MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .drill_filters(Some(drill_filters)) + .build(); + + let result_filters = measure.drill_filters().unwrap().unwrap(); + assert_eq!(result_filters.len(), 1); + } + + #[test] + fn test_measure_with_flags() { + let measure = MockMeasureDefinition::builder() + .measure_type("sum".to_string()) + .sql("{CUBE.amount}".to_string()) + .multi_stage(Some(true)) + .owned_by_cube(Some(false)) + .build(); + + assert_eq!(measure.static_data().multi_stage, Some(true)); + assert_eq!(measure.static_data().owned_by_cube, Some(false)); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_order_by.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_order_by.rs new file mode 100644 index 0000000000000..5be93a558be06 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_order_by.rs @@ -0,0 +1,54 @@ +use crate::cube_bridge::member_order_by::MemberOrderBy; +use crate::cube_bridge::member_sql::MemberSql; +use crate::test_fixtures::cube_bridge::MockMemberSql; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of MemberOrderBy for testing +#[derive(Debug, Clone, TypedBuilder)] +pub struct MockMemberOrderBy { + sql: String, + #[builder(default = "asc".to_string())] + dir: String, +} + +impl MemberOrderBy for MockMemberOrderBy { + fn sql(&self) -> Result, CubeError> { + Ok(Rc::new(MockMemberSql::new(&self.sql)?)) + } + + fn dir(&self) -> Result { + Ok(self.dir.clone()) + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mock_member_order_by() { + let order = MockMemberOrderBy::builder() + .sql("{CUBE.created_at}".to_string()) + .dir("desc".to_string()) + .build(); + + assert!(order.sql().is_ok()); + assert_eq!(order.dir().unwrap(), "desc"); + } + + #[test] + fn test_mock_member_order_by_default_dir() { + let order = MockMemberOrderBy::builder() + .sql("{CUBE.name}".to_string()) + .build(); + + assert_eq!(order.dir().unwrap(), "asc"); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_struct_with_sql_member.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_struct_with_sql_member.rs new file mode 100644 index 0000000000000..6b6e03fba99cc --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_struct_with_sql_member.rs @@ -0,0 +1,39 @@ +use crate::cube_bridge::member_sql::MemberSql; +use crate::cube_bridge::struct_with_sql_member::StructWithSqlMember; +use crate::test_fixtures::cube_bridge::MockMemberSql; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of StructWithSqlMember for testing +#[derive(Debug, Clone, TypedBuilder)] +pub struct MockStructWithSqlMember { + sql: String, +} + +impl StructWithSqlMember for MockStructWithSqlMember { + fn sql(&self) -> Result, CubeError> { + Ok(Rc::new(MockMemberSql::new(&self.sql)?)) + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mock_struct_with_sql_member() { + let item = MockStructWithSqlMember::builder() + .sql("{CUBE.field} > 0".to_string()) + .build(); + + assert!(item.sql().is_ok()); + let sql = item.sql().unwrap(); + assert_eq!(sql.args_names(), &vec!["CUBE"]); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index 3b1575a9b2f48..01b443c70c4cb 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -9,9 +9,12 @@ mod mock_case_switch_else_item; mod mock_case_switch_item; mod mock_dimension_definition; mod mock_geo_item; +mod mock_measure_definition; +mod mock_member_order_by; mod mock_member_sql; mod mock_security_context; mod mock_sql_utils; +mod mock_struct_with_sql_member; mod mock_timeshift_definition; pub use mock_case_definition::MockCaseDefinition; @@ -21,7 +24,9 @@ pub use mock_case_switch_definition::MockCaseSwitchDefinition; pub use mock_case_switch_else_item::MockCaseSwitchElseItem; pub use mock_case_switch_item::MockCaseSwitchItem; pub use mock_geo_item::MockGeoItem; +pub use mock_member_order_by::MockMemberOrderBy; pub use mock_member_sql::MockMemberSql; pub use mock_security_context::MockSecurityContext; pub use mock_sql_utils::MockSqlUtils; +pub use mock_struct_with_sql_member::MockStructWithSqlMember; pub use mock_timeshift_definition::MockTimeShiftDefinition; \ No newline at end of file From 55df9ec20a21eb3be8c981e3243b43b1ec1b57e2 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 17:23:29 +0100 Subject: [PATCH 09/42] cube mock --- .../cube_bridge/mock_cube_definition.rs | 175 ++++++++++++++++++ .../src/test_fixtures/cube_bridge/mod.rs | 1 + 2 files changed, 176 insertions(+) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_cube_definition.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_cube_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_cube_definition.rs new file mode 100644 index 0000000000000..4e16c6d4ea89e --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_cube_definition.rs @@ -0,0 +1,175 @@ +use crate::cube_bridge::cube_definition::{CubeDefinition, CubeDefinitionStatic}; +use crate::cube_bridge::member_sql::MemberSql; +use crate::impl_static_data; +use crate::test_fixtures::cube_bridge::MockMemberSql; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of CubeDefinition for testing +#[derive(TypedBuilder)] +pub struct MockCubeDefinition { + // Fields from CubeDefinitionStatic + name: String, + #[builder(default)] + sql_alias: Option, + #[builder(default)] + is_view: Option, + #[builder(default)] + is_calendar: Option, + #[builder(default)] + join_map: Option>>, + + // Optional trait fields + #[builder(default, setter(strip_option))] + sql_table: Option, + #[builder(default, setter(strip_option))] + sql: Option, +} + +impl_static_data!( + MockCubeDefinition, + CubeDefinitionStatic, + name, + sql_alias, + is_view, + is_calendar, + join_map +); + +impl CubeDefinition for MockCubeDefinition { + fn static_data(&self) -> &CubeDefinitionStatic { + Box::leak(Box::new(Self::static_data(self))) + } + + fn has_sql_table(&self) -> Result { + Ok(self.sql_table.is_some()) + } + + fn sql_table(&self) -> Result>, CubeError> { + match &self.sql_table { + Some(sql_str) => Ok(Some(Rc::new(MockMemberSql::new(sql_str)?))), + None => Ok(None), + } + } + + fn has_sql(&self) -> Result { + Ok(self.sql.is_some()) + } + + fn sql(&self) -> Result>, CubeError> { + match &self.sql { + Some(sql_str) => Ok(Some(Rc::new(MockMemberSql::new(sql_str)?))), + None => Ok(None), + } + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_cube() { + let cube = MockCubeDefinition::builder() + .name("users".to_string()) + .sql_table("public.users".to_string()) + .build(); + + assert_eq!(cube.static_data().name, "users"); + assert!(cube.has_sql_table().unwrap()); + assert!(cube.sql_table().unwrap().is_some()); + } + + #[test] + fn test_cube_with_alias() { + let cube = MockCubeDefinition::builder() + .name("users".to_string()) + .sql_alias(Some("u".to_string())) + .sql_table("public.users".to_string()) + .build(); + + let static_data = cube.static_data(); + assert_eq!(static_data.name, "users"); + assert_eq!(static_data.sql_alias, Some("u".to_string())); + assert_eq!(static_data.resolved_alias(), "u"); + } + + #[test] + fn test_cube_without_alias_uses_name() { + let cube = MockCubeDefinition::builder() + .name("users".to_string()) + .sql_table("public.users".to_string()) + .build(); + + let static_data = cube.static_data(); + assert_eq!(static_data.resolved_alias(), "users"); + } + + #[test] + fn test_view_cube() { + let cube = MockCubeDefinition::builder() + .name("active_users".to_string()) + .is_view(Some(true)) + .sql("SELECT * FROM users WHERE status = 'active'".to_string()) + .build(); + + assert_eq!(cube.static_data().is_view, Some(true)); + assert!(cube.has_sql().unwrap()); + assert!(!cube.has_sql_table().unwrap()); + } + + #[test] + fn test_calendar_cube() { + let cube = MockCubeDefinition::builder() + .name("calendar".to_string()) + .is_calendar(Some(true)) + .sql_table("public.calendar".to_string()) + .build(); + + assert_eq!(cube.static_data().is_calendar, Some(true)); + } + + #[test] + fn test_cube_with_join_map() { + let join_map = vec![ + vec!["users".to_string(), "orders".to_string()], + vec!["orders".to_string(), "products".to_string()], + ]; + + let cube = MockCubeDefinition::builder() + .name("users".to_string()) + .sql_table("public.users".to_string()) + .join_map(Some(join_map.clone())) + .build(); + + assert_eq!(cube.static_data().join_map, Some(join_map)); + } + + #[test] + fn test_cube_sql_parsing() { + let cube = MockCubeDefinition::builder() + .name("derived_cube".to_string()) + .sql("SELECT {other_cube.id}, COUNT(*) FROM {other_cube} GROUP BY 1".to_string()) + .build(); + + let sql = cube.sql().unwrap().unwrap(); + assert_eq!(sql.args_names(), &vec!["other_cube"]); + } + + #[test] + fn test_cube_with_sql_table_reference() { + let cube = MockCubeDefinition::builder() + .name("users".to_string()) + .sql_table("{database.schema.users}".to_string()) + .build(); + + let sql_table = cube.sql_table().unwrap().unwrap(); + assert_eq!(sql_table.args_names(), &vec!["database"]); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index 01b443c70c4cb..cfd66e06dc9c3 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -7,6 +7,7 @@ mod mock_case_item; mod mock_case_switch_definition; mod mock_case_switch_else_item; mod mock_case_switch_item; +mod mock_cube_definition; mod mock_dimension_definition; mod mock_geo_item; mod mock_measure_definition; From cd5d6312220def96859cc50ebeccb390d893347f Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 17:28:21 +0100 Subject: [PATCH 10/42] member expression mock --- .../cube_bridge/mock_expression_struct.rs | 123 ++++++++++++++++ .../mock_member_expression_definition.rs | 137 ++++++++++++++++++ .../src/test_fixtures/cube_bridge/mod.rs | 3 + 3 files changed, 263 insertions(+) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_expression_struct.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_expression_definition.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_expression_struct.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_expression_struct.rs new file mode 100644 index 0000000000000..d4038482fc064 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_expression_struct.rs @@ -0,0 +1,123 @@ +use crate::cube_bridge::member_expression::{ExpressionStruct, ExpressionStructStatic}; +use crate::cube_bridge::struct_with_sql_member::StructWithSqlMember; +use crate::impl_static_data; +use crate::test_fixtures::cube_bridge::MockStructWithSqlMember; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of ExpressionStruct for testing +#[derive(TypedBuilder)] +pub struct MockExpressionStruct { + // Fields from ExpressionStructStatic + expression_type: String, + #[builder(default)] + source_measure: Option, + #[builder(default)] + replace_aggregation_type: Option, + + // Optional trait fields + #[builder(default)] + add_filters: Option>>, +} + +impl_static_data!( + MockExpressionStruct, + ExpressionStructStatic, + expression_type, + source_measure, + replace_aggregation_type +); + +impl ExpressionStruct for MockExpressionStruct { + fn static_data(&self) -> &ExpressionStructStatic { + Box::leak(Box::new(Self::static_data(self))) + } + + fn has_add_filters(&self) -> Result { + Ok(self.add_filters.is_some()) + } + + fn add_filters(&self) -> Result>>, CubeError> { + match &self.add_filters { + Some(filters) => { + let result: Vec> = filters + .iter() + .map(|f| f.clone() as Rc) + .collect(); + Ok(Some(result)) + } + None => Ok(None), + } + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_expression_struct() { + let expr = MockExpressionStruct::builder() + .expression_type("aggregate".to_string()) + .build(); + + assert_eq!(expr.static_data().expression_type, "aggregate"); + assert!(!expr.has_add_filters().unwrap()); + } + + #[test] + fn test_expression_struct_with_source_measure() { + let expr = MockExpressionStruct::builder() + .expression_type("measure_reference".to_string()) + .source_measure(Some("users.count".to_string())) + .build(); + + let static_data = expr.static_data(); + assert_eq!(static_data.source_measure, Some("users.count".to_string())); + } + + #[test] + fn test_expression_struct_with_replace_aggregation() { + let expr = MockExpressionStruct::builder() + .expression_type("aggregate".to_string()) + .replace_aggregation_type(Some("avg".to_string())) + .build(); + + let static_data = expr.static_data(); + assert_eq!( + static_data.replace_aggregation_type, + Some("avg".to_string()) + ); + } + + #[test] + fn test_expression_struct_with_add_filters() { + let filters = vec![ + Rc::new( + MockStructWithSqlMember::builder() + .sql("{CUBE.status} = 'active'".to_string()) + .build(), + ), + Rc::new( + MockStructWithSqlMember::builder() + .sql("{CUBE.deleted} = false".to_string()) + .build(), + ), + ]; + + let expr = MockExpressionStruct::builder() + .expression_type("aggregate".to_string()) + .add_filters(Some(filters)) + .build(); + + assert!(expr.has_add_filters().unwrap()); + let result = expr.add_filters().unwrap().unwrap(); + assert_eq!(result.len(), 2); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_expression_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_expression_definition.rs new file mode 100644 index 0000000000000..fa433bbadcb34 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_expression_definition.rs @@ -0,0 +1,137 @@ +use crate::cube_bridge::member_expression::{ + MemberExpressionDefinition, MemberExpressionDefinitionStatic, MemberExpressionExpressionDef, +}; +use crate::impl_static_data; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of MemberExpressionDefinition for testing +#[derive(TypedBuilder)] +pub struct MockMemberExpressionDefinition { + // Fields from MemberExpressionDefinitionStatic + #[builder(default)] + expression_name: Option, + #[builder(default)] + name: Option, + #[builder(default)] + cube_name: Option, + #[builder(default)] + definition: Option, + + // Trait field + expression: MemberExpressionExpressionDef, +} + +impl_static_data!( + MockMemberExpressionDefinition, + MemberExpressionDefinitionStatic, + expression_name, + name, + cube_name, + definition +); + +impl MemberExpressionDefinition for MockMemberExpressionDefinition { + fn static_data(&self) -> &MemberExpressionDefinitionStatic { + Box::leak(Box::new(Self::static_data(self))) + } + + fn expression(&self) -> Result { + Ok(match &self.expression { + MemberExpressionExpressionDef::Sql(sql) => { + MemberExpressionExpressionDef::Sql(sql.clone()) + } + MemberExpressionExpressionDef::Struct(expr) => { + MemberExpressionExpressionDef::Struct(expr.clone()) + } + }) + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_fixtures::cube_bridge::{MockExpressionStruct, MockMemberSql}; + + #[test] + fn test_member_expression_with_sql() { + let sql = Rc::new(MockMemberSql::new("{CUBE.amount} * 2").unwrap()); + + let expr = MockMemberExpressionDefinition::builder() + .expression_name(Some("double_amount".to_string())) + .name(Some("doubleAmount".to_string())) + .cube_name(Some("orders".to_string())) + .expression(MemberExpressionExpressionDef::Sql( + sql as Rc, + )) + .build(); + + let static_data = expr.static_data(); + assert_eq!( + static_data.expression_name, + Some("double_amount".to_string()) + ); + assert_eq!(static_data.cube_name, Some("orders".to_string())); + + let result = expr.expression().unwrap(); + assert!(matches!(result, MemberExpressionExpressionDef::Sql(_))); + } + + #[test] + fn test_member_expression_with_struct() { + let expr_struct = Rc::new( + MockExpressionStruct::builder() + .expression_type("aggregate".to_string()) + .source_measure(Some("users.revenue".to_string())) + .build(), + ); + + let expr = MockMemberExpressionDefinition::builder() + .expression_name(Some("rolling_revenue".to_string())) + .name(Some("rollingRevenue".to_string())) + .cube_name(Some("users".to_string())) + .definition(Some("rolling revenue calculation".to_string())) + .expression(MemberExpressionExpressionDef::Struct( + expr_struct as Rc, + )) + .build(); + + let static_data = expr.static_data(); + assert_eq!( + static_data.expression_name, + Some("rolling_revenue".to_string()) + ); + assert_eq!( + static_data.definition, + Some("rolling revenue calculation".to_string()) + ); + + let result = expr.expression().unwrap(); + assert!(matches!( + result, + MemberExpressionExpressionDef::Struct(_) + )); + } + + #[test] + fn test_member_expression_minimal() { + let sql = Rc::new(MockMemberSql::new("{CUBE.field}").unwrap()); + + let expr = MockMemberExpressionDefinition::builder() + .expression(MemberExpressionExpressionDef::Sql( + sql as Rc, + )) + .build(); + + let static_data = expr.static_data(); + assert_eq!(static_data.expression_name, None); + assert_eq!(static_data.name, None); + assert_eq!(static_data.cube_name, None); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index cfd66e06dc9c3..442b0763e44d9 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -9,8 +9,10 @@ mod mock_case_switch_else_item; mod mock_case_switch_item; mod mock_cube_definition; mod mock_dimension_definition; +mod mock_expression_struct; mod mock_geo_item; mod mock_measure_definition; +mod mock_member_expression_definition; mod mock_member_order_by; mod mock_member_sql; mod mock_security_context; @@ -24,6 +26,7 @@ pub use mock_case_item::MockCaseItem; pub use mock_case_switch_definition::MockCaseSwitchDefinition; pub use mock_case_switch_else_item::MockCaseSwitchElseItem; pub use mock_case_switch_item::MockCaseSwitchItem; +pub use mock_expression_struct::MockExpressionStruct; pub use mock_geo_item::MockGeoItem; pub use mock_member_order_by::MockMemberOrderBy; pub use mock_member_sql::MockMemberSql; From faa76ae431c7fdfcba6da2d8f3fbfce30110040e Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 17:57:53 +0100 Subject: [PATCH 11/42] join mock --- .../cube_bridge/mock_join_definition.rs | 207 ++++++++++++++++++ .../cube_bridge/mock_join_item.rs | 97 ++++++++ .../cube_bridge/mock_join_item_definition.rs | 71 ++++++ .../src/test_fixtures/cube_bridge/mod.rs | 5 + 4 files changed, 380 insertions(+) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_definition.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item_definition.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_definition.rs new file mode 100644 index 0000000000000..6f878f043d609 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_definition.rs @@ -0,0 +1,207 @@ +use crate::cube_bridge::join_definition::{JoinDefinition, JoinDefinitionStatic}; +use crate::cube_bridge::join_item::JoinItem; +use crate::impl_static_data; +use crate::test_fixtures::cube_bridge::MockJoinItem; +use cubenativeutils::CubeError; +use std::any::Any; +use std::collections::HashMap; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of JoinDefinition for testing +#[derive(TypedBuilder)] +pub struct MockJoinDefinition { + // Fields from JoinDefinitionStatic + root: String, + #[builder(default)] + multiplication_factor: HashMap, + + // Trait field + joins: Vec>, +} + +impl_static_data!( + MockJoinDefinition, + JoinDefinitionStatic, + root, + multiplication_factor +); + +impl JoinDefinition for MockJoinDefinition { + fn static_data(&self) -> &JoinDefinitionStatic { + Box::leak(Box::new(Self::static_data(self))) + } + + fn joins(&self) -> Result>, CubeError> { + Ok(self + .joins + .iter() + .map(|j| j.clone() as Rc) + .collect()) + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_fixtures::cube_bridge::MockJoinItemDefinition; + + #[test] + fn test_basic_join_definition() { + let join_item = Rc::new( + MockJoinItem::builder() + .from("orders".to_string()) + .to("users".to_string()) + .original_from("Orders".to_string()) + .original_to("Users".to_string()) + .join(Rc::new( + MockJoinItemDefinition::builder() + .relationship("many_to_one".to_string()) + .sql("{orders.user_id} = {users.id}".to_string()) + .build(), + )) + .build(), + ); + + let join_def = MockJoinDefinition::builder() + .root("orders".to_string()) + .joins(vec![join_item]) + .build(); + + assert_eq!(join_def.static_data().root, "orders"); + let joins = join_def.joins().unwrap(); + assert_eq!(joins.len(), 1); + } + + #[test] + fn test_join_definition_with_multiplication_factor() { + let mut mult_factor = HashMap::new(); + mult_factor.insert("orders".to_string(), true); + mult_factor.insert("users".to_string(), false); + + let join_def = MockJoinDefinition::builder() + .root("orders".to_string()) + .multiplication_factor(mult_factor.clone()) + .joins(vec![]) + .build(); + + assert_eq!(join_def.static_data().multiplication_factor, mult_factor); + } + + #[test] + fn test_join_definition_with_multiple_joins() { + let join_to_users = Rc::new( + MockJoinItem::builder() + .from("orders".to_string()) + .to("users".to_string()) + .original_from("Orders".to_string()) + .original_to("Users".to_string()) + .join(Rc::new( + MockJoinItemDefinition::builder() + .relationship("many_to_one".to_string()) + .sql("{orders.user_id} = {users.id}".to_string()) + .build(), + )) + .build(), + ); + + let join_to_products = Rc::new( + MockJoinItem::builder() + .from("orders".to_string()) + .to("products".to_string()) + .original_from("Orders".to_string()) + .original_to("Products".to_string()) + .join(Rc::new( + MockJoinItemDefinition::builder() + .relationship("many_to_one".to_string()) + .sql("{orders.product_id} = {products.id}".to_string()) + .build(), + )) + .build(), + ); + + let join_def = MockJoinDefinition::builder() + .root("orders".to_string()) + .joins(vec![join_to_users, join_to_products]) + .build(); + + let joins = join_def.joins().unwrap(); + assert_eq!(joins.len(), 2); + assert_eq!(joins[0].static_data().to, "users"); + assert_eq!(joins[1].static_data().to, "products"); + } + + #[test] + fn test_complex_join_graph() { + // Orders -> Users + let join_orders_users = Rc::new( + MockJoinItem::builder() + .from("orders".to_string()) + .to("users".to_string()) + .original_from("Orders".to_string()) + .original_to("Users".to_string()) + .join(Rc::new( + MockJoinItemDefinition::builder() + .relationship("many_to_one".to_string()) + .sql("{orders.user_id} = {users.id}".to_string()) + .build(), + )) + .build(), + ); + + // Users -> Countries + let join_users_countries = Rc::new( + MockJoinItem::builder() + .from("users".to_string()) + .to("countries".to_string()) + .original_from("Users".to_string()) + .original_to("Countries".to_string()) + .join(Rc::new( + MockJoinItemDefinition::builder() + .relationship("many_to_one".to_string()) + .sql("{users.country_id} = {countries.id}".to_string()) + .build(), + )) + .build(), + ); + + // Orders -> Products + let join_orders_products = Rc::new( + MockJoinItem::builder() + .from("orders".to_string()) + .to("products".to_string()) + .original_from("Orders".to_string()) + .original_to("Products".to_string()) + .join(Rc::new( + MockJoinItemDefinition::builder() + .relationship("many_to_many".to_string()) + .sql("{orders.id} = {order_items.order_id} AND {order_items.product_id} = {products.id}".to_string()) + .build(), + )) + .build(), + ); + + let mut mult_factor = HashMap::new(); + mult_factor.insert("products".to_string(), true); + + let join_def = MockJoinDefinition::builder() + .root("orders".to_string()) + .joins(vec![join_orders_users, join_users_countries, join_orders_products]) + .multiplication_factor(mult_factor) + .build(); + + let static_data = join_def.static_data(); + assert_eq!(static_data.root, "orders"); + assert_eq!( + static_data.multiplication_factor.get("products"), + Some(&true) + ); + + let joins = join_def.joins().unwrap(); + assert_eq!(joins.len(), 3); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item.rs new file mode 100644 index 0000000000000..35a23e7bd8967 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item.rs @@ -0,0 +1,97 @@ +use crate::cube_bridge::join_item::{JoinItem, JoinItemStatic}; +use crate::cube_bridge::join_item_definition::JoinItemDefinition; +use crate::impl_static_data; +use crate::test_fixtures::cube_bridge::MockJoinItemDefinition; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of JoinItem for testing +#[derive(TypedBuilder)] +pub struct MockJoinItem { + // Fields from JoinItemStatic + from: String, + to: String, + original_from: String, + original_to: String, + + // Trait field + join: Rc, +} + +impl_static_data!( + MockJoinItem, + JoinItemStatic, + from, + to, + original_from, + original_to +); + +impl JoinItem for MockJoinItem { + fn static_data(&self) -> &JoinItemStatic { + Box::leak(Box::new(Self::static_data(self))) + } + + fn join(&self) -> Result, CubeError> { + Ok(self.join.clone() as Rc) + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_join_item() { + let join_def = Rc::new( + MockJoinItemDefinition::builder() + .relationship("many_to_one".to_string()) + .sql("{orders.user_id} = {users.id}".to_string()) + .build(), + ); + + let join_item = MockJoinItem::builder() + .from("orders".to_string()) + .to("users".to_string()) + .original_from("Orders".to_string()) + .original_to("Users".to_string()) + .join(join_def) + .build(); + + let static_data = join_item.static_data(); + assert_eq!(static_data.from, "orders"); + assert_eq!(static_data.to, "users"); + assert_eq!(static_data.original_from, "Orders"); + assert_eq!(static_data.original_to, "Users"); + + let join = join_item.join().unwrap(); + assert_eq!(join.static_data().relationship, "many_to_one"); + } + + #[test] + fn test_join_item_with_aliases() { + let join_def = Rc::new( + MockJoinItemDefinition::builder() + .relationship("one_to_many".to_string()) + .sql("{u.id} = {o.user_id}".to_string()) + .build(), + ); + + let join_item = MockJoinItem::builder() + .from("u".to_string()) + .to("o".to_string()) + .original_from("users".to_string()) + .original_to("orders".to_string()) + .join(join_def) + .build(); + + assert_eq!(join_item.static_data().from, "u"); + assert_eq!(join_item.static_data().original_from, "users"); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item_definition.rs new file mode 100644 index 0000000000000..a0fe1de88e2fd --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item_definition.rs @@ -0,0 +1,71 @@ +use crate::cube_bridge::join_item_definition::{JoinItemDefinition, JoinItemDefinitionStatic}; +use crate::cube_bridge::member_sql::MemberSql; +use crate::impl_static_data; +use crate::test_fixtures::cube_bridge::MockMemberSql; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of JoinItemDefinition for testing +#[derive(TypedBuilder)] +pub struct MockJoinItemDefinition { + // Fields from JoinItemDefinitionStatic + relationship: String, + + // Trait field + sql: String, +} + +impl_static_data!(MockJoinItemDefinition, JoinItemDefinitionStatic, relationship); + +impl JoinItemDefinition for MockJoinItemDefinition { + fn static_data(&self) -> &JoinItemDefinitionStatic { + Box::leak(Box::new(Self::static_data(self))) + } + + fn sql(&self) -> Result, CubeError> { + Ok(Rc::new(MockMemberSql::new(&self.sql)?)) + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_many_to_one_join() { + let join_def = MockJoinItemDefinition::builder() + .relationship("many_to_one".to_string()) + .sql("{CUBE.user_id} = {users.id}".to_string()) + .build(); + + assert_eq!(join_def.static_data().relationship, "many_to_one"); + let sql = join_def.sql().unwrap(); + assert_eq!(sql.args_names(), &vec!["CUBE", "users"]); + } + + #[test] + fn test_one_to_many_join() { + let join_def = MockJoinItemDefinition::builder() + .relationship("one_to_many".to_string()) + .sql("{CUBE.id} = {orders.user_id}".to_string()) + .build(); + + assert_eq!(join_def.static_data().relationship, "one_to_many"); + } + + #[test] + fn test_one_to_one_join() { + let join_def = MockJoinItemDefinition::builder() + .relationship("one_to_one".to_string()) + .sql("{CUBE.id} = {profile.user_id}".to_string()) + .build(); + + assert_eq!(join_def.static_data().relationship, "one_to_one"); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index 442b0763e44d9..dc0f022280822 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -11,6 +11,9 @@ mod mock_cube_definition; mod mock_dimension_definition; mod mock_expression_struct; mod mock_geo_item; +mod mock_join_definition; +mod mock_join_item; +mod mock_join_item_definition; mod mock_measure_definition; mod mock_member_expression_definition; mod mock_member_order_by; @@ -28,6 +31,8 @@ pub use mock_case_switch_else_item::MockCaseSwitchElseItem; pub use mock_case_switch_item::MockCaseSwitchItem; pub use mock_expression_struct::MockExpressionStruct; pub use mock_geo_item::MockGeoItem; +pub use mock_join_item::MockJoinItem; +pub use mock_join_item_definition::MockJoinItemDefinition; pub use mock_member_order_by::MockMemberOrderBy; pub use mock_member_sql::MockMemberSql; pub use mock_security_context::MockSecurityContext; From cd4e55be0c292465bb439cc209f160013ba11663 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 18:11:05 +0100 Subject: [PATCH 12/42] schema mock --- .../test_fixtures/cube_bridge/mock_schema.rs | 384 ++++++++++++++++++ .../src/test_fixtures/cube_bridge/mod.rs | 4 + 2 files changed, 388 insertions(+) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs new file mode 100644 index 0000000000000..0a39623bae4fd --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs @@ -0,0 +1,384 @@ +use crate::test_fixtures::cube_bridge::{ + MockCubeDefinition, MockDimensionDefinition, MockMeasureDefinition, +}; +use std::collections::HashMap; +use std::rc::Rc; + +/// Mock schema containing cubes with their measures and dimensions +pub struct MockSchema { + cubes: HashMap, +} + +/// Single cube with its definition and members +pub struct MockCube { + pub definition: MockCubeDefinition, + pub measures: HashMap>, + pub dimensions: HashMap>, +} + +impl MockSchema { + /// Get cube by name + pub fn get_cube(&self, name: &str) -> Option<&MockCube> { + self.cubes.get(name) + } + + /// Get dimension by cube name and dimension name + pub fn get_dimension( + &self, + cube_name: &str, + dimension_name: &str, + ) -> Option> { + self.cubes + .get(cube_name) + .and_then(|cube| cube.dimensions.get(dimension_name).cloned()) + } + + /// Get measure by cube name and measure name + pub fn get_measure( + &self, + cube_name: &str, + measure_name: &str, + ) -> Option> { + self.cubes + .get(cube_name) + .and_then(|cube| cube.measures.get(measure_name).cloned()) + } + + /// Get all cube names + pub fn cube_names(&self) -> Vec<&String> { + self.cubes.keys().collect() + } +} + +/// Builder for MockSchema with fluent API +pub struct MockSchemaBuilder { + cubes: HashMap, +} + +impl MockSchemaBuilder { + /// Create a new schema builder + pub fn new() -> Self { + Self { + cubes: HashMap::new(), + } + } + + /// Add a cube and return a cube builder + pub fn add_cube(self, name: impl Into) -> MockCubeBuilder { + MockCubeBuilder { + schema_builder: self, + cube_name: name.into(), + cube_definition: None, + measures: HashMap::new(), + dimensions: HashMap::new(), + } + } + + /// Build the final schema + pub fn build(self) -> MockSchema { + MockSchema { cubes: self.cubes } + } +} + +impl Default for MockSchemaBuilder { + fn default() -> Self { + Self::new() + } +} + +/// Builder for a single cube within a schema +pub struct MockCubeBuilder { + schema_builder: MockSchemaBuilder, + cube_name: String, + cube_definition: Option, + measures: HashMap>, + dimensions: HashMap>, +} + +impl MockCubeBuilder { + /// Set the cube definition + pub fn cube_def(mut self, definition: MockCubeDefinition) -> Self { + self.cube_definition = Some(definition); + self + } + + /// Add a dimension to the cube + pub fn add_dimension( + mut self, + name: impl Into, + definition: MockDimensionDefinition, + ) -> Self { + self.dimensions + .insert(name.into(), Rc::new(definition)); + self + } + + /// Add a measure to the cube + pub fn add_measure( + mut self, + name: impl Into, + definition: MockMeasureDefinition, + ) -> Self { + self.measures.insert(name.into(), Rc::new(definition)); + self + } + + /// Finish building this cube and return to schema builder + pub fn finish_cube(mut self) -> MockSchemaBuilder { + let cube_def = self.cube_definition.unwrap_or_else(|| { + // Create default cube definition with the cube name + MockCubeDefinition::builder() + .name(self.cube_name.clone()) + .sql_table(format!("public.{}", self.cube_name)) + .build() + }); + + let cube = MockCube { + definition: cube_def, + measures: self.measures, + dimensions: self.dimensions, + }; + + self.schema_builder.cubes.insert(self.cube_name, cube); + self.schema_builder + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_schema() { + let schema = MockSchemaBuilder::new() + .add_cube("users") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_dimension( + "name", + MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("name".to_string()) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .build(), + ) + .finish_cube() + .build(); + + assert!(schema.get_cube("users").is_some()); + assert!(schema.get_dimension("users", "id").is_some()); + assert!(schema.get_dimension("users", "name").is_some()); + assert!(schema.get_measure("users", "count").is_some()); + } + + #[test] + fn test_multiple_cubes() { + let schema = MockSchemaBuilder::new() + .add_cube("users") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .finish_cube() + .add_cube("orders") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_measure( + "total", + MockMeasureDefinition::builder() + .measure_type("sum".to_string()) + .sql("amount".to_string()) + .build(), + ) + .finish_cube() + .build(); + + assert_eq!(schema.cube_names().len(), 2); + assert!(schema.get_cube("users").is_some()); + assert!(schema.get_cube("orders").is_some()); + assert!(schema.get_dimension("orders", "id").is_some()); + assert!(schema.get_measure("orders", "total").is_some()); + } + + #[test] + fn test_cube_with_custom_definition() { + let schema = MockSchemaBuilder::new() + .add_cube("users") + .cube_def( + MockCubeDefinition::builder() + .name("users".to_string()) + .sql_table("public.app_users".to_string()) + .sql_alias(Some("u".to_string())) + .build(), + ) + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .finish_cube() + .build(); + + let cube = schema.get_cube("users").unwrap(); + assert_eq!(cube.definition.static_data().name, "users"); + assert_eq!( + cube.definition.static_data().sql_alias, + Some("u".to_string()) + ); + } + + #[test] + fn test_schema_lookups() { + let schema = MockSchemaBuilder::new() + .add_cube("users") + .add_dimension( + "visitor_id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("visitor_id".to_string()) + .build(), + ) + .add_dimension( + "source", + MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("source".to_string()) + .build(), + ) + .add_dimension( + "created_at", + MockDimensionDefinition::builder() + .dimension_type("time".to_string()) + .sql("created_at".to_string()) + .build(), + ) + .finish_cube() + .build(); + + // Test dimension lookups + let visitor_id = schema.get_dimension("users", "visitor_id").unwrap(); + assert_eq!(visitor_id.static_data().dimension_type, "number"); + + let source = schema.get_dimension("users", "source").unwrap(); + assert_eq!(source.static_data().dimension_type, "string"); + + let created_at = schema.get_dimension("users", "created_at").unwrap(); + assert_eq!(created_at.static_data().dimension_type, "time"); + + // Test missing dimension + assert!(schema.get_dimension("users", "nonexistent").is_none()); + assert!(schema.get_dimension("nonexistent_cube", "id").is_none()); + } + + #[test] + fn test_complex_schema() { + let schema = MockSchemaBuilder::new() + .add_cube("users") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_dimension( + "name", + MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("name".to_string()) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .build(), + ) + .finish_cube() + .add_cube("orders") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_dimension( + "user_id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("user_id".to_string()) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .build(), + ) + .add_measure( + "total_amount", + MockMeasureDefinition::builder() + .measure_type("sum".to_string()) + .sql("amount".to_string()) + .build(), + ) + .finish_cube() + .add_cube("cards") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .build(), + ) + .finish_cube() + .build(); + + // Verify all cubes exist + assert_eq!(schema.cube_names().len(), 3); + assert!(schema.get_cube("users").is_some()); + assert!(schema.get_cube("orders").is_some()); + assert!(schema.get_cube("cards").is_some()); + + // Verify measures across cubes + assert!(schema.get_measure("users", "count").is_some()); + assert!(schema.get_measure("orders", "count").is_some()); + assert!(schema.get_measure("orders", "total_amount").is_some()); + assert!(schema.get_measure("cards", "count").is_some()); + + // Verify dimensions + assert!(schema.get_dimension("users", "name").is_some()); + assert!(schema.get_dimension("orders", "user_id").is_some()); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index dc0f022280822..9292db03b9512 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -18,6 +18,7 @@ mod mock_measure_definition; mod mock_member_expression_definition; mod mock_member_order_by; mod mock_member_sql; +mod mock_schema; mod mock_security_context; mod mock_sql_utils; mod mock_struct_with_sql_member; @@ -29,10 +30,13 @@ pub use mock_case_item::MockCaseItem; pub use mock_case_switch_definition::MockCaseSwitchDefinition; pub use mock_case_switch_else_item::MockCaseSwitchElseItem; pub use mock_case_switch_item::MockCaseSwitchItem; +pub use mock_cube_definition::MockCubeDefinition; +pub use mock_dimension_definition::MockDimensionDefinition; pub use mock_expression_struct::MockExpressionStruct; pub use mock_geo_item::MockGeoItem; pub use mock_join_item::MockJoinItem; pub use mock_join_item_definition::MockJoinItemDefinition; +pub use mock_measure_definition::MockMeasureDefinition; pub use mock_member_order_by::MockMemberOrderBy; pub use mock_member_sql::MockMemberSql; pub use mock_security_context::MockSecurityContext; From 91d41254edab78a201463a62f2d6e58a3f1f06d7 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 18:31:27 +0100 Subject: [PATCH 13/42] in work --- .../src/cube_bridge/evaluator.rs | 2 - .../src/test_fixtures/cube_bridge/macros.rs | 95 +++++++++++++++++-- .../cube_bridge/mock_case_switch_item.rs | 4 +- .../cube_bridge/mock_cube_definition.rs | 4 +- .../cube_bridge/mock_dimension_definition.rs | 4 +- .../cube_bridge/mock_expression_struct.rs | 4 +- .../cube_bridge/mock_join_definition.rs | 4 +- .../cube_bridge/mock_join_item.rs | 4 +- .../cube_bridge/mock_join_item_definition.rs | 10 +- .../cube_bridge/mock_measure_definition.rs | 4 +- .../mock_member_expression_definition.rs | 4 +- .../cube_bridge/mock_timeshift_definition.rs | 6 +- 12 files changed, 104 insertions(+), 41 deletions(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/evaluator.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/evaluator.rs index 21e43b82a88bb..40a3429d9852c 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/evaluator.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/evaluator.rs @@ -35,8 +35,6 @@ pub struct CallDep { #[nativebridge::native_bridge(CubeEvaluatorStatic)] pub trait CubeEvaluator { - #[nbridge(field)] - fn primary_keys(&self) -> Result, CubeError>; fn parse_path(&self, path_type: String, path: String) -> Result, CubeError>; fn measure_by_path(&self, measure_path: String) -> Result, CubeError>; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/macros.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/macros.rs index 6bfd6f72451d9..eba9075b7750f 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/macros.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/macros.rs @@ -1,21 +1,36 @@ -/// Macro to implement static_data() method for mock bridge types +/// Macro to implement static_data() helper method for mock bridge types +/// +/// This macro generates a helper method that returns an owned StaticData struct. +/// The helper is used by the trait's static_data() method which applies Box::leak. /// /// # Usage /// ```ignore /// impl_static_data!( -/// MockDimensionDefinition, -/// DimensionDefinitionStatic, -/// dimension_type, +/// MockDimensionDefinition, // The mock type +/// DimensionDefinitionStatic, // The static data type +/// dimension_type, // Fields to include /// owned_by_cube, /// multi_stage /// ); /// ``` /// -/// This generates a method that creates StaticData struct on the fly from struct fields +/// # Generated Code +/// ```ignore +/// impl MockDimensionDefinition { +/// pub fn static_data(&self) -> DimensionDefinitionStatic { +/// DimensionDefinitionStatic { +/// dimension_type: self.dimension_type.clone(), +/// owned_by_cube: self.owned_by_cube.clone(), +/// multi_stage: self.multi_stage.clone(), +/// } +/// } +/// } +/// ``` #[macro_export] macro_rules! impl_static_data { // Pattern: impl_static_data!(MockType, StaticType, field1, field2, ...) ($mock_type:ty, $static_type:path, $($field:ident),* $(,)?) => { + // Helper method that returns owned StaticData impl $mock_type { pub fn static_data(&self) -> $static_type { $static_type { @@ -26,6 +41,53 @@ macro_rules! impl_static_data { }; } +/// Macro to implement the trait's static_data() method using Box::leak +/// +/// This macro should be used INSIDE the trait implementation block to generate +/// the static_data() method that returns &'static references. +/// +/// # Memory Leak Explanation +/// This macro uses `Box::leak(Box::new(...))` to convert owned values into static +/// references. This intentionally leaks memory, which is acceptable because: +/// - Mock objects are only used in tests with short lifetimes +/// - Tests typically create a small number of mock objects +/// - The leaked memory is minimal and reclaimed when the test process exits +/// - This approach significantly simplifies test code by avoiding complex lifetime management +/// +/// # Usage +/// ```ignore +/// impl DimensionDefinition for MockDimensionDefinition { +/// impl_static_data_method!(DimensionDefinitionStatic); +/// +/// fn sql(&self) -> Result>, CubeError> { +/// // ... other trait methods +/// } +/// } +/// ``` +/// +/// # Generated Code +/// ```ignore +/// fn static_data(&self) -> &DimensionDefinitionStatic { +/// // Intentional memory leak - acceptable for test mocks +/// // The Box::leak pattern converts the owned value to a static reference +/// Box::leak(Box::new(Self::static_data(self))) +/// } +/// ``` +#[macro_export] +macro_rules! impl_static_data_method { + ($static_type:path) => { + fn static_data(&self) -> &$static_type { + // Intentional memory leak for test mocks - see macro documentation + // This converts the owned StaticData from the helper method into a &'static reference + // required by the trait. The leak is acceptable because: + // 1. Test mocks have short lifetimes (duration of test) + // 2. Small number of instances created + // 3. Memory reclaimed when test process exits + Box::leak(Box::new(Self::static_data(self))) + } + }; +} + #[cfg(test)] mod tests { use serde::{Deserialize, Serialize}; @@ -37,6 +99,10 @@ mod tests { pub value: Option, } + pub trait TestTrait { + fn static_data(&self) -> &TestStatic; + } + #[derive(TypedBuilder)] pub struct MockTest { #[builder(default = "test".to_string())] @@ -47,8 +113,12 @@ mod tests { impl_static_data!(MockTest, TestStatic, name, value); + impl TestTrait for MockTest { + impl_static_data_method!(TestStatic); + } + #[test] - fn test_static_data_macro() { + fn test_static_data_helper_method() { let mock = MockTest::builder() .name("hello".to_string()) .value(Some(42)) @@ -59,6 +129,19 @@ mod tests { assert_eq!(static_data.value, Some(42)); } + #[test] + fn test_static_data_trait_method() { + let mock = MockTest::builder() + .name("world".to_string()) + .value(Some(123)) + .build(); + + // Call trait method + let static_data: &TestStatic = TestTrait::static_data(&mock); + assert_eq!(static_data.name, "world"); + assert_eq!(static_data.value, Some(123)); + } + #[test] fn test_static_data_macro_with_defaults() { let mock = MockTest::builder().build(); diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_item.rs index 792999239210b..ad948a4fe0f0b 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_item.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_item.rs @@ -17,9 +17,7 @@ pub struct MockCaseSwitchItem { impl_static_data!(MockCaseSwitchItem, CaseSwitchItemStatic, value); impl CaseSwitchItem for MockCaseSwitchItem { - fn static_data(&self) -> &CaseSwitchItemStatic { - Box::leak(Box::new(Self::static_data(self))) - } + crate::impl_static_data_method!(CaseSwitchItemStatic); fn sql(&self) -> Result, CubeError> { Ok(Rc::new(MockMemberSql::new(&self.sql)?)) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_cube_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_cube_definition.rs index 4e16c6d4ea89e..3b409261c0d43 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_cube_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_cube_definition.rs @@ -39,9 +39,7 @@ impl_static_data!( ); impl CubeDefinition for MockCubeDefinition { - fn static_data(&self) -> &CubeDefinitionStatic { - Box::leak(Box::new(Self::static_data(self))) - } + crate::impl_static_data_method!(CubeDefinitionStatic); fn has_sql_table(&self) -> Result { Ok(self.sql_table.is_some()) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs index 7531ec08d4f90..7a10e067d0bd6 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs @@ -55,9 +55,7 @@ impl_static_data!( ); impl DimensionDefinition for MockDimensionDefinition { - fn static_data(&self) -> &DimensionDefinitionStatic { - Box::leak(Box::new(Self::static_data(self))) - } + crate::impl_static_data_method!(DimensionDefinitionStatic); fn has_sql(&self) -> Result { Ok(self.sql.is_some()) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_expression_struct.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_expression_struct.rs index d4038482fc064..d14844163cadd 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_expression_struct.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_expression_struct.rs @@ -31,9 +31,7 @@ impl_static_data!( ); impl ExpressionStruct for MockExpressionStruct { - fn static_data(&self) -> &ExpressionStructStatic { - Box::leak(Box::new(Self::static_data(self))) - } + crate::impl_static_data_method!(ExpressionStructStatic); fn has_add_filters(&self) -> Result { Ok(self.add_filters.is_some()) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_definition.rs index 6f878f043d609..a754ee1e8033a 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_definition.rs @@ -28,9 +28,7 @@ impl_static_data!( ); impl JoinDefinition for MockJoinDefinition { - fn static_data(&self) -> &JoinDefinitionStatic { - Box::leak(Box::new(Self::static_data(self))) - } + crate::impl_static_data_method!(JoinDefinitionStatic); fn joins(&self) -> Result>, CubeError> { Ok(self diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item.rs index 35a23e7bd8967..9c1c0f91f8843 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item.rs @@ -30,9 +30,7 @@ impl_static_data!( ); impl JoinItem for MockJoinItem { - fn static_data(&self) -> &JoinItemStatic { - Box::leak(Box::new(Self::static_data(self))) - } + crate::impl_static_data_method!(JoinItemStatic); fn join(&self) -> Result, CubeError> { Ok(self.join.clone() as Rc) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item_definition.rs index a0fe1de88e2fd..271ca6201d07d 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item_definition.rs @@ -17,12 +17,14 @@ pub struct MockJoinItemDefinition { sql: String, } -impl_static_data!(MockJoinItemDefinition, JoinItemDefinitionStatic, relationship); +impl_static_data!( + MockJoinItemDefinition, + JoinItemDefinitionStatic, + relationship +); impl JoinItemDefinition for MockJoinItemDefinition { - fn static_data(&self) -> &JoinItemDefinitionStatic { - Box::leak(Box::new(Self::static_data(self))) - } + crate::impl_static_data_method!(JoinItemDefinitionStatic); fn sql(&self) -> Result, CubeError> { Ok(Rc::new(MockMemberSql::new(&self.sql)?)) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_measure_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_measure_definition.rs index 3884bbe9240ed..1d04019d61de7 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_measure_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_measure_definition.rs @@ -62,9 +62,7 @@ impl_static_data!( ); impl MeasureDefinition for MockMeasureDefinition { - fn static_data(&self) -> &MeasureDefinitionStatic { - Box::leak(Box::new(Self::static_data(self))) - } + crate::impl_static_data_method!(MeasureDefinitionStatic); fn has_sql(&self) -> Result { Ok(self.sql.is_some()) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_expression_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_expression_definition.rs index fa433bbadcb34..cee57156ec409 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_expression_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_expression_definition.rs @@ -34,9 +34,7 @@ impl_static_data!( ); impl MemberExpressionDefinition for MockMemberExpressionDefinition { - fn static_data(&self) -> &MemberExpressionDefinitionStatic { - Box::leak(Box::new(Self::static_data(self))) - } + crate::impl_static_data_method!(MemberExpressionDefinitionStatic); fn expression(&self) -> Result { Ok(match &self.expression { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_timeshift_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_timeshift_definition.rs index 0d7582d4e70a5..9e872876d25c3 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_timeshift_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_timeshift_definition.rs @@ -29,11 +29,7 @@ impl_static_data!( ); impl TimeShiftDefinition for MockTimeShiftDefinition { - fn static_data(&self) -> &TimeShiftDefinitionStatic { - // Store static data in a thread-local or use lazy_static - // For now, we'll use a simpler approach - Box::leak for tests - Box::leak(Box::new(Self::static_data(self))) - } + crate::impl_static_data_method!(TimeShiftDefinitionStatic); fn has_sql(&self) -> Result { Ok(self.sql.is_some()) From 7cfacdefc01ab896b5d954330982fc8c92e78b11 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 18:37:24 +0100 Subject: [PATCH 14/42] segment definition --- .../test_fixtures/cube_bridge/mock_schema.rs | 153 +++++++++++++++++- .../cube_bridge/mock_segment_definition.rs | 119 ++++++++++++++ .../src/test_fixtures/cube_bridge/mod.rs | 2 + 3 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_segment_definition.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs index 0a39623bae4fd..313be9a61249e 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs @@ -1,5 +1,5 @@ use crate::test_fixtures::cube_bridge::{ - MockCubeDefinition, MockDimensionDefinition, MockMeasureDefinition, + MockCubeDefinition, MockDimensionDefinition, MockMeasureDefinition, MockSegmentDefinition, }; use std::collections::HashMap; use std::rc::Rc; @@ -14,6 +14,7 @@ pub struct MockCube { pub definition: MockCubeDefinition, pub measures: HashMap>, pub dimensions: HashMap>, + pub segments: HashMap>, } impl MockSchema { @@ -44,6 +45,17 @@ impl MockSchema { .and_then(|cube| cube.measures.get(measure_name).cloned()) } + /// Get segment by cube name and segment name + pub fn get_segment( + &self, + cube_name: &str, + segment_name: &str, + ) -> Option> { + self.cubes + .get(cube_name) + .and_then(|cube| cube.segments.get(segment_name).cloned()) + } + /// Get all cube names pub fn cube_names(&self) -> Vec<&String> { self.cubes.keys().collect() @@ -71,6 +83,7 @@ impl MockSchemaBuilder { cube_definition: None, measures: HashMap::new(), dimensions: HashMap::new(), + segments: HashMap::new(), } } @@ -93,6 +106,7 @@ pub struct MockCubeBuilder { cube_definition: Option, measures: HashMap>, dimensions: HashMap>, + segments: HashMap>, } impl MockCubeBuilder { @@ -123,6 +137,16 @@ impl MockCubeBuilder { self } + /// Add a segment to the cube + pub fn add_segment( + mut self, + name: impl Into, + definition: MockSegmentDefinition, + ) -> Self { + self.segments.insert(name.into(), Rc::new(definition)); + self + } + /// Finish building this cube and return to schema builder pub fn finish_cube(mut self) -> MockSchemaBuilder { let cube_def = self.cube_definition.unwrap_or_else(|| { @@ -137,6 +161,7 @@ impl MockCubeBuilder { definition: cube_def, measures: self.measures, dimensions: self.dimensions, + segments: self.segments, }; self.schema_builder.cubes.insert(self.cube_name, cube); @@ -147,6 +172,7 @@ impl MockCubeBuilder { #[cfg(test)] mod tests { use super::*; + use crate::cube_bridge::segment_definition::SegmentDefinition; #[test] fn test_basic_schema() { @@ -381,4 +407,129 @@ mod tests { assert!(schema.get_dimension("users", "name").is_some()); assert!(schema.get_dimension("orders", "user_id").is_some()); } + + #[test] + fn test_schema_with_segments() { + use crate::test_fixtures::cube_bridge::MockSegmentDefinition; + + let schema = MockSchemaBuilder::new() + .add_cube("users") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .build(), + ) + .add_segment( + "active", + MockSegmentDefinition::builder() + .sql("{CUBE.status} = 'active'".to_string()) + .build(), + ) + .add_segment( + "premium", + MockSegmentDefinition::builder() + .sql("{CUBE.is_premium} = true".to_string()) + .segment_type(Some("filter".to_string())) + .build(), + ) + .finish_cube() + .build(); + + // Verify cube exists + assert!(schema.get_cube("users").is_some()); + + // Verify segments + let active_segment = schema.get_segment("users", "active").unwrap(); + let sql = active_segment.sql().unwrap(); + assert_eq!(sql.args_names(), &vec!["CUBE"]); + + let premium_segment = schema.get_segment("users", "premium").unwrap(); + assert_eq!( + premium_segment.static_data().segment_type, + Some("filter".to_string()) + ); + + // Verify missing segment + assert!(schema.get_segment("users", "nonexistent").is_none()); + assert!(schema.get_segment("nonexistent_cube", "active").is_none()); + } + + #[test] + fn test_complete_schema_with_all_members() { + use crate::test_fixtures::cube_bridge::MockSegmentDefinition; + + let schema = MockSchemaBuilder::new() + .add_cube("orders") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_dimension( + "status", + MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("status".to_string()) + .build(), + ) + .add_dimension( + "created_at", + MockDimensionDefinition::builder() + .dimension_type("time".to_string()) + .sql("created_at".to_string()) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .build(), + ) + .add_measure( + "total_amount", + MockMeasureDefinition::builder() + .measure_type("sum".to_string()) + .sql("amount".to_string()) + .build(), + ) + .add_segment( + "completed", + MockSegmentDefinition::builder() + .sql("{CUBE.status} = 'completed'".to_string()) + .build(), + ) + .add_segment( + "high_value", + MockSegmentDefinition::builder() + .sql("{CUBE.amount} > 1000".to_string()) + .build(), + ) + .finish_cube() + .build(); + + let cube = schema.get_cube("orders").unwrap(); + + // Verify all member types exist + assert_eq!(cube.dimensions.len(), 3); + assert_eq!(cube.measures.len(), 2); + assert_eq!(cube.segments.len(), 2); + + // Verify lookups work for all member types + assert!(schema.get_dimension("orders", "status").is_some()); + assert!(schema.get_measure("orders", "count").is_some()); + assert!(schema.get_segment("orders", "completed").is_some()); + assert!(schema.get_segment("orders", "high_value").is_some()); + } } \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_segment_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_segment_definition.rs new file mode 100644 index 0000000000000..0b067aa3c17e6 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_segment_definition.rs @@ -0,0 +1,119 @@ +use crate::cube_bridge::member_sql::MemberSql; +use crate::cube_bridge::segment_definition::{SegmentDefinition, SegmentDefinitionStatic}; +use crate::impl_static_data; +use crate::test_fixtures::cube_bridge::MockMemberSql; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of SegmentDefinition for testing +#[derive(TypedBuilder)] +pub struct MockSegmentDefinition { + // Fields from SegmentDefinitionStatic + #[builder(default)] + segment_type: Option, + #[builder(default)] + owned_by_cube: Option, + + // Trait field + sql: String, +} + +impl_static_data!( + MockSegmentDefinition, + SegmentDefinitionStatic, + segment_type, + owned_by_cube +); + +impl SegmentDefinition for MockSegmentDefinition { + crate::impl_static_data_method!(SegmentDefinitionStatic); + + fn sql(&self) -> Result, CubeError> { + Ok(Rc::new(MockMemberSql::new(&self.sql)?)) + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_segment() { + let segment = MockSegmentDefinition::builder() + .sql("{CUBE.status} = 'active'".to_string()) + .build(); + + let sql = segment.sql().unwrap(); + assert_eq!(sql.args_names(), &vec!["CUBE"]); + } + + #[test] + fn test_segment_with_type() { + let segment = MockSegmentDefinition::builder() + .segment_type(Some("filter".to_string())) + .sql("{CUBE.deleted} = false".to_string()) + .build(); + + assert_eq!( + segment.static_data().segment_type, + Some("filter".to_string()) + ); + } + + #[test] + fn test_segment_owned_by_cube() { + let segment = MockSegmentDefinition::builder() + .owned_by_cube(Some(true)) + .sql("{CUBE.is_valid} = true".to_string()) + .build(); + + assert_eq!(segment.static_data().owned_by_cube, Some(true)); + } + + #[test] + fn test_complex_segment_sql() { + let segment = MockSegmentDefinition::builder() + .sql("{CUBE.created_at} >= '2024-01-01' AND {CUBE.status} IN ('active', 'pending')" + .to_string()) + .build(); + + let sql = segment.sql().unwrap(); + assert_eq!(sql.args_names(), &vec!["CUBE"]); + + use crate::test_fixtures::cube_bridge::{MockSecurityContext, MockSqlUtils}; + let (template, args) = sql + .compile_template_sql(Rc::new(MockSqlUtils), Rc::new(MockSecurityContext)) + .unwrap(); + + match template { + crate::cube_bridge::member_sql::SqlTemplate::String(s) => { + assert_eq!( + s, + "{arg:0} >= '2024-01-01' AND {arg:1} IN ('active', 'pending')" + ); + } + _ => panic!("Expected String template"), + } + + assert_eq!(args.symbol_paths.len(), 2); + assert_eq!(args.symbol_paths[0], vec!["CUBE", "created_at"]); + assert_eq!(args.symbol_paths[1], vec!["CUBE", "status"]); + } + + #[test] + fn test_segment_with_cross_cube_reference() { + let segment = MockSegmentDefinition::builder() + .sql("{CUBE.user_id} IN (SELECT id FROM {users} WHERE {users.is_premium} = true)" + .to_string()) + .build(); + + let sql = segment.sql().unwrap(); + assert_eq!(sql.args_names(), &vec!["CUBE", "users"]); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index 9292db03b9512..d85d235d366ef 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -20,6 +20,7 @@ mod mock_member_order_by; mod mock_member_sql; mod mock_schema; mod mock_security_context; +mod mock_segment_definition; mod mock_sql_utils; mod mock_struct_with_sql_member; mod mock_timeshift_definition; @@ -40,6 +41,7 @@ pub use mock_measure_definition::MockMeasureDefinition; pub use mock_member_order_by::MockMemberOrderBy; pub use mock_member_sql::MockMemberSql; pub use mock_security_context::MockSecurityContext; +pub use mock_segment_definition::MockSegmentDefinition; pub use mock_sql_utils::MockSqlUtils; pub use mock_struct_with_sql_member::MockStructWithSqlMember; pub use mock_timeshift_definition::MockTimeShiftDefinition; \ No newline at end of file From e663d9ac7d6ef00dc09ea82a4dda4abf8deffb6b Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 18:39:15 +0100 Subject: [PATCH 15/42] in work --- .../cubesqlplanner/src/cube_bridge/evaluator.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/evaluator.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/evaluator.rs index 40a3429d9852c..bbcb25af716e3 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/evaluator.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/evaluator.rs @@ -49,11 +49,6 @@ pub trait CubeEvaluator { fn is_dimension(&self, path: Vec) -> Result; fn is_segment(&self, path: Vec) -> Result; fn cube_exists(&self, name: String) -> Result; - fn resolve_symbols_call_deps( - &self, - cube_name: String, - sql: Rc, - ) -> Result, CubeError>; fn resolve_granularity( &self, path: Vec, From 3a50ee0d20bbf4f085aedafa7eb2b57d1ffcd299 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 18:54:10 +0100 Subject: [PATCH 16/42] evaluator mock --- .../cube_bridge/mock_cube_definition.rs | 2 +- .../cube_bridge/mock_evaluator.rs | 540 ++++++++++++++++++ .../src/test_fixtures/cube_bridge/mod.rs | 3 + 3 files changed, 544 insertions(+), 1 deletion(-) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_cube_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_cube_definition.rs index 3b409261c0d43..2f8ae7ca748a9 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_cube_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_cube_definition.rs @@ -8,7 +8,7 @@ use std::rc::Rc; use typed_builder::TypedBuilder; /// Mock implementation of CubeDefinition for testing -#[derive(TypedBuilder)] +#[derive(Clone, TypedBuilder)] pub struct MockCubeDefinition { // Fields from CubeDefinitionStatic name: String, diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs new file mode 100644 index 0000000000000..c115bd113817c --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs @@ -0,0 +1,540 @@ +use crate::cube_bridge::cube_definition::CubeDefinition; +use crate::cube_bridge::dimension_definition::DimensionDefinition; +use crate::cube_bridge::evaluator::{CubeEvaluator, CubeEvaluatorStatic}; +use crate::cube_bridge::granularity_definition::GranularityDefinition; +use crate::cube_bridge::measure_definition::MeasureDefinition; +use crate::cube_bridge::member_sql::MemberSql; +use crate::cube_bridge::pre_aggregation_description::PreAggregationDescription; +use crate::cube_bridge::segment_definition::SegmentDefinition; +use crate::impl_static_data; +use crate::test_fixtures::cube_bridge::mock_schema::MockSchema; +use cubenativeutils::CubeError; +use std::any::Any; +use std::collections::HashMap; +use std::rc::Rc; + +/// Mock implementation of CubeEvaluator for testing +pub struct MockCubeEvaluator { + schema: MockSchema, + primary_keys: HashMap>, +} + +impl MockCubeEvaluator { + /// Create a new MockCubeEvaluator with the given schema + pub fn new(schema: MockSchema) -> Self { + Self { + schema, + primary_keys: HashMap::new(), + } + } + + /// Create a new MockCubeEvaluator with schema and primary keys + pub fn with_primary_keys( + schema: MockSchema, + primary_keys: HashMap>, + ) -> Self { + Self { + schema, + primary_keys, + } + } + + /// Parse a path string like "cube.member" into ["cube", "member"] + /// Returns error if the path doesn't exist in schema for the given type + fn parse_and_validate_path( + &self, + path_type: &str, + path: &str, + ) -> Result, CubeError> { + let parts: Vec = path.split('.').map(|s| s.to_string()).collect(); + + if parts.len() != 2 { + return Err(CubeError::user(format!( + "Invalid path format: '{}'. Expected format: 'cube.member'", + path + ))); + } + + let cube_name = &parts[0]; + let member_name = &parts[1]; + + // Check if cube exists + if self.schema.get_cube(cube_name).is_none() { + return Err(CubeError::user(format!("Cube '{}' not found", cube_name))); + } + + // Validate member exists for the given type + let exists = match path_type { + "measure" | "measures" => self.schema.get_measure(cube_name, member_name).is_some(), + "dimension" | "dimensions" => { + self.schema.get_dimension(cube_name, member_name).is_some() + } + "segment" | "segments" => self.schema.get_segment(cube_name, member_name).is_some(), + _ => { + return Err(CubeError::user(format!( + "Unknown path type: '{}'. Expected: measure, dimension, or segment", + path_type + ))) + } + }; + + if !exists { + return Err(CubeError::user(format!( + "{} '{}' not found in cube '{}'", + path_type, member_name, cube_name + ))); + } + + Ok(parts) + } +} + +impl_static_data!(MockCubeEvaluator, CubeEvaluatorStatic, primary_keys); + +impl CubeEvaluator for MockCubeEvaluator { + crate::impl_static_data_method!(CubeEvaluatorStatic); + + fn parse_path(&self, path_type: String, path: String) -> Result, CubeError> { + self.parse_and_validate_path(&path_type, &path) + } + + fn measure_by_path( + &self, + measure_path: String, + ) -> Result, CubeError> { + let parts = self.parse_and_validate_path("measure", &measure_path)?; + let cube_name = &parts[0]; + let measure_name = &parts[1]; + + self.schema + .get_measure(cube_name, measure_name) + .map(|m| m as Rc) + .ok_or_else(|| { + CubeError::user(format!( + "Measure '{}' not found in cube '{}'", + measure_name, cube_name + )) + }) + } + + fn dimension_by_path( + &self, + dimension_path: String, + ) -> Result, CubeError> { + let parts = self.parse_and_validate_path("dimension", &dimension_path)?; + let cube_name = &parts[0]; + let dimension_name = &parts[1]; + + self.schema + .get_dimension(cube_name, dimension_name) + .map(|d| d as Rc) + .ok_or_else(|| { + CubeError::user(format!( + "Dimension '{}' not found in cube '{}'", + dimension_name, cube_name + )) + }) + } + + fn segment_by_path( + &self, + segment_path: String, + ) -> Result, CubeError> { + let parts = self.parse_and_validate_path("segment", &segment_path)?; + let cube_name = &parts[0]; + let segment_name = &parts[1]; + + self.schema + .get_segment(cube_name, segment_name) + .map(|s| s as Rc) + .ok_or_else(|| { + CubeError::user(format!( + "Segment '{}' not found in cube '{}'", + segment_name, cube_name + )) + }) + } + + fn cube_from_path(&self, cube_path: String) -> Result, CubeError> { + self.schema + .get_cube(&cube_path) + .map(|c| Rc::new(c.definition.clone()) as Rc) + .ok_or_else(|| CubeError::user(format!("Cube '{}' not found", cube_path))) + } + + fn is_measure(&self, path: Vec) -> Result { + if path.len() != 2 { + return Ok(false); + } + Ok(self.schema.get_measure(&path[0], &path[1]).is_some()) + } + + fn is_dimension(&self, path: Vec) -> Result { + if path.len() != 2 { + return Ok(false); + } + Ok(self.schema.get_dimension(&path[0], &path[1]).is_some()) + } + + fn is_segment(&self, path: Vec) -> Result { + if path.len() != 2 { + return Ok(false); + } + Ok(self.schema.get_segment(&path[0], &path[1]).is_some()) + } + + fn cube_exists(&self, name: String) -> Result { + Ok(self.schema.get_cube(&name).is_some()) + } + + fn resolve_granularity( + &self, + _path: Vec, + ) -> Result, CubeError> { + todo!("resolve_granularity is not implemented in MockCubeEvaluator") + } + + fn pre_aggregations_for_cube_as_array( + &self, + _cube_name: String, + ) -> Result>, CubeError> { + todo!("pre_aggregations_for_cube_as_array is not implemented in MockCubeEvaluator") + } + + fn has_pre_aggregation_description_by_name(&self) -> Result { + todo!("has_pre_aggregation_description_by_name is not implemented in MockCubeEvaluator") + } + + fn pre_aggregation_description_by_name( + &self, + _cube_name: String, + _name: String, + ) -> Result>, CubeError> { + todo!("pre_aggregation_description_by_name is not implemented in MockCubeEvaluator") + } + + fn evaluate_rollup_references( + &self, + _cube: String, + _sql: Rc, + ) -> Result, CubeError> { + todo!("evaluate_rollup_references is not implemented in MockCubeEvaluator") + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_fixtures::cube_bridge::{ + MockDimensionDefinition, MockMeasureDefinition, MockSchemaBuilder, MockSegmentDefinition, + }; + + fn create_test_schema() -> MockSchema { + MockSchemaBuilder::new() + .add_cube("users") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_dimension( + "name", + MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("name".to_string()) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .build(), + ) + .add_segment( + "active", + MockSegmentDefinition::builder() + .sql("{CUBE.status} = 'active'".to_string()) + .build(), + ) + .finish_cube() + .add_cube("orders") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_measure( + "total", + MockMeasureDefinition::builder() + .measure_type("sum".to_string()) + .sql("amount".to_string()) + .build(), + ) + .finish_cube() + .build() + } + + #[test] + fn test_parse_path_measure() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + let result = evaluator.parse_path("measure".to_string(), "users.count".to_string()); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), vec!["users", "count"]); + } + + #[test] + fn test_parse_path_dimension() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + let result = evaluator.parse_path("dimension".to_string(), "users.name".to_string()); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), vec!["users", "name"]); + } + + #[test] + fn test_parse_path_segment() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + let result = evaluator.parse_path("segment".to_string(), "users.active".to_string()); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), vec!["users", "active"]); + } + + #[test] + fn test_parse_path_invalid_format() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + let result = evaluator.parse_path("measure".to_string(), "invalid".to_string()); + assert!(result.is_err()); + assert!(result.unwrap_err().message.contains("Invalid path format")); + } + + #[test] + fn test_parse_path_cube_not_found() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + let result = + evaluator.parse_path("measure".to_string(), "nonexistent.count".to_string()); + assert!(result.is_err()); + assert!(result.unwrap_err().message.contains("Cube 'nonexistent' not found")); + } + + #[test] + fn test_parse_path_member_not_found() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + let result = + evaluator.parse_path("measure".to_string(), "users.nonexistent".to_string()); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .message + .contains("measure 'nonexistent' not found")); + } + + #[test] + fn test_measure_by_path() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + let measure = evaluator.measure_by_path("users.count".to_string()).unwrap(); + assert_eq!(measure.static_data().measure_type, "count"); + } + + #[test] + fn test_dimension_by_path() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + let dimension = evaluator + .dimension_by_path("users.name".to_string()) + .unwrap(); + assert_eq!(dimension.static_data().dimension_type, "string"); + } + + #[test] + fn test_segment_by_path() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + let segment = evaluator.segment_by_path("users.active".to_string()).unwrap(); + // Verify it's a valid segment + assert!(segment.sql().is_ok()); + } + + #[test] + fn test_cube_from_path() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + let cube = evaluator.cube_from_path("users".to_string()).unwrap(); + assert_eq!(cube.static_data().name, "users"); + } + + #[test] + fn test_cube_from_path_not_found() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + let result = evaluator.cube_from_path("nonexistent".to_string()); + assert!(result.is_err()); + if let Err(err) = result { + assert!(err.message.contains("Cube 'nonexistent' not found")); + } + } + + #[test] + fn test_is_measure() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + assert!(evaluator + .is_measure(vec!["users".to_string(), "count".to_string()]) + .unwrap()); + assert!(!evaluator + .is_measure(vec!["users".to_string(), "name".to_string()]) + .unwrap()); + assert!(!evaluator + .is_measure(vec!["users".to_string(), "nonexistent".to_string()]) + .unwrap()); + } + + #[test] + fn test_is_dimension() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + assert!(evaluator + .is_dimension(vec!["users".to_string(), "name".to_string()]) + .unwrap()); + assert!(!evaluator + .is_dimension(vec!["users".to_string(), "count".to_string()]) + .unwrap()); + assert!(!evaluator + .is_dimension(vec!["users".to_string(), "nonexistent".to_string()]) + .unwrap()); + } + + #[test] + fn test_is_segment() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + assert!(evaluator + .is_segment(vec!["users".to_string(), "active".to_string()]) + .unwrap()); + assert!(!evaluator + .is_segment(vec!["users".to_string(), "count".to_string()]) + .unwrap()); + assert!(!evaluator + .is_segment(vec!["users".to_string(), "nonexistent".to_string()]) + .unwrap()); + } + + #[test] + fn test_cube_exists() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + assert!(evaluator.cube_exists("users".to_string()).unwrap()); + assert!(evaluator.cube_exists("orders".to_string()).unwrap()); + assert!(!evaluator.cube_exists("nonexistent".to_string()).unwrap()); + } + + #[test] + fn test_with_primary_keys() { + let schema = create_test_schema(); + let mut primary_keys = HashMap::new(); + primary_keys.insert("users".to_string(), vec!["id".to_string()]); + primary_keys.insert( + "orders".to_string(), + vec!["id".to_string(), "user_id".to_string()], + ); + + let evaluator = MockCubeEvaluator::with_primary_keys(schema, primary_keys.clone()); + + let static_data = evaluator.static_data(); + assert_eq!(static_data.primary_keys, primary_keys); + } + + #[test] + fn test_multiple_cubes() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + // Test users cube + assert!(evaluator.cube_exists("users".to_string()).unwrap()); + assert!(evaluator + .is_measure(vec!["users".to_string(), "count".to_string()]) + .unwrap()); + assert!(evaluator + .is_dimension(vec!["users".to_string(), "name".to_string()]) + .unwrap()); + + // Test orders cube + assert!(evaluator.cube_exists("orders".to_string()).unwrap()); + assert!(evaluator + .is_measure(vec!["orders".to_string(), "total".to_string()]) + .unwrap()); + assert!(evaluator + .is_dimension(vec!["orders".to_string(), "id".to_string()]) + .unwrap()); + } + + #[test] + #[should_panic(expected = "resolve_granularity is not implemented")] + fn test_resolve_granularity_panics() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + let _ = evaluator.resolve_granularity(vec!["users".to_string(), "created_at".to_string()]); + } + + #[test] + #[should_panic(expected = "pre_aggregations_for_cube_as_array is not implemented")] + fn test_pre_aggregations_for_cube_panics() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + let _ = evaluator.pre_aggregations_for_cube_as_array("users".to_string()); + } + + #[test] + #[should_panic(expected = "pre_aggregation_description_by_name is not implemented")] + fn test_pre_aggregation_by_name_panics() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + let _ = evaluator.pre_aggregation_description_by_name( + "users".to_string(), + "main".to_string(), + ); + } + + #[test] + #[should_panic(expected = "evaluate_rollup_references is not implemented")] + fn test_evaluate_rollup_references_panics() { + let schema = create_test_schema(); + let evaluator = MockCubeEvaluator::new(schema); + + use crate::test_fixtures::cube_bridge::MockMemberSql; + let sql = Rc::new(MockMemberSql::new("{CUBE.id}").unwrap()); + let _ = evaluator.evaluate_rollup_references("users".to_string(), sql); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index d85d235d366ef..5a4337ccf3b26 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -9,6 +9,7 @@ mod mock_case_switch_else_item; mod mock_case_switch_item; mod mock_cube_definition; mod mock_dimension_definition; +mod mock_evaluator; mod mock_expression_struct; mod mock_geo_item; mod mock_join_definition; @@ -33,6 +34,7 @@ pub use mock_case_switch_else_item::MockCaseSwitchElseItem; pub use mock_case_switch_item::MockCaseSwitchItem; pub use mock_cube_definition::MockCubeDefinition; pub use mock_dimension_definition::MockDimensionDefinition; +pub use mock_evaluator::MockCubeEvaluator; pub use mock_expression_struct::MockExpressionStruct; pub use mock_geo_item::MockGeoItem; pub use mock_join_item::MockJoinItem; @@ -40,6 +42,7 @@ pub use mock_join_item_definition::MockJoinItemDefinition; pub use mock_measure_definition::MockMeasureDefinition; pub use mock_member_order_by::MockMemberOrderBy; pub use mock_member_sql::MockMemberSql; +pub use mock_schema::{MockSchema, MockSchemaBuilder}; pub use mock_security_context::MockSecurityContext; pub use mock_segment_definition::MockSegmentDefinition; pub use mock_sql_utils::MockSqlUtils; From 70728cb75484f0ca2d427c97b9d972634b1510cc Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 19:01:17 +0100 Subject: [PATCH 17/42] in work --- .../cubesqlplanner/src/test_fixtures/mod.rs | 1 + .../src/test_fixtures/schemas/mod.rs | 3 + .../test_fixtures/schemas/visitors_schema.rs | 284 ++++++++++++++++++ 3 files changed, 288 insertions(+) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/mod.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/mod.rs index 226c08aca4dc3..4baca0ca1977f 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/mod.rs @@ -1 +1,2 @@ pub mod cube_bridge; +pub mod schemas; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/mod.rs new file mode 100644 index 0000000000000..a54bba2083605 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/mod.rs @@ -0,0 +1,3 @@ +pub mod visitors_schema; + +pub use visitors_schema::create_visitors_schema; \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs new file mode 100644 index 0000000000000..c8bdd21dea6df --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs @@ -0,0 +1,284 @@ +use crate::test_fixtures::cube_bridge::{ + MockDimensionDefinition, MockMeasureDefinition, MockSchema, MockSchemaBuilder, + MockSegmentDefinition, +}; + +/// Creates a schema for visitors and visitor_checkins cubes +/// +/// This schema demonstrates: +/// - Basic dimensions with different types +/// - Geo dimensions with latitude/longitude +/// - Sub-query dimensions that reference other cubes +/// - Dimensions with complex SQL including special characters (question marks) +/// - Time dimensions +pub fn create_visitors_schema() -> MockSchema { + MockSchemaBuilder::new() + // visitor_checkins cube - referenced by visitors cube + .add_cube("visitor_checkins") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_dimension( + "visitor_id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("visitor_id".to_string()) + .build(), + ) + .add_dimension( + "minDate", + MockDimensionDefinition::builder() + .dimension_type("time".to_string()) + .sql("MIN(created_at)".to_string()) + .build(), + ) + .add_dimension( + "minDate1", + MockDimensionDefinition::builder() + .dimension_type("time".to_string()) + .sql("MIN(created_at) + INTERVAL '1 day'".to_string()) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .build(), + ) + .finish_cube() + // visitors cube - main cube with various dimension types + .add_cube("visitors") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_dimension( + "visitor_id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("visitor_id".to_string()) + .build(), + ) + .add_dimension( + "source", + MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("source".to_string()) + .build(), + ) + .add_dimension( + "created_at", + MockDimensionDefinition::builder() + .dimension_type("time".to_string()) + .sql("created_at".to_string()) + .build(), + ) + // Sub-query dimension referencing visitor_checkins.minDate + .add_dimension( + "minVisitorCheckinDate", + MockDimensionDefinition::builder() + .dimension_type("time".to_string()) + .sql("{visitor_checkins.minDate}".to_string()) + .sub_query(Some(true)) + .build(), + ) + // Sub-query dimension referencing visitor_checkins.minDate1 + .add_dimension( + "minVisitorCheckinDate1", + MockDimensionDefinition::builder() + .dimension_type("time".to_string()) + .sql("{visitor_checkins.minDate1}".to_string()) + .sub_query(Some(true)) + .build(), + ) + // Geo dimension with latitude and longitude + .add_dimension( + "location", + MockDimensionDefinition::builder() + .dimension_type("geo".to_string()) + .latitude("latitude".to_string()) + .longitude("longitude".to_string()) + .build(), + ) + // Dimension with SQL containing question marks (special characters) + .add_dimension( + "questionMark", + MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql( + "replace('some string question string ? ?? ???', 'string', 'with some ? ?? ???')" + .to_string(), + ) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .build(), + ) + .add_measure( + "total_revenue", + MockMeasureDefinition::builder() + .measure_type("sum".to_string()) + .sql("revenue".to_string()) + .build(), + ) + .add_segment( + "google", + MockSegmentDefinition::builder() + .sql("{CUBE.source} = 'google'".to_string()) + .build(), + ) + .finish_cube() + .build() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cube_bridge::dimension_definition::DimensionDefinition; + use crate::cube_bridge::measure_definition::MeasureDefinition; + use crate::cube_bridge::segment_definition::SegmentDefinition; + + #[test] + fn test_schema_has_both_cubes() { + let schema = create_visitors_schema(); + + assert!(schema.get_cube("visitors").is_some()); + assert!(schema.get_cube("visitor_checkins").is_some()); + } + + #[test] + fn test_visitors_dimensions() { + let schema = create_visitors_schema(); + + // Basic dimensions + assert!(schema.get_dimension("visitors", "visitor_id").is_some()); + assert!(schema.get_dimension("visitors", "source").is_some()); + assert!(schema.get_dimension("visitors", "created_at").is_some()); + + // Sub-query dimensions + let min_checkin = schema + .get_dimension("visitors", "minVisitorCheckinDate") + .unwrap(); + assert_eq!(min_checkin.static_data().dimension_type, "time"); + assert_eq!(min_checkin.static_data().sub_query, Some(true)); + + let min_checkin1 = schema + .get_dimension("visitors", "minVisitorCheckinDate1") + .unwrap(); + assert_eq!(min_checkin1.static_data().dimension_type, "time"); + assert_eq!(min_checkin1.static_data().sub_query, Some(true)); + + // Geo dimension + let location = schema.get_dimension("visitors", "location").unwrap(); + assert_eq!(location.static_data().dimension_type, "geo"); + assert!(location.has_latitude().unwrap()); + assert!(location.has_longitude().unwrap()); + + // Dimension with special characters + let question_mark = schema.get_dimension("visitors", "questionMark").unwrap(); + assert_eq!(question_mark.static_data().dimension_type, "string"); + let sql = question_mark.sql().unwrap().unwrap(); + // Verify SQL contains question marks + use crate::cube_bridge::member_sql::MemberSql; + use crate::test_fixtures::cube_bridge::{MockSecurityContext, MockSqlUtils}; + use std::rc::Rc; + let (template, _args) = sql + .compile_template_sql(Rc::new(MockSqlUtils), Rc::new(MockSecurityContext)) + .unwrap(); + match template { + crate::cube_bridge::member_sql::SqlTemplate::String(s) => { + assert!(s.contains("?")); + } + _ => panic!("Expected String template"), + } + } + + #[test] + fn test_visitor_checkins_dimensions() { + let schema = create_visitors_schema(); + + assert!(schema + .get_dimension("visitor_checkins", "visitor_id") + .is_some()); + + let min_date = schema + .get_dimension("visitor_checkins", "minDate") + .unwrap(); + assert_eq!(min_date.static_data().dimension_type, "time"); + + let min_date1 = schema + .get_dimension("visitor_checkins", "minDate1") + .unwrap(); + assert_eq!(min_date1.static_data().dimension_type, "time"); + } + + #[test] + fn test_visitors_measures() { + let schema = create_visitors_schema(); + + let count = schema.get_measure("visitors", "count").unwrap(); + assert_eq!(count.static_data().measure_type, "count"); + + let revenue = schema.get_measure("visitors", "total_revenue").unwrap(); + assert_eq!(revenue.static_data().measure_type, "sum"); + } + + #[test] + fn test_visitors_segments() { + let schema = create_visitors_schema(); + + let google_segment = schema.get_segment("visitors", "google").unwrap(); + let sql = google_segment.sql().unwrap(); + + use crate::cube_bridge::member_sql::MemberSql; + assert_eq!(sql.args_names(), &vec!["CUBE"]); + } + + #[test] + fn test_subquery_dimension_references() { + let schema = create_visitors_schema(); + + let min_checkin = schema + .get_dimension("visitors", "minVisitorCheckinDate") + .unwrap(); + let sql = min_checkin.sql().unwrap().unwrap(); + + use crate::cube_bridge::member_sql::MemberSql; + // Should reference visitor_checkins.minDate + assert_eq!(sql.args_names(), &vec!["visitor_checkins"]); + } + + #[test] + fn test_geo_dimension_structure() { + use crate::cube_bridge::geo_item::GeoItem; + use crate::cube_bridge::member_sql::MemberSql; + + let schema = create_visitors_schema(); + + let location = schema.get_dimension("visitors", "location").unwrap(); + + assert_eq!(location.static_data().dimension_type, "geo"); + + // Test using trait methods + let latitude = location.latitude().unwrap().unwrap(); + let lat_sql = latitude.sql().unwrap(); + // Verify the SQL is correct - it should have no template parameters + assert_eq!(lat_sql.args_names().len(), 0); + + let longitude = location.longitude().unwrap().unwrap(); + let lon_sql = longitude.sql().unwrap(); + assert_eq!(lon_sql.args_names().len(), 0); + } +} \ No newline at end of file From ce463e37310f62b2dc36bf1ecd2e92b097f2db5f Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 19:32:53 +0100 Subject: [PATCH 18/42] compiler tests in work --- .../src/planner/sql_evaluator/compiler.rs | 169 ++++++++++++++++++ .../sql_evaluator/symbols/geo_dimension.rs | 48 ----- .../src/planner/sql_evaluator/symbols/mod.rs | 12 -- .../sql_evaluator/symbols/primitive_type.rs | 108 ----------- .../sql_evaluator/symbols/simple_dimension.rs | 47 ----- .../sql_evaluator/symbols/switch_dimension.rs | 35 ---- 6 files changed, 169 insertions(+), 250 deletions(-) delete mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/geo_dimension.rs delete mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/primitive_type.rs delete mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/simple_dimension.rs delete mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/switch_dimension.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs index 938a9891f15c6..1eca407c7a616 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs @@ -161,3 +161,172 @@ impl Compiler { Ok(node) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_fixtures::cube_bridge::{MockCubeEvaluator, MockSecurityContext, MockSqlUtils}; + use crate::test_fixtures::schemas::create_visitors_schema; + use chrono_tz::Tz; + + #[test] + fn test_add_dimension_evaluator_number_dimension() { + let schema = create_visitors_schema(); + let evaluator = Rc::new(MockCubeEvaluator::new(schema)); + let sql_utils = Rc::new(MockSqlUtils); + let security_context = Rc::new(MockSecurityContext); + let timezone = Tz::UTC; + + let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + + let symbol = compiler + .add_dimension_evaluator("visitors.id".to_string()) + .unwrap(); + + // Check symbol type + assert!(symbol.is_dimension()); + assert!(!symbol.is_measure()); + + // Check full name + assert_eq!(symbol.full_name(), "visitors.id"); + + // Check cube name and member name + assert_eq!(symbol.cube_name(), "visitors"); + assert_eq!(symbol.name(), "id"); + + // Check no dependencies for simple dimension + let dependencies = symbol.get_dependencies(); + assert_eq!( + dependencies.len(), + 0, + "Simple dimension should have no dependencies" + ); + + // Check dimension type + let dimension = symbol.as_dimension().unwrap(); + assert_eq!(dimension.dimension_type(), "number"); + } + + #[test] + fn test_add_dimension_evaluator_string_dimension() { + let schema = create_visitors_schema(); + let evaluator = Rc::new(MockCubeEvaluator::new(schema)); + let sql_utils = Rc::new(MockSqlUtils); + let security_context = Rc::new(MockSecurityContext); + let timezone = Tz::UTC; + + let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + + let symbol = compiler + .add_dimension_evaluator("visitors.source".to_string()) + .unwrap(); + + // Check symbol type + assert!(symbol.is_dimension()); + assert!(!symbol.is_measure()); + + // Check full name + assert_eq!(symbol.full_name(), "visitors.source"); + + // Check cube name and member name + assert_eq!(symbol.cube_name(), "visitors"); + assert_eq!(symbol.name(), "source"); + + // Check no dependencies for simple dimension + let dependencies = symbol.get_dependencies(); + assert_eq!( + dependencies.len(), + 0, + "Simple dimension should have no dependencies" + ); + + // Check dimension type + let dimension = symbol.as_dimension().unwrap(); + assert_eq!(dimension.dimension_type(), "string"); + } + + #[test] + fn test_add_dimension_evaluator_caching() { + let schema = create_visitors_schema(); + let evaluator = Rc::new(MockCubeEvaluator::new(schema)); + let sql_utils = Rc::new(MockSqlUtils); + let security_context = Rc::new(MockSecurityContext); + let timezone = Tz::UTC; + + let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + + // Add dimension twice + let symbol1 = compiler + .add_dimension_evaluator("visitors.id".to_string()) + .unwrap(); + let symbol2 = compiler + .add_dimension_evaluator("visitors.id".to_string()) + .unwrap(); + + // Should return the same cached instance + assert_eq!( + symbol1.full_name(), + symbol2.full_name(), + "Cached symbols should have the same full name" + ); + } + + #[test] + fn test_add_dimension_evaluator_invalid_path() { + let schema = create_visitors_schema(); + let evaluator = Rc::new(MockCubeEvaluator::new(schema)); + let sql_utils = Rc::new(MockSqlUtils); + let security_context = Rc::new(MockSecurityContext); + let timezone = Tz::UTC; + + let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + + // Try to add non-existent dimension + let result = compiler.add_dimension_evaluator("nonexistent.dimension".to_string()); + + assert!(result.is_err(), "Should fail for non-existent dimension"); + } + + #[test] + fn test_add_dimension_evaluator_multiple_dimensions() { + let schema = create_visitors_schema(); + let evaluator = Rc::new(MockCubeEvaluator::new(schema)); + let sql_utils = Rc::new(MockSqlUtils); + let security_context = Rc::new(MockSecurityContext); + let timezone = Tz::UTC; + + let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + + // Add multiple different dimensions + let id_symbol = compiler + .add_dimension_evaluator("visitors.id".to_string()) + .unwrap(); + let source_symbol = compiler + .add_dimension_evaluator("visitors.source".to_string()) + .unwrap(); + let created_at_symbol = compiler + .add_dimension_evaluator("visitors.created_at".to_string()) + .unwrap(); + + // Verify each dimension + assert_eq!(id_symbol.full_name(), "visitors.id"); + assert_eq!(id_symbol.as_dimension().unwrap().dimension_type(), "number"); + + assert_eq!(source_symbol.full_name(), "visitors.source"); + assert_eq!( + source_symbol.as_dimension().unwrap().dimension_type(), + "string" + ); + + assert_eq!(created_at_symbol.full_name(), "visitors.created_at"); + assert_eq!( + created_at_symbol.as_dimension().unwrap().dimension_type(), + "time" + ); + + // All should have no dependencies + assert_eq!(id_symbol.get_dependencies().len(), 0); + assert_eq!(source_symbol.get_dependencies().len(), 0); + assert_eq!(created_at_symbol.get_dependencies().len(), 0); + } +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/geo_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/geo_dimension.rs deleted file mode 100644 index 8d4e82de35f3a..0000000000000 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/geo_dimension.rs +++ /dev/null @@ -1,48 +0,0 @@ -use super::MemberSymbol; -use crate::planner::sql_evaluator::SqlCall; -use cubenativeutils::CubeError; -use std::rc::Rc; - -/// Represents a geo dimension with latitude and longitude -#[derive(Clone)] -pub struct GeoDimension { - latitude: Rc, - longitude: Rc, -} - -impl GeoDimension { - pub fn new(latitude: Rc, longitude: Rc) -> Self { - Self { - latitude, - longitude, - } - } - - pub fn latitude(&self) -> &Rc { - &self.latitude - } - - pub fn longitude(&self) -> &Rc { - &self.longitude - } - - pub fn get_dependencies(&self, deps: &mut Vec>) { - self.latitude.extract_symbol_deps(deps); - self.longitude.extract_symbol_deps(deps); - } - - pub fn get_dependencies_with_path(&self, deps: &mut Vec<(Rc, Vec)>) { - self.latitude.extract_symbol_deps_with_path(deps); - self.longitude.extract_symbol_deps_with_path(deps); - } - - pub fn apply_to_deps) -> Result, CubeError>>( - &self, - f: &F, - ) -> Result { - Ok(Self { - latitude: self.latitude.apply_recursive(f)?, - longitude: self.longitude.apply_recursive(f)?, - }) - } -} \ No newline at end of file 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 e78a6a8a5eae1..84d02f0d1997c 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/mod.rs @@ -1,33 +1,21 @@ -mod calendar_dimension; -mod case_dimension; mod common; mod cube_symbol; mod dimension_symbol; -mod geo_dimension; mod measure_symbol; mod member_expression_symbol; mod member_symbol; -mod primitive_type; -mod simple_dimension; -mod switch_dimension; mod symbol_factory; mod time_dimension_symbol; -pub use calendar_dimension::CalendarDimension; -pub use case_dimension::CaseDimension; pub use common::*; pub use cube_symbol::{ CubeNameSymbol, CubeNameSymbolFactory, CubeTableSymbol, CubeTableSymbolFactory, }; pub use dimension_symbol::*; -pub use geo_dimension::GeoDimension; pub use measure_symbol::{ DimensionTimeShift, MeasureSymbol, MeasureSymbolFactory, MeasureTimeShifts, }; pub use member_expression_symbol::{MemberExpressionExpression, MemberExpressionSymbol}; pub use member_symbol::MemberSymbol; -pub use primitive_type::*; -pub use simple_dimension::SimpleDimension; -pub use switch_dimension::SwitchDimension; pub use symbol_factory::SymbolFactory; pub use time_dimension_symbol::TimeDimensionSymbol; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/primitive_type.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/primitive_type.rs deleted file mode 100644 index b437588511262..0000000000000 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/primitive_type.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::fmt; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum PrimitiveType { - Number, - String, - Boolean, - Time, -} - -impl PrimitiveType { - /// Try to construct PrimitiveType from string - pub fn try_from_str(s: &str) -> Option { - match s { - "number" => Some(Self::Number), - "string" => Some(Self::String), - "boolean" => Some(Self::Boolean), - "time" => Some(Self::Time), - _ => None, - } - } - - /// Get string representation of the type - pub fn as_str(&self) -> &'static str { - match self { - Self::Number => "number", - Self::String => "string", - Self::Boolean => "boolean", - Self::Time => "time", - } - } -} - -impl fmt::Display for PrimitiveType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.as_str()) - } -} - -impl TryFrom<&str> for PrimitiveType { - type Error = (); - - fn try_from(value: &str) -> Result { - Self::try_from_str(value).ok_or(()) - } -} - -impl TryFrom for PrimitiveType { - type Error = (); - - fn try_from(value: String) -> Result { - Self::try_from_str(&value).ok_or(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_try_from_str() { - assert_eq!( - PrimitiveType::try_from_str("number"), - Some(PrimitiveType::Number) - ); - assert_eq!( - PrimitiveType::try_from_str("string"), - Some(PrimitiveType::String) - ); - assert_eq!( - PrimitiveType::try_from_str("boolean"), - Some(PrimitiveType::Boolean) - ); - assert_eq!( - PrimitiveType::try_from_str("time"), - Some(PrimitiveType::Time) - ); - assert_eq!(PrimitiveType::try_from_str("unknown"), None); - } - - #[test] - fn test_as_str() { - assert_eq!(PrimitiveType::Number.as_str(), "number"); - assert_eq!(PrimitiveType::String.as_str(), "string"); - assert_eq!(PrimitiveType::Boolean.as_str(), "boolean"); - assert_eq!(PrimitiveType::Time.as_str(), "time"); - } - - #[test] - fn test_try_from() { - assert_eq!(PrimitiveType::try_from("number"), Ok(PrimitiveType::Number)); - assert_eq!(PrimitiveType::try_from("invalid"), Err(())); - - assert_eq!( - PrimitiveType::try_from("string".to_string()), - Ok(PrimitiveType::String) - ); - assert_eq!(PrimitiveType::try_from("invalid".to_string()), Err(())); - } - - #[test] - fn test_display() { - assert_eq!(format!("{}", PrimitiveType::Number), "number"); - assert_eq!(format!("{}", PrimitiveType::String), "string"); - assert_eq!(format!("{}", PrimitiveType::Boolean), "boolean"); - assert_eq!(format!("{}", PrimitiveType::Time), "time"); - } -} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/simple_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/simple_dimension.rs deleted file mode 100644 index dc1907661e1be..0000000000000 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/simple_dimension.rs +++ /dev/null @@ -1,47 +0,0 @@ -use super::{MemberSymbol, PrimitiveType}; -use crate::planner::sql_evaluator::SqlCall; -use cubenativeutils::CubeError; -use std::rc::Rc; - -/// Represents a simple dimension with a primitive type -#[derive(Clone)] -pub struct SimpleDimension { - primitive_type: PrimitiveType, - member_sql: Rc, -} - -impl SimpleDimension { - pub fn new(primitive_type: PrimitiveType, member_sql: Rc) -> Self { - Self { - primitive_type, - member_sql, - } - } - - pub fn primitive_type(&self) -> PrimitiveType { - self.primitive_type - } - - pub fn member_sql(&self) -> &Rc { - &self.member_sql - } - - pub fn get_dependencies(&self, deps: &mut Vec>) { - self.member_sql.extract_symbol_deps(deps); - } - - pub fn get_dependencies_with_path(&self, deps: &mut Vec<(Rc, Vec)>) { - self.member_sql.extract_symbol_deps_with_path(deps); - } - - pub fn apply_to_deps) -> Result, CubeError>>( - &self, - f: &F, - ) -> Result { - Ok(Self { - primitive_type: self.primitive_type, - member_sql: self.member_sql.apply_recursive(f)?, - }) - } -} - diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/switch_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/switch_dimension.rs deleted file mode 100644 index 3e5e59f46527b..0000000000000 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/switch_dimension.rs +++ /dev/null @@ -1,35 +0,0 @@ -use super::MemberSymbol; -use cubenativeutils::CubeError; -use std::rc::Rc; - -/// Represents a switch dimension with predefined values -#[derive(Clone)] -pub struct SwitchDimension { - values: Vec, -} - -impl SwitchDimension { - pub fn new(values: Vec) -> Self { - Self { values } - } - - pub fn values(&self) -> &Vec { - &self.values - } - - pub fn get_dependencies(&self, _deps: &mut Vec>) { - // Switch dimension has no SQL dependencies - } - - pub fn get_dependencies_with_path(&self, _deps: &mut Vec<(Rc, Vec)>) { - // Switch dimension has no SQL dependencies - } - - pub fn apply_to_deps) -> Result, CubeError>>( - &self, - _f: &F, - ) -> Result { - // Switch dimension has no SQL dependencies, return clone - Ok(self.clone()) - } -} \ No newline at end of file From 2aafc361f62ed1f4db6658af19bc4e4b15052b73 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 19:43:04 +0100 Subject: [PATCH 19/42] compiler tests in work --- .../src/planner/sql_evaluator/compiler.rs | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs index 1eca407c7a616..c3e874ba1235a 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs @@ -329,4 +329,152 @@ mod tests { assert_eq!(source_symbol.get_dependencies().len(), 0); assert_eq!(created_at_symbol.get_dependencies().len(), 0); } + + #[test] + fn test_add_measure_evaluator_count_measure() { + let schema = create_visitors_schema(); + let evaluator = Rc::new(MockCubeEvaluator::new(schema)); + let sql_utils = Rc::new(MockSqlUtils); + let security_context = Rc::new(MockSecurityContext); + let timezone = Tz::UTC; + + let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + + let symbol = compiler + .add_measure_evaluator("visitor_checkins.count".to_string()) + .unwrap(); + + // Check symbol type + assert!(symbol.is_measure()); + assert!(!symbol.is_dimension()); + + // Check full name + assert_eq!(symbol.full_name(), "visitor_checkins.count"); + + // Check cube name and member name + assert_eq!(symbol.cube_name(), "visitor_checkins"); + assert_eq!(symbol.name(), "count"); + + // Check no dependencies for simple measure + let dependencies = symbol.get_dependencies(); + assert_eq!( + dependencies.len(), + 0, + "Simple measure should have no dependencies" + ); + + // Check measure type + let measure = symbol.as_measure().unwrap(); + assert_eq!(measure.measure_type(), "count"); + } + + #[test] + fn test_add_measure_evaluator_sum_measure() { + let schema = create_visitors_schema(); + let evaluator = Rc::new(MockCubeEvaluator::new(schema)); + let sql_utils = Rc::new(MockSqlUtils); + let security_context = Rc::new(MockSecurityContext); + let timezone = Tz::UTC; + + let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + + let symbol = compiler + .add_measure_evaluator("visitors.total_revenue".to_string()) + .unwrap(); + + // Check symbol type + assert!(symbol.is_measure()); + assert!(!symbol.is_dimension()); + + // Check full name + assert_eq!(symbol.full_name(), "visitors.total_revenue"); + + // Check cube name and member name + assert_eq!(symbol.cube_name(), "visitors"); + assert_eq!(symbol.name(), "total_revenue"); + + // Check no dependencies for simple measure + let dependencies = symbol.get_dependencies(); + assert_eq!( + dependencies.len(), + 0, + "Simple measure should have no dependencies" + ); + + // Check measure type + let measure = symbol.as_measure().unwrap(); + assert_eq!(measure.measure_type(), "sum"); + } + + #[test] + fn test_add_measure_evaluator_caching() { + let schema = create_visitors_schema(); + let evaluator = Rc::new(MockCubeEvaluator::new(schema)); + let sql_utils = Rc::new(MockSqlUtils); + let security_context = Rc::new(MockSecurityContext); + let timezone = Tz::UTC; + + let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + + // Add measure twice + let symbol1 = compiler + .add_measure_evaluator("visitors.total_revenue".to_string()) + .unwrap(); + let symbol2 = compiler + .add_measure_evaluator("visitors.total_revenue".to_string()) + .unwrap(); + + // Should return the same cached instance + assert_eq!( + symbol1.full_name(), + symbol2.full_name(), + "Cached symbols should have the same full name" + ); + } + + #[test] + fn test_add_measure_evaluator_invalid_path() { + let schema = create_visitors_schema(); + let evaluator = Rc::new(MockCubeEvaluator::new(schema)); + let sql_utils = Rc::new(MockSqlUtils); + let security_context = Rc::new(MockSecurityContext); + let timezone = Tz::UTC; + + let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + + // Try to add non-existent measure + let result = compiler.add_measure_evaluator("nonexistent.measure".to_string()); + + assert!(result.is_err(), "Should fail for non-existent measure"); + } + + #[test] + fn test_add_measure_evaluator_multiple_measures() { + let schema = create_visitors_schema(); + let evaluator = Rc::new(MockCubeEvaluator::new(schema)); + let sql_utils = Rc::new(MockSqlUtils); + let security_context = Rc::new(MockSecurityContext); + let timezone = Tz::UTC; + + let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + + // Add multiple different measures + let count_symbol = compiler + .add_measure_evaluator("visitor_checkins.count".to_string()) + .unwrap(); + let revenue_symbol = compiler + .add_measure_evaluator("visitors.total_revenue".to_string()) + .unwrap(); + + // Verify each measure + assert_eq!(count_symbol.full_name(), "visitor_checkins.count"); + assert_eq!(count_symbol.as_measure().unwrap().measure_type(), "count"); + + assert_eq!(revenue_symbol.full_name(), "visitors.total_revenue"); + assert_eq!(revenue_symbol.as_measure().unwrap().measure_type(), "sum"); + + // All should have no dependencies + assert_eq!(count_symbol.get_dependencies().len(), 0); + assert_eq!(revenue_symbol.get_dependencies().len(), 0); + } } From c8721c2e128ede7a7df19e8adffc5dd749df8217 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 20:03:36 +0100 Subject: [PATCH 20/42] compiler tests in work --- .../src/planner/sql_evaluator/compiler.rs | 300 +++++++----------- .../test_fixtures/cube_bridge/mock_schema.rs | 16 +- .../src/test_fixtures/schemas/mod.rs | 28 +- 3 files changed, 148 insertions(+), 196 deletions(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs index c3e874ba1235a..c470476b688bd 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs @@ -165,166 +165,99 @@ impl Compiler { #[cfg(test)] mod tests { use super::*; - use crate::test_fixtures::cube_bridge::{MockCubeEvaluator, MockSecurityContext, MockSqlUtils}; - use crate::test_fixtures::schemas::create_visitors_schema; - use chrono_tz::Tz; + use crate::test_fixtures::schemas::{create_visitors_schema, TestCompiler}; #[test] fn test_add_dimension_evaluator_number_dimension() { - let schema = create_visitors_schema(); - let evaluator = Rc::new(MockCubeEvaluator::new(schema)); - let sql_utils = Rc::new(MockSqlUtils); - let security_context = Rc::new(MockSecurityContext); - let timezone = Tz::UTC; + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); - let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); - - let symbol = compiler + let symbol = test_compiler + .compiler .add_dimension_evaluator("visitors.id".to_string()) .unwrap(); - // Check symbol type assert!(symbol.is_dimension()); assert!(!symbol.is_measure()); - - // Check full name assert_eq!(symbol.full_name(), "visitors.id"); - - // Check cube name and member name assert_eq!(symbol.cube_name(), "visitors"); assert_eq!(symbol.name(), "id"); - - // Check no dependencies for simple dimension - let dependencies = symbol.get_dependencies(); - assert_eq!( - dependencies.len(), - 0, - "Simple dimension should have no dependencies" - ); - - // Check dimension type - let dimension = symbol.as_dimension().unwrap(); - assert_eq!(dimension.dimension_type(), "number"); + assert_eq!(symbol.get_dependencies().len(), 0); + assert_eq!(symbol.as_dimension().unwrap().dimension_type(), "number"); } #[test] fn test_add_dimension_evaluator_string_dimension() { - let schema = create_visitors_schema(); - let evaluator = Rc::new(MockCubeEvaluator::new(schema)); - let sql_utils = Rc::new(MockSqlUtils); - let security_context = Rc::new(MockSecurityContext); - let timezone = Tz::UTC; + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); - let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); - - let symbol = compiler + let symbol = test_compiler + .compiler .add_dimension_evaluator("visitors.source".to_string()) .unwrap(); - // Check symbol type assert!(symbol.is_dimension()); assert!(!symbol.is_measure()); - - // Check full name assert_eq!(symbol.full_name(), "visitors.source"); - - // Check cube name and member name assert_eq!(symbol.cube_name(), "visitors"); assert_eq!(symbol.name(), "source"); - - // Check no dependencies for simple dimension - let dependencies = symbol.get_dependencies(); - assert_eq!( - dependencies.len(), - 0, - "Simple dimension should have no dependencies" - ); - - // Check dimension type - let dimension = symbol.as_dimension().unwrap(); - assert_eq!(dimension.dimension_type(), "string"); + assert_eq!(symbol.get_dependencies().len(), 0); + assert_eq!(symbol.as_dimension().unwrap().dimension_type(), "string"); } #[test] fn test_add_dimension_evaluator_caching() { - let schema = create_visitors_schema(); - let evaluator = Rc::new(MockCubeEvaluator::new(schema)); - let sql_utils = Rc::new(MockSqlUtils); - let security_context = Rc::new(MockSecurityContext); - let timezone = Tz::UTC; + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); - let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); - - // Add dimension twice - let symbol1 = compiler + let symbol1 = test_compiler + .compiler .add_dimension_evaluator("visitors.id".to_string()) .unwrap(); - let symbol2 = compiler + let symbol2 = test_compiler + .compiler .add_dimension_evaluator("visitors.id".to_string()) .unwrap(); - // Should return the same cached instance - assert_eq!( - symbol1.full_name(), - symbol2.full_name(), - "Cached symbols should have the same full name" - ); + assert_eq!(symbol1.full_name(), symbol2.full_name()); } #[test] fn test_add_dimension_evaluator_invalid_path() { - let schema = create_visitors_schema(); - let evaluator = Rc::new(MockCubeEvaluator::new(schema)); - let sql_utils = Rc::new(MockSqlUtils); - let security_context = Rc::new(MockSecurityContext); - let timezone = Tz::UTC; + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); - let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + let result = test_compiler + .compiler + .add_dimension_evaluator("nonexistent.dimension".to_string()); - // Try to add non-existent dimension - let result = compiler.add_dimension_evaluator("nonexistent.dimension".to_string()); - - assert!(result.is_err(), "Should fail for non-existent dimension"); + assert!(result.is_err()); } #[test] fn test_add_dimension_evaluator_multiple_dimensions() { - let schema = create_visitors_schema(); - let evaluator = Rc::new(MockCubeEvaluator::new(schema)); - let sql_utils = Rc::new(MockSqlUtils); - let security_context = Rc::new(MockSecurityContext); - let timezone = Tz::UTC; - - let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); - // Add multiple different dimensions - let id_symbol = compiler + let id_symbol = test_compiler + .compiler .add_dimension_evaluator("visitors.id".to_string()) .unwrap(); - let source_symbol = compiler + let source_symbol = test_compiler + .compiler .add_dimension_evaluator("visitors.source".to_string()) .unwrap(); - let created_at_symbol = compiler + let created_at_symbol = test_compiler + .compiler .add_dimension_evaluator("visitors.created_at".to_string()) .unwrap(); - // Verify each dimension assert_eq!(id_symbol.full_name(), "visitors.id"); assert_eq!(id_symbol.as_dimension().unwrap().dimension_type(), "number"); - assert_eq!(source_symbol.full_name(), "visitors.source"); - assert_eq!( - source_symbol.as_dimension().unwrap().dimension_type(), - "string" - ); - + assert_eq!(source_symbol.as_dimension().unwrap().dimension_type(), "string"); assert_eq!(created_at_symbol.full_name(), "visitors.created_at"); - assert_eq!( - created_at_symbol.as_dimension().unwrap().dimension_type(), - "time" - ); - - // All should have no dependencies + assert_eq!(created_at_symbol.as_dimension().unwrap().dimension_type(), "time"); assert_eq!(id_symbol.get_dependencies().len(), 0); assert_eq!(source_symbol.get_dependencies().len(), 0); assert_eq!(created_at_symbol.get_dependencies().len(), 0); @@ -332,149 +265,128 @@ mod tests { #[test] fn test_add_measure_evaluator_count_measure() { - let schema = create_visitors_schema(); - let evaluator = Rc::new(MockCubeEvaluator::new(schema)); - let sql_utils = Rc::new(MockSqlUtils); - let security_context = Rc::new(MockSecurityContext); - let timezone = Tz::UTC; - - let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); - let symbol = compiler + let symbol = test_compiler + .compiler .add_measure_evaluator("visitor_checkins.count".to_string()) .unwrap(); - // Check symbol type assert!(symbol.is_measure()); assert!(!symbol.is_dimension()); - - // Check full name assert_eq!(symbol.full_name(), "visitor_checkins.count"); - - // Check cube name and member name assert_eq!(symbol.cube_name(), "visitor_checkins"); assert_eq!(symbol.name(), "count"); - - // Check no dependencies for simple measure - let dependencies = symbol.get_dependencies(); - assert_eq!( - dependencies.len(), - 0, - "Simple measure should have no dependencies" - ); - - // Check measure type - let measure = symbol.as_measure().unwrap(); - assert_eq!(measure.measure_type(), "count"); + assert_eq!(symbol.get_dependencies().len(), 0); + assert_eq!(symbol.as_measure().unwrap().measure_type(), "count"); } #[test] fn test_add_measure_evaluator_sum_measure() { - let schema = create_visitors_schema(); - let evaluator = Rc::new(MockCubeEvaluator::new(schema)); - let sql_utils = Rc::new(MockSqlUtils); - let security_context = Rc::new(MockSecurityContext); - let timezone = Tz::UTC; - - let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); - let symbol = compiler + let symbol = test_compiler + .compiler .add_measure_evaluator("visitors.total_revenue".to_string()) .unwrap(); - // Check symbol type assert!(symbol.is_measure()); assert!(!symbol.is_dimension()); - - // Check full name assert_eq!(symbol.full_name(), "visitors.total_revenue"); - - // Check cube name and member name assert_eq!(symbol.cube_name(), "visitors"); assert_eq!(symbol.name(), "total_revenue"); - - // Check no dependencies for simple measure - let dependencies = symbol.get_dependencies(); - assert_eq!( - dependencies.len(), - 0, - "Simple measure should have no dependencies" - ); - - // Check measure type - let measure = symbol.as_measure().unwrap(); - assert_eq!(measure.measure_type(), "sum"); + assert_eq!(symbol.get_dependencies().len(), 0); + assert_eq!(symbol.as_measure().unwrap().measure_type(), "sum"); } #[test] fn test_add_measure_evaluator_caching() { - let schema = create_visitors_schema(); - let evaluator = Rc::new(MockCubeEvaluator::new(schema)); - let sql_utils = Rc::new(MockSqlUtils); - let security_context = Rc::new(MockSecurityContext); - let timezone = Tz::UTC; + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); - let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); - - // Add measure twice - let symbol1 = compiler + let symbol1 = test_compiler + .compiler .add_measure_evaluator("visitors.total_revenue".to_string()) .unwrap(); - let symbol2 = compiler + let symbol2 = test_compiler + .compiler .add_measure_evaluator("visitors.total_revenue".to_string()) .unwrap(); - // Should return the same cached instance - assert_eq!( - symbol1.full_name(), - symbol2.full_name(), - "Cached symbols should have the same full name" - ); + assert_eq!(symbol1.full_name(), symbol2.full_name()); } #[test] fn test_add_measure_evaluator_invalid_path() { - let schema = create_visitors_schema(); - let evaluator = Rc::new(MockCubeEvaluator::new(schema)); - let sql_utils = Rc::new(MockSqlUtils); - let security_context = Rc::new(MockSecurityContext); - let timezone = Tz::UTC; + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); - let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + let result = test_compiler + .compiler + .add_measure_evaluator("nonexistent.measure".to_string()); - // Try to add non-existent measure - let result = compiler.add_measure_evaluator("nonexistent.measure".to_string()); - - assert!(result.is_err(), "Should fail for non-existent measure"); + assert!(result.is_err()); } #[test] fn test_add_measure_evaluator_multiple_measures() { - let schema = create_visitors_schema(); - let evaluator = Rc::new(MockCubeEvaluator::new(schema)); - let sql_utils = Rc::new(MockSqlUtils); - let security_context = Rc::new(MockSecurityContext); - let timezone = Tz::UTC; - - let mut compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); - // Add multiple different measures - let count_symbol = compiler + let count_symbol = test_compiler + .compiler .add_measure_evaluator("visitor_checkins.count".to_string()) .unwrap(); - let revenue_symbol = compiler + let revenue_symbol = test_compiler + .compiler .add_measure_evaluator("visitors.total_revenue".to_string()) .unwrap(); - // Verify each measure assert_eq!(count_symbol.full_name(), "visitor_checkins.count"); assert_eq!(count_symbol.as_measure().unwrap().measure_type(), "count"); - assert_eq!(revenue_symbol.full_name(), "visitors.total_revenue"); assert_eq!(revenue_symbol.as_measure().unwrap().measure_type(), "sum"); - - // All should have no dependencies assert_eq!(count_symbol.get_dependencies().len(), 0); assert_eq!(revenue_symbol.get_dependencies().len(), 0); } -} + + #[test] + fn test_add_auto_resolved_member_evaluator_dimension() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let symbol = test_compiler + .compiler + .add_auto_resolved_member_evaluator("visitors.source".to_string()) + .unwrap(); + + assert!(symbol.is_dimension()); + assert!(!symbol.is_measure()); + assert_eq!(symbol.full_name(), "visitors.source"); + assert_eq!(symbol.cube_name(), "visitors"); + assert_eq!(symbol.name(), "source"); + assert_eq!(symbol.get_dependencies().len(), 0); + assert_eq!(symbol.as_dimension().unwrap().dimension_type(), "string"); + } + + #[test] + fn test_add_auto_resolved_member_evaluator_measure() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let symbol = test_compiler + .compiler + .add_auto_resolved_member_evaluator("visitors.total_revenue".to_string()) + .unwrap(); + + assert!(symbol.is_measure()); + assert!(!symbol.is_dimension()); + assert_eq!(symbol.full_name(), "visitors.total_revenue"); + assert_eq!(symbol.cube_name(), "visitors"); + assert_eq!(symbol.name(), "total_revenue"); + assert_eq!(symbol.get_dependencies().len(), 0); + assert_eq!(symbol.as_measure().unwrap().measure_type(), "sum"); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs index 313be9a61249e..84a722cdaea83 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs @@ -1,5 +1,6 @@ use crate::test_fixtures::cube_bridge::{ - MockCubeDefinition, MockDimensionDefinition, MockMeasureDefinition, MockSegmentDefinition, + MockCubeDefinition, MockCubeEvaluator, MockDimensionDefinition, MockMeasureDefinition, + MockSecurityContext, MockSegmentDefinition, MockSqlUtils, }; use std::collections::HashMap; use std::rc::Rc; @@ -60,6 +61,19 @@ impl MockSchema { pub fn cube_names(&self) -> Vec<&String> { self.cubes.keys().collect() } + + /// Create a MockCubeEvaluator from this schema + pub fn create_evaluator(self) -> Rc { + Rc::new(MockCubeEvaluator::new(self)) + } + + /// Create a MockCubeEvaluator with primary keys from this schema + pub fn create_evaluator_with_primary_keys( + self, + primary_keys: std::collections::HashMap>, + ) -> Rc { + Rc::new(MockCubeEvaluator::with_primary_keys(self, primary_keys)) + } } /// Builder for MockSchema with fluent API diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/mod.rs index a54bba2083605..4e7d8244a5a47 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/mod.rs @@ -1,3 +1,29 @@ pub mod visitors_schema; -pub use visitors_schema::create_visitors_schema; \ No newline at end of file +pub use visitors_schema::create_visitors_schema; + +use crate::planner::sql_evaluator::Compiler; +use crate::test_fixtures::cube_bridge::{MockCubeEvaluator, MockSecurityContext, MockSqlUtils}; +use chrono_tz::Tz; +use std::rc::Rc; + +/// Helper struct that bundles together a Compiler with its dependencies for testing +pub struct TestCompiler { + pub compiler: Compiler, +} + +impl TestCompiler { + /// Create a new TestCompiler from a MockCubeEvaluator + pub fn new(evaluator: Rc) -> Self { + Self::new_with_timezone(evaluator, Tz::UTC) + } + + /// Create a new TestCompiler with a specific timezone + pub fn new_with_timezone(evaluator: Rc, timezone: Tz) -> Self { + let sql_utils = Rc::new(MockSqlUtils); + let security_context = Rc::new(MockSecurityContext); + let compiler = Compiler::new(evaluator, sql_utils, security_context, timezone); + + Self { compiler } + } +} \ No newline at end of file From f757159a8e8dce5a53db47b242985c0e5ada6d6f Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 20:28:54 +0100 Subject: [PATCH 21/42] compiler tests in work --- rust/cubesqlplanner/cubesqlplanner/src/lib.rs | 2 + .../src/planner/sql_evaluator/compiler.rs | 229 --------------- .../src/tests/cube_evaluator/compilation.rs | 268 ++++++++++++++++++ .../src/tests/cube_evaluator/mod.rs | 1 + .../cubesqlplanner/src/tests/mod.rs | 1 + 5 files changed, 272 insertions(+), 229 deletions(-) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/mod.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/tests/mod.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/lib.rs b/rust/cubesqlplanner/cubesqlplanner/src/lib.rs index b872291090504..a4e4354a1f844 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/lib.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/lib.rs @@ -5,4 +5,6 @@ pub mod plan; pub mod planner; #[cfg(test)] pub(crate) mod test_fixtures; +#[cfg(test)] +mod tests; pub(crate) mod utils; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs index c470476b688bd..9f78cc5622785 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs @@ -160,233 +160,4 @@ impl Compiler { } Ok(node) } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_fixtures::schemas::{create_visitors_schema, TestCompiler}; - - #[test] - fn test_add_dimension_evaluator_number_dimension() { - let evaluator = create_visitors_schema().create_evaluator(); - let mut test_compiler = TestCompiler::new(evaluator); - - let symbol = test_compiler - .compiler - .add_dimension_evaluator("visitors.id".to_string()) - .unwrap(); - - assert!(symbol.is_dimension()); - assert!(!symbol.is_measure()); - assert_eq!(symbol.full_name(), "visitors.id"); - assert_eq!(symbol.cube_name(), "visitors"); - assert_eq!(symbol.name(), "id"); - assert_eq!(symbol.get_dependencies().len(), 0); - assert_eq!(symbol.as_dimension().unwrap().dimension_type(), "number"); - } - - #[test] - fn test_add_dimension_evaluator_string_dimension() { - let evaluator = create_visitors_schema().create_evaluator(); - let mut test_compiler = TestCompiler::new(evaluator); - - let symbol = test_compiler - .compiler - .add_dimension_evaluator("visitors.source".to_string()) - .unwrap(); - - assert!(symbol.is_dimension()); - assert!(!symbol.is_measure()); - assert_eq!(symbol.full_name(), "visitors.source"); - assert_eq!(symbol.cube_name(), "visitors"); - assert_eq!(symbol.name(), "source"); - assert_eq!(symbol.get_dependencies().len(), 0); - assert_eq!(symbol.as_dimension().unwrap().dimension_type(), "string"); - } - - #[test] - fn test_add_dimension_evaluator_caching() { - let evaluator = create_visitors_schema().create_evaluator(); - let mut test_compiler = TestCompiler::new(evaluator); - - let symbol1 = test_compiler - .compiler - .add_dimension_evaluator("visitors.id".to_string()) - .unwrap(); - let symbol2 = test_compiler - .compiler - .add_dimension_evaluator("visitors.id".to_string()) - .unwrap(); - - assert_eq!(symbol1.full_name(), symbol2.full_name()); - } - - #[test] - fn test_add_dimension_evaluator_invalid_path() { - let evaluator = create_visitors_schema().create_evaluator(); - let mut test_compiler = TestCompiler::new(evaluator); - - let result = test_compiler - .compiler - .add_dimension_evaluator("nonexistent.dimension".to_string()); - - assert!(result.is_err()); - } - - #[test] - fn test_add_dimension_evaluator_multiple_dimensions() { - let evaluator = create_visitors_schema().create_evaluator(); - let mut test_compiler = TestCompiler::new(evaluator); - - let id_symbol = test_compiler - .compiler - .add_dimension_evaluator("visitors.id".to_string()) - .unwrap(); - let source_symbol = test_compiler - .compiler - .add_dimension_evaluator("visitors.source".to_string()) - .unwrap(); - let created_at_symbol = test_compiler - .compiler - .add_dimension_evaluator("visitors.created_at".to_string()) - .unwrap(); - - assert_eq!(id_symbol.full_name(), "visitors.id"); - assert_eq!(id_symbol.as_dimension().unwrap().dimension_type(), "number"); - assert_eq!(source_symbol.full_name(), "visitors.source"); - assert_eq!(source_symbol.as_dimension().unwrap().dimension_type(), "string"); - assert_eq!(created_at_symbol.full_name(), "visitors.created_at"); - assert_eq!(created_at_symbol.as_dimension().unwrap().dimension_type(), "time"); - assert_eq!(id_symbol.get_dependencies().len(), 0); - assert_eq!(source_symbol.get_dependencies().len(), 0); - assert_eq!(created_at_symbol.get_dependencies().len(), 0); - } - - #[test] - fn test_add_measure_evaluator_count_measure() { - let evaluator = create_visitors_schema().create_evaluator(); - let mut test_compiler = TestCompiler::new(evaluator); - - let symbol = test_compiler - .compiler - .add_measure_evaluator("visitor_checkins.count".to_string()) - .unwrap(); - - assert!(symbol.is_measure()); - assert!(!symbol.is_dimension()); - assert_eq!(symbol.full_name(), "visitor_checkins.count"); - assert_eq!(symbol.cube_name(), "visitor_checkins"); - assert_eq!(symbol.name(), "count"); - assert_eq!(symbol.get_dependencies().len(), 0); - assert_eq!(symbol.as_measure().unwrap().measure_type(), "count"); - } - - #[test] - fn test_add_measure_evaluator_sum_measure() { - let evaluator = create_visitors_schema().create_evaluator(); - let mut test_compiler = TestCompiler::new(evaluator); - - let symbol = test_compiler - .compiler - .add_measure_evaluator("visitors.total_revenue".to_string()) - .unwrap(); - - assert!(symbol.is_measure()); - assert!(!symbol.is_dimension()); - assert_eq!(symbol.full_name(), "visitors.total_revenue"); - assert_eq!(symbol.cube_name(), "visitors"); - assert_eq!(symbol.name(), "total_revenue"); - assert_eq!(symbol.get_dependencies().len(), 0); - assert_eq!(symbol.as_measure().unwrap().measure_type(), "sum"); - } - - #[test] - fn test_add_measure_evaluator_caching() { - let evaluator = create_visitors_schema().create_evaluator(); - let mut test_compiler = TestCompiler::new(evaluator); - - let symbol1 = test_compiler - .compiler - .add_measure_evaluator("visitors.total_revenue".to_string()) - .unwrap(); - let symbol2 = test_compiler - .compiler - .add_measure_evaluator("visitors.total_revenue".to_string()) - .unwrap(); - - assert_eq!(symbol1.full_name(), symbol2.full_name()); - } - - #[test] - fn test_add_measure_evaluator_invalid_path() { - let evaluator = create_visitors_schema().create_evaluator(); - let mut test_compiler = TestCompiler::new(evaluator); - - let result = test_compiler - .compiler - .add_measure_evaluator("nonexistent.measure".to_string()); - - assert!(result.is_err()); - } - - #[test] - fn test_add_measure_evaluator_multiple_measures() { - let evaluator = create_visitors_schema().create_evaluator(); - let mut test_compiler = TestCompiler::new(evaluator); - - let count_symbol = test_compiler - .compiler - .add_measure_evaluator("visitor_checkins.count".to_string()) - .unwrap(); - let revenue_symbol = test_compiler - .compiler - .add_measure_evaluator("visitors.total_revenue".to_string()) - .unwrap(); - - assert_eq!(count_symbol.full_name(), "visitor_checkins.count"); - assert_eq!(count_symbol.as_measure().unwrap().measure_type(), "count"); - assert_eq!(revenue_symbol.full_name(), "visitors.total_revenue"); - assert_eq!(revenue_symbol.as_measure().unwrap().measure_type(), "sum"); - assert_eq!(count_symbol.get_dependencies().len(), 0); - assert_eq!(revenue_symbol.get_dependencies().len(), 0); - } - - #[test] - fn test_add_auto_resolved_member_evaluator_dimension() { - let evaluator = create_visitors_schema().create_evaluator(); - let mut test_compiler = TestCompiler::new(evaluator); - - let symbol = test_compiler - .compiler - .add_auto_resolved_member_evaluator("visitors.source".to_string()) - .unwrap(); - - assert!(symbol.is_dimension()); - assert!(!symbol.is_measure()); - assert_eq!(symbol.full_name(), "visitors.source"); - assert_eq!(symbol.cube_name(), "visitors"); - assert_eq!(symbol.name(), "source"); - assert_eq!(symbol.get_dependencies().len(), 0); - assert_eq!(symbol.as_dimension().unwrap().dimension_type(), "string"); - } - - #[test] - fn test_add_auto_resolved_member_evaluator_measure() { - let evaluator = create_visitors_schema().create_evaluator(); - let mut test_compiler = TestCompiler::new(evaluator); - - let symbol = test_compiler - .compiler - .add_auto_resolved_member_evaluator("visitors.total_revenue".to_string()) - .unwrap(); - - assert!(symbol.is_measure()); - assert!(!symbol.is_dimension()); - assert_eq!(symbol.full_name(), "visitors.total_revenue"); - assert_eq!(symbol.cube_name(), "visitors"); - assert_eq!(symbol.name(), "total_revenue"); - assert_eq!(symbol.get_dependencies().len(), 0); - assert_eq!(symbol.as_measure().unwrap().measure_type(), "sum"); - } } \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs new file mode 100644 index 0000000000000..e69393831d708 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs @@ -0,0 +1,268 @@ +//! Tests for Compiler member evaluation + +use crate::planner::sql_evaluator::Compiler; +use crate::test_fixtures::schemas::{create_visitors_schema, TestCompiler}; + +#[test] +fn test_add_dimension_evaluator_number_dimension() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let symbol = test_compiler + .compiler + .add_dimension_evaluator("visitors.id".to_string()) + .unwrap(); + + assert!(symbol.is_dimension()); + assert!(!symbol.is_measure()); + assert_eq!(symbol.full_name(), "visitors.id"); + assert_eq!(symbol.cube_name(), "visitors"); + assert_eq!(symbol.name(), "id"); + assert_eq!(symbol.get_dependencies().len(), 0); + assert_eq!(symbol.as_dimension().unwrap().dimension_type(), "number"); +} + +#[test] +fn test_add_dimension_evaluator_string_dimension() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let symbol = test_compiler + .compiler + .add_dimension_evaluator("visitors.source".to_string()) + .unwrap(); + + assert!(symbol.is_dimension()); + assert!(!symbol.is_measure()); + assert_eq!(symbol.full_name(), "visitors.source"); + assert_eq!(symbol.cube_name(), "visitors"); + assert_eq!(symbol.name(), "source"); + assert_eq!(symbol.get_dependencies().len(), 0); + assert_eq!(symbol.as_dimension().unwrap().dimension_type(), "string"); +} + +#[test] +fn test_add_dimension_evaluator_caching() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let symbol1 = test_compiler + .compiler + .add_dimension_evaluator("visitors.id".to_string()) + .unwrap(); + let symbol2 = test_compiler + .compiler + .add_dimension_evaluator("visitors.id".to_string()) + .unwrap(); + + assert_eq!(symbol1.full_name(), symbol2.full_name()); +} + +#[test] +fn test_add_dimension_evaluator_invalid_path() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let result = test_compiler + .compiler + .add_dimension_evaluator("nonexistent.dimension".to_string()); + + assert!(result.is_err()); +} + +#[test] +fn test_add_dimension_evaluator_multiple_dimensions() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let id_symbol = test_compiler + .compiler + .add_dimension_evaluator("visitors.id".to_string()) + .unwrap(); + let source_symbol = test_compiler + .compiler + .add_dimension_evaluator("visitors.source".to_string()) + .unwrap(); + let created_at_symbol = test_compiler + .compiler + .add_dimension_evaluator("visitors.created_at".to_string()) + .unwrap(); + + assert_eq!(id_symbol.full_name(), "visitors.id"); + assert_eq!(id_symbol.as_dimension().unwrap().dimension_type(), "number"); + assert_eq!(source_symbol.full_name(), "visitors.source"); + assert_eq!( + source_symbol.as_dimension().unwrap().dimension_type(), + "string" + ); + assert_eq!(created_at_symbol.full_name(), "visitors.created_at"); + assert_eq!( + created_at_symbol.as_dimension().unwrap().dimension_type(), + "time" + ); + assert_eq!(id_symbol.get_dependencies().len(), 0); + assert_eq!(source_symbol.get_dependencies().len(), 0); + assert_eq!(created_at_symbol.get_dependencies().len(), 0); +} + +#[test] +fn test_add_measure_evaluator_count_measure() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let symbol = test_compiler + .compiler + .add_measure_evaluator("visitor_checkins.count".to_string()) + .unwrap(); + + assert!(symbol.is_measure()); + assert!(!symbol.is_dimension()); + assert_eq!(symbol.full_name(), "visitor_checkins.count"); + assert_eq!(symbol.cube_name(), "visitor_checkins"); + assert_eq!(symbol.name(), "count"); + assert_eq!(symbol.get_dependencies().len(), 0); + assert_eq!(symbol.as_measure().unwrap().measure_type(), "count"); +} + +#[test] +fn test_add_measure_evaluator_sum_measure() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let symbol = test_compiler + .compiler + .add_measure_evaluator("visitors.total_revenue".to_string()) + .unwrap(); + + assert!(symbol.is_measure()); + assert!(!symbol.is_dimension()); + assert_eq!(symbol.full_name(), "visitors.total_revenue"); + assert_eq!(symbol.cube_name(), "visitors"); + assert_eq!(symbol.name(), "total_revenue"); + assert_eq!(symbol.get_dependencies().len(), 0); + assert_eq!(symbol.as_measure().unwrap().measure_type(), "sum"); +} + +#[test] +fn test_add_measure_evaluator_caching() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let symbol1 = test_compiler + .compiler + .add_measure_evaluator("visitors.total_revenue".to_string()) + .unwrap(); + let symbol2 = test_compiler + .compiler + .add_measure_evaluator("visitors.total_revenue".to_string()) + .unwrap(); + + assert_eq!(symbol1.full_name(), symbol2.full_name()); +} + +#[test] +fn test_add_measure_evaluator_invalid_path() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let result = test_compiler + .compiler + .add_measure_evaluator("nonexistent.measure".to_string()); + + assert!(result.is_err()); +} + +#[test] +fn test_add_measure_evaluator_multiple_measures() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let count_symbol = test_compiler + .compiler + .add_measure_evaluator("visitor_checkins.count".to_string()) + .unwrap(); + let revenue_symbol = test_compiler + .compiler + .add_measure_evaluator("visitors.total_revenue".to_string()) + .unwrap(); + + assert_eq!(count_symbol.full_name(), "visitor_checkins.count"); + assert_eq!(count_symbol.as_measure().unwrap().measure_type(), "count"); + assert_eq!(revenue_symbol.full_name(), "visitors.total_revenue"); + assert_eq!(revenue_symbol.as_measure().unwrap().measure_type(), "sum"); + assert_eq!(count_symbol.get_dependencies().len(), 0); + assert_eq!(revenue_symbol.get_dependencies().len(), 0); +} + +#[test] +fn test_add_auto_resolved_member_evaluator_dimension() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let symbol = test_compiler + .compiler + .add_auto_resolved_member_evaluator("visitors.source".to_string()) + .unwrap(); + + assert!(symbol.is_dimension()); + assert!(!symbol.is_measure()); + assert_eq!(symbol.full_name(), "visitors.source"); + assert_eq!(symbol.cube_name(), "visitors"); + assert_eq!(symbol.name(), "source"); + assert_eq!(symbol.get_dependencies().len(), 0); + assert_eq!(symbol.as_dimension().unwrap().dimension_type(), "string"); +} + +#[test] +fn test_add_auto_resolved_member_evaluator_measure() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let symbol = test_compiler + .compiler + .add_auto_resolved_member_evaluator("visitors.total_revenue".to_string()) + .unwrap(); + + assert!(symbol.is_measure()); + assert!(!symbol.is_dimension()); + assert_eq!(symbol.full_name(), "visitors.total_revenue"); + assert_eq!(symbol.cube_name(), "visitors"); + assert_eq!(symbol.name(), "total_revenue"); + assert_eq!(symbol.get_dependencies().len(), 0); + assert_eq!(symbol.as_measure().unwrap().measure_type(), "sum"); +} + +#[test] +fn test_add_cube_table_evaluator() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let symbol = test_compiler + .compiler + .add_cube_table_evaluator("visitors".to_string()) + .unwrap(); + + assert!(symbol.is_cube()); + assert!(!symbol.is_dimension()); + assert!(!symbol.is_measure()); + assert_eq!(symbol.full_name(), "visitors"); + assert_eq!(symbol.cube_name(), "visitors"); +} + +#[test] +fn test_add_cube_name_evaluator() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let symbol = test_compiler + .compiler + .add_cube_name_evaluator("visitors".to_string()) + .unwrap(); + + assert!(symbol.is_cube()); + assert!(!symbol.is_dimension()); + assert!(!symbol.is_measure()); + assert_eq!(symbol.full_name(), "visitors"); + assert_eq!(symbol.cube_name(), "visitors"); +} + diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/mod.rs new file mode 100644 index 0000000000000..ca7cfbb04fe79 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/mod.rs @@ -0,0 +1 @@ +mod compilation; \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/mod.rs new file mode 100644 index 0000000000000..507bada3fdeb8 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/mod.rs @@ -0,0 +1 @@ +mod cube_evaluator; \ No newline at end of file From f332a7212bbf90336afa3d3754f70acd66cec64f Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 20:55:42 +0100 Subject: [PATCH 22/42] compiler tests in work --- .../test_fixtures/schemas/visitors_schema.rs | 37 +++- .../src/tests/cube_evaluator/compilation.rs | 163 ++++++++++++++++++ 2 files changed, 195 insertions(+), 5 deletions(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs index c8bdd21dea6df..89194629f92d9 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs @@ -64,7 +64,14 @@ pub fn create_visitors_schema() -> MockSchema { "visitor_id", MockDimensionDefinition::builder() .dimension_type("number".to_string()) - .sql("visitor_id".to_string()) + .sql("{CUBE}.visitor_id".to_string()) + .build(), + ) + .add_dimension( + "visitor_id_twice", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("{visitor_id} * 2".to_string()) .build(), ) .add_dimension( @@ -74,6 +81,13 @@ pub fn create_visitors_schema() -> MockSchema { .sql("source".to_string()) .build(), ) + .add_dimension( + "source_concat_id", + MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("CONCAT({CUBE.source}, ' ', {visitors.visitor_id})".to_string()) + .build(), + ) .add_dimension( "created_at", MockDimensionDefinition::builder() @@ -133,6 +147,20 @@ pub fn create_visitors_schema() -> MockSchema { .sql("revenue".to_string()) .build(), ) + .add_measure( + "revenue", + MockMeasureDefinition::builder() + .measure_type("sum".to_string()) + .sql("{CUBE}.revenue".to_string()) + .build(), + ) + .add_measure( + "total_revenue_per_count", + MockMeasureDefinition::builder() + .measure_type("number".to_string()) + .sql("{visitors.count} / {total_revenue}".to_string()) + .build(), + ) .add_segment( "google", MockSegmentDefinition::builder() @@ -213,9 +241,7 @@ mod tests { .get_dimension("visitor_checkins", "visitor_id") .is_some()); - let min_date = schema - .get_dimension("visitor_checkins", "minDate") - .unwrap(); + let min_date = schema.get_dimension("visitor_checkins", "minDate").unwrap(); assert_eq!(min_date.static_data().dimension_type, "time"); let min_date1 = schema @@ -281,4 +307,5 @@ mod tests { let lon_sql = longitude.sql().unwrap(); assert_eq!(lon_sql.args_names().len(), 0); } -} \ No newline at end of file +} + diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs index e69393831d708..970c9035d5558 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs @@ -266,3 +266,166 @@ fn test_add_cube_name_evaluator() { assert_eq!(symbol.cube_name(), "visitors"); } +// Tests for dimensions and measures with dependencies + +#[test] +fn test_dimension_with_cube_table_dependency() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + // visitor_id has dependency on {CUBE.}visitor_id + let symbol = test_compiler + .compiler + .add_dimension_evaluator("visitors.visitor_id".to_string()) + .unwrap(); + + assert!(symbol.is_dimension()); + assert_eq!(symbol.full_name(), "visitors.visitor_id"); + assert_eq!(symbol.cube_name(), "visitors"); + assert_eq!(symbol.as_dimension().unwrap().dimension_type(), "number"); + + // Should have 1 dependency: CubeTable + let dependencies = symbol.get_dependencies(); + assert_eq!(dependencies.len(), 1, "Should have 1 dependency on CUBE"); + + let dep = &dependencies[0]; + assert!(dep.is_cube(), "Dependency should be a cube symbol"); + assert_eq!(dep.full_name(), "visitors"); + assert_eq!(dep.cube_name(), "visitors"); +} + +#[test] +fn test_dimension_with_member_dependency_no_prefix() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + // visitor_id_twice has dependency on {visitor_id} without cube prefix + let symbol = test_compiler + .compiler + .add_dimension_evaluator("visitors.visitor_id_twice".to_string()) + .unwrap(); + + assert!(symbol.is_dimension()); + assert_eq!(symbol.full_name(), "visitors.visitor_id_twice"); + assert_eq!(symbol.cube_name(), "visitors"); + assert_eq!(symbol.as_dimension().unwrap().dimension_type(), "number"); + + // Should have 1 dependency: visitor_id dimension + let dependencies = symbol.get_dependencies(); + assert_eq!( + dependencies.len(), + 1, + "Should have 1 dependency on visitor_id" + ); + + let dep = &dependencies[0]; + assert!(dep.is_dimension(), "Dependency should be a dimension"); + assert_eq!(dep.full_name(), "visitors.visitor_id"); + assert_eq!(dep.cube_name(), "visitors"); +} + +#[test] +fn test_dimension_with_mixed_dependencies() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + // source_concat_id has dependencies on {CUBE.source} and {visitors.visitor_id} + let symbol = test_compiler + .compiler + .add_dimension_evaluator("visitors.source_concat_id".to_string()) + .unwrap(); + + assert!(symbol.is_dimension()); + assert_eq!(symbol.full_name(), "visitors.source_concat_id"); + assert_eq!(symbol.cube_name(), "visitors"); + assert_eq!(symbol.as_dimension().unwrap().dimension_type(), "string"); + + // Should have 2 dependencies: visitors.source and visitors.visitor_id + let dependencies = symbol.get_dependencies(); + assert_eq!( + dependencies.len(), + 2, + "Should have 2 dimension dependencies" + ); + + // Both should be dimensions + for dep in &dependencies { + assert!(dep.is_dimension(), "All dependencies should be dimensions"); + assert_eq!(dep.cube_name(), "visitors"); + } + + // Check we have both expected dependencies + let dep_names: Vec = dependencies.iter().map(|d| d.full_name()).collect(); + assert!( + dep_names.contains(&"visitors.source".to_string()), + "Should have dependency on visitors.source" + ); + assert!( + dep_names.contains(&"visitors.visitor_id".to_string()), + "Should have dependency on visitors.visitor_id" + ); +} + +#[test] +fn test_measure_with_cube_table_dependency() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + // revenue has dependency on {CUBE}.revenue + let symbol = test_compiler + .compiler + .add_measure_evaluator("visitors.revenue".to_string()) + .unwrap(); + + assert!(symbol.is_measure()); + assert_eq!(symbol.full_name(), "visitors.revenue"); + assert_eq!(symbol.cube_name(), "visitors"); + assert_eq!(symbol.as_measure().unwrap().measure_type(), "sum"); + + // Should have 1 dependency: CubeTable + let dependencies = symbol.get_dependencies(); + assert_eq!(dependencies.len(), 1, "Should have 1 dependency on CUBE"); + + let dep = &dependencies[0]; + assert!(dep.is_cube(), "Dependency should be a cube symbol"); + assert_eq!(dep.full_name(), "visitors"); + assert_eq!(dep.cube_name(), "visitors"); +} + +#[test] +fn test_measure_with_explicit_cube_and_member_dependencies() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + // total_revenue_per_count has dependencies on {visitors.count} and {total_revenue} + let symbol = test_compiler + .compiler + .add_measure_evaluator("visitors.total_revenue_per_count".to_string()) + .unwrap(); + + assert!(symbol.is_measure()); + assert_eq!(symbol.full_name(), "visitors.total_revenue_per_count"); + assert_eq!(symbol.cube_name(), "visitors"); + assert_eq!(symbol.as_measure().unwrap().measure_type(), "number"); + + // Should have 2 dependencies: visitors.count and total_revenue + let dependencies = symbol.get_dependencies(); + assert_eq!(dependencies.len(), 2, "Should have 2 measure dependencies"); + + // Both should be measures + for dep in &dependencies { + assert!(dep.is_measure(), "All dependencies should be measures"); + assert_eq!(dep.cube_name(), "visitors"); + } + + // Check we have both expected dependencies + let dep_names: Vec = dependencies.iter().map(|d| d.full_name()).collect(); + assert!( + dep_names.contains(&"visitors.count".to_string()), + "Should have dependency on visitors.count" + ); + assert!( + dep_names.contains(&"visitors.total_revenue".to_string()), + "Should have dependency on visitors.total_revenue" + ); +} From f140d84946140db0d8838b66618ee68499c9d8ea Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 21:43:32 +0100 Subject: [PATCH 23/42] view mock --- .../test_fixtures/cube_bridge/mock_schema.rs | 523 +++++++++++++++++- 1 file changed, 522 insertions(+), 1 deletion(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs index 84a722cdaea83..3bb47a144d5b9 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs @@ -1,6 +1,6 @@ use crate::test_fixtures::cube_bridge::{ MockCubeDefinition, MockCubeEvaluator, MockDimensionDefinition, MockMeasureDefinition, - MockSecurityContext, MockSegmentDefinition, MockSqlUtils, + MockSegmentDefinition, }; use std::collections::HashMap; use std::rc::Rc; @@ -101,6 +101,18 @@ impl MockSchemaBuilder { } } + /// Add a view and return a view builder + pub fn add_view(self, name: impl Into) -> MockViewBuilder { + MockViewBuilder { + schema_builder: self, + view_name: name.into(), + view_cubes: Vec::new(), + measures: HashMap::new(), + dimensions: HashMap::new(), + segments: HashMap::new(), + } + } + /// Build the final schema pub fn build(self) -> MockSchema { MockSchema { cubes: self.cubes } @@ -183,9 +195,193 @@ impl MockCubeBuilder { } } +/// Represents a cube to include in a view +pub struct ViewCube { + /// Join path to the cube (e.g., "visitors" or "visitors.visitor_checkins") + pub join_path: String, + /// Member names to include, empty vec means include all + pub includes: Vec, +} + +/// Builder for a view within a schema +pub struct MockViewBuilder { + schema_builder: MockSchemaBuilder, + view_name: String, + view_cubes: Vec, + measures: HashMap>, + dimensions: HashMap>, + segments: HashMap>, +} + +impl MockViewBuilder { + /// Add a cube to include in this view + pub fn include_cube( + mut self, + join_path: impl Into, + includes: Vec, + ) -> Self { + self.view_cubes.push(ViewCube { + join_path: join_path.into(), + includes, + }); + self + } + + /// Add a custom dimension to the view + pub fn add_dimension( + mut self, + name: impl Into, + definition: MockDimensionDefinition, + ) -> Self { + self.dimensions + .insert(name.into(), Rc::new(definition)); + self + } + + /// Add a custom measure to the view + pub fn add_measure( + mut self, + name: impl Into, + definition: MockMeasureDefinition, + ) -> Self { + self.measures.insert(name.into(), Rc::new(definition)); + self + } + + /// Add a custom segment to the view + pub fn add_segment( + mut self, + name: impl Into, + definition: MockSegmentDefinition, + ) -> Self { + self.segments.insert(name.into(), Rc::new(definition)); + self + } + + /// Finish building this view and return to schema builder + pub fn finish_view(mut self) -> MockSchemaBuilder { + let mut all_dimensions = self.dimensions; + let mut all_measures = self.measures; + let mut all_segments = self.segments; + + // Process each included cube + for view_cube in &self.view_cubes { + let join_path_parts: Vec<&str> = view_cube.join_path.split('.').collect(); + let target_cube_name = join_path_parts.last().unwrap(); + + // Get the target cube from schema + if let Some(source_cube) = self.schema_builder.cubes.get(*target_cube_name) { + // Determine which members to include + let members_to_include: Vec = if view_cube.includes.is_empty() { + // Include all members + let mut all_members = Vec::new(); + all_members.extend(source_cube.dimensions.keys().cloned()); + all_members.extend(source_cube.measures.keys().cloned()); + all_members.extend(source_cube.segments.keys().cloned()); + all_members + } else { + view_cube.includes.clone() + }; + + // Add dimensions + for member_name in &members_to_include { + if let Some(dimension) = source_cube.dimensions.get(member_name) { + let view_member_sql = format!("{{{}.{}}}", view_cube.join_path, member_name); + + // Check for duplicates + if all_dimensions.contains_key(member_name) { + panic!( + "Duplicate member '{}' in view '{}'. Members must be unique.", + member_name, self.view_name + ); + } + + all_dimensions.insert( + member_name.clone(), + Rc::new( + MockDimensionDefinition::builder() + .dimension_type(dimension.static_data().dimension_type.clone()) + .sql(view_member_sql) + .build(), + ), + ); + } + } + + // Add measures + for member_name in &members_to_include { + if let Some(measure) = source_cube.measures.get(member_name) { + let view_member_sql = format!("{{{}.{}}}", view_cube.join_path, member_name); + + // Check for duplicates + if all_measures.contains_key(member_name) { + panic!( + "Duplicate member '{}' in view '{}'. Members must be unique.", + member_name, self.view_name + ); + } + + all_measures.insert( + member_name.clone(), + Rc::new( + MockMeasureDefinition::builder() + .measure_type(measure.static_data().measure_type.clone()) + .sql(view_member_sql) + .build(), + ), + ); + } + } + + // Add segments + for member_name in &members_to_include { + if source_cube.segments.contains_key(member_name) { + let view_member_sql = format!("{{{}.{}}}", view_cube.join_path, member_name); + + // Check for duplicates + if all_segments.contains_key(member_name) { + panic!( + "Duplicate member '{}' in view '{}'. Members must be unique.", + member_name, self.view_name + ); + } + + all_segments.insert( + member_name.clone(), + Rc::new( + MockSegmentDefinition::builder() + .sql(view_member_sql) + .build(), + ), + ); + } + } + } + } + + // Create view cube definition with is_view = true + let view_def = MockCubeDefinition::builder() + .name(self.view_name.clone()) + .is_view(Some(true)) + .build(); + + let view_cube = MockCube { + definition: view_def, + measures: all_measures, + dimensions: all_dimensions, + segments: all_segments, + }; + + self.schema_builder.cubes.insert(self.view_name, view_cube); + self.schema_builder + } +} + #[cfg(test)] mod tests { use super::*; + use crate::cube_bridge::dimension_definition::DimensionDefinition; + use crate::cube_bridge::measure_definition::MeasureDefinition; use crate::cube_bridge::segment_definition::SegmentDefinition; #[test] @@ -546,4 +742,329 @@ mod tests { assert!(schema.get_segment("orders", "completed").is_some()); assert!(schema.get_segment("orders", "high_value").is_some()); } + + #[test] + fn test_view_with_includes_all() { + let schema = MockSchemaBuilder::new() + .add_cube("users") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_dimension( + "name", + MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("name".to_string()) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .build(), + ) + .finish_cube() + .add_view("users_view") + .include_cube("users", vec![]) // Include all members + .finish_view() + .build(); + + // Verify view exists and is marked as view + let view_cube = schema.get_cube("users_view").unwrap(); + assert_eq!(view_cube.definition.static_data().is_view, Some(true)); + + // Verify all members were included + assert_eq!(view_cube.dimensions.len(), 2); + assert_eq!(view_cube.measures.len(), 1); + + // Verify member SQL references original cube + let id_dim = schema.get_dimension("users_view", "id").unwrap(); + let id_sql = id_dim.sql().unwrap().unwrap(); + assert_eq!(id_sql.args_names(), &vec!["users"]); + + let count_measure = schema.get_measure("users_view", "count").unwrap(); + let count_sql = count_measure.sql().unwrap().unwrap(); + assert_eq!(count_sql.args_names(), &vec!["users"]); + } + + #[test] + fn test_view_with_specific_includes() { + let schema = MockSchemaBuilder::new() + .add_cube("users") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_dimension( + "name", + MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("name".to_string()) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .build(), + ) + .finish_cube() + .add_view("users_view") + .include_cube("users", vec!["id".to_string(), "count".to_string()]) + .finish_view() + .build(); + + let view_cube = schema.get_cube("users_view").unwrap(); + + // Only specified members should be included + assert_eq!(view_cube.dimensions.len(), 1); + assert_eq!(view_cube.measures.len(), 1); + + assert!(schema.get_dimension("users_view", "id").is_some()); + assert!(schema.get_dimension("users_view", "name").is_none()); + assert!(schema.get_measure("users_view", "count").is_some()); + } + + #[test] + fn test_view_with_custom_members() { + let schema = MockSchemaBuilder::new() + .add_cube("users") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .build(), + ) + .finish_cube() + .add_view("users_view") + .include_cube("users", vec!["id".to_string()]) + .add_dimension( + "custom_dim", + MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("CUSTOM_SQL".to_string()) + .build(), + ) + .add_measure( + "custom_measure", + MockMeasureDefinition::builder() + .measure_type("sum".to_string()) + .sql("custom_value".to_string()) + .build(), + ) + .finish_view() + .build(); + + let view_cube = schema.get_cube("users_view").unwrap(); + + // Should have both included and custom members + assert_eq!(view_cube.dimensions.len(), 2); // id + custom_dim + assert_eq!(view_cube.measures.len(), 1); // custom_measure + + assert!(schema.get_dimension("users_view", "id").is_some()); + assert!(schema.get_dimension("users_view", "custom_dim").is_some()); + assert!(schema.get_measure("users_view", "custom_measure").is_some()); + } + + #[test] + fn test_view_with_join_path() { + let schema = MockSchemaBuilder::new() + .add_cube("orders") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .build(), + ) + .finish_cube() + .add_view("orders_view") + .include_cube("users.orders", vec!["id".to_string(), "count".to_string()]) + .finish_view() + .build(); + + let view_cube = schema.get_cube("orders_view").unwrap(); + + // Verify member SQL uses full join path + let id_dim = schema.get_dimension("orders_view", "id").unwrap(); + let id_sql = id_dim.sql().unwrap().unwrap(); + assert_eq!(id_sql.args_names(), &vec!["users"]); + + let count_measure = schema.get_measure("orders_view", "count").unwrap(); + let count_sql = count_measure.sql().unwrap().unwrap(); + assert_eq!(count_sql.args_names(), &vec!["users"]); + } + + #[test] + fn test_view_with_multiple_long_join_paths() { + use crate::cube_bridge::member_sql::MemberSql; + use crate::test_fixtures::cube_bridge::{MockSecurityContext, MockSqlUtils}; + + let schema = MockSchemaBuilder::new() + .add_cube("visitors") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .build(), + ) + .finish_cube() + .add_cube("visitor_checkins") + .add_dimension( + "checkin_id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_measure( + "checkin_count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .sql("COUNT(*)".to_string()) + .build(), + ) + .finish_cube() + .add_view("multi_path_view") + .include_cube( + "visitors.visitor_checkins", + vec!["checkin_id".to_string(), "checkin_count".to_string()], + ) + .include_cube("visitors", vec!["id".to_string(), "count".to_string()]) + .finish_view() + .build(); + + let view_cube = schema.get_cube("multi_path_view").unwrap(); + + // Verify all members from both cubes are included + assert_eq!(view_cube.dimensions.len(), 2); // checkin_id + id + assert_eq!(view_cube.measures.len(), 2); // checkin_count + count + + // Verify SQL for members from first include (with long join path) + // SQL template should contain full path: {visitors.visitor_checkins.checkin_id} + let checkin_id_dim = schema.get_dimension("multi_path_view", "checkin_id").unwrap(); + let checkin_id_sql = checkin_id_dim.sql().unwrap().unwrap(); + + // Compile template and check symbol_paths structure + let (_template, args) = checkin_id_sql + .compile_template_sql(Rc::new(MockSqlUtils), Rc::new(MockSecurityContext)) + .unwrap(); + + // Should have exactly one symbol path + assert_eq!(args.symbol_paths.len(), 1, "Should have exactly one symbol path"); + + // The symbol path should be ["visitors", "visitor_checkins", "checkin_id"] + assert_eq!( + args.symbol_paths[0], + vec!["visitors", "visitor_checkins", "checkin_id"], + "Symbol path should be visitors.visitor_checkins.checkin_id" + ); + + let checkin_count_measure = schema + .get_measure("multi_path_view", "checkin_count") + .unwrap(); + let checkin_count_sql = checkin_count_measure.sql().unwrap().unwrap(); + + let (_template, args) = checkin_count_sql + .compile_template_sql(Rc::new(MockSqlUtils), Rc::new(MockSecurityContext)) + .unwrap(); + + assert_eq!(args.symbol_paths.len(), 1, "Should have exactly one symbol path"); + assert_eq!( + args.symbol_paths[0], + vec!["visitors", "visitor_checkins", "checkin_count"], + "Symbol path should be visitors.visitor_checkins.checkin_count" + ); + + // Verify SQL for members from second include (simple path) + // SQL template should be: {visitors.id} + let id_dim = schema.get_dimension("multi_path_view", "id").unwrap(); + let id_sql = id_dim.sql().unwrap().unwrap(); + + let (_template, args) = id_sql + .compile_template_sql(Rc::new(MockSqlUtils), Rc::new(MockSecurityContext)) + .unwrap(); + + assert_eq!(args.symbol_paths.len(), 1, "Should have exactly one symbol path"); + assert_eq!( + args.symbol_paths[0], + vec!["visitors", "id"], + "Symbol path should be visitors.id" + ); + + let count_measure = schema.get_measure("multi_path_view", "count").unwrap(); + let count_sql = count_measure.sql().unwrap().unwrap(); + + let (_template, args) = count_sql + .compile_template_sql(Rc::new(MockSqlUtils), Rc::new(MockSecurityContext)) + .unwrap(); + + assert_eq!(args.symbol_paths.len(), 1, "Should have exactly one symbol path"); + assert_eq!( + args.symbol_paths[0], + vec!["visitors", "count"], + "Symbol path should be visitors.count" + ); + } + + #[test] + #[should_panic(expected = "Duplicate member 'id' in view 'multi_view'")] + fn test_view_duplicate_members_panic() { + MockSchemaBuilder::new() + .add_cube("users") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .finish_cube() + .add_cube("orders") + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .finish_cube() + .add_view("multi_view") + .include_cube("users", vec![]) + .include_cube("orders", vec![]) + .finish_view() + .build(); + } } \ No newline at end of file From 49b4e9b8137cf6d5c527051b84d11b7dc2795728 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 22:07:24 +0100 Subject: [PATCH 24/42] view symbol compilation --- .../test_fixtures/schemas/visitors_schema.rs | 4 + .../src/tests/cube_evaluator/compilation.rs | 86 +++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs index 89194629f92d9..9f1fc569d537a 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs @@ -168,6 +168,10 @@ pub fn create_visitors_schema() -> MockSchema { .build(), ) .finish_cube() + .add_view("visitors_visitors_checkins") + .include_cube("visitors", vec!["id".to_string(), "source_concat_id".to_string()]) + .include_cube("visitors.visitor_checkins", vec!["visitor_id".to_string(), "count".to_string()]) + .finish_view() .build() } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs index 970c9035d5558..4cbbe42af7797 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs @@ -429,3 +429,89 @@ fn test_measure_with_explicit_cube_and_member_dependencies() { "Should have dependency on visitors.total_revenue" ); } + +#[test] +fn test_view_dimension_compilation() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + // Compile dimension from view with simple join path + let id_symbol = test_compiler + .compiler + .add_dimension_evaluator("visitors_visitors_checkins.id".to_string()) + .unwrap(); + + // Check basic properties + assert!(id_symbol.is_dimension()); + assert_eq!(id_symbol.full_name(), "visitors_visitors_checkins.id"); + assert_eq!(id_symbol.cube_name(), "visitors_visitors_checkins"); + assert_eq!(id_symbol.name(), "id"); + + // Check that it's a view member + let dimension = id_symbol.as_dimension().unwrap(); + assert!(dimension.is_view(), "Should be a view member"); + + // Check that it's a reference (view members reference original cube members) + assert!(dimension.is_reference(), "Should be a reference to original member"); + + // Resolve reference chain to get the original member + let resolved = id_symbol.clone().resolve_reference_chain(); + assert_eq!(resolved.full_name(), "visitors.id", "Should resolve to visitors.id"); + assert!(!resolved.as_dimension().unwrap().is_view(), "Resolved member should not be a view"); + + // Compile dimension from view with long join path + let visitor_id_symbol = test_compiler + .compiler + .add_dimension_evaluator("visitors_visitors_checkins.visitor_id".to_string()) + .unwrap(); + + assert!(visitor_id_symbol.is_dimension()); + assert_eq!(visitor_id_symbol.full_name(), "visitors_visitors_checkins.visitor_id"); + + let visitor_id_dim = visitor_id_symbol.as_dimension().unwrap(); + assert!(visitor_id_dim.is_view(), "Should be a view member"); + assert!(visitor_id_dim.is_reference(), "Should be a reference"); + + // Resolve to original member from visitor_checkins cube + let resolved = visitor_id_symbol.clone().resolve_reference_chain(); + assert_eq!( + resolved.full_name(), + "visitor_checkins.visitor_id", + "Should resolve to visitor_checkins.visitor_id" + ); + assert!(!resolved.as_dimension().unwrap().is_view(), "Resolved member should not be a view"); +} + +#[test] +fn test_view_measure_compilation() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + // Compile measure from view with long join path + let count_symbol = test_compiler + .compiler + .add_measure_evaluator("visitors_visitors_checkins.count".to_string()) + .unwrap(); + + // Check basic properties + assert!(count_symbol.is_measure()); + assert_eq!(count_symbol.full_name(), "visitors_visitors_checkins.count"); + assert_eq!(count_symbol.cube_name(), "visitors_visitors_checkins"); + assert_eq!(count_symbol.name(), "count"); + + // Check that it's a view member + let measure = count_symbol.as_measure().unwrap(); + assert!(measure.is_view(), "Should be a view member"); + + // Check that it's a reference + assert!(measure.is_reference(), "Should be a reference to original member"); + + // Resolve reference chain to get the original member + let resolved = count_symbol.clone().resolve_reference_chain(); + assert_eq!( + resolved.full_name(), + "visitor_checkins.count", + "Should resolve to visitor_checkins.count" + ); + assert!(!resolved.as_measure().unwrap().is_view(), "Resolved member should not be a view"); +} From e01da5af4b52938e004a4f152c78b2fe1043eae2 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 22:23:35 +0100 Subject: [PATCH 25/42] proxy symbol compilation --- .../cube_bridge/mock_dimension_definition.rs | 3 +- .../cube_bridge/mock_measure_definition.rs | 4 +- .../test_fixtures/schemas/visitors_schema.rs | 14 ++++ .../src/tests/cube_evaluator/compilation.rs | 80 +++++++++++++++++++ 4 files changed, 97 insertions(+), 4 deletions(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs index 7a10e067d0bd6..8cac0e69ae12e 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs @@ -16,7 +16,7 @@ pub struct MockDimensionDefinition { // Fields from DimensionDefinitionStatic #[builder(default = "string".to_string())] dimension_type: String, - #[builder(default)] + #[builder(default = Some(false))] owned_by_cube: Option, #[builder(default)] multi_stage: Option, @@ -447,4 +447,3 @@ mod tests { } } } - diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_measure_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_measure_definition.rs index 1d04019d61de7..dea5ebb176595 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_measure_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_measure_definition.rs @@ -20,7 +20,7 @@ pub struct MockMeasureDefinition { // Fields from MeasureDefinitionStatic #[builder(default = "number".to_string())] measure_type: String, - #[builder(default)] + #[builder(default = Some(false))] owned_by_cube: Option, #[builder(default)] multi_stage: Option, @@ -371,4 +371,4 @@ mod tests { assert_eq!(measure.static_data().multi_stage, Some(true)); assert_eq!(measure.static_data().owned_by_cube, Some(false)); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs index 9f1fc569d537a..3885269ee31bb 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs @@ -67,6 +67,13 @@ pub fn create_visitors_schema() -> MockSchema { .sql("{CUBE}.visitor_id".to_string()) .build(), ) + .add_dimension( + "visitor_id_proxy", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("{visitors.visitor_id}".to_string()) + .build(), + ) .add_dimension( "visitor_id_twice", MockDimensionDefinition::builder() @@ -147,6 +154,13 @@ pub fn create_visitors_schema() -> MockSchema { .sql("revenue".to_string()) .build(), ) + .add_measure( + "total_revenue_proxy", + MockMeasureDefinition::builder() + .measure_type("number".to_string()) + .sql("{total_revenue}".to_string()) + .build(), + ) .add_measure( "revenue", MockMeasureDefinition::builder() diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs index 4cbbe42af7797..445399a08a901 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs @@ -515,3 +515,83 @@ fn test_view_measure_compilation() { ); assert!(!resolved.as_measure().unwrap().is_view(), "Resolved member should not be a view"); } + +#[test] +fn test_proxy_dimension_compilation() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + // Compile proxy dimension that references another dimension + let proxy_symbol = test_compiler + .compiler + .add_dimension_evaluator("visitors.visitor_id_proxy".to_string()) + .unwrap(); + + // Check basic properties + assert!(proxy_symbol.is_dimension()); + assert_eq!(proxy_symbol.full_name(), "visitors.visitor_id_proxy"); + assert_eq!(proxy_symbol.cube_name(), "visitors"); + assert_eq!(proxy_symbol.name(), "visitor_id_proxy"); + + let dimension = proxy_symbol.as_dimension().unwrap(); + + // Check that it's NOT a view member + assert!(!dimension.is_view(), "Proxy should not be a view member"); + + // Check that it IS a reference (proxy references another member) + assert!(dimension.is_reference(), "Proxy should be a reference to another member"); + + // Resolve reference chain to get the target member + let resolved = proxy_symbol.clone().resolve_reference_chain(); + assert_eq!( + resolved.full_name(), + "visitors.visitor_id", + "Proxy should resolve to visitors.visitor_id" + ); + + // Verify the resolved member is not a view + assert!(!resolved.as_dimension().unwrap().is_view(), "Target member should not be a view"); + + // Verify the resolved member is also not a reference (it's the actual dimension) + assert!(!resolved.as_dimension().unwrap().is_reference(), "Target member should not be a reference"); +} + +#[test] +fn test_proxy_measure_compilation() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + // Compile proxy measure that references another measure + let proxy_symbol = test_compiler + .compiler + .add_measure_evaluator("visitors.total_revenue_proxy".to_string()) + .unwrap(); + + // Check basic properties + assert!(proxy_symbol.is_measure()); + assert_eq!(proxy_symbol.full_name(), "visitors.total_revenue_proxy"); + assert_eq!(proxy_symbol.cube_name(), "visitors"); + assert_eq!(proxy_symbol.name(), "total_revenue_proxy"); + + let measure = proxy_symbol.as_measure().unwrap(); + + // Check that it's NOT a view member + assert!(!measure.is_view(), "Proxy should not be a view member"); + + // Check that it IS a reference (proxy references another member) + assert!(measure.is_reference(), "Proxy should be a reference to another member"); + + // Resolve reference chain to get the target member + let resolved = proxy_symbol.clone().resolve_reference_chain(); + assert_eq!( + resolved.full_name(), + "visitors.total_revenue", + "Proxy should resolve to visitors.total_revenue" + ); + + // Verify the resolved member is not a view + assert!(!resolved.as_measure().unwrap().is_view(), "Target member should not be a view"); + + // Verify the resolved member is not a reference (it's the actual measure) + assert!(!resolved.as_measure().unwrap().is_reference(), "Target member should not be a reference"); +} From 37e36eaf141ce68bafb745e9d7dd3fbfe8e65e16 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 22:49:13 +0100 Subject: [PATCH 26/42] granularity mock --- .../sql_evaluator/symbols/dimension_symbol.rs | 1 + .../cube_bridge/mock_evaluator.rs | 177 ++++++++++++++++-- .../mock_granularity_definition.rs | 76 ++++++++ .../src/test_fixtures/cube_bridge/mod.rs | 2 + .../src/tests/cube_evaluator/compilation.rs | 51 +++++ 5 files changed, 287 insertions(+), 20 deletions(-) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_granularity_definition.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs index 897c22bc5c890..76f7de108637b 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs @@ -350,6 +350,7 @@ impl DimensionSymbolFactory { // 3. "cube.cube.cube...cube.member" might come from pre-agg references (as it include full join paths) // And we can not distinguish between "cube.member.granularity" and "cube.cube.member" here, // so we have to try-catch 2 variants of evaluation. + println!("!!! {}", full_name); if let Ok(iter_by_start) = cube_evaluator.parse_path("dimensions".to_string(), full_name.clone()) { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs index c115bd113817c..b34230ec6dca0 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs @@ -48,9 +48,10 @@ impl MockCubeEvaluator { ) -> Result, CubeError> { let parts: Vec = path.split('.').map(|s| s.to_string()).collect(); - if parts.len() != 2 { + // Allow 2 parts (cube.member) or 3 parts (cube.dimension.granularity for time dimensions) + if parts.len() != 2 && parts.len() != 3 { return Err(CubeError::user(format!( - "Invalid path format: '{}'. Expected format: 'cube.member'", + "Invalid path format: '{}'. Expected format: 'cube.member' or 'cube.time_dimension.granularity'", path ))); } @@ -63,7 +64,36 @@ impl MockCubeEvaluator { return Err(CubeError::user(format!("Cube '{}' not found", cube_name))); } - // Validate member exists for the given type + // If we have 3 parts, check if the dimension is a time dimension + if parts.len() == 3 { + // Only dimensions can have granularity + if path_type != "dimension" && path_type != "dimensions" { + return Err(CubeError::user(format!( + "Granularity can only be specified for dimensions, not for {}", + path_type + ))); + } + + // Check if the dimension exists and is of type 'time' + if let Some(dimension) = self.schema.get_dimension(cube_name, member_name) { + if dimension.static_data().dimension_type != "time" { + return Err(CubeError::user(format!( + "Granularity can only be specified for time dimensions, but '{}' is of type '{}'", + member_name, + dimension.static_data().dimension_type + ))); + } + // Granularity is valid - return all 3 parts + return Ok(parts); + } else { + return Err(CubeError::user(format!( + "Dimension '{}' not found in cube '{}'", + member_name, cube_name + ))); + } + } + + // For 2-part paths, validate member exists for the given type let exists = match path_type { "measure" | "measures" => self.schema.get_measure(cube_name, member_name).is_some(), "dimension" | "dimensions" => { @@ -189,9 +219,44 @@ impl CubeEvaluator for MockCubeEvaluator { fn resolve_granularity( &self, - _path: Vec, + path: Vec, ) -> Result, CubeError> { - todo!("resolve_granularity is not implemented in MockCubeEvaluator") + // path should be [cube_name, dimension_name, "granularities", granularity] + if path.len() != 4 { + return Err(CubeError::user(format!( + "Invalid granularity path: expected 4 parts (cube.dimension.granularities.granularity), got {}", + path.len() + ))); + } + + if path[2] != "granularities" { + return Err(CubeError::user(format!( + "Invalid granularity path: expected 'granularities' at position 2, got '{}'", + path[2] + ))); + } + + let granularity = &path[3]; + + // Validate granularity is one of the supported ones + let valid_granularities = vec![ + "second", "minute", "hour", "day", "week", "month", "quarter", "year", + ]; + + if !valid_granularities.contains(&granularity.as_str()) { + return Err(CubeError::user(format!( + "Unsupported granularity: '{}'. Supported: second, minute, hour, day, week, month, quarter, year", + granularity + ))); + } + + // Create mock granularity definition with interval equal to granularity + use crate::test_fixtures::cube_bridge::MockGranularityDefinition; + Ok(Rc::new( + MockGranularityDefinition::builder() + .interval(granularity.clone()) + .build(), + ) as Rc) } fn pre_aggregations_for_cube_as_array( @@ -328,10 +393,12 @@ mod tests { let schema = create_test_schema(); let evaluator = MockCubeEvaluator::new(schema); - let result = - evaluator.parse_path("measure".to_string(), "nonexistent.count".to_string()); + let result = evaluator.parse_path("measure".to_string(), "nonexistent.count".to_string()); assert!(result.is_err()); - assert!(result.unwrap_err().message.contains("Cube 'nonexistent' not found")); + assert!(result + .unwrap_err() + .message + .contains("Cube 'nonexistent' not found")); } #[test] @@ -339,8 +406,7 @@ mod tests { let schema = create_test_schema(); let evaluator = MockCubeEvaluator::new(schema); - let result = - evaluator.parse_path("measure".to_string(), "users.nonexistent".to_string()); + let result = evaluator.parse_path("measure".to_string(), "users.nonexistent".to_string()); assert!(result.is_err()); assert!(result .unwrap_err() @@ -353,7 +419,9 @@ mod tests { let schema = create_test_schema(); let evaluator = MockCubeEvaluator::new(schema); - let measure = evaluator.measure_by_path("users.count".to_string()).unwrap(); + let measure = evaluator + .measure_by_path("users.count".to_string()) + .unwrap(); assert_eq!(measure.static_data().measure_type, "count"); } @@ -373,7 +441,9 @@ mod tests { let schema = create_test_schema(); let evaluator = MockCubeEvaluator::new(schema); - let segment = evaluator.segment_by_path("users.active".to_string()).unwrap(); + let segment = evaluator + .segment_by_path("users.active".to_string()) + .unwrap(); // Verify it's a valid segment assert!(segment.sql().is_ok()); } @@ -498,12 +568,80 @@ mod tests { } #[test] - #[should_panic(expected = "resolve_granularity is not implemented")] - fn test_resolve_granularity_panics() { + fn test_resolve_granularity() { + let schema = MockSchemaBuilder::new() + .add_cube("users") + .add_dimension( + "created_at", + MockDimensionDefinition::builder() + .dimension_type("time".to_string()) + .sql("created_at".to_string()) + .build(), + ) + .finish_cube() + .build(); + let evaluator = MockCubeEvaluator::new(schema); + + // Test valid granularities with 4-part path: [cube, dimension, "granularities", granularity] + let granularities = vec![ + "second", "minute", "hour", "day", "week", "month", "quarter", "year", + ]; + for gran in granularities { + let result = evaluator.resolve_granularity(vec![ + "users".to_string(), + "created_at".to_string(), + "granularities".to_string(), + gran.to_string(), + ]); + assert!(result.is_ok()); + let granularity_def = result.unwrap(); + assert_eq!(granularity_def.static_data().interval, gran); + assert_eq!(granularity_def.static_data().origin, None); + assert_eq!(granularity_def.static_data().offset, None); + } + } + + #[test] + fn test_resolve_granularity_invalid_path_length() { let schema = create_test_schema(); let evaluator = MockCubeEvaluator::new(schema); - let _ = evaluator.resolve_granularity(vec!["users".to_string(), "created_at".to_string()]); + let result = evaluator.resolve_granularity(vec![ + "users".to_string(), + "created_at".to_string(), + "granularities".to_string(), + ]); + assert!(result.is_err()); + if let Err(err) = result { + assert!(err.message.contains("expected 4 parts")); + } + } + + #[test] + fn test_resolve_granularity_unsupported() { + let schema = MockSchemaBuilder::new() + .add_cube("users") + .add_dimension( + "created_at", + MockDimensionDefinition::builder() + .dimension_type("time".to_string()) + .sql("created_at".to_string()) + .build(), + ) + .finish_cube() + .build(); + let evaluator = MockCubeEvaluator::new(schema); + + let result = evaluator.resolve_granularity(vec![ + "users".to_string(), + "created_at".to_string(), + "granularities".to_string(), + "invalid".to_string(), + ]); + assert!(result.is_err()); + if let Err(err) = result { + assert!(err.message.contains("Unsupported granularity")); + } } #[test] @@ -521,10 +659,8 @@ mod tests { let schema = create_test_schema(); let evaluator = MockCubeEvaluator::new(schema); - let _ = evaluator.pre_aggregation_description_by_name( - "users".to_string(), - "main".to_string(), - ); + let _ = + evaluator.pre_aggregation_description_by_name("users".to_string(), "main".to_string()); } #[test] @@ -537,4 +673,5 @@ mod tests { let sql = Rc::new(MockMemberSql::new("{CUBE.id}").unwrap()); let _ = evaluator.evaluate_rollup_references("users".to_string(), sql); } -} \ No newline at end of file +} + diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_granularity_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_granularity_definition.rs new file mode 100644 index 0000000000000..3f0b1ed57c4e4 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_granularity_definition.rs @@ -0,0 +1,76 @@ +use crate::cube_bridge::granularity_definition::{GranularityDefinition, GranularityDefinitionStatic}; +use crate::cube_bridge::member_sql::MemberSql; +use crate::impl_static_data; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of GranularityDefinition for testing +#[derive(Clone, TypedBuilder)] +pub struct MockGranularityDefinition { + #[builder(setter(into))] + interval: String, + #[builder(default, setter(strip_option, into))] + origin: Option, + #[builder(default, setter(strip_option, into))] + offset: Option, + #[builder(default, setter(strip_option))] + sql: Option>, +} + +impl_static_data!( + MockGranularityDefinition, + GranularityDefinitionStatic, + interval, + origin, + offset +); + +impl GranularityDefinition for MockGranularityDefinition { + crate::impl_static_data_method!(GranularityDefinitionStatic); + + fn sql(&self) -> Result>, CubeError> { + Ok(self.sql.clone()) + } + + fn has_sql(&self) -> Result { + Ok(self.sql.is_some()) + } + + fn as_any(self: Rc) -> Rc { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mock_granularity_basic() { + let granularity = MockGranularityDefinition::builder() + .interval("month") + .build(); + + let static_data = granularity.static_data(); + assert_eq!(static_data.interval, "month"); + assert_eq!(static_data.origin, None); + assert_eq!(static_data.offset, None); + assert!(granularity.sql().unwrap().is_none()); + } + + #[test] + fn test_mock_granularity_with_origin_and_offset() { + let granularity = MockGranularityDefinition::builder() + .interval("week") + .origin("2020-01-01") + .offset("3 days") + .build(); + + let static_data = granularity.static_data(); + assert_eq!(static_data.interval, "week"); + assert_eq!(static_data.origin, Some("2020-01-01".to_string())); + assert_eq!(static_data.offset, Some("3 days".to_string())); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index 5a4337ccf3b26..9792a0eba071a 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -12,6 +12,7 @@ mod mock_dimension_definition; mod mock_evaluator; mod mock_expression_struct; mod mock_geo_item; +mod mock_granularity_definition; mod mock_join_definition; mod mock_join_item; mod mock_join_item_definition; @@ -37,6 +38,7 @@ pub use mock_dimension_definition::MockDimensionDefinition; pub use mock_evaluator::MockCubeEvaluator; pub use mock_expression_struct::MockExpressionStruct; pub use mock_geo_item::MockGeoItem; +pub use mock_granularity_definition::MockGranularityDefinition; pub use mock_join_item::MockJoinItem; pub use mock_join_item_definition::MockJoinItemDefinition; pub use mock_measure_definition::MockMeasureDefinition; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs index 445399a08a901..e42cb68885e75 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs @@ -595,3 +595,54 @@ fn test_proxy_measure_compilation() { // Verify the resolved member is not a reference (it's the actual measure) assert!(!resolved.as_measure().unwrap().is_reference(), "Target member should not be a reference"); } + +#[test] +fn test_time_dimension_with_granularity_compilation() { + let evaluator = create_visitors_schema().create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + // Compile time dimension with month granularity + let time_symbol = test_compiler + .compiler + .add_dimension_evaluator("visitors.created_at.month".to_string()) + .unwrap(); + + // Check that it's a time dimension, not a regular dimension + assert!(time_symbol.as_time_dimension().is_ok(), "Should be a time dimension"); + + // Check full name includes granularity + assert_eq!( + time_symbol.full_name(), + "visitors.created_at_month", + "Full name should be visitors.created_at_month" + ); + assert_eq!(time_symbol.cube_name(), "visitors"); + assert_eq!(time_symbol.name(), "created_at"); + + // Get as time dimension to check specific properties + let time_dim = time_symbol.as_time_dimension().unwrap(); + + // Check granularity + assert_eq!( + time_dim.granularity(), + &Some("month".to_string()), + "Granularity should be month" + ); + + // Check that it's NOT a reference + assert!(!time_dim.is_reference(), "Time dimension with granularity should not be a reference"); + + // Check base symbol - should be the original dimension without granularity + let base_symbol = time_dim.base_symbol(); + assert!(base_symbol.is_dimension(), "Base symbol should be a dimension"); + assert_eq!( + base_symbol.full_name(), + "visitors.created_at", + "Base symbol should be visitors.created_at" + ); + assert_eq!( + base_symbol.as_dimension().unwrap().dimension_type(), + "time", + "Base dimension should be time type" + ); +} From 6a96c77b6a0eabb25b037a94e523ca5803985461 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 23:12:19 +0100 Subject: [PATCH 27/42] templates mock --- .../cube_bridge/mock_sql_templates_render.rs | 564 ++++++++++++++++++ .../src/test_fixtures/cube_bridge/mod.rs | 2 + 2 files changed, 566 insertions(+) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_sql_templates_render.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_sql_templates_render.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_sql_templates_render.rs new file mode 100644 index 0000000000000..ac6799d9a4310 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_sql_templates_render.rs @@ -0,0 +1,564 @@ +use crate::cube_bridge::sql_templates_render::SqlTemplatesRender; +use cubenativeutils::CubeError; +use minijinja::{value::Value, Environment}; +use std::collections::HashMap; + +/// Mock implementation of SqlTemplatesRender for testing +/// +/// This mock provides a simple in-memory template rendering system using minijinja. +/// It allows tests to define SQL templates and render them with context values. +/// +/// # Example +/// +/// ``` +/// use cubesqlplanner::test_fixtures::cube_bridge::MockSqlTemplatesRender; +/// use minijinja::context; +/// +/// let mut templates = std::collections::HashMap::new(); +/// templates.insert("filters/equals".to_string(), "{{column}} = {{value}}".to_string()); +/// +/// let render = MockSqlTemplatesRender::try_new(templates).unwrap(); +/// let result = render.render_template( +/// "filters/equals", +/// minijinja::context! { column => "id", value => "123" } +/// ).unwrap(); +/// +/// assert_eq!(result, "id = 123"); +/// ``` +#[derive(Clone)] +pub struct MockSqlTemplatesRender { + templates: HashMap, + jinja: Environment<'static>, +} + +impl MockSqlTemplatesRender { + /// Creates a new MockSqlTemplatesRender with the given templates + /// + /// # Arguments + /// + /// * `templates` - HashMap of template name to template content + /// + /// # Returns + /// + /// Result containing the MockSqlTemplatesRender or a CubeError if template parsing fails + pub fn try_new(templates: HashMap) -> Result { + let mut jinja = Environment::new(); + for (name, template) in templates.iter() { + jinja + .add_template_owned(name.to_string(), template.to_string()) + .map_err(|e| { + CubeError::internal(format!( + "Error parsing template {} '{}': {}", + name, template, e + )) + })?; + } + + Ok(Self { templates, jinja }) + } + + /// Creates a default MockSqlTemplatesRender with common SQL templates + /// + /// This includes templates for common filter operations, functions, and types + /// that are frequently used in tests. Based on BaseQuery.sqlTemplates() from + /// packages/cubejs-schema-compiler/src/adapter/BaseQuery.js + pub fn default_templates() -> Self { + let mut templates = HashMap::new(); + + // Functions - based on BaseQuery.js:4241-4315 + templates.insert("functions/SUM".to_string(), "SUM({{ args_concat }})".to_string()); + templates.insert("functions/MIN".to_string(), "MIN({{ args_concat }})".to_string()); + templates.insert("functions/MAX".to_string(), "MAX({{ args_concat }})".to_string()); + templates.insert("functions/COUNT".to_string(), "COUNT({{ args_concat }})".to_string()); + templates.insert("functions/COUNT_DISTINCT".to_string(), "COUNT(DISTINCT {{ args_concat }})".to_string()); + templates.insert("functions/AVG".to_string(), "AVG({{ args_concat }})".to_string()); + templates.insert("functions/STDDEV_POP".to_string(), "STDDEV_POP({{ args_concat }})".to_string()); + templates.insert("functions/STDDEV_SAMP".to_string(), "STDDEV_SAMP({{ args_concat }})".to_string()); + templates.insert("functions/VAR_POP".to_string(), "VAR_POP({{ args_concat }})".to_string()); + templates.insert("functions/VAR_SAMP".to_string(), "VAR_SAMP({{ args_concat }})".to_string()); + templates.insert("functions/COVAR_POP".to_string(), "COVAR_POP({{ args_concat }})".to_string()); + templates.insert("functions/COVAR_SAMP".to_string(), "COVAR_SAMP({{ args_concat }})".to_string()); + templates.insert("functions/GROUP_ANY".to_string(), "max({{ expr }})".to_string()); + templates.insert("functions/COALESCE".to_string(), "COALESCE({{ args_concat }})".to_string()); + templates.insert("functions/CONCAT".to_string(), "CONCAT({{ args_concat }})".to_string()); + templates.insert("functions/FLOOR".to_string(), "FLOOR({{ args_concat }})".to_string()); + templates.insert("functions/CEIL".to_string(), "CEIL({{ args_concat }})".to_string()); + templates.insert("functions/TRUNC".to_string(), "TRUNC({{ args_concat }})".to_string()); + templates.insert("functions/LOWER".to_string(), "LOWER({{ args_concat }})".to_string()); + templates.insert("functions/UPPER".to_string(), "UPPER({{ args_concat }})".to_string()); + templates.insert("functions/LEFT".to_string(), "LEFT({{ args_concat }})".to_string()); + templates.insert("functions/RIGHT".to_string(), "RIGHT({{ args_concat }})".to_string()); + templates.insert("functions/SQRT".to_string(), "SQRT({{ args_concat }})".to_string()); + templates.insert("functions/ABS".to_string(), "ABS({{ args_concat }})".to_string()); + templates.insert("functions/ACOS".to_string(), "ACOS({{ args_concat }})".to_string()); + templates.insert("functions/ASIN".to_string(), "ASIN({{ args_concat }})".to_string()); + templates.insert("functions/ATAN".to_string(), "ATAN({{ args_concat }})".to_string()); + templates.insert("functions/COS".to_string(), "COS({{ args_concat }})".to_string()); + templates.insert("functions/EXP".to_string(), "EXP({{ args_concat }})".to_string()); + templates.insert("functions/LN".to_string(), "LN({{ args_concat }})".to_string()); + templates.insert("functions/LOG".to_string(), "LOG({{ args_concat }})".to_string()); + templates.insert("functions/DLOG10".to_string(), "LOG10({{ args_concat }})".to_string()); + templates.insert("functions/PI".to_string(), "PI()".to_string()); + templates.insert("functions/POWER".to_string(), "POWER({{ args_concat }})".to_string()); + templates.insert("functions/SIN".to_string(), "SIN({{ args_concat }})".to_string()); + templates.insert("functions/TAN".to_string(), "TAN({{ args_concat }})".to_string()); + templates.insert("functions/REPEAT".to_string(), "REPEAT({{ args_concat }})".to_string()); + templates.insert("functions/NULLIF".to_string(), "NULLIF({{ args_concat }})".to_string()); + templates.insert("functions/ROUND".to_string(), "ROUND({{ args_concat }})".to_string()); + templates.insert("functions/STDDEV".to_string(), "STDDEV_SAMP({{ args_concat }})".to_string()); + templates.insert("functions/SUBSTR".to_string(), "SUBSTRING({{ args_concat }})".to_string()); + templates.insert("functions/CHARACTERLENGTH".to_string(), "CHAR_LENGTH({{ args[0] }})".to_string()); + templates.insert("functions/BTRIM".to_string(), "BTRIM({{ args_concat }})".to_string()); + templates.insert("functions/LTRIM".to_string(), "LTRIM({{ args_concat }})".to_string()); + templates.insert("functions/RTRIM".to_string(), "RTRIM({{ args_concat }})".to_string()); + templates.insert("functions/ATAN2".to_string(), "ATAN2({{ args_concat }})".to_string()); + templates.insert("functions/COT".to_string(), "COT({{ args_concat }})".to_string()); + templates.insert("functions/DEGREES".to_string(), "DEGREES({{ args_concat }})".to_string()); + templates.insert("functions/RADIANS".to_string(), "RADIANS({{ args_concat }})".to_string()); + templates.insert("functions/SIGN".to_string(), "SIGN({{ args_concat }})".to_string()); + templates.insert("functions/ASCII".to_string(), "ASCII({{ args_concat }})".to_string()); + templates.insert("functions/STRPOS".to_string(), "POSITION({{ args[1] }} IN {{ args[0] }})".to_string()); + templates.insert("functions/REPLACE".to_string(), "REPLACE({{ args_concat }})".to_string()); + templates.insert("functions/DATEDIFF".to_string(), "DATEDIFF({{ date_part }}, {{ args[1] }}, {{ args[2] }})".to_string()); + templates.insert("functions/TO_CHAR".to_string(), "TO_CHAR({{ args_concat }})".to_string()); + templates.insert("functions/DATE".to_string(), "DATE({{ args_concat }})".to_string()); + templates.insert("functions/PERCENTILECONT".to_string(), "PERCENTILE_CONT({{ args_concat }})".to_string()); + + // Expressions - based on BaseQuery.js:4360-4391 + templates.insert("expressions/column_reference".to_string(), "{% if table_name %}{{ table_name }}.{% endif %}{{ name }}".to_string()); + templates.insert("expressions/column_aliased".to_string(), "{{expr}} {{quoted_alias}}".to_string()); + templates.insert("expressions/query_aliased".to_string(), "{{ query }} AS {{ quoted_alias }}".to_string()); + templates.insert("expressions/case".to_string(), "CASE{% if expr %} {{ expr }}{% endif %}{% for when, then in when_then %} WHEN {{ when }} THEN {{ then }}{% endfor %}{% if else_expr %} ELSE {{ else_expr }}{% endif %} END".to_string()); + templates.insert("expressions/is_null".to_string(), "({{ expr }} IS {% if negate %}NOT {% endif %}NULL)".to_string()); + templates.insert("expressions/binary".to_string(), "({{ left }} {{ op }} {{ right }})".to_string()); + templates.insert("expressions/sort".to_string(), "{{ expr }} {% if asc %}ASC{% else %}DESC{% endif %} NULLS {% if nulls_first %}FIRST{% else %}LAST{% endif %}".to_string()); + templates.insert("expressions/order_by".to_string(), "{% if index %} {{ index }} {% else %} {{ expr }} {% endif %} {% if asc %}ASC{% else %}DESC{% endif %}{% if nulls_first %} NULLS FIRST{% endif %}".to_string()); + templates.insert("expressions/cast".to_string(), "CAST({{ expr }} AS {{ data_type }})".to_string()); + templates.insert("expressions/window_function".to_string(), "{{ fun_call }} OVER ({% if partition_by_concat %}PARTITION BY {{ partition_by_concat }}{% if order_by_concat or window_frame %} {% endif %}{% endif %}{% if order_by_concat %}ORDER BY {{ order_by_concat }}{% if window_frame %} {% endif %}{% endif %}{% if window_frame %}{{ window_frame }}{% endif %})".to_string()); + templates.insert("expressions/window_frame_bounds".to_string(), "{{ frame_type }} BETWEEN {{ frame_start }} AND {{ frame_end }}".to_string()); + templates.insert("expressions/in_list".to_string(), "{{ expr }} {% if negated %}NOT {% endif %}IN ({{ in_exprs_concat }})".to_string()); + templates.insert("expressions/subquery".to_string(), "({{ expr }})".to_string()); + templates.insert("expressions/in_subquery".to_string(), "{{ expr }} {% if negated %}NOT {% endif %}IN {{ subquery_expr }}".to_string()); + templates.insert("expressions/rollup".to_string(), "ROLLUP({{ exprs_concat }})".to_string()); + templates.insert("expressions/cube".to_string(), "CUBE({{ exprs_concat }})".to_string()); + templates.insert("expressions/negative".to_string(), "-({{ expr }})".to_string()); + templates.insert("expressions/not".to_string(), "NOT ({{ expr }})".to_string()); + templates.insert("expressions/add_interval".to_string(), "{{ date }} + interval '{{ interval }}'".to_string()); + templates.insert("expressions/sub_interval".to_string(), "{{ date }} - interval '{{ interval }}'".to_string()); + templates.insert("expressions/true".to_string(), "TRUE".to_string()); + templates.insert("expressions/false".to_string(), "FALSE".to_string()); + templates.insert("expressions/like".to_string(), "{{ expr }} {% if negated %}NOT {% endif %}LIKE {{ pattern }}".to_string()); + templates.insert("expressions/ilike".to_string(), "{{ expr }} {% if negated %}NOT {% endif %}ILIKE {{ pattern }}".to_string()); + templates.insert("expressions/like_escape".to_string(), "{{ like_expr }} ESCAPE {{ escape_char }}".to_string()); + templates.insert("expressions/within_group".to_string(), "{{ fun_sql }} WITHIN GROUP (ORDER BY {{ within_group_concat }})".to_string()); + templates.insert("expressions/concat_strings".to_string(), "{{ strings | join(' || ' ) }}".to_string()); + templates.insert("expressions/rolling_window_expr_timestamp_cast".to_string(), "{{ value }}".to_string()); + templates.insert("expressions/timestamp_literal".to_string(), "{{ value }}".to_string()); + templates.insert("expressions/between".to_string(), "{{ expr }} {% if negated %}NOT {% endif %}BETWEEN {{ low }} AND {{ high }}".to_string()); + + // Tesseract - based on BaseQuery.js:4392-4397 + templates.insert("tesseract/ilike".to_string(), "{{ expr }} {% if negated %}NOT {% endif %}ILIKE {{ pattern }}".to_string()); + templates.insert("tesseract/series_bounds_cast".to_string(), "{{ expr }}".to_string()); + templates.insert("tesseract/bool_param_cast".to_string(), "{{ expr }}".to_string()); + templates.insert("tesseract/number_param_cast".to_string(), "{{ expr }}".to_string()); + + // Filters - based on BaseQuery.js:4398-4414 + templates.insert("filters/equals".to_string(), "{{ column }} = {{ value }}{{ is_null_check }}".to_string()); + templates.insert("filters/not_equals".to_string(), "{{ column }} <> {{ value }}{{ is_null_check }}".to_string()); + templates.insert("filters/or_is_null_check".to_string(), " OR {{ column }} IS NULL".to_string()); + templates.insert("filters/set_where".to_string(), "{{ column }} IS NOT NULL".to_string()); + templates.insert("filters/not_set_where".to_string(), "{{ column }} IS NULL".to_string()); + templates.insert("filters/in".to_string(), "{{ column }} IN ({{ values_concat }}){{ is_null_check }}".to_string()); + templates.insert("filters/not_in".to_string(), "{{ column }} NOT IN ({{ values_concat }}){{ is_null_check }}".to_string()); + templates.insert("filters/time_range_filter".to_string(), "{{ column }} >= {{ from_timestamp }} AND {{ column }} <= {{ to_timestamp }}".to_string()); + templates.insert("filters/time_not_in_range_filter".to_string(), "{{ column }} < {{ from_timestamp }} OR {{ column }} > {{ to_timestamp }}".to_string()); + templates.insert("filters/gt".to_string(), "{{ column }} > {{ param }}".to_string()); + templates.insert("filters/gte".to_string(), "{{ column }} >= {{ param }}".to_string()); + templates.insert("filters/lt".to_string(), "{{ column }} < {{ param }}".to_string()); + templates.insert("filters/lte".to_string(), "{{ column }} <= {{ param }}".to_string()); + templates.insert("filters/like_pattern".to_string(), "{% if start_wild %}'%' || {% endif %}{{ value }}{% if end_wild %}|| '%'{% endif %}".to_string()); + templates.insert("filters/always_true".to_string(), "1 = 1".to_string()); + + // Quotes - based on BaseQuery.js:4417-4420 + templates.insert("quotes/identifiers".to_string(), "\"".to_string()); + templates.insert("quotes/escape".to_string(), "\"\"".to_string()); + + // Params - based on BaseQuery.js:4421-4423 + templates.insert("params/param".to_string(), "?".to_string()); + + // Join types - based on BaseQuery.js:4424-4427 + templates.insert("join_types/inner".to_string(), "INNER".to_string()); + templates.insert("join_types/left".to_string(), "LEFT".to_string()); + + // Window frame types - based on BaseQuery.js:4428-4431 + templates.insert("window_frame_types/rows".to_string(), "ROWS".to_string()); + templates.insert("window_frame_types/range".to_string(), "RANGE".to_string()); + + // Window frame bounds - based on BaseQuery.js:4432-4436 + templates.insert("window_frame_bounds/preceding".to_string(), "{% if n is not none %}{{ n }}{% else %}UNBOUNDED{% endif %} PRECEDING".to_string()); + templates.insert("window_frame_bounds/current_row".to_string(), "CURRENT ROW".to_string()); + templates.insert("window_frame_bounds/following".to_string(), "{% if n is not none %}{{ n }}{% else %}UNBOUNDED{% endif %} FOLLOWING".to_string()); + + // Types - based on BaseQuery.js:4437-4452 + templates.insert("types/string".to_string(), "STRING".to_string()); + templates.insert("types/boolean".to_string(), "BOOLEAN".to_string()); + templates.insert("types/tinyint".to_string(), "TINYINT".to_string()); + templates.insert("types/smallint".to_string(), "SMALLINT".to_string()); + templates.insert("types/integer".to_string(), "INTEGER".to_string()); + templates.insert("types/bigint".to_string(), "BIGINT".to_string()); + templates.insert("types/float".to_string(), "FLOAT".to_string()); + templates.insert("types/double".to_string(), "DOUBLE".to_string()); + templates.insert("types/decimal".to_string(), "DECIMAL({{ precision }},{{ scale }})".to_string()); + templates.insert("types/timestamp".to_string(), "TIMESTAMP".to_string()); + templates.insert("types/date".to_string(), "DATE".to_string()); + templates.insert("types/time".to_string(), "TIME".to_string()); + templates.insert("types/interval".to_string(), "INTERVAL".to_string()); + templates.insert("types/binary".to_string(), "BINARY".to_string()); + + Self::try_new(templates).expect("Default templates should always parse successfully") + } +} + +impl SqlTemplatesRender for MockSqlTemplatesRender { + fn contains_template(&self, template_name: &str) -> bool { + self.templates.contains_key(template_name) + } + + fn get_template(&self, template_name: &str) -> Result<&String, CubeError> { + self.templates + .get(template_name) + .ok_or_else(|| CubeError::user(format!("{} template not found", template_name))) + } + + fn render_template(&self, name: &str, ctx: Value) -> Result { + Ok(self + .jinja + .get_template(name) + .map_err(|e| CubeError::internal(format!("Error getting {} template: {}", name, e)))? + .render(ctx) + .map_err(|e| { + CubeError::internal(format!("Error rendering {} template: {}", name, e)) + })?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use minijinja::context; + + #[test] + fn test_basic_template_rendering() { + let mut templates = HashMap::new(); + templates.insert("test".to_string(), "Hello {{name}}!".to_string()); + + let render = MockSqlTemplatesRender::try_new(templates).unwrap(); + + let result = render + .render_template("test", context! { name => "World" }) + .unwrap(); + assert_eq!(result, "Hello World!"); + } + + #[test] + fn test_contains_template() { + let mut templates = HashMap::new(); + templates.insert("exists".to_string(), "template".to_string()); + + let render = MockSqlTemplatesRender::try_new(templates).unwrap(); + + assert!(render.contains_template("exists")); + assert!(!render.contains_template("not_exists")); + } + + #[test] + fn test_get_template() { + let mut templates = HashMap::new(); + templates.insert("test".to_string(), "content".to_string()); + + let render = MockSqlTemplatesRender::try_new(templates).unwrap(); + + assert_eq!(render.get_template("test").unwrap(), "content"); + assert!(render.get_template("not_exists").is_err()); + } + + #[test] + fn test_invalid_template_syntax() { + let mut templates = HashMap::new(); + templates.insert("bad".to_string(), "{{unclosed".to_string()); + + let result = MockSqlTemplatesRender::try_new(templates); + assert!(result.is_err()); + } + + #[test] + fn test_template_with_multiple_variables() { + let mut templates = HashMap::new(); + templates.insert( + "complex".to_string(), + "{{column}} = {{value}}".to_string(), + ); + + let render = MockSqlTemplatesRender::try_new(templates).unwrap(); + + let result = render + .render_template( + "complex", + context! { column => "id", value => "123" }, + ) + .unwrap(); + + assert_eq!(result, "id = 123"); + } + + #[test] + fn test_template_with_numeric_values() { + let mut templates = HashMap::new(); + templates.insert("numeric".to_string(), "LIMIT {{limit}} OFFSET {{offset}}".to_string()); + + let render = MockSqlTemplatesRender::try_new(templates).unwrap(); + + let result = render + .render_template("numeric", context! { limit => 10, offset => 20 }) + .unwrap(); + + assert_eq!(result, "LIMIT 10 OFFSET 20"); + } + + #[test] + fn test_default_templates_functions() { + let render = MockSqlTemplatesRender::default_templates(); + + // Test SUM + let result = render + .render_template("functions/SUM", context! { args_concat => "revenue" }) + .unwrap(); + assert_eq!(result, "SUM(revenue)"); + + // Test COUNT DISTINCT + let result = render + .render_template("functions/COUNT_DISTINCT", context! { args_concat => "user_id" }) + .unwrap(); + assert_eq!(result, "COUNT(DISTINCT user_id)"); + + // Test GROUP_ANY (uses expr instead of args_concat) + let result = render + .render_template("functions/GROUP_ANY", context! { expr => "status" }) + .unwrap(); + assert_eq!(result, "max(status)"); + + // Test COALESCE + let result = render + .render_template("functions/COALESCE", context! { args_concat => "a, b, c" }) + .unwrap(); + assert_eq!(result, "COALESCE(a, b, c)"); + } + + #[test] + fn test_default_templates_filters() { + let render = MockSqlTemplatesRender::default_templates(); + + // Test equals + let result = render + .render_template( + "filters/equals", + context! { column => "id", value => "123", is_null_check => "" }, + ) + .unwrap(); + assert_eq!(result, "id = 123"); + + // Test not_equals with null check + let result = render + .render_template( + "filters/not_equals", + context! { column => "status", value => "'active'", is_null_check => " OR status IS NULL" }, + ) + .unwrap(); + assert_eq!(result, "status <> 'active' OR status IS NULL"); + + // Test in filter + let result = render + .render_template( + "filters/in", + context! { column => "status", values_concat => "'active', 'pending'", is_null_check => "" }, + ) + .unwrap(); + assert_eq!(result, "status IN ('active', 'pending')"); + + // Test time_range_filter + let result = render + .render_template( + "filters/time_range_filter", + context! { + column => "created_at", + from_timestamp => "TIMESTAMP '2024-01-01'", + to_timestamp => "TIMESTAMP '2024-12-31'" + }, + ) + .unwrap(); + assert_eq!( + result, + "created_at >= TIMESTAMP '2024-01-01' AND created_at <= TIMESTAMP '2024-12-31'" + ); + + // Test like_pattern with wildcards + let result = render + .render_template( + "filters/like_pattern", + context! { value => "'john'", start_wild => true, end_wild => true }, + ) + .unwrap(); + assert_eq!(result, "'%' || 'john'|| '%'"); + } + + #[test] + fn test_default_templates_expressions() { + let render = MockSqlTemplatesRender::default_templates(); + + // Test column_reference with table + let result = render + .render_template( + "expressions/column_reference", + context! { table_name => "users", name => "id" }, + ) + .unwrap(); + assert_eq!(result, "users.id"); + + // Test column_reference without table + let result = render + .render_template( + "expressions/column_reference", + context! { name => "id" }, + ) + .unwrap(); + assert_eq!(result, "id"); + + // Test cast + let result = render + .render_template( + "expressions/cast", + context! { expr => "value", data_type => "INTEGER" }, + ) + .unwrap(); + assert_eq!(result, "CAST(value AS INTEGER)"); + + // Test binary expression + let result = render + .render_template( + "expressions/binary", + context! { left => "a", op => "+", right => "b" }, + ) + .unwrap(); + assert_eq!(result, "(a + b)"); + + // Test is_null + let result = render + .render_template( + "expressions/is_null", + context! { expr => "value", negate => false }, + ) + .unwrap(); + assert_eq!(result, "(value IS NULL)"); + + // Test is_null with negation + let result = render + .render_template( + "expressions/is_null", + context! { expr => "value", negate => true }, + ) + .unwrap(); + assert_eq!(result, "(value IS NOT NULL)"); + } + + #[test] + fn test_default_templates_types() { + let render = MockSqlTemplatesRender::default_templates(); + + // Test simple types + assert_eq!( + render.render_template("types/string", context! {}).unwrap(), + "STRING" + ); + assert_eq!( + render.render_template("types/integer", context! {}).unwrap(), + "INTEGER" + ); + assert_eq!( + render.render_template("types/timestamp", context! {}).unwrap(), + "TIMESTAMP" + ); + + // Test decimal with parameters + let result = render + .render_template("types/decimal", context! { precision => 10, scale => 2 }) + .unwrap(); + assert_eq!(result, "DECIMAL(10,2)"); + } + + #[test] + fn test_default_templates_window_functions() { + let render = MockSqlTemplatesRender::default_templates(); + + // Test window_frame_bounds with specific n + let result = render + .render_template("window_frame_bounds/preceding", context! { n => 5 }) + .unwrap(); + assert_eq!(result, "5 PRECEDING"); + + // Test window_frame_bounds with UNBOUNDED (n is None) + let result = render + .render_template("window_frame_bounds/preceding", context! { n => Value::from(()) }) + .unwrap(); + assert_eq!(result, "UNBOUNDED PRECEDING"); + + // Test window_frame_bounds following with UNBOUNDED + let result = render + .render_template("window_frame_bounds/following", context! { n => Value::from(()) }) + .unwrap(); + assert_eq!(result, "UNBOUNDED FOLLOWING"); + + // Test current_row + let result = render + .render_template("window_frame_bounds/current_row", context! {}) + .unwrap(); + assert_eq!(result, "CURRENT ROW"); + } + + #[test] + fn test_default_templates_join_types() { + let render = MockSqlTemplatesRender::default_templates(); + + assert_eq!( + render.render_template("join_types/inner", context! {}).unwrap(), + "INNER" + ); + assert_eq!( + render.render_template("join_types/left", context! {}).unwrap(), + "LEFT" + ); + } + + #[test] + fn test_default_templates_params() { + let render = MockSqlTemplatesRender::default_templates(); + + assert_eq!( + render.render_template("params/param", context! {}).unwrap(), + "?" + ); + } + + #[test] + fn test_default_templates_quotes() { + let render = MockSqlTemplatesRender::default_templates(); + + assert_eq!( + render.render_template("quotes/identifiers", context! {}).unwrap(), + "\"" + ); + assert_eq!( + render.render_template("quotes/escape", context! {}).unwrap(), + "\"\"" + ); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index 9792a0eba071a..e156c0df68780 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -24,6 +24,7 @@ mod mock_schema; mod mock_security_context; mod mock_segment_definition; mod mock_sql_utils; +mod mock_sql_templates_render; mod mock_struct_with_sql_member; mod mock_timeshift_definition; @@ -48,5 +49,6 @@ pub use mock_schema::{MockSchema, MockSchemaBuilder}; pub use mock_security_context::MockSecurityContext; pub use mock_segment_definition::MockSegmentDefinition; pub use mock_sql_utils::MockSqlUtils; +pub use mock_sql_templates_render::MockSqlTemplatesRender; pub use mock_struct_with_sql_member::MockStructWithSqlMember; pub use mock_timeshift_definition::MockTimeShiftDefinition; \ No newline at end of file From 765a5a315bf476445444dfc48ba917b530c8bbca Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 23:31:45 +0100 Subject: [PATCH 28/42] driver tools mock --- .../cube_bridge/mock_driver_tools.rs | 469 ++++++++++++++++++ .../src/test_fixtures/cube_bridge/mod.rs | 2 + 2 files changed, 471 insertions(+) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_driver_tools.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_driver_tools.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_driver_tools.rs new file mode 100644 index 0000000000000..0a5305dc10b19 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_driver_tools.rs @@ -0,0 +1,469 @@ +use crate::cube_bridge::driver_tools::DriverTools; +use crate::cube_bridge::sql_templates_render::SqlTemplatesRender; +use crate::test_fixtures::cube_bridge::MockSqlTemplatesRender; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; + +/// Mock implementation of DriverTools for testing +/// +/// This mock provides implementations based on PostgresQuery.ts and BaseQuery.js +/// from packages/cubejs-schema-compiler/src/adapter/ +/// +/// # Example +/// +/// ``` +/// use cubesqlplanner::test_fixtures::cube_bridge::MockDriverTools; +/// +/// let tools = MockDriverTools::new(); +/// let result = tools.time_grouped_column("day".to_string(), "created_at".to_string()).unwrap(); +/// assert_eq!(result, "date_trunc('day', created_at)"); +/// ``` +#[derive(Clone)] +pub struct MockDriverTools { + timezone: String, + timestamp_precision: u32, + sql_templates: Rc, +} + +impl MockDriverTools { + /// Creates a new MockDriverTools with default settings (UTC timezone) + pub fn new() -> Self { + Self { + timezone: "UTC".to_string(), + timestamp_precision: 3, + sql_templates: Rc::new(MockSqlTemplatesRender::default_templates()), + } + } + + /// Creates a new MockDriverTools with a specific timezone + pub fn with_timezone(timezone: String) -> Self { + Self { + timezone, + timestamp_precision: 3, + sql_templates: Rc::new(MockSqlTemplatesRender::default_templates()), + } + } + + /// Creates a new MockDriverTools with custom SQL templates + pub fn with_sql_templates(sql_templates: MockSqlTemplatesRender) -> Self { + Self { + timezone: "UTC".to_string(), + timestamp_precision: 3, + sql_templates: Rc::new(sql_templates), + } + } +} + +impl Default for MockDriverTools { + fn default() -> Self { + Self::new() + } +} + +impl DriverTools for MockDriverTools { + fn as_any(self: Rc) -> Rc { + self + } + + /// Convert timezone - based on PostgresQuery.ts:26-28 + /// Returns: `(field::timestamptz AT TIME ZONE 'timezone')` + fn convert_tz(&self, field: String) -> Result { + Ok(format!( + "({}::timestamptz AT TIME ZONE '{}')", + field, self.timezone + )) + } + + /// Time grouped column - based on PostgresQuery.ts:30-32 + /// Uses date_trunc function with granularity mapping + fn time_grouped_column( + &self, + granularity: String, + dimension: String, + ) -> Result { + // Map granularity to Postgres interval (from PostgresQuery.ts:4-13) + let interval = match granularity.as_str() { + "day" => "day", + "week" => "week", + "hour" => "hour", + "minute" => "minute", + "second" => "second", + "month" => "month", + "quarter" => "quarter", + "year" => "year", + _ => { + return Err(CubeError::user(format!( + "Unsupported granularity: {}", + granularity + ))) + } + }; + + Ok(format!("date_trunc('{}', {})", interval, dimension)) + } + + /// Returns SQL templates renderer + fn sql_templates(&self) -> Result, CubeError> { + Ok(self.sql_templates.clone()) + } + + /// Timestamp precision - based on BaseQuery.js:3834-3836 + fn timestamp_precision(&self) -> Result { + Ok(self.timestamp_precision) + } + + /// Timestamp cast - based on BaseQuery.js:2101-2103 + /// Returns: `value::timestamptz` + fn time_stamp_cast(&self, field: String) -> Result { + Ok(format!("{}::timestamptz", field)) + } + + /// DateTime cast - based on BaseQuery.js:2105-2107 + /// Returns: `value::timestamp` + fn date_time_cast(&self, field: String) -> Result { + Ok(format!("{}::timestamp", field)) + } + + /// Convert date to DB timezone - based on BaseQuery.js:3820-3822 + /// This is a simplified version that returns the date as-is + /// The full implementation would use localTimestampToUtc utility + fn in_db_time_zone(&self, date: String) -> Result { + // In real implementation this calls localTimestampToUtc(timezone, timestampFormat(), date) + // For mock we just return the date as-is + Ok(date) + } + + /// Get allocated parameters - returns empty vec for mock + fn get_allocated_params(&self) -> Result, CubeError> { + Ok(Vec::new()) + } + + /// Subtract interval - based on BaseQuery.js:1166-1169 + /// Returns: `date - interval 'interval'` + fn subtract_interval(&self, date: String, interval: String) -> Result { + let interval_str = self.interval_string(interval)?; + Ok(format!("{} - interval {}", date, interval_str)) + } + + /// Add interval - based on BaseQuery.js:1176-1179 + /// Returns: `date + interval 'interval'` + fn add_interval(&self, date: String, interval: String) -> Result { + let interval_str = self.interval_string(interval)?; + Ok(format!("{} + interval {}", date, interval_str)) + } + + /// Format interval string - based on BaseQuery.js:1190-1192 + /// Returns: `'interval'` + fn interval_string(&self, interval: String) -> Result { + Ok(format!("'{}'", interval)) + } + + /// Add timestamp interval - based on BaseQuery.js:1199-1201 + /// Delegates to add_interval + fn add_timestamp_interval(&self, date: String, interval: String) -> Result { + self.add_interval(date, interval) + } + + /// Get interval and minimal time unit - based on BaseQuery.js:2116-2119 + /// Returns: [interval, minimal_time_unit] + /// The minimal time unit is the lowest unit in the interval (e.g., "day" for "5 days") + fn interval_and_minimal_time_unit(&self, interval: String) -> Result, CubeError> { + // Parse minimal granularity from interval + // This is a simplified version - full implementation would call diffTimeUnitForInterval + let min_unit = if interval.contains("second") { + "second" + } else if interval.contains("minute") { + "minute" + } else if interval.contains("hour") { + "hour" + } else if interval.contains("day") { + "day" + } else if interval.contains("week") { + "week" + } else if interval.contains("month") { + "month" + } else if interval.contains("quarter") { + "quarter" + } else if interval.contains("year") { + "year" + } else { + "day" // default + }; + + Ok(vec![interval, min_unit.to_string()]) + } + + /// HLL init - based on PostgresQuery.ts:48-50 + /// Returns: `hll_add_agg(hll_hash_any(sql))` + fn hll_init(&self, sql: String) -> Result { + Ok(format!("hll_add_agg(hll_hash_any({}))", sql)) + } + + /// HLL merge - based on PostgresQuery.ts:52-54 + /// Returns: `round(hll_cardinality(hll_union_agg(sql)))` + fn hll_merge(&self, sql: String) -> Result { + Ok(format!("round(hll_cardinality(hll_union_agg({})))", sql)) + } + + /// HLL cardinality merge - based on BaseQuery.js:3734-3736 + /// Delegates to hll_merge + fn hll_cardinality_merge(&self, sql: String) -> Result { + self.hll_merge(sql) + } + + /// Count distinct approx - based on PostgresQuery.ts:56-58 + /// Returns: `round(hll_cardinality(hll_add_agg(hll_hash_any(sql))))` + fn count_distinct_approx(&self, sql: String) -> Result { + Ok(format!( + "round(hll_cardinality(hll_add_agg(hll_hash_any({}))))", + sql + )) + } + + /// Support generated series for custom time dimensions - based on PostgresQuery.ts:60-62 + /// Postgres supports this, so returns true + fn support_generated_series_for_custom_td(&self) -> Result { + Ok(true) + } + + /// Date bin function - based on PostgresQuery.ts:40-46 + /// Returns sql for source expression floored to timestamps aligned with + /// intervals relative to origin timestamp point + fn date_bin( + &self, + interval: String, + source: String, + origin: String, + ) -> Result { + Ok(format!( + "('{}' ::timestamp + INTERVAL '{}' * FLOOR(EXTRACT(EPOCH FROM ({} - '{}'::timestamp)) / EXTRACT(EPOCH FROM INTERVAL '{}')))", + origin, interval, source, origin, interval + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_convert_tz() { + let tools = MockDriverTools::new(); + let result = tools.convert_tz("created_at".to_string()).unwrap(); + assert_eq!(result, "(created_at::timestamptz AT TIME ZONE 'UTC')"); + } + + #[test] + fn test_convert_tz_with_custom_timezone() { + let tools = MockDriverTools::with_timezone("America/Los_Angeles".to_string()); + let result = tools.convert_tz("created_at".to_string()).unwrap(); + assert_eq!( + result, + "(created_at::timestamptz AT TIME ZONE 'America/Los_Angeles')" + ); + } + + #[test] + fn test_time_grouped_column() { + let tools = MockDriverTools::new(); + + // Test various granularities + assert_eq!( + tools + .time_grouped_column("day".to_string(), "created_at".to_string()) + .unwrap(), + "date_trunc('day', created_at)" + ); + + assert_eq!( + tools + .time_grouped_column("month".to_string(), "updated_at".to_string()) + .unwrap(), + "date_trunc('month', updated_at)" + ); + + assert_eq!( + tools + .time_grouped_column("year".to_string(), "timestamp".to_string()) + .unwrap(), + "date_trunc('year', timestamp)" + ); + } + + #[test] + fn test_time_grouped_column_invalid_granularity() { + let tools = MockDriverTools::new(); + let result = tools.time_grouped_column("invalid".to_string(), "created_at".to_string()); + assert!(result.is_err()); + } + + #[test] + fn test_timestamp_precision() { + let tools = MockDriverTools::new(); + assert_eq!(tools.timestamp_precision().unwrap(), 3); + } + + #[test] + fn test_time_stamp_cast() { + let tools = MockDriverTools::new(); + assert_eq!( + tools.time_stamp_cast("?".to_string()).unwrap(), + "?::timestamptz" + ); + } + + #[test] + fn test_date_time_cast() { + let tools = MockDriverTools::new(); + assert_eq!( + tools.date_time_cast("date_from".to_string()).unwrap(), + "date_from::timestamp" + ); + } + + #[test] + fn test_subtract_interval() { + let tools = MockDriverTools::new(); + assert_eq!( + tools + .subtract_interval("NOW()".to_string(), "1 day".to_string()) + .unwrap(), + "NOW() - interval '1 day'" + ); + } + + #[test] + fn test_add_interval() { + let tools = MockDriverTools::new(); + assert_eq!( + tools + .add_interval("created_at".to_string(), "7 days".to_string()) + .unwrap(), + "created_at + interval '7 days'" + ); + } + + #[test] + fn test_interval_string() { + let tools = MockDriverTools::new(); + assert_eq!( + tools.interval_string("1 hour".to_string()).unwrap(), + "'1 hour'" + ); + } + + #[test] + fn test_add_timestamp_interval() { + let tools = MockDriverTools::new(); + assert_eq!( + tools + .add_timestamp_interval("timestamp".to_string(), "5 minutes".to_string()) + .unwrap(), + "timestamp + interval '5 minutes'" + ); + } + + #[test] + fn test_interval_and_minimal_time_unit() { + let tools = MockDriverTools::new(); + + let result = tools + .interval_and_minimal_time_unit("5 days".to_string()) + .unwrap(); + assert_eq!(result, vec!["5 days", "day"]); + + let result = tools + .interval_and_minimal_time_unit("2 hours".to_string()) + .unwrap(); + assert_eq!(result, vec!["2 hours", "hour"]); + + let result = tools + .interval_and_minimal_time_unit("30 seconds".to_string()) + .unwrap(); + assert_eq!(result, vec!["30 seconds", "second"]); + } + + #[test] + fn test_hll_init() { + let tools = MockDriverTools::new(); + assert_eq!( + tools.hll_init("user_id".to_string()).unwrap(), + "hll_add_agg(hll_hash_any(user_id))" + ); + } + + #[test] + fn test_hll_merge() { + let tools = MockDriverTools::new(); + assert_eq!( + tools.hll_merge("hll_column".to_string()).unwrap(), + "round(hll_cardinality(hll_union_agg(hll_column)))" + ); + } + + #[test] + fn test_hll_cardinality_merge() { + let tools = MockDriverTools::new(); + assert_eq!( + tools.hll_cardinality_merge("hll_data".to_string()).unwrap(), + "round(hll_cardinality(hll_union_agg(hll_data)))" + ); + } + + #[test] + fn test_count_distinct_approx() { + let tools = MockDriverTools::new(); + assert_eq!( + tools.count_distinct_approx("visitor_id".to_string()).unwrap(), + "round(hll_cardinality(hll_add_agg(hll_hash_any(visitor_id))))" + ); + } + + #[test] + fn test_support_generated_series_for_custom_td() { + let tools = MockDriverTools::new(); + assert!(tools.support_generated_series_for_custom_td().unwrap()); + } + + #[test] + fn test_date_bin() { + let tools = MockDriverTools::new(); + let result = tools + .date_bin( + "1 day".to_string(), + "created_at".to_string(), + "2024-01-01".to_string(), + ) + .unwrap(); + + assert_eq!( + result, + "('2024-01-01' ::timestamp + INTERVAL '1 day' * FLOOR(EXTRACT(EPOCH FROM (created_at - '2024-01-01'::timestamp)) / EXTRACT(EPOCH FROM INTERVAL '1 day')))" + ); + } + + #[test] + fn test_in_db_time_zone() { + let tools = MockDriverTools::new(); + let result = tools.in_db_time_zone("2024-01-01T00:00:00".to_string()).unwrap(); + assert_eq!(result, "2024-01-01T00:00:00"); + } + + #[test] + fn test_get_allocated_params() { + let tools = MockDriverTools::new(); + let result = tools.get_allocated_params().unwrap(); + assert_eq!(result, Vec::::new()); + } + + #[test] + fn test_sql_templates() { + let tools = MockDriverTools::new(); + let templates = tools.sql_templates().unwrap(); + + // Verify it returns a valid SqlTemplatesRender + assert!(templates.contains_template("filters/equals")); + assert!(templates.contains_template("functions/SUM")); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index e156c0df68780..de0e17019f8ad 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -9,6 +9,7 @@ mod mock_case_switch_else_item; mod mock_case_switch_item; mod mock_cube_definition; mod mock_dimension_definition; +mod mock_driver_tools; mod mock_evaluator; mod mock_expression_struct; mod mock_geo_item; @@ -36,6 +37,7 @@ pub use mock_case_switch_else_item::MockCaseSwitchElseItem; pub use mock_case_switch_item::MockCaseSwitchItem; pub use mock_cube_definition::MockCubeDefinition; pub use mock_dimension_definition::MockDimensionDefinition; +pub use mock_driver_tools::MockDriverTools; pub use mock_evaluator::MockCubeEvaluator; pub use mock_expression_struct::MockExpressionStruct; pub use mock_geo_item::MockGeoItem; From fc241b6715ef7ea2c4e924592a38d3d8fa487572 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 23:42:15 +0100 Subject: [PATCH 29/42] base tools mock --- .../cube_bridge/mock_base_tools.rs | 261 ++++++++++++++++++ .../src/test_fixtures/cube_bridge/mod.rs | 2 + 2 files changed, 263 insertions(+) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_base_tools.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_base_tools.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_base_tools.rs new file mode 100644 index 0000000000000..8bbd93e1be205 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_base_tools.rs @@ -0,0 +1,261 @@ +use crate::cube_bridge::base_tools::BaseTools; +use crate::cube_bridge::driver_tools::DriverTools; +use crate::cube_bridge::join_definition::JoinDefinition; +use crate::cube_bridge::join_hints::JoinHintItem; +use crate::cube_bridge::pre_aggregation_obj::PreAggregationObj; +use crate::cube_bridge::security_context::SecurityContext; +use crate::cube_bridge::sql_templates_render::SqlTemplatesRender; +use crate::cube_bridge::sql_utils::SqlUtils; +use crate::test_fixtures::cube_bridge::{ + MockDriverTools, MockSecurityContext, MockSqlTemplatesRender, MockSqlUtils, +}; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; +use typed_builder::TypedBuilder; + +/// Mock implementation of BaseTools for testing +/// +/// This mock provides implementations for driver_tools, sql_templates, +/// security_context_for_rust, and sql_utils_for_rust. +/// Other methods throw todo!() errors. +/// +/// # Example +/// +/// ``` +/// use cubesqlplanner::test_fixtures::cube_bridge::MockBaseTools; +/// +/// // Use builder pattern +/// let tools = MockBaseTools::builder().build(); +/// let driver_tools = tools.driver_tools(false).unwrap(); +/// let sql_templates = tools.sql_templates().unwrap(); +/// +/// // Or with custom components +/// let custom_driver = MockDriverTools::with_timezone("Europe/London".to_string()); +/// let tools = MockBaseTools::builder() +/// .driver_tools(custom_driver) +/// .build(); +/// ``` +#[derive(Clone, TypedBuilder)] +pub struct MockBaseTools { + #[builder(default = Rc::new(MockDriverTools::new()))] + driver_tools: Rc, + + #[builder(default = Rc::new(MockSqlTemplatesRender::default_templates()))] + sql_templates: Rc, + + #[builder(default = Rc::new(MockSecurityContext))] + security_context: Rc, + + #[builder(default = Rc::new(MockSqlUtils))] + sql_utils: Rc, +} + +impl Default for MockBaseTools { + fn default() -> Self { + Self::builder().build() + } +} + +impl BaseTools for MockBaseTools { + fn as_any(self: Rc) -> Rc { + self + } + + /// Returns driver tools - uses MockDriverTools + fn driver_tools(&self, _external: bool) -> Result, CubeError> { + Ok(self.driver_tools.clone()) + } + + /// Returns SQL templates renderer - uses MockSqlTemplatesRender + fn sql_templates(&self) -> Result, CubeError> { + Ok(self.sql_templates.clone()) + } + + /// Returns security context - uses MockSecurityContext + fn security_context_for_rust(&self) -> Result, CubeError> { + Ok(self.security_context.clone()) + } + + /// Returns SQL utils - uses MockSqlUtils + fn sql_utils_for_rust(&self) -> Result, CubeError> { + Ok(self.sql_utils.clone()) + } + + /// Generate time series - not implemented in mock + fn generate_time_series( + &self, + _granularity: String, + _date_range: Vec, + ) -> Result>, CubeError> { + todo!("generate_time_series not implemented in mock") + } + + /// Generate custom time series - not implemented in mock + fn generate_custom_time_series( + &self, + _granularity: String, + _date_range: Vec, + _origin: String, + ) -> Result>, CubeError> { + todo!("generate_custom_time_series not implemented in mock") + } + + /// Get allocated parameters - not implemented in mock + fn get_allocated_params(&self) -> Result, CubeError> { + todo!("get_allocated_params not implemented in mock") + } + + /// Get all cube members - not implemented in mock + fn all_cube_members(&self, _path: String) -> Result, CubeError> { + todo!("all_cube_members not implemented in mock") + } + + /// Get interval and minimal time unit - not implemented in mock + fn interval_and_minimal_time_unit(&self, _interval: String) -> Result, CubeError> { + todo!("interval_and_minimal_time_unit not implemented in mock") + } + + /// Get pre-aggregation by name - not implemented in mock + fn get_pre_aggregation_by_name( + &self, + _cube_name: String, + _name: String, + ) -> Result, CubeError> { + todo!("get_pre_aggregation_by_name not implemented in mock") + } + + /// Get pre-aggregation table name - not implemented in mock + fn pre_aggregation_table_name( + &self, + _cube_name: String, + _name: String, + ) -> Result { + todo!("pre_aggregation_table_name not implemented in mock") + } + + /// Get join tree for hints - not implemented in mock + fn join_tree_for_hints( + &self, + _hints: Vec, + ) -> Result, CubeError> { + todo!("join_tree_for_hints not implemented in mock") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_builder_default() { + let tools = MockBaseTools::builder().build(); + assert!(tools.driver_tools(false).is_ok()); + assert!(tools.sql_templates().is_ok()); + assert!(tools.security_context_for_rust().is_ok()); + assert!(tools.sql_utils_for_rust().is_ok()); + } + + #[test] + fn test_default_trait() { + let tools = MockBaseTools::default(); + assert!(tools.driver_tools(false).is_ok()); + assert!(tools.sql_templates().is_ok()); + assert!(tools.security_context_for_rust().is_ok()); + assert!(tools.sql_utils_for_rust().is_ok()); + } + + #[test] + fn test_driver_tools() { + let tools = MockBaseTools::builder().build(); + let driver_tools = tools.driver_tools(false).unwrap(); + + // Test that it returns a valid DriverTools implementation + let result = driver_tools + .time_grouped_column("day".to_string(), "created_at".to_string()) + .unwrap(); + assert_eq!(result, "date_trunc('day', created_at)"); + } + + #[test] + fn test_driver_tools_external_flag() { + let tools = MockBaseTools::builder().build(); + + // Both external true and false should work (mock ignores the flag) + assert!(tools.driver_tools(false).is_ok()); + assert!(tools.driver_tools(true).is_ok()); + } + + #[test] + fn test_sql_templates() { + let tools = MockBaseTools::builder().build(); + let templates = tools.sql_templates().unwrap(); + + // Test that it returns a valid SqlTemplatesRender implementation + assert!(templates.contains_template("filters/equals")); + assert!(templates.contains_template("functions/SUM")); + } + + #[test] + fn test_security_context() { + let tools = MockBaseTools::builder().build(); + // Just verify it returns without error + assert!(tools.security_context_for_rust().is_ok()); + } + + #[test] + fn test_sql_utils() { + let tools = MockBaseTools::builder().build(); + // Just verify it returns without error + assert!(tools.sql_utils_for_rust().is_ok()); + } + + #[test] + fn test_builder_with_custom_driver_tools() { + let custom_driver = MockDriverTools::with_timezone("Europe/London".to_string()); + let tools = MockBaseTools::builder() + .driver_tools(Rc::new(custom_driver)) + .build(); + + let driver_tools = tools.driver_tools(false).unwrap(); + let result = driver_tools.convert_tz("timestamp".to_string()).unwrap(); + assert_eq!( + result, + "(timestamp::timestamptz AT TIME ZONE 'Europe/London')" + ); + } + + #[test] + fn test_builder_with_custom_sql_templates() { + let mut custom_templates = std::collections::HashMap::new(); + custom_templates.insert("test/template".to_string(), "TEST {{value}}".to_string()); + let sql_templates = MockSqlTemplatesRender::try_new(custom_templates).unwrap(); + + let tools = MockBaseTools::builder() + .sql_templates(Rc::new(sql_templates)) + .build(); + + let templates = tools.sql_templates().unwrap(); + assert!(templates.contains_template("test/template")); + } + + #[test] + fn test_builder_with_all_custom_components() { + let driver_tools = MockDriverTools::with_timezone("Asia/Tokyo".to_string()); + let sql_templates = MockSqlTemplatesRender::default_templates(); + let security_context = MockSecurityContext; + let sql_utils = MockSqlUtils; + + let tools = MockBaseTools::builder() + .driver_tools(Rc::new(driver_tools)) + .sql_templates(Rc::new(sql_templates)) + .security_context(Rc::new(security_context)) + .sql_utils(Rc::new(sql_utils)) + .build(); + + assert!(tools.driver_tools(false).is_ok()); + assert!(tools.sql_templates().is_ok()); + assert!(tools.security_context_for_rust().is_ok()); + assert!(tools.sql_utils_for_rust().is_ok()); + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index de0e17019f8ad..4dbc230977cf1 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -1,6 +1,7 @@ #[macro_use] mod macros; +mod mock_base_tools; mod mock_case_definition; mod mock_case_else_item; mod mock_case_item; @@ -29,6 +30,7 @@ mod mock_sql_templates_render; mod mock_struct_with_sql_member; mod mock_timeshift_definition; +pub use mock_base_tools::MockBaseTools; pub use mock_case_definition::MockCaseDefinition; pub use mock_case_else_item::MockCaseElseItem; pub use mock_case_item::MockCaseItem; From 36c5fe00b53e6c273e00e3881a4d4d360bde8dac Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 13 Nov 2025 23:44:42 +0100 Subject: [PATCH 30/42] in work --- .../cube_bridge/mock_join_graph.rs | 45 +++++++++++++++++++ .../src/test_fixtures/cube_bridge/mod.rs | 2 + 2 files changed, 47 insertions(+) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_graph.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_graph.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_graph.rs new file mode 100644 index 0000000000000..b071b1e797035 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_graph.rs @@ -0,0 +1,45 @@ +use crate::cube_bridge::join_definition::JoinDefinition; +use crate::cube_bridge::join_graph::JoinGraph; +use crate::cube_bridge::join_hints::JoinHintItem; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; + +/// Mock implementation of JoinGraph for testing +/// +/// This mock provides a placeholder implementation. +/// The build_join method is not implemented and will panic with todo!(). +/// +/// # Example +/// +/// ``` +/// use cubesqlplanner::test_fixtures::cube_bridge::MockJoinGraph; +/// +/// let join_graph = MockJoinGraph; +/// // Note: calling build_join will panic with todo!() +/// ``` +pub struct MockJoinGraph; + +impl JoinGraph for MockJoinGraph { + fn as_any(self: Rc) -> Rc { + self + } + + fn build_join( + &self, + _cubes_to_join: Vec, + ) -> Result, CubeError> { + todo!("build_join not implemented in MockJoinGraph") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_can_create() { + let _join_graph = MockJoinGraph; + // Just verify we can create the mock + } +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index 4dbc230977cf1..480d2773f617c 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -16,6 +16,7 @@ mod mock_expression_struct; mod mock_geo_item; mod mock_granularity_definition; mod mock_join_definition; +mod mock_join_graph; mod mock_join_item; mod mock_join_item_definition; mod mock_measure_definition; @@ -44,6 +45,7 @@ pub use mock_evaluator::MockCubeEvaluator; pub use mock_expression_struct::MockExpressionStruct; pub use mock_geo_item::MockGeoItem; pub use mock_granularity_definition::MockGranularityDefinition; +pub use mock_join_graph::MockJoinGraph; pub use mock_join_item::MockJoinItem; pub use mock_join_item_definition::MockJoinItemDefinition; pub use mock_measure_definition::MockMeasureDefinition; From c5f05580ad77596d0e6eb334a9e0fecafba4671e Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Fri, 14 Nov 2025 15:03:04 +0100 Subject: [PATCH 31/42] fix warnings --- .../cube_bridge/mock_driver_tools.rs | 1 + .../test_fixtures/cube_bridge/mock_schema.rs | 58 ++++++++++++------- .../src/test_fixtures/cube_bridge/mod.rs | 2 - .../test_fixtures/schemas/visitors_schema.rs | 13 +---- .../src/tests/cube_evaluator/compilation.rs | 1 - 5 files changed, 41 insertions(+), 34 deletions(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_driver_tools.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_driver_tools.rs index 0a5305dc10b19..e4b572c9a0869 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_driver_tools.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_driver_tools.rs @@ -46,6 +46,7 @@ impl MockDriverTools { } /// Creates a new MockDriverTools with custom SQL templates + #[allow(dead_code)] pub fn with_sql_templates(sql_templates: MockSqlTemplatesRender) -> Self { Self { timezone: "UTC".to_string(), diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs index 3bb47a144d5b9..46b8ea402d405 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs @@ -68,6 +68,7 @@ impl MockSchema { } /// Create a MockCubeEvaluator with primary keys from this schema + #[allow(dead_code)] pub fn create_evaluator_with_primary_keys( self, primary_keys: std::collections::HashMap>, @@ -148,8 +149,7 @@ impl MockCubeBuilder { name: impl Into, definition: MockDimensionDefinition, ) -> Self { - self.dimensions - .insert(name.into(), Rc::new(definition)); + self.dimensions.insert(name.into(), Rc::new(definition)); self } @@ -215,11 +215,7 @@ pub struct MockViewBuilder { impl MockViewBuilder { /// Add a cube to include in this view - pub fn include_cube( - mut self, - join_path: impl Into, - includes: Vec, - ) -> Self { + pub fn include_cube(mut self, join_path: impl Into, includes: Vec) -> Self { self.view_cubes.push(ViewCube { join_path: join_path.into(), includes, @@ -233,8 +229,7 @@ impl MockViewBuilder { name: impl Into, definition: MockDimensionDefinition, ) -> Self { - self.dimensions - .insert(name.into(), Rc::new(definition)); + self.dimensions.insert(name.into(), Rc::new(definition)); self } @@ -249,6 +244,7 @@ impl MockViewBuilder { } /// Add a custom segment to the view + #[allow(dead_code)] pub fn add_segment( mut self, name: impl Into, @@ -286,7 +282,8 @@ impl MockViewBuilder { // Add dimensions for member_name in &members_to_include { if let Some(dimension) = source_cube.dimensions.get(member_name) { - let view_member_sql = format!("{{{}.{}}}", view_cube.join_path, member_name); + let view_member_sql = + format!("{{{}.{}}}", view_cube.join_path, member_name); // Check for duplicates if all_dimensions.contains_key(member_name) { @@ -311,7 +308,8 @@ impl MockViewBuilder { // Add measures for member_name in &members_to_include { if let Some(measure) = source_cube.measures.get(member_name) { - let view_member_sql = format!("{{{}.{}}}", view_cube.join_path, member_name); + let view_member_sql = + format!("{{{}.{}}}", view_cube.join_path, member_name); // Check for duplicates if all_measures.contains_key(member_name) { @@ -336,7 +334,8 @@ impl MockViewBuilder { // Add segments for member_name in &members_to_include { if source_cube.segments.contains_key(member_name) { - let view_member_sql = format!("{{{}.{}}}", view_cube.join_path, member_name); + let view_member_sql = + format!("{{{}.{}}}", view_cube.join_path, member_name); // Check for duplicates if all_segments.contains_key(member_name) { @@ -907,8 +906,6 @@ mod tests { .finish_view() .build(); - let view_cube = schema.get_cube("orders_view").unwrap(); - // Verify member SQL uses full join path let id_dim = schema.get_dimension("orders_view", "id").unwrap(); let id_sql = id_dim.sql().unwrap().unwrap(); @@ -921,8 +918,8 @@ mod tests { #[test] fn test_view_with_multiple_long_join_paths() { - use crate::cube_bridge::member_sql::MemberSql; use crate::test_fixtures::cube_bridge::{MockSecurityContext, MockSqlUtils}; + use std::rc::Rc; let schema = MockSchemaBuilder::new() .add_cube("visitors") @@ -974,7 +971,9 @@ mod tests { // Verify SQL for members from first include (with long join path) // SQL template should contain full path: {visitors.visitor_checkins.checkin_id} - let checkin_id_dim = schema.get_dimension("multi_path_view", "checkin_id").unwrap(); + let checkin_id_dim = schema + .get_dimension("multi_path_view", "checkin_id") + .unwrap(); let checkin_id_sql = checkin_id_dim.sql().unwrap().unwrap(); // Compile template and check symbol_paths structure @@ -983,7 +982,11 @@ mod tests { .unwrap(); // Should have exactly one symbol path - assert_eq!(args.symbol_paths.len(), 1, "Should have exactly one symbol path"); + assert_eq!( + args.symbol_paths.len(), + 1, + "Should have exactly one symbol path" + ); // The symbol path should be ["visitors", "visitor_checkins", "checkin_id"] assert_eq!( @@ -1001,7 +1004,11 @@ mod tests { .compile_template_sql(Rc::new(MockSqlUtils), Rc::new(MockSecurityContext)) .unwrap(); - assert_eq!(args.symbol_paths.len(), 1, "Should have exactly one symbol path"); + assert_eq!( + args.symbol_paths.len(), + 1, + "Should have exactly one symbol path" + ); assert_eq!( args.symbol_paths[0], vec!["visitors", "visitor_checkins", "checkin_count"], @@ -1017,7 +1024,11 @@ mod tests { .compile_template_sql(Rc::new(MockSqlUtils), Rc::new(MockSecurityContext)) .unwrap(); - assert_eq!(args.symbol_paths.len(), 1, "Should have exactly one symbol path"); + assert_eq!( + args.symbol_paths.len(), + 1, + "Should have exactly one symbol path" + ); assert_eq!( args.symbol_paths[0], vec!["visitors", "id"], @@ -1031,7 +1042,11 @@ mod tests { .compile_template_sql(Rc::new(MockSqlUtils), Rc::new(MockSecurityContext)) .unwrap(); - assert_eq!(args.symbol_paths.len(), 1, "Should have exactly one symbol path"); + assert_eq!( + args.symbol_paths.len(), + 1, + "Should have exactly one symbol path" + ); assert_eq!( args.symbol_paths[0], vec!["visitors", "count"], @@ -1067,4 +1082,5 @@ mod tests { .finish_view() .build(); } -} \ No newline at end of file +} + diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index 480d2773f617c..1714e401983af 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -31,7 +31,6 @@ mod mock_sql_templates_render; mod mock_struct_with_sql_member; mod mock_timeshift_definition; -pub use mock_base_tools::MockBaseTools; pub use mock_case_definition::MockCaseDefinition; pub use mock_case_else_item::MockCaseElseItem; pub use mock_case_item::MockCaseItem; @@ -45,7 +44,6 @@ pub use mock_evaluator::MockCubeEvaluator; pub use mock_expression_struct::MockExpressionStruct; pub use mock_geo_item::MockGeoItem; pub use mock_granularity_definition::MockGranularityDefinition; -pub use mock_join_graph::MockJoinGraph; pub use mock_join_item::MockJoinItem; pub use mock_join_item_definition::MockJoinItemDefinition; pub use mock_measure_definition::MockMeasureDefinition; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs index 3885269ee31bb..553ccc9414cd4 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/visitors_schema.rs @@ -193,7 +193,6 @@ pub fn create_visitors_schema() -> MockSchema { mod tests { use super::*; use crate::cube_bridge::dimension_definition::DimensionDefinition; - use crate::cube_bridge::measure_definition::MeasureDefinition; use crate::cube_bridge::segment_definition::SegmentDefinition; #[test] @@ -206,6 +205,9 @@ mod tests { #[test] fn test_visitors_dimensions() { + use crate::test_fixtures::cube_bridge::{MockSecurityContext, MockSqlUtils}; + use std::rc::Rc; + let schema = create_visitors_schema(); // Basic dimensions @@ -237,9 +239,6 @@ mod tests { assert_eq!(question_mark.static_data().dimension_type, "string"); let sql = question_mark.sql().unwrap().unwrap(); // Verify SQL contains question marks - use crate::cube_bridge::member_sql::MemberSql; - use crate::test_fixtures::cube_bridge::{MockSecurityContext, MockSqlUtils}; - use std::rc::Rc; let (template, _args) = sql .compile_template_sql(Rc::new(MockSqlUtils), Rc::new(MockSecurityContext)) .unwrap(); @@ -286,7 +285,6 @@ mod tests { let google_segment = schema.get_segment("visitors", "google").unwrap(); let sql = google_segment.sql().unwrap(); - use crate::cube_bridge::member_sql::MemberSql; assert_eq!(sql.args_names(), &vec!["CUBE"]); } @@ -299,16 +297,12 @@ mod tests { .unwrap(); let sql = min_checkin.sql().unwrap().unwrap(); - use crate::cube_bridge::member_sql::MemberSql; // Should reference visitor_checkins.minDate assert_eq!(sql.args_names(), &vec!["visitor_checkins"]); } #[test] fn test_geo_dimension_structure() { - use crate::cube_bridge::geo_item::GeoItem; - use crate::cube_bridge::member_sql::MemberSql; - let schema = create_visitors_schema(); let location = schema.get_dimension("visitors", "location").unwrap(); @@ -326,4 +320,3 @@ mod tests { assert_eq!(lon_sql.args_names().len(), 0); } } - diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs index e42cb68885e75..9d40216cd3725 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs @@ -1,6 +1,5 @@ //! Tests for Compiler member evaluation -use crate::planner::sql_evaluator::Compiler; use crate::test_fixtures::schemas::{create_visitors_schema, TestCompiler}; #[test] From d6b7d93dfe7ddbfe42c2d4780b930ac842df3037 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Fri, 14 Nov 2025 15:35:35 +0100 Subject: [PATCH 32/42] basic evaluation test --- .../sql_evaluator/symbols/dimension_symbol.rs | 1 - .../src/test_fixtures/cube_bridge/mod.rs | 2 + .../src/tests/cube_evaluator/mod.rs | 4 +- .../tests/cube_evaluator/symbol_evaluator.rs | 120 ++++++++++++++++++ 4 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs index 76f7de108637b..897c22bc5c890 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs @@ -350,7 +350,6 @@ impl DimensionSymbolFactory { // 3. "cube.cube.cube...cube.member" might come from pre-agg references (as it include full join paths) // And we can not distinguish between "cube.member.granularity" and "cube.cube.member" here, // so we have to try-catch 2 variants of evaluation. - println!("!!! {}", full_name); if let Ok(iter_by_start) = cube_evaluator.parse_path("dimensions".to_string(), full_name.clone()) { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index 1714e401983af..480d2773f617c 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -31,6 +31,7 @@ mod mock_sql_templates_render; mod mock_struct_with_sql_member; mod mock_timeshift_definition; +pub use mock_base_tools::MockBaseTools; pub use mock_case_definition::MockCaseDefinition; pub use mock_case_else_item::MockCaseElseItem; pub use mock_case_item::MockCaseItem; @@ -44,6 +45,7 @@ pub use mock_evaluator::MockCubeEvaluator; pub use mock_expression_struct::MockExpressionStruct; pub use mock_geo_item::MockGeoItem; pub use mock_granularity_definition::MockGranularityDefinition; +pub use mock_join_graph::MockJoinGraph; pub use mock_join_item::MockJoinItem; pub use mock_join_item_definition::MockJoinItemDefinition; pub use mock_measure_definition::MockMeasureDefinition; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/mod.rs index ca7cfbb04fe79..f9492b9d33be8 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/mod.rs @@ -1 +1,3 @@ -mod compilation; \ No newline at end of file +mod compilation; +mod symbol_evaluator; + diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs new file mode 100644 index 0000000000000..04e2297e2143e --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs @@ -0,0 +1,120 @@ +//! Tests for SQL generation for individual symbols + +use crate::cube_bridge::base_tools::BaseTools; +use crate::planner::query_tools::QueryTools; +use crate::planner::sql_evaluator::sql_nodes::{SqlNode, SqlNodesFactory}; +use crate::planner::sql_evaluator::SqlEvaluatorVisitor; +use crate::planner::sql_templates::PlanSqlTemplates; +use crate::test_fixtures::cube_bridge::{ + MockBaseTools, MockCubeDefinition, MockDimensionDefinition, MockJoinGraph, MockSchema, + MockSchemaBuilder, MockSecurityContext, +}; +use cubenativeutils::CubeError; +use std::rc::Rc; + +/// Creates a test schema for symbol SQL generation tests +fn create_test_schema() -> MockSchema { + MockSchemaBuilder::new() + .add_cube("test_cube") + .cube_def( + MockCubeDefinition::builder() + .name("test_cube".to_string()) + .sql("SELECT 1".to_string()) + .build(), + ) + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .finish_cube() + .build() +} + +/// Helper structure for SQL evaluation in tests +/// +/// Encapsulates all the boilerplate needed to evaluate symbols to SQL: +/// - QueryTools with all mock dependencies +/// - SqlEvaluatorVisitor +/// - PlanSqlTemplates +/// - Default node processor +pub struct SqlEvaluationContext { + query_tools: Rc, + visitor: SqlEvaluatorVisitor, + templates: PlanSqlTemplates, + node_processor: Rc, +} + +impl SqlEvaluationContext { + /// Create a new SQL evaluation context with test schema + pub fn new() -> Self { + // Create schema and evaluator + let schema = create_test_schema(); + let evaluator = schema.create_evaluator(); + + // Create QueryTools with mocks + let security_context = Rc::new(MockSecurityContext); + let base_tools = Rc::new(MockBaseTools::builder().build()); + let join_graph = Rc::new(MockJoinGraph); + + let query_tools = QueryTools::try_new( + evaluator.clone(), + security_context, + base_tools.clone(), + join_graph, + None, // timezone + false, // export_annotated_sql + ) + .unwrap(); + + // Create SqlEvaluatorVisitor + let visitor = SqlEvaluatorVisitor::new(query_tools.clone(), None); + + // Create PlanSqlTemplates + let driver_tools = base_tools.driver_tools(false).unwrap(); + let templates = PlanSqlTemplates::try_new(driver_tools, false).unwrap(); + + // Get default node processor + let node_processor = SqlNodesFactory::default().default_node_processor(); + + Self { + query_tools, + visitor, + templates, + node_processor, + } + } + + /// Evaluate a dimension to SQL + pub fn evaluate_dimension(&self, path: &str) -> Result { + let mut compiler = self.query_tools.evaluator_compiler().borrow_mut(); + let symbol = compiler.add_dimension_evaluator(path.to_string())?; + drop(compiler); // Release borrow before calling visitor + + self.visitor + .apply(&symbol, self.node_processor.clone(), &self.templates) + } + + /// Evaluate a measure to SQL + #[allow(dead_code)] + pub fn evaluate_measure(&self, path: &str) -> Result { + let mut compiler = self.query_tools.evaluator_compiler().borrow_mut(); + let symbol = compiler.add_measure_evaluator(path.to_string())?; + drop(compiler); // Release borrow before calling visitor + + self.visitor + .apply(&symbol, self.node_processor.clone(), &self.templates) + } +} + +#[test] +fn test_dimension_sql_evaluation() { + let context = SqlEvaluationContext::new(); + let sql = context.evaluate_dimension("test_cube.id").unwrap(); + + println!("Generated SQL: {}", sql); + assert_eq!(sql, r#""test_cube".id"#); +} + From bc2cef1678deed2eaeeb02ce827a6d7a03888e5e Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Fri, 14 Nov 2025 15:54:13 +0100 Subject: [PATCH 33/42] basic evaluation test --- .../tests/cube_evaluator/symbol_evaluator.rs | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs index 04e2297e2143e..2c7933af913a3 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs @@ -29,6 +29,28 @@ fn create_test_schema() -> MockSchema { .sql("id".to_string()) .build(), ) + .add_dimension( + "source", + MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("{CUBE}.source".to_string()) + .build(), + ) + .add_dimension( + "created_at", + MockDimensionDefinition::builder() + .dimension_type("time".to_string()) + .sql("created_at".to_string()) + .build(), + ) + .add_dimension( + "location", + MockDimensionDefinition::builder() + .dimension_type("geo".to_string()) + .latitude("latitude".to_string()) + .longitude("longitude".to_string()) + .build(), + ) .finish_cube() .build() } @@ -110,11 +132,31 @@ impl SqlEvaluationContext { } #[test] -fn test_dimension_sql_evaluation() { +fn simple_dimension_sql_evaluation() { let context = SqlEvaluationContext::new(); - let sql = context.evaluate_dimension("test_cube.id").unwrap(); - println!("Generated SQL: {}", sql); - assert_eq!(sql, r#""test_cube".id"#); -} + // Test simple dimension without dependencies + let id_sql = context.evaluate_dimension("test_cube.id").unwrap(); + assert_eq!(id_sql, r#""test_cube".id"#); + + // Test dimension with {CUBE} reference + let source_sql = context.evaluate_dimension("test_cube.source").unwrap(); + assert_eq!(source_sql, r#""test_cube".source"#); + // Test time dimension + let created_at_sql = context.evaluate_dimension("test_cube.created_at").unwrap(); + assert_eq!(created_at_sql, r#""test_cube".created_at"#); + + // Test geo dimension (latitude || ',' || longitude) + let location_sql = context.evaluate_dimension("test_cube.location").unwrap(); + assert_eq!(location_sql, "latitude || ',' || longitude"); + + // Test time dimension with granularity (day) + let created_at_day_sql = context + .evaluate_dimension("test_cube.created_at.day") + .unwrap(); + assert_eq!( + created_at_day_sql, + "date_trunc('day', (\"test_cube\".created_at::timestamptz AT TIME ZONE 'UTC'))" + ); +} From 07082f3a4b28dd5a397c957b9175fab53cc8bdbe Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Fri, 14 Nov 2025 16:17:50 +0100 Subject: [PATCH 34/42] in work --- .../src/cube_bridge/dimension_definition.rs | 2 ++ .../cube_bridge/mock_dimension_definition.rs | 5 ++++- .../test_fixtures/cube_bridge/mock_schema.rs | 19 ++++++++++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs index 80967faa7c992..2f25a0d42d351 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs @@ -28,6 +28,8 @@ pub struct DimensionDefinitionStatic { #[serde(rename = "propagateFiltersToSubQuery")] pub propagate_filters_to_sub_query: Option, pub values: Option>, + #[serde(rename = "primaryKey")] + pub primary_key: Option, } #[nativebridge::native_bridge(DimensionDefinitionStatic)] diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs index 8cac0e69ae12e..8245c520e8e72 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs @@ -28,6 +28,8 @@ pub struct MockDimensionDefinition { propagate_filters_to_sub_query: Option, #[builder(default)] values: Option>, + #[builder(default)] + primary_key: Option, // Optional trait fields #[builder(default, setter(strip_option))] @@ -51,7 +53,8 @@ impl_static_data!( add_group_by_references, sub_query, propagate_filters_to_sub_query, - values + values, + primary_key ); impl DimensionDefinition for MockDimensionDefinition { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs index 46b8ea402d405..d6ffbb2220c29 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs @@ -64,7 +64,24 @@ impl MockSchema { /// Create a MockCubeEvaluator from this schema pub fn create_evaluator(self) -> Rc { - Rc::new(MockCubeEvaluator::new(self)) + // Collect primary keys from all cubes + let mut primary_keys = std::collections::HashMap::new(); + + for (cube_name, cube) in &self.cubes { + let mut pk_dimensions = Vec::new(); + + for (dim_name, dimension) in &cube.dimensions { + if dimension.static_data().primary_key == Some(true) { + pk_dimensions.push(dim_name.clone()); + } + } + + if !pk_dimensions.is_empty() { + primary_keys.insert(cube_name.clone(), pk_dimensions); + } + } + + Rc::new(MockCubeEvaluator::with_primary_keys(self, primary_keys)) } /// Create a MockCubeEvaluator with primary keys from this schema From fcfeb26a7324eb34311415697cd19f63d0fb1229 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Fri, 14 Nov 2025 16:34:57 +0100 Subject: [PATCH 35/42] simple measures tests --- .../tests/cube_evaluator/symbol_evaluator.rs | 83 ++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs index 2c7933af913a3..1384ea90b6be7 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs @@ -6,8 +6,8 @@ use crate::planner::sql_evaluator::sql_nodes::{SqlNode, SqlNodesFactory}; use crate::planner::sql_evaluator::SqlEvaluatorVisitor; use crate::planner::sql_templates::PlanSqlTemplates; use crate::test_fixtures::cube_bridge::{ - MockBaseTools, MockCubeDefinition, MockDimensionDefinition, MockJoinGraph, MockSchema, - MockSchemaBuilder, MockSecurityContext, + MockBaseTools, MockCubeDefinition, MockDimensionDefinition, MockJoinGraph, + MockMeasureDefinition, MockSchema, MockSchemaBuilder, MockSecurityContext, }; use cubenativeutils::CubeError; use std::rc::Rc; @@ -27,6 +27,7 @@ fn create_test_schema() -> MockSchema { MockDimensionDefinition::builder() .dimension_type("number".to_string()) .sql("id".to_string()) + .primary_key(Some(true)) .build(), ) .add_dimension( @@ -51,6 +52,48 @@ fn create_test_schema() -> MockSchema { .longitude("longitude".to_string()) .build(), ) + .add_measure( + "sum_revenue", + MockMeasureDefinition::builder() + .measure_type("sum".to_string()) + .sql("revenue".to_string()) + .build(), + ) + .add_measure( + "min_revenue", + MockMeasureDefinition::builder() + .measure_type("min".to_string()) + .sql("revenue".to_string()) + .build(), + ) + .add_measure( + "max_revenue", + MockMeasureDefinition::builder() + .measure_type("max".to_string()) + .sql("revenue".to_string()) + .build(), + ) + .add_measure( + "avg_revenue", + MockMeasureDefinition::builder() + .measure_type("avg".to_string()) + .sql("revenue".to_string()) + .build(), + ) + .add_measure( + "count_distinct_id", + MockMeasureDefinition::builder() + .measure_type("countDistinct".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_measure( + "count_distinct_approx_id", + MockMeasureDefinition::builder() + .measure_type("countDistinctApprox".to_string()) + .sql("id".to_string()) + .build(), + ) .finish_cube() .build() } @@ -160,3 +203,39 @@ fn simple_dimension_sql_evaluation() { "date_trunc('day', (\"test_cube\".created_at::timestamptz AT TIME ZONE 'UTC'))" ); } + +#[test] +fn simple_aggregate_measures() { + let context = SqlEvaluationContext::new(); + + // Test SUM measure + let sum_sql = context.evaluate_measure("test_cube.sum_revenue").unwrap(); + assert_eq!(sum_sql, r#"sum("test_cube".revenue)"#); + + // Test MIN measure + let min_sql = context.evaluate_measure("test_cube.min_revenue").unwrap(); + assert_eq!(min_sql, r#"min("test_cube".revenue)"#); + + // Test MAX measure + let max_sql = context.evaluate_measure("test_cube.max_revenue").unwrap(); + assert_eq!(max_sql, r#"max("test_cube".revenue)"#); + + // Test AVG measure + let avg_sql = context.evaluate_measure("test_cube.avg_revenue").unwrap(); + assert_eq!(avg_sql, r#"avg("test_cube".revenue)"#); + + // Test COUNT DISTINCT measure + let count_distinct_sql = context + .evaluate_measure("test_cube.count_distinct_id") + .unwrap(); + assert_eq!(count_distinct_sql, r#"COUNT(DISTINCT "test_cube".id)"#); + + // Test COUNT DISTINCT APPROX measure + let count_distinct_approx_sql = context + .evaluate_measure("test_cube.count_distinct_approx_id") + .unwrap(); + assert_eq!( + count_distinct_approx_sql, + r#"round(hll_cardinality(hll_add_agg(hll_hash_any("test_cube".id))))"# + ); +} From 3b2634975c7eb8f8630dd5a03fed67718115935e Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Fri, 14 Nov 2025 16:46:08 +0100 Subject: [PATCH 36/42] count measure test --- .../tests/cube_evaluator/symbol_evaluator.rs | 141 +++++++++++++++++- 1 file changed, 137 insertions(+), 4 deletions(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs index 1384ea90b6be7..ad3426ba3e567 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs @@ -12,6 +12,111 @@ use crate::test_fixtures::cube_bridge::{ use cubenativeutils::CubeError; use std::rc::Rc; +/// Creates a schema for count measure testing with no primary keys +fn create_count_schema_no_pk() -> MockSchema { + MockSchemaBuilder::new() + .add_cube("users") + .cube_def( + MockCubeDefinition::builder() + .name("users".to_string()) + .sql("SELECT 1".to_string()) + .build(), + ) + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .build(), + ) + .add_dimension( + "userName", + MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("user_name".to_string()) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .build(), + ) + .finish_cube() + .build() +} + +/// Creates a schema for count measure testing with one primary key +fn create_count_schema_one_pk() -> MockSchema { + MockSchemaBuilder::new() + .add_cube("users") + .cube_def( + MockCubeDefinition::builder() + .name("users".to_string()) + .sql("SELECT 1".to_string()) + .build(), + ) + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .primary_key(Some(true)) + .build(), + ) + .add_dimension( + "userName", + MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("user_name".to_string()) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .build(), + ) + .finish_cube() + .build() +} + +/// Creates a schema for count measure testing with two primary keys +fn create_count_schema_two_pk() -> MockSchema { + MockSchemaBuilder::new() + .add_cube("users") + .cube_def( + MockCubeDefinition::builder() + .name("users".to_string()) + .sql("SELECT 1".to_string()) + .build(), + ) + .add_dimension( + "id", + MockDimensionDefinition::builder() + .dimension_type("number".to_string()) + .sql("id".to_string()) + .primary_key(Some(true)) + .build(), + ) + .add_dimension( + "userName", + MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("user_name".to_string()) + .primary_key(Some(true)) + .build(), + ) + .add_measure( + "count", + MockMeasureDefinition::builder() + .measure_type("count".to_string()) + .build(), + ) + .finish_cube() + .build() +} + /// Creates a test schema for symbol SQL generation tests fn create_test_schema() -> MockSchema { MockSchemaBuilder::new() @@ -113,10 +218,8 @@ pub struct SqlEvaluationContext { } impl SqlEvaluationContext { - /// Create a new SQL evaluation context with test schema - pub fn new() -> Self { - // Create schema and evaluator - let schema = create_test_schema(); + /// Create a new SQL evaluation context with a custom schema + pub fn new_with_schema(schema: MockSchema) -> Self { let evaluator = schema.create_evaluator(); // Create QueryTools with mocks @@ -152,6 +255,12 @@ impl SqlEvaluationContext { } } + /// Create a new SQL evaluation context with test schema + pub fn new() -> Self { + let schema = create_test_schema(); + Self::new_with_schema(schema) + } + /// Evaluate a dimension to SQL pub fn evaluate_dimension(&self, path: &str) -> Result { let mut compiler = self.query_tools.evaluator_compiler().borrow_mut(); @@ -239,3 +348,27 @@ fn simple_aggregate_measures() { r#"round(hll_cardinality(hll_add_agg(hll_hash_any("test_cube".id))))"# ); } + +#[test] +fn count_measure_variants() { + // Test COUNT with no primary keys - should use COUNT(*) + let schema_no_pk = create_count_schema_no_pk(); + let context_no_pk = SqlEvaluationContext::new_with_schema(schema_no_pk); + let count_no_pk_sql = context_no_pk.evaluate_measure("users.count").unwrap(); + assert_eq!(count_no_pk_sql, "count(*)"); + + // Test COUNT with one primary key - should use count(pk) + let schema_one_pk = create_count_schema_one_pk(); + let context_one_pk = SqlEvaluationContext::new_with_schema(schema_one_pk); + let count_one_pk_sql = context_one_pk.evaluate_measure("users.count").unwrap(); + assert_eq!(count_one_pk_sql, r#"count("users".id)"#); + + // Test COUNT with two primary keys - should use count(CAST(pk1) || CAST(pk2)) + let schema_two_pk = create_count_schema_two_pk(); + let context_two_pk = SqlEvaluationContext::new_with_schema(schema_two_pk); + let count_two_pk_sql = context_two_pk.evaluate_measure("users.count").unwrap(); + assert_eq!( + count_two_pk_sql, + "count(CAST(id AS STRING) || CAST(user_name AS STRING))" + ); +} From babc23f23e8d7ee5d3833a7bbc9ec394706e0f53 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Fri, 14 Nov 2025 16:53:44 +0100 Subject: [PATCH 37/42] composite symbols test --- .../tests/cube_evaluator/symbol_evaluator.rs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs index ad3426ba3e567..021e00e3b05eb 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs @@ -142,6 +142,13 @@ fn create_test_schema() -> MockSchema { .sql("{CUBE}.source".to_string()) .build(), ) + .add_dimension( + "source_extended", + MockDimensionDefinition::builder() + .dimension_type("string".to_string()) + .sql("CONCAT({CUBE.source}, '_source')".to_string()) + .build(), + ) .add_dimension( "created_at", MockDimensionDefinition::builder() @@ -185,6 +192,13 @@ fn create_test_schema() -> MockSchema { .sql("revenue".to_string()) .build(), ) + .add_measure( + "complex_measure", + MockMeasureDefinition::builder() + .measure_type("number".to_string()) + .sql("{sum_revenue} + {CUBE.avg_revenue}/{test_cube.min_revenue} - {test_cube.min_revenue}".to_string()) + .build(), + ) .add_measure( "count_distinct_id", MockMeasureDefinition::builder() @@ -372,3 +386,27 @@ fn count_measure_variants() { "count(CAST(id AS STRING) || CAST(user_name AS STRING))" ); } + +#[test] +fn composite_symbols() { + let context = SqlEvaluationContext::new(); + + // Test dimension with member dependency ({CUBE.source}) + let source_extended_sql = context + .evaluate_dimension("test_cube.source_extended") + .unwrap(); + assert_eq!( + source_extended_sql, + r#"CONCAT("test_cube".source, '_source')"# + ); + + // Test measure with multiple member dependencies + // {sum_revenue} + {CUBE.avg_revenue}/{test_cube.min_revenue} - {test_cube.min_revenue} + let complex_measure_sql = context + .evaluate_measure("test_cube.complex_measure") + .unwrap(); + assert_eq!( + complex_measure_sql, + r#"sum("test_cube".revenue) + avg("test_cube".revenue)/min("test_cube".revenue) - min("test_cube".revenue)"# + ); +} From 98e66ea10d68f454702bffacd3a5c71d9bf1bd16 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Fri, 14 Nov 2025 16:58:02 +0100 Subject: [PATCH 38/42] in work --- .../symbols/calendar_dimension.rs | 80 ------------------- .../sql_evaluator/symbols/case_dimension.rs | 36 --------- 2 files changed, 116 deletions(-) delete mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/calendar_dimension.rs delete mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/case_dimension.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/calendar_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/calendar_dimension.rs deleted file mode 100644 index 710b7a1592fb2..0000000000000 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/calendar_dimension.rs +++ /dev/null @@ -1,80 +0,0 @@ -use super::{CalendarDimensionTimeShift, MemberSymbol}; -use cubenativeutils::CubeError; -use std::rc::Rc; - -/// Represents a calendar dimension with time shift capabilities -#[derive(Clone)] -pub struct CalendarDimension { - time_shift: Vec, - time_shift_pk_full_name: Option, - is_self_time_shift_pk: bool, -} - -impl CalendarDimension { - pub fn new( - time_shift: Vec, - time_shift_pk_full_name: Option, - is_self_time_shift_pk: bool, - ) -> Self { - Self { - time_shift, - time_shift_pk_full_name, - is_self_time_shift_pk, - } - } - - pub fn time_shift(&self) -> &Vec { - &self.time_shift - } - - pub fn time_shift_pk_full_name(&self) -> Option { - self.time_shift_pk_full_name.clone() - } - - pub fn is_self_time_shift_pk(&self) -> bool { - self.is_self_time_shift_pk - } - - pub fn get_dependencies(&self, deps: &mut Vec>) { - for shift in &self.time_shift { - if let Some(sql) = &shift.sql { - sql.extract_symbol_deps(deps); - } - } - } - - pub fn get_dependencies_with_path(&self, deps: &mut Vec<(Rc, Vec)>) { - for shift in &self.time_shift { - if let Some(sql) = &shift.sql { - sql.extract_symbol_deps_with_path(deps); - } - } - } - - pub fn apply_to_deps) -> Result, CubeError>>( - &self, - f: &F, - ) -> Result { - let time_shift = self - .time_shift - .iter() - .map(|shift| -> Result<_, CubeError> { - Ok(CalendarDimensionTimeShift { - interval: shift.interval.clone(), - name: shift.name.clone(), - sql: if let Some(sql) = &shift.sql { - Some(sql.apply_recursive(f)?) - } else { - None - }, - }) - }) - .collect::, _>>()?; - - Ok(Self { - time_shift, - time_shift_pk_full_name: self.time_shift_pk_full_name.clone(), - is_self_time_shift_pk: self.is_self_time_shift_pk, - }) - } -} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/case_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/case_dimension.rs deleted file mode 100644 index 3d8c206318d7f..0000000000000 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/case_dimension.rs +++ /dev/null @@ -1,36 +0,0 @@ -use super::{Case, MemberSymbol}; -use cubenativeutils::CubeError; -use std::rc::Rc; - -/// Represents a case dimension with conditional logic -#[derive(Clone)] -pub struct CaseDimension { - case: Case, -} - -impl CaseDimension { - pub fn new(case: Case) -> Self { - Self { case } - } - - pub fn case(&self) -> &Case { - &self.case - } - - pub fn get_dependencies(&self, deps: &mut Vec>) { - self.case.extract_symbol_deps(deps); - } - - pub fn get_dependencies_with_path(&self, deps: &mut Vec<(Rc, Vec)>) { - self.case.extract_symbol_deps_with_path(deps); - } - - pub fn apply_to_deps) -> Result, CubeError>>( - &self, - f: &F, - ) -> Result { - Ok(Self { - case: self.case.apply_to_deps(f)?, - }) - } -} \ No newline at end of file From 480f9cdae5f700b0a1b0bf4ed5d757a0fb7ae7fb Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Fri, 14 Nov 2025 19:39:36 +0100 Subject: [PATCH 39/42] fmt --- .../src/planner/sql_evaluator/compiler.rs | 2 +- .../src/test_fixtures/cube_bridge/macros.rs | 2 +- .../cube_bridge/mock_base_tools.rs | 2 +- .../cube_bridge/mock_case_definition.rs | 2 +- .../cube_bridge/mock_case_else_item.rs | 2 +- .../cube_bridge/mock_case_item.rs | 2 +- .../mock_case_switch_definition.rs | 2 +- .../cube_bridge/mock_case_switch_else_item.rs | 2 +- .../cube_bridge/mock_case_switch_item.rs | 2 +- .../cube_bridge/mock_cube_definition.rs | 2 +- .../cube_bridge/mock_driver_tools.rs | 10 +- .../cube_bridge/mock_evaluator.rs | 1 - .../cube_bridge/mock_expression_struct.rs | 2 +- .../cube_bridge/mock_geo_item.rs | 2 +- .../mock_granularity_definition.rs | 6 +- .../cube_bridge/mock_join_definition.rs | 8 +- .../cube_bridge/mock_join_graph.rs | 2 +- .../cube_bridge/mock_join_item.rs | 2 +- .../cube_bridge/mock_join_item_definition.rs | 2 +- .../mock_member_expression_definition.rs | 7 +- .../cube_bridge/mock_member_order_by.rs | 2 +- .../cube_bridge/mock_member_sql.rs | 20 +- .../test_fixtures/cube_bridge/mock_schema.rs | 1 - .../cube_bridge/mock_security_context.rs | 2 +- .../cube_bridge/mock_segment_definition.rs | 14 +- .../cube_bridge/mock_sql_templates_render.rs | 574 ++++++++++++++---- .../cube_bridge/mock_sql_utils.rs | 2 +- .../mock_struct_with_sql_member.rs | 2 +- .../cube_bridge/mock_timeshift_definition.rs | 1 - .../src/test_fixtures/cube_bridge/mod.rs | 6 +- .../src/test_fixtures/schemas/mod.rs | 2 +- .../src/tests/cube_evaluator/compilation.rs | 81 ++- .../src/tests/cube_evaluator/mod.rs | 1 - .../cubesqlplanner/src/tests/mod.rs | 2 +- 34 files changed, 581 insertions(+), 191 deletions(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs index 9f78cc5622785..938a9891f15c6 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs @@ -160,4 +160,4 @@ impl Compiler { } Ok(node) } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/macros.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/macros.rs index eba9075b7750f..2b99022d69fed 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/macros.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/macros.rs @@ -150,4 +150,4 @@ mod tests { assert_eq!(static_data.name, "test"); assert_eq!(static_data.value, None); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_base_tools.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_base_tools.rs index 8bbd93e1be205..6397dd12a2a75 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_base_tools.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_base_tools.rs @@ -258,4 +258,4 @@ mod tests { assert!(tools.security_context_for_rust().is_ok()); assert!(tools.sql_utils_for_rust().is_ok()); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_definition.rs index 8947eda608e98..2351975d53a8e 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_definition.rs @@ -71,4 +71,4 @@ mod tests { let else_result = case_def.else_label().unwrap(); assert!(else_result.label().is_ok()); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_else_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_else_item.rs index 8c1dd7110bc2a..820de6f753871 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_else_item.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_else_item.rs @@ -33,4 +33,4 @@ mod tests { assert!(matches!(item.label().unwrap(), StringOrSql::String(_))); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_item.rs index 35daa266ad896..f857ea1ef796d 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_item.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_item.rs @@ -42,4 +42,4 @@ mod tests { assert!(item.sql().is_ok()); assert!(matches!(item.label().unwrap(), StringOrSql::String(_))); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_definition.rs index 5234d0b13727f..05d3792e46e31 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_definition.rs @@ -81,4 +81,4 @@ mod tests { let else_result = case_switch.else_sql().unwrap(); assert!(else_result.sql().is_ok()); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_else_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_else_item.rs index 7677e9badb342..cf67520fc15ea 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_else_item.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_else_item.rs @@ -34,4 +34,4 @@ mod tests { assert!(item.sql().is_ok()); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_item.rs index ad948a4fe0f0b..7f7ca43b6e63c 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_item.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_case_switch_item.rs @@ -42,4 +42,4 @@ mod tests { assert_eq!(item.static_data().value, "1"); assert!(item.sql().is_ok()); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_cube_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_cube_definition.rs index 2f8ae7ca748a9..9a00a52e43ae4 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_cube_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_cube_definition.rs @@ -170,4 +170,4 @@ mod tests { let sql_table = cube.sql_table().unwrap().unwrap(); assert_eq!(sql_table.args_names(), &vec!["database"]); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_driver_tools.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_driver_tools.rs index e4b572c9a0869..79274a8cb9b37 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_driver_tools.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_driver_tools.rs @@ -416,7 +416,9 @@ mod tests { fn test_count_distinct_approx() { let tools = MockDriverTools::new(); assert_eq!( - tools.count_distinct_approx("visitor_id".to_string()).unwrap(), + tools + .count_distinct_approx("visitor_id".to_string()) + .unwrap(), "round(hll_cardinality(hll_add_agg(hll_hash_any(visitor_id))))" ); } @@ -447,7 +449,9 @@ mod tests { #[test] fn test_in_db_time_zone() { let tools = MockDriverTools::new(); - let result = tools.in_db_time_zone("2024-01-01T00:00:00".to_string()).unwrap(); + let result = tools + .in_db_time_zone("2024-01-01T00:00:00".to_string()) + .unwrap(); assert_eq!(result, "2024-01-01T00:00:00"); } @@ -467,4 +471,4 @@ mod tests { assert!(templates.contains_template("filters/equals")); assert!(templates.contains_template("functions/SUM")); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs index b34230ec6dca0..d8c552e1c4a3b 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs @@ -674,4 +674,3 @@ mod tests { let _ = evaluator.evaluate_rollup_references("users".to_string(), sql); } } - diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_expression_struct.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_expression_struct.rs index d14844163cadd..9f10d02066baa 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_expression_struct.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_expression_struct.rs @@ -118,4 +118,4 @@ mod tests { let result = expr.add_filters().unwrap().unwrap(); assert_eq!(result.len(), 2); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_geo_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_geo_item.rs index 5ebf5d577c049..80be311aa8ed1 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_geo_item.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_geo_item.rs @@ -34,4 +34,4 @@ mod tests { let sql = geo_item.sql().unwrap(); assert_eq!(sql.args_names(), &vec!["CUBE"]); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_granularity_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_granularity_definition.rs index 3f0b1ed57c4e4..805854ea535c1 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_granularity_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_granularity_definition.rs @@ -1,4 +1,6 @@ -use crate::cube_bridge::granularity_definition::{GranularityDefinition, GranularityDefinitionStatic}; +use crate::cube_bridge::granularity_definition::{ + GranularityDefinition, GranularityDefinitionStatic, +}; use crate::cube_bridge::member_sql::MemberSql; use crate::impl_static_data; use cubenativeutils::CubeError; @@ -73,4 +75,4 @@ mod tests { assert_eq!(static_data.origin, Some("2020-01-01".to_string())); assert_eq!(static_data.offset, Some("3 days".to_string())); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_definition.rs index a754ee1e8033a..973ced3782e1f 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_definition.rs @@ -188,7 +188,11 @@ mod tests { let join_def = MockJoinDefinition::builder() .root("orders".to_string()) - .joins(vec![join_orders_users, join_users_countries, join_orders_products]) + .joins(vec![ + join_orders_users, + join_users_countries, + join_orders_products, + ]) .multiplication_factor(mult_factor) .build(); @@ -202,4 +206,4 @@ mod tests { let joins = join_def.joins().unwrap(); assert_eq!(joins.len(), 3); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_graph.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_graph.rs index b071b1e797035..808ec59c3a82b 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_graph.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_graph.rs @@ -42,4 +42,4 @@ mod tests { let _join_graph = MockJoinGraph; // Just verify we can create the mock } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item.rs index 9c1c0f91f8843..29c412c602d70 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item.rs @@ -92,4 +92,4 @@ mod tests { assert_eq!(join_item.static_data().from, "u"); assert_eq!(join_item.static_data().original_from, "users"); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item_definition.rs index 271ca6201d07d..fe138a6fafe6f 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_join_item_definition.rs @@ -70,4 +70,4 @@ mod tests { assert_eq!(join_def.static_data().relationship, "one_to_one"); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_expression_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_expression_definition.rs index cee57156ec409..a8fc5875d4ddc 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_expression_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_expression_definition.rs @@ -111,10 +111,7 @@ mod tests { ); let result = expr.expression().unwrap(); - assert!(matches!( - result, - MemberExpressionExpressionDef::Struct(_) - )); + assert!(matches!(result, MemberExpressionExpressionDef::Struct(_))); } #[test] @@ -132,4 +129,4 @@ mod tests { assert_eq!(static_data.name, None); assert_eq!(static_data.cube_name, None); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_order_by.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_order_by.rs index 5be93a558be06..33cefcc253d29 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_order_by.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_order_by.rs @@ -51,4 +51,4 @@ mod tests { assert_eq!(order.dir().unwrap(), "asc"); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_sql.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_sql.rs index 744c1555248f2..8b4d9e4f3cce3 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_sql.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_sql.rs @@ -115,7 +115,10 @@ impl MemberSql for MockMemberSql { _sql_utils: Rc, _security_context: Rc, ) -> Result<(SqlTemplate, SqlTemplateArgs), CubeError> { - Ok((SqlTemplate::String(self.template.clone()), self.args.clone())) + Ok(( + SqlTemplate::String(self.template.clone()), + self.args.clone(), + )) } } @@ -150,18 +153,25 @@ mod tests { assert_eq!(mock.template, "{arg:0}"); assert_eq!(mock.args.symbol_paths.len(), 1); - assert_eq!(mock.args.symbol_paths[0], vec!["other_cube", "cube2", "field"]); + assert_eq!( + mock.args.symbol_paths[0], + vec!["other_cube", "cube2", "field"] + ); assert_eq!(mock.args_names, vec!["other_cube"]); } #[test] fn test_complex_expression() { - let mock = MockMemberSql::new("{CUBE.field} / {other_cube.cube2.field} + {revenue}").unwrap(); + let mock = + MockMemberSql::new("{CUBE.field} / {other_cube.cube2.field} + {revenue}").unwrap(); assert_eq!(mock.template, "{arg:0} / {arg:1} + {arg:2}"); assert_eq!(mock.args.symbol_paths.len(), 3); assert_eq!(mock.args.symbol_paths[0], vec!["CUBE", "field"]); - assert_eq!(mock.args.symbol_paths[1], vec!["other_cube", "cube2", "field"]); + assert_eq!( + mock.args.symbol_paths[1], + vec!["other_cube", "cube2", "field"] + ); assert_eq!(mock.args.symbol_paths[2], vec!["revenue"]); assert_eq!(mock.args_names, vec!["CUBE", "other_cube", "revenue"]); } @@ -246,4 +256,4 @@ mod tests { assert_eq!(args.symbol_paths[0], vec!["CUBE", "field"]); assert_eq!(args.symbol_paths[1], vec!["other", "field"]); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs index d6ffbb2220c29..665e41168de97 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs @@ -1100,4 +1100,3 @@ mod tests { .build(); } } - diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_security_context.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_security_context.rs index 28ddf1bd595f4..7161cd8efae32 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_security_context.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_security_context.rs @@ -9,4 +9,4 @@ impl SecurityContext for MockSecurityContext { fn as_any(self: Rc) -> Rc { self } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_segment_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_segment_definition.rs index 0b067aa3c17e6..e8900971d3d54 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_segment_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_segment_definition.rs @@ -79,8 +79,10 @@ mod tests { #[test] fn test_complex_segment_sql() { let segment = MockSegmentDefinition::builder() - .sql("{CUBE.created_at} >= '2024-01-01' AND {CUBE.status} IN ('active', 'pending')" - .to_string()) + .sql( + "{CUBE.created_at} >= '2024-01-01' AND {CUBE.status} IN ('active', 'pending')" + .to_string(), + ) .build(); let sql = segment.sql().unwrap(); @@ -109,11 +111,13 @@ mod tests { #[test] fn test_segment_with_cross_cube_reference() { let segment = MockSegmentDefinition::builder() - .sql("{CUBE.user_id} IN (SELECT id FROM {users} WHERE {users.is_premium} = true)" - .to_string()) + .sql( + "{CUBE.user_id} IN (SELECT id FROM {users} WHERE {users.is_premium} = true)" + .to_string(), + ) .build(); let sql = segment.sql().unwrap(); assert_eq!(sql.args_names(), &vec!["CUBE", "users"]); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_sql_templates_render.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_sql_templates_render.rs index ac6799d9a4310..be2d04fb257c8 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_sql_templates_render.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_sql_templates_render.rs @@ -66,117 +66,414 @@ impl MockSqlTemplatesRender { let mut templates = HashMap::new(); // Functions - based on BaseQuery.js:4241-4315 - templates.insert("functions/SUM".to_string(), "SUM({{ args_concat }})".to_string()); - templates.insert("functions/MIN".to_string(), "MIN({{ args_concat }})".to_string()); - templates.insert("functions/MAX".to_string(), "MAX({{ args_concat }})".to_string()); - templates.insert("functions/COUNT".to_string(), "COUNT({{ args_concat }})".to_string()); - templates.insert("functions/COUNT_DISTINCT".to_string(), "COUNT(DISTINCT {{ args_concat }})".to_string()); - templates.insert("functions/AVG".to_string(), "AVG({{ args_concat }})".to_string()); - templates.insert("functions/STDDEV_POP".to_string(), "STDDEV_POP({{ args_concat }})".to_string()); - templates.insert("functions/STDDEV_SAMP".to_string(), "STDDEV_SAMP({{ args_concat }})".to_string()); - templates.insert("functions/VAR_POP".to_string(), "VAR_POP({{ args_concat }})".to_string()); - templates.insert("functions/VAR_SAMP".to_string(), "VAR_SAMP({{ args_concat }})".to_string()); - templates.insert("functions/COVAR_POP".to_string(), "COVAR_POP({{ args_concat }})".to_string()); - templates.insert("functions/COVAR_SAMP".to_string(), "COVAR_SAMP({{ args_concat }})".to_string()); - templates.insert("functions/GROUP_ANY".to_string(), "max({{ expr }})".to_string()); - templates.insert("functions/COALESCE".to_string(), "COALESCE({{ args_concat }})".to_string()); - templates.insert("functions/CONCAT".to_string(), "CONCAT({{ args_concat }})".to_string()); - templates.insert("functions/FLOOR".to_string(), "FLOOR({{ args_concat }})".to_string()); - templates.insert("functions/CEIL".to_string(), "CEIL({{ args_concat }})".to_string()); - templates.insert("functions/TRUNC".to_string(), "TRUNC({{ args_concat }})".to_string()); - templates.insert("functions/LOWER".to_string(), "LOWER({{ args_concat }})".to_string()); - templates.insert("functions/UPPER".to_string(), "UPPER({{ args_concat }})".to_string()); - templates.insert("functions/LEFT".to_string(), "LEFT({{ args_concat }})".to_string()); - templates.insert("functions/RIGHT".to_string(), "RIGHT({{ args_concat }})".to_string()); - templates.insert("functions/SQRT".to_string(), "SQRT({{ args_concat }})".to_string()); - templates.insert("functions/ABS".to_string(), "ABS({{ args_concat }})".to_string()); - templates.insert("functions/ACOS".to_string(), "ACOS({{ args_concat }})".to_string()); - templates.insert("functions/ASIN".to_string(), "ASIN({{ args_concat }})".to_string()); - templates.insert("functions/ATAN".to_string(), "ATAN({{ args_concat }})".to_string()); - templates.insert("functions/COS".to_string(), "COS({{ args_concat }})".to_string()); - templates.insert("functions/EXP".to_string(), "EXP({{ args_concat }})".to_string()); - templates.insert("functions/LN".to_string(), "LN({{ args_concat }})".to_string()); - templates.insert("functions/LOG".to_string(), "LOG({{ args_concat }})".to_string()); - templates.insert("functions/DLOG10".to_string(), "LOG10({{ args_concat }})".to_string()); + templates.insert( + "functions/SUM".to_string(), + "SUM({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/MIN".to_string(), + "MIN({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/MAX".to_string(), + "MAX({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/COUNT".to_string(), + "COUNT({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/COUNT_DISTINCT".to_string(), + "COUNT(DISTINCT {{ args_concat }})".to_string(), + ); + templates.insert( + "functions/AVG".to_string(), + "AVG({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/STDDEV_POP".to_string(), + "STDDEV_POP({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/STDDEV_SAMP".to_string(), + "STDDEV_SAMP({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/VAR_POP".to_string(), + "VAR_POP({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/VAR_SAMP".to_string(), + "VAR_SAMP({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/COVAR_POP".to_string(), + "COVAR_POP({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/COVAR_SAMP".to_string(), + "COVAR_SAMP({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/GROUP_ANY".to_string(), + "max({{ expr }})".to_string(), + ); + templates.insert( + "functions/COALESCE".to_string(), + "COALESCE({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/CONCAT".to_string(), + "CONCAT({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/FLOOR".to_string(), + "FLOOR({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/CEIL".to_string(), + "CEIL({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/TRUNC".to_string(), + "TRUNC({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/LOWER".to_string(), + "LOWER({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/UPPER".to_string(), + "UPPER({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/LEFT".to_string(), + "LEFT({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/RIGHT".to_string(), + "RIGHT({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/SQRT".to_string(), + "SQRT({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/ABS".to_string(), + "ABS({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/ACOS".to_string(), + "ACOS({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/ASIN".to_string(), + "ASIN({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/ATAN".to_string(), + "ATAN({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/COS".to_string(), + "COS({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/EXP".to_string(), + "EXP({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/LN".to_string(), + "LN({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/LOG".to_string(), + "LOG({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/DLOG10".to_string(), + "LOG10({{ args_concat }})".to_string(), + ); templates.insert("functions/PI".to_string(), "PI()".to_string()); - templates.insert("functions/POWER".to_string(), "POWER({{ args_concat }})".to_string()); - templates.insert("functions/SIN".to_string(), "SIN({{ args_concat }})".to_string()); - templates.insert("functions/TAN".to_string(), "TAN({{ args_concat }})".to_string()); - templates.insert("functions/REPEAT".to_string(), "REPEAT({{ args_concat }})".to_string()); - templates.insert("functions/NULLIF".to_string(), "NULLIF({{ args_concat }})".to_string()); - templates.insert("functions/ROUND".to_string(), "ROUND({{ args_concat }})".to_string()); - templates.insert("functions/STDDEV".to_string(), "STDDEV_SAMP({{ args_concat }})".to_string()); - templates.insert("functions/SUBSTR".to_string(), "SUBSTRING({{ args_concat }})".to_string()); - templates.insert("functions/CHARACTERLENGTH".to_string(), "CHAR_LENGTH({{ args[0] }})".to_string()); - templates.insert("functions/BTRIM".to_string(), "BTRIM({{ args_concat }})".to_string()); - templates.insert("functions/LTRIM".to_string(), "LTRIM({{ args_concat }})".to_string()); - templates.insert("functions/RTRIM".to_string(), "RTRIM({{ args_concat }})".to_string()); - templates.insert("functions/ATAN2".to_string(), "ATAN2({{ args_concat }})".to_string()); - templates.insert("functions/COT".to_string(), "COT({{ args_concat }})".to_string()); - templates.insert("functions/DEGREES".to_string(), "DEGREES({{ args_concat }})".to_string()); - templates.insert("functions/RADIANS".to_string(), "RADIANS({{ args_concat }})".to_string()); - templates.insert("functions/SIGN".to_string(), "SIGN({{ args_concat }})".to_string()); - templates.insert("functions/ASCII".to_string(), "ASCII({{ args_concat }})".to_string()); - templates.insert("functions/STRPOS".to_string(), "POSITION({{ args[1] }} IN {{ args[0] }})".to_string()); - templates.insert("functions/REPLACE".to_string(), "REPLACE({{ args_concat }})".to_string()); - templates.insert("functions/DATEDIFF".to_string(), "DATEDIFF({{ date_part }}, {{ args[1] }}, {{ args[2] }})".to_string()); - templates.insert("functions/TO_CHAR".to_string(), "TO_CHAR({{ args_concat }})".to_string()); - templates.insert("functions/DATE".to_string(), "DATE({{ args_concat }})".to_string()); - templates.insert("functions/PERCENTILECONT".to_string(), "PERCENTILE_CONT({{ args_concat }})".to_string()); + templates.insert( + "functions/POWER".to_string(), + "POWER({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/SIN".to_string(), + "SIN({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/TAN".to_string(), + "TAN({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/REPEAT".to_string(), + "REPEAT({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/NULLIF".to_string(), + "NULLIF({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/ROUND".to_string(), + "ROUND({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/STDDEV".to_string(), + "STDDEV_SAMP({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/SUBSTR".to_string(), + "SUBSTRING({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/CHARACTERLENGTH".to_string(), + "CHAR_LENGTH({{ args[0] }})".to_string(), + ); + templates.insert( + "functions/BTRIM".to_string(), + "BTRIM({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/LTRIM".to_string(), + "LTRIM({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/RTRIM".to_string(), + "RTRIM({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/ATAN2".to_string(), + "ATAN2({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/COT".to_string(), + "COT({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/DEGREES".to_string(), + "DEGREES({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/RADIANS".to_string(), + "RADIANS({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/SIGN".to_string(), + "SIGN({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/ASCII".to_string(), + "ASCII({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/STRPOS".to_string(), + "POSITION({{ args[1] }} IN {{ args[0] }})".to_string(), + ); + templates.insert( + "functions/REPLACE".to_string(), + "REPLACE({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/DATEDIFF".to_string(), + "DATEDIFF({{ date_part }}, {{ args[1] }}, {{ args[2] }})".to_string(), + ); + templates.insert( + "functions/TO_CHAR".to_string(), + "TO_CHAR({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/DATE".to_string(), + "DATE({{ args_concat }})".to_string(), + ); + templates.insert( + "functions/PERCENTILECONT".to_string(), + "PERCENTILE_CONT({{ args_concat }})".to_string(), + ); // Expressions - based on BaseQuery.js:4360-4391 - templates.insert("expressions/column_reference".to_string(), "{% if table_name %}{{ table_name }}.{% endif %}{{ name }}".to_string()); - templates.insert("expressions/column_aliased".to_string(), "{{expr}} {{quoted_alias}}".to_string()); - templates.insert("expressions/query_aliased".to_string(), "{{ query }} AS {{ quoted_alias }}".to_string()); + templates.insert( + "expressions/column_reference".to_string(), + "{% if table_name %}{{ table_name }}.{% endif %}{{ name }}".to_string(), + ); + templates.insert( + "expressions/column_aliased".to_string(), + "{{expr}} {{quoted_alias}}".to_string(), + ); + templates.insert( + "expressions/query_aliased".to_string(), + "{{ query }} AS {{ quoted_alias }}".to_string(), + ); templates.insert("expressions/case".to_string(), "CASE{% if expr %} {{ expr }}{% endif %}{% for when, then in when_then %} WHEN {{ when }} THEN {{ then }}{% endfor %}{% if else_expr %} ELSE {{ else_expr }}{% endif %} END".to_string()); - templates.insert("expressions/is_null".to_string(), "({{ expr }} IS {% if negate %}NOT {% endif %}NULL)".to_string()); - templates.insert("expressions/binary".to_string(), "({{ left }} {{ op }} {{ right }})".to_string()); + templates.insert( + "expressions/is_null".to_string(), + "({{ expr }} IS {% if negate %}NOT {% endif %}NULL)".to_string(), + ); + templates.insert( + "expressions/binary".to_string(), + "({{ left }} {{ op }} {{ right }})".to_string(), + ); templates.insert("expressions/sort".to_string(), "{{ expr }} {% if asc %}ASC{% else %}DESC{% endif %} NULLS {% if nulls_first %}FIRST{% else %}LAST{% endif %}".to_string()); templates.insert("expressions/order_by".to_string(), "{% if index %} {{ index }} {% else %} {{ expr }} {% endif %} {% if asc %}ASC{% else %}DESC{% endif %}{% if nulls_first %} NULLS FIRST{% endif %}".to_string()); - templates.insert("expressions/cast".to_string(), "CAST({{ expr }} AS {{ data_type }})".to_string()); + templates.insert( + "expressions/cast".to_string(), + "CAST({{ expr }} AS {{ data_type }})".to_string(), + ); templates.insert("expressions/window_function".to_string(), "{{ fun_call }} OVER ({% if partition_by_concat %}PARTITION BY {{ partition_by_concat }}{% if order_by_concat or window_frame %} {% endif %}{% endif %}{% if order_by_concat %}ORDER BY {{ order_by_concat }}{% if window_frame %} {% endif %}{% endif %}{% if window_frame %}{{ window_frame }}{% endif %})".to_string()); - templates.insert("expressions/window_frame_bounds".to_string(), "{{ frame_type }} BETWEEN {{ frame_start }} AND {{ frame_end }}".to_string()); - templates.insert("expressions/in_list".to_string(), "{{ expr }} {% if negated %}NOT {% endif %}IN ({{ in_exprs_concat }})".to_string()); - templates.insert("expressions/subquery".to_string(), "({{ expr }})".to_string()); - templates.insert("expressions/in_subquery".to_string(), "{{ expr }} {% if negated %}NOT {% endif %}IN {{ subquery_expr }}".to_string()); - templates.insert("expressions/rollup".to_string(), "ROLLUP({{ exprs_concat }})".to_string()); - templates.insert("expressions/cube".to_string(), "CUBE({{ exprs_concat }})".to_string()); - templates.insert("expressions/negative".to_string(), "-({{ expr }})".to_string()); - templates.insert("expressions/not".to_string(), "NOT ({{ expr }})".to_string()); - templates.insert("expressions/add_interval".to_string(), "{{ date }} + interval '{{ interval }}'".to_string()); - templates.insert("expressions/sub_interval".to_string(), "{{ date }} - interval '{{ interval }}'".to_string()); + templates.insert( + "expressions/window_frame_bounds".to_string(), + "{{ frame_type }} BETWEEN {{ frame_start }} AND {{ frame_end }}".to_string(), + ); + templates.insert( + "expressions/in_list".to_string(), + "{{ expr }} {% if negated %}NOT {% endif %}IN ({{ in_exprs_concat }})".to_string(), + ); + templates.insert( + "expressions/subquery".to_string(), + "({{ expr }})".to_string(), + ); + templates.insert( + "expressions/in_subquery".to_string(), + "{{ expr }} {% if negated %}NOT {% endif %}IN {{ subquery_expr }}".to_string(), + ); + templates.insert( + "expressions/rollup".to_string(), + "ROLLUP({{ exprs_concat }})".to_string(), + ); + templates.insert( + "expressions/cube".to_string(), + "CUBE({{ exprs_concat }})".to_string(), + ); + templates.insert( + "expressions/negative".to_string(), + "-({{ expr }})".to_string(), + ); + templates.insert( + "expressions/not".to_string(), + "NOT ({{ expr }})".to_string(), + ); + templates.insert( + "expressions/add_interval".to_string(), + "{{ date }} + interval '{{ interval }}'".to_string(), + ); + templates.insert( + "expressions/sub_interval".to_string(), + "{{ date }} - interval '{{ interval }}'".to_string(), + ); templates.insert("expressions/true".to_string(), "TRUE".to_string()); templates.insert("expressions/false".to_string(), "FALSE".to_string()); - templates.insert("expressions/like".to_string(), "{{ expr }} {% if negated %}NOT {% endif %}LIKE {{ pattern }}".to_string()); - templates.insert("expressions/ilike".to_string(), "{{ expr }} {% if negated %}NOT {% endif %}ILIKE {{ pattern }}".to_string()); - templates.insert("expressions/like_escape".to_string(), "{{ like_expr }} ESCAPE {{ escape_char }}".to_string()); - templates.insert("expressions/within_group".to_string(), "{{ fun_sql }} WITHIN GROUP (ORDER BY {{ within_group_concat }})".to_string()); - templates.insert("expressions/concat_strings".to_string(), "{{ strings | join(' || ' ) }}".to_string()); - templates.insert("expressions/rolling_window_expr_timestamp_cast".to_string(), "{{ value }}".to_string()); - templates.insert("expressions/timestamp_literal".to_string(), "{{ value }}".to_string()); - templates.insert("expressions/between".to_string(), "{{ expr }} {% if negated %}NOT {% endif %}BETWEEN {{ low }} AND {{ high }}".to_string()); + templates.insert( + "expressions/like".to_string(), + "{{ expr }} {% if negated %}NOT {% endif %}LIKE {{ pattern }}".to_string(), + ); + templates.insert( + "expressions/ilike".to_string(), + "{{ expr }} {% if negated %}NOT {% endif %}ILIKE {{ pattern }}".to_string(), + ); + templates.insert( + "expressions/like_escape".to_string(), + "{{ like_expr }} ESCAPE {{ escape_char }}".to_string(), + ); + templates.insert( + "expressions/within_group".to_string(), + "{{ fun_sql }} WITHIN GROUP (ORDER BY {{ within_group_concat }})".to_string(), + ); + templates.insert( + "expressions/concat_strings".to_string(), + "{{ strings | join(' || ' ) }}".to_string(), + ); + templates.insert( + "expressions/rolling_window_expr_timestamp_cast".to_string(), + "{{ value }}".to_string(), + ); + templates.insert( + "expressions/timestamp_literal".to_string(), + "{{ value }}".to_string(), + ); + templates.insert( + "expressions/between".to_string(), + "{{ expr }} {% if negated %}NOT {% endif %}BETWEEN {{ low }} AND {{ high }}" + .to_string(), + ); // Tesseract - based on BaseQuery.js:4392-4397 - templates.insert("tesseract/ilike".to_string(), "{{ expr }} {% if negated %}NOT {% endif %}ILIKE {{ pattern }}".to_string()); - templates.insert("tesseract/series_bounds_cast".to_string(), "{{ expr }}".to_string()); - templates.insert("tesseract/bool_param_cast".to_string(), "{{ expr }}".to_string()); - templates.insert("tesseract/number_param_cast".to_string(), "{{ expr }}".to_string()); + templates.insert( + "tesseract/ilike".to_string(), + "{{ expr }} {% if negated %}NOT {% endif %}ILIKE {{ pattern }}".to_string(), + ); + templates.insert( + "tesseract/series_bounds_cast".to_string(), + "{{ expr }}".to_string(), + ); + templates.insert( + "tesseract/bool_param_cast".to_string(), + "{{ expr }}".to_string(), + ); + templates.insert( + "tesseract/number_param_cast".to_string(), + "{{ expr }}".to_string(), + ); // Filters - based on BaseQuery.js:4398-4414 - templates.insert("filters/equals".to_string(), "{{ column }} = {{ value }}{{ is_null_check }}".to_string()); - templates.insert("filters/not_equals".to_string(), "{{ column }} <> {{ value }}{{ is_null_check }}".to_string()); - templates.insert("filters/or_is_null_check".to_string(), " OR {{ column }} IS NULL".to_string()); - templates.insert("filters/set_where".to_string(), "{{ column }} IS NOT NULL".to_string()); - templates.insert("filters/not_set_where".to_string(), "{{ column }} IS NULL".to_string()); - templates.insert("filters/in".to_string(), "{{ column }} IN ({{ values_concat }}){{ is_null_check }}".to_string()); - templates.insert("filters/not_in".to_string(), "{{ column }} NOT IN ({{ values_concat }}){{ is_null_check }}".to_string()); - templates.insert("filters/time_range_filter".to_string(), "{{ column }} >= {{ from_timestamp }} AND {{ column }} <= {{ to_timestamp }}".to_string()); - templates.insert("filters/time_not_in_range_filter".to_string(), "{{ column }} < {{ from_timestamp }} OR {{ column }} > {{ to_timestamp }}".to_string()); - templates.insert("filters/gt".to_string(), "{{ column }} > {{ param }}".to_string()); - templates.insert("filters/gte".to_string(), "{{ column }} >= {{ param }}".to_string()); - templates.insert("filters/lt".to_string(), "{{ column }} < {{ param }}".to_string()); - templates.insert("filters/lte".to_string(), "{{ column }} <= {{ param }}".to_string()); - templates.insert("filters/like_pattern".to_string(), "{% if start_wild %}'%' || {% endif %}{{ value }}{% if end_wild %}|| '%'{% endif %}".to_string()); + templates.insert( + "filters/equals".to_string(), + "{{ column }} = {{ value }}{{ is_null_check }}".to_string(), + ); + templates.insert( + "filters/not_equals".to_string(), + "{{ column }} <> {{ value }}{{ is_null_check }}".to_string(), + ); + templates.insert( + "filters/or_is_null_check".to_string(), + " OR {{ column }} IS NULL".to_string(), + ); + templates.insert( + "filters/set_where".to_string(), + "{{ column }} IS NOT NULL".to_string(), + ); + templates.insert( + "filters/not_set_where".to_string(), + "{{ column }} IS NULL".to_string(), + ); + templates.insert( + "filters/in".to_string(), + "{{ column }} IN ({{ values_concat }}){{ is_null_check }}".to_string(), + ); + templates.insert( + "filters/not_in".to_string(), + "{{ column }} NOT IN ({{ values_concat }}){{ is_null_check }}".to_string(), + ); + templates.insert( + "filters/time_range_filter".to_string(), + "{{ column }} >= {{ from_timestamp }} AND {{ column }} <= {{ to_timestamp }}" + .to_string(), + ); + templates.insert( + "filters/time_not_in_range_filter".to_string(), + "{{ column }} < {{ from_timestamp }} OR {{ column }} > {{ to_timestamp }}".to_string(), + ); + templates.insert( + "filters/gt".to_string(), + "{{ column }} > {{ param }}".to_string(), + ); + templates.insert( + "filters/gte".to_string(), + "{{ column }} >= {{ param }}".to_string(), + ); + templates.insert( + "filters/lt".to_string(), + "{{ column }} < {{ param }}".to_string(), + ); + templates.insert( + "filters/lte".to_string(), + "{{ column }} <= {{ param }}".to_string(), + ); + templates.insert( + "filters/like_pattern".to_string(), + "{% if start_wild %}'%' || {% endif %}{{ value }}{% if end_wild %}|| '%'{% endif %}" + .to_string(), + ); templates.insert("filters/always_true".to_string(), "1 = 1".to_string()); // Quotes - based on BaseQuery.js:4417-4420 @@ -195,9 +492,18 @@ impl MockSqlTemplatesRender { templates.insert("window_frame_types/range".to_string(), "RANGE".to_string()); // Window frame bounds - based on BaseQuery.js:4432-4436 - templates.insert("window_frame_bounds/preceding".to_string(), "{% if n is not none %}{{ n }}{% else %}UNBOUNDED{% endif %} PRECEDING".to_string()); - templates.insert("window_frame_bounds/current_row".to_string(), "CURRENT ROW".to_string()); - templates.insert("window_frame_bounds/following".to_string(), "{% if n is not none %}{{ n }}{% else %}UNBOUNDED{% endif %} FOLLOWING".to_string()); + templates.insert( + "window_frame_bounds/preceding".to_string(), + "{% if n is not none %}{{ n }}{% else %}UNBOUNDED{% endif %} PRECEDING".to_string(), + ); + templates.insert( + "window_frame_bounds/current_row".to_string(), + "CURRENT ROW".to_string(), + ); + templates.insert( + "window_frame_bounds/following".to_string(), + "{% if n is not none %}{{ n }}{% else %}UNBOUNDED{% endif %} FOLLOWING".to_string(), + ); // Types - based on BaseQuery.js:4437-4452 templates.insert("types/string".to_string(), "STRING".to_string()); @@ -208,7 +514,10 @@ impl MockSqlTemplatesRender { templates.insert("types/bigint".to_string(), "BIGINT".to_string()); templates.insert("types/float".to_string(), "FLOAT".to_string()); templates.insert("types/double".to_string(), "DOUBLE".to_string()); - templates.insert("types/decimal".to_string(), "DECIMAL({{ precision }},{{ scale }})".to_string()); + templates.insert( + "types/decimal".to_string(), + "DECIMAL({{ precision }},{{ scale }})".to_string(), + ); templates.insert("types/timestamp".to_string(), "TIMESTAMP".to_string()); templates.insert("types/date".to_string(), "DATE".to_string()); templates.insert("types/time".to_string(), "TIME".to_string()); @@ -294,18 +603,12 @@ mod tests { #[test] fn test_template_with_multiple_variables() { let mut templates = HashMap::new(); - templates.insert( - "complex".to_string(), - "{{column}} = {{value}}".to_string(), - ); + templates.insert("complex".to_string(), "{{column}} = {{value}}".to_string()); let render = MockSqlTemplatesRender::try_new(templates).unwrap(); let result = render - .render_template( - "complex", - context! { column => "id", value => "123" }, - ) + .render_template("complex", context! { column => "id", value => "123" }) .unwrap(); assert_eq!(result, "id = 123"); @@ -314,7 +617,10 @@ mod tests { #[test] fn test_template_with_numeric_values() { let mut templates = HashMap::new(); - templates.insert("numeric".to_string(), "LIMIT {{limit}} OFFSET {{offset}}".to_string()); + templates.insert( + "numeric".to_string(), + "LIMIT {{limit}} OFFSET {{offset}}".to_string(), + ); let render = MockSqlTemplatesRender::try_new(templates).unwrap(); @@ -337,7 +643,10 @@ mod tests { // Test COUNT DISTINCT let result = render - .render_template("functions/COUNT_DISTINCT", context! { args_concat => "user_id" }) + .render_template( + "functions/COUNT_DISTINCT", + context! { args_concat => "user_id" }, + ) .unwrap(); assert_eq!(result, "COUNT(DISTINCT user_id)"); @@ -426,10 +735,7 @@ mod tests { // Test column_reference without table let result = render - .render_template( - "expressions/column_reference", - context! { name => "id" }, - ) + .render_template("expressions/column_reference", context! { name => "id" }) .unwrap(); assert_eq!(result, "id"); @@ -480,11 +786,15 @@ mod tests { "STRING" ); assert_eq!( - render.render_template("types/integer", context! {}).unwrap(), + render + .render_template("types/integer", context! {}) + .unwrap(), "INTEGER" ); assert_eq!( - render.render_template("types/timestamp", context! {}).unwrap(), + render + .render_template("types/timestamp", context! {}) + .unwrap(), "TIMESTAMP" ); @@ -507,13 +817,19 @@ mod tests { // Test window_frame_bounds with UNBOUNDED (n is None) let result = render - .render_template("window_frame_bounds/preceding", context! { n => Value::from(()) }) + .render_template( + "window_frame_bounds/preceding", + context! { n => Value::from(()) }, + ) .unwrap(); assert_eq!(result, "UNBOUNDED PRECEDING"); // Test window_frame_bounds following with UNBOUNDED let result = render - .render_template("window_frame_bounds/following", context! { n => Value::from(()) }) + .render_template( + "window_frame_bounds/following", + context! { n => Value::from(()) }, + ) .unwrap(); assert_eq!(result, "UNBOUNDED FOLLOWING"); @@ -529,11 +845,15 @@ mod tests { let render = MockSqlTemplatesRender::default_templates(); assert_eq!( - render.render_template("join_types/inner", context! {}).unwrap(), + render + .render_template("join_types/inner", context! {}) + .unwrap(), "INNER" ); assert_eq!( - render.render_template("join_types/left", context! {}).unwrap(), + render + .render_template("join_types/left", context! {}) + .unwrap(), "LEFT" ); } @@ -553,12 +873,16 @@ mod tests { let render = MockSqlTemplatesRender::default_templates(); assert_eq!( - render.render_template("quotes/identifiers", context! {}).unwrap(), + render + .render_template("quotes/identifiers", context! {}) + .unwrap(), "\"" ); assert_eq!( - render.render_template("quotes/escape", context! {}).unwrap(), + render + .render_template("quotes/escape", context! {}) + .unwrap(), "\"\"" ); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_sql_utils.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_sql_utils.rs index 1d101fb0baacf..0a54addec50a8 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_sql_utils.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_sql_utils.rs @@ -9,4 +9,4 @@ impl SqlUtils for MockSqlUtils { fn as_any(self: Rc) -> Rc { self } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_struct_with_sql_member.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_struct_with_sql_member.rs index 6b6e03fba99cc..b194d370eea17 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_struct_with_sql_member.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_struct_with_sql_member.rs @@ -36,4 +36,4 @@ mod tests { let sql = item.sql().unwrap(); assert_eq!(sql.args_names(), &vec!["CUBE"]); } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_timeshift_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_timeshift_definition.rs index 9e872876d25c3..1091b99e3a8fa 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_timeshift_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_timeshift_definition.rs @@ -76,4 +76,3 @@ mod tests { assert!(timeshift.sql().unwrap().is_none()); } } - diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index 480d2773f617c..4296a6e61abf1 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -26,8 +26,8 @@ mod mock_member_sql; mod mock_schema; mod mock_security_context; mod mock_segment_definition; -mod mock_sql_utils; mod mock_sql_templates_render; +mod mock_sql_utils; mod mock_struct_with_sql_member; mod mock_timeshift_definition; @@ -54,7 +54,7 @@ pub use mock_member_sql::MockMemberSql; pub use mock_schema::{MockSchema, MockSchemaBuilder}; pub use mock_security_context::MockSecurityContext; pub use mock_segment_definition::MockSegmentDefinition; -pub use mock_sql_utils::MockSqlUtils; pub use mock_sql_templates_render::MockSqlTemplatesRender; +pub use mock_sql_utils::MockSqlUtils; pub use mock_struct_with_sql_member::MockStructWithSqlMember; -pub use mock_timeshift_definition::MockTimeShiftDefinition; \ No newline at end of file +pub use mock_timeshift_definition::MockTimeShiftDefinition; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/mod.rs index 4e7d8244a5a47..2c20c4c3c85cb 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/mod.rs @@ -26,4 +26,4 @@ impl TestCompiler { Self { compiler } } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs index 9d40216cd3725..35e81092402b6 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs @@ -451,12 +451,22 @@ fn test_view_dimension_compilation() { assert!(dimension.is_view(), "Should be a view member"); // Check that it's a reference (view members reference original cube members) - assert!(dimension.is_reference(), "Should be a reference to original member"); + assert!( + dimension.is_reference(), + "Should be a reference to original member" + ); // Resolve reference chain to get the original member let resolved = id_symbol.clone().resolve_reference_chain(); - assert_eq!(resolved.full_name(), "visitors.id", "Should resolve to visitors.id"); - assert!(!resolved.as_dimension().unwrap().is_view(), "Resolved member should not be a view"); + assert_eq!( + resolved.full_name(), + "visitors.id", + "Should resolve to visitors.id" + ); + assert!( + !resolved.as_dimension().unwrap().is_view(), + "Resolved member should not be a view" + ); // Compile dimension from view with long join path let visitor_id_symbol = test_compiler @@ -465,7 +475,10 @@ fn test_view_dimension_compilation() { .unwrap(); assert!(visitor_id_symbol.is_dimension()); - assert_eq!(visitor_id_symbol.full_name(), "visitors_visitors_checkins.visitor_id"); + assert_eq!( + visitor_id_symbol.full_name(), + "visitors_visitors_checkins.visitor_id" + ); let visitor_id_dim = visitor_id_symbol.as_dimension().unwrap(); assert!(visitor_id_dim.is_view(), "Should be a view member"); @@ -478,7 +491,10 @@ fn test_view_dimension_compilation() { "visitor_checkins.visitor_id", "Should resolve to visitor_checkins.visitor_id" ); - assert!(!resolved.as_dimension().unwrap().is_view(), "Resolved member should not be a view"); + assert!( + !resolved.as_dimension().unwrap().is_view(), + "Resolved member should not be a view" + ); } #[test] @@ -503,7 +519,10 @@ fn test_view_measure_compilation() { assert!(measure.is_view(), "Should be a view member"); // Check that it's a reference - assert!(measure.is_reference(), "Should be a reference to original member"); + assert!( + measure.is_reference(), + "Should be a reference to original member" + ); // Resolve reference chain to get the original member let resolved = count_symbol.clone().resolve_reference_chain(); @@ -512,7 +531,10 @@ fn test_view_measure_compilation() { "visitor_checkins.count", "Should resolve to visitor_checkins.count" ); - assert!(!resolved.as_measure().unwrap().is_view(), "Resolved member should not be a view"); + assert!( + !resolved.as_measure().unwrap().is_view(), + "Resolved member should not be a view" + ); } #[test] @@ -538,7 +560,10 @@ fn test_proxy_dimension_compilation() { assert!(!dimension.is_view(), "Proxy should not be a view member"); // Check that it IS a reference (proxy references another member) - assert!(dimension.is_reference(), "Proxy should be a reference to another member"); + assert!( + dimension.is_reference(), + "Proxy should be a reference to another member" + ); // Resolve reference chain to get the target member let resolved = proxy_symbol.clone().resolve_reference_chain(); @@ -549,10 +574,16 @@ fn test_proxy_dimension_compilation() { ); // Verify the resolved member is not a view - assert!(!resolved.as_dimension().unwrap().is_view(), "Target member should not be a view"); + assert!( + !resolved.as_dimension().unwrap().is_view(), + "Target member should not be a view" + ); // Verify the resolved member is also not a reference (it's the actual dimension) - assert!(!resolved.as_dimension().unwrap().is_reference(), "Target member should not be a reference"); + assert!( + !resolved.as_dimension().unwrap().is_reference(), + "Target member should not be a reference" + ); } #[test] @@ -578,7 +609,10 @@ fn test_proxy_measure_compilation() { assert!(!measure.is_view(), "Proxy should not be a view member"); // Check that it IS a reference (proxy references another member) - assert!(measure.is_reference(), "Proxy should be a reference to another member"); + assert!( + measure.is_reference(), + "Proxy should be a reference to another member" + ); // Resolve reference chain to get the target member let resolved = proxy_symbol.clone().resolve_reference_chain(); @@ -589,10 +623,16 @@ fn test_proxy_measure_compilation() { ); // Verify the resolved member is not a view - assert!(!resolved.as_measure().unwrap().is_view(), "Target member should not be a view"); + assert!( + !resolved.as_measure().unwrap().is_view(), + "Target member should not be a view" + ); // Verify the resolved member is not a reference (it's the actual measure) - assert!(!resolved.as_measure().unwrap().is_reference(), "Target member should not be a reference"); + assert!( + !resolved.as_measure().unwrap().is_reference(), + "Target member should not be a reference" + ); } #[test] @@ -607,7 +647,10 @@ fn test_time_dimension_with_granularity_compilation() { .unwrap(); // Check that it's a time dimension, not a regular dimension - assert!(time_symbol.as_time_dimension().is_ok(), "Should be a time dimension"); + assert!( + time_symbol.as_time_dimension().is_ok(), + "Should be a time dimension" + ); // Check full name includes granularity assert_eq!( @@ -629,11 +672,17 @@ fn test_time_dimension_with_granularity_compilation() { ); // Check that it's NOT a reference - assert!(!time_dim.is_reference(), "Time dimension with granularity should not be a reference"); + assert!( + !time_dim.is_reference(), + "Time dimension with granularity should not be a reference" + ); // Check base symbol - should be the original dimension without granularity let base_symbol = time_dim.base_symbol(); - assert!(base_symbol.is_dimension(), "Base symbol should be a dimension"); + assert!( + base_symbol.is_dimension(), + "Base symbol should be a dimension" + ); assert_eq!( base_symbol.full_name(), "visitors.created_at", diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/mod.rs index f9492b9d33be8..555e0e4be93d1 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/mod.rs @@ -1,3 +1,2 @@ mod compilation; mod symbol_evaluator; - diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/mod.rs index 507bada3fdeb8..7e795c57c65e0 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/mod.rs @@ -1 +1 @@ -mod cube_evaluator; \ No newline at end of file +mod cube_evaluator; From 6238ea8ba515c07f7f7df65b67b5c4aa8ce5b411 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Fri, 14 Nov 2025 21:27:00 +0100 Subject: [PATCH 40/42] fix --- .../src/test_fixtures/cube_bridge/mock_schema.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs index 665e41168de97..177971315c2ce 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_schema.rs @@ -76,6 +76,9 @@ impl MockSchema { } } + // Sort primary keys by name to ensure stable ordering + pk_dimensions.sort(); + if !pk_dimensions.is_empty() { primary_keys.insert(cube_name.clone(), pk_dimensions); } From a6487a77ea6d800e3282296f2a35cdb488ffd421 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Fri, 14 Nov 2025 22:27:24 +0100 Subject: [PATCH 41/42] fix --- .../src/test_fixtures/cube_bridge/mock_evaluator.rs | 4 +--- .../src/test_fixtures/cube_bridge/mock_member_sql.rs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs index d8c552e1c4a3b..ea72d620e5a92 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs @@ -239,9 +239,7 @@ impl CubeEvaluator for MockCubeEvaluator { let granularity = &path[3]; // Validate granularity is one of the supported ones - let valid_granularities = vec![ - "second", "minute", "hour", "day", "week", "month", "quarter", "year", - ]; + let valid_granularities = ["second", "minute", "hour", "day", "week", "month", "quarter", "year"]; if !valid_granularities.contains(&granularity.as_str()) { return Err(CubeError::user(format!( diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_sql.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_sql.rs index 8b4d9e4f3cce3..3707f98938d5a 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_sql.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_member_sql.rs @@ -51,7 +51,7 @@ impl MockMemberSql { let mut path = String::new(); let mut found_closing = false; - while let Some(ch) = chars.next() { + for ch in chars.by_ref() { if ch == '}' { found_closing = true; break; From f185c68a6afc70c6f7fdf584b57a4630e3ad8032 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Fri, 14 Nov 2025 22:29:42 +0100 Subject: [PATCH 42/42] fix --- .../src/test_fixtures/cube_bridge/mock_evaluator.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs index ea72d620e5a92..a731d54998bec 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs @@ -239,7 +239,9 @@ impl CubeEvaluator for MockCubeEvaluator { let granularity = &path[3]; // Validate granularity is one of the supported ones - let valid_granularities = ["second", "minute", "hour", "day", "week", "month", "quarter", "year"]; + let valid_granularities = [ + "second", "minute", "hour", "day", "week", "month", "quarter", "year", + ]; if !valid_granularities.contains(&granularity.as_str()) { return Err(CubeError::user(format!(