diff --git a/rust/cubesqlplanner/Cargo.lock b/rust/cubesqlplanner/Cargo.lock index afb1181ac98d5..b32b048a4c5c7 100644 --- a/rust/cubesqlplanner/Cargo.lock +++ b/rust/cubesqlplanner/Cargo.lock @@ -314,9 +314,9 @@ dependencies = [ [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encode_unicode" diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs index e9f23022f765a..8a0bfa5dcc4dd 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/compiler.rs @@ -154,6 +154,7 @@ impl Compiler { factory: T, ) -> Result, CubeError> { let node = factory.build(self)?; + node.validate()?; let key = (T::symbol_name().to_string(), full_name.clone()); if T::is_cachable() { self.members.insert(key, node.clone()); diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call.rs index 99b998ed8fb90..574192490272d 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call.rs @@ -3,13 +3,13 @@ use super::{symbols::MemberSymbol, SqlEvaluatorVisitor}; use crate::cube_bridge::member_sql::{FilterParamsColumn, SecutityContextProps, SqlTemplate}; use crate::planner::query_tools::QueryTools; use crate::planner::sql_evaluator::sql_nodes::SqlNodesFactory; +use crate::planner::sql_evaluator::CubeNameSymbol; use crate::planner::sql_templates::PlanSqlTemplates; use crate::planner::VisitorContext; use cubenativeutils::CubeError; use itertools::Itertools; use std::collections::HashMap; use std::rc::Rc; -use typed_builder::TypedBuilder; pub struct SqlCallArg; @@ -50,7 +50,7 @@ pub struct SqlCallFilterGroupItem { pub filter_params: Vec, } -#[derive(Clone, TypedBuilder, Debug)] +#[derive(Clone, Debug)] pub struct SqlCall { template: SqlTemplate, deps: Vec, @@ -60,6 +60,22 @@ pub struct SqlCall { } impl SqlCall { + pub(super) fn new( + template: SqlTemplate, + deps: Vec, + filter_params: Vec, + filter_groups: Vec, + security_context: SecutityContextProps, + ) -> Self { + Self { + template, + deps, + filter_params, + filter_groups, + security_context, + } + } + pub fn eval( &self, visitor: &SqlEvaluatorVisitor, @@ -71,7 +87,6 @@ impl SqlCall { let (filter_params, filter_groups, deps, context_values) = self.prepare_template_params(visitor, node_processor, &query_tools, templates)?; - // Substitute placeholders in template in a single pass Self::substitute_template( template, &deps, @@ -122,6 +137,27 @@ impl SqlCall { Ok(result) } + pub fn is_owned_by_cube(&self) -> bool { + if self.deps.is_empty() { + true + } else { + self.deps.iter().any(|dep| dep.symbol.is_cube()) + } + } + + pub fn cube_name_deps(&self) -> Vec> { + self.deps + .iter() + .filter_map(|dep| { + if let Ok(cube) = dep.symbol.as_cube_name() { + Some(cube.clone()) + } else { + None + } + }) + .collect() + } + fn prepare_template_params( &self, visitor: &SqlEvaluatorVisitor, 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 aea777a0a3220..ff1ae4a8dcb89 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 @@ -58,13 +58,13 @@ impl<'a> SqlCallBuilder<'a> { .map(|itm| self.build_filter_group_item(itm)) .collect::, _>>()?; - let result = SqlCall::builder() - .template(template.clone()) - .deps(deps) - .filter_params(filter_params) - .filter_groups(filter_groups) - .security_context(template_args.security_context.clone()) - .build(); + let result = SqlCall::new( + template.clone(), + deps, + filter_params, + filter_groups, + template_args.security_context.clone(), + ); Ok(result) } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/case.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/case.rs index 0542e3a259d4b..9123e7612e0fd 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/case.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/case.rs @@ -52,7 +52,7 @@ impl CaseDefinition { } } - pub fn apply_to_deps) -> Result, CubeError>>( + fn apply_to_deps) -> Result, CubeError>>( &self, f: &F, ) -> Result { @@ -77,6 +77,18 @@ impl CaseDefinition { let res = CaseDefinition { items, else_label }; Ok(res) } + + fn iter_sql_calls(&self) -> Box> + '_> { + Box::new(self.items.iter().map(|item| &item.sql)) + } + + fn is_owned_by_cube(&self) -> bool { + let mut owned = false; + for itm in self.items.iter() { + owned |= itm.sql.is_owned_by_cube(); + } + owned + } } #[derive(Clone)] @@ -106,7 +118,7 @@ impl CaseSwitchItem { } } - pub fn apply_to_deps) -> Result, CubeError>>( + fn apply_to_deps) -> Result, CubeError>>( &self, f: &F, ) -> Result { @@ -116,6 +128,13 @@ impl CaseSwitchItem { }; Ok(res) } + + fn iter_sql_calls(&self) -> Box> + '_> { + match self { + CaseSwitchItem::Sql(sql_call) => Box::new(std::iter::once(sql_call)), + CaseSwitchItem::Member(_) => Box::new(std::iter::empty()), + } + } } #[derive(Clone)] @@ -164,6 +183,32 @@ impl CaseSwitchDefinition { } values_len == 1 } + + fn iter_sql_calls(&self) -> Box> + '_> { + let result = self + .switch + .iter_sql_calls() + .chain(self.items.iter().map(|item| &item.sql)); + if let Some(else_sql) = &self.else_sql { + Box::new(result.chain(std::iter::once(else_sql))) + } else { + Box::new(result) + } + } + fn is_owned_by_cube(&self) -> bool { + let mut owned = false; + if let CaseSwitchItem::Sql(sql) = &self.switch { + owned |= sql.is_owned_by_cube(); + } + for itm in self.items.iter() { + owned |= itm.sql.is_owned_by_cube(); + } + if let Some(sql) = &self.else_sql { + owned |= sql.is_owned_by_cube(); + } + owned + } + fn extract_symbol_deps(&self, result: &mut Vec>) { self.switch.extract_symbol_deps(result); for itm in self.items.iter() { @@ -370,6 +415,19 @@ impl Case { Case::CaseSwitch(case) => case.is_single_value(), } } + + pub fn iter_sql_calls(&self) -> Box> + '_> { + match self { + Case::Case(case) => Box::new(case.iter_sql_calls()), + Case::CaseSwitch(case) => Box::new(case.iter_sql_calls()), + } + } + pub fn is_owned_by_cube(&self) -> bool { + match self { + Case::Case(case) => case.is_owned_by_cube(), + Case::CaseSwitch(case) => case.is_owned_by_cube(), + } + } } impl crate::utils::debug::DebugSql for Case { 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 aab65a70e775e..d7a49389fc4f1 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 @@ -227,6 +227,16 @@ impl DimensionSymbol { Ok(MemberSymbol::new_dimension(Rc::new(result))) } + pub fn iter_sql_calls(&self) -> Box> + '_> { + let result = self + .member_sql + .iter() + .chain(self.latitude.iter()) + .chain(self.longitude.iter()) + .chain(self.case.iter().flat_map(|case| case.iter_sql_calls())); + Box::new(result) + } + pub fn get_dependencies(&self) -> Vec> { let mut deps = vec![]; if let Some(member_sql) = &self.member_sql { @@ -517,13 +527,27 @@ impl SymbolFactory for DimensionSymbolFactory { None }; + let is_sub_query = definition.static_data().sub_query.unwrap_or(false); let is_multi_stage = definition.static_data().multi_stage.unwrap_or(false); - //TODO move owned logic to rust - let owned_by_cube = definition.static_data().owned_by_cube.unwrap_or(true); - let owned_by_cube = - owned_by_cube && !is_multi_stage && definition.static_data().dimension_type != "switch"; - let is_sub_query = definition.static_data().sub_query.unwrap_or(false); + let owned_by_cube = if is_multi_stage || dimension_type == "switch" { + false + } else { + let mut owned = false; + if let Some(sql) = &sql { + owned |= sql.is_owned_by_cube(); + } + if let Some(sql) = &latitude { + owned |= sql.is_owned_by_cube(); + } + if let Some(sql) = &longitude { + owned |= sql.is_owned_by_cube(); + } + if let Some(case) = &case { + owned |= case.is_owned_by_cube(); + } + owned + }; let is_reference = (is_view && is_sql_direct_ref) || (!owned_by_cube && !is_sub_query @@ -595,12 +619,10 @@ impl SymbolFactory for DimensionSymbolFactory { impl crate::utils::debug::DebugSql for DimensionSymbol { fn debug_sql(&self, expand_deps: bool) -> String { - // Handle case expressions if let Some(case) = &self.case { return case.debug_sql(expand_deps); } - // Handle geo dimensions (latitude/longitude pair) if self.dimension_type == "geo" { let lat = self .latitude @@ -615,12 +637,10 @@ impl crate::utils::debug::DebugSql for DimensionSymbol { return format!("GEO({}, {})", lat, lon); } - // Handle switch type dimensions without SQL if self.dimension_type == "switch" && self.member_sql.is_none() { return format!("SWITCH({})", self.full_name()); } - // Standard dimension SQL let res = if let Some(sql) = &self.member_sql { sql.debug_sql(expand_deps) } else { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs index 4781ba76acc8c..5530b3d082964 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs @@ -358,6 +358,16 @@ impl MeasureSymbol { Ok(MemberSymbol::new_measure(Rc::new(result))) } + pub fn iter_sql_calls(&self) -> Box> + '_> { + //FIXME We don't include filters and order_by here for backward compatibility + // because BaseQuery doesn't validate these SQL calls + let result = self + .member_sql + .iter() + .chain(self.case.iter().flat_map(|case| case.iter_sql_calls())); + Box::new(result) + } + pub fn get_dependencies(&self) -> Vec> { let mut deps = vec![]; if let Some(member_sql) = &self.member_sql { @@ -746,14 +756,32 @@ impl SymbolFactory for MeasureSymbolFactory { None }; - let is_calculated = - MeasureSymbol::is_calculated_type(&definition.static_data().measure_type) - && !definition.static_data().multi_stage.unwrap_or(false); + let measure_type = &definition.static_data().measure_type; + let is_calculated = MeasureSymbol::is_calculated_type(&measure_type) + && !definition.static_data().multi_stage.unwrap_or(false); let is_multi_stage = definition.static_data().multi_stage.unwrap_or(false); - //TODO move owned logic to rust - let owned_by_cube = definition.static_data().owned_by_cube.unwrap_or(true); - let owned_by_cube = owned_by_cube && !is_multi_stage; + let owned_by_cube = if is_multi_stage { + false + } else if measure_type == "count" && sql.is_none() { + true + } else { + let mut owned = false; + if let Some(sql) = &sql { + owned |= sql.is_owned_by_cube(); + } + for sql in &measure_filters { + owned |= sql.is_owned_by_cube(); + } + for sql in &measure_drill_filters { + owned |= sql.is_owned_by_cube(); + } + if let Some(case) = &case { + owned |= case.is_owned_by_cube(); + } + owned + }; + let cube = cube_evaluator.cube_from_path(cube_name.clone())?; let alias = PlanSqlTemplates::memeber_alias_name(cube.static_data().resolved_alias(), &name, &None); diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/member_symbol.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/member_symbol.rs index 6190462fc6ba2..05e2c037f8be3 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/member_symbol.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/member_symbol.rs @@ -1,7 +1,8 @@ use cubenativeutils::CubeError; +use itertools::Itertools; use crate::planner::sql_evaluator::collectors::has_multi_stage_members; -use crate::planner::sql_evaluator::Case; +use crate::planner::sql_evaluator::{Case, SqlCall}; use super::{ CubeNameSymbol, CubeTableSymbol, DimensionSymbol, MeasureSymbol, MemberExpressionSymbol, @@ -295,6 +296,16 @@ impl MemberSymbol { } } + pub fn as_cube_name(&self) -> Result, CubeError> { + match self { + Self::CubeName(c) => Ok(c.clone()), + _ => Err(CubeError::internal(format!( + "{} is not a cube name", + self.full_name() + ))), + } + } + pub fn as_member_expression(&self) -> Result, CubeError> { match self { Self::MemberExpression(m) => Ok(m.clone()), @@ -323,6 +334,65 @@ impl MemberSymbol { pub fn is_leaf(&self) -> bool { self.get_dependencies().is_empty() } + + pub fn validate(&self) -> Result<(), CubeError> { + self.validate_cube_refs() + } + fn validate_cube_refs(&self) -> Result<(), CubeError> { + let sql_calls = match self { + Self::Dimension(dim) => dim.iter_sql_calls(), + Self::Measure(meas) => meas.iter_sql_calls(), + _ => Box::new(std::iter::empty()), + }; + if self.is_multi_stage() { + for call in sql_calls { + self.validate_multi_stage_cube_refs(call)?; + } + } else { + for call in sql_calls { + self.validate_regular_member_cube_refs(call)?; + } + } + Ok(()) + } + fn validate_multi_stage_cube_refs(&self, sql_call: &Rc) -> Result<(), CubeError> { + let sql_cube_deps = sql_call.cube_name_deps(); + if !sql_cube_deps.is_empty() { + Err(CubeError::user(format!( + "Multi stage member '{}' references cubes {}. Multi stage members can only reference other members.", + self.full_name(), sql_cube_deps.iter().map(|dep| dep.cube_name()).join(", ") + ))) + } else if sql_call.dependencies_count() == 0 { + Err(CubeError::user(format!( + "Multi stage member '{}' don't reference other members.", + self.full_name() + ))) + } else { + Ok(()) + } + } + fn validate_regular_member_cube_refs(&self, sql_call: &Rc) -> Result<(), CubeError> { + let cube_name = self.cube_name(); + let sql_cube_deps = sql_call.cube_name_deps(); + if sql_cube_deps + .iter() + .any(|dep| dep.cube_name() != &cube_name) + { + Err(CubeError::user(format!( + "Member '{}' references foreign cubes: {}. Please split and move this definition to corresponding cubes.", + self.full_name(), sql_cube_deps.iter().filter_map(|dep| + if dep.cube_name() != &cube_name { + Some(dep.cube_name()) + } else { + None + } + + ).join(", ") + ))) + } else { + Ok(()) + } + } } impl crate::utils::debug::DebugSql for MemberSymbol { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/compilation_tests/ownership_test.yaml b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/compilation_tests/ownership_test.yaml new file mode 100644 index 0000000000000..806496d8fa065 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/compilation_tests/ownership_test.yaml @@ -0,0 +1,215 @@ +cubes: + - name: users + sql: "SELECT 1" + joins: + - name: orders + relationship: one_to_many + sql: "{users}.id = {orders.user_id}" + dimensions: + - name: id + type: number + sql: id + primary_key: true + + - name: userName + type: string + sql: "{CUBE}.user_name" + + - name: userType + type: string + sql: "{CUBE}.user_type" + + - name: userNameProxy + type: string + sql: "{CUBE.userName}" + + - name: secondId + type: number + sql: "{CUBE}.secondId" + + - name: complexId + type: number + sql: "{id} % {users.secondId}" + + - name: ownedCase + type: string + case: + when: + - sql: "{CUBE}.user_type = 'admin'" + label: "Admin" + - sql: "{CUBE}.user_type = 'user'" + label: "User" + else: + label: "Unknown" + + - name: anotherOwnedCase + type: string + case: + when: + - sql: "{CUBE}.user_type = 'admin'" + label: "Admin" + - sql: "{orders.orderType} = 'user'" + label: "User" + else: + label: "Unknown" + + - name: notOwnedCase + type: string + case: + when: + - sql: "{CUBE.userType} = 'admin'" + label: "Admin" + - sql: "{orders.orderType} = 'user'" + label: "User" + else: + label: "Unknown" + + - name: notOwnedCaseOtherCube + type: string + case: + when: + - sql: "{orders.orderType} = 'user'" + label: "User" + else: + label: "Unknown" + + - name: latitude + type: number + sql: "{CUBE}.latitude" + + - name: longitude + type: number + sql: "{CUBE}.longitude" + + - name: ownedGeo + type: geo + latitude: latitude + longitude: longitude + + - name: anotherOwnedGeo + type: geo + latitude: "{CUBE.latitude}" + longitude: "{CUBE}.longitude" + + - name: notOwnedGeo + type: geo + latitude: "{CUBE.latitude}" + longitude: "{CUBE.longitude}" + + - name: notOwnedGeoTypeOtherCube + type: geo + latitude: "{CUBE.latitude}" + longitude: "{CUBE.longitude}" + + measures: + - name: count + type: count + + - name: amount + type: sum + sql: "amount" + + - name: minPayment + type: min + sql: "{users}.payment" + + - name: proxyAmount + type: number + sql: "{CUBE.amount}" + + - name: complexCalculation + type: number + sql: "{CUBE.amount} / {CUBE.minPayment}" + + - name: complexCalculationOtherCube + type: number + sql: "{orders.revenue} / {orders.amount}" + + - name: multiStage + type: number + sql: "{CUBE.amount} / {CUBE.minPayment}" + multi_stage: true + + - name: ownedFilter + type: number + sql: "{CUBE.amount} / {CUBE.minPayment}" + filters: + - sql: "{CUBE}.amount > 100" + + - name: otherOwnedFilter + type: number + sql: "{orders.revenue}" + filters: + - sql: "{CUBE}.amount > 100" + + - name: notOwnedFilter + type: number + sql: "{CUBE.amount} / {CUBE.minPayment}" + filters: + - sql: "{CUBE.amount} > 100" + + - name: notOwnedFilterOtherCube + type: number + sql: "{orders.revenue}" + filters: + - sql: "{orders.count} > 100" + + - name: ownedDrillFilter + type: number + sql: "{CUBE.amount} / {CUBE.minPayment}" + drill_filters: + - sql: "{CUBE}.amount > 100" + + - name: otherOwnedDrillFilter + type: number + sql: "{orders.revenue}" + drill_filters: + - sql: "{CUBE}.amount > 100" + + - name: notOwnedDrillFilter + type: number + sql: "{CUBE.amount} / {CUBE.minPayment}" + drill_filters: + - sql: "{CUBE.amount} > 100" + + - name: notOwnedDrillFilterOtherCube + type: number + sql: "{orders.revenue}" + drill_filters: + - sql: "{orders.count} > 100" + + - name: orders + sql: "SELECT 1" + dimensions: + - name: id + type: number + sql: id + primary_key: true + - name: orderUserName + type: string + sql: "{users.userName}" + - name: orderType + type: string + sql: "{CUBE}.order_type" + - name: latitude + type: number + sql: "{CUBE}.latitude" + - name: longitude + type: number + sql: "{CUBE}.longitude" + measures: + - name: count + type: count + + - name: revenue + type: sum + sql: "{CUBE}.revenue" +views: + - name: users_to_orders + cubes: + - join_path: users + includes: "*" + - join_path: users.orders + includes: + - orderUserName + - orderType diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/compilation_tests/wrong_cube_refs_test.yaml b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/compilation_tests/wrong_cube_refs_test.yaml new file mode 100644 index 0000000000000..69c700870683c --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/compilation_tests/wrong_cube_refs_test.yaml @@ -0,0 +1,300 @@ +cubes: + - name: users + sql: "SELECT 1" + joins: + - name: orders + relationship: one_to_many + sql: "{users}.id = {orders.user_id}" + dimensions: + - name: id + type: number + sql: id + primary_key: true + + # Regular dimensions refers to foreign cubes + - name: otherCubeRefInSql + type: string + sql: "{orders}.user_name" + + - name: otherCubeRefInSql2 + type: string + sql: "{CUBE}.val + {orders}.user_name" + + - name: otherCubeRefInLongitude + type: geo + longitude: "{orders}.longitude" + latitude: "{CUBE}.latitude" + + - name: otherCubeRefInLatitude + type: geo + longitude: "{CUBE}.longitude" + latitude: "{orders}.latitude" + + - name: otherCubeRefInCaseItem + type: string + case: + when: + - sql: "{CUBE}.id = 1" + label: a + - sql: "{orders}.id = 1" + label: b + else: + label: c + + - name: otherCubeRefInCaseSwitchSwitch + type: string + case: + switch: "{orders}.state" + when: + - value: "a" + sql: "{CUBE}.a" + - value: "b" + sql: "{CUBE}.b" + else: + sql: "{CUBE}.c" + + - name: otherCubeRefInCaseSwitchItem + type: string + case: + switch: "{CUBE}.state" + when: + - value: "a" + sql: "{orders}.a" + - value: "b" + sql: "{CUBE}.b" + else: + sql: "{CUBE}.c" + + - name: otherCubeRefInCaseSwitchElse + type: string + case: + switch: "{CUBE}.state" + when: + - value: "a" + sql: "{CUBE}.a" + - value: "b" + sql: "{CUBE}.b" + else: + sql: "{orders}.c" + + # MultiStage dimensions refers any cubes + - name: multiStageCubeRefInSql + type: string + multi_stage: true + sql: "{CUBE}.id" + + - name: multiStageOtherCubeRefInSql + type: string + multi_stage: true + sql: "{orders}.id" + + - name: multiStageCubeRefInLongitude + type: geo + longitude: "{CUBE}.longitude" + latitude: "{CUBE.id}" + multi_stage: true + + - name: multiStageCubeRefInLatitude + type: geo + longitude: "{CUBE.id}" + latitude: "{CUBE}.latitude" + multi_stage: true + + - name: multiStageCubeRefInCaseItem + type: string + multi_stage: true + case: + when: + - sql: "{CUBE}.id = 1" + label: a + - sql: "{orders}.id = 1" + label: b + else: + label: c + + - name: multiStageCubeRefInCaseSwitchSwitch + type: string + multi_stage: true + case: + switch: "{CUBE}.state" + when: + - value: "a" + sql: "{CUBE.id}" + - value: "b" + sql: "{CUBE.id}" + else: + sql: "{orders.id}" + + - name: multiStageCubeRefInCaseSwitchItem + type: string + multi_stage: true + case: + switch: "{CUBE.id}" + when: + - value: "a" + sql: "{CUBE}.a" + - value: "b" + sql: "{CUBE.id}" + else: + sql: "{orders.id}" + + - name: multiStageCubeRefInCaseSwitchElse + type: string + multi_stage: true + case: + switch: "{CUBE.id}" + when: + - value: "a" + sql: "{CUBE.id}" + - value: "b" + sql: "{orders.id}" + else: + sql: "{CUBE}.c" + + # MultiStage dimensions withiout references to other members + - name: multiStageWithoutRefInSql + type: string + multi_stage: true + sql: "id" + + - name: multiStageWithoutRefInLongitude + type: geo + longitude: "longitude" + latitude: "{CUBE.id}" + multi_stage: true + + - name: multiStageWithoutRefInLatitude + type: geo + longitude: "{CUBE.id}" + latitude: "latitude" + multi_stage: true + + - name: multiStageWithoutRefInCaseItem + type: string + multi_stage: true + case: + when: + - sql: "{CUBE.id} = 1" + label: a + - sql: "id = 1" + label: b + else: + label: c + + - name: multiStageWithoutRefInCaseSwitchSwitch + type: string + multi_stage: true + case: + switch: "state" + when: + - value: "a" + sql: "{CUBE.id}" + - value: "b" + sql: "{CUBE.id}" + else: + sql: "{orders.id}" + + - name: multiStageWithoutRefInCaseSwitchItem + type: string + multi_stage: true + case: + switch: "{CUBE.id}" + when: + - value: "a" + sql: "a" + - value: "b" + sql: "{CUBE.id}" + else: + sql: "{orders.id}" + + - name: multiStageWithoutRefInCaseSwitchElse + type: string + multi_stage: true + case: + switch: "{CUBE.id}" + when: + - value: "a" + sql: "{CUBE.id}" + - value: "b" + sql: "{orders.id}" + else: + sql: "c" + + measures: + - name: count + type: count + + - name: mOtherCubeRefInSql + type: string + sql: "{orders}.count" + + - name: mMultiStageCubeRefInSql + type: string + multi_stage: true + sql: "{CUBE}.count" + + - name: mMultiStageWithoutRefInCaseSwitchSwitch + type: string + multi_stage: true + case: + switch: "state" + when: + - value: "a" + sql: "{CUBE.count}" + - value: "b" + sql: "{CUBE.count}" + else: + sql: "{orders.count}" + + - name: mMultiStageWithoutRefInCaseSwitchItem + type: string + multi_stage: true + case: + switch: "{CUBE.id}" + when: + - value: "a" + sql: "a" + - value: "b" + sql: "{CUBE.count}" + else: + sql: "{orders.count}" + + - name: mMultiStageWithoutRefInCaseSwitchElse + type: string + multi_stage: true + case: + switch: "{CUBE.count}" + when: + - value: "a" + sql: "{CUBE.count}" + - value: "b" + sql: "{orders.count}" + else: + sql: "c" + + - name: orders + sql: "SELECT 1" + dimensions: + - name: id + type: number + sql: id + primary_key: true + - name: orderUserName + type: string + sql: "{users.userName}" + - name: orderType + type: string + sql: "{CUBE}.order_type" + - name: latitude + type: number + sql: "{CUBE}.latitude" + - name: longitude + type: number + sql: "{CUBE}.longitude" + measures: + - name: count + type: count + + - name: revenue + type: sum + sql: "{CUBE}.revenue" diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/symbol_evaluator/count_no_pk.yaml b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/symbol_evaluator/count_no_pk.yaml new file mode 100644 index 0000000000000..ac678cd6d6428 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/symbol_evaluator/count_no_pk.yaml @@ -0,0 +1,13 @@ +cubes: + - name: users + sql: "SELECT 1" + dimensions: + - name: id + type: number + sql: id + - name: userName + type: string + sql: user_name + measures: + - name: count + type: count diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/symbol_evaluator/count_one_pk.yaml b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/symbol_evaluator/count_one_pk.yaml new file mode 100644 index 0000000000000..bd47f34286ca9 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/symbol_evaluator/count_one_pk.yaml @@ -0,0 +1,14 @@ +cubes: + - name: users + sql: "SELECT 1" + dimensions: + - name: id + type: number + sql: id + primary_key: true + - name: userName + type: string + sql: user_name + measures: + - name: count + type: count diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/symbol_evaluator/count_two_pk.yaml b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/symbol_evaluator/count_two_pk.yaml new file mode 100644 index 0000000000000..92f72344dc1ee --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/symbol_evaluator/count_two_pk.yaml @@ -0,0 +1,15 @@ +cubes: + - name: users + sql: "SELECT 1" + dimensions: + - name: id + type: number + sql: id + primary_key: true + - name: userName + type: string + sql: user_name + primary_key: true + measures: + - name: count + type: count diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/symbol_evaluator/test_cube.yaml b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/symbol_evaluator/test_cube.yaml new file mode 100644 index 0000000000000..d57aee3971476 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/symbol_evaluator/test_cube.yaml @@ -0,0 +1,43 @@ +cubes: + - name: test_cube + sql: "SELECT 1" + dimensions: + - name: id + type: number + sql: id + primary_key: true + - name: source + type: string + sql: "{CUBE}.source" + - name: source_extended + type: string + sql: "CONCAT({CUBE.source}, '_source')" + - name: created_at + type: time + sql: created_at + - name: location + type: geo + latitude: latitude + longitude: longitude + measures: + - name: sum_revenue + type: sum + sql: revenue + - name: min_revenue + type: min + sql: revenue + - name: max_revenue + type: max + sql: revenue + - name: avg_revenue + type: avg + sql: revenue + - name: complex_measure + type: number + sql: "{sum_revenue} + {CUBE.avg_revenue}/{test_cube.min_revenue} - {test_cube.min_revenue}" + - name: count_distinct_id + type: countDistinct + sql: id + - name: count_distinct_approx_id + type: countDistinctApprox + sql: id diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs index f0cb8b507f209..dadad8e0ad8b1 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/compilation.rs @@ -1,6 +1,8 @@ //! Tests for Compiler member evaluation -use crate::test_fixtures::{cube_bridge::MockSchema, schemas::TestCompiler}; +use crate::test_fixtures::cube_bridge::MockSchema; +use crate::test_fixtures::schemas::TestCompiler; +use crate::test_fixtures::test_utils::TestContext; #[test] fn test_add_dimension_evaluator_number_dimension() { @@ -279,15 +281,12 @@ 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 schema = MockSchema::from_yaml_file("common/visitors.yaml"); let evaluator = 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()) @@ -298,7 +297,6 @@ fn test_dimension_with_cube_table_dependency() { 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"); @@ -314,7 +312,6 @@ fn test_dimension_with_member_dependency_no_prefix() { let evaluator = 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()) @@ -325,7 +322,6 @@ fn test_dimension_with_member_dependency_no_prefix() { 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(), @@ -345,7 +341,6 @@ fn test_dimension_with_mixed_dependencies() { let evaluator = 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()) @@ -356,7 +351,6 @@ fn test_dimension_with_mixed_dependencies() { 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(), @@ -364,13 +358,11 @@ fn test_dimension_with_mixed_dependencies() { "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()), @@ -388,7 +380,6 @@ fn test_measure_with_cube_table_dependency() { let evaluator = 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()) @@ -399,7 +390,6 @@ fn test_measure_with_cube_table_dependency() { 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"); @@ -415,7 +405,6 @@ fn test_measure_with_explicit_cube_and_member_dependencies() { let evaluator = 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()) @@ -426,17 +415,14 @@ fn test_measure_with_explicit_cube_and_member_dependencies() { 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()), @@ -454,29 +440,24 @@ fn test_view_dimension_compilation() { let evaluator = 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(), @@ -488,7 +469,6 @@ fn test_view_dimension_compilation() { "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()) @@ -504,7 +484,6 @@ fn test_view_dimension_compilation() { 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(), @@ -523,29 +502,24 @@ fn test_view_measure_compilation() { let evaluator = 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(), @@ -564,13 +538,11 @@ fn test_proxy_dimension_compilation() { let evaluator = 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"); @@ -578,16 +550,13 @@ fn test_proxy_dimension_compilation() { 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(), @@ -595,13 +564,11 @@ fn test_proxy_dimension_compilation() { "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" @@ -614,13 +581,11 @@ fn test_proxy_measure_compilation() { let evaluator = 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"); @@ -632,13 +597,11 @@ 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" ); - // Resolve reference chain to get the target member let resolved = proxy_symbol.clone().resolve_reference_chain(); assert_eq!( resolved.full_name(), @@ -646,13 +609,11 @@ fn test_proxy_measure_compilation() { "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" @@ -665,19 +626,16 @@ fn test_time_dimension_with_granularity_compilation() { let evaluator = 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", @@ -686,23 +644,19 @@ fn test_time_dimension_with_granularity_compilation() { 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(), @@ -719,3 +673,149 @@ fn test_time_dimension_with_granularity_compilation() { "Base dimension should be time type" ); } + +#[test] +fn test_sql_deps_validation() { + let schema = MockSchema::from_yaml_file("common/visitors.yaml"); + let evaluator = schema.create_evaluator(); + let mut test_compiler = TestCompiler::new(evaluator); + + let time_symbol = test_compiler + .compiler + .add_dimension_evaluator("visitors.created_at.month".to_string()) + .unwrap(); + + assert!( + time_symbol.as_time_dimension().is_ok(), + "Should be a time dimension" + ); + + 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"); + + let time_dim = time_symbol.as_time_dimension().unwrap(); + + assert_eq!( + time_dim.granularity(), + &Some("month".to_string()), + "Granularity should be month" + ); + + assert!( + !time_dim.is_reference(), + "Time dimension with granularity should not be a reference" + ); + + 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" + ); +} + +#[test] +fn test_sql_regular_dimension_wrong_cube_ref() { + let schema = MockSchema::from_yaml_file("compilation_tests/wrong_cube_refs_test.yaml"); + let context = TestContext::new(schema).unwrap(); + let wron_dims = vec![ + "users.otherCubeRefInSql", + "users.otherCubeRefInSql2", + "users.otherCubeRefInLongitude", + "users.otherCubeRefInLatitude", + "users.otherCubeRefInCaseItem", + "users.otherCubeRefInCaseSwitchSwitch", + "users.otherCubeRefInCaseSwitchItem", + "users.otherCubeRefInCaseSwitchElse", + ]; + + for dim in wron_dims { + assert!( + context.create_dimension(dim).is_err(), + "Dimension {} should not compile", + dim + ); + } +} + +#[test] +fn test_sql_multi_stage_dimension_wrong_cube_ref() { + let schema = MockSchema::from_yaml_file("compilation_tests/wrong_cube_refs_test.yaml"); + let context = TestContext::new(schema).unwrap(); + let wron_dims = vec![ + "users.multiStageCubeRefInSql", + "users.multiStageOtherCubeRefInSql", + "users.multiStageCubeRefInLongitude", + "users.multiStageCubeRefInLatitude", + "users.multiStageCubeRefInCaseItem", + "users.multiStageCubeRefInCaseSwitchSwitch", + "users.multiStageCubeRefInCaseSwitchItem", + "users.multiStageCubeRefInCaseSwitchElse", + ]; + + for dim in wron_dims { + assert!( + context.create_dimension(dim).is_err(), + "Dimension {} should not compile", + dim + ); + } +} + +#[test] +fn test_sql_multi_stage_dimension_wihtout_member_ref() { + let schema = MockSchema::from_yaml_file("compilation_tests/wrong_cube_refs_test.yaml"); + let context = TestContext::new(schema).unwrap(); + let wron_dims = vec![ + "users.multiStageWithoutRefInSql", + "users.multiStageWithoutRefInLongitude", + "users.multiStageWithoutRefInLatitude", + "users.multiStageWithoutRefInCaseItem", + "users.multiStageWithoutRefInCaseSwitchSwitch", + "users.multiStageWithoutRefInCaseSwitchItem", + "users.multiStageWithoutRefInCaseSwitchElse", + ]; + + for dim in wron_dims { + assert!( + context.create_dimension(dim).is_err(), + "Dimension {} should not compile", + dim + ); + } +} + +#[test] +fn test_sql_multi_stage_measures() { + let schema = MockSchema::from_yaml_file("compilation_tests/wrong_cube_refs_test.yaml"); + let context = TestContext::new(schema).unwrap(); + let wron_dims = vec![ + "users.mOtherCubeRefInSql", + "users.mMultiStageCubeRefInSql", + "users.mMultiStageWithoutRefInCaseSwitchSwitch", + "users.mMultiStageWithoutRefInCaseSwitchItem", + "users.mMultiStageWithoutRefInCaseSwitchElse", + ]; + + for dim in wron_dims { + assert!( + context.create_dimension(dim).is_err(), + "Dimension {} should not compile", + dim + ); + } +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/mod.rs index 555e0e4be93d1..756337fe54785 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/mod.rs @@ -1,2 +1,3 @@ mod compilation; +mod owned_by_cube; mod symbol_evaluator; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/owned_by_cube.rs b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/owned_by_cube.rs new file mode 100644 index 0000000000000..8a64781523a62 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/owned_by_cube.rs @@ -0,0 +1,87 @@ +use crate::test_fixtures::cube_bridge::MockSchema; +use crate::test_fixtures::test_utils::TestContext; + +#[test] +fn dimensions_ownships() { + let schema = MockSchema::from_yaml_file("compilation_tests/ownership_test.yaml"); + let context = TestContext::new(schema).unwrap(); + + let owned_dims = vec![ + "users.id", + "users.userName", + "users.ownedCase", + "users.anotherOwnedCase", + "users.ownedGeo", + "users.anotherOwnedGeo", + "orders.orderType", + ]; + + for dim in owned_dims { + assert!( + context.create_dimension(dim).unwrap().owned_by_cube(), + "Dimension {} should be owned by cube", + dim + ); + } + + let not_owned_dims = vec![ + "users.userNameProxy", + "users.complexId", + "users.notOwnedCase", + "users.notOwnedCaseOtherCube", + "users.notOwnedGeo", + "users.notOwnedGeoTypeOtherCube", + "users_to_orders.id", + "users_to_orders.orderType", + ]; + + for dim in not_owned_dims { + assert!( + !context.create_dimension(dim).unwrap().owned_by_cube(), + "Dimension {} should not be owned by cube", + dim + ); + } +} + +#[test] +fn measures_ownships() { + let schema = MockSchema::from_yaml_file("compilation_tests/ownership_test.yaml"); + let context = TestContext::new(schema).unwrap(); + + let owned_measures = vec![ + "users.count", + "users.amount", + "users.minPayment", + "users.ownedFilter", + "users.otherOwnedFilter", + "users.ownedDrillFilter", + "users.otherOwnedDrillFilter", + ]; + + for meas in owned_measures { + assert!( + context.create_measure(meas).unwrap().owned_by_cube(), + "Measure {} should be owned by cube", + meas + ); + } + + let not_owned_measures = vec![ + "users.proxyAmount", + "users.complexCalculation", + "users.notOwnedFilter", + "users.notOwnedFilterOtherCube", + "users.notOwnedDrillFilter", + "users.notOwnedDrillFilterOtherCube", + "users_to_orders.count", + ]; + + for meas in not_owned_measures { + assert!( + !context.create_measure(meas).unwrap().owned_by_cube(), + "Measure {} should not be owned by cube", + meas + ); + } +} 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 5a47266f60f5a..4f075628df207 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/tests/cube_evaluator/symbol_evaluator.rs @@ -2,120 +2,10 @@ use crate::test_fixtures::cube_bridge::MockSchema; use crate::test_fixtures::test_utils::TestContext; -use indoc::indoc; - -fn create_count_schema_no_pk() -> MockSchema { - let yaml = indoc! {r#" - cubes: - - name: users - sql: "SELECT 1" - dimensions: - - name: id - type: number - sql: id - - name: userName - type: string - sql: user_name - measures: - - name: count - type: count - "#}; - MockSchema::from_yaml(yaml).unwrap() -} - -fn create_count_schema_one_pk() -> MockSchema { - let yaml = indoc! {r#" - cubes: - - name: users - sql: "SELECT 1" - dimensions: - - name: id - type: number - sql: id - primary_key: true - - name: userName - type: string - sql: user_name - measures: - - name: count - type: count - "#}; - MockSchema::from_yaml(yaml).unwrap() -} - -fn create_count_schema_two_pk() -> MockSchema { - let yaml = indoc! {r#" - cubes: - - name: users - sql: "SELECT 1" - dimensions: - - name: id - type: number - sql: id - primary_key: true - - name: userName - type: string - sql: user_name - primary_key: true - measures: - - name: count - type: count - "#}; - MockSchema::from_yaml(yaml).unwrap() -} - -fn create_test_schema() -> MockSchema { - let yaml = indoc! {r#" - cubes: - - name: test_cube - sql: "SELECT 1" - dimensions: - - name: id - type: number - sql: id - primary_key: true - - name: source - type: string - sql: "{CUBE}.source" - - name: source_extended - type: string - sql: "CONCAT({CUBE.source}, '_source')" - - name: created_at - type: time - sql: created_at - - name: location - type: geo - latitude: latitude - longitude: longitude - measures: - - name: sum_revenue - type: sum - sql: revenue - - name: min_revenue - type: min - sql: revenue - - name: max_revenue - type: max - sql: revenue - - name: avg_revenue - type: avg - sql: revenue - - name: complex_measure - type: number - sql: "{sum_revenue} + {CUBE.avg_revenue}/{test_cube.min_revenue} - {test_cube.min_revenue}" - - name: count_distinct_id - type: countDistinct - sql: id - - name: count_distinct_approx_id - type: countDistinctApprox - sql: id - "#}; - MockSchema::from_yaml(yaml).unwrap() -} #[test] fn simple_dimension_sql_evaluation() { - let schema = create_test_schema(); + let schema = MockSchema::from_yaml_file("symbol_evaluator/test_cube.yaml"); let context = TestContext::new(schema).unwrap(); let id_symbol = context.create_dimension("test_cube.id").unwrap(); @@ -146,7 +36,7 @@ fn simple_dimension_sql_evaluation() { #[test] fn simple_aggregate_measures() { - let schema = create_test_schema(); + let schema = MockSchema::from_yaml_file("symbol_evaluator/test_cube.yaml"); let context = TestContext::new(schema).unwrap(); let sum_symbol = context.create_measure("test_cube.sum_revenue").unwrap(); @@ -185,13 +75,13 @@ fn simple_aggregate_measures() { #[test] fn count_measure_variants() { - let schema_no_pk = create_count_schema_no_pk(); + let schema_no_pk = MockSchema::from_yaml_file("symbol_evaluator/count_no_pk.yaml"); let context_no_pk = TestContext::new(schema_no_pk).unwrap(); let count_no_pk_symbol = context_no_pk.create_measure("users.count").unwrap(); let count_no_pk_sql = context_no_pk.evaluate_symbol(&count_no_pk_symbol).unwrap(); assert_eq!(count_no_pk_sql, "count(*)"); - let schema_one_pk = create_count_schema_one_pk(); + let schema_one_pk = MockSchema::from_yaml_file("symbol_evaluator/count_one_pk.yaml"); let context_one_pk = TestContext::new(schema_one_pk).unwrap(); let count_one_pk_symbol = context_one_pk.create_measure("users.count").unwrap(); let count_one_pk_sql = context_one_pk @@ -199,7 +89,8 @@ fn count_measure_variants() { .unwrap(); assert_eq!(count_one_pk_sql, r#"count("users".id)"#); - let schema_two_pk = create_count_schema_two_pk(); + // Test COUNT with two primary keys - should use count(CAST(pk1) || CAST(pk2)) + let schema_two_pk = MockSchema::from_yaml_file("symbol_evaluator/count_two_pk.yaml"); let context_two_pk = TestContext::new(schema_two_pk).unwrap(); let count_two_pk_symbol = context_two_pk.create_measure("users.count").unwrap(); let count_two_pk_sql = context_two_pk @@ -213,7 +104,7 @@ fn count_measure_variants() { #[test] fn composite_symbols() { - let schema = create_test_schema(); + let schema = MockSchema::from_yaml_file("symbol_evaluator/test_cube.yaml"); let context = TestContext::new(schema).unwrap(); // Test dimension with member dependency ({CUBE.source})