Skip to content

Commit 162c5b4

Browse files
authored
feat(schema-compiler): auto include hierarchy dimensions (#9288)
1 parent ef799f5 commit 162c5b4

File tree

5 files changed

+103
-21
lines changed

5 files changed

+103
-21
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,8 @@ export class CubeEvaluator extends CubeSymbols {
263263
}
264264

265265
return null;
266-
}).filter(Boolean);
266+
})
267+
.filter(Boolean);
267268

268269
const name = hierarchyPathToName[[cubeName, it.name].join('.')];
269270
if (!name) {

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

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -243,10 +243,39 @@ export class CubeSymbols {
243243
allMembers: new Set(),
244244
};
245245

246-
const types = ['measures', 'dimensions', 'segments', 'hierarchies'];
246+
const autoIncludeMembers = new Set();
247+
// `hierarchies` must be processed first
248+
const types = ['hierarchies', 'measures', 'dimensions', 'segments'];
249+
247250
for (const type of types) {
248-
const cubeIncludes = cube.cubes && this.membersFromCubes(cube, cube.cubes, type, errorReporter, splitViews, memberSets) || [];
251+
let cubeIncludes = [];
252+
if (cube.cubes) {
253+
// If the hierarchy is included all members from it should be included as well
254+
// Extend `includes` with members from hierarchies that should be auto-included
255+
const cubes = type === 'dimensions' ? cube.cubes.map((it) => {
256+
const fullPath = this.evaluateReferences(null, it.joinPath, { collectJoinHints: true });
257+
const split = fullPath.split('.');
258+
const cubeRef = split[split.length - 1];
259+
260+
if (it.includes === '*') {
261+
return it;
262+
}
263+
264+
const currentCubeAutoIncludeMembers = Array.from(autoIncludeMembers)
265+
.filter((path) => path.startsWith(`${cubeRef}.`))
266+
.map((path) => path.split('.')[1])
267+
.filter(memberName => !it.includes.find((include) => (include.name || include) === memberName));
249268

269+
return {
270+
...it,
271+
includes: (it.includes || []).concat(currentCubeAutoIncludeMembers),
272+
};
273+
}) : cube.cubes;
274+
275+
cubeIncludes = this.membersFromCubes(cube, cubes, type, errorReporter, splitViews, memberSets) || [];
276+
}
277+
278+
// This is the deprecated approach
250279
const includes = cube.includes && this.membersFromIncludeExclude(cube.includes, cube.name, type) || [];
251280
const excludes = cube.excludes && this.membersFromIncludeExclude(cube.excludes, cube.name, type) || [];
252281

@@ -256,6 +285,21 @@ export class CubeSymbols {
256285
excludes
257286
);
258287

288+
if (type === 'hierarchies') {
289+
for (const member of finalIncludes) {
290+
const path = member.member.split('.');
291+
const cubeName = path[path.length - 2];
292+
const hierarchyName = path[path.length - 1];
293+
const hierarchy = this.getResolvedMember(type, cubeName, hierarchyName);
294+
295+
if (hierarchy) {
296+
const levels = this.evaluateReferences(cubeName, this.getResolvedMember('hierarchies', cubeName, hierarchyName).levels, { originalSorting: true });
297+
298+
levels.forEach((level) => autoIncludeMembers.add(level));
299+
}
300+
}
301+
}
302+
259303
const includeMembers = this.generateIncludeMembers(finalIncludes, cube.name, type);
260304
this.applyIncludeMembers(includeMembers, cube, type, errorReporter);
261305

@@ -278,7 +322,7 @@ export class CubeSymbols {
278322
applyIncludeMembers(includeMembers, cube, type, errorReporter) {
279323
for (const [memberName, memberDefinition] of includeMembers) {
280324
if (cube[type]?.[memberName]) {
281-
errorReporter.error(`Included member '${memberName}' conflicts with existing member of '${cube.name}'. Please consider excluding this member.`);
325+
errorReporter.error(`Included member '${memberName}' conflicts with existing member of '${cube.name}'. Please consider excluding this member or assigning it an alias.`);
282326
} else if (type !== 'hierarchies') {
283327
cube[type][memberName] = memberDefinition;
284328
}
@@ -300,11 +344,7 @@ export class CubeSymbols {
300344

301345
if (cubeInclude.includes === '*') {
302346
const membersObj = this.symbols[cubeReference]?.cubeObj()?.[type] || {};
303-
if (Array.isArray(membersObj)) {
304-
includes = membersObj.map(it => ({ member: `${fullPath}.${it.name}`, name: fullMemberName(it.name) }));
305-
} else {
306-
includes = Object.keys(membersObj).map(memberName => ({ member: `${fullPath}.${memberName}`, name: fullMemberName(memberName) }));
307-
}
347+
includes = Object.keys(membersObj).map(memberName => ({ member: `${fullPath}.${memberName}`, name: fullMemberName(memberName) }));
308348
} else {
309349
includes = cubeInclude.includes.map(include => {
310350
const member = include.alias || include;
@@ -397,10 +437,6 @@ export class CubeSymbols {
397437
* @protected
398438
*/
399439
getResolvedMember(type, cubeName, memberName) {
400-
if (Array.isArray(this.symbols[cubeName]?.cubeObj()?.[type])) {
401-
return this.symbols[cubeName]?.cubeObj()?.[type]?.find((it) => it.name === memberName);
402-
}
403-
404440
return this.symbols[cubeName]?.cubeObj()?.[type]?.[memberName];
405441
}
406442

packages/cubejs-schema-compiler/test/unit/fixtures/folders.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@ cubes:
2222
- name: status
2323
sql: status
2424
type: string
25-
26-
- name: city
27-
sql: city
28-
type: string
2925
hierarchies:
3026
- name: orders_hierarchy
3127
levels:

packages/cubejs-schema-compiler/test/unit/fixtures/hierarchies.yml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ views:
6666
includes:
6767
- age
6868
- state
69+
- name: city
70+
alias: user_city
6971
- name: orders_includes_excludes_view
7072
cubes:
7173
- join_path: orders
@@ -85,5 +87,17 @@ views:
8587
- join_path: users
8688
prefix: true
8789
includes: "*"
88-
89-
90+
- name: only_hierarchy_included_view
91+
cubes:
92+
- join_path: orders
93+
includes:
94+
- orders_hierarchy
95+
- join_path: users
96+
includes:
97+
- city
98+
- name: auto_include_view
99+
cubes:
100+
- join_path: orders
101+
includes:
102+
- orders_hierarchy
103+
- some_other_hierarchy

packages/cubejs-schema-compiler/test/unit/hierarchies.test.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@ describe('Cube hierarchies', () => {
2525
public: true,
2626
levels: [
2727
'orders_users_view.status',
28-
'orders_users_view.number'
28+
'orders_users_view.number',
29+
'orders_users_view.user_city'
2930
],
3031
},
3132
{
3233
name: 'orders_users_view.some_other_hierarchy',
3334
public: true,
3435
title: 'Some other hierarchy',
35-
levels: ['orders_users_view.state']
36+
levels: ['orders_users_view.state', 'orders_users_view.user_city']
3637
}
3738
]);
3839

@@ -56,6 +57,40 @@ describe('Cube hierarchies', () => {
5657
expect(prefixedHierarchy?.levels).toEqual(['all_hierarchy_view.users_age', 'all_hierarchy_view.users_city']);
5758
});
5859

60+
it('auto include hierarchy members', async () => {
61+
const modelContent = fs.readFileSync(
62+
path.join(process.cwd(), '/test/unit/fixtures/hierarchies.yml'),
63+
'utf8'
64+
);
65+
const { compiler, metaTransformer } = prepareYamlCompiler(modelContent);
66+
67+
await compiler.compile();
68+
69+
const view1 = metaTransformer.cubes.find(
70+
(it) => it.config.name === 'only_hierarchy_included_view'
71+
);
72+
73+
expect(view1.config.dimensions).toEqual(
74+
expect.arrayContaining([
75+
expect.objectContaining({ name: 'only_hierarchy_included_view.status' }),
76+
expect.objectContaining({ name: 'only_hierarchy_included_view.number' }),
77+
expect.objectContaining({ name: 'only_hierarchy_included_view.city' })
78+
])
79+
);
80+
81+
// Members from the `users` cube are not included as `users` is not selected (not joined)
82+
const view2 = metaTransformer.cubes.find(
83+
(it) => it.config.name === 'auto_include_view'
84+
);
85+
expect(view2.config.dimensions.length).toEqual(2);
86+
expect(view2.config.dimensions).toEqual(
87+
expect.arrayContaining([
88+
expect.objectContaining({ name: 'auto_include_view.status' }),
89+
expect.objectContaining({ name: 'auto_include_view.number' }),
90+
])
91+
);
92+
});
93+
5994
it(('hierarchy with measure'), async () => {
6095
const modelContent = fs.readFileSync(
6196
path.join(process.cwd(), '/test/unit/fixtures/hierarchy-with-measure.yml'),

0 commit comments

Comments
 (0)