Skip to content
75 changes: 62 additions & 13 deletions packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,20 @@ export interface CubeSymbolsBase {

export type CubeSymbolsDefinition = CubeSymbolsBase & Record<string, CubeSymbolDefinition>;

type MemberSets = {
resolvedMembers: Set<string>;
allMembers: Set<string>;
};

type ViewResolvedMember = {
member: string;
name: string;
};

type ViewExcludedMember = {
member: string;
};

const FunctionRegex = /function\s+\w+\(([A-Za-z0-9_,]*)|\(([\s\S]*?)\)\s*=>|\(?(\w+)\)?\s*=>/;
export const CONTEXT_SYMBOLS = {
SECURITY_CONTEXT: 'securityContext',
Expand Down Expand Up @@ -553,17 +567,21 @@ export class CubeSymbols implements TranspilerSymbolResolver {
return;
}

const memberSets = {
const memberSets: MemberSets = {
resolvedMembers: new Set<string>(),
allMembers: new Set<string>(),
};

const autoIncludeMembers = new Set<string>();
// `hierarchies` must be processed first
const types = ['hierarchies', 'measures', 'dimensions', 'segments'];
// It's also important `dimensions` to be processed BEFORE `measures`
// because drillMembers processing for views in generateIncludeMembers() relies on this
const types = ['hierarchies', 'dimensions', 'measures', 'segments'];

const viewAllMembers: ViewResolvedMember[] = [];

for (const type of types) {
let cubeIncludes: any[] = [];
let cubeIncludes: ViewResolvedMember[] = [];

// 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
Expand All @@ -589,6 +607,7 @@ export class CubeSymbols implements TranspilerSymbolResolver {
}) : includedCubes;

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

if (type === 'hierarchies') {
for (const member of cubeIncludes) {
Expand All @@ -606,7 +625,7 @@ export class CubeSymbols implements TranspilerSymbolResolver {
}
}

const includeMembers = this.generateIncludeMembers(cubeIncludes, type);
const includeMembers = this.generateIncludeMembers(cubeIncludes, type, cube, viewAllMembers);
this.applyIncludeMembers(includeMembers, cube, type, errorReporter);

const existing = cube.includedMembers ?? [];
Expand Down Expand Up @@ -657,9 +676,9 @@ export class CubeSymbols implements TranspilerSymbolResolver {
type: string,
errorReporter: ErrorReporter,
splitViews: SplitViews,
memberSets: any
) {
const result: any[] = [];
memberSets: MemberSets
): ViewResolvedMember[] {
const result: ViewResolvedMember[] = [];
const seen = new Set<string>();

for (const cubeInclude of cubes) {
Expand Down Expand Up @@ -757,7 +776,8 @@ export class CubeSymbols implements TranspilerSymbolResolver {
splitViewDef = splitViews[viewName];
}

const includeMembers = this.generateIncludeMembers(finalIncludes, type);
const viewAllMembers: ViewResolvedMember[] = [];
const includeMembers = this.generateIncludeMembers(finalIncludes, type, splitViewDef, viewAllMembers);
this.applyIncludeMembers(includeMembers, splitViewDef, type, errorReporter);
} else {
for (const member of finalIncludes) {
Expand All @@ -773,7 +793,7 @@ export class CubeSymbols implements TranspilerSymbolResolver {
return result;
}

protected diffByMember(includes: any[], excludes: any[]) {
protected diffByMember(includes: ViewResolvedMember[], excludes: ViewExcludedMember[]) {
const excludesMap = new Map();

for (const exclude of excludes) {
Expand All @@ -787,14 +807,42 @@ export class CubeSymbols implements TranspilerSymbolResolver {
return this.symbols[cubeName]?.cubeObj()?.[type]?.[memberName];
}

protected generateIncludeMembers(members: any[], type: string) {
protected generateIncludeMembers(members: any[], type: string, targetCube: CubeDefinitionExtended, viewAllMembers: ViewResolvedMember[]) {
return members.map(memberRef => {
const path = memberRef.member.split('.');
const resolvedMember = this.getResolvedMember(type, path[path.length - 2], path[path.length - 1]);
if (!resolvedMember) {
throw new Error(`Can't resolve '${memberRef.member}' while generating include members`);
}

let processedDrillMembers = resolvedMember.drillMembers;

// We need to filter only included drillMembers for views
if (type === 'measures' && resolvedMember.drillMembers && targetCube.isView) {
const sourceCubeName = path[path.length - 2];

const evaluatedDrillMembers = this.evaluateReferences(
sourceCubeName,
resolvedMember.drillMembers,
{ originalSorting: true }
);

const drillMembersArray = (Array.isArray(evaluatedDrillMembers)
? evaluatedDrillMembers
: [evaluatedDrillMembers]);

const filteredDrillMembers = drillMembersArray.flatMap(member => {
const found = viewAllMembers.find(v => v.member.endsWith(member));
if (!found) {
return [];
}

return [`${targetCube.name}.${found.name}`];
});

processedDrillMembers = () => filteredDrillMembers;
}

// eslint-disable-next-line no-new-func
const sql = new Function(path[0], `return \`\${${memberRef.member}}\`;`);
let memberDefinition;
Expand All @@ -810,6 +858,8 @@ export class CubeSymbols implements TranspilerSymbolResolver {
...(resolvedMember.multiStage && { multiStage: resolvedMember.multiStage }),
...(resolvedMember.timeShift && { timeShift: resolvedMember.timeShift }),
...(resolvedMember.orderBy && { orderBy: resolvedMember.orderBy }),
...(processedDrillMembers && { drillMembers: processedDrillMembers }),
...(resolvedMember.drillMembersGrouped && { drillMembersGrouped: resolvedMember.drillMembersGrouped }),
};
} else if (type === 'dimensions') {
memberDefinition = {
Expand Down Expand Up @@ -892,8 +942,7 @@ export class CubeSymbols implements TranspilerSymbolResolver {
name
);
// eslint-disable-next-line no-underscore-dangle
// if (resolvedSymbol && resolvedSymbol._objectWithResolvedProperties) {
if (resolvedSymbol._objectWithResolvedProperties) {
if (resolvedSymbol?._objectWithResolvedProperties) {
return resolvedSymbol;
}
return cubeEvaluator.pathFromArray(fullPath(cubeEvaluator.joinHints(), [referencedCube, name]));
Expand Down Expand Up @@ -1003,7 +1052,7 @@ export class CubeSymbols implements TranspilerSymbolResolver {
cubeName,
name
);
if (resolvedSymbol._objectWithResolvedProperties) {
if (resolvedSymbol?._objectWithResolvedProperties) {
return resolvedSymbol;
}
return '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export class ErrorReporter {
if (this.rootReporter().errors.length) {
throw new CompileError(
this.rootReporter().errors.map((e) => e.message).join('\n'),
this.rootReporter().errors.map((e) => e.plainMessage).join('\n')
this.rootReporter().errors.map((e) => e.plainMessage || e.message || '').join('\n')
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ cube(\`Orders\`, {
measures: {
count: {
type: \`count\`,
//drillMembers: [id, createdAt]
drillMembers: [id, createdAt, Products.ProductCategories.name]
},

runningTotal: {
Expand Down Expand Up @@ -183,6 +183,10 @@ cube(\`ProductCategories\`, {
measures: {
count: {
type: \`count\`,
},
count2: {
type: \`count\`,
drillMembers: [id, name]
}
},

Expand Down Expand Up @@ -255,7 +259,35 @@ view(\`OrdersView3\`, {
split: true
}]
});
`);

view(\`OrdersSimpleView\`, {
cubes: [{
join_path: Orders,
includes: ['createdAt', 'count']
}]
});

view(\`OrdersViewDrillMembers\`, {
cubes: [{
join_path: Orders,
includes: ['createdAt', 'count']
}, {
join_path: Orders.Products.ProductCategories,
includes: ['name', 'count2']
}]
});

view(\`OrdersViewDrillMembersWithPrefix\`, {
cubes: [{
join_path: Orders,
includes: ['createdAt', 'count']
}, {
join_path: Orders.Products.ProductCategories,
includes: ['name', 'count2'],
prefix: true
}]
});
`);

async function runQueryTest(q: any, expectedResult: any, additionalTest?: (query: BaseQuery) => any) {
await compiler.compile();
Expand Down Expand Up @@ -429,4 +461,104 @@ view(\`OrdersView3\`, {
orders_view3__count: '2',
orders_view3__product_categories__name: 'Groceries',
}]));

it('check drillMembers are inherited in views', async () => {
await compiler.compile();
const cube = metaTransformer.cubes.find(c => c.config.name === 'OrdersView');
const countMeasure = cube.config.measures.find((m) => m.name === 'OrdersView.count');
expect(countMeasure.drillMembers).toEqual(['OrdersView.id', 'OrdersView.ProductCategories_name']);
expect(countMeasure.drillMembersGrouped).toEqual({
measures: [],
dimensions: ['OrdersView.id', 'OrdersView.ProductCategories_name']
});
});

it('verify drill member inheritance functionality', async () => {
await compiler.compile();

// Check that the source Orders cube has drill members
const sourceOrdersCube = metaTransformer.cubes.find(c => c.config.name === 'Orders');
const sourceCountMeasure = sourceOrdersCube.config.measures.find((m) => m.name === 'Orders.count');
expect(sourceCountMeasure.drillMembers).toEqual(['Orders.id', 'Orders.createdAt', 'ProductCategories.name']);

// Check that the OrdersView cube inherits these drill members with correct naming
const viewCube = metaTransformer.cubes.find(c => c.config.name === 'OrdersView');
const viewCountMeasure = viewCube.config.measures.find((m) => m.name === 'OrdersView.count');

expect(viewCountMeasure.drillMembers).toBeDefined();
expect(Array.isArray(viewCountMeasure.drillMembers)).toBe(true);
expect(viewCountMeasure.drillMembers.length).toBeGreaterThan(0);
expect(viewCountMeasure.drillMembers).toContain('OrdersView.id');
expect(viewCountMeasure.drillMembersGrouped).toBeDefined();
});

it('check drill member inheritance with limited includes in OrdersSimpleView', async () => {
await compiler.compile();
const cube = metaTransformer.cubes.find(c => c.config.name === 'OrdersSimpleView');

if (!cube) {
throw new Error('OrdersSimpleView not found in compiled cubes');
}

const countMeasure = cube.config.measures.find((m) => m.name === 'OrdersSimpleView.count');

if (!countMeasure) {
throw new Error('OrdersSimpleView.count measure not found');
}

// Check what dimensions are actually available in this limited view
const availableDimensions = cube.config.dimensions?.map(d => d.name) || [];

// This view only includes 'createdAt' dimension and should not include id
expect(availableDimensions).not.toContain('OrdersSimpleView.id');
expect(availableDimensions).toContain('OrdersSimpleView.createdAt');

// The source measure has drillMembers: ['Orders.id', 'Orders.createdAt']
// Both should be available in this view since we explicitly included them
expect(countMeasure.drillMembers).toBeDefined();
// Verify drill members are inherited and correctly transformed to use View naming
expect(countMeasure.drillMembers).toEqual(['OrdersSimpleView.createdAt']);
expect(countMeasure.drillMembersGrouped).toEqual({
measures: [],
dimensions: ['OrdersSimpleView.createdAt']
});
});

it('verify drill member inheritance functionality (with transitive joins)', async () => {
await compiler.compile();

// Check that the OrdersView cube inherits these drill members with correct naming
const viewCube = metaTransformer.cubes.find(c => c.config.name === 'OrdersViewDrillMembers');

const viewCountMeasure = viewCube.config.measures.find((m) => m.name === 'OrdersViewDrillMembers.count');
expect(viewCountMeasure.drillMembers).toBeDefined();
expect(Array.isArray(viewCountMeasure.drillMembers)).toBe(true);
expect(viewCountMeasure.drillMembers.length).toEqual(2);
expect(viewCountMeasure.drillMembers).toEqual(['OrdersViewDrillMembers.createdAt', 'OrdersViewDrillMembers.name']);

const viewCount2Measure = viewCube.config.measures.find((m) => m.name === 'OrdersViewDrillMembers.count2');
expect(viewCount2Measure.drillMembers).toBeDefined();
expect(Array.isArray(viewCount2Measure.drillMembers)).toBe(true);
expect(viewCount2Measure.drillMembers.length).toEqual(1);
expect(viewCount2Measure.drillMembers).toContain('OrdersViewDrillMembers.name');
});

it('verify drill member inheritance functionality (with transitive joins + prefix)', async () => {
await compiler.compile();

// Check that the OrdersView cube inherits these drill members with correct naming
const viewCube = metaTransformer.cubes.find(c => c.config.name === 'OrdersViewDrillMembersWithPrefix');

const viewCountMeasure = viewCube.config.measures.find((m) => m.name === 'OrdersViewDrillMembersWithPrefix.count');
expect(viewCountMeasure.drillMembers).toBeDefined();
expect(Array.isArray(viewCountMeasure.drillMembers)).toBe(true);
expect(viewCountMeasure.drillMembers.length).toEqual(2);
expect(viewCountMeasure.drillMembers).toEqual(['OrdersViewDrillMembersWithPrefix.createdAt', 'OrdersViewDrillMembersWithPrefix.ProductCategories_name']);

const viewCount2Measure = viewCube.config.measures.find((m) => m.name === 'OrdersViewDrillMembersWithPrefix.ProductCategories_count2');
expect(viewCount2Measure.drillMembers).toBeDefined();
expect(Array.isArray(viewCount2Measure.drillMembers)).toBe(true);
expect(viewCount2Measure.drillMembers.length).toEqual(1);
expect(viewCount2Measure.drillMembers).toContain('OrdersViewDrillMembersWithPrefix.ProductCategories_name');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1848,11 +1848,6 @@ Object {
"name": "hello",
"type": "hierarchies",
},
Object {
"memberPath": "orders.count",
"name": "count",
"type": "measures",
},
Object {
"memberPath": "orders.status",
"name": "my_beloved_status",
Expand All @@ -1863,6 +1858,11 @@ Object {
"name": "my_beloved_created_at",
"type": "dimensions",
},
Object {
"memberPath": "orders.count",
"name": "count",
"type": "measures",
},
],
"isView": true,
"joins": Array [],
Expand Down
Loading