Skip to content

Commit 2bb8666

Browse files
KSDaemonpaco-valdez
authored andcommitted
fix(schema-compiler): Fix inherited drill members for views (#9966)
* Add drillMembers and drillMembersGrouped to inhereted properties by views * feat: Implement drill member inheritance for view cubes and enhance error reporting # Conflicts: # packages/cubejs-schema-compiler/src/compiler/ErrorReporter.ts * PR comments * Move filtering logic to generateIncludeMembers * more types in CubeSymbols * Simplified drillMembers filtering for views * fix/correct test comments * update snapshot * fix drill members inheritance (cases of non-owned members) * fix tests --------- Co-authored-by: Paco Valdez <paco@cube.dev>
1 parent 6aec8a7 commit 2bb8666

File tree

3 files changed

+201
-20
lines changed

3 files changed

+201
-20
lines changed

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

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,20 @@ export interface CubeSymbolsBase {
187187

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

190+
type MemberSets = {
191+
resolvedMembers: Set<string>;
192+
allMembers: Set<string>;
193+
};
194+
195+
type ViewResolvedMember = {
196+
member: string;
197+
name: string;
198+
};
199+
200+
type ViewExcludedMember = {
201+
member: string;
202+
};
203+
190204
const FunctionRegex = /function\s+\w+\(([A-Za-z0-9_,]*)|\(([\s\S]*?)\)\s*=>|\(?(\w+)\)?\s*=>/;
191205
export const CONTEXT_SYMBOLS = {
192206
SECURITY_CONTEXT: 'securityContext',
@@ -560,19 +574,23 @@ export class CubeSymbols implements TranspilerSymbolResolver {
560574
return;
561575
}
562576

563-
const memberSets = {
577+
const memberSets: MemberSets = {
564578
resolvedMembers: new Set<string>(),
565579
allMembers: new Set<string>(),
566580
};
567581

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

572588
const joinMap: string[][] = [];
573589

590+
const viewAllMembers: ViewResolvedMember[] = [];
591+
574592
for (const type of types) {
575-
let cubeIncludes: any[] = [];
593+
let cubeIncludes: ViewResolvedMember[] = [];
576594

577595
// If the hierarchy is included all members from it should be included as well
578596
// Extend `includes` with members from hierarchies that should be auto-included
@@ -603,6 +621,7 @@ export class CubeSymbols implements TranspilerSymbolResolver {
603621
}) : includedCubes;
604622

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

607626
if (type === 'hierarchies') {
608627
for (const member of cubeIncludes) {
@@ -620,7 +639,7 @@ export class CubeSymbols implements TranspilerSymbolResolver {
620639
}
621640
}
622641

623-
const includeMembers = this.generateIncludeMembers(cubeIncludes, type);
642+
const includeMembers = this.generateIncludeMembers(cubeIncludes, type, cube, viewAllMembers);
624643
this.applyIncludeMembers(includeMembers, cube, type, errorReporter);
625644

626645
const existing = cube.includedMembers ?? [];
@@ -669,9 +688,9 @@ export class CubeSymbols implements TranspilerSymbolResolver {
669688
type: string,
670689
errorReporter: ErrorReporter,
671690
splitViews: SplitViews,
672-
memberSets: any
673-
) {
674-
const result: any[] = [];
691+
memberSets: MemberSets
692+
): ViewResolvedMember[] {
693+
const result: ViewResolvedMember[] = [];
675694
const seen = new Set<string>();
676695

677696
for (const cubeInclude of cubes) {
@@ -769,7 +788,8 @@ export class CubeSymbols implements TranspilerSymbolResolver {
769788
splitViewDef = splitViews[viewName];
770789
}
771790

772-
const includeMembers = this.generateIncludeMembers(finalIncludes, type);
791+
const viewAllMembers: ViewResolvedMember[] = [];
792+
const includeMembers = this.generateIncludeMembers(finalIncludes, type, splitViewDef, viewAllMembers);
773793
this.applyIncludeMembers(includeMembers, splitViewDef, type, errorReporter);
774794
} else {
775795
for (const member of finalIncludes) {
@@ -785,7 +805,7 @@ export class CubeSymbols implements TranspilerSymbolResolver {
785805
return result;
786806
}
787807

788-
protected diffByMember(includes: any[], excludes: any[]) {
808+
protected diffByMember(includes: ViewResolvedMember[], excludes: ViewExcludedMember[]) {
789809
const excludesMap = new Map();
790810

791811
for (const exclude of excludes) {
@@ -799,14 +819,42 @@ export class CubeSymbols implements TranspilerSymbolResolver {
799819
return this.symbols[cubeName]?.cubeObj()?.[type]?.[memberName];
800820
}
801821

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

830+
let processedDrillMembers = resolvedMember.drillMembers;
831+
832+
// We need to filter only included drillMembers for views
833+
if (type === 'measures' && resolvedMember.drillMembers && targetCube.isView) {
834+
const sourceCubeName = path[path.length - 2];
835+
836+
const evaluatedDrillMembers = this.evaluateReferences(
837+
sourceCubeName,
838+
resolvedMember.drillMembers,
839+
{ originalSorting: true }
840+
);
841+
842+
const drillMembersArray = (Array.isArray(evaluatedDrillMembers)
843+
? evaluatedDrillMembers
844+
: [evaluatedDrillMembers]);
845+
846+
const filteredDrillMembers = drillMembersArray.flatMap(member => {
847+
const found = viewAllMembers.find(v => v.member.endsWith(member));
848+
if (!found) {
849+
return [];
850+
}
851+
852+
return [`${targetCube.name}.${found.name}`];
853+
});
854+
855+
processedDrillMembers = () => filteredDrillMembers;
856+
}
857+
810858
// eslint-disable-next-line no-new-func
811859
const sql = new Function(path[0], `return \`\${${memberRef.member}}\`;`);
812860
let memberDefinition;
@@ -822,6 +870,8 @@ export class CubeSymbols implements TranspilerSymbolResolver {
822870
...(resolvedMember.multiStage && { multiStage: resolvedMember.multiStage }),
823871
...(resolvedMember.timeShift && { timeShift: resolvedMember.timeShift }),
824872
...(resolvedMember.orderBy && { orderBy: resolvedMember.orderBy }),
873+
...(processedDrillMembers && { drillMembers: processedDrillMembers }),
874+
...(resolvedMember.drillMembersGrouped && { drillMembersGrouped: resolvedMember.drillMembersGrouped }),
825875
};
826876
} else if (type === 'dimensions') {
827877
memberDefinition = {
@@ -904,8 +954,7 @@ export class CubeSymbols implements TranspilerSymbolResolver {
904954
name
905955
);
906956
// eslint-disable-next-line no-underscore-dangle
907-
// if (resolvedSymbol && resolvedSymbol._objectWithResolvedProperties) {
908-
if (resolvedSymbol._objectWithResolvedProperties) {
957+
if (resolvedSymbol?._objectWithResolvedProperties) {
909958
return resolvedSymbol;
910959
}
911960
return cubeEvaluator.pathFromArray(fullPath(cubeEvaluator.joinHints(), [referencedCube, name]));
@@ -1015,7 +1064,7 @@ export class CubeSymbols implements TranspilerSymbolResolver {
10151064
cubeName,
10161065
name
10171066
);
1018-
if (resolvedSymbol._objectWithResolvedProperties) {
1067+
if (resolvedSymbol?._objectWithResolvedProperties) {
10191068
return resolvedSymbol;
10201069
}
10211070
return '';

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

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ cube(\`Orders\`, {
4747
measures: {
4848
count: {
4949
type: \`count\`,
50-
//drillMembers: [id, createdAt]
50+
drillMembers: [id, createdAt, Products.ProductCategories.name]
5151
},
5252
5353
runningTotal: {
@@ -183,6 +183,10 @@ cube(\`ProductCategories\`, {
183183
measures: {
184184
count: {
185185
type: \`count\`,
186+
},
187+
count2: {
188+
type: \`count\`,
189+
drillMembers: [id, name]
186190
}
187191
},
188192
@@ -255,7 +259,35 @@ view(\`OrdersView3\`, {
255259
split: true
256260
}]
257261
});
258-
`);
262+
263+
view(\`OrdersSimpleView\`, {
264+
cubes: [{
265+
join_path: Orders,
266+
includes: ['createdAt', 'count']
267+
}]
268+
});
269+
270+
view(\`OrdersViewDrillMembers\`, {
271+
cubes: [{
272+
join_path: Orders,
273+
includes: ['createdAt', 'count']
274+
}, {
275+
join_path: Orders.Products.ProductCategories,
276+
includes: ['name', 'count2']
277+
}]
278+
});
279+
280+
view(\`OrdersViewDrillMembersWithPrefix\`, {
281+
cubes: [{
282+
join_path: Orders,
283+
includes: ['createdAt', 'count']
284+
}, {
285+
join_path: Orders.Products.ProductCategories,
286+
includes: ['name', 'count2'],
287+
prefix: true
288+
}]
289+
});
290+
`);
259291

260292
async function runQueryTest(q: any, expectedResult: any, additionalTest?: (query: BaseQuery) => any) {
261293
await compiler.compile();
@@ -429,4 +461,104 @@ view(\`OrdersView3\`, {
429461
orders_view3__count: '2',
430462
orders_view3__product_categories__name: 'Groceries',
431463
}]));
464+
465+
it('check drillMembers are inherited in views', async () => {
466+
await compiler.compile();
467+
const cube = metaTransformer.cubes.find(c => c.config.name === 'OrdersView');
468+
const countMeasure = cube.config.measures.find((m) => m.name === 'OrdersView.count');
469+
expect(countMeasure.drillMembers).toEqual(['OrdersView.id', 'OrdersView.ProductCategories_name']);
470+
expect(countMeasure.drillMembersGrouped).toEqual({
471+
measures: [],
472+
dimensions: ['OrdersView.id', 'OrdersView.ProductCategories_name']
473+
});
474+
});
475+
476+
it('verify drill member inheritance functionality', async () => {
477+
await compiler.compile();
478+
479+
// Check that the source Orders cube has drill members
480+
const sourceOrdersCube = metaTransformer.cubes.find(c => c.config.name === 'Orders');
481+
const sourceCountMeasure = sourceOrdersCube.config.measures.find((m) => m.name === 'Orders.count');
482+
expect(sourceCountMeasure.drillMembers).toEqual(['Orders.id', 'Orders.createdAt', 'ProductCategories.name']);
483+
484+
// Check that the OrdersView cube inherits these drill members with correct naming
485+
const viewCube = metaTransformer.cubes.find(c => c.config.name === 'OrdersView');
486+
const viewCountMeasure = viewCube.config.measures.find((m) => m.name === 'OrdersView.count');
487+
488+
expect(viewCountMeasure.drillMembers).toBeDefined();
489+
expect(Array.isArray(viewCountMeasure.drillMembers)).toBe(true);
490+
expect(viewCountMeasure.drillMembers.length).toBeGreaterThan(0);
491+
expect(viewCountMeasure.drillMembers).toContain('OrdersView.id');
492+
expect(viewCountMeasure.drillMembersGrouped).toBeDefined();
493+
});
494+
495+
it('check drill member inheritance with limited includes in OrdersSimpleView', async () => {
496+
await compiler.compile();
497+
const cube = metaTransformer.cubes.find(c => c.config.name === 'OrdersSimpleView');
498+
499+
if (!cube) {
500+
throw new Error('OrdersSimpleView not found in compiled cubes');
501+
}
502+
503+
const countMeasure = cube.config.measures.find((m) => m.name === 'OrdersSimpleView.count');
504+
505+
if (!countMeasure) {
506+
throw new Error('OrdersSimpleView.count measure not found');
507+
}
508+
509+
// Check what dimensions are actually available in this limited view
510+
const availableDimensions = cube.config.dimensions?.map(d => d.name) || [];
511+
512+
// This view only includes 'createdAt' dimension and should not include id
513+
expect(availableDimensions).not.toContain('OrdersSimpleView.id');
514+
expect(availableDimensions).toContain('OrdersSimpleView.createdAt');
515+
516+
// The source measure has drillMembers: ['Orders.id', 'Orders.createdAt']
517+
// Both should be available in this view since we explicitly included them
518+
expect(countMeasure.drillMembers).toBeDefined();
519+
// Verify drill members are inherited and correctly transformed to use View naming
520+
expect(countMeasure.drillMembers).toEqual(['OrdersSimpleView.createdAt']);
521+
expect(countMeasure.drillMembersGrouped).toEqual({
522+
measures: [],
523+
dimensions: ['OrdersSimpleView.createdAt']
524+
});
525+
});
526+
527+
it('verify drill member inheritance functionality (with transitive joins)', async () => {
528+
await compiler.compile();
529+
530+
// Check that the OrdersView cube inherits these drill members with correct naming
531+
const viewCube = metaTransformer.cubes.find(c => c.config.name === 'OrdersViewDrillMembers');
532+
533+
const viewCountMeasure = viewCube.config.measures.find((m) => m.name === 'OrdersViewDrillMembers.count');
534+
expect(viewCountMeasure.drillMembers).toBeDefined();
535+
expect(Array.isArray(viewCountMeasure.drillMembers)).toBe(true);
536+
expect(viewCountMeasure.drillMembers.length).toEqual(2);
537+
expect(viewCountMeasure.drillMembers).toEqual(['OrdersViewDrillMembers.createdAt', 'OrdersViewDrillMembers.name']);
538+
539+
const viewCount2Measure = viewCube.config.measures.find((m) => m.name === 'OrdersViewDrillMembers.count2');
540+
expect(viewCount2Measure.drillMembers).toBeDefined();
541+
expect(Array.isArray(viewCount2Measure.drillMembers)).toBe(true);
542+
expect(viewCount2Measure.drillMembers.length).toEqual(1);
543+
expect(viewCount2Measure.drillMembers).toContain('OrdersViewDrillMembers.name');
544+
});
545+
546+
it('verify drill member inheritance functionality (with transitive joins + prefix)', async () => {
547+
await compiler.compile();
548+
549+
// Check that the OrdersView cube inherits these drill members with correct naming
550+
const viewCube = metaTransformer.cubes.find(c => c.config.name === 'OrdersViewDrillMembersWithPrefix');
551+
552+
const viewCountMeasure = viewCube.config.measures.find((m) => m.name === 'OrdersViewDrillMembersWithPrefix.count');
553+
expect(viewCountMeasure.drillMembers).toBeDefined();
554+
expect(Array.isArray(viewCountMeasure.drillMembers)).toBe(true);
555+
expect(viewCountMeasure.drillMembers.length).toEqual(2);
556+
expect(viewCountMeasure.drillMembers).toEqual(['OrdersViewDrillMembersWithPrefix.createdAt', 'OrdersViewDrillMembersWithPrefix.ProductCategories_name']);
557+
558+
const viewCount2Measure = viewCube.config.measures.find((m) => m.name === 'OrdersViewDrillMembersWithPrefix.ProductCategories_count2');
559+
expect(viewCount2Measure.drillMembers).toBeDefined();
560+
expect(Array.isArray(viewCount2Measure.drillMembers)).toBe(true);
561+
expect(viewCount2Measure.drillMembers.length).toEqual(1);
562+
expect(viewCount2Measure.drillMembers).toContain('OrdersViewDrillMembersWithPrefix.ProductCategories_name');
563+
});
432564
});

packages/cubejs-schema-compiler/test/unit/__snapshots__/schema.test.ts.snap

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1848,11 +1848,6 @@ Object {
18481848
"name": "hello",
18491849
"type": "hierarchies",
18501850
},
1851-
Object {
1852-
"memberPath": "orders.count",
1853-
"name": "count",
1854-
"type": "measures",
1855-
},
18561851
Object {
18571852
"memberPath": "orders.status",
18581853
"name": "my_beloved_status",
@@ -1863,6 +1858,11 @@ Object {
18631858
"name": "my_beloved_created_at",
18641859
"type": "dimensions",
18651860
},
1861+
Object {
1862+
"memberPath": "orders.count",
1863+
"name": "count",
1864+
"type": "measures",
1865+
},
18661866
],
18671867
"isView": true,
18681868
"joinMap": Array [],

0 commit comments

Comments
 (0)