diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js index a7fdec383d8a5..b017929168129 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js +++ b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js @@ -22,7 +22,6 @@ import { localTimestampToUtc, timeSeries as timeSeriesBase, timeSeriesFromCustomInterval, - parseSqlInterval, findMinGranularityDimension } from '@cubejs-backend/shared'; @@ -437,6 +436,8 @@ export class BaseQuery { get allJoinHints() { if (!this.collectedJoinHints) { const [rootOfJoin, ...allMembersJoinHints] = this.collectJoinHintsFromMembers(this.allMembersConcat(false)); + const allMembersHintsFlattened = [rootOfJoin, ...allMembersJoinHints].flat(); + const originalQueryMembersJoinPredecessors = this.buildPredecessors(allMembersHintsFlattened); const customSubQueryJoinHints = this.collectJoinHintsFromMembers(this.joinMembersFromCustomSubQuery()); let joinMembersJoinHints = this.collectJoinHintsFromMembers(this.joinMembersFromJoin(this.join)); @@ -448,7 +449,7 @@ export class BaseQuery { // It is important to use queryLevelJoinHints during the calculation if it is set. const constructJH = () => { - const filteredJoinMembersJoinHints = joinMembersJoinHints.filter(m => !allMembersJoinHints.includes(m)); + const filteredJoinMembersJoinHints = joinMembersJoinHints.filter(m => !allMembersJoinHints.includes(m) && m !== allMembersHintsFlattened[0]); return [ ...this.queryLevelJoinHints, ...(rootOfJoin ? [rootOfJoin] : []), @@ -458,15 +459,36 @@ export class BaseQuery { ]; }; - let prevJoins = this.join; - let prevJoinMembersJoinHints = joinMembersJoinHints; + let prevJoin = this.join; let newJoin = this.joinGraph.buildJoin(constructJH()); - const isOrderPreserved = (base, updated) => { - const common = base.filter(value => updated.includes(value)); - const bFiltered = updated.filter(value => common.includes(value)); + const isOrderPreserved = (updatedJoinHints) => { + for (let i = 0, l = updatedJoinHints.length; i < l; i++) { + const predecessors = originalQueryMembersJoinPredecessors[updatedJoinHints[i]]; - return common.every((x, i) => x === bFiltered[i]); + if (predecessors?.length > 0) { + const predLen = predecessors.length; + + let predIdx = 0; + let joinHintIdx = 0; + + while (joinHintIdx < i && predIdx < predLen) { + if (updatedJoinHints[joinHintIdx] === predecessors[predIdx]) { + joinHintIdx++; + predIdx++; + } else { + joinHintIdx++; + } + } + + if (predIdx < predLen) { + // We still have a must be present predecessor for current hint + return [false, `${updatedJoinHints[i]} <-> ${predecessors[predIdx]}`]; + } + } + } + + return [true, '']; }; const isJoinTreesEqual = (a, b) => { @@ -495,14 +517,21 @@ export class BaseQuery { // Safeguard against infinite loop in case of cyclic joins somehow managed to slip through let cnt = 0; - while (newJoin?.joins.length > 0 && !isJoinTreesEqual(prevJoins, newJoin) && cnt < 10000) { - prevJoins = newJoin; + while (newJoin?.joins.length > 0 && cnt < 10000) { + prevJoin = newJoin; joinMembersJoinHints = this.collectJoinHintsFromMembers(this.joinMembersFromJoin(newJoin)); - if (!isOrderPreserved(prevJoinMembersJoinHints, joinMembersJoinHints)) { - throw new UserError(`Can not construct joins for the query, potential loop detected: ${prevJoinMembersJoinHints.join('->')} vs ${joinMembersJoinHints.join('->')}`); - } newJoin = this.joinGraph.buildJoin(constructJH()); - prevJoinMembersJoinHints = joinMembersJoinHints; + + if (isJoinTreesEqual(prevJoin, newJoin)) { + break; + } + + const [isOrdered, msg] = isOrderPreserved([allMembersHintsFlattened[0], ...joinMembersJoinHints]); + + if (!isOrdered) { + throw new UserError(`Can not construct joins for the query, potential loop detected around ${msg}`); + } + cnt++; } @@ -538,6 +567,51 @@ export class BaseQuery { ); } + /** + * @private + * @param {Array} arr + * @returns {{}|any} + */ + buildPredecessors(arr) { + if (!arr || arr.length === 0) return {}; + + const root = arr[0]; + + // the first position of each unique element + const firstPos = new Map(); + for (let i = 0; i < arr.length; i++) { + if (!firstPos.has(arr[i])) firstPos.set(arr[i], i); + } + + const result = {}; + + for (const [elem, idx] of firstPos.entries()) { + if (elem === root) { + result[elem] = []; + } else { + // finding the nearest root on the left << + const seen = new Set(); + const path = []; + + for (let j = idx - 1; j >= 0; j--) { + const v = arr[j]; + if (!seen.has(v)) { + seen.add(v); + path.push(v); + } + if (v === root) { + break; + } + } + + path.reverse(); + result[elem] = path; + } + } + + return result; + } + initUngrouped() { this.ungrouped = this.options.ungrouped; if (this.ungrouped) { diff --git a/packages/cubejs-schema-compiler/src/compiler/JoinGraph.ts b/packages/cubejs-schema-compiler/src/compiler/JoinGraph.ts index 39b9acc3296e7..2646398e9f238 100644 --- a/packages/cubejs-schema-compiler/src/compiler/JoinGraph.ts +++ b/packages/cubejs-schema-compiler/src/compiler/JoinGraph.ts @@ -36,14 +36,14 @@ export class JoinGraph { private readonly cubeEvaluator: CubeEvaluator; // source node -> destination node -> weight - private nodes: Record>; + private nodes: Record>; // source node -> destination node -> weight - private undirectedNodes: Record>; + private undirectedNodes: Record>; private edges: Record; - private builtJoins: Record; + private readonly builtJoins: Record; private graph: Graph | null; @@ -60,50 +60,56 @@ export class JoinGraph { this.graph = null; } - public compile(cubes: unknown, errorReporter: ErrorReporter): void { - this.edges = R.compose< - Array, - Array, - Array<[string, JoinEdge][]>, - Array<[string, JoinEdge]>, - Record - >( - R.fromPairs, - R.unnest, - R.map((v: CubeDefinition): [string, JoinEdge][] => this.buildJoinEdges(v, errorReporter.inContext(`${v.name} cube`))), - R.filter(this.cubeValidator.isCubeValid.bind(this.cubeValidator)) - )(this.cubeEvaluator.cubeList); - - // This requires @types/ramda@0.29 or newer - // @ts-ignore - this.nodes = R.compose< - Record, - Array<[string, JoinEdge]>, - Array, - Record | undefined>, - Record> - >( - // This requires @types/ramda@0.29 or newer - // @ts-ignore - R.map(groupedByFrom => R.fromPairs(groupedByFrom.map(join => [join.to, 1]))), - R.groupBy((join: JoinEdge) => join.from), - R.map(v => v[1]), - R.toPairs - // @ts-ignore - )(this.edges); - - // @ts-ignore - this.undirectedNodes = R.compose( - // @ts-ignore - R.map(groupedByFrom => R.fromPairs(groupedByFrom.map(join => [join.from, 1]))), - // @ts-ignore - R.groupBy(join => join.to), - R.unnest, - // @ts-ignore - R.map(v => [v[1], { from: v[1].to, to: v[1].from }]), - R.toPairs - // @ts-ignore - )(this.edges); + public compile(_cubes: unknown, errorReporter: ErrorReporter): void { + this.edges = Object.fromEntries( + this.cubeEvaluator.cubeList + .filter(this.cubeValidator.isCubeValid.bind(this.cubeValidator)) + .flatMap((v: CubeDefinition): [string, JoinEdge][] => this.buildJoinEdges( + v, errorReporter.inContext(`${v.name} cube`) + )) + ); + + const grouped: Record = {}; + + for (const join of Object.values(this.edges)) { + if (!grouped[join.from]) { + grouped[join.from] = []; + } + grouped[join.from].push(join); + } + + this.nodes = Object.fromEntries( + Object.entries(grouped).map(([from, edges]) => [ + from, + Object.fromEntries(edges.map((join) => [join.to, 100])), + ]) + ); + + const undirectedNodesGrouped: Record = {}; + + for (const join of Object.values(this.edges)) { + const reverseJoin: JoinEdge = { + join: join.join, + from: join.to, + to: join.from, + originalFrom: join.originalFrom, + originalTo: join.originalTo, + }; + + for (const e of [join, reverseJoin]) { + if (!undirectedNodesGrouped[e.to]) { + undirectedNodesGrouped[e.to] = []; + } + undirectedNodesGrouped[e.to].push(e); + } + } + + this.undirectedNodes = Object.fromEntries( + Object.entries(undirectedNodesGrouped).map(([to, joins]) => [ + to, + Object.fromEntries(joins.map(join => [join.from, 100])) + ]) + ); this.graph = new Graph(this.nodes); } @@ -158,57 +164,36 @@ export class JoinGraph { }); } - protected buildJoinNode(cube: CubeDefinition): Record { - if (!cube.joins) { - return {}; - } - - return cube.joins.reduce((acc, join) => { - acc[join.name] = 1; - return acc; - }, {} as Record); - } - public buildJoin(cubesToJoin: JoinHints): FinishedJoinTree | null { if (!cubesToJoin.length) { return null; } const key = JSON.stringify(cubesToJoin); if (!this.builtJoins[key]) { - const join = R.pipe< - JoinHints, - Array, - Array, - Array - >( - R.map( - (cube: JoinHint): JoinTree | null => this.buildJoinTreeForRoot(cube, R.without([cube], cubesToJoin)) - ), - // @ts-ignore - R.filter(R.identity), - R.sortBy((joinTree: JoinTree) => joinTree.joins.length) - // @ts-ignore - )(cubesToJoin)[0]; + const join = cubesToJoin + .map((cube: JoinHint): JoinTree | null => this.buildJoinTreeForRoot(cube, cubesToJoin.filter(c => c !== cube))) + .filter((jt): jt is JoinTree => Boolean(jt)) + .sort((a, b) => a.joins.length - b.joins.length)[0]; if (!join) { - throw new UserError(`Can't find join path to join ${cubesToJoin.map(v => `'${v}'`).join(', ')}`); + const errCubes = cubesToJoin.map(v => `'${v}'`).join(', '); + throw new UserError(`Can't find join path to join ${errCubes}`); } this.builtJoins[key] = Object.assign(join, { - multiplicationFactor: R.compose< - JoinHints, - Array<[string, boolean]>, - Record - >( - R.fromPairs, - R.map(v => [this.cubeFromPath(v), this.findMultiplicationFactorFor(this.cubeFromPath(v), join.joins)]) - )(cubesToJoin) + multiplicationFactor: Object.fromEntries( + cubesToJoin.map((v) => { + const cubeName = this.cubeFromPath(v); + const factor = this.findMultiplicationFactorFor(cubeName, join.joins); + return [cubeName, factor]; + }) + ) }); } return this.builtJoins[key]; } - protected cubeFromPath(cubePath) { + protected cubeFromPath(cubePath: string | string[]): string { if (Array.isArray(cubePath)) { return cubePath[cubePath.length - 1]; } @@ -224,6 +209,8 @@ export class JoinGraph { return null; } + const tunedGraph = this.getFixedWeightsGraph([root, ...cubesToJoin]); + if (Array.isArray(root)) { const [newRoot, ...additionalToJoin] = root; if (additionalToJoin.length > 0) { @@ -231,6 +218,7 @@ export class JoinGraph { } root = newRoot; } + const nodesJoined = {}; const result = cubesToJoin.map(joinHints => { if (!Array.isArray(joinHints)) { @@ -243,7 +231,7 @@ export class JoinGraph { return { joins: [] }; } - const path = graph.path(prevNode, toJoin); + const path = tunedGraph.path(prevNode, toJoin); if (!path) { return null; } @@ -296,6 +284,38 @@ export class JoinGraph { }; } + /** + * Returns compiled graph with updated weights for view join-hints + */ + protected getFixedWeightsGraph(joinHints: JoinHints): Graph { + const PRIORITY_WEIGHT = 20; // Lower weight for preferred paths + + // Create a deep copy of this.nodes to avoid modifying the original + const tunedNodes: Record> = {}; + for (const [from, destinations] of Object.entries(this.nodes)) { + tunedNodes[from] = {}; + for (const [to, weight] of Object.entries(destinations)) { + tunedNodes[from][to] = weight; + } + } + + // Update weights only for array hints (view join hints) + for (const hint of joinHints) { + if (Array.isArray(hint) && hint.length > 1) { + for (let i = 0; i < hint.length - 1; i++) { + const from = hint[i]; + const to = hint[i + 1]; + + if (tunedNodes[from]?.[to] !== undefined) { + tunedNodes[from][to] = PRIORITY_WEIGHT; + } + } + } + } + + return new Graph(tunedNodes); + } + protected findMultiplicationFactorFor(cube: string, joins: JoinTreeJoins): boolean { const visited = {}; const self = this; @@ -333,10 +353,11 @@ export class JoinGraph { if (!this.cachedConnectedComponents) { let componentId = 1; const components = {}; - R.toPairs(this.nodes).map(nameToConnection => nameToConnection[0]).forEach(node => { - this.findConnectedComponent(componentId, node, components); - componentId += 1; - }); + Object.entries(this.nodes) + .forEach(([node]) => { + this.findConnectedComponent(componentId, node, components); + componentId += 1; + }); this.cachedConnectedComponents = components; } return this.cachedConnectedComponents; @@ -345,9 +366,8 @@ export class JoinGraph { protected findConnectedComponent(componentId: number, node: string, components: Record): void { if (!components[node]) { components[node] = componentId; - R.toPairs(this.undirectedNodes[node]) - .map(connectedNodeNames => connectedNodeNames[0]) - .forEach(connectedNode => { + Object.entries(this.undirectedNodes[node]) + .forEach(([connectedNode]) => { this.findConnectedComponent(componentId, connectedNode, components); }); } diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/views-join-order-2.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/views-join-order-2.test.ts index 20dac065bec76..65e36fcf8758e 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/views-join-order-2.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/views-join-order-2.test.ts @@ -48,7 +48,7 @@ cube('A', { dimensions: { id: { sql: \`id\`, - type: \`string\`, + type: \`number\`, primaryKey: true, }, name: { @@ -60,7 +60,7 @@ cube('A', { cube('B', { sql: \` - SELECT 2 id, 'b'::text as "name"\`, + SELECT 2 id, 1 fk, 'b'::text as "name"\`, joins: { A: { relationship: \`many_to_one\`, @@ -75,7 +75,7 @@ cube('B', { dimensions: { id: { sql: \`id\`, - type: \`string\`, + type: \`number\`, primaryKey: true, }, name: { @@ -84,7 +84,7 @@ cube('B', { }, fk: { sql: \`fk\`, - type: \`string\`, + type: \`number\`, }, }, }); @@ -95,7 +95,7 @@ cube('E', { dimensions: { id: { sql: \`id\`, - type: \`string\`, + type: \`number\`, primaryKey: true, }, name: { @@ -125,7 +125,7 @@ cube('D', { dimensions: { id: { sql: \`id\`, - type: \`string\`, + type: \`number\`, primaryKey: true, }, name: { @@ -134,11 +134,15 @@ cube('D', { }, fk: { sql: \`fk\`, - type: \`string\`, + type: \`number\`, }, bFk: { sql: \`b_fk\`, - type: \`string\`, + type: \`number\`, + }, + eFk: { + sql: \`e_fk\`, + type: \`number\`, }, }, }); diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/views-join-order-3.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/views-join-order-3.test.ts new file mode 100644 index 0000000000000..2a6967ede7c66 --- /dev/null +++ b/packages/cubejs-schema-compiler/test/integration/postgres/views-join-order-3.test.ts @@ -0,0 +1,328 @@ +import { getEnv } from '@cubejs-backend/shared'; +import { prepareJsCompiler } from '../../unit/PrepareCompiler'; +import { dbRunner } from './PostgresDBRunner'; + +/** + * This tests the cube join correctness for cases, when there are + * multiple equal-cost paths between few cubes via transitive joins. + */ + +describe('Views Join Order 3', () => { + jest.setTimeout(200000); + + const { compiler, joinGraph, cubeEvaluator } = prepareJsCompiler( + // language=JavaScript + ` +cube(\`D\`, { + sql: \`SELECT 1 id, 125 as balance\`, + dimensions: { + Balance: { + sql: \`balance\`, + type: \`number\` + } + } +}); + +cube(\`A\`, { + sql: \`SELECT 1 id, 250 as balance\`, + joins: { + E: { + sql: \`\${CUBE}.id = \${E}.id\`, + relationship: \`many_to_one\` + }, + B: { + sql: \`\${CUBE}.id = \${B}.id\`, + relationship: \`many_to_one\` + }, + C: { + sql: \`\${CUBE}.id = \${C}.id\`, + relationship: \`many_to_one\` + } + }, + dimensions: { + Balance: { + sql: \`balance\`, + type: \`number\` + } + } +}); + +cube('B', { + sql: \`SELECT 1 id\`, + joins: { + D: { + sql: \`\${CUBE}.id = \${D}.id\`, + relationship: \`many_to_one\` + }, + E: { + sql: \`\${CUBE}.id = \${E}.id\`, + relationship: \`many_to_one\` + } + }, + dimensions: { + ActivityBalance: { + sql: \`\${D.Balance}\`, + type: \`number\` + } + } +}); + +cube(\`E\`, { + sql: \`SELECT 1 id, 1 as plan_id, 1 as party_id\`, + joins: { + D: { + sql: \`\${CUBE}.id = \${D}.id\`, + relationship: \`many_to_one\` + }, + F: { + sql: \`\${CUBE}.plan_id = \${F}.plan_id\`, + relationship: \`many_to_one\` + }, + C: { + sql: \`\${CUBE}.party_id = \${C}.party_id\`, + relationship: \`many_to_one\` + } + } +}); + +cube('C', { + sql: \`SELECT 1 id, 1 as plan_id, 1 as party_id\`, + joins: { + F: { + sql: \`\${CUBE}.plan_id = \${F}.plan_id\`, + relationship: \`many_to_one\` + } + } +}); + +cube(\`F\`, { + sql: \`SELECT 1 id, 1 as plan_id, 'PLAN_CODE'::text as plan_code\`, + dimensions: { + PlanCode: { + sql: \`plan_code\`, + type: \`string\` + } + } +}); + +view(\`V\`, { + cubes: [ + { + join_path: A.B, + includes: [\`ActivityBalance\`] + }, + { + join_path: A.C.F, + includes: [\`PlanCode\`] + } + ] +}); + ` + ); + + if (getEnv('nativeSqlPlanner')) { + it('correct join for simple cube B dimension', async () => { + const [sql, _params] = await dbRunner.runQueryTest({ + dimensions: ['B.ActivityBalance'], + timeDimensions: [], + segments: [], + filters: [], + }, [{ + b__activity_balance: 125, + }], { compiler, joinGraph, cubeEvaluator }); + + expect(sql).toMatch(/AS "b"/); + expect(sql).toMatch(/AS "d"/); + expect(sql).toMatch(/ON "b".id = "d".id/); + expect(sql).not.toMatch(/AS "a"/); + expect(sql).not.toMatch(/AS "e"/); + expect(sql).not.toMatch(/AS "c"/); + }); + + it('correct join for simple view B-dimension', async () => { + const [sql, _params] = await dbRunner.runQueryTest({ + dimensions: ['V.ActivityBalance'], + timeDimensions: [], + segments: [], + filters: [], + }, [{ + v__activity_balance: 125, + }], { compiler, joinGraph, cubeEvaluator }); + + // expect(sql).toMatch(/AS "a"/); + // expect(sql).toMatch(/AS "b"/); + // expect(sql).toMatch(/AS "d"/); + // expect(sql).toMatch(/ON "a".id = "b".id/); + // expect(sql).toMatch(/ON "b".id = "d".id/); + // expect(sql).not.toMatch(/AS "e"/); + }); + + it('correct join for simple view F-dimension', async () => { + const [sql, _params] = await dbRunner.runQueryTest({ + dimensions: ['V.PlanCode'], + timeDimensions: [], + segments: [], + filters: [], + }, [{ + v__plan_code: 'PLAN_CODE', + }], { compiler, joinGraph, cubeEvaluator }); + + // expect(sql).toMatch(/AS "a"/); + // expect(sql).toMatch(/AS "c"/); + // expect(sql).toMatch(/AS "f"/); + // expect(sql).toMatch(/ON "a".id = "c".id/); + // expect(sql).toMatch(/ON "c".plan_id = "f".plan_id/); + // expect(sql).not.toMatch(/AS "b"/); + // expect(sql).not.toMatch(/AS "d"/); + // expect(sql).not.toMatch(/AS "e"/); + }); + + it('correct join for view F-dimension + B-dimension', async () => { + const [sql, _params] = await dbRunner.runQueryTest({ + dimensions: ['V.PlanCode', 'V.ActivityBalance'], + timeDimensions: [], + segments: [], + filters: [], + }, [{ + v__plan_code: 'PLAN_CODE', + v__activity_balance: 125, + }], { compiler, joinGraph, cubeEvaluator }); + + // expect(sql).toMatch(/AS "a"/); + // expect(sql).toMatch(/AS "c"/); + // expect(sql).toMatch(/AS "f"/); + // expect(sql).toMatch(/AS "b"/); + // expect(sql).toMatch(/AS "d"/); + // expect(sql).toMatch(/ON "a".id = "c".id/); + // expect(sql).toMatch(/ON "a".id = "b".id/); + // expect(sql).toMatch(/ON "c".plan_id = "f".plan_id/); + // expect(sql).toMatch(/ON "b".id = "d".id/); + // expect(sql).not.toMatch(/AS "e"/); + }); + + it('correct join for view B-dimension + F-dimension', async () => { + const [sql, _params] = await dbRunner.runQueryTest({ + dimensions: ['V.ActivityBalance', 'V.PlanCode'], + timeDimensions: [], + segments: [], + filters: [], + }, [{ + v__activity_balance: 125, + v__plan_code: 'PLAN_CODE', + }], { compiler, joinGraph, cubeEvaluator }); + + // expect(sql).toMatch(/AS "a"/); + // expect(sql).toMatch(/AS "c"/); + // expect(sql).toMatch(/AS "f"/); + // expect(sql).toMatch(/AS "b"/); + // expect(sql).toMatch(/AS "d"/); + // expect(sql).toMatch(/ON "a".id = "c".id/); + // expect(sql).toMatch(/ON "a".id = "b".id/); + // expect(sql).toMatch(/ON "c".plan_id = "f".plan_id/); + // expect(sql).toMatch(/ON "b".id = "d".id/); + // expect(sql).not.toMatch(/AS "e"/); + }); + } else { + it('correct join for simple cube B dimension', async () => { + const [sql, _params] = await dbRunner.runQueryTest({ + dimensions: ['B.ActivityBalance'], + timeDimensions: [], + segments: [], + filters: [], + }, [{ + b___activity_balance: 125, + }], { compiler, joinGraph, cubeEvaluator }); + + expect(sql).toMatch(/AS "b"/); + expect(sql).toMatch(/AS "d"/); + expect(sql).toMatch(/ON "b".id = "d".id/); + expect(sql).not.toMatch(/AS "a"/); + expect(sql).not.toMatch(/AS "e"/); + expect(sql).not.toMatch(/AS "c"/); + }); + + it('correct join for simple view B-dimension', async () => { + const [sql, _params] = await dbRunner.runQueryTest({ + dimensions: ['V.ActivityBalance'], + timeDimensions: [], + segments: [], + filters: [], + }, [{ + v___activity_balance: 125, + }], { compiler, joinGraph, cubeEvaluator }); + + expect(sql).toMatch(/AS "a"/); + expect(sql).toMatch(/AS "b"/); + expect(sql).toMatch(/AS "d"/); + expect(sql).toMatch(/ON "a".id = "b".id/); + expect(sql).toMatch(/ON "b".id = "d".id/); + expect(sql).not.toMatch(/AS "e"/); + }); + + it('correct join for simple view F-dimension', async () => { + const [sql, _params] = await dbRunner.runQueryTest({ + dimensions: ['V.PlanCode'], + timeDimensions: [], + segments: [], + filters: [], + }, [{ + v___plan_code: 'PLAN_CODE', + }], { compiler, joinGraph, cubeEvaluator }); + + expect(sql).toMatch(/AS "a"/); + expect(sql).toMatch(/AS "c"/); + expect(sql).toMatch(/AS "f"/); + expect(sql).toMatch(/ON "a".id = "c".id/); + expect(sql).toMatch(/ON "c".plan_id = "f".plan_id/); + expect(sql).not.toMatch(/AS "b"/); + expect(sql).not.toMatch(/AS "d"/); + expect(sql).not.toMatch(/AS "e"/); + }); + + it('correct join for view F-dimension + B-dimension', async () => { + const [sql, _params] = await dbRunner.runQueryTest({ + dimensions: ['V.PlanCode', 'V.ActivityBalance'], + timeDimensions: [], + segments: [], + filters: [], + }, [{ + v___plan_code: 'PLAN_CODE', + v___activity_balance: 125, + }], { compiler, joinGraph, cubeEvaluator }); + + expect(sql).toMatch(/AS "a"/); + expect(sql).toMatch(/AS "c"/); + expect(sql).toMatch(/AS "f"/); + expect(sql).toMatch(/AS "b"/); + expect(sql).toMatch(/AS "d"/); + expect(sql).toMatch(/ON "a".id = "c".id/); + expect(sql).toMatch(/ON "a".id = "b".id/); + expect(sql).toMatch(/ON "c".plan_id = "f".plan_id/); + expect(sql).toMatch(/ON "b".id = "d".id/); + expect(sql).not.toMatch(/AS "e"/); + }); + + it('correct join for view B-dimension + F-dimension', async () => { + const [sql, _params] = await dbRunner.runQueryTest({ + dimensions: ['V.ActivityBalance', 'V.PlanCode'], + timeDimensions: [], + segments: [], + filters: [], + }, [{ + v___activity_balance: 125, + v___plan_code: 'PLAN_CODE', + }], { compiler, joinGraph, cubeEvaluator }); + + expect(sql).toMatch(/AS "a"/); + expect(sql).toMatch(/AS "c"/); + expect(sql).toMatch(/AS "f"/); + expect(sql).toMatch(/AS "b"/); + expect(sql).toMatch(/AS "d"/); + expect(sql).toMatch(/ON "a".id = "c".id/); + expect(sql).toMatch(/ON "a".id = "b".id/); + expect(sql).toMatch(/ON "c".plan_id = "f".plan_id/); + expect(sql).toMatch(/ON "b".id = "d".id/); + expect(sql).not.toMatch(/AS "e"/); + }); + } +});