Skip to content

Commit 14ae6a4

Browse files
authored
fix(tesseract): Fix issues with member expressions over multi stage (#9416)
1 parent ac93f96 commit 14ae6a4

File tree

16 files changed

+373
-28
lines changed

16 files changed

+373
-28
lines changed

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -697,9 +697,11 @@ export class BaseQuery {
697697
rowLimit: this.options.rowLimit ? this.options.rowLimit.toString() : null,
698698
offset: this.options.offset ? this.options.offset.toString() : null,
699699
baseTools: this,
700-
ungrouped: this.options.ungrouped
700+
ungrouped: this.options.ungrouped,
701+
exportAnnotatedSql: exportAnnotatedSql === true
701702

702703
};
704+
703705
const buildResult = nativeBuildSqlAndParams(queryParams);
704706

705707
if (buildResult.error) {

packages/cubejs-schema-compiler/test/integration/postgres/member-expression.test.ts

Lines changed: 261 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { PostgresQuery } from '../../../src/adapter/PostgresQuery';
55
import { prepareYamlCompiler } from '../../unit/PrepareCompiler';
66
import { dbRunner } from './PostgresDBRunner';
77

8-
describe('Member Expression', () => {
8+
describe('Member Expression Multistage', () => {
99
jest.setTimeout(200000);
1010

1111
const { compiler, joinGraph, cubeEvaluator } = prepareYamlCompiler(`
@@ -37,6 +37,170 @@ cubes:
3737
- name: count
3838
type: count
3939
40+
- name: orders
41+
sql: >
42+
select 10 AS ID, 'complited' AS STATUS, '2021-01-05 00:00:00'::timestamp AS CREATED_AT, 100 AS CUSTOMER_ID, 50.0 as revenue
43+
UNION ALL
44+
select 11 AS ID, 'complited' AS STATUS, '2021-05-01 00:00:00'::timestamp AS CREATED_AT, 100 AS CUSTOMER_ID, 150.0 as revenue
45+
UNION ALL
46+
select 12 AS ID, 'complited' AS STATUS, '2021-06-01 00:00:00'::timestamp AS CREATED_AT, 100 AS CUSTOMER_ID, 200.0 as revenue
47+
UNION ALL
48+
select 13 AS ID, 'complited' AS STATUS, '2022-01-04 00:00:00'::timestamp AS CREATED_AT, 100 AS CUSTOMER_ID, 10.0 as revenue
49+
UNION ALL
50+
select 14 AS ID, 'complited' AS STATUS, '2022-05-04 00:00:00'::timestamp AS CREATED_AT, 100 AS CUSTOMER_ID, 30.0 as revenue
51+
public: false
52+
53+
joins:
54+
- name: line_items
55+
sql: "{CUBE}.ID = {line_items}.order_id"
56+
relationship: many_to_one
57+
58+
- name: customers
59+
sql: "{CUBE}.CUSTOMER_ID = {customers}.ID"
60+
relationship: many_to_one
61+
62+
dimensions:
63+
- name: id
64+
sql: ID
65+
type: number
66+
primary_key: true
67+
68+
- name: status
69+
sql: STATUS
70+
type: string
71+
72+
- name: date
73+
sql: CREATED_AT
74+
type: time
75+
76+
- name: amount
77+
sql: '{line_items.total_amount}'
78+
type: number
79+
sub_query: true
80+
81+
measures:
82+
- name: count
83+
type: count
84+
85+
- name: completed_count
86+
type: count
87+
filters:
88+
- sql: "{CUBE}.STATUS = 'completed'"
89+
90+
- name: returned_count
91+
type: count
92+
filters:
93+
- sql: "{CUBE}.STATUS = 'returned'"
94+
95+
- name: return_rate
96+
type: number
97+
sql: "({returned_count} / NULLIF({completed_count}, 0)) * 100.0"
98+
description: "Percentage of returned orders out of completed, exclude just placed orders."
99+
format: percent
100+
101+
- name: total_amount
102+
sql: '{CUBE.amount}'
103+
type: sum
104+
105+
- name: revenue
106+
sql: "revenue"
107+
type: sum
108+
format: currency
109+
110+
- name: average_order_value
111+
sql: '{CUBE.amount}'
112+
type: avg
113+
114+
- name: revenue_1_y_ago
115+
sql: "{revenue}"
116+
multi_stage: true
117+
type: number
118+
format: currency
119+
time_shift:
120+
- time_dimension: date
121+
interval: 1 year
122+
type: prior
123+
- time_dimension: orders_view.date
124+
interval: 1 year
125+
type: prior
126+
127+
- name: cagr_1_y
128+
sql: "(({revenue} / {revenue_1_y_ago}) - 1)"
129+
type: number
130+
format: percent
131+
description: "Annual CAGR, year over year growth in revenue"
132+
133+
- name: line_items
134+
sql: >
135+
SELECT 10 AS ID, 10 AS PRODUCT_ID, '2021-01-01 00:00:00'::timestamp AS CREATED_AT, 10 as order_id
136+
UNION ALL
137+
SELECT 11 AS ID, 10 AS PRODUCT_ID, '2021-01-01 00:00:00'::timestamp AS CREATED_AT, 11 as order_id
138+
UNION ALL
139+
SELECT 12 AS ID, 10 AS PRODUCT_ID, '2021-01-01 00:00:00'::timestamp AS CREATED_AT, 11 as order_id
140+
UNION ALL
141+
SELECT 13 AS ID, 10 AS PRODUCT_ID, '2021-01-01 00:00:00'::timestamp AS CREATED_AT, 12 as order_id
142+
public: false
143+
144+
joins:
145+
- name: products
146+
sql: "{CUBE}.PRODUCT_ID = {products}.ID"
147+
relationship: many_to_one
148+
149+
dimensions:
150+
- name: id
151+
sql: ID
152+
type: number
153+
primary_key: true
154+
155+
- name: created_at
156+
sql: CREATED_AT
157+
type: time
158+
159+
- name: price
160+
sql: "{products.price}"
161+
type: number
162+
163+
measures:
164+
- name: count
165+
type: count
166+
167+
- name: total_amount
168+
sql: "{price}"
169+
type: sum
170+
- name: products
171+
sql: >
172+
SELECT 10 AS ID, 'Clothes' AS PRODUCT_CATEGORY, 'Shirt' AS NAME, 10 AS PRICE
173+
UNION ALL
174+
SELECT 11 AS ID, 'Clothes' AS PRODUCT_CATEGORY, 'Shirt' AS NAME, 20 AS PRICE
175+
public: false
176+
description: >
177+
Products and categories in our e-commerce store.
178+
179+
dimensions:
180+
- name: id
181+
sql: ID
182+
type: number
183+
primary_key: true
184+
185+
- name: product_category
186+
sql: PRODUCT_CATEGORY
187+
type: string
188+
189+
- name: name
190+
sql: NAME
191+
type: string
192+
193+
- name: price
194+
sql: PRICE
195+
type: number
196+
197+
measures:
198+
- name: count
199+
type: count
200+
201+
202+
203+
40204
views:
41205
- name: customers_view
42206
@@ -47,6 +211,29 @@ views:
47211
48212
- city
49213
214+
- name: orders_view
215+
cubes:
216+
- join_path: orders
217+
includes:
218+
- count
219+
- date
220+
- revenue
221+
- cagr_1_y
222+
- return_rate
223+
224+
- join_path: line_items.products
225+
prefix: true
226+
includes:
227+
- product_category
228+
229+
- join_path: orders.customers
230+
prefix: true
231+
includes:
232+
- city
233+
- count
234+
- id
235+
236+
50237
`);
51238

52239
async function runQueryTest(q, expectedResult) {
@@ -124,4 +311,77 @@ views:
124311
},
125312

126313
[{ count: 1, city: 'New York', cubejoinfield: 'NULL' }, { count: 1, city: 'New York', cubejoinfield: 'NULL' }]));
314+
if (getEnv('nativeSqlPlanner')) {
315+
it('member expression multi stage', async () => runQueryTest({
316+
measures: [
317+
{
318+
// eslint-disable-next-line no-new-func
319+
expression: new Function(
320+
'orders',
321+
// eslint-disable-next-line no-template-curly-in-string
322+
'return `${orders.cagr_1_y}`'
323+
),
324+
// eslint-disable-next-line no-template-curly-in-string
325+
definition: '${orders.cagr_1_y}',
326+
expressionName: 'orders__cagr_2023',
327+
cubeName: 'orders',
328+
},
329+
],
330+
timeDimensions: [
331+
{
332+
dimension: 'orders.date',
333+
dateRange: ['2022-01-01', '2022-10-31'],
334+
},
335+
],
336+
timezone: 'America/Los_Angeles'
337+
},
338+
339+
[{ orders__cagr_2023: '-0.90000000000000000000' }]));
340+
} else {
341+
it.skip('member expression multi stage', () => {
342+
// Skipping because it works only in Tesseract
343+
});
344+
}
345+
346+
if (getEnv('nativeSqlPlanner')) {
347+
it('member expression multi stage with time dimension segment', async () => runQueryTest({
348+
measures: [
349+
{
350+
// eslint-disable-next-line no-new-func
351+
expression: new Function(
352+
'orders',
353+
// eslint-disable-next-line no-template-curly-in-string
354+
'return `${orders.cagr_1_y}`'
355+
),
356+
// eslint-disable-next-line no-template-curly-in-string
357+
definition: '${orders.cagr_1_y}',
358+
expressionName: 'orders__cagr_2023',
359+
cubeName: 'orders',
360+
},
361+
],
362+
segments: [
363+
{
364+
cubeName: 'orders',
365+
name: 'orders_date____c',
366+
expressionName: 'orders_date____c',
367+
// eslint-disable-next-line no-new-func
368+
expression: new Function(
369+
'orders',
370+
// eslint-disable-next-line no-template-curly-in-string
371+
'return `((${orders.date} >= CAST(\'2022-01-01\' AS TIMESTAMP)) AND (${orders.date} < CAST(\'2022-10-31\' AS TIMESTAMP)))`'
372+
),
373+
// eslint-disable-next-line no-template-curly-in-string
374+
definition: '{"cube_name":"orders","alias":"orders_date____c","cube_params":["orders"],"expr":"((${orders.date} >= CAST($0$ AS TIMESTAMP)) AND (${orders.date} < CAST($1$ AS TIMESTAMP)))","grouping_set":null}',
375+
}
376+
],
377+
378+
timezone: 'America/Los_Angeles'
379+
},
380+
381+
[{ orders__cagr_2023: '-0.90000000000000000000' }]));
382+
} else {
383+
it.skip('member expression multi stage with time dimension segment', () => {
384+
// Skipping because it works only in Tesseract
385+
});
386+
}
127387
});

rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_query_options.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ pub struct BaseQueryOptionsStatic {
5959
pub row_limit: Option<String>,
6060
pub offset: Option<String>,
6161
pub ungrouped: Option<bool>,
62+
#[serde(rename = "exportAnnotatedSql")]
63+
pub export_annotated_sql: bool,
6264
}
6365

6466
#[nativebridge::native_bridge(BaseQueryOptionsStatic)]

rust/cubesqlplanner/cubesqlplanner/src/planner/base_dimension.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,23 @@ impl BaseDimension {
8181
default_alias,
8282
}))
8383
}
84+
MemberSymbol::MemberExpression(expression_symbol) => {
85+
let full_name = expression_symbol.full_name();
86+
let cube_name = expression_symbol.cube_name().clone();
87+
let name = expression_symbol.name().clone();
88+
let member_expression_definition = expression_symbol.definition().clone();
89+
let default_alias = PlanSqlTemplates::alias_name(&name);
90+
Some(Rc::new(Self {
91+
dimension: full_name,
92+
query_tools: query_tools.clone(),
93+
member_evaluator: evaluation_node,
94+
definition: None,
95+
cube_name,
96+
name,
97+
member_expression_definition,
98+
default_alias,
99+
}))
100+
}
84101
_ => None,
85102
};
86103
Ok(result)

rust/cubesqlplanner/cubesqlplanner/src/planner/base_measure.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ impl BaseMember for BaseMeasure {
113113
}
114114

115115
fn full_name(&self) -> String {
116-
format!("{}.{}", self.cube_name, self.name)
116+
self.member_evaluator.full_name()
117117
}
118118

119119
fn cube_name(&self) -> &String {
@@ -151,6 +151,24 @@ impl BaseMeasure {
151151
default_alias,
152152
}))
153153
}
154+
MemberSymbol::MemberExpression(expression_symbol) => {
155+
let full_name = expression_symbol.full_name();
156+
let cube_name = expression_symbol.cube_name().clone();
157+
let name = expression_symbol.name().clone();
158+
let member_expression_definition = expression_symbol.definition().clone();
159+
let default_alias = PlanSqlTemplates::alias_name(&name);
160+
Some(Rc::new(Self {
161+
measure: full_name,
162+
query_tools: query_tools.clone(),
163+
member_evaluator: evaluation_node,
164+
definition: None,
165+
cube_name,
166+
name,
167+
member_expression_definition,
168+
default_alias,
169+
time_shifts: vec![],
170+
}))
171+
}
154172
_ => None,
155173
};
156174
Ok(res)

rust/cubesqlplanner/cubesqlplanner/src/planner/base_query.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ impl<IT: InnerTypes> BaseQuery<IT> {
2727
options.base_tools()?,
2828
options.join_graph()?,
2929
options.static_data().timezone.clone(),
30+
options.static_data().export_annotated_sql,
3031
)?;
3132

3233
let request = QueryProperties::try_new(query_tools.clone(), options)?;

0 commit comments

Comments
 (0)