@@ -654,6 +654,96 @@ public function testJoinQuery(): void
654654 );
655655 }
656656
657+ public function testWrapOnWhereGroupsAllExistingOnTokens (): void
658+ {
659+ // `on()` and `onWhere()` share the same internal token list, so wrapOnWhere
660+ // encloses the join key together with onWhere conditions. This is harmless
661+ // (equality join key inside the group keeps the same logical result) and
662+ // even more protective against later orOnWhere additions.
663+ $ select = $ this ->database ->select ()
664+ ->from (['users ' ])
665+ ->leftJoin ('posts ' )
666+ ->on ('posts.user_id ' , 'users.id ' )
667+ ->onWhere ('posts.published ' , true )
668+ ->orOnWhere ('posts.featured ' , true )
669+ ->wrapOnWhere ()
670+ ->onWhere ('posts.archived ' , false );
671+
672+ $ this ->assertSameQueryWithParameters (
673+ 'SELECT * FROM {users} LEFT JOIN {posts}
674+ ON ({posts}.{user_id} = {users}.{id}
675+ AND {posts}.{published} = ? OR {posts}.{featured} = ?)
676+ AND {posts}.{archived} = ? ' ,
677+ [true , true , false ],
678+ $ select ,
679+ );
680+ }
681+
682+ public function testWrapOnWhereProtectsScopeFromLaterOrOnWhere (): void
683+ {
684+ // Mirrors the soft-delete-style scope on a joined relation: condition added
685+ // first, then wrapped, then a user-supplied orOnWhere — without wrapOnWhere
686+ // the scope would be lost on the OR arm.
687+ $ select = $ this ->database ->select ()
688+ ->from (['users ' ])
689+ ->leftJoin ('posts ' )
690+ ->on ('posts.user_id ' , 'users.id ' )
691+ ->onWhere ('posts.deleted_at ' , null )
692+ ->wrapOnWhere ()
693+ ->orOnWhere ('posts.id ' , 5 );
694+
695+ $ this ->assertSameQueryWithParameters (
696+ 'SELECT * FROM {users} LEFT JOIN {posts}
697+ ON ({posts}.{user_id} = {users}.{id}
698+ AND {posts}.{deleted_at} IS NULL)
699+ OR {posts}.{id} = ? ' ,
700+ [5 ],
701+ $ select ,
702+ );
703+ }
704+
705+ public function testWrapOnWhereWithNoExistingOnTokensIsNoop (): void
706+ {
707+ // Join registered but with no on/onWhere yet — wrapOnWhere does nothing.
708+ $ select = $ this ->database ->select ()
709+ ->from (['users ' ])
710+ ->leftJoin ('posts ' )
711+ ->wrapOnWhere ()
712+ ->onWhere ('posts.published ' , true );
713+
714+ $ this ->assertSameQueryWithParameters (
715+ 'SELECT * FROM {users} LEFT JOIN {posts}
716+ ON {posts}.{published} = ? ' ,
717+ [true ],
718+ $ select ,
719+ );
720+ }
721+
722+ public function testWrapOnWhereTargetsOnlyLastRegisteredJoinInSql (): void
723+ {
724+ $ select = $ this ->database ->select ()
725+ ->from (['users ' ])
726+ ->leftJoin ('posts ' )
727+ ->on ('posts.user_id ' , 'users.id ' )
728+ ->onWhere ('posts.published ' , true )
729+ ->orOnWhere ('posts.featured ' , true )
730+ ->leftJoin ('comments ' )
731+ ->on ('comments.user_id ' , 'users.id ' )
732+ ->onWhere ('comments.approved ' , true )
733+ ->orOnWhere ('comments.pinned ' , true )
734+ ->wrapOnWhere (); // affects only the second join
735+
736+ $ this ->assertSameQueryWithParameters (
737+ 'SELECT * FROM {users}
738+ LEFT JOIN {posts} ON {posts}.{user_id} = {users}.{id}
739+ AND {posts}.{published} = ? OR {posts}.{featured} = ?
740+ LEFT JOIN {comments} ON ({comments}.{user_id} = {users}.{id}
741+ AND {comments}.{approved} = ? OR {comments}.{pinned} = ?) ' ,
742+ [true , true , true , true ],
743+ $ select ,
744+ );
745+ }
746+
657747 public function testJoinQueryWithParameters (): void
658748 {
659749 $ subSelect = $ this ->db ('prefixed ' , 'prefix_ ' )->select ()
0 commit comments