33// See the LICENSE file in the project root for more information.
44
55using System . Collections . Generic ;
6+ using System . Diagnostics . CodeAnalysis ;
67using System . Linq ;
78
89namespace Elastic . Clients . Elasticsearch . QueryDsl ;
@@ -14,17 +15,23 @@ internal static Query CombineAsMust(this Query leftContainer, Query rightContain
1415 var hasLeftBool = leftContainer . TryGet < BoolQuery > ( out var leftBool ) ;
1516 var hasRightBool = rightContainer . TryGet < BoolQuery > ( out var rightBool ) ;
1617
17- //neither side is a bool, no special handling needed wrap in a bool must
18+ // neither side is a bool, no special handling needed wrap in a bool must
1819 if ( ! hasLeftBool && ! hasRightBool )
20+ {
1921 return CreateMustContainer ( new List < Query > { leftContainer , rightContainer } ) ;
22+ }
2023
2124 else if ( TryHandleBoolsWithOnlyShouldClauses ( leftContainer , rightContainer , leftBool , rightBool , out var query ) )
25+ {
2226 return query ;
27+ }
2328
2429 else if ( TryHandleUnmergableBools ( leftContainer , rightContainer , leftBool , rightBool , out query ) )
30+ {
2531 return query ;
32+ }
2633
27- //neither side is unmergable so neither is a bool with should clauses
34+ // neither side is unmergable so neither is a bool with should clauses
2835
2936 var mustNotClauses = OrphanMustNots ( leftContainer ) . EagerConcat ( OrphanMustNots ( rightContainer ) ) ;
3037 var filterClauses = OrphanFilters ( leftContainer ) . EagerConcat ( OrphanFilters ( rightContainer ) ) ;
@@ -36,45 +43,92 @@ internal static Query CombineAsMust(this Query leftContainer, Query rightContain
3643 }
3744
3845 /// <summary>
39- /// Handles cases where either side is a bool which indicates it can't be merged yet the other side is mergable.
46+ /// Handles cases where either side is a bool which indicates it can't be merged, yet the other side is mergable.
4047 /// A side is considered unmergable if its locked (has important metadata) or has should clauses.
41- /// Instead of always wrapping these cases in another bool we merge to unmergable side into to others must clause therefor flattening the
42- /// generated graph
48+ /// Instead of always wrapping these cases in another bool we merge the unmergable side into to others must clause therefore flattening the
49+ /// generated graph. In such cases, we copy the existing BoolQuery so as not to cause a potentially surprising side-effect. (see https://github.com/elastic/elasticsearch-net/issues/5076).
4350 /// </summary>
44- private static bool TryHandleUnmergableBools ( Query leftContainer , Query rightContainer , BoolQuery leftBool , BoolQuery rightBool , out Query query )
51+ private static bool TryHandleUnmergableBools ( Query leftContainer , Query rightContainer , BoolQuery ? leftBool , BoolQuery ? rightBool , [ NotNullWhen ( true ) ] out Query ? query )
4552 {
4653 query = null ;
47- var leftCantMergeAnd = leftBool != null && ! leftBool . CanMergeAnd ( ) ;
48- var rightCantMergeAnd = rightBool != null && ! rightBool . CanMergeAnd ( ) ;
54+
55+ var leftCantMergeAnd = leftBool is not null && ! leftBool . CanMergeAnd ( ) ;
56+ var rightCantMergeAnd = rightBool is not null && ! rightBool . CanMergeAnd ( ) ;
57+
4958 if ( ! leftCantMergeAnd && ! rightCantMergeAnd )
59+ {
5060 return false ;
61+ }
5162
5263 if ( leftCantMergeAnd && rightCantMergeAnd )
64+ {
5365 query = CreateMustContainer ( leftContainer , rightContainer ) ;
66+ }
5467
55- //right can't merge but left can and is a bool so we add left to the must clause of right
68+ // right can't merge but left can and is a bool so we add left to the must clause of right
5669 else if ( ! leftCantMergeAnd && leftBool != null && rightCantMergeAnd )
5770 {
58- leftBool . Must = leftBool . Must . AddIfNotNull ( rightContainer ) . ToArray ( ) ;
59- query = leftContainer ;
71+ if ( rightContainer is null )
72+ {
73+ query = leftContainer ;
74+ }
75+ else
76+ {
77+ // We create a copy here to avoid side-effects on a user provided bool query such as
78+ // adding a must clause where one did not previously exist.
79+
80+ var leftBoolCopy = new BoolQuery
81+ {
82+ Boost = leftBool . Boost ,
83+ MinimumShouldMatch = leftBool . MinimumShouldMatch ,
84+ QueryName = leftBool . QueryName ,
85+ Should = leftBool . Should ,
86+ Must = leftBool . Must . AddIfNotNull ( rightContainer ) . ToArray ( ) ,
87+ MustNot = leftBool . MustNot ,
88+ Filter = leftBool . Filter
89+ } ;
90+
91+ query = leftBoolCopy ;
92+ }
6093 }
6194
62- //right can't merge and left is not a bool, we forcefully create a wrapped must container
63- else if ( ! leftCantMergeAnd && leftBool == null && rightCantMergeAnd )
95+ // right can't merge and left is not a bool, we forcefully create a wrapped must container
96+ else if ( ! leftCantMergeAnd && leftBool is null && rightCantMergeAnd )
97+ {
6498 query = CreateMustContainer ( leftContainer , rightContainer ) ;
99+ }
65100
66- //left can't merge but right can and is a bool so we add left to the must clause of right
67- else if ( leftCantMergeAnd && ! rightCantMergeAnd && rightBool != null )
101+ // left can't merge but right can and is a bool so we add left to the must clause of right
102+ else if ( leftCantMergeAnd && ! rightCantMergeAnd && rightBool is not null )
68103 {
69- rightBool . Must = rightBool . Must . AddIfNotNull ( leftContainer ) . ToArray ( ) ;
70- query = rightContainer ;
104+ if ( leftContainer is null )
105+ {
106+ query = rightContainer ;
107+ }
108+ else
109+ {
110+ var rightBoolCopy = new BoolQuery
111+ {
112+ Boost = rightBool . Boost ,
113+ MinimumShouldMatch = rightBool . MinimumShouldMatch ,
114+ QueryName = rightBool . QueryName ,
115+ Should = rightBool . Should ,
116+ Must = rightBool . Must . AddIfNotNull ( leftContainer ) . ToArray ( ) ,
117+ MustNot = rightBool . MustNot ,
118+ Filter = rightBool . Filter
119+ } ;
120+
121+ query = rightBoolCopy ;
122+ }
71123 }
72124
73125 //left can't merge and right is not a bool, we forcefully create a wrapped must container
74126 else if ( leftCantMergeAnd && ! rightCantMergeAnd && rightBool == null )
127+ {
75128 query = CreateMustContainer ( new List < Query > { leftContainer , rightContainer } ) ;
129+ }
76130
77- return query != null ;
131+ return query is not null ;
78132 }
79133
80134 /// <summary>
@@ -86,10 +140,14 @@ private static bool TryHandleUnmergableBools(Query leftContainer, Query rightCon
86140 private static bool TryHandleBoolsWithOnlyShouldClauses ( Query leftContainer , Query rightContainer , BoolQuery leftBool , BoolQuery rightBool , out Query query )
87141 {
88142 query = null ;
143+
89144 var leftHasOnlyShoulds = leftBool . HasOnlyShouldClauses ( ) ;
90145 var rightHasOnlyShoulds = rightBool . HasOnlyShouldClauses ( ) ;
146+
91147 if ( ! leftHasOnlyShoulds && ! rightHasOnlyShoulds )
148+ {
92149 return false ;
150+ }
93151
94152 if ( leftContainer . HoldsOnlyShouldMusts && rightHasOnlyShoulds )
95153 {
@@ -106,25 +164,26 @@ private static bool TryHandleBoolsWithOnlyShouldClauses(Query leftContainer, Que
106164 query = CreateMustContainer ( new List < Query > { leftContainer , rightContainer } ) ;
107165 query . HoldsOnlyShouldMusts = rightHasOnlyShoulds && leftHasOnlyShoulds ;
108166 }
167+
109168 return true ;
110169 }
111170
112171 private static Query CreateMustContainer ( Query left , Query right ) =>
113172 CreateMustContainer ( new List < Query > { left , right } ) ;
114173
115174 private static Query CreateMustContainer ( List < Query > mustClauses ) =>
116- new Query ( new BoolQuery ( ) { Must = mustClauses . ToListOrNullIfEmpty ( ) } ) ;
175+ new ( new BoolQuery ( ) { Must = mustClauses . ToListOrNullIfEmpty ( ) } ) ;
117176
118177 private static Query CreateMustContainer (
119178 List < Query > mustClauses ,
120179 List < Query > mustNotClauses ,
121180 List < Query > filters
122- ) => new Query ( new BoolQuery
123- {
124- Must = mustClauses . ToListOrNullIfEmpty ( ) ,
125- MustNot = mustNotClauses . ToListOrNullIfEmpty ( ) ,
126- Filter = filters . ToListOrNullIfEmpty ( )
127- } ) ;
181+ ) => new ( new BoolQuery
182+ {
183+ Must = mustClauses . ToListOrNullIfEmpty ( ) ,
184+ MustNot = mustNotClauses . ToListOrNullIfEmpty ( ) ,
185+ Filter = filters . ToListOrNullIfEmpty ( )
186+ } ) ;
128187
129188 private static bool CanMergeAnd ( this BoolQuery boolQuery ) =>
130189 boolQuery != null && ! boolQuery . Locked && ! boolQuery . Should . HasAny ( ) ;
0 commit comments