From 10b40e7dc29b69c3566227129e4c2ad594376c24 Mon Sep 17 00:00:00 2001 From: Paco Valdez Date: Wed, 10 Sep 2025 10:33:54 -0700 Subject: [PATCH 1/4] 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 5330b8f9120ec..7fa9535007ef4 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -809,6 +809,8 @@ export class CubeSymbols { ...(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 b33b038efe1d05535e9e06ec79d7f546a7581f3c Mon Sep 17 00:00:00 2001 From: Paco Valdez Date: Wed, 10 Sep 2025 15:21:22 -0700 Subject: [PATCH 2/4] 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 7fa9535007ef4..9deefadffe595 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -893,8 +893,7 @@ export class CubeSymbols { 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])); @@ -1004,7 +1003,7 @@ export class CubeSymbols { 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 fb5ac95a7b2d0..720fde1121655 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 104c7fc15fed19128d6bd330d0a77dce4c211a15 Mon Sep 17 00:00:00 2001 From: Paco Valdez Date: Thu, 11 Sep 2025 13:11:21 -0700 Subject: [PATCH 3/4] 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 9deefadffe595..750ae921bb648 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -893,7 +893,7 @@ export class CubeSymbols { 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])); @@ -1003,7 +1003,7 @@ export class CubeSymbols { 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 aa6bc443cfbc127d015197af8a72cd00f0ad3412 Mon Sep 17 00:00:00 2001 From: Paco Valdez Date: Thu, 11 Sep 2025 14:18:33 -0700 Subject: [PATCH 4/4] 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 750ae921bb648..3b68ad49507db 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -605,7 +605,7 @@ export class CubeSymbols { } } - const includeMembers = this.generateIncludeMembers(cubeIncludes, type); + const includeMembers = this.generateIncludeMembers(cubeIncludes, type, cube); this.applyIncludeMembers(includeMembers, cube, type, errorReporter); const existing = cube.includedMembers ?? []; @@ -756,7 +756,7 @@ export class CubeSymbols { 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) { @@ -786,13 +786,84 @@ export class CubeSymbols { 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}}\`;`); @@ -809,7 +880,7 @@ export class CubeSymbols { ...(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');