Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 88 additions & 14 deletions packages/cubejs-schema-compiler/src/adapter/BaseQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
localTimestampToUtc,
timeSeries as timeSeriesBase,
timeSeriesFromCustomInterval,
parseSqlInterval,
findMinGranularityDimension
} from '@cubejs-backend/shared';

Expand Down Expand Up @@ -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));

Expand All @@ -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] : []),
Expand All @@ -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) => {
Expand Down Expand Up @@ -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++;
}

Expand Down Expand Up @@ -538,6 +567,51 @@ export class BaseQuery {
);
}

/**
* @private
* @param {Array<string>} 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) {
Expand Down
Loading
Loading