Skip to content

Commit b62446e

Browse files
authored
feat(tesseract): Support multiple join paths within single query (#9047)
* feat(tesseract): Support multiple join paths within single query * Linter * Fix join key doesn't respect root
1 parent a56462e commit b62446e

File tree

21 files changed

+571
-73
lines changed

21 files changed

+571
-73
lines changed

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,10 @@ export class BaseQuery {
261261
}).filter(R.identity).map(this.newTimeDimension.bind(this));
262262
this.allFilters = this.timeDimensions.concat(this.segments).concat(this.filters);
263263

264-
this.join = this.joinGraph.buildJoin(this.allJoinHints);
264+
if (!getEnv('nativeSqlPlanner')) {
265+
// Tesseract doesn't require join to be prebuilt and there's a case where single join can't be built for multi-fact query
266+
this.join = this.joinGraph.buildJoin(this.allJoinHints);
267+
}
265268
this.cubeAliasPrefix = this.options.cubeAliasPrefix;
266269
this.preAggregationsSchemaOption = this.options.preAggregationsSchema ?? DEFAULT_PREAGGREGATIONS_SCHEMA;
267270
this.externalQueryClass = this.options.externalQueryClass;
@@ -349,7 +352,8 @@ export class BaseQuery {
349352
initUngrouped() {
350353
this.ungrouped = this.options.ungrouped;
351354
if (this.ungrouped) {
352-
if (!this.options.allowUngroupedWithoutPrimaryKey) {
355+
// this.join is not defined for Tesseract
356+
if (!this.options.allowUngroupedWithoutPrimaryKey && !getEnv('nativeSqlPlanner')) {
353357
const cubes = R.uniq([this.join.root].concat(this.join.joins.map(j => j.originalTo)));
354358
const primaryKeyNames = cubes.flatMap(c => this.primaryKeyNames(c));
355359
const missingPrimaryKeys = primaryKeyNames.filter(key => !this.dimensions.find(d => d.dimension === key));
@@ -616,7 +620,6 @@ export class BaseQuery {
616620
dimensions: this.options.dimensions,
617621
timeDimensions: this.options.timeDimensions,
618622
timezone: this.options.timezone,
619-
joinRoot: this.join.root,
620623
joinGraph: this.joinGraph,
621624
cubeEvaluator: this.cubeEvaluator,
622625
order,
@@ -3312,6 +3315,7 @@ export class BaseQuery {
33123315
always_true: '1 = 1'
33133316

33143317
},
3318+
operators: {},
33153319
quotes: {
33163320
identifiers: '"',
33173321
escape: '""'
@@ -3321,7 +3325,8 @@ export class BaseQuery {
33213325
},
33223326
join_types: {
33233327
inner: 'INNER',
3324-
left: 'LEFT'
3328+
left: 'LEFT',
3329+
full: 'FULL',
33253330
},
33263331
window_frame_types: {
33273332
rows: 'ROWS',

packages/cubejs-schema-compiler/src/adapter/BigqueryQuery.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ export class BigqueryQuery extends BaseQuery {
256256
templates.types.double = 'FLOAT64';
257257
templates.types.decimal = 'BIGDECIMAL({{ precision }},{{ scale }})';
258258
templates.types.binary = 'BYTES';
259+
templates.operators.is_not_distinct_from = 'IS NOT DISTINCT FROM';
259260
return templates;
260261
}
261262
}

packages/cubejs-schema-compiler/src/adapter/PostgresQuery.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export class PostgresQuery extends BaseQuery {
8181
templates.types.float = 'REAL';
8282
templates.types.double = 'DOUBLE PRECISION';
8383
templates.types.binary = 'BYTEA';
84+
templates.operators.is_not_distinct_from = 'IS NOT DISTINCT FROM';
8485
return templates;
8586
}
8687

packages/cubejs-schema-compiler/src/adapter/SnowflakeQuery.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export class SnowflakeQuery extends BaseQuery {
115115
templates.expressions.extract = 'EXTRACT({{ date_part }} FROM {{ expr }})';
116116
templates.expressions.interval = 'INTERVAL \'{{ interval }}\'';
117117
templates.expressions.timestamp_literal = '\'{{ value }}\'::timestamp_tz';
118+
templates.operators.is_not_distinct_from = 'IS NOT DISTINCT FROM';
118119
delete templates.types.interval;
119120
return templates;
120121
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import {
2+
getEnv,
3+
} from '@cubejs-backend/shared';
4+
import { PostgresQuery } from '../../../src/adapter/PostgresQuery';
5+
import { prepareCompiler } from '../../unit/PrepareCompiler';
6+
import { dbRunner } from './PostgresDBRunner';
7+
8+
describe('Multi-fact join', () => {
9+
jest.setTimeout(200000);
10+
11+
const { compiler, joinGraph, cubeEvaluator } = prepareCompiler(`
12+
cube(\`orders\`, {
13+
sql: \`
14+
SELECT 79 AS id, 1 AS amount, 1 AS city_id UNION ALL
15+
SELECT 80 AS id, 2 AS amount, 1 AS city_id UNION ALL
16+
SELECT 81 AS id, 3 AS amount, 1 AS city_id UNION ALL
17+
SELECT 82 AS id, 4 AS amount, 2 AS city_id UNION ALL
18+
SELECT 83 AS id, 5 AS amount, 2 AS city_id UNION ALL
19+
SELECT 84 AS id, 6 AS amount, 3 AS city_id
20+
\`,
21+
22+
joins: {
23+
city: {
24+
relationship: \`many_to_one\`,
25+
sql: \`\${orders}.city_id = \${city}.id\`,
26+
},
27+
},
28+
29+
measures: {
30+
amount: {
31+
sql: \`amount\`,
32+
type: 'sum'
33+
}
34+
},
35+
36+
dimensions: {
37+
id: {
38+
sql: \`id\`,
39+
type: \`number\`,
40+
primaryKey: true,
41+
},
42+
},
43+
});
44+
45+
cube(\`shipments\`, {
46+
sql: \`
47+
SELECT 100 AS id, 1 AS foo_id, 1 AS city_id UNION ALL
48+
SELECT 101 AS id, 2 AS foo_id, 2 AS city_id UNION ALL
49+
SELECT 102 AS id, 3 AS foo_id, 2 AS city_id UNION ALL
50+
SELECT 103 AS id, 4 AS foo_id, 2 AS city_id UNION ALL
51+
SELECT 104 AS id, 5 AS foo_id, 4 AS city_id
52+
\`,
53+
54+
joins: {
55+
city: {
56+
relationship: \`many_to_one\`,
57+
sql: \`\${shipments}.city_id = \${city}.id\`,
58+
},
59+
},
60+
61+
measures: {
62+
count: {
63+
type: \`count\`
64+
},
65+
},
66+
67+
dimensions: {
68+
id: {
69+
sql: \`id\`,
70+
type: \`number\`,
71+
primaryKey: true,
72+
shown: true
73+
},
74+
}
75+
});
76+
77+
cube(\`city\`, {
78+
sql: \`
79+
SELECT 1 AS id, 'San Francisco' AS name UNION ALL
80+
SELECT 2 AS id, 'New York City' AS name
81+
\`,
82+
83+
dimensions: {
84+
id: {
85+
sql: \`id\`,
86+
type: \`number\`,
87+
primaryKey: true,
88+
},
89+
90+
name: {
91+
sql: \`\${CUBE}.name\`,
92+
type: \`string\`,
93+
},
94+
},
95+
});
96+
`);
97+
98+
async function runQueryTest(q, expectedResult) {
99+
if (!getEnv('nativeSqlPlanner')) {
100+
return;
101+
}
102+
await compiler.compile();
103+
const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, q);
104+
105+
console.log(query.buildSqlAndParams());
106+
107+
const res = await dbRunner.testQuery(query.buildSqlAndParams());
108+
console.log(JSON.stringify(res));
109+
110+
expect(res).toEqual(
111+
expectedResult
112+
);
113+
}
114+
115+
it('two regular sub-queries', async () => runQueryTest({
116+
measures: ['orders.amount', 'shipments.count'],
117+
dimensions: [
118+
'city.name'
119+
],
120+
order: [{ id: 'city.name' }]
121+
}, [{
122+
city__name: 'New York City',
123+
orders__amount: '9',
124+
shipments__count: '3',
125+
}, {
126+
city__name: 'San Francisco',
127+
orders__amount: '6',
128+
shipments__count: '1',
129+
}, {
130+
city__name: null,
131+
orders__amount: '6',
132+
shipments__count: '1',
133+
}]));
134+
});

rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/join_item.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::any::Any;
1111
use std::marker::PhantomData;
1212
use std::rc::Rc;
1313

14-
#[derive(Serialize, Deserialize, Debug)]
14+
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)]
1515
pub struct JoinItemStatic {
1616
pub from: String,
1717
pub to: String,

rust/cubesqlplanner/cubesqlplanner/src/plan/builder/join.rs

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::plan::join::JoinType;
12
use crate::plan::{Join, JoinCondition, JoinItem, QueryPlan, Schema, Select, SingleAliasedSource};
23
use crate::planner::BaseCube;
34
use std::rc::Rc;
@@ -41,15 +42,19 @@ impl JoinBuilder {
4142
}
4243

4344
pub fn left_join_subselect(&mut self, subquery: Rc<Select>, alias: String, on: JoinCondition) {
44-
self.join_subselect(subquery, alias, on, false)
45+
self.join_subselect(subquery, alias, on, JoinType::Left)
4546
}
4647

4748
pub fn inner_join_subselect(&mut self, subquery: Rc<Select>, alias: String, on: JoinCondition) {
48-
self.join_subselect(subquery, alias, on, true)
49+
self.join_subselect(subquery, alias, on, JoinType::Inner)
50+
}
51+
52+
pub fn full_join_subselect(&mut self, subquery: Rc<Select>, alias: String, on: JoinCondition) {
53+
self.join_subselect(subquery, alias, on, JoinType::Full)
4954
}
5055

5156
pub fn left_join_cube(&mut self, cube: Rc<BaseCube>, alias: Option<String>, on: JoinCondition) {
52-
self.join_cube(cube, alias, on, false)
57+
self.join_cube(cube, alias, on, JoinType::Left)
5358
}
5459

5560
pub fn inner_join_cube(
@@ -58,7 +63,7 @@ impl JoinBuilder {
5863
alias: Option<String>,
5964
on: JoinCondition,
6065
) {
61-
self.join_cube(cube, alias, on, true)
66+
self.join_cube(cube, alias, on, JoinType::Inner)
6267
}
6368

6469
pub fn left_join_table_reference(
@@ -68,7 +73,7 @@ impl JoinBuilder {
6873
alias: Option<String>,
6974
on: JoinCondition,
7075
) {
71-
self.join_table_reference(reference, schema, alias, on, false)
76+
self.join_table_reference(reference, schema, alias, on, JoinType::Left)
7277
}
7378

7479
pub fn inner_join_table_reference(
@@ -78,7 +83,7 @@ impl JoinBuilder {
7883
alias: Option<String>,
7984
on: JoinCondition,
8085
) {
81-
self.join_table_reference(reference, schema, alias, on, true)
86+
self.join_table_reference(reference, schema, alias, on, JoinType::Inner)
8287
}
8388

8489
pub fn build(self) -> Rc<Join> {
@@ -93,22 +98,30 @@ impl JoinBuilder {
9398
subquery: Rc<Select>,
9499
alias: String,
95100
on: JoinCondition,
96-
is_inner: bool,
101+
join_type: JoinType,
97102
) {
98103
let subquery = Rc::new(QueryPlan::Select(subquery));
99104
let from = SingleAliasedSource::new_from_subquery(subquery, alias);
100-
self.joins.push(JoinItem { from, on, is_inner })
105+
self.joins.push(JoinItem {
106+
from,
107+
on,
108+
join_type,
109+
})
101110
}
102111

103112
fn join_cube(
104113
&mut self,
105114
cube: Rc<BaseCube>,
106115
alias: Option<String>,
107116
on: JoinCondition,
108-
is_inner: bool,
117+
join_type: JoinType,
109118
) {
110119
let from = SingleAliasedSource::new_from_cube(cube, alias);
111-
self.joins.push(JoinItem { from, on, is_inner })
120+
self.joins.push(JoinItem {
121+
from,
122+
on,
123+
join_type,
124+
})
112125
}
113126

114127
fn join_table_reference(
@@ -117,9 +130,13 @@ impl JoinBuilder {
117130
schema: Rc<Schema>,
118131
alias: Option<String>,
119132
on: JoinCondition,
120-
is_inner: bool,
133+
join_type: JoinType,
121134
) {
122135
let from = SingleAliasedSource::new_from_table_reference(reference, schema, alias);
123-
self.joins.push(JoinItem { from, on, is_inner })
136+
self.joins.push(JoinItem {
137+
from,
138+
on,
139+
join_type,
140+
})
124141
}
125142
}

rust/cubesqlplanner/cubesqlplanner/src/plan/filter.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::planner::filter::BaseFilter;
2+
use crate::planner::sql_evaluator::MemberSymbol;
23
use crate::planner::sql_templates::PlanSqlTemplates;
34
use crate::planner::VisitorContext;
45
use cubenativeutils::CubeError;
@@ -79,6 +80,23 @@ impl FilterItem {
7980
};
8081
Ok(res)
8182
}
83+
84+
pub fn all_member_evaluators(&self) -> Vec<Rc<MemberSymbol>> {
85+
let mut result = Vec::new();
86+
self.find_all_member_evaluators(&mut result);
87+
result
88+
}
89+
90+
pub fn find_all_member_evaluators(&self, result: &mut Vec<Rc<MemberSymbol>>) {
91+
match self {
92+
FilterItem::Group(group) => {
93+
for item in group.items.iter() {
94+
item.find_all_member_evaluators(result)
95+
}
96+
}
97+
FilterItem::Item(item) => result.push(item.member_evaluator().clone()),
98+
}
99+
}
82100
}
83101

84102
impl Filter {

rust/cubesqlplanner/cubesqlplanner/src/plan/join.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,14 +179,20 @@ impl JoinCondition {
179179
pub struct JoinItem {
180180
pub from: SingleAliasedSource,
181181
pub on: JoinCondition,
182-
pub is_inner: bool,
182+
pub join_type: JoinType,
183183
}
184184

185185
pub struct Join {
186186
pub root: SingleAliasedSource,
187187
pub joins: Vec<JoinItem>,
188188
}
189189

190+
pub enum JoinType {
191+
Inner,
192+
Left,
193+
Full,
194+
}
195+
190196
impl JoinItem {
191197
pub fn to_sql(
192198
&self,
@@ -197,7 +203,7 @@ impl JoinItem {
197203
let result = templates.join(
198204
&self.from.to_sql(templates, context)?,
199205
&on_sql,
200-
self.is_inner,
206+
&self.join_type,
201207
)?;
202208
Ok(result)
203209
}

0 commit comments

Comments
 (0)