From 0b495e087d4af49988e77b71c3ae14e2eda2ea88 Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Fri, 19 Jun 2026 14:34:06 +0200 Subject: [PATCH 1/2] fix(tesseract): Push ad-hoc filters into view measure aggregation A measure exposed through a view is a reference wrapper whose declared `type` is collapsed to `number` (a Calculated kind), hiding the real aggregation. PatchMeasure then rejected ad-hoc CASE-WHEN filters with "Unsupported additional filters for measure ... type number", because Calculated measures don't support additional filters. Resolve the reference chain to the owning cube measure before patching, so the filter is pushed inside the real aggregation (`SUM(CASE WHEN ... END)`), matching the legacy planner's `patchMeasurePushDownFilterSql`. `resolve_reference_chain` is a no-op for plain cube measures, so non-view PatchMeasure behaviour is unchanged. --- .../src/planner/query_properties_compiler.rs | 8 +- .../src/tests/member_expressions_on_views.rs | 79 ++++++++++++++++++- ...ny_to_one_view_patched_measure_filter.snap | 7 ++ 3 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 rust/cube/cubesqlplanner/cubesqlplanner/src/tests/snapshots/cubesqlplanner__tests__member_expressions_on_views__many_to_one_view_patched_measure_filter.snap 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 9e41d7990fa8b..f4677deaceafc 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties_compiler.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties_compiler.rs @@ -272,7 +272,13 @@ impl QueryPropertiesCompiler { } let source_measure_compiled = evaluator_compiler.add_measure_evaluator(source_measure.clone())?; - let symbol = if let Ok(source_measure) = source_measure_compiled.as_measure() { + // A view measure is a reference wrapper whose type collapses to + // `number` (Calculated), which rejects additional filters. Resolve + // the reference chain to the owning cube measure so the filter is + // pushed inside the aggregation (`SUM(CASE WHEN ... END)`); a no-op + // for plain cube measures. + let resolved_source = source_measure_compiled.clone().resolve_reference_chain(); + let symbol = if let Ok(source_measure) = resolved_source.as_measure() { let patched_measure = source_measure.new_patched(new_measure_type, filters_to_add)?; MemberSymbol::new_measure(patched_measure) diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/member_expressions_on_views.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/member_expressions_on_views.rs index 781ca1dab7d95..c6fd4f70d56d8 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/member_expressions_on_views.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/member_expressions_on_views.rs @@ -1,9 +1,9 @@ -use crate::cube_bridge::member_expression::MemberExpressionExpressionDef; +use crate::cube_bridge::member_expression::{ExpressionStruct, MemberExpressionExpressionDef}; use crate::cube_bridge::member_sql::MemberSql; use crate::cube_bridge::options_member::OptionsMember; use crate::test_fixtures::cube_bridge::{ - members_from_strings, MockBaseQueryOptions, MockMemberExpressionDefinition, MockMemberSql, - MockSchema, + members_from_strings, MockBaseQueryOptions, MockExpressionStruct, + MockMemberExpressionDefinition, MockMemberSql, MockSchema, MockStructWithSqlMember, }; use crate::test_fixtures::test_utils::TestContext; use indoc::indoc; @@ -25,6 +25,34 @@ fn make_member_expression(expression_name: &str, cube_name: &str, sql: &str) -> OptionsMember::MemberExpression(Rc::new(expr)) } +// Builds a `PatchMeasure` member expression that adds one ad-hoc CASE-WHEN +// filter to `source_measure` — the SQL-API mechanism for pushing a filter +// inside a measure's aggregation. +fn make_patched_measure( + expression_name: &str, + cube_name: &str, + source_measure: &str, + filter_sql: &str, +) -> OptionsMember { + let filter = MockStructWithSqlMember::builder() + .sql(filter_sql.to_string()) + .build(); + let expr_struct = MockExpressionStruct::builder() + .expression_type("PatchMeasure".to_string()) + .source_measure(Some(source_measure.to_string())) + .add_filters(Some(vec![Rc::new(filter)])) + .build(); + let expr = MockMemberExpressionDefinition::builder() + .expression_name(Some(expression_name.to_string())) + .name(Some(expression_name.to_string())) + .cube_name(Some(cube_name.to_string())) + .expression(MemberExpressionExpressionDef::Struct( + Rc::new(expr_struct) as Rc + )) + .build(); + OptionsMember::MemberExpression(Rc::new(expr)) +} + fn build_options_with_member_expression( ctx: &TestContext, extra_measure: OptionsMember, @@ -183,3 +211,48 @@ async fn test_many_to_one_view_child_distinct_dim() { insta::assert_snapshot!(result); } } + +// Regression for 58b5c96 (push ad-hoc filters into view measure aggregation). +// A PatchMeasure adding a CASE-WHEN filter to a measure exposed through a view +// (`root_val_sum`, a SUM) must resolve the reference chain to the owning cube +// measure so the filter is pushed inside the aggregation. Before the fix the +// view measure's type collapsed to `number` (Calculated) and build_sql failed +// with "Unsupported additional filters for measure ... type number". +// root_test_dim='rt_x' → roots 1,2 → SUM = 10 + 20 = 30. +#[tokio::test(flavor = "multi_thread")] +async fn test_many_to_one_view_patched_measure_filter() { + let ctx = create_test_context(); + let expr = make_patched_measure( + "filtered_root_sum", + "many_to_one_view", + "many_to_one_view.root_val_sum", + "{many_to_one_view.root_test_dim} = 'rt_x'", + ); + + 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(), + ); + + // build_sql itself is the regression guard: before the fix it returned + // Err("Unsupported additional filters ...") and this unwrap panicked. + let sql = ctx.build_sql_from_options(options.clone()).unwrap(); + // The ad-hoc filter is pushed inside the aggregation (measure_filter.rs + // renders `CASE WHEN THEN END`), not as an outer WHERE. + assert!( + sql.contains("CASE WHEN"), + "ad-hoc filter must be pushed inside the aggregation, got: {sql}" + ); + + if let Some(result) = ctx + .try_execute_pg_from_options(options, "many_to_one_tables.sql") + .await + { + insta::assert_snapshot!(result); + } +} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/snapshots/cubesqlplanner__tests__member_expressions_on_views__many_to_one_view_patched_measure_filter.snap b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/snapshots/cubesqlplanner__tests__member_expressions_on_views__many_to_one_view_patched_measure_filter.snap new file mode 100644 index 0000000000000..8726549f5aad1 --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/snapshots/cubesqlplanner__tests__member_expressions_on_views__many_to_one_view_patched_measure_filter.snap @@ -0,0 +1,7 @@ +--- +source: cubesqlplanner/src/tests/member_expressions_on_views.rs +expression: result +--- +filtered_root_sum +----------------- +30.00 From 01c0626f89ad73379e2fac847178d681ff02fd62 Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Mon, 22 Jun 2026 18:33:10 +0200 Subject: [PATCH 2/2] chore: up code quality --- .../src/tests/member_expressions_on_views.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/member_expressions_on_views.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/member_expressions_on_views.rs index c6fd4f70d56d8..1b7e948d23a26 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/member_expressions_on_views.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/member_expressions_on_views.rs @@ -6,6 +6,7 @@ use crate::test_fixtures::cube_bridge::{ MockMemberExpressionDefinition, MockMemberSql, MockSchema, MockStructWithSqlMember, }; use crate::test_fixtures::test_utils::TestContext; +use cubenativeutils::CubeError; use indoc::indoc; use std::rc::Rc; @@ -220,7 +221,7 @@ async fn test_many_to_one_view_child_distinct_dim() { // with "Unsupported additional filters for measure ... type number". // root_test_dim='rt_x' → roots 1,2 → SUM = 10 + 20 = 30. #[tokio::test(flavor = "multi_thread")] -async fn test_many_to_one_view_patched_measure_filter() { +async fn test_many_to_one_view_patched_measure_filter() -> Result<(), CubeError> { let ctx = create_test_context(); let expr = make_patched_measure( "filtered_root_sum", @@ -240,8 +241,9 @@ async fn test_many_to_one_view_patched_measure_filter() { ); // build_sql itself is the regression guard: before the fix it returned - // Err("Unsupported additional filters ...") and this unwrap panicked. - let sql = ctx.build_sql_from_options(options.clone()).unwrap(); + // Err("Unsupported additional filters ...") and this propagated as a + // test failure. + let sql = ctx.build_sql_from_options(options.clone())?; // The ad-hoc filter is pushed inside the aggregation (measure_filter.rs // renders `CASE WHEN THEN END`), not as an outer WHERE. assert!( @@ -255,4 +257,6 @@ async fn test_many_to_one_view_patched_measure_filter() { { insta::assert_snapshot!(result); } + + Ok(()) }