diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/sql-generation.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/sql-generation.test.ts index 4d5d404c5b458..84bcbdca87323 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/sql-generation.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/sql-generation.test.ts @@ -1066,6 +1066,57 @@ SELECT 1 AS revenue, cast('2024-01-01' AS timestamp) as time UNION ALL } ])); + it('member to alias for granularized time dimension', async () => runQueryTest({ + measures: [ + 'visitors.visitor_revenue', + 'visitors.visitor_count', + 'visitors.per_visitor_revenue' + ], + dimensions: [ + 'visitors.source' + ], + timeDimensions: [{ + dimension: 'visitors.created_at', + dateRange: ['2017-01-01', '2017-01-30'], + granularity: 'month' + }], + timezone: 'America/Los_Angeles', + // SQL API keys a granularized override `{member}.{granularity}` (dotted) and + // references the CubeScan column by it. The native planner must honor it + // instead of defaulting to `{base alias}_{granularity}`. + memberToAlias: { + 'visitors.visitor_revenue': 'custom_revenue', + 'visitors.visitor_count': 'custom_count', + 'visitors.source': 'custom_source', + 'visitors.created_at.month': 'custom_created_at_month', + }, + order: [] + }, + + [ + { + custom_source: 'google', + custom_created_at_month: '2017-01-01T00:00:00.000Z', + custom_revenue: null, + custom_count: '1', + visitors__per_visitor_revenue: null + }, + { + custom_source: 'some', + custom_created_at_month: '2017-01-01T00:00:00.000Z', + custom_revenue: '300', + custom_count: '2', + visitors__per_visitor_revenue: '150' + }, + { + custom_source: null, + custom_created_at_month: '2017-01-01T00:00:00.000Z', + custom_revenue: null, + custom_count: '2', + visitors__per_visitor_revenue: null + } + ])); + it('running total', async () => { await compiler.compile(); diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties_compiler.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties_compiler.rs index 9e5d781d68007..c29daab5aa7a3 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties_compiler.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties_compiler.rs @@ -246,12 +246,26 @@ impl QueryPropertiesCompiler { } else { None }; - Ok(MemberSymbol::new_time_dimension(TimeDimensionSymbol::new( - base_symbol, - d.granularity.clone(), - granularity_obj, - date_range_tuple, - ))) + // Honor an explicit `memberToAlias` override for the granularized + // member. The SQL API (cubesql) keys it `{member}.{granularity}` + // (dotted) and references the CubeScan column by that alias; the + // default `{base alias}_{granularity}` would otherwise mismatch. + let alias_override = d.granularity.as_ref().and_then(|granularity| { + evaluator_compiler.alias_for_member(&format!( + "{}.{}", + base_symbol.full_name(), + granularity + )) + }); + Ok(MemberSymbol::new_time_dimension( + TimeDimensionSymbol::new_with_alias( + base_symbol, + d.granularity.clone(), + granularity_obj, + date_range_tuple, + alias_override, + ), + )) }) .collect() } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/time_dimension_symbol.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/time_dimension_symbol.rs index efdccc541eeee..599d585c43e88 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/time_dimension_symbol.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/time_dimension_symbol.rs @@ -30,6 +30,19 @@ impl TimeDimensionSymbol { granularity: Option, granularity_obj: Option, date_range: Option<(String, String)>, + ) -> Rc { + Self::new_with_alias(base_symbol, granularity, granularity_obj, date_range, None) + } + + /// Like [`Self::new`] but with an explicit alias override (e.g. the SQL + /// API's `memberToAlias` entry for the granularized member). When `None`, + /// the alias falls back to `{base alias}_{granularity}`. + pub fn new_with_alias( + base_symbol: Rc, + granularity: Option, + granularity_obj: Option, + date_range: Option<(String, String)>, + alias_override: Option, ) -> Rc { let name_suffix = if let Some(granularity) = &granularity { granularity.clone() @@ -37,7 +50,8 @@ impl TimeDimensionSymbol { "day".to_string() }; let full_name = format!("{}_{}", base_symbol.full_name(), name_suffix); - let alias = format!("{}_{}", base_symbol.alias(), name_suffix); + let alias = + alias_override.unwrap_or_else(|| format!("{}_{}", base_symbol.alias(), name_suffix)); let compiled_path = CompiledMemberPath::new( base_symbol.compiled_path().cube().clone(), full_name, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/common_sql_generation.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/common_sql_generation.rs index faa1aa45c5a92..44a1f06c1d06a 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/common_sql_generation.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/common_sql_generation.rs @@ -1,5 +1,6 @@ use crate::test_fixtures::cube_bridge::{members_from_strings, MockBaseQueryOptions, MockSchema}; use crate::test_fixtures::test_utils::TestContext; +use cubenativeutils::CubeError; use indoc::indoc; use std::rc::Rc; @@ -40,6 +41,40 @@ fn test_member_to_alias() { ); } +#[test] +fn test_member_to_alias_time_dimension_granularity() -> Result<(), CubeError> { + let schema = MockSchema::from_yaml_file("common/visitors.yaml"); + let test_context = TestContext::new(schema)?; + + // The SQL API references a granularized time-dimension column by an alias + // sent via `memberToAlias` keyed `{member}.{granularity}`. The planner must + // honor it instead of defaulting to `{base alias}_{granularity}`. + let query_yaml = indoc! {r#" + measures: + - visitors.count + time_dimensions: + - dimension: visitors.created_at + granularity: month + memberToAlias: + visitors.created_at.month: "td_month_alias" + "#}; + + let sql = test_context.build_sql(query_yaml)?; + + // The override must be used as the projected alias … + assert!( + sql.contains("\"td_month_alias\""), + "expected granularized memberToAlias override, got: {sql}" + ); + // … instead of the default `{base alias}_{granularity}`. + assert!( + !sql.contains("visitors__created_at_month"), + "should not fall back to default granularized alias, got: {sql}" + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn test_simple_join_sql() { let schema = MockSchema::from_yaml_file("common/diamond_joins.yaml");