diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index 5330b8f9120ec..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,6 +880,8 @@ export class CubeSymbols { ...(resolvedMember.multiStage && { multiStage: resolvedMember.multiStage }), ...(resolvedMember.timeShift && { timeShift: resolvedMember.timeShift }), ...(resolvedMember.orderBy && { orderBy: resolvedMember.orderBy }), + ...(processedDrillMembers && { drillMembers: processedDrillMembers }), + ...(resolvedMember.drillMembersGrouped && { drillMembersGrouped: resolvedMember.drillMembersGrouped }), }; } else if (type === 'dimensions') { memberDefinition = { @@ -891,8 +964,7 @@ export class CubeSymbols { name ); // eslint-disable-next-line no-underscore-dangle - // if (resolvedSymbol && resolvedSymbol._objectWithResolvedProperties) { - if (resolvedSymbol._objectWithResolvedProperties) { + if (resolvedSymbol?._objectWithResolvedProperties) { return resolvedSymbol; } return cubeEvaluator.pathFromArray(fullPath(cubeEvaluator.joinHints(), [referencedCube, name])); @@ -1002,7 +1074,7 @@ export class CubeSymbols { cubeName, name ); - if (resolvedSymbol._objectWithResolvedProperties) { + if (resolvedSymbol?._objectWithResolvedProperties) { return resolvedSymbol; } 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 049e938ba2243..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 @@ -47,7 +47,7 @@ cube(\`Orders\`, { measures: { count: { type: \`count\`, - //drillMembers: [id, createdAt] + drillMembers: [id, createdAt] }, runningTotal: { @@ -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) { @@ -429,4 +436,71 @@ 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'] + }); + }); + + 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) || []; + + // 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'] + }); + }); });