Skip to content

Commit 741138a

Browse files
authored
fix(tesseract): Resolve join for hint-less member-expression measures (cube-js#11136)
A dependency-free member-expression measure such as `count(*)`, in a query with no dimensions or filters to seed the join, resolved an empty join-hint set. That path calls `JoinGraph.buildJoin([])`, which returns null, and the Rust bridge then fails to deserialize the missing `JoinDefinitionStatic` ("invalid type: unit value, expected struct JoinDefinitionStatic"). In the multi-fact join grouping, fall back to the measure's own cube when its hint set is empty — but only for real, joinable cubes. View members are left untouched: their join comes from the other query members, and the view itself is not a joinable cube.
1 parent 8e9d38c commit 741138a

3 files changed

Lines changed: 63 additions & 1 deletion

File tree

rust/cube/cubesqlplanner/cubesqlplanner/src/planner/multi_fact_join_groups.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::cube_bridge::join_hints::JoinHintItem;
12
use crate::planner::collectors::{
23
collect_join_hints, collect_multiplied_measures, has_multi_stage_members,
34
};
@@ -207,7 +208,12 @@ impl MultiFactJoinGroups {
207208
.measure_hints
208209
.iter()
209210
.map(|mh| -> Result<_, CubeError> {
210-
let (key, join_tree) = resolve(&mh.hints)?;
211+
let measure_hints = if mh.hints.is_empty() {
212+
Self::fallback_hints_for_measure(query_tools, &mh.measure)?
213+
} else {
214+
mh.hints.clone()
215+
};
216+
let (key, join_tree) = resolve(&measure_hints)?;
211217
Ok((vec![mh.measure.clone()], key, join_tree))
212218
})
213219
.collect::<Result<Vec<_>, _>>()?
@@ -230,6 +236,27 @@ impl MultiFactJoinGroups {
230236
.collect())
231237
}
232238

239+
/// Hints to use for a measure whose own hint set resolved to empty.
240+
/// Seeds the measure's owning cube when it is a real, joinable cube;
241+
/// returns empty for views (resolved via the query's other members).
242+
fn fallback_hints_for_measure(
243+
query_tools: &Rc<State>,
244+
measure: &Rc<MemberSymbol>,
245+
) -> Result<JoinHints, CubeError> {
246+
let cube_name = measure.cube_name();
247+
let is_view = query_tools
248+
.cube_evaluator()
249+
.cube_from_path(cube_name.clone())
250+
.ok()
251+
.and_then(|cube| cube.static_data().is_view)
252+
.unwrap_or(false);
253+
if is_view {
254+
Ok(JoinHints::new())
255+
} else {
256+
Ok(JoinHints::from_items(vec![JoinHintItem::Single(cube_name)]))
257+
}
258+
}
259+
233260
pub fn measures_join_hints(&self) -> &MeasuresJoinHints {
234261
&self.measures_join_hints
235262
}

rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/member_expressions.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,34 @@ async fn test_expr_measure_sum() {
145145
}
146146
}
147147

148+
// COUNT(*) is a dependency-free measure expression: it references no members, so it
149+
// resolves an empty join-hint set, and with no dimensions/filters to seed the join the
150+
// planner must fall back to the measure's own cube (`orders`) instead of building a
151+
// null join. Regression for hint-less member-expression measures: without the fallback,
152+
// the empty hints reach `JoinGraph.build_join([])`, which yields no joinable cube.
153+
// integration_basic seed has 9 orders → 9.
154+
#[tokio::test(flavor = "multi_thread")]
155+
async fn test_expr_measure_count_star_no_hints() {
156+
let ctx = create_context();
157+
let expr = make_measure_expression("total_count", "orders", "COUNT(*)");
158+
159+
let options = Rc::new(
160+
MockBaseQueryOptions::builder()
161+
.cube_evaluator(ctx.query_tools().cube_evaluator().clone())
162+
.base_tools(ctx.query_tools().base_tools().clone())
163+
.join_graph(ctx.query_tools().join_graph().clone())
164+
.security_context(ctx.security_context().clone())
165+
.measures(Some(vec![expr]))
166+
.build(),
167+
);
168+
169+
ctx.build_sql_from_options(options.clone()).unwrap();
170+
171+
if let Some(result) = ctx.try_execute_pg_from_options(options, SEED).await {
172+
insta::assert_snapshot!(result);
173+
}
174+
}
175+
148176
// Multiplied dim-only ME: a measure expression evaluating to a
149177
// dimension expression (MAX over `customers.city`) used together
150178
// with an `orders` dimension. `orders→customers` is many_to_one, so
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
source: cubesqlplanner/src/tests/integration/member_expressions.rs
3+
expression: result
4+
---
5+
total_count
6+
-----------
7+
9

0 commit comments

Comments
 (0)