diff --git a/packages/cubejs-schema-compiler/test/unit/base-query.test.ts b/packages/cubejs-schema-compiler/test/unit/base-query.test.ts index b00945d95dc42..f1a98c6a5c0c6 100644 --- a/packages/cubejs-schema-compiler/test/unit/base-query.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/base-query.test.ts @@ -1106,6 +1106,20 @@ describe('SQL Generation', () => { ); }); + it('CORE-541: measure filter casts bound param - bigquery tesseract planner', async () => { + await compilers.compiler.compile(); + + const query = new BigqueryQuery(compilers, { + measures: ['cards.count'], + filters: [ + { member: 'cards.count', operator: 'gt', values: ['10'] } + ], + useNativeSqlPlanner: true, + }); + const [sql] = query.buildSqlAndParams(); + expect(sql).toContain('CAST(? AS FLOAT64)'); + }); + it('Test time series with different granularity - postgres', async () => { await compilers.compiler.compile(); diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/typed_filter.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/typed_filter.rs index 548e37d5250e7..ce2d2830e89e0 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/typed_filter.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/typed_filter.rs @@ -158,7 +158,17 @@ impl TypedFilterBuilder { let symbol = resolve_base_symbol(member_evaluator); match symbol.as_ref() { MemberSymbol::Dimension(d) => Some(d.dimension_type().to_string()), - MemberSymbol::Measure(m) => Some(m.measure_type().to_string()), + // The cast type drives how a bound comparison value is wrapped. + // Aggregations (count, sum, ...) and number measures compare as + // numbers. String/time measures are non-numeric scalars and must + // not be coerced to a number; date comparisons take the dedicated + // date operators instead. min/max carry their operand type, which + // isn't known here, so they fall through to the numeric default. + MemberSymbol::Measure(m) => match m.measure_type() { + "boolean" => Some("boolean".to_string()), + "string" | "time" => None, + _ => Some("number".to_string()), + }, _ => None, } } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/common/visitors.yaml b/rust/cube/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/common/visitors.yaml index cd9b29bcb6664..3447fd616eec8 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/common/visitors.yaml +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/common/visitors.yaml @@ -83,6 +83,9 @@ cubes: - name: total_revenue_per_count type: number sql: "{visitors.count} / {total_revenue}" + - name: source_label + type: string + sql: "MAX({CUBE}.source)" segments: - name: google sql: "{CUBE.source} = 'google'" diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/filter/to_sql.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/filter/to_sql.rs index 69a6f908bbfaf..3a7dbeabda159 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/filter/to_sql.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/filter/to_sql.rs @@ -595,6 +595,48 @@ fn test_not_contains_with_null() { ); } +// ── measure filters (HAVING) ────────────────────────────────────────────────── + +#[test] +fn test_measure_filter_count_gt_casts_param() { + let result = build(indoc! {" + filters: + - member: visitors.count + operator: gt + values: + - \"10\" + "}); + assert_filter(&result, r#"(count(COUNT(*)) > $_0_$::numeric)"#, &["10"]); +} + +#[test] +fn test_measure_filter_sum_lt_casts_param() { + let result = build(indoc! {" + filters: + - member: visitors.total_revenue + operator: lt + values: + - \"100\" + "}); + assert_filter( + &result, + r#"(sum("visitors".revenue) < $_0_$::numeric)"#, + &["100"], + ); +} + +#[test] +fn test_measure_filter_string_no_cast() { + let result = build(indoc! {" + filters: + - member: visitors.source_label + operator: equals + values: + - google + "}); + assert_filter(&result, r#"(MAX("visitors".source) = $_0_$)"#, &["google"]); +} + // ── filter groups (OR / AND) ──────────────────────────────────────────────── #[test]