@@ -419,10 +419,81 @@ export class BaseQuery {
419
419
*/
420
420
get allJoinHints ( ) {
421
421
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 ( ) ) ;
426
497
}
427
498
return this . collectedJoinHints ;
428
499
}
@@ -2429,7 +2500,17 @@ export class BaseQuery {
2429
2500
} else if ( s . patchedMeasure ?. patchedFrom ) {
2430
2501
return [ s . patchedMeasure . patchedFrom . cubeName ] . concat ( this . evaluateSymbolSql ( s . patchedMeasure . patchedFrom . cubeName , s . patchedMeasure . patchedFrom . name , s . definition ( ) ) ) ;
2431
2502
} 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 ;
2433
2514
}
2434
2515
}
2435
2516
@@ -2451,7 +2532,17 @@ export class BaseQuery {
2451
2532
* @returns {Array<Array<string>> }
2452
2533
*/
2453
2534
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 => {
2455
2546
const res = {
2456
2547
path : ( ) => null ,
2457
2548
cube : ( ) => this . cubeEvaluator . cubeFromPath ( j . on . cubeName ) ,
@@ -2465,22 +2556,18 @@ export class BaseQuery {
2465
2556
getMembers : ( ) => [ res ] ,
2466
2557
} ;
2467
2558
} ) ;
2559
+ }
2468
2560
2469
- const joinMembers = this . join ? this . join . joins . map ( j => ( {
2561
+ joinMembersFromJoin ( join ) {
2562
+ return join ? join . joins . map ( j => ( {
2470
2563
getMembers : ( ) => [ {
2471
2564
path : ( ) => null ,
2472
2565
cube : ( ) => this . cubeEvaluator . cubeFromPath ( j . originalFrom ) ,
2473
2566
definition : ( ) => j . join ,
2567
+ isJoinCondition : true ,
2568
+ targetCubeName : ( ) => j . originalTo ,
2474
2569
} ]
2475
2570
} ) ) : [ ] ;
2476
-
2477
- const membersToCollectFrom = [
2478
- ...this . allMembersConcat ( excludeTimeDimensions ) ,
2479
- ...joinMembers ,
2480
- ...customSubQueryJoinMembers ,
2481
- ] ;
2482
-
2483
- return this . collectJoinHintsFromMembers ( membersToCollectFrom ) ;
2484
2571
}
2485
2572
2486
2573
collectJoinHintsFromMembers ( members ) {
@@ -2885,7 +2972,7 @@ export class BaseQuery {
2885
2972
2886
2973
pushJoinHints ( joinHints ) {
2887
2974
if ( this . safeEvaluateSymbolContext ( ) . joinHints && joinHints ) {
2888
- if ( joinHints . length === 1 ) {
2975
+ if ( Array . isArray ( joinHints ) && joinHints . length === 1 ) {
2889
2976
[ joinHints ] = joinHints ;
2890
2977
}
2891
2978
this . safeEvaluateSymbolContext ( ) . joinHints . push ( joinHints ) ;
0 commit comments