@@ -419,10 +419,81 @@ export class BaseQuery {
419419 */
420420 get allJoinHints ( ) {
421421 if ( ! this . collectedJoinHints ) {
422- this . collectedJoinHints = [
423- ...this . queryLevelJoinHints ,
424- ...this . collectJoinHints ( ) ,
425- ] ;
422+ const [ rootOfJoin , ...allMembersJoinHints ] = this . collectJoinHintsFromMembers ( this . allMembersConcat ( false ) ) ;
423+ const customSubQueryJoinHints = this . collectJoinHintsFromMembers ( this . joinMembersFromCustomSubQuery ( ) ) ;
424+ let joinMembersJoinHints = this . collectJoinHintsFromMembers ( this . joinMembersFromJoin ( this . join ) ) ;
425+
426+ // One cube may join the other cube via transitive joined cubes,
427+ // members from which are referenced in the join `on` clauses.
428+ // We need to collect such join hints and push them upfront of the joining one
429+ // but only if they don't exist yet. Cause in other case we might affect what
430+ // join path will be constructed in join graph.
431+ // It is important to use queryLevelJoinHints during the calculation if it is set.
432+
433+ const constructJH = ( ) => {
434+ const filteredJoinMembersJoinHints = joinMembersJoinHints . filter ( m => ! allMembersJoinHints . includes ( m ) ) ;
435+ return [
436+ ...this . queryLevelJoinHints ,
437+ ...( rootOfJoin ? [ rootOfJoin ] : [ ] ) ,
438+ ...filteredJoinMembersJoinHints ,
439+ ...allMembersJoinHints ,
440+ ...customSubQueryJoinHints ,
441+ ] ;
442+ } ;
443+
444+ let prevJoins = this . join ;
445+ let prevJoinMembersJoinHints = joinMembersJoinHints ;
446+ let newJoin = this . joinGraph . buildJoin ( constructJH ( ) ) ;
447+
448+ const isOrderPreserved = ( base , updated ) => {
449+ const common = base . filter ( value => updated . includes ( value ) ) ;
450+ const bFiltered = updated . filter ( value => common . includes ( value ) ) ;
451+
452+ return common . every ( ( x , i ) => x === bFiltered [ i ] ) ;
453+ } ;
454+
455+ const isJoinTreesEqual = ( a , b ) => {
456+ if ( ! a || ! b || a . root !== b . root || a . joins . length !== b . joins . length ) {
457+ return false ;
458+ }
459+
460+ // We don't care about the order of joins on the same level, so
461+ // we can compare them as sets.
462+ const aJoinsSet = new Set ( a . joins . map ( j => `${ j . originalFrom } ->${ j . originalTo } ` ) ) ;
463+ const bJoinsSet = new Set ( b . joins . map ( j => `${ j . originalFrom } ->${ j . originalTo } ` ) ) ;
464+
465+ if ( aJoinsSet . size !== bJoinsSet . size ) {
466+ return false ;
467+ }
468+
469+ for ( const val of aJoinsSet ) {
470+ if ( ! bJoinsSet . has ( val ) ) {
471+ return false ;
472+ }
473+ }
474+
475+ return true ;
476+ } ;
477+
478+ // Safeguard against infinite loop in case of cyclic joins somehow managed to slip through
479+ let cnt = 0 ;
480+
481+ while ( newJoin ?. joins . length > 0 && ! isJoinTreesEqual ( prevJoins , newJoin ) && cnt < 10000 ) {
482+ prevJoins = newJoin ;
483+ joinMembersJoinHints = this . collectJoinHintsFromMembers ( this . joinMembersFromJoin ( newJoin ) ) ;
484+ if ( ! isOrderPreserved ( prevJoinMembersJoinHints , joinMembersJoinHints ) ) {
485+ throw new UserError ( `Can not construct joins for the query, potential loop detected: ${ prevJoinMembersJoinHints . join ( '->' ) } vs ${ joinMembersJoinHints . join ( '->' ) } ` ) ;
486+ }
487+ newJoin = this . joinGraph . buildJoin ( constructJH ( ) ) ;
488+ prevJoinMembersJoinHints = joinMembersJoinHints ;
489+ cnt ++ ;
490+ }
491+
492+ if ( cnt >= 10000 ) {
493+ throw new UserError ( 'Can not construct joins for the query, potential loop detected' ) ;
494+ }
495+
496+ this . collectedJoinHints = R . uniq ( constructJH ( ) ) ;
426497 }
427498 return this . collectedJoinHints ;
428499 }
@@ -2429,7 +2500,17 @@ export class BaseQuery {
24292500 } else if ( s . patchedMeasure ?. patchedFrom ) {
24302501 return [ s . patchedMeasure . patchedFrom . cubeName ] . concat ( this . evaluateSymbolSql ( s . patchedMeasure . patchedFrom . cubeName , s . patchedMeasure . patchedFrom . name , s . definition ( ) ) ) ;
24312502 } else {
2432- return this . evaluateSql ( s . cube ( ) . name , s . definition ( ) . sql ) ;
2503+ const res = this . evaluateSql ( s . cube ( ) . name , s . definition ( ) . sql ) ;
2504+ if ( s . isJoinCondition ) {
2505+ // In a join between Cube A and Cube B, sql() may reference members from other cubes.
2506+ // These referenced cubes must be added as join hints before Cube B to ensure correct SQL generation.
2507+ const targetCube = s . targetCubeName ( ) ;
2508+ let { joinHints } = this . safeEvaluateSymbolContext ( ) ;
2509+ joinHints = joinHints . filter ( e => e !== targetCube ) ;
2510+ joinHints . push ( targetCube ) ;
2511+ this . safeEvaluateSymbolContext ( ) . joinHints = joinHints ;
2512+ }
2513+ return res ;
24332514 }
24342515 }
24352516
@@ -2451,7 +2532,17 @@ export class BaseQuery {
24512532 * @returns {Array<Array<string>> }
24522533 */
24532534 collectJoinHints ( excludeTimeDimensions = false ) {
2454- const customSubQueryJoinMembers = this . customSubQueryJoins . map ( j => {
2535+ const membersToCollectFrom = [
2536+ ...this . allMembersConcat ( excludeTimeDimensions ) ,
2537+ ...this . joinMembersFromJoin ( this . join ) ,
2538+ ...this . joinMembersFromCustomSubQuery ( ) ,
2539+ ] ;
2540+
2541+ return this . collectJoinHintsFromMembers ( membersToCollectFrom ) ;
2542+ }
2543+
2544+ joinMembersFromCustomSubQuery ( ) {
2545+ return this . customSubQueryJoins . map ( j => {
24552546 const res = {
24562547 path : ( ) => null ,
24572548 cube : ( ) => this . cubeEvaluator . cubeFromPath ( j . on . cubeName ) ,
@@ -2465,22 +2556,18 @@ export class BaseQuery {
24652556 getMembers : ( ) => [ res ] ,
24662557 } ;
24672558 } ) ;
2559+ }
24682560
2469- const joinMembers = this . join ? this . join . joins . map ( j => ( {
2561+ joinMembersFromJoin ( join ) {
2562+ return join ? join . joins . map ( j => ( {
24702563 getMembers : ( ) => [ {
24712564 path : ( ) => null ,
24722565 cube : ( ) => this . cubeEvaluator . cubeFromPath ( j . originalFrom ) ,
24732566 definition : ( ) => j . join ,
2567+ isJoinCondition : true ,
2568+ targetCubeName : ( ) => j . originalTo ,
24742569 } ]
24752570 } ) ) : [ ] ;
2476-
2477- const membersToCollectFrom = [
2478- ...this . allMembersConcat ( excludeTimeDimensions ) ,
2479- ...joinMembers ,
2480- ...customSubQueryJoinMembers ,
2481- ] ;
2482-
2483- return this . collectJoinHintsFromMembers ( membersToCollectFrom ) ;
24842571 }
24852572
24862573 collectJoinHintsFromMembers ( members ) {
@@ -2885,7 +2972,7 @@ export class BaseQuery {
28852972
28862973 pushJoinHints ( joinHints ) {
28872974 if ( this . safeEvaluateSymbolContext ( ) . joinHints && joinHints ) {
2888- if ( joinHints . length === 1 ) {
2975+ if ( Array . isArray ( joinHints ) && joinHints . length === 1 ) {
28892976 [ joinHints ] = joinHints ;
28902977 }
28912978 this . safeEvaluateSymbolContext ( ) . joinHints . push ( joinHints ) ;
0 commit comments