Skip to content

Commit c0be00c

Browse files
authored
feat(cubesql): Data source per member (#9537)
This should fix SQL push-down targeting views. Before this, views would always have a default datasource, and could use an incorrect dialect in multi-datasource deployments. Now, the JS side would pass the "member -> data source" mapping to SQL API, and this would reflect each view member properly. Using members as keys is necessary for multi-data-source views (even if they are not supported right now in SQL pushdown, they are valid as a data model, and can be used with rollupJoin pre-aggregations)
1 parent 8cc6346 commit c0be00c

File tree

49 files changed

+3571
-2999
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+3571
-2999
lines changed

packages/cubejs-api-gateway/src/gateway.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,15 +1486,14 @@ class ApiGateway {
14861486
const query = {
14871487
requestId: context.requestId,
14881488
};
1489-
const cubeNameToDataSource = await compilerApi.cubeNameToDataSource(query);
1489+
const memberToDataSource: Record<string, string> = await compilerApi.memberToDataSource(query);
14901490

1491-
let dataSources = Object.keys(cubeNameToDataSource).map(c => cubeNameToDataSource[c]);
1492-
dataSources = [...new Set(dataSources)];
1491+
const dataSources = new Set(Object.values(memberToDataSource));
14931492
const dataSourceToSqlGenerator = (await Promise.all(
1494-
dataSources.map(async dataSource => ({ [dataSource]: (await compilerApi.getSqlGenerator(query, dataSource)).sqlGenerator }))
1493+
[...dataSources].map(async dataSource => ({ [dataSource]: (await compilerApi.getSqlGenerator(query, dataSource)).sqlGenerator }))
14951494
)).reduce((a, b) => ({ ...a, ...b }), {});
14961495

1497-
res({ cubeNameToDataSource, dataSourceToSqlGenerator });
1496+
res({ memberToDataSource, dataSourceToSqlGenerator });
14981497
} catch (e: any) {
14991498
this.handleError({
15001499
e, context, res, requestStarted

packages/cubejs-backend-native/Cargo.lock

Lines changed: 18 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cubejs-backend-native/src/transport.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ impl TransportService for NodeBridgeTransport {
142142

143143
let channel = self.channel.clone();
144144

145-
let (cube_to_data_source, data_source_to_sql_generator) =
145+
let (member_to_data_source, data_source_to_sql_generator) =
146146
call_raw_js_with_channel_as_callback(
147147
self.channel.clone(),
148148
self.sql_generators.clone(),
@@ -152,14 +152,14 @@ impl TransportService for NodeBridgeTransport {
152152
let obj = v
153153
.downcast::<JsObject, _>(cx)
154154
.map_err(|e| CubeError::user(e.to_string()))?;
155-
let cube_to_data_source_obj = obj
156-
.get::<JsObject, _, _>(cx, "cubeNameToDataSource")
157-
.map_cube_err("Can't cast cubeNameToDataSource to object")?;
158155

159-
let cube_to_data_source =
160-
key_to_values(cx, cube_to_data_source_obj, |cx, v| {
156+
let member_to_data_source_obj = obj
157+
.get::<JsObject, _, _>(cx, "memberToDataSource")
158+
.map_cube_err("Can't cast memberToDataSource to object")?;
159+
let member_to_data_source =
160+
key_to_values(cx, member_to_data_source_obj, |cx, v| {
161161
let res = v.downcast::<JsString, _>(cx).map_cube_err(
162-
"Can't cast value to string in cube_to_data_source",
162+
"Can't cast value to string in member_to_data_source",
163163
)?;
164164
Ok(res.value(cx))
165165
})?;
@@ -183,7 +183,7 @@ impl TransportService for NodeBridgeTransport {
183183
Ok(res)
184184
})?;
185185

186-
Ok((cube_to_data_source, data_source_to_sql_generator))
186+
Ok((member_to_data_source, data_source_to_sql_generator))
187187
}),
188188
)
189189
.await?;
@@ -208,7 +208,7 @@ impl TransportService for NodeBridgeTransport {
208208
})?;
209209
Ok(Arc::new(MetaContext::new(
210210
response.cubes.unwrap_or_default(),
211-
cube_to_data_source,
211+
member_to_data_source,
212212
data_source_to_sql_generator,
213213
compiler_id,
214214
)))

packages/cubejs-backend-native/test/server.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ const meta_fixture = require('./meta');
9898

9999
return {
100100
cubeNameToDataSource: {},
101+
memberToDataSource: {},
101102
dataSourceToSqlGenerator: {},
102103
};
103104
};

packages/cubejs-backend-native/test/sql.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ function interfaceMethods() {
100100

101101
return {
102102
cubeNameToDataSource: {},
103+
memberToDataSource: {},
103104
dataSourceToSqlGenerator: {},
104105
};
105106
}),

packages/cubejs-server-core/src/core/CompilerApi.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,34 @@ export class CompilerApi {
602602
).reduce((a, b) => ({ ...a, ...b }), {});
603603
}
604604

605+
async memberToDataSource(query) {
606+
const { cubeEvaluator } = await this.getCompilers({ requestId: query.requestId });
607+
608+
const entries = cubeEvaluator
609+
.cubeNames()
610+
.flatMap(cube => {
611+
const cubeDef = cubeEvaluator.cubeFromPath(cube);
612+
if (cubeDef.isView) {
613+
const viewName = cubeDef.name;
614+
return cubeDef.includedMembers.map(included => {
615+
const memberName = `${viewName}.${included.name}`;
616+
const refCubeDef = cubeEvaluator.cubeFromPath(included.memberPath);
617+
const dataSource = refCubeDef.dataSource ?? 'default';
618+
return [memberName, dataSource];
619+
});
620+
} else {
621+
const cubeName = cubeDef.name;
622+
const dataSource = cubeDef.dataSource ?? 'default';
623+
return [
624+
...Object.keys(cubeDef.dimensions),
625+
...Object.keys(cubeDef.measures),
626+
...Object.keys(cubeDef.segments),
627+
].map(mem => [`${cubeName}.${mem}`, dataSource]);
628+
}
629+
});
630+
return Object.fromEntries(entries);
631+
}
632+
605633
async dataSources(orchestratorApi, query) {
606634
const cubeNameToDataSource = await this.cubeNameToDataSource(query || { requestId: `datasources-${uuidv4()}` });
607635

packages/cubejs-testing/birdbox-fixtures/multidb/schema/Products.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,10 @@ cube(`Products`, {
5050

5151
dataSource: 'products',
5252
});
53+
54+
view(`ProductsView`, {
55+
cubes: [{
56+
joinPath: Products,
57+
includes: `*`,
58+
}]
59+
});

packages/cubejs-testing/birdbox-fixtures/multidb/schema/Suppliers.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,10 @@ cube(`Suppliers`, {
3232

3333
dataSource: 'suppliers',
3434
});
35+
36+
view(`SuppliersView`, {
37+
cubes: [{
38+
joinPath: Suppliers,
39+
includes: `*`,
40+
}]
41+
});

packages/cubejs-testing/test/__snapshots__/smoke-multidb.test.ts.snap

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ Array [
88
]
99
`;
1010

11+
exports[`multidb SQL pushdown queries to different data sources: ProductsView 1`] = `
12+
Array [
13+
Object {
14+
"name": "apples",
15+
},
16+
]
17+
`;
18+
1119
exports[`multidb SQL pushdown queries to different data sources: Suppliers 1`] = `
1220
Array [
1321
Object {
@@ -16,6 +24,14 @@ Array [
1624
]
1725
`;
1826

27+
exports[`multidb SQL pushdown queries to different data sources: SuppliersView 1`] = `
28+
Array [
29+
Object {
30+
"company": "Fruits Inc",
31+
},
32+
]
33+
`;
34+
1935
exports[`multidb query: query 1`] = `
2036
Array [
2137
Object {

packages/cubejs-testing/test/smoke-multidb.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ describe('multidb', () => {
120120
expect(res.rows).toMatchSnapshot();
121121
});
122122

123+
test('SQL pushdown queries to different data sources: ProductsView', async () => {
124+
const res = await connection.query(`
125+
SELECT
126+
name
127+
FROM
128+
ProductsView
129+
WHERE
130+
LOWER(name) = 'apples'
131+
GROUP BY
132+
1
133+
`);
134+
expect(res.rows).toMatchSnapshot();
135+
});
136+
123137
test('SQL pushdown queries to different data sources: Suppliers', async () => {
124138
const res = await connection.query(`
125139
SELECT
@@ -133,4 +147,18 @@ describe('multidb', () => {
133147
`);
134148
expect(res.rows).toMatchSnapshot();
135149
});
150+
151+
test('SQL pushdown queries to different data sources: SuppliersView', async () => {
152+
const res = await connection.query(`
153+
SELECT
154+
company
155+
FROM
156+
SuppliersView
157+
WHERE
158+
LOWER(company) = 'fruits inc'
159+
GROUP BY
160+
1
161+
`);
162+
expect(res.rows).toMatchSnapshot();
163+
});
136164
});

0 commit comments

Comments
 (0)