Skip to content

Commit ca71721

Browse files
authored
fix(tesseract): Multi-stage dimension dependent on another multi-stage dimension didn’t work (#10147)
1 parent 016595e commit ca71721

File tree

9 files changed

+365
-25
lines changed

9 files changed

+365
-25
lines changed

packages/cubejs-schema-compiler/test/integration/postgres/bucketing.test.ts

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,27 @@ cubes:
8484
type: string
8585
add_group_by: [orders.customerId]
8686
87+
- name: changeTypeComplexWithJoin
88+
sql: >
89+
CASE
90+
WHEN {revenueYearAgo} IS NULL THEN 'New'
91+
WHEN {revenue} > {revenueYearAgo} THEN 'Grow'
92+
ELSE 'Down'
93+
END
94+
multi_stage: true
95+
type: string
96+
add_group_by: [first_date.customerId]
97+
98+
- name: changeTypeConcat
99+
sql: "CONCAT({changeTypeComplex}, '-test')"
100+
type: string
101+
multi_stage: true
102+
103+
- name: twoDimsConcat
104+
sql: "CONCAT({changeTypeComplex}, '-', {first_date.customerType2})"
105+
type: string
106+
multi_stage: true
107+
87108
88109
measures:
89110
- name: count
@@ -111,6 +132,55 @@ cubes:
111132
END
112133
type: string
113134
135+
- name: first_date
136+
sql: >
137+
SELECT 1 AS id, '2023-03-01T00:00:00Z'::timestamptz AS createdAt, 1 AS customerId UNION ALL
138+
SELECT 8 AS id, '2023-09-01T00:00:00Z'::timestamptz AS createdAt, 2 AS customerId UNION ALL
139+
SELECT 16 AS id, '2024-09-01T00:00:00Z'::timestamptz AS createdAt, 3 AS customerId UNION ALL
140+
SELECT 23 AS id, '2025-03-01T00:00:00Z'::timestamptz AS createdAt, 4 AS customerId UNION ALL
141+
SELECT 29 AS id, '2025-03-01T00:00:00Z'::timestamptz AS createdAt, 5 AS customerId UNION ALL
142+
SELECT 36 AS id, '2025-09-01T00:00:00Z'::timestamptz AS createdAt, 6 AS customerId
143+
144+
joins:
145+
- name: orders
146+
sql: "{first_date.customerId} = {orders.customerId}"
147+
relationship: one_to_many
148+
149+
dimensions:
150+
- name: customerId
151+
sql: customerId
152+
type: number
153+
154+
- name: createdAt
155+
sql: createdAt
156+
type: time
157+
158+
- name: customerType
159+
sql: >
160+
CASE
161+
WHEN {orders.revenue} < 10000 THEN 'Low'
162+
WHEN {orders.revenue} < 20000 THEN 'Medium'
163+
ELSE 'Top'
164+
END
165+
multi_stage: true
166+
type: string
167+
add_group_by: [first_date.customerId]
168+
169+
- name: customerType2
170+
sql: >
171+
CASE
172+
WHEN {orders.revenue} < 3000 THEN 'Low'
173+
ELSE 'Top'
174+
END
175+
multi_stage: true
176+
type: string
177+
add_group_by: [first_date.customerId]
178+
179+
- name: customerTypeConcat
180+
sql: "CONCAT('Customer type: ', {customerType})"
181+
multi_stage: true
182+
type: string
183+
add_group_by: [first_date.customerId]
114184
115185
116186
`);
@@ -242,6 +312,205 @@ cubes:
242312
},
243313
],
244314
{ joinGraph, cubeEvaluator, compiler }));
315+
it('bucketing with dimension over complex dimension', async () => dbRunner.runQueryTest({
316+
dimensions: ['orders.changeTypeConcat'],
317+
measures: ['orders.revenue', 'orders.revenueYearAgo'],
318+
timeDimensions: [
319+
{
320+
dimension: 'orders.createdAt',
321+
granularity: 'year',
322+
dateRange: ['2024-01-02T00:00:00', '2026-01-01T00:00:00']
323+
}
324+
],
325+
timezone: 'UTC',
326+
order: [{
327+
id: 'orders.changeTypeConcat'
328+
}, { id: 'orders.createdAt' }],
329+
},
330+
[
331+
{
332+
orders__change_type_concat: 'Down-test',
333+
orders__created_at_year: '2024-01-01T00:00:00.000Z',
334+
orders__revenue: '20400',
335+
orders__revenue_year_ago: '22800'
336+
},
337+
{
338+
orders__change_type_concat: 'Down-test',
339+
orders__created_at_year: '2025-01-01T00:00:00.000Z',
340+
orders__revenue: '17800',
341+
orders__revenue_year_ago: '20400'
342+
},
343+
{
344+
orders__change_type_concat: 'Grow-test',
345+
orders__created_at_year: '2024-01-01T00:00:00.000Z',
346+
orders__revenue: '11700',
347+
orders__revenue_year_ago: '9400'
348+
},
349+
{
350+
orders__change_type_concat: 'Grow-test',
351+
orders__created_at_year: '2025-01-01T00:00:00.000Z',
352+
orders__revenue: '14100',
353+
orders__revenue_year_ago: '11700'
354+
},
355+
],
356+
{ joinGraph, cubeEvaluator, compiler }));
357+
it('bucketing with join and bucket dimension', async () => dbRunner.runQueryTest({
358+
dimensions: ['orders.changeTypeComplexWithJoin'],
359+
measures: ['orders.revenue', 'orders.revenueYearAgo'],
360+
timeDimensions: [
361+
{
362+
dimension: 'orders.createdAt',
363+
granularity: 'year',
364+
dateRange: ['2024-01-02T00:00:00', '2026-01-01T00:00:00']
365+
}
366+
],
367+
timezone: 'UTC',
368+
order: [{
369+
id: 'orders.changeTypeComplexWithJoin'
370+
}, { id: 'orders.createdAt' }],
371+
},
372+
[
373+
{
374+
orders__change_type_complex_with_join: 'Down',
375+
orders__created_at_year: '2024-01-01T00:00:00.000Z',
376+
orders__revenue: '20400',
377+
orders__revenue_year_ago: '22800'
378+
},
379+
{
380+
orders__change_type_complex_with_join: 'Down',
381+
orders__created_at_year: '2025-01-01T00:00:00.000Z',
382+
orders__revenue: '17800',
383+
orders__revenue_year_ago: '20400'
384+
},
385+
{
386+
orders__change_type_complex_with_join: 'Grow',
387+
orders__created_at_year: '2024-01-01T00:00:00.000Z',
388+
orders__revenue: '11700',
389+
orders__revenue_year_ago: '9400'
390+
},
391+
{
392+
orders__change_type_complex_with_join: 'Grow',
393+
orders__created_at_year: '2025-01-01T00:00:00.000Z',
394+
orders__revenue: '14100',
395+
orders__revenue_year_ago: '11700'
396+
},
397+
],
398+
{ joinGraph, cubeEvaluator, compiler }));
399+
it('bucketing dim reference other cube measure', async () => dbRunner.runQueryTest({
400+
dimensions: ['first_date.customerType'],
401+
measures: ['orders.revenue'],
402+
timezone: 'UTC',
403+
order: [{
404+
id: 'first_date.customerType'
405+
}],
406+
},
407+
[
408+
{ first_date__customer_type: 'Low', orders__revenue: '8100' },
409+
{ first_date__customer_type: 'Medium', orders__revenue: '41700' },
410+
{ first_date__customer_type: 'Top', orders__revenue: '46400' }
411+
],
412+
{ joinGraph, cubeEvaluator, compiler }));
413+
it('bucketing with two dimensions', async () => dbRunner.runQueryTest({
414+
dimensions: ['orders.changeTypeConcat', 'first_date.customerType2'],
415+
measures: ['orders.revenue', 'orders.revenueYearAgo'],
416+
timeDimensions: [
417+
{
418+
dimension: 'orders.createdAt',
419+
granularity: 'year',
420+
dateRange: ['2024-01-02T00:00:00', '2026-01-01T00:00:00']
421+
}
422+
],
423+
timezone: 'UTC',
424+
order: [{
425+
id: 'orders.changeTypeConcat'
426+
}, { id: 'orders.createdAt' }],
427+
},
428+
[
429+
{
430+
orders__change_type_concat: 'Down-test',
431+
first_date__customer_type2: 'Top',
432+
orders__created_at_year: '2024-01-01T00:00:00.000Z',
433+
orders__revenue: '20400',
434+
orders__revenue_year_ago: '22800'
435+
},
436+
{
437+
orders__change_type_concat: 'Down-test',
438+
first_date__customer_type2: 'Top',
439+
orders__created_at_year: '2025-01-01T00:00:00.000Z',
440+
orders__revenue: '17800',
441+
orders__revenue_year_ago: '20400'
442+
},
443+
{
444+
orders__change_type_concat: 'Grow-test',
445+
first_date__customer_type2: 'Low',
446+
orders__created_at_year: '2024-01-01T00:00:00.000Z',
447+
orders__revenue: '2700',
448+
orders__revenue_year_ago: '2100'
449+
},
450+
{
451+
orders__change_type_concat: 'Grow-test',
452+
first_date__customer_type2: 'Top',
453+
orders__created_at_year: '2024-01-01T00:00:00.000Z',
454+
orders__revenue: '9000',
455+
orders__revenue_year_ago: '7300'
456+
},
457+
{
458+
orders__change_type_concat: 'Grow-test',
459+
first_date__customer_type2: 'Top',
460+
orders__created_at_year: '2025-01-01T00:00:00.000Z',
461+
orders__revenue: '14100',
462+
orders__revenue_year_ago: '11700'
463+
}
464+
],
465+
{ joinGraph, cubeEvaluator, compiler }));
466+
it('bucketing with two dims concacted', async () => dbRunner.runQueryTest({
467+
dimensions: ['orders.twoDimsConcat'],
468+
measures: ['orders.revenue', 'orders.revenueYearAgo'],
469+
timeDimensions: [
470+
{
471+
dimension: 'orders.createdAt',
472+
granularity: 'year',
473+
dateRange: ['2024-01-02T00:00:00', '2026-01-01T00:00:00']
474+
}
475+
],
476+
timezone: 'UTC',
477+
order: [{
478+
id: 'orders.twoDimsConcat'
479+
}, { id: 'orders.createdAt' }],
480+
},
481+
[
482+
{
483+
orders__two_dims_concat: 'Down-Top',
484+
orders__created_at_year: '2024-01-01T00:00:00.000Z',
485+
orders__revenue: '20400',
486+
orders__revenue_year_ago: '22800'
487+
},
488+
{
489+
orders__two_dims_concat: 'Down-Top',
490+
orders__created_at_year: '2025-01-01T00:00:00.000Z',
491+
orders__revenue: '17800',
492+
orders__revenue_year_ago: '20400'
493+
},
494+
{
495+
orders__two_dims_concat: 'Grow-Low',
496+
orders__created_at_year: '2024-01-01T00:00:00.000Z',
497+
orders__revenue: '2700',
498+
orders__revenue_year_ago: '2100'
499+
},
500+
{
501+
orders__two_dims_concat: 'Grow-Top',
502+
orders__created_at_year: '2024-01-01T00:00:00.000Z',
503+
orders__revenue: '9000',
504+
orders__revenue_year_ago: '7300'
505+
},
506+
{
507+
orders__two_dims_concat: 'Grow-Top',
508+
orders__created_at_year: '2025-01-01T00:00:00.000Z',
509+
orders__revenue: '14100',
510+
orders__revenue_year_ago: '11700'
511+
}
512+
],
513+
{ joinGraph, cubeEvaluator, compiler }));
245514
} else {
246515
// This test is working only in tesseract
247516
test.skip('multi stage over sub query', () => { expect(1).toBe(1); });

rust/cubesqlplanner/cubesqlplanner/src/logical_plan/full_key_aggregate.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ impl FullKeyAggregate {
9696
pub fn multi_stage_subquery_refs(&self) -> &Vec<Rc<MultiStageSubqueryRef>> {
9797
&self.multi_stage_subquery_refs
9898
}
99+
100+
pub fn is_empty(&self) -> bool {
101+
self.multi_stage_subquery_refs.is_empty() && self.multiplied_measures_resolver.is_none()
102+
}
99103
}
100104

101105
impl LogicalNode for FullKeyAggregate {

rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/query.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub struct QueryProcessor<'a> {
1717
impl QueryProcessor<'_> {
1818
fn is_over_full_aggregated_source(&self, logical_plan: &Query) -> bool {
1919
match logical_plan.source() {
20-
QuerySource::FullKeyAggregate(_) => true,
20+
QuerySource::FullKeyAggregate(fk) => !fk.is_empty(),
2121
QuerySource::PreAggregation(_) => false,
2222
QuerySource::LogicalJoin(_) => false,
2323
}

rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member_query_planner.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,20 @@ impl MultiStageMemberQueryPlanner {
294294
MemberSymbol::Measure(_) => measures.push(cte_member.clone()),
295295
_ => {}
296296
}
297+
// We add all non–multi-stage dimensions from the underlying states because
298+
// they’re needed to join a multi-stage dimension into the measure query
299+
let (all_dependend_dimensions, all_dependend_time_dimensions) =
300+
self.description.collect_all_non_multi_stage_dimension()?;
301+
dimensions.extend(all_dependend_dimensions.iter().cloned());
302+
time_dimensions.extend(all_dependend_time_dimensions.iter().cloned());
303+
dimensions = dimensions
304+
.into_iter()
305+
.unique_by(|d| d.full_name())
306+
.collect_vec();
307+
time_dimensions = time_dimensions
308+
.into_iter()
309+
.unique_by(|d| d.full_name())
310+
.collect_vec();
297311

298312
let schema = LogicalSchema::default()
299313
.set_dimensions(dimensions)

rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ impl MultiStageQueryPlanner {
219219
descriptions,
220220
resolved_multi_stage_dimensions,
221221
)?;
222-
if !description.is_multi_stage_dimension() {
222+
if !description.is_multi_stage_dimension() || member.as_dimension().is_ok() {
223223
result.push(description);
224224
}
225225
}
@@ -403,17 +403,6 @@ impl MultiStageQueryPlanner {
403403
resolved_multi_stage_dimensions,
404404
)?;
405405

406-
// Add GROUP BY to the dimension subquery itself
407-
// if a multi-stage dimension has the `add_group_by` field.
408-
let self_state =
409-
if !multi_stage_member.add_group_by_symbols().is_empty() && member.is_dimension() {
410-
let mut self_state = state.clone_state();
411-
self_state.add_dimensions(multi_stage_member.add_group_by_symbols().clone());
412-
Rc::new(self_state)
413-
} else {
414-
state.clone()
415-
};
416-
417406
let alias = format!("cte_{}", descriptions.len());
418407
MultiStageQueryDescription::new(
419408
MultiStageMember::new(
@@ -422,7 +411,7 @@ impl MultiStageQueryPlanner {
422411
is_ungrupped,
423412
false,
424413
),
425-
self_state,
414+
state.clone(),
426415
input,
427416
alias.clone(),
428417
)

0 commit comments

Comments
 (0)