Skip to content

Commit

Permalink
feat(schema-compiler): auto include hierarchy dimensions
Browse files Browse the repository at this point in the history
  • Loading branch information
vasilev-alex committed Feb 28, 2025
1 parent d67d168 commit 80806c8
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,8 @@ export class CubeEvaluator extends CubeSymbols {
}

return null;
}).filter(Boolean);
})
.filter(Boolean);

const name = hierarchyPathToName[[cubeName, it.name].join('.')];
if (!name) {
Expand Down
60 changes: 48 additions & 12 deletions packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,39 @@ export class CubeSymbols {
allMembers: new Set(),
};

const types = ['measures', 'dimensions', 'segments', 'hierarchies'];
const autoIncludeMembers = new Set();
// `hierarchies` must be processed first
const types = ['hierarchies', 'measures', 'dimensions', 'segments'];

for (const type of types) {
const cubeIncludes = cube.cubes && this.membersFromCubes(cube, cube.cubes, type, errorReporter, splitViews, memberSets) || [];
let cubeIncludes = [];
if (cube.cubes) {
// If the hierarchy is included all members from it should be included as well
// Extend `includes` with members from hierarchies that should be auto-included
const cubes = type === 'dimensions' ? cube.cubes.map((it) => {
const fullPath = this.evaluateReferences(null, it.joinPath, { collectJoinHints: true });
const split = fullPath.split('.');
const cubeRef = split[split.length - 1];

if (it.includes === '*') {
return it;
}

const currentCubeAutoIncludeMembers = Array.from(autoIncludeMembers)
.filter((path) => path.startsWith(`${cubeRef}.`))
.map((path) => path.split('.')[1])
.filter(memberName => !it.includes.find((include) => (include.name || include) === memberName));

return {
...it,
includes: (it.includes || []).concat(currentCubeAutoIncludeMembers),
};
}) : cube.cubes;

cubeIncludes = this.membersFromCubes(cube, cubes, type, errorReporter, splitViews, memberSets) || [];
}

// This is the deprecated approach
const includes = cube.includes && this.membersFromIncludeExclude(cube.includes, cube.name, type) || [];
const excludes = cube.excludes && this.membersFromIncludeExclude(cube.excludes, cube.name, type) || [];

Expand All @@ -256,6 +285,21 @@ export class CubeSymbols {
excludes
);

if (type === 'hierarchies') {
for (const member of finalIncludes) {
const path = member.member.split('.');
const cubeName = path[path.length - 2];
const hierarchyName = path[path.length - 1];
const hierarchy = this.getResolvedMember(type, cubeName, hierarchyName);

if (hierarchy) {
const levels = this.evaluateReferences(cubeName, this.getResolvedMember('hierarchies', cubeName, hierarchyName).levels, { originalSorting: true });

levels.forEach((level) => autoIncludeMembers.add(level));
}
}
}

const includeMembers = this.generateIncludeMembers(finalIncludes, cube.name, type);
this.applyIncludeMembers(includeMembers, cube, type, errorReporter);

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

if (cubeInclude.includes === '*') {
const membersObj = this.symbols[cubeReference]?.cubeObj()?.[type] || {};
if (Array.isArray(membersObj)) {
includes = membersObj.map(it => ({ member: `${fullPath}.${it.name}`, name: fullMemberName(it.name) }));
} else {
includes = Object.keys(membersObj).map(memberName => ({ member: `${fullPath}.${memberName}`, name: fullMemberName(memberName) }));
}
includes = Object.keys(membersObj).map(memberName => ({ member: `${fullPath}.${memberName}`, name: fullMemberName(memberName) }));
} else {
includes = cubeInclude.includes.map(include => {
const member = include.alias || include;
Expand Down Expand Up @@ -397,10 +437,6 @@ export class CubeSymbols {
* @protected
*/
getResolvedMember(type, cubeName, memberName) {
if (Array.isArray(this.symbols[cubeName]?.cubeObj()?.[type])) {
return this.symbols[cubeName]?.cubeObj()?.[type]?.find((it) => it.name === memberName);
}

return this.symbols[cubeName]?.cubeObj()?.[type]?.[memberName];
}

Expand Down
18 changes: 16 additions & 2 deletions packages/cubejs-schema-compiler/test/unit/fixtures/hierarchies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ views:
includes:
- age
- state
- name: city
alias: user_city
- name: orders_includes_excludes_view
cubes:
- join_path: orders
Expand All @@ -85,5 +87,17 @@ views:
- join_path: users
prefix: true
includes: "*"


- name: only_hierarchy_included_view
cubes:
- join_path: orders
includes:
- orders_hierarchy
- join_path: users
includes:
- city
- name: auto_include_view
cubes:
- join_path: orders
includes:
- orders_hierarchy
- some_other_hierarchy
39 changes: 37 additions & 2 deletions packages/cubejs-schema-compiler/test/unit/hierarchies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ describe('Cube hierarchies', () => {
public: true,
levels: [
'orders_users_view.status',
'orders_users_view.number'
'orders_users_view.number',
'orders_users_view.user_city'
],
},
{
name: 'orders_users_view.some_other_hierarchy',
public: true,
title: 'Some other hierarchy',
levels: ['orders_users_view.state']
levels: ['orders_users_view.state', 'orders_users_view.user_city']
}
]);

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

it('auto include hierarchy members', async () => {
const modelContent = fs.readFileSync(
path.join(process.cwd(), '/test/unit/fixtures/hierarchies.yml'),
'utf8'
);
const { compiler, metaTransformer } = prepareYamlCompiler(modelContent);

await compiler.compile();

const view1 = metaTransformer.cubes.find(
(it) => it.config.name === 'only_hierarchy_included_view'
);

expect(view1.config.dimensions).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: 'only_hierarchy_included_view.status' }),
expect.objectContaining({ name: 'only_hierarchy_included_view.number' }),
expect.objectContaining({ name: 'only_hierarchy_included_view.city' })
])
);

// Members from the `users` cube are not included as `users` is not selected (not joined)
const view2 = metaTransformer.cubes.find(
(it) => it.config.name === 'auto_include_view'
);
expect(view2.config.dimensions.length).toEqual(2);
expect(view2.config.dimensions).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: 'auto_include_view.status' }),
expect.objectContaining({ name: 'auto_include_view.number' }),
])
);
});

it(('hierarchy with measure'), async () => {
const modelContent = fs.readFileSync(
path.join(process.cwd(), '/test/unit/fixtures/hierarchy-with-measure.yml'),
Expand Down

0 comments on commit 80806c8

Please sign in to comment.