Skip to content

Commit 28e91ed

Browse files
authored
fix(tesseract): cast bound param on measure filters (cube-js#11176)
1 parent f0569ad commit 28e91ed

4 files changed

Lines changed: 70 additions & 1 deletion

File tree

β€Žpackages/cubejs-schema-compiler/test/unit/base-query.test.tsβ€Ž

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,20 @@ describe('SQL Generation', () => {
11061106
);
11071107
});
11081108

1109+
it('CORE-541: measure filter casts bound param - bigquery tesseract planner', async () => {
1110+
await compilers.compiler.compile();
1111+
1112+
const query = new BigqueryQuery(compilers, {
1113+
measures: ['cards.count'],
1114+
filters: [
1115+
{ member: 'cards.count', operator: 'gt', values: ['10'] }
1116+
],
1117+
useNativeSqlPlanner: true,
1118+
});
1119+
const [sql] = query.buildSqlAndParams();
1120+
expect(sql).toContain('CAST(? AS FLOAT64)');
1121+
});
1122+
11091123
it('Test time series with different granularity - postgres', async () => {
11101124
await compilers.compiler.compile();
11111125

β€Žrust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/typed_filter.rsβ€Ž

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,17 @@ impl TypedFilterBuilder {
158158
let symbol = resolve_base_symbol(member_evaluator);
159159
match symbol.as_ref() {
160160
MemberSymbol::Dimension(d) => Some(d.dimension_type().to_string()),
161-
MemberSymbol::Measure(m) => Some(m.measure_type().to_string()),
161+
// The cast type drives how a bound comparison value is wrapped.
162+
// Aggregations (count, sum, ...) and number measures compare as
163+
// numbers. String/time measures are non-numeric scalars and must
164+
// not be coerced to a number; date comparisons take the dedicated
165+
// date operators instead. min/max carry their operand type, which
166+
// isn't known here, so they fall through to the numeric default.
167+
MemberSymbol::Measure(m) => match m.measure_type() {
168+
"boolean" => Some("boolean".to_string()),
169+
"string" | "time" => None,
170+
_ => Some("number".to_string()),
171+
},
162172
_ => None,
163173
}
164174
}

β€Žrust/cube/cubesqlplanner/cubesqlplanner/src/test_fixtures/schemas/yaml_files/common/visitors.yamlβ€Ž

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ cubes:
8383
- name: total_revenue_per_count
8484
type: number
8585
sql: "{visitors.count} / {total_revenue}"
86+
- name: source_label
87+
type: string
88+
sql: "MAX({CUBE}.source)"
8689
segments:
8790
- name: google
8891
sql: "{CUBE.source} = 'google'"

β€Žrust/cube/cubesqlplanner/cubesqlplanner/src/tests/filter/to_sql.rsβ€Ž

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,48 @@ fn test_not_contains_with_null() {
595595
);
596596
}
597597

598+
// ── measure filters (HAVING) ──────────────────────────────────────────────────
599+
600+
#[test]
601+
fn test_measure_filter_count_gt_casts_param() {
602+
let result = build(indoc! {"
603+
filters:
604+
- member: visitors.count
605+
operator: gt
606+
values:
607+
- \"10\"
608+
"});
609+
assert_filter(&result, r#"(count(COUNT(*)) > $_0_$::numeric)"#, &["10"]);
610+
}
611+
612+
#[test]
613+
fn test_measure_filter_sum_lt_casts_param() {
614+
let result = build(indoc! {"
615+
filters:
616+
- member: visitors.total_revenue
617+
operator: lt
618+
values:
619+
- \"100\"
620+
"});
621+
assert_filter(
622+
&result,
623+
r#"(sum("visitors".revenue) < $_0_$::numeric)"#,
624+
&["100"],
625+
);
626+
}
627+
628+
#[test]
629+
fn test_measure_filter_string_no_cast() {
630+
let result = build(indoc! {"
631+
filters:
632+
- member: visitors.source_label
633+
operator: equals
634+
values:
635+
- google
636+
"});
637+
assert_filter(&result, r#"(MAX("visitors".source) = $_0_$)"#, &["google"]);
638+
}
639+
598640
// ── filter groups (OR / AND) ────────────────────────────────────────────────
599641

600642
#[test]

0 commit comments

Comments
Β (0)