Skip to content

Commit 6bbbf11

Browse files
committed
measure switch
1 parent 9b5dd5b commit 6bbbf11

File tree

12 files changed

+232
-107
lines changed

12 files changed

+232
-107
lines changed

packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -588,13 +588,50 @@ const timeShiftItemOptional = Joi.object({
588588
.xor('name', 'interval')
589589
.and('interval', 'type');
590590

591+
const CaseSchema = Joi.object().keys({
592+
when: Joi.array().items(Joi.object().keys({
593+
sql: Joi.func().required(),
594+
label: Joi.alternatives([
595+
Joi.string(),
596+
Joi.object().keys({
597+
sql: Joi.func().required()
598+
})
599+
])
600+
})),
601+
else: Joi.object().keys({
602+
label: Joi.alternatives([
603+
Joi.string(),
604+
Joi.object().keys({
605+
sql: Joi.func().required()
606+
})
607+
])
608+
})
609+
}).required();
610+
611+
const SwitchCaseSchema = Joi.object().keys({
612+
switch: Joi.func().required(),
613+
when: Joi.array().items(Joi.object().keys({
614+
value: Joi.string().required(),
615+
sql: Joi.func().required()
616+
})),
617+
else: Joi.object().keys({
618+
sql: Joi.func().required()
619+
})
620+
}).required();
621+
622+
const CaseVariants = Joi.alternatives().try(
623+
CaseSchema,
624+
SwitchCaseSchema
625+
);
626+
591627
const MeasuresSchema = Joi.object().pattern(identifierRegex, Joi.alternatives().conditional(Joi.ref('.multiStage'), [
592628
{
593629
is: true,
594630
then: inherit(BaseMeasure, {
595631
multiStage: Joi.boolean().strict(),
596632
type: multiStageMeasureType.required(),
597633
sql: Joi.func(), // TODO .required(),
634+
case: CaseVariants,
598635
groupBy: Joi.func(),
599636
reduceBy: Joi.func(),
600637
addGroupBy: Joi.func(),
@@ -657,42 +694,6 @@ const SwitchDimension = Joi.object({
657694
values: Joi.array().items(Joi.string()).min(1).required()
658695
});
659696

660-
const CaseSchema = Joi.object().keys({
661-
when: Joi.array().items(Joi.object().keys({
662-
sql: Joi.func().required(),
663-
label: Joi.alternatives([
664-
Joi.string(),
665-
Joi.object().keys({
666-
sql: Joi.func().required()
667-
})
668-
])
669-
})),
670-
else: Joi.object().keys({
671-
label: Joi.alternatives([
672-
Joi.string(),
673-
Joi.object().keys({
674-
sql: Joi.func().required()
675-
})
676-
])
677-
})
678-
}).required();
679-
680-
const SwitchCaseSchema = Joi.object().keys({
681-
switch: Joi.func().required(),
682-
when: Joi.array().items(Joi.object().keys({
683-
value: Joi.string().required(),
684-
sql: Joi.func().required()
685-
})),
686-
else: Joi.object().keys({
687-
sql: Joi.func().required()
688-
})
689-
}).required();
690-
691-
const CaseVariants = Joi.alternatives().try(
692-
CaseSchema,
693-
SwitchCaseSchema
694-
);
695-
696697
const DimensionsSchema = Joi.object().pattern(identifierRegex, Joi.alternatives().conditional(Joi.ref('.type'), {
697698
is: 'switch',
698699
then: SwitchDimension,

packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const transpiledFieldsPatterns: Array<RegExp> = [
1919
/^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(orderBy|order_by)\.[0-9]+\.sql$/,
2020
/^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(timeShift|time_shift)\.[0-9]+\.(timeDimension|time_dimension)$/,
2121
/^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(reduceBy|reduce_by|groupBy|group_by|addGroupBy|add_group_by)$/,
22+
/^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.case\.switch$/,
2223
/^dimensions\.[_a-zA-Z][_a-zA-Z0-9]*\.(reduceBy|reduce_by|groupBy|group_by|addGroupBy|add_group_by)$/,
2324
/^dimensions\.[_a-zA-Z][_a-zA-Z0-9]*\.case\.switch$/,
2425
/^(preAggregations|pre_aggregations)\.[_a-zA-Z][_a-zA-Z0-9]*\.indexes\.[_a-zA-Z][_a-zA-Z0-9]*\.columns$/,

packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ describe('Calc-Groups', () => {
1111
cubes:
1212
- name: orders
1313
sql: >
14-
SELECT 9 as ID, 'completed' as STATUS, '2022-01-12T20:00:00.000Z'::timestamptz as CREATED_AT
14+
SELECT 9 as ID, 'completed' as STATUS, 100.0 as amount_usd, 97.4 as amount_eur, 80.6 as amount_gbp, '2022-01-12T20:00:00.000Z'::timestamptz as CREATED_AT
1515
union all
16-
SELECT 10 as ID, 'completed' as STATUS, '2023-01-12T20:00:00.000Z'::timestamptz as CREATED_AT
16+
SELECT 10 as ID, 'completed' as STATUS, 10.0 as amount_usd, 9.74 as amount_eur, 8.06 as amount_gbp, '2023-01-12T20:00:00.000Z'::timestamptz as CREATED_AT
1717
union all
18-
SELECT 11 as ID, 'completed' as STATUS, '2024-01-14T20:00:00.000Z'::timestamptz as CREATED_AT
18+
SELECT 11 as ID, 'completed' as STATUS, 1000.0 as amount_usd, 974 as amount_eur, 806 as amount_gbp,'2024-01-14T20:00:00.000Z'::timestamptz as CREATED_AT
1919
union all
20-
SELECT 12 as ID, 'completed' as STATUS, '2024-02-14T20:00:00.000Z'::timestamptz as CREATED_AT
20+
SELECT 12 as ID, 'completed' as STATUS, 30.0 as amount_usd, 28 as amount_eur, 22 as amount_gbp,'2024-02-14T20:00:00.000Z'::timestamptz as CREATED_AT
2121
union all
22-
SELECT 13 as ID, 'completed' as STATUS, '2025-03-14T20:00:00.000Z'::timestamptz as CREATED_AT
22+
SELECT 13 as ID, 'completed' as STATUS, 40.0 as amount_usd, 38 as amount_eur, 33 as amount_gbp, '2025-03-14T20:00:00.000Z'::timestamptz as CREATED_AT
2323
joins:
2424
- name: line_items
2525
sql: "{CUBE}.ID = {line_items}.order_id"
@@ -78,6 +78,31 @@ cubes:
7878
filters:
7979
- sql: "{CUBE}.STATUS = 'completed'"
8080
81+
- name: amount_usd
82+
type: sum
83+
sql: amount_usd
84+
85+
- name: amount_eur
86+
type: sum
87+
sql: amount_eur
88+
89+
- name: amount_gbp
90+
type: sum
91+
sql: amount_gbp
92+
93+
- name: amount_in_currency
94+
type: number
95+
multi_stage: true
96+
case:
97+
switch: "{CUBE.currency}"
98+
when:
99+
- value: USD
100+
sql: "{CUBE.amount_usd}"
101+
- value: EUR
102+
sql: "{CUBE.amount_eur}"
103+
else:
104+
sql: "{CUBE.amount_gbp}"
105+
81106
- name: returned_count
82107
type: count
83108
filters:
@@ -516,6 +541,99 @@ views:
516541
}
517542
],
518543
{ joinGraph, cubeEvaluator, compiler }));
544+
545+
it('measure switch cross join', async () => dbRunner.runQueryTest({
546+
dimensions: ['orders.currency'],
547+
measures: ['orders.amount_usd', 'orders.amount_in_currency'],
548+
timeDimensions: [
549+
{
550+
dimension: 'orders.date',
551+
granularity: 'year',
552+
dateRange: ['2024-01-01', '2026-01-01']
553+
}
554+
],
555+
timezone: 'UTC',
556+
order: [{
557+
id: 'orders.date'
558+
}, {
559+
id: 'orders.currency'
560+
},
561+
],
562+
}, [
563+
{
564+
orders__currency: 'EUR',
565+
orders__date_year: '2024-01-01T00:00:00.000Z',
566+
orders__amount_usd: '1030.0',
567+
orders__amount_in_currency: '1002'
568+
},
569+
{
570+
orders__currency: 'GBP',
571+
orders__date_year: '2024-01-01T00:00:00.000Z',
572+
orders__amount_usd: '1030.0',
573+
orders__amount_in_currency: '828'
574+
},
575+
{
576+
orders__currency: 'USD',
577+
orders__date_year: '2024-01-01T00:00:00.000Z',
578+
orders__amount_usd: '1030.0',
579+
orders__amount_in_currency: '1030.0'
580+
},
581+
{
582+
orders__currency: 'EUR',
583+
orders__date_year: '2025-01-01T00:00:00.000Z',
584+
orders__amount_usd: '40.0',
585+
orders__amount_in_currency: '38'
586+
},
587+
{
588+
orders__currency: 'GBP',
589+
orders__date_year: '2025-01-01T00:00:00.000Z',
590+
orders__amount_usd: '40.0',
591+
orders__amount_in_currency: '33'
592+
},
593+
{
594+
orders__currency: 'USD',
595+
orders__date_year: '2025-01-01T00:00:00.000Z',
596+
orders__amount_usd: '40.0',
597+
orders__amount_in_currency: '40.0'
598+
}
599+
],
600+
{ joinGraph, cubeEvaluator, compiler }));
601+
602+
it('measure switch with filter', async () => dbRunner.runQueryTest({
603+
dimensions: ['orders.currency'],
604+
measures: ['orders.amount_usd', 'orders.amount_in_currency'],
605+
timeDimensions: [
606+
{
607+
dimension: 'orders.date',
608+
granularity: 'year',
609+
dateRange: ['2024-01-01', '2026-01-01']
610+
}
611+
],
612+
filters: [
613+
{ dimension: 'orders.currency', operator: 'equals', values: ['EUR'] }
614+
],
615+
timezone: 'UTC',
616+
order: [{
617+
id: 'orders.date'
618+
}, {
619+
id: 'orders.currency'
620+
},
621+
],
622+
}, [
623+
{
624+
orders__currency: 'EUR',
625+
orders__date_year: '2024-01-01T00:00:00.000Z',
626+
orders__amount_usd: '1030.0',
627+
orders__amount_in_currency: '1002'
628+
},
629+
{
630+
orders__currency: 'EUR',
631+
orders__date_year: '2025-01-01T00:00:00.000Z',
632+
orders__amount_usd: '40.0',
633+
orders__amount_in_currency: '38'
634+
},
635+
],
636+
{ joinGraph, cubeEvaluator, compiler }));
519637
} else {
520638
// This test is working only in tesseract
521639
test.skip('calc groups testst', () => { expect(1).toBe(1); });

rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/measure_definition.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use super::case_variant::CaseVariant;
12
use super::cube_definition::{CubeDefinition, NativeCubeDefinition};
23
use super::member_order_by::{MemberOrderBy, NativeMemberOrderBy};
34
use super::member_sql::{MemberSql, NativeMemberSql};
@@ -60,6 +61,9 @@ pub trait MeasureDefinition {
6061

6162
fn cube(&self) -> Result<Rc<dyn CubeDefinition>, CubeError>;
6263

64+
#[nbridge(field, optional)]
65+
fn case(&self) -> Result<Option<CaseVariant>, CubeError>;
66+
6367
#[nbridge(field, optional, vec)]
6468
fn filters(&self) -> Result<Option<Vec<Rc<dyn StructWithSqlMember>>>, CubeError>;
6569

rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/mod.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,5 @@ pub use dependecy::{CubeDepProperty, Dependency};
1313
pub use references_builder::ReferencesBuilder;
1414
pub use sql_call::SqlCall;
1515
pub use sql_visitor::SqlEvaluatorVisitor;
16-
pub use symbols::{
17-
CubeNameSymbol, CubeNameSymbolFactory, CubeTableSymbol, CubeTableSymbolFactory,
18-
DimensionCaseDefinition, DimensionCaseWhenItem, DimensionSymbol, DimensionSymbolFactory,
19-
DimensionTimeShift, DimenstionCaseLabel, MeasureSymbol, MeasureSymbolFactory,
20-
MeasureTimeShifts, MemberExpressionExpression, MemberExpressionSymbol, MemberSymbol,
21-
SymbolFactory, TimeDimensionSymbol,
22-
};
16+
pub use symbols::*;
2317
pub use visitor::TraversalVisitor;

rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,6 @@ impl SqlCall {
128128
}
129129
}
130130

131-
pub fn get_dependent_cubes(&self) -> Vec<String> {
132-
let mut deps = Vec::new();
133-
self.extract_cube_deps(&mut deps);
134-
deps
135-
}
136-
137131
pub fn extract_cube_deps(&self, result: &mut Vec<String>) {
138132
for dep in self.deps.iter() {
139133
match dep {
Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ use cubenativeutils::CubeError;
1010
use std::any::Any;
1111
use std::rc::Rc;
1212

13-
pub struct CaseDimensionSqlNode {
13+
pub struct CaseSqlNode {
1414
input: Rc<dyn SqlNode>,
1515
}
1616

17-
impl CaseDimensionSqlNode {
17+
impl CaseSqlNode {
1818
pub fn new(input: Rc<dyn SqlNode>) -> Rc<Self> {
1919
Rc::new(Self { input })
2020
}
@@ -101,7 +101,7 @@ impl CaseDimensionSqlNode {
101101
}
102102
}
103103

104-
impl SqlNode for CaseDimensionSqlNode {
104+
impl SqlNode for CaseSqlNode {
105105
fn to_sql(
106106
&self,
107107
visitor: &SqlEvaluatorVisitor,
@@ -135,9 +135,33 @@ impl SqlNode for CaseDimensionSqlNode {
135135
)?
136136
}
137137
}
138+
MemberSymbol::Measure(ev) => {
139+
if let Some(case) = ev.case() {
140+
match case {
141+
Case::Case(case) => {
142+
self.case_to_sql(visitor, case, query_tools, node_processor, templates)?
143+
}
144+
Case::CaseSwitch(case) => self.case_switch_to_sql(
145+
visitor,
146+
case,
147+
query_tools,
148+
node_processor,
149+
templates,
150+
)?,
151+
}
152+
} else {
153+
self.input.to_sql(
154+
visitor,
155+
node,
156+
query_tools.clone(),
157+
node_processor.clone(),
158+
templates,
159+
)?
160+
}
161+
}
138162
_ => {
139163
return Err(CubeError::internal(format!(
140-
"CaseDimension node processor called for wrong node",
164+
"Case node processor called for wrong node",
141165
)));
142166
}
143167
};

rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/factory.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::{
2-
AutoPrefixSqlNode, CaseDimensionSqlNode, EvaluateSqlNode, FinalMeasureSqlNode,
2+
AutoPrefixSqlNode, CaseSqlNode, EvaluateSqlNode, FinalMeasureSqlNode,
33
FinalPreAggregationMeasureSqlNode, GeoDimensionSqlNode, MeasureFilterSqlNode,
44
MultiStageRankNode, MultiStageWindowNode, OriginalSqlPreAggregationSqlNode,
55
RenderReferencesSqlNode, RollingWindowNode, RootSqlNode, SqlNode, TimeDimensionNode,
@@ -185,9 +185,9 @@ impl SqlNodesFactory {
185185
);
186186

187187
let measure_filter_processor = MeasureFilterSqlNode::new(auto_prefix_processor.clone());
188+
let measure_processor = CaseSqlNode::new(measure_filter_processor.clone());
188189

189-
let measure_processor =
190-
self.add_ungrouped_measure_reference_if_needed(measure_filter_processor.clone());
190+
let measure_processor = self.add_ungrouped_measure_reference_if_needed(measure_processor);
191191
let measure_processor = self.final_measure_node_processor(measure_processor);
192192
let measure_processor = self
193193
.add_multi_stage_window_if_needed(measure_processor, measure_filter_processor.clone());
@@ -279,7 +279,7 @@ impl SqlNodesFactory {
279279
RenderReferencesSqlNode::new(input, self.pre_aggregation_dimensions_references.clone())
280280
} else {
281281
let input: Rc<dyn SqlNode> = GeoDimensionSqlNode::new(input);
282-
let input: Rc<dyn SqlNode> = CaseDimensionSqlNode::new(input);
282+
let input: Rc<dyn SqlNode> = CaseSqlNode::new(input);
283283
input
284284
};
285285
let input: Rc<dyn SqlNode> =

rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
pub mod auto_prefix;
22
pub mod calendar_time_shift;
3-
pub mod case_dimension;
3+
pub mod case;
44
pub mod cube_calc_groups;
55
pub mod evaluate_sql;
66
pub mod factory;
@@ -21,7 +21,7 @@ pub mod ungroupped_measure;
2121
pub mod ungroupped_query_final_measure;
2222

2323
pub use auto_prefix::AutoPrefixSqlNode;
24-
pub use case_dimension::CaseDimensionSqlNode;
24+
pub use case::CaseSqlNode;
2525
pub use cube_calc_groups::CubeCalcGroupsSqlNode;
2626
pub use evaluate_sql::EvaluateSqlNode;
2727
pub use factory::SqlNodesFactory;

0 commit comments

Comments
 (0)