diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index 1161716d01378..6e13a15c691b8 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -180,6 +180,20 @@ export interface CubeSymbolsBase { export type CubeSymbolsDefinition = CubeSymbolsBase & Record; +type MemberSets = { + resolvedMembers: Set; + 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', @@ -553,17 +567,21 @@ 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']; + // 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 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 @@ -589,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) { @@ -606,7 +625,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { } } - const includeMembers = this.generateIncludeMembers(cubeIncludes, type); + const includeMembers = this.generateIncludeMembers(cubeIncludes, type, cube, viewAllMembers); this.applyIncludeMembers(includeMembers, cube, type, errorReporter); const existing = cube.includedMembers ?? []; @@ -657,9 +676,9 @@ export class CubeSymbols implements TranspilerSymbolResolver { type: string, errorReporter: ErrorReporter, splitViews: SplitViews, - memberSets: any - ) { - const result: any[] = []; + memberSets: MemberSets + ): ViewResolvedMember[] { + const result: ViewResolvedMember[] = []; const seen = new Set(); for (const cubeInclude of cubes) { @@ -757,7 +776,8 @@ export class CubeSymbols implements TranspilerSymbolResolver { splitViewDef = splitViews[viewName]; } - const includeMembers = this.generateIncludeMembers(finalIncludes, type); + const viewAllMembers: ViewResolvedMember[] = []; + const includeMembers = this.generateIncludeMembers(finalIncludes, type, splitViewDef, viewAllMembers); this.applyIncludeMembers(includeMembers, splitViewDef, type, errorReporter); } else { for (const member of finalIncludes) { @@ -773,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) { @@ -787,7 +807,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { return this.symbols[cubeName]?.cubeObj()?.[type]?.[memberName]; } - protected generateIncludeMembers(members: any[], type: string) { + 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]); @@ -795,6 +815,34 @@ export class CubeSymbols implements TranspilerSymbolResolver { throw new Error(`Can't resolve '${memberRef.member}' while generating include members`); } + let processedDrillMembers = resolvedMember.drillMembers; + + // 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( + sourceCubeName, + resolvedMember.drillMembers, + { originalSorting: true } + ); + + const drillMembersArray = (Array.isArray(evaluatedDrillMembers) + ? evaluatedDrillMembers + : [evaluatedDrillMembers]); + + const filteredDrillMembers = drillMembersArray.flatMap(member => { + const found = viewAllMembers.find(v => v.member.endsWith(member)); + if (!found) { + return []; + } + + return [`${targetCube.name}.${found.name}`]; + }); + + processedDrillMembers = () => filteredDrillMembers; + } + // eslint-disable-next-line no-new-func const sql = new Function(path[0], `return \`\${${memberRef.member}}\`;`); let memberDefinition; @@ -810,6 +858,8 @@ export class CubeSymbols implements TranspilerSymbolResolver { ...(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 = { @@ -892,8 +942,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { 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])); @@ -1003,7 +1052,7 @@ export class CubeSymbols implements TranspilerSymbolResolver { 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 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 049e938ba2243..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] } }, @@ -255,7 +259,35 @@ view(\`OrdersView3\`, { split: true }] }); - `); + +view(\`OrdersSimpleView\`, { + cubes: [{ + join_path: Orders, + 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(); @@ -429,4 +461,104 @@ 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.ProductCategories_name']); + expect(countMeasure.drillMembersGrouped).toEqual({ + measures: [], + dimensions: ['OrdersView.id', 'OrdersView.ProductCategories_name'] + }); + }); + + 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', 'ProductCategories.name']); + + // 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'); + + 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 '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(); + // 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'] + }); + }); + + 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'); + }); }); 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 [],