Skip to content

Commit a4a3e79

Browse files
authored
fix(tesseract): Push ad-hoc filters into view measure aggregation (cube-js#11137)
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.
1 parent 741138a commit a4a3e79

3 files changed

Lines changed: 88 additions & 4 deletions

File tree

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,13 @@ impl QueryPropertiesCompiler {
274274
}
275275
let source_measure_compiled =
276276
evaluator_compiler.add_measure_evaluator(source_measure.clone())?;
277-
let symbol = if let Ok(source_measure) = source_measure_compiled.as_measure() {
277+
// A view measure is a reference wrapper whose type collapses to
278+
// `number` (Calculated), which rejects additional filters. Resolve
279+
// the reference chain to the owning cube measure so the filter is
280+
// pushed inside the aggregation (`SUM(CASE WHEN ... END)`); a no-op
281+
// for plain cube measures.
282+
let resolved_source = source_measure_compiled.clone().resolve_reference_chain();
283+
let symbol = if let Ok(source_measure) = resolved_source.as_measure() {
278284
let patched_measure =
279285
source_measure.new_patched(new_measure_type, filters_to_add)?;
280286
MemberSymbol::new_measure(patched_measure)

rust/cube/cubesqlplanner/cubesqlplanner/src/tests/member_expressions_on_views.rs

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
use crate::cube_bridge::member_expression::MemberExpressionExpressionDef;
1+
use crate::cube_bridge::member_expression::{ExpressionStruct, MemberExpressionExpressionDef};
22
use crate::cube_bridge::member_sql::MemberSql;
33
use crate::cube_bridge::options_member::OptionsMember;
44
use crate::test_fixtures::cube_bridge::{
5-
members_from_strings, MockBaseQueryOptions, MockMemberExpressionDefinition, MockMemberSql,
6-
MockSchema,
5+
members_from_strings, MockBaseQueryOptions, MockExpressionStruct,
6+
MockMemberExpressionDefinition, MockMemberSql, MockSchema, MockStructWithSqlMember,
77
};
88
use crate::test_fixtures::test_utils::TestContext;
9+
use cubenativeutils::CubeError;
910
use indoc::indoc;
1011
use std::rc::Rc;
1112

@@ -25,6 +26,34 @@ fn make_member_expression(expression_name: &str, cube_name: &str, sql: &str) ->
2526
OptionsMember::MemberExpression(Rc::new(expr))
2627
}
2728

29+
// Builds a `PatchMeasure` member expression that adds one ad-hoc CASE-WHEN
30+
// filter to `source_measure` — the SQL-API mechanism for pushing a filter
31+
// inside a measure's aggregation.
32+
fn make_patched_measure(
33+
expression_name: &str,
34+
cube_name: &str,
35+
source_measure: &str,
36+
filter_sql: &str,
37+
) -> OptionsMember {
38+
let filter = MockStructWithSqlMember::builder()
39+
.sql(filter_sql.to_string())
40+
.build();
41+
let expr_struct = MockExpressionStruct::builder()
42+
.expression_type("PatchMeasure".to_string())
43+
.source_measure(Some(source_measure.to_string()))
44+
.add_filters(Some(vec![Rc::new(filter)]))
45+
.build();
46+
let expr = MockMemberExpressionDefinition::builder()
47+
.expression_name(Some(expression_name.to_string()))
48+
.name(Some(expression_name.to_string()))
49+
.cube_name(Some(cube_name.to_string()))
50+
.expression(MemberExpressionExpressionDef::Struct(
51+
Rc::new(expr_struct) as Rc<dyn ExpressionStruct>
52+
))
53+
.build();
54+
OptionsMember::MemberExpression(Rc::new(expr))
55+
}
56+
2857
fn build_options_with_member_expression(
2958
ctx: &TestContext,
3059
extra_measure: OptionsMember,
@@ -183,3 +212,45 @@ async fn test_many_to_one_view_child_distinct_dim() {
183212
insta::assert_snapshot!(result);
184213
}
185214
}
215+
216+
// A PatchMeasure adding a CASE-WHEN filter to a measure exposed through a view
217+
// (`root_val_sum`, a SUM) must resolve the reference chain to the owning cube
218+
// measure so the filter is pushed inside the aggregation.
219+
// root_test_dim='rt_x' → roots 1,2 → SUM = 10 + 20 = 30.
220+
#[tokio::test(flavor = "multi_thread")]
221+
async fn test_many_to_one_view_patched_measure_filter() -> Result<(), CubeError> {
222+
let ctx = create_test_context();
223+
let expr = make_patched_measure(
224+
"filtered_root_sum",
225+
"many_to_one_view",
226+
"many_to_one_view.root_val_sum",
227+
"{many_to_one_view.root_test_dim} = 'rt_x'",
228+
);
229+
230+
let options = Rc::new(
231+
MockBaseQueryOptions::builder()
232+
.cube_evaluator(ctx.query_tools().cube_evaluator().clone())
233+
.base_tools(ctx.query_tools().base_tools().clone())
234+
.join_graph(ctx.query_tools().join_graph().clone())
235+
.security_context(ctx.security_context().clone())
236+
.measures(Some(vec![expr]))
237+
.build(),
238+
);
239+
240+
let sql = ctx.build_sql_from_options(options.clone())?;
241+
// The ad-hoc filter is pushed inside the aggregation (measure_filter.rs
242+
// renders `CASE WHEN <filter> THEN <result> END`), not as an outer WHERE.
243+
assert!(
244+
sql.contains("CASE WHEN"),
245+
"ad-hoc filter must be pushed inside the aggregation, got: {sql}"
246+
);
247+
248+
if let Some(result) = ctx
249+
.try_execute_pg_from_options(options, "many_to_one_tables.sql")
250+
.await
251+
{
252+
insta::assert_snapshot!(result);
253+
}
254+
255+
Ok(())
256+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
source: cubesqlplanner/src/tests/member_expressions_on_views.rs
3+
expression: result
4+
---
5+
filtered_root_sum
6+
-----------------
7+
30.00

0 commit comments

Comments
 (0)