Skip to content

Commit 1e722dd

Browse files
authored
feat: Split view support (#7308)
1 parent 3dad82a commit 1e722dd

File tree

3 files changed

+69
-14
lines changed

3 files changed

+69
-14
lines changed

packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@ export class CubeSymbols {
3737
R.sortBy(c => !!c.isView),
3838
)(cubes);
3939
for (const cube of sortedByDependency) {
40-
this.symbols[cube.name] = this.transform(cube.name, errorReporter.inContext(`${cube.name} cube`));
40+
const splitViews = {};
41+
this.symbols[cube.name] = this.transform(cube.name, errorReporter.inContext(`${cube.name} cube`), splitViews);
42+
for (const viewName of Object.keys(splitViews)) {
43+
// TODO can we define it when cubeList is defined?
44+
this.cubeList.push(splitViews[viewName]);
45+
this.symbols[viewName] = splitViews[viewName];
46+
}
4147
}
4248
}
4349

@@ -110,7 +116,7 @@ export class CubeSymbols {
110116
return cubeObject;
111117
}
112118

113-
transform(cubeName, errorReporter) {
119+
transform(cubeName, errorReporter, splitViews) {
114120
const cube = this.getCubeDefinition(cubeName);
115121
const duplicateNames = R.compose(
116122
R.map(nameToDefinitions => nameToDefinitions[0]),
@@ -138,7 +144,7 @@ export class CubeSymbols {
138144
}
139145

140146
if (this.evaluateViews) {
141-
this.prepareIncludes(cube, errorReporter);
147+
this.prepareIncludes(cube, errorReporter, splitViews);
142148
}
143149

144150
return Object.assign(
@@ -209,32 +215,36 @@ export class CubeSymbols {
209215
/**
210216
* @protected
211217
*/
212-
prepareIncludes(cube, errorReporter) {
218+
prepareIncludes(cube, errorReporter, splitViews) {
213219
if (!cube.includes && !cube.cubes) {
214220
return;
215221
}
216222
const types = ['measures', 'dimensions', 'segments'];
217223
for (const type of types) {
218-
const cubeIncludes = cube.cubes && this.membersFromCubes(cube.cubes, type, errorReporter) || [];
224+
const cubeIncludes = cube.cubes && this.membersFromCubes(cube, cube.cubes, type, errorReporter, splitViews) || [];
219225
const includes = cube.includes && this.membersFromIncludeExclude(cube.includes, cube.name, type) || [];
220226
const excludes = cube.excludes && this.membersFromIncludeExclude(cube.excludes, cube.name, type) || [];
221227
// cube includes will take precedence in case of member clash
222228
const finalIncludes = this.diffByMember(this.diffByMember(includes, cubeIncludes).concat(cubeIncludes), excludes);
223229
const includeMembers = this.generateIncludeMembers(finalIncludes, cube.name, type);
224-
for (const [memberName, memberDefinition] of includeMembers) {
225-
if (cube[type]?.[memberName]) {
226-
errorReporter.error(`Included member '${memberName}' conflicts with existing member of '${cube.name}'. Please consider excluding this member.`);
227-
} else {
228-
cube[type][memberName] = memberDefinition;
229-
}
230+
this.applyIncludeMembers(includeMembers, cube, type, errorReporter);
231+
}
232+
}
233+
234+
applyIncludeMembers(includeMembers, cube, type, errorReporter) {
235+
for (const [memberName, memberDefinition] of includeMembers) {
236+
if (cube[type]?.[memberName]) {
237+
errorReporter.error(`Included member '${memberName}' conflicts with existing member of '${cube.name}'. Please consider excluding this member.`);
238+
} else {
239+
cube[type][memberName] = memberDefinition;
230240
}
231241
}
232242
}
233243

234244
/**
235245
* @protected
236246
*/
237-
membersFromCubes(cubes, type, errorReporter) {
247+
membersFromCubes(parentCube, cubes, type, errorReporter, splitViews) {
238248
return R.unnest(cubes.map(cubeInclude => {
239249
const fullPath = this.evaluateReferences(null, cubeInclude.joinPath, { collectJoinHints: true });
240250
const split = fullPath.split('.');
@@ -277,7 +287,29 @@ export class CubeSymbols {
277287
member: `${cubeReference}.${exclude}`
278288
} : undefined;
279289
});
280-
return this.diffByMember(includes.filter(Boolean), excludes.filter(Boolean));
290+
291+
const finalIncludes = this.diffByMember(includes.filter(Boolean), excludes.filter(Boolean));
292+
293+
if (cubeInclude.split) {
294+
const viewName = `${parentCube.name}_${cubeName}`;
295+
let splitViewDef = splitViews[viewName];
296+
if (!splitViewDef) {
297+
splitViews[viewName] = this.createCube({
298+
name: viewName,
299+
isView: true,
300+
// TODO might worth adding to validation as it goes around it right now
301+
isSplitView: true,
302+
});
303+
splitViewDef = splitViews[viewName];
304+
}
305+
306+
const includeMembers = this.generateIncludeMembers(finalIncludes, parentCube.name, type);
307+
this.applyIncludeMembers(includeMembers, splitViewDef, type, errorReporter);
308+
309+
return [];
310+
} else {
311+
return finalIncludes;
312+
}
281313
}));
282314
}
283315

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ const viewSchema = inherit(baseSchema, {
541541
Joi.object().keys({
542542
joinPath: Joi.func().required(),
543543
prefix: Joi.boolean(),
544+
split: Joi.boolean(),
544545
alias: Joi.string(),
545546
includes: Joi.alternatives([
546547
Joi.string().valid('*'),
@@ -553,6 +554,8 @@ const viewSchema = inherit(baseSchema, {
553554
]))
554555
]).required(),
555556
excludes: Joi.array().items(Joi.string().required()),
557+
}).oxor('split', 'prefix').messages({
558+
'object.oxor': 'Using split together with prefix is not supported'
556559
})
557560
),
558561
});
@@ -641,6 +644,6 @@ export class CubeValidator {
641644
}
642645

643646
isCubeValid(cube) {
644-
return this.validCubes[cube.name];
647+
return this.validCubes[cube.name] || cube.isSplitView;
645648
}
646649
}

packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,17 @@ view(\`OrdersView\`, {
235235
view(\`OrdersView2\`, {
236236
includes: [Orders.count],
237237
});
238+
239+
view(\`OrdersView3\`, {
240+
cubes: [{
241+
join_path: Orders,
242+
includes: '*'
243+
}, {
244+
join_path: Orders.Products.ProductCategories,
245+
includes: '*',
246+
split: true
247+
}]
248+
});
238249
`);
239250

240251
async function runQueryTest(q: any, expectedResult: any, additionalTest?: (query: BaseQuery) => any) {
@@ -400,4 +411,13 @@ view(\`OrdersView2\`, {
400411
const cube = metaTransformer.cubes.find(c => c.config.name === 'Orders');
401412
expect(cube.config.measures.filter((({ isVisible }) => isVisible)).length).toBe(0);
402413
});
414+
415+
it('split views', async () => runQueryTest({
416+
measures: ['OrdersView3.count'],
417+
dimensions: ['OrdersView3_ProductCategories.name'],
418+
order: [{ id: 'OrdersView3_ProductCategories.name' }],
419+
}, [{
420+
orders_view3__count: '2',
421+
orders_view3__product_categories__name: 'Groceries',
422+
}]));
403423
});

0 commit comments

Comments
 (0)