Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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,
};
Expand Down Expand Up @@ -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::<Result<Vec<_>, _>>()?
Expand All @@ -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<State>,
measure: &Rc<MemberSymbol>,
) -> Result<JoinHints, CubeError> {
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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: cubesqlplanner/src/tests/integration/member_expressions.rs
expression: result
---
total_count
-----------
9
Loading