diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/multi_fact_join_groups.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/multi_fact_join_groups.rs index 1e53d1d3bce3d..9323c5e882b01 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/multi_fact_join_groups.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/multi_fact_join_groups.rs @@ -1,3 +1,4 @@ +use crate::cube_bridge::join_hints::JoinHintItem; use crate::planner::collectors::{ collect_join_hints, collect_multiplied_measures, has_multi_stage_members, }; @@ -207,7 +208,12 @@ impl MultiFactJoinGroups { .measure_hints .iter() .map(|mh| -> Result<_, CubeError> { - let (key, join_tree) = resolve(&mh.hints)?; + let measure_hints = if mh.hints.is_empty() { + Self::fallback_hints_for_measure(query_tools, &mh.measure)? + } else { + mh.hints.clone() + }; + let (key, join_tree) = resolve(&measure_hints)?; Ok((vec![mh.measure.clone()], key, join_tree)) }) .collect::, _>>()? @@ -230,6 +236,27 @@ impl MultiFactJoinGroups { .collect()) } + /// Hints to use for a measure whose own hint set resolved to empty. + /// Seeds the measure's owning cube when it is a real, joinable cube; + /// returns empty for views (resolved via the query's other members). + fn fallback_hints_for_measure( + query_tools: &Rc, + measure: &Rc, + ) -> Result { + let cube_name = measure.cube_name(); + let is_view = query_tools + .cube_evaluator() + .cube_from_path(cube_name.clone()) + .ok() + .and_then(|cube| cube.static_data().is_view) + .unwrap_or(false); + if is_view { + Ok(JoinHints::new()) + } else { + Ok(JoinHints::from_items(vec![JoinHintItem::Single(cube_name)])) + } + } + pub fn measures_join_hints(&self) -> &MeasuresJoinHints { &self.measures_join_hints } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/member_expressions.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/member_expressions.rs index be41a92022834..ed2fe2e9ad6a9 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/member_expressions.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/member_expressions.rs @@ -143,6 +143,34 @@ async fn test_expr_measure_sum() { } } +// COUNT(*) is a dependency-free measure expression: it references no members, so it +// resolves an empty join-hint set, and with no dimensions/filters to seed the join the +// planner must fall back to the measure's own cube (`orders`) instead of building a +// null join. Regression for hint-less member-expression measures: without the fallback, +// the empty hints reach `JoinGraph.build_join([])`, which yields no joinable cube. +// integration_basic seed has 9 orders → 9. +#[tokio::test(flavor = "multi_thread")] +async fn test_expr_measure_count_star_no_hints() { + let ctx = create_context(); + let expr = make_measure_expression("total_count", "orders", "COUNT(*)"); + + let options = Rc::new( + MockBaseQueryOptions::builder() + .cube_evaluator(ctx.query_tools().cube_evaluator().clone()) + .base_tools(ctx.query_tools().base_tools().clone()) + .join_graph(ctx.query_tools().join_graph().clone()) + .security_context(ctx.security_context().clone()) + .measures(Some(vec![expr])) + .build(), + ); + + ctx.build_sql_from_options(options.clone()).unwrap(); + + if let Some(result) = ctx.try_execute_pg_from_options(options, SEED).await { + insta::assert_snapshot!(result); + } +} + // Multiplied dim-only ME: a measure expression evaluating to a // dimension expression (MAX over `customers.city`) used together // with an `orders` dimension. `orders→customers` is many_to_one, so diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/snapshots/cubesqlplanner__tests__integration__member_expressions__expr_measure_count_star_no_hints.snap b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/snapshots/cubesqlplanner__tests__integration__member_expressions__expr_measure_count_star_no_hints.snap new file mode 100644 index 0000000000000..a4790e00483de --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/snapshots/cubesqlplanner__tests__integration__member_expressions__expr_measure_count_star_no_hints.snap @@ -0,0 +1,7 @@ +--- +source: cubesqlplanner/src/tests/integration/member_expressions.rs +expression: result +--- +total_count +----------- +9