@@ -437,6 +437,8 @@ export class BaseQuery {
437437 get allJoinHints ( ) {
438438 if ( ! this . collectedJoinHints ) {
439439 const [ rootOfJoin , ...allMembersJoinHints ] = this . collectJoinHintsFromMembers ( this . allMembersConcat ( false ) ) ;
440+ const allMembersHintsFlattened = [ rootOfJoin , ...allMembersJoinHints ] . flat ( ) ;
441+ const originalQueryMembersJoinPredecessors = this . buildPredecessors ( allMembersHintsFlattened ) ;
440442 const customSubQueryJoinHints = this . collectJoinHintsFromMembers ( this . joinMembersFromCustomSubQuery ( ) ) ;
441443 let joinMembersJoinHints = this . collectJoinHintsFromMembers ( this . joinMembersFromJoin ( this . join ) ) ;
442444
@@ -448,7 +450,7 @@ export class BaseQuery {
448450 // It is important to use queryLevelJoinHints during the calculation if it is set.
449451
450452 const constructJH = ( ) => {
451- const filteredJoinMembersJoinHints = joinMembersJoinHints . filter ( m => ! allMembersJoinHints . includes ( m ) ) ;
453+ const filteredJoinMembersJoinHints = joinMembersJoinHints . filter ( m => ! allMembersJoinHints . includes ( m ) && m !== allMembersHintsFlattened [ 0 ] ) ;
452454 return [
453455 ...this . queryLevelJoinHints ,
454456 ...( rootOfJoin ? [ rootOfJoin ] : [ ] ) ,
@@ -458,15 +460,36 @@ export class BaseQuery {
458460 ] ;
459461 } ;
460462
461- let prevJoins = this . join ;
462- let prevJoinMembersJoinHints = joinMembersJoinHints ;
463+ let prevJoin = this . join ;
463464 let newJoin = this . joinGraph . buildJoin ( constructJH ( ) ) ;
464465
465- const isOrderPreserved = ( base , updated ) => {
466- const common = base . filter ( value => updated . includes ( value ) ) ;
467- const bFiltered = updated . filter ( value => common . includes ( value ) ) ;
466+ const isOrderPreserved = ( updatedJoinHints ) => {
467+ for ( let i = 0 , l = updatedJoinHints . length ; i < l ; i ++ ) {
468+ const predecessors = originalQueryMembersJoinPredecessors [ updatedJoinHints [ i ] ] ;
468469
469- return common . every ( ( x , i ) => x === bFiltered [ i ] ) ;
470+ if ( predecessors ?. length > 0 ) {
471+ const predLen = predecessors . length ;
472+
473+ let predIdx = 0 ;
474+ let joinHintIdx = 0 ;
475+
476+ while ( joinHintIdx < i && predIdx < predLen ) {
477+ if ( updatedJoinHints [ joinHintIdx ] === predecessors [ predIdx ] ) {
478+ joinHintIdx ++ ;
479+ predIdx ++ ;
480+ } else {
481+ joinHintIdx ++ ;
482+ }
483+ }
484+
485+ if ( predIdx < predLen ) {
486+ // We still have a must be present predecessor for current hint
487+ return [ false , `${ updatedJoinHints [ i ] } <-> ${ predecessors [ predIdx ] } ` ] ;
488+ }
489+ }
490+ }
491+
492+ return [ true , '' ] ;
470493 } ;
471494
472495 const isJoinTreesEqual = ( a , b ) => {
@@ -495,16 +518,17 @@ export class BaseQuery {
495518 // Safeguard against infinite loop in case of cyclic joins somehow managed to slip through
496519 let cnt = 0 ;
497520
498- while ( newJoin ?. joins . length > 0 && ! isJoinTreesEqual ( prevJoins , newJoin ) && cnt < 10000 ) {
499- prevJoins = newJoin ;
521+ while ( newJoin ?. joins . length > 0 && ! isJoinTreesEqual ( prevJoin , newJoin ) && cnt < 10000 ) {
522+ prevJoin = newJoin ;
500523 joinMembersJoinHints = this . collectJoinHintsFromMembers ( this . joinMembersFromJoin ( newJoin ) ) ;
524+ newJoin = this . joinGraph . buildJoin ( constructJH ( ) ) ;
501525
502- if ( ! isOrderPreserved ( prevJoinMembersJoinHints , joinMembersJoinHints ) ) {
503- throw new UserError ( `Can not construct joins for the query, potential loop detected: ${ prevJoinMembersJoinHints . join ( '->' ) } vs ${ joinMembersJoinHints . join ( '->' ) } ` ) ;
526+ const [ isOrdered , msg ] = isOrderPreserved ( [ allMembersHintsFlattened [ 0 ] , ...joinMembersJoinHints ] ) ;
527+
528+ if ( ! isOrdered ) {
529+ throw new UserError ( `Can not construct joins for the query, potential loop detected around ${ msg } ` ) ;
504530 }
505531
506- newJoin = this . joinGraph . buildJoin ( constructJH ( ) ) ;
507- prevJoinMembersJoinHints = joinMembersJoinHints ;
508532 cnt ++ ;
509533 }
510534
@@ -540,6 +564,51 @@ export class BaseQuery {
540564 ) ;
541565 }
542566
567+ /**
568+ * @private
569+ * @param {Array<string> } arr
570+ * @returns {{}|any }
571+ */
572+ buildPredecessors ( arr ) {
573+ if ( ! arr || arr . length === 0 ) return { } ;
574+
575+ const root = arr [ 0 ] ;
576+
577+ // the first position of each unique element
578+ const firstPos = new Map ( ) ;
579+ for ( let i = 0 ; i < arr . length ; i ++ ) {
580+ if ( ! firstPos . has ( arr [ i ] ) ) firstPos . set ( arr [ i ] , i ) ;
581+ }
582+
583+ const result = { } ;
584+
585+ for ( const [ elem , idx ] of firstPos . entries ( ) ) {
586+ if ( elem === root ) {
587+ result [ elem ] = [ ] ;
588+ } else {
589+ // finding the nearest root on the left <<
590+ const seen = new Set ( ) ;
591+ const path = [ ] ;
592+
593+ for ( let j = idx - 1 ; j >= 0 ; j -- ) {
594+ const v = arr [ j ] ;
595+ if ( ! seen . has ( v ) ) {
596+ seen . add ( v ) ;
597+ path . push ( v ) ;
598+ }
599+ if ( v === root ) {
600+ break ;
601+ }
602+ }
603+
604+ path . reverse ( ) ;
605+ result [ elem ] = path ;
606+ }
607+ }
608+
609+ return result ;
610+ }
611+
543612 initUngrouped ( ) {
544613 this . ungrouped = this . options . ungrouped ;
545614 if ( this . ungrouped ) {
0 commit comments