From 6de2a231efbd15e9d28fd3e7eb904b8539b1d32d Mon Sep 17 00:00:00 2001 From: Pavel Tiunov Date: Wed, 25 Oct 2023 19:55:36 -0700 Subject: [PATCH] feat: Split view support --- .../src/compiler/CubeSymbols.js | 58 ++++++++++++++----- .../src/compiler/CubeValidator.js | 5 +- .../integration/postgres/cube-views.test.ts | 20 +++++++ 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js index 194eb3a6dca97..62a25f3ccc666 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js @@ -37,7 +37,13 @@ export class CubeSymbols { R.sortBy(c => !!c.isView), )(cubes); for (const cube of sortedByDependency) { - this.symbols[cube.name] = this.transform(cube.name, errorReporter.inContext(`${cube.name} cube`)); + const splitViews = {}; + this.symbols[cube.name] = this.transform(cube.name, errorReporter.inContext(`${cube.name} cube`), splitViews); + for (const viewName of Object.keys(splitViews)) { + // TODO can we define it when cubeList is defined? + this.cubeList.push(splitViews[viewName]); + this.symbols[viewName] = splitViews[viewName]; + } } } @@ -110,7 +116,7 @@ export class CubeSymbols { return cubeObject; } - transform(cubeName, errorReporter) { + transform(cubeName, errorReporter, splitViews) { const cube = this.getCubeDefinition(cubeName); const duplicateNames = R.compose( R.map(nameToDefinitions => nameToDefinitions[0]), @@ -138,7 +144,7 @@ export class CubeSymbols { } if (this.evaluateViews) { - this.prepareIncludes(cube, errorReporter); + this.prepareIncludes(cube, errorReporter, splitViews); } return Object.assign( @@ -209,24 +215,28 @@ export class CubeSymbols { /** * @protected */ - prepareIncludes(cube, errorReporter) { + prepareIncludes(cube, errorReporter, splitViews) { if (!cube.includes && !cube.cubes) { return; } const types = ['measures', 'dimensions', 'segments']; for (const type of types) { - const cubeIncludes = cube.cubes && this.membersFromCubes(cube.cubes, type, errorReporter) || []; + const cubeIncludes = cube.cubes && this.membersFromCubes(cube, cube.cubes, type, errorReporter, splitViews) || []; const includes = cube.includes && this.membersFromIncludeExclude(cube.includes, cube.name, type) || []; const excludes = cube.excludes && this.membersFromIncludeExclude(cube.excludes, cube.name, type) || []; // cube includes will take precedence in case of member clash const finalIncludes = this.diffByMember(this.diffByMember(includes, cubeIncludes).concat(cubeIncludes), excludes); const includeMembers = this.generateIncludeMembers(finalIncludes, cube.name, type); - for (const [memberName, memberDefinition] of includeMembers) { - if (cube[type]?.[memberName]) { - errorReporter.error(`Included member '${memberName}' conflicts with existing member of '${cube.name}'. Please consider excluding this member.`); - } else { - cube[type][memberName] = memberDefinition; - } + this.applyIncludeMembers(includeMembers, cube, type, errorReporter); + } + } + + applyIncludeMembers(includeMembers, cube, type, errorReporter) { + for (const [memberName, memberDefinition] of includeMembers) { + if (cube[type]?.[memberName]) { + errorReporter.error(`Included member '${memberName}' conflicts with existing member of '${cube.name}'. Please consider excluding this member.`); + } else { + cube[type][memberName] = memberDefinition; } } } @@ -234,7 +244,7 @@ export class CubeSymbols { /** * @protected */ - membersFromCubes(cubes, type, errorReporter) { + membersFromCubes(parentCube, cubes, type, errorReporter, splitViews) { return R.unnest(cubes.map(cubeInclude => { const fullPath = this.evaluateReferences(null, cubeInclude.joinPath, { collectJoinHints: true }); const split = fullPath.split('.'); @@ -277,7 +287,29 @@ export class CubeSymbols { member: `${cubeReference}.${exclude}` } : undefined; }); - return this.diffByMember(includes.filter(Boolean), excludes.filter(Boolean)); + + const finalIncludes = this.diffByMember(includes.filter(Boolean), excludes.filter(Boolean)); + + if (cubeInclude.split) { + const viewName = `${parentCube.name}_${cubeName}`; + let splitViewDef = splitViews[viewName]; + if (!splitViewDef) { + splitViews[viewName] = this.createCube({ + name: viewName, + isView: true, + // TODO might worth adding to validation as it goes around it right now + isSplitView: true, + }); + splitViewDef = splitViews[viewName]; + } + + const includeMembers = this.generateIncludeMembers(finalIncludes, parentCube.name, type); + this.applyIncludeMembers(includeMembers, splitViewDef, type, errorReporter); + + return []; + } else { + return finalIncludes; + } })); } diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.js b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.js index 5b9b435493064..1eb3382579f26 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.js +++ b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.js @@ -541,6 +541,7 @@ const viewSchema = inherit(baseSchema, { Joi.object().keys({ joinPath: Joi.func().required(), prefix: Joi.boolean(), + split: Joi.boolean(), alias: Joi.string(), includes: Joi.alternatives([ Joi.string().valid('*'), @@ -553,6 +554,8 @@ const viewSchema = inherit(baseSchema, { ])) ]).required(), excludes: Joi.array().items(Joi.string().required()), + }).oxor('split', 'prefix').messages({ + 'object.oxor': 'Using split together with prefix is not supported' }) ), }); @@ -641,6 +644,6 @@ export class CubeValidator { } isCubeValid(cube) { - return this.validCubes[cube.name]; + return this.validCubes[cube.name] || cube.isSplitView; } } 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 f584e3bbe2f28..cb0f60a240dc1 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 @@ -235,6 +235,17 @@ view(\`OrdersView\`, { view(\`OrdersView2\`, { includes: [Orders.count], }); + +view(\`OrdersView3\`, { + cubes: [{ + join_path: Orders, + includes: '*' + }, { + join_path: Orders.Products.ProductCategories, + includes: '*', + split: true + }] +}); `); async function runQueryTest(q: any, expectedResult: any, additionalTest?: (query: BaseQuery) => any) { @@ -400,4 +411,13 @@ view(\`OrdersView2\`, { const cube = metaTransformer.cubes.find(c => c.config.name === 'Orders'); expect(cube.config.measures.filter((({ isVisible }) => isVisible)).length).toBe(0); }); + + it('split views', async () => runQueryTest({ + measures: ['OrdersView3.count'], + dimensions: ['OrdersView3_ProductCategories.name'], + order: [{ id: 'OrdersView3_ProductCategories.name' }], + }, [{ + orders_view3__count: '2', + orders_view3__product_categories__name: 'Groceries', + }])); });