From 9fa5869c28bb8d8186b843b523260023792d4324 Mon Sep 17 00:00:00 2001 From: Paco Valdez Date: Wed, 10 Sep 2025 10:33:54 -0700 Subject: [PATCH 01/10] Add drillMembers and drillMembersGrouped to inhereted properties by views --- .../src/compiler/CubeSymbols.ts | 2 ++ .../test/integration/postgres/cube-views.test.ts | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index e57efcb42a7f3..b0e18f766c2e6 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -822,6 +822,8 @@ export class CubeSymbols implements TranspilerSymbolResolver { ...(resolvedMember.multiStage && { multiStage: resolvedMember.multiStage }), ...(resolvedMember.timeShift && { timeShift: resolvedMember.timeShift }), ...(resolvedMember.orderBy && { orderBy: resolvedMember.orderBy }), + ...(resolvedMember.drillMembers && { drillMembers: resolvedMember.drillMembers }), + ...(resolvedMember.drillMembersGrouped && { drillMembersGrouped: resolvedMember.drillMembersGrouped }), }; } else if (type === 'dimensions') { memberDefinition = { diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts index 049e938ba2243..29eae42ac3c69 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts @@ -47,7 +47,7 @@ cube(\`Orders\`, { measures: { count: { type: \`count\`, - //drillMembers: [id, createdAt] + drillMembers: [id, createdAt] }, runningTotal: { @@ -429,4 +429,15 @@ 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.createdAt']); + expect(countMeasure.drillMembersGrouped).toEqual({ + measures: [], + dimensions: ['OrdersView.id', 'OrdersView.createdAt'] + }); + }); }); From 46c621a8898ac6397ffb675431af63270cbe38b9 Mon Sep 17 00:00:00 2001 From: Paco Valdez Date: Wed, 10 Sep 2025 15:21:22 -0700 Subject: [PATCH 02/10] feat: Implement drill member inheritance for view cubes and enhance error reporting # Conflicts: # packages/cubejs-schema-compiler/src/compiler/ErrorReporter.ts --- .../src/compiler/CubeSymbols.ts | 5 +- .../src/compiler/CubeToMetaTransformer.js | 26 ++++++++ .../integration/postgres/cube-views.test.ts | 65 +++++++++++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index b0e18f766c2e6..2f8778dc96eb0 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -906,8 +906,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { name ); // eslint-disable-next-line no-underscore-dangle - // if (resolvedSymbol && resolvedSymbol._objectWithResolvedProperties) { - if (resolvedSymbol._objectWithResolvedProperties) { + if (resolvedSymbol && resolvedSymbol._objectWithResolvedProperties) { return resolvedSymbol; } return cubeEvaluator.pathFromArray(fullPath(cubeEvaluator.joinHints(), [referencedCube, name])); @@ -1017,7 +1016,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { cubeName, name ); - if (resolvedSymbol._objectWithResolvedProperties) { + if (resolvedSymbol && resolvedSymbol._objectWithResolvedProperties) { return resolvedSymbol; } return ''; diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js b/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js index 91bfe840cc74b..cada31cb195f7 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js +++ b/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js @@ -210,6 +210,32 @@ export class CubeToMetaTransformer { cubeName, drillMembers, { originalSorting: true } )) || []; + // Filter drill members for views to only include available members + if (drillMembersArray.length > 0) { + const cubeSymbol = this.cubeEvaluator.symbols[cubeName]; + if (cubeSymbol) { + const cube = cubeSymbol.cubeObj(); + if (cube && cube.isView) { + const availableMembers = new Set(); + // Collect all available member names from all types + ['measures', 'dimensions', 'segments'].forEach(memberType => { + if (cube[memberType]) { + Object.keys(cube[memberType]).forEach(memberName => { + availableMembers.add(`${cubeName}.${memberName}`); + }); + } + }); + + // Filter drill members to only include those available in the view + const filteredDrillMembers = drillMembersArray.filter(member => availableMembers.has(member)); + + // Update the drillMembersArray with filtered results + drillMembersArray.length = 0; + drillMembersArray.push(...filteredDrillMembers); + } + } + } + const type = CubeSymbols.toMemberDataType(nameToMetric[1].type); return { diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts index 29eae42ac3c69..42efb78da6e43 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts @@ -255,6 +255,13 @@ view(\`OrdersView3\`, { split: true }] }); + +view(\`OrdersSimpleView\`, { + cubes: [{ + join_path: Orders, + includes: ['createdAt', 'count'] + }] +}); `); async function runQueryTest(q: any, expectedResult: any, additionalTest?: (query: BaseQuery) => any) { @@ -440,4 +447,62 @@ view(\`OrdersView3\`, { dimensions: ['OrdersView.id', 'OrdersView.createdAt'] }); }); + + 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']); + + // 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'); + + // Before our fix, this would have been undefined or empty + // After our fix, drill members are properly inherited and renamed to use the view naming + 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) || []; + console.log('OrdersSimpleView dimensions:', availableDimensions); + console.log('OrdersSimpleView drill members:', countMeasure.drillMembers); + + // This view only includes ['id', 'createdAt', 'count'] - should have both id and createdAt + 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(); + expect(Array.isArray(countMeasure.drillMembers)).toBe(true); + expect(countMeasure.drillMembers.length).toBeGreaterThan(0); + + // 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'] + }); + }); }); From af27836615e2ece5f28046d24408534adfe12e62 Mon Sep 17 00:00:00 2001 From: Paco Valdez Date: Thu, 11 Sep 2025 13:11:21 -0700 Subject: [PATCH 03/10] PR comments --- packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts | 4 ++-- .../src/compiler/CubeToMetaTransformer.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index 2f8778dc96eb0..6058be2e1bfad 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -906,7 +906,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { name ); // eslint-disable-next-line no-underscore-dangle - if (resolvedSymbol && resolvedSymbol._objectWithResolvedProperties) { + if (resolvedSymbol?._objectWithResolvedProperties) { return resolvedSymbol; } return cubeEvaluator.pathFromArray(fullPath(cubeEvaluator.joinHints(), [referencedCube, name])); @@ -1016,7 +1016,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { cubeName, name ); - if (resolvedSymbol && resolvedSymbol._objectWithResolvedProperties) { + if (resolvedSymbol?._objectWithResolvedProperties) { return resolvedSymbol; } return ''; diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js b/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js index cada31cb195f7..9aee64becfd91 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js +++ b/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js @@ -230,8 +230,7 @@ export class CubeToMetaTransformer { const filteredDrillMembers = drillMembersArray.filter(member => availableMembers.has(member)); // Update the drillMembersArray with filtered results - drillMembersArray.length = 0; - drillMembersArray.push(...filteredDrillMembers); + drillMembersArray.splice(0, drillMembersArray.length, ...filteredDrillMembers); } } } From c3b5c3f3f866a428b72b99c0caacbbd5cb9e9030 Mon Sep 17 00:00:00 2001 From: Paco Valdez Date: Thu, 11 Sep 2025 14:18:33 -0700 Subject: [PATCH 04/10] Move filtering logic to generateIncludeMembers --- .../src/compiler/CubeSymbols.ts | 79 ++++++++++++++++++- .../src/compiler/CubeToMetaTransformer.js | 25 ------ .../integration/postgres/cube-views.test.ts | 2 - 3 files changed, 75 insertions(+), 31 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index 6058be2e1bfad..703caade08d6d 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -620,7 +620,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { } } - const includeMembers = this.generateIncludeMembers(cubeIncludes, type); + const includeMembers = this.generateIncludeMembers(cubeIncludes, type, cube); this.applyIncludeMembers(includeMembers, cube, type, errorReporter); const existing = cube.includedMembers ?? []; @@ -769,7 +769,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { splitViewDef = splitViews[viewName]; } - const includeMembers = this.generateIncludeMembers(finalIncludes, type); + const includeMembers = this.generateIncludeMembers(finalIncludes, type, splitViewDef); this.applyIncludeMembers(includeMembers, splitViewDef, type, errorReporter); } else { for (const member of finalIncludes) { @@ -799,13 +799,84 @@ export class CubeSymbols implements TranspilerSymbolResolver { return this.symbols[cubeName]?.cubeObj()?.[type]?.[memberName]; } - protected generateIncludeMembers(members: any[], type: string) { + protected createViewAwareDrillMemberFunction( + originalFunction: Function, + sourceCubeName: string, + targetCubeName: string, + originalDrillMembers: string[] + ) { + const cubeEvaluator = this; + + return function drillMemberFilter(..._args: any[]) { + // Transform source cube references to target cube references + // e.g., "Orders.id" -> "OrdersSimpleView.id" + const transformedDrillMembers = originalDrillMembers.map(member => { + const memberParts = member.split('.'); + if (memberParts[0] === sourceCubeName) { + return `${targetCubeName}.${memberParts[1]}`; + } + return member; // Keep as-is if not from source cube + }); + + // Get the target cube to check which members actually exist + const targetCubeSymbol = cubeEvaluator.symbols[targetCubeName]; + if (!targetCubeSymbol) { + return []; + } + + const targetCube = targetCubeSymbol.cubeObj(); + if (!targetCube) { + return []; + } + + // Build set of available members in the target cube + const availableMembers = new Set(); + ['measures', 'dimensions', 'segments'].forEach(memberType => { + if (targetCube[memberType]) { + Object.keys(targetCube[memberType]).forEach(memberName => { + availableMembers.add(`${targetCubeName}.${memberName}`); + }); + } + }); + + // Filter drill members to only include available ones + return transformedDrillMembers.filter(member => availableMembers.has(member)); + }; + } + + protected generateIncludeMembers(members: any[], type: string, targetCube?: any) { 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`); } + + // Store drill member processing info for later use in the member definition + let processedDrillMembers = resolvedMember.drillMembers; + + if (type === 'measures' && resolvedMember.drillMembers && targetCube?.isView) { + const sourceCubeName = path[path.length - 2]; // e.g., "Orders" + + const evaluatedDrillMembers = this.evaluateReferences( + sourceCubeName, + resolvedMember.drillMembers, + { originalSorting: true } + ); + + // Ensure we have an array + const drillMembersArray = Array.isArray(evaluatedDrillMembers) + ? evaluatedDrillMembers + : [evaluatedDrillMembers]; + + // Create a new filtered function for this view + processedDrillMembers = this.createViewAwareDrillMemberFunction( + resolvedMember.drillMembers, + sourceCubeName, + targetCube.name, + drillMembersArray + ); + } // eslint-disable-next-line no-new-func const sql = new Function(path[0], `return \`\${${memberRef.member}}\`;`); @@ -822,7 +893,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { ...(resolvedMember.multiStage && { multiStage: resolvedMember.multiStage }), ...(resolvedMember.timeShift && { timeShift: resolvedMember.timeShift }), ...(resolvedMember.orderBy && { orderBy: resolvedMember.orderBy }), - ...(resolvedMember.drillMembers && { drillMembers: resolvedMember.drillMembers }), + ...(processedDrillMembers && { drillMembers: processedDrillMembers }), ...(resolvedMember.drillMembersGrouped && { drillMembersGrouped: resolvedMember.drillMembersGrouped }), }; } else if (type === 'dimensions') { diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js b/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js index 9aee64becfd91..91bfe840cc74b 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js +++ b/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js @@ -210,31 +210,6 @@ export class CubeToMetaTransformer { cubeName, drillMembers, { originalSorting: true } )) || []; - // Filter drill members for views to only include available members - if (drillMembersArray.length > 0) { - const cubeSymbol = this.cubeEvaluator.symbols[cubeName]; - if (cubeSymbol) { - const cube = cubeSymbol.cubeObj(); - if (cube && cube.isView) { - const availableMembers = new Set(); - // Collect all available member names from all types - ['measures', 'dimensions', 'segments'].forEach(memberType => { - if (cube[memberType]) { - Object.keys(cube[memberType]).forEach(memberName => { - availableMembers.add(`${cubeName}.${memberName}`); - }); - } - }); - - // Filter drill members to only include those available in the view - const filteredDrillMembers = drillMembersArray.filter(member => availableMembers.has(member)); - - // Update the drillMembersArray with filtered results - drillMembersArray.splice(0, drillMembersArray.length, ...filteredDrillMembers); - } - } - } - const type = CubeSymbols.toMemberDataType(nameToMetric[1].type); return { diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts index 42efb78da6e43..38fbe9229b49a 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts @@ -485,8 +485,6 @@ view(\`OrdersSimpleView\`, { // Check what dimensions are actually available in this limited view const availableDimensions = cube.config.dimensions?.map(d => d.name) || []; - console.log('OrdersSimpleView dimensions:', availableDimensions); - console.log('OrdersSimpleView drill members:', countMeasure.drillMembers); // This view only includes ['id', 'createdAt', 'count'] - should have both id and createdAt expect(availableDimensions).not.toContain('OrdersSimpleView.id'); From c583714f7ec59dec34a594364490461b075e05ce Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Fri, 12 Sep 2025 16:07:18 +0300 Subject: [PATCH 05/10] more types in CubeSymbols --- .../src/compiler/CubeSymbols.ts | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index 703caade08d6d..eeb985e93c1af 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -187,6 +187,11 @@ export interface CubeSymbolsBase { export type CubeSymbolsDefinition = CubeSymbolsBase & Record; +type MemberSets = { + resolvedMembers: Set; + allMembers: Set; +}; + const FunctionRegex = /function\s+\w+\(([A-Za-z0-9_,]*)|\(([\s\S]*?)\)\s*=>|\(?(\w+)\)?\s*=>/; export const CONTEXT_SYMBOLS = { SECURITY_CONTEXT: 'securityContext', @@ -560,14 +565,14 @@ export class CubeSymbols implements TranspilerSymbolResolver { return; } - const memberSets = { + const memberSets: MemberSets = { resolvedMembers: new Set(), allMembers: new Set(), }; const autoIncludeMembers = new Set(); // `hierarchies` must be processed first - const types = ['hierarchies', 'measures', 'dimensions', 'segments']; + const types = ['hierarchies', 'dimensions', 'measures', 'segments']; const joinMap: string[][] = []; @@ -669,7 +674,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { type: string, errorReporter: ErrorReporter, splitViews: SplitViews, - memberSets: any + memberSets: MemberSets ) { const result: any[] = []; const seen = new Set(); @@ -817,18 +822,18 @@ export class CubeSymbols implements TranspilerSymbolResolver { } return member; // Keep as-is if not from source cube }); - + // Get the target cube to check which members actually exist const targetCubeSymbol = cubeEvaluator.symbols[targetCubeName]; if (!targetCubeSymbol) { return []; } - + const targetCube = targetCubeSymbol.cubeObj(); if (!targetCube) { return []; } - + // Build set of available members in the target cube const availableMembers = new Set(); ['measures', 'dimensions', 'segments'].forEach(memberType => { @@ -838,25 +843,25 @@ export class CubeSymbols implements TranspilerSymbolResolver { }); } }); - + // Filter drill members to only include available ones return transformedDrillMembers.filter(member => availableMembers.has(member)); }; } - protected generateIncludeMembers(members: any[], type: string, targetCube?: any) { + protected generateIncludeMembers(members: any[], type: string, targetCube?: CubeDefinitionExtended) { 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`); } - + // Store drill member processing info for later use in the member definition let processedDrillMembers = resolvedMember.drillMembers; - + if (type === 'measures' && resolvedMember.drillMembers && targetCube?.isView) { - const sourceCubeName = path[path.length - 2]; // e.g., "Orders" + const sourceCubeName = path[path.length - 2]; const evaluatedDrillMembers = this.evaluateReferences( sourceCubeName, @@ -864,7 +869,6 @@ export class CubeSymbols implements TranspilerSymbolResolver { { originalSorting: true } ); - // Ensure we have an array const drillMembersArray = Array.isArray(evaluatedDrillMembers) ? evaluatedDrillMembers : [evaluatedDrillMembers]; From 0af57a206fc11d7f6181ac18162476b5f9c45712 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Fri, 12 Sep 2025 17:08:16 +0300 Subject: [PATCH 06/10] Simplified drillMembers filtering for views --- .../src/compiler/CubeSymbols.ts | 75 ++++++------------- 1 file changed, 21 insertions(+), 54 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index eeb985e93c1af..52ecf62b311e1 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -572,6 +572,8 @@ export class CubeSymbols implements TranspilerSymbolResolver { const autoIncludeMembers = new Set(); // `hierarchies` must be processed first + // 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 joinMap: string[][] = []; @@ -804,52 +806,15 @@ export class CubeSymbols implements TranspilerSymbolResolver { return this.symbols[cubeName]?.cubeObj()?.[type]?.[memberName]; } - protected createViewAwareDrillMemberFunction( - originalFunction: Function, - sourceCubeName: string, - targetCubeName: string, - originalDrillMembers: string[] - ) { - const cubeEvaluator = this; - - return function drillMemberFilter(..._args: any[]) { - // Transform source cube references to target cube references - // e.g., "Orders.id" -> "OrdersSimpleView.id" - const transformedDrillMembers = originalDrillMembers.map(member => { - const memberParts = member.split('.'); - if (memberParts[0] === sourceCubeName) { - return `${targetCubeName}.${memberParts[1]}`; - } - return member; // Keep as-is if not from source cube - }); + protected generateIncludeMembers(members: any[], type: string, targetCube: CubeDefinitionExtended) { + const availableDimMembers = new Set(); - // Get the target cube to check which members actually exist - const targetCubeSymbol = cubeEvaluator.symbols[targetCubeName]; - if (!targetCubeSymbol) { - return []; - } - - const targetCube = targetCubeSymbol.cubeObj(); - if (!targetCube) { - return []; - } - - // Build set of available members in the target cube - const availableMembers = new Set(); - ['measures', 'dimensions', 'segments'].forEach(memberType => { - if (targetCube[memberType]) { - Object.keys(targetCube[memberType]).forEach(memberName => { - availableMembers.add(`${targetCubeName}.${memberName}`); - }); - } + if (type === 'measures') { + Object.keys(targetCube.dimensions || {}).forEach(dimName => { + availableDimMembers.add(`${targetCube.name}.${dimName}`); }); + } - // Filter drill members to only include available ones - return transformedDrillMembers.filter(member => availableMembers.has(member)); - }; - } - - protected generateIncludeMembers(members: any[], type: string, targetCube?: CubeDefinitionExtended) { return members.map(memberRef => { const path = memberRef.member.split('.'); const resolvedMember = this.getResolvedMember(type, path[path.length - 2], path[path.length - 1]); @@ -857,10 +822,10 @@ export class CubeSymbols implements TranspilerSymbolResolver { throw new Error(`Can't resolve '${memberRef.member}' while generating include members`); } - // Store drill member processing info for later use in the member definition let processedDrillMembers = resolvedMember.drillMembers; - if (type === 'measures' && resolvedMember.drillMembers && targetCube?.isView) { + // 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( @@ -869,17 +834,19 @@ export class CubeSymbols implements TranspilerSymbolResolver { { originalSorting: true } ); - const drillMembersArray = Array.isArray(evaluatedDrillMembers) + const drillMembersArray = (Array.isArray(evaluatedDrillMembers) ? evaluatedDrillMembers - : [evaluatedDrillMembers]; + : [evaluatedDrillMembers]).map(member => { + const memberParts = member.split('.'); + if (memberParts[0] === sourceCubeName) { + return `${targetCube.name}.${memberParts[1]}`; + } + return member; // Keep as-is if not from source cube + }); - // Create a new filtered function for this view - processedDrillMembers = this.createViewAwareDrillMemberFunction( - resolvedMember.drillMembers, - sourceCubeName, - targetCube.name, - drillMembersArray - ); + const filteredDrillMembers = drillMembersArray.filter(member => availableDimMembers.has(member)); + + processedDrillMembers = () => filteredDrillMembers; } // eslint-disable-next-line no-new-func From d43dce66b22bda5816a07deef0fee69cba314471 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Fri, 12 Sep 2025 17:12:54 +0300 Subject: [PATCH 07/10] fix/correct test comments --- .../integration/postgres/cube-views.test.ts | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts index 38fbe9229b49a..1acf27985dfdc 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts @@ -450,18 +450,16 @@ view(\`OrdersSimpleView\`, { 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']); - + // 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'); - - // Before our fix, this would have been undefined or empty - // After our fix, drill members are properly inherited and renamed to use the view naming + expect(viewCountMeasure.drillMembers).toBeDefined(); expect(Array.isArray(viewCountMeasure.drillMembers)).toBe(true); expect(viewCountMeasure.drillMembers.length).toBeGreaterThan(0); @@ -472,30 +470,27 @@ view(\`OrdersSimpleView\`, { 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 ['id', 'createdAt', 'count'] - should have both id and createdAt + + // 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(); - expect(Array.isArray(countMeasure.drillMembers)).toBe(true); - expect(countMeasure.drillMembers.length).toBeGreaterThan(0); - // Verify drill members are inherited and correctly transformed to use View naming expect(countMeasure.drillMembers).toEqual(['OrdersSimpleView.createdAt']); expect(countMeasure.drillMembersGrouped).toEqual({ From 3e4de5250a87499937fdbbf181016d5b8f86b572 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Fri, 12 Sep 2025 17:31:31 +0300 Subject: [PATCH 08/10] update snapshot --- .../test/unit/__snapshots__/schema.test.ts.snap | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cubejs-schema-compiler/test/unit/__snapshots__/schema.test.ts.snap b/packages/cubejs-schema-compiler/test/unit/__snapshots__/schema.test.ts.snap index b0742584817db..bdd4996198b7f 100644 --- a/packages/cubejs-schema-compiler/test/unit/__snapshots__/schema.test.ts.snap +++ b/packages/cubejs-schema-compiler/test/unit/__snapshots__/schema.test.ts.snap @@ -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", @@ -1863,6 +1858,11 @@ Object { "name": "my_beloved_created_at", "type": "dimensions", }, + Object { + "memberPath": "orders.count", + "name": "count", + "type": "measures", + }, ], "isView": true, "joinMap": Array [], From d8908ff5450085bc735f463c55a6b6c7de08bd13 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 22 Sep 2025 17:14:08 +0300 Subject: [PATCH 09/10] fix drill members inheritance (cases of non-owned members) --- .../src/compiler/CubeSymbols.ts | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index 52ecf62b311e1..49aac8d7ea388 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -192,6 +192,15 @@ type MemberSets = { allMembers: Set; }; +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', @@ -578,8 +587,10 @@ export class CubeSymbols implements TranspilerSymbolResolver { const joinMap: string[][] = []; + 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 @@ -610,6 +621,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) { @@ -627,7 +639,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { } } - const includeMembers = this.generateIncludeMembers(cubeIncludes, type, cube); + const includeMembers = this.generateIncludeMembers(cubeIncludes, type, cube, viewAllMembers); this.applyIncludeMembers(includeMembers, cube, type, errorReporter); const existing = cube.includedMembers ?? []; @@ -677,8 +689,8 @@ export class CubeSymbols implements TranspilerSymbolResolver { errorReporter: ErrorReporter, splitViews: SplitViews, memberSets: MemberSets - ) { - const result: any[] = []; + ): ViewResolvedMember[] { + const result: ViewResolvedMember[] = []; const seen = new Set(); for (const cubeInclude of cubes) { @@ -776,7 +788,8 @@ export class CubeSymbols implements TranspilerSymbolResolver { splitViewDef = splitViews[viewName]; } - const includeMembers = this.generateIncludeMembers(finalIncludes, type, splitViewDef); + const viewAllMembers: ViewResolvedMember[] = []; + const includeMembers = this.generateIncludeMembers(finalIncludes, type, splitViewDef, viewAllMembers); this.applyIncludeMembers(includeMembers, splitViewDef, type, errorReporter); } else { for (const member of finalIncludes) { @@ -792,7 +805,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) { @@ -806,15 +819,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { return this.symbols[cubeName]?.cubeObj()?.[type]?.[memberName]; } - protected generateIncludeMembers(members: any[], type: string, targetCube: CubeDefinitionExtended) { - const availableDimMembers = new Set(); - - if (type === 'measures') { - Object.keys(targetCube.dimensions || {}).forEach(dimName => { - availableDimMembers.add(`${targetCube.name}.${dimName}`); - }); - } - + 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]); @@ -836,15 +841,16 @@ export class CubeSymbols implements TranspilerSymbolResolver { const drillMembersArray = (Array.isArray(evaluatedDrillMembers) ? evaluatedDrillMembers - : [evaluatedDrillMembers]).map(member => { - const memberParts = member.split('.'); - if (memberParts[0] === sourceCubeName) { - return `${targetCube.name}.${memberParts[1]}`; + : [evaluatedDrillMembers]); + + const filteredDrillMembers = drillMembersArray.flatMap(member => { + const found = viewAllMembers.find(v => v.member.endsWith(member)); + if (!found) { + return []; } - return member; // Keep as-is if not from source cube - }); - const filteredDrillMembers = drillMembersArray.filter(member => availableDimMembers.has(member)); + return [`${targetCube.name}.${found.name}`]; + }); processedDrillMembers = () => filteredDrillMembers; } From fde77151f1dad0ee992303b298d73b529a77ef1b Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 22 Sep 2025 17:15:38 +0300 Subject: [PATCH 10/10] fix tests --- .../integration/postgres/cube-views.test.ts | 73 +++++++++++++++++-- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts index 1acf27985dfdc..4b69ca05e9d25 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/cube-views.test.ts @@ -47,7 +47,7 @@ cube(\`Orders\`, { measures: { count: { type: \`count\`, - drillMembers: [id, createdAt] + drillMembers: [id, createdAt, Products.ProductCategories.name] }, runningTotal: { @@ -183,6 +183,10 @@ cube(\`ProductCategories\`, { measures: { count: { type: \`count\`, + }, + count2: { + type: \`count\`, + drillMembers: [id, name] } }, @@ -262,7 +266,28 @@ view(\`OrdersSimpleView\`, { 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(); @@ -441,10 +466,10 @@ view(\`OrdersSimpleView\`, { 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.createdAt']); + expect(countMeasure.drillMembers).toEqual(['OrdersView.id', 'OrdersView.ProductCategories_name']); expect(countMeasure.drillMembersGrouped).toEqual({ measures: [], - dimensions: ['OrdersView.id', 'OrdersView.createdAt'] + dimensions: ['OrdersView.id', 'OrdersView.ProductCategories_name'] }); }); @@ -454,7 +479,7 @@ view(\`OrdersSimpleView\`, { // 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']); + 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'); @@ -498,4 +523,42 @@ view(\`OrdersSimpleView\`, { 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'); + }); });