From 137ed2d8bb31e6988cf7e911573f0a4e8fd89ca4 Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Fri, 19 Jun 2026 15:00:12 +0200 Subject: [PATCH 1/3] fix(tesseract): Resolve join for hint-less member-expression measures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/planner/multi_fact_join_groups.rs | 36 ++++++++++++++++++- .../tests/integration/member_expressions.rs | 28 +++++++++++++++ ...ons__expr_measure_count_star_no_hints.snap | 7 ++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 rust/cube/cubesqlplanner/cubesqlplanner/src/tests/integration/snapshots/cubesqlplanner__tests__integration__member_expressions__expr_measure_count_star_no_hints.snap 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..7ceb7e6077bf5 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,6 +1,7 @@ use crate::planner::collectors::{ collect_join_hints, collect_multiplied_measures, has_multi_stage_members, }; +use crate::cube_bridge::join_hints::JoinHintItem; use crate::planner::filter::FilterItem; use crate::planner::join_hints::JoinHints; use crate::planner::planners::JoinTreeBuilder; @@ -207,7 +208,19 @@ impl MultiFactJoinGroups { .measure_hints .iter() .map(|mh| -> Result<_, CubeError> { - let (key, join_tree) = resolve(&mh.hints)?; + // A measure with no resolvable hints is a dependency-free + // member expression such as `count(*)` whose query carries + // no dimensions/filters to seed the join. Resolving an empty + // hint set yields a null join (`JoinGraph.buildJoin([])`), + // which fails to deserialize. Fall back to the measure's own + // cube — but only when it is a real, joinable cube; for view + // members the join is provided by the other query members. + 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 +243,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 From 398a5739e23534154d0790ea10e2b96d94f16c5e Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Mon, 22 Jun 2026 18:25:33 +0200 Subject: [PATCH 2/3] chore: drop comment --- .../cubesqlplanner/src/planner/multi_fact_join_groups.rs | 7 ------- 1 file changed, 7 deletions(-) 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 7ceb7e6077bf5..e81e1d199d479 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 @@ -208,13 +208,6 @@ impl MultiFactJoinGroups { .measure_hints .iter() .map(|mh| -> Result<_, CubeError> { - // A measure with no resolvable hints is a dependency-free - // member expression such as `count(*)` whose query carries - // no dimensions/filters to seed the join. Resolving an empty - // hint set yields a null join (`JoinGraph.buildJoin([])`), - // which fails to deserialize. Fall back to the measure's own - // cube — but only when it is a real, joinable cube; for view - // members the join is provided by the other query members. let measure_hints = if mh.hints.is_empty() { Self::fallback_hints_for_measure(query_tools, &mh.measure)? } else { From 0aaf4ab56258b47e5e76407e95f5edf0971cf0df Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Mon, 22 Jun 2026 18:33:42 +0200 Subject: [PATCH 3/3] chore: fmt --- .../cubesqlplanner/src/planner/multi_fact_join_groups.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e81e1d199d479..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,7 +1,7 @@ +use crate::cube_bridge::join_hints::JoinHintItem; use crate::planner::collectors::{ collect_join_hints, collect_multiplied_measures, has_multi_stage_members, }; -use crate::cube_bridge::join_hints::JoinHintItem; use crate::planner::filter::FilterItem; use crate::planner::join_hints::JoinHints; use crate::planner::planners::JoinTreeBuilder;