From 8fd97e607fdbfe27312ed6d9a04327813904184d 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 1161716d01378..2e38fbbd9a0cf 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -810,6 +810,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 b31477a172eee99e05f266328a63f9d9ffe092ac 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 --- .../src/compiler/CubeSymbols.ts | 5 +- .../src/compiler/CubeToMetaTransformer.js | 26 ++++++++ .../src/compiler/ErrorReporter.ts | 2 +- .../integration/postgres/cube-views.test.ts | 65 +++++++++++++++++++ 4 files changed, 94 insertions(+), 4 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index 2e38fbbd9a0cf..40d92bd7e6676 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -894,8 +894,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])); @@ -1005,7 +1004,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 798b14faf2b3d..d02db4400f265 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js +++ b/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js @@ -206,6 +206,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/src/compiler/ErrorReporter.ts b/packages/cubejs-schema-compiler/src/compiler/ErrorReporter.ts index 94b96e278372f..c225eb7a9f55c 100644 --- a/packages/cubejs-schema-compiler/src/compiler/ErrorReporter.ts +++ b/packages/cubejs-schema-compiler/src/compiler/ErrorReporter.ts @@ -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') ); } } 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 5fc2c094f3acc0e838795c09d0d11aa2bd311490 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 40d92bd7e6676..4c2227d698116 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -894,7 +894,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])); @@ -1004,7 +1004,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 d02db4400f265..a02d984d8531f 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js +++ b/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js @@ -226,8 +226,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 ed8b70c5ea6a5d11e0e4321fe383352d99fe9865 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 4c2227d698116..ad5a86716db3e 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -606,7 +606,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 ?? []; @@ -757,7 +757,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) { @@ -787,13 +787,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}}\`;`); @@ -810,7 +881,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 a02d984d8531f..798b14faf2b3d 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js +++ b/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js @@ -206,31 +206,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 e2261575cb9d88ab6ebf1075f173100e044f6ace 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 ad5a86716db3e..f71af02852fd2 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -180,6 +180,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', @@ -553,14 +558,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']; for (const type of types) { let cubeIncludes: any[] = []; @@ -657,7 +662,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { type: string, errorReporter: ErrorReporter, splitViews: SplitViews, - memberSets: any + memberSets: MemberSets ) { const result: any[] = []; const seen = new Set(); @@ -805,18 +810,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 => { @@ -826,25 +831,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, @@ -852,7 +857,6 @@ export class CubeSymbols implements TranspilerSymbolResolver { { originalSorting: true } ); - // Ensure we have an array const drillMembersArray = Array.isArray(evaluatedDrillMembers) ? evaluatedDrillMembers : [evaluatedDrillMembers]; From 09fcec76f311bdc9211566b1f6dfb5f3e1369e44 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 f71af02852fd2..cdb2d03059bf6 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -565,6 +565,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']; for (const type of types) { @@ -792,52 +794,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]); @@ -845,10 +810,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( @@ -857,17 +822,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 e0a493f1ee030c5459d2e99c15445a9d5436451f 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 ca9d3b7a2babdea700012a625e9826abddc16e5e 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 7f79bc7421f29..b682c97df2af9 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, "joins": Array [], From 837c4c2bf58090995cb837385fda3d4581479837 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 cdb2d03059bf6..6e13a15c691b8 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -185,6 +185,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', @@ -569,8 +578,10 @@ export class CubeSymbols implements TranspilerSymbolResolver { // 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 @@ -596,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) { @@ -613,7 +625,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 ?? []; @@ -665,8 +677,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) { @@ -764,7 +776,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) { @@ -780,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) { @@ -794,15 +807,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]); @@ -824,15 +829,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 4e37f9db28a5659e93790bca0782a27515bb2ecd 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'); + }); });