Skip to content

Commit 213d37b

Browse files
rojiranma42
andauthored
Rewrite SearchConditionConverter (#34905)
Co-authored-by: Andrea Canciani <[email protected]>
1 parent 1567ab3 commit 213d37b

File tree

3 files changed

+318
-865
lines changed

3 files changed

+318
-865
lines changed
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
6+
7+
namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
8+
9+
/// <summary>
10+
/// <para>
11+
/// A SQL Server visitor which converts boolean expressions that represent search conditions to bit values and vice versa, depending
12+
/// on context:
13+
/// </para>
14+
/// <code>
15+
/// WHERE b.SomeBitColumn => WHERE b.SomeBitColumn = 1
16+
/// SELECT a LIKE b => SELECT CASE WHEN a LIKE b THEN 1 ELSE 0 END
17+
/// </code>
18+
/// </summary>
19+
/// <remarks>
20+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
21+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
22+
/// any release. You should only use it directly in your code with extreme caution and knowing that
23+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
24+
/// </remarks>
25+
public class SearchConditionConverter(ISqlExpressionFactory sqlExpressionFactory) : ExpressionVisitor
26+
{
27+
/// <summary>
28+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
29+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
30+
/// any release. You should only use it directly in your code with extreme caution and knowing that
31+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
32+
/// </summary>
33+
[return: NotNullIfNotNull(nameof(expression))]
34+
public override Expression? Visit(Expression? expression)
35+
=> Visit(expression, inSearchConditionContext: false);
36+
37+
/// <summary>
38+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
39+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
40+
/// any release. You should only use it directly in your code with extreme caution and knowing that
41+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
42+
/// </summary>
43+
[return: NotNullIfNotNull(nameof(expression))]
44+
protected virtual Expression? Visit(Expression? expression, bool inSearchConditionContext)
45+
=> expression switch
46+
{
47+
CaseExpression e => VisitCase(e, inSearchConditionContext),
48+
SelectExpression e => VisitSelect(e),
49+
SqlBinaryExpression e => VisitSqlBinary(e, inSearchConditionContext),
50+
SqlUnaryExpression e => VisitSqlUnary(e, inSearchConditionContext),
51+
PredicateJoinExpressionBase e => VisitPredicateJoin(e),
52+
53+
// The following are search condition expressions: they can appear directly in a WHERE, and cannot e.g. be projected out
54+
// directly
55+
SqlExpression e and
56+
(ExistsExpression or InExpression or LikeExpression or SqlFunctionExpression { Name: "FREETEXT" or "CONTAINS" })
57+
=> ApplyConversion((SqlExpression)base.VisitExtension(e), inSearchConditionContext, isExpressionSearchCondition: true),
58+
59+
SqlExpression e => ApplyConversion(
60+
(SqlExpression)base.VisitExtension(e), inSearchConditionContext, isExpressionSearchCondition: false),
61+
62+
_ => base.Visit(expression)
63+
};
64+
65+
private SqlExpression ApplyConversion(SqlExpression sqlExpression, bool inSearchConditionContext, bool isExpressionSearchCondition)
66+
=> (inSearchCondition: inSearchConditionContext, isExpressionSearchCondition) switch
67+
{
68+
// A non-search condition expression in a search condition context - add equality to convert to search condition:
69+
// WHERE b.SomeBitColumn => WHERE b.SomeBitColumn = 1
70+
(true, false) => sqlExpression is SqlConstantExpression { Value: bool boolValue }
71+
? sqlExpressionFactory.Equal(
72+
boolValue
73+
? sqlExpressionFactory.Constant(1)
74+
: sqlExpressionFactory.Constant(0),
75+
sqlExpressionFactory.Constant(1))
76+
: sqlExpressionFactory.Equal(sqlExpression, sqlExpressionFactory.Constant(true)),
77+
78+
// A search condition expression in non-search condition context - wrap in CASE/WHEN to convert to bit:
79+
// e.g. SELECT a LIKE b => SELECT CASE WHEN a LIKE b THEN 1 ELSE 0 END
80+
// TODO: NULL is not handled properly here, see #34001
81+
(false, true) => sqlExpressionFactory.Case(
82+
[
83+
new CaseWhenClause(
84+
SimplifyNegatedBinary(sqlExpression),
85+
sqlExpressionFactory.ApplyDefaultTypeMapping(sqlExpressionFactory.Constant(true)))
86+
],
87+
sqlExpressionFactory.Constant(false)),
88+
89+
// All other cases (e.g. WHERE a LIKE b, SELECT b.SomebitColumn) - no need to do anything.
90+
_ => sqlExpression
91+
};
92+
93+
private SqlExpression SimplifyNegatedBinary(SqlExpression sqlExpression)
94+
{
95+
if (sqlExpression is SqlUnaryExpression { OperatorType: ExpressionType.Not } sqlUnaryExpression
96+
&& sqlUnaryExpression.Type == typeof(bool)
97+
&& sqlUnaryExpression.Operand is SqlBinaryExpression
98+
{
99+
OperatorType: ExpressionType.Equal
100+
} sqlBinaryOperand)
101+
{
102+
if (sqlBinaryOperand.Left.Type == typeof(bool)
103+
&& sqlBinaryOperand.Right.Type == typeof(bool)
104+
&& (sqlBinaryOperand.Left is SqlConstantExpression
105+
|| sqlBinaryOperand.Right is SqlConstantExpression))
106+
{
107+
var constant = sqlBinaryOperand.Left as SqlConstantExpression ?? (SqlConstantExpression)sqlBinaryOperand.Right;
108+
if (sqlBinaryOperand.Left is SqlConstantExpression)
109+
{
110+
return sqlExpressionFactory.MakeBinary(
111+
ExpressionType.Equal,
112+
sqlExpressionFactory.Constant(!(bool)constant.Value!, constant.TypeMapping),
113+
sqlBinaryOperand.Right,
114+
sqlBinaryOperand.TypeMapping)!;
115+
}
116+
117+
return sqlExpressionFactory.MakeBinary(
118+
ExpressionType.Equal,
119+
sqlBinaryOperand.Left,
120+
sqlExpressionFactory.Constant(!(bool)constant.Value!, constant.TypeMapping),
121+
sqlBinaryOperand.TypeMapping)!;
122+
}
123+
124+
return sqlExpressionFactory.MakeBinary(
125+
sqlBinaryOperand.OperatorType == ExpressionType.Equal
126+
? ExpressionType.NotEqual
127+
: ExpressionType.Equal,
128+
sqlBinaryOperand.Left,
129+
sqlBinaryOperand.Right,
130+
sqlBinaryOperand.TypeMapping)!;
131+
}
132+
133+
return sqlExpression;
134+
}
135+
136+
/// <summary>
137+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
138+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
139+
/// any release. You should only use it directly in your code with extreme caution and knowing that
140+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
141+
/// </summary>
142+
protected virtual Expression VisitCase(CaseExpression caseExpression, bool inSearchConditionContext)
143+
{
144+
var testIsCondition = caseExpression.Operand is null;
145+
var operand = (SqlExpression?)Visit(caseExpression.Operand);
146+
var whenClauses = new List<CaseWhenClause>();
147+
foreach (var whenClause in caseExpression.WhenClauses)
148+
{
149+
var test = (SqlExpression)Visit(whenClause.Test, testIsCondition);
150+
var result = (SqlExpression)Visit(whenClause.Result);
151+
whenClauses.Add(new CaseWhenClause(test, result));
152+
}
153+
154+
var elseResult = (SqlExpression?)Visit(caseExpression.ElseResult);
155+
156+
return ApplyConversion(
157+
sqlExpressionFactory.Case(operand, whenClauses, elseResult, caseExpression),
158+
inSearchConditionContext,
159+
isExpressionSearchCondition: false);
160+
}
161+
162+
/// <summary>
163+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
164+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
165+
/// any release. You should only use it directly in your code with extreme caution and knowing that
166+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
167+
/// </summary>
168+
protected virtual Expression VisitPredicateJoin(PredicateJoinExpressionBase join)
169+
=> join.Update(
170+
(TableExpressionBase)Visit(join.Table),
171+
(SqlExpression)Visit(join.JoinPredicate, inSearchConditionContext: true));
172+
173+
/// <summary>
174+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
175+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
176+
/// any release. You should only use it directly in your code with extreme caution and knowing that
177+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
178+
/// </summary>
179+
protected virtual Expression VisitSelect(SelectExpression select)
180+
{
181+
var tables = this.VisitAndConvert(select.Tables);
182+
var predicate = (SqlExpression?)Visit(select.Predicate, inSearchConditionContext: true);
183+
var groupBy = this.VisitAndConvert(select.GroupBy);
184+
var havingExpression = (SqlExpression?)Visit(select.Having, inSearchConditionContext: true);
185+
var projections = this.VisitAndConvert(select.Projection);
186+
var orderings = this.VisitAndConvert(select.Orderings);
187+
var offset = (SqlExpression?)Visit(select.Offset);
188+
var limit = (SqlExpression?)Visit(select.Limit);
189+
190+
return select.Update(tables, predicate, groupBy, havingExpression, projections, orderings, offset, limit);
191+
}
192+
193+
/// <summary>
194+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
195+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
196+
/// any release. You should only use it directly in your code with extreme caution and knowing that
197+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
198+
/// </summary>
199+
protected virtual Expression VisitSqlBinary(SqlBinaryExpression binary, bool inSearchConditionContext)
200+
{
201+
// Only logical operations need conditions on both sides
202+
var areOperandsInSearchConditionContext = binary.OperatorType is ExpressionType.AndAlso or ExpressionType.OrElse;
203+
204+
var newLeft = (SqlExpression)Visit(binary.Left, areOperandsInSearchConditionContext);
205+
var newRight = (SqlExpression)Visit(binary.Right, areOperandsInSearchConditionContext);
206+
207+
if (binary.OperatorType is ExpressionType.NotEqual or ExpressionType.Equal)
208+
{
209+
if (!inSearchConditionContext
210+
&& (newLeft.Type == typeof(bool) || newLeft.Type.IsEnum || newLeft.Type.IsInteger())
211+
&& (newRight.Type == typeof(bool) || newRight.Type.IsEnum || newRight.Type.IsInteger()))
212+
{
213+
// "lhs != rhs" is the same as "CAST(lhs ^ rhs AS BIT)", except that
214+
// the first is a boolean, the second is a BIT
215+
var result = sqlExpressionFactory.MakeBinary(ExpressionType.ExclusiveOr, newLeft, newRight, null)!;
216+
217+
if (result.Type != typeof(bool))
218+
{
219+
result = sqlExpressionFactory.Convert(result, typeof(bool), binary.TypeMapping);
220+
}
221+
222+
// "lhs == rhs" is the same as "NOT(lhs != rhs)" aka "~(lhs ^ rhs)"
223+
if (binary.OperatorType is ExpressionType.Equal)
224+
{
225+
result = sqlExpressionFactory.MakeUnary(ExpressionType.OnesComplement, result, result.Type, result.TypeMapping)!;
226+
}
227+
228+
return result;
229+
}
230+
231+
if (newLeft is SqlUnaryExpression { OperatorType: ExpressionType.OnesComplement } negatedLeft
232+
&& newRight is SqlUnaryExpression { OperatorType: ExpressionType.OnesComplement } negatedRight)
233+
{
234+
newLeft = negatedLeft.Operand;
235+
newRight = negatedRight.Operand;
236+
}
237+
}
238+
239+
binary = binary.Update(newLeft, newRight);
240+
241+
var isExpressionSearchCondition = binary.OperatorType is ExpressionType.AndAlso
242+
or ExpressionType.OrElse
243+
or ExpressionType.Equal
244+
or ExpressionType.NotEqual
245+
or ExpressionType.GreaterThan
246+
or ExpressionType.GreaterThanOrEqual
247+
or ExpressionType.LessThan
248+
or ExpressionType.LessThanOrEqual;
249+
250+
return ApplyConversion(binary, inSearchConditionContext, isExpressionSearchCondition);
251+
}
252+
253+
/// <summary>
254+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
255+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
256+
/// any release. You should only use it directly in your code with extreme caution and knowing that
257+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
258+
/// </summary>
259+
protected virtual Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpression, bool inSearchConditionContext)
260+
{
261+
bool isOperandInSearchConditionContext, isSearchConditionExpression;
262+
263+
switch (sqlUnaryExpression.OperatorType)
264+
{
265+
case ExpressionType.Not when sqlUnaryExpression.Type == typeof(bool):
266+
{
267+
// when possible, avoid converting to/from predicate form
268+
if (!inSearchConditionContext && sqlUnaryExpression.Operand is not (ExistsExpression or InExpression or LikeExpression))
269+
{
270+
var negatedOperand = (SqlExpression)Visit(sqlUnaryExpression.Operand);
271+
272+
if (negatedOperand is SqlUnaryExpression { OperatorType: ExpressionType.OnesComplement } unary)
273+
{
274+
return unary.Operand;
275+
}
276+
277+
return sqlExpressionFactory.MakeUnary(
278+
ExpressionType.OnesComplement, negatedOperand, negatedOperand.Type, negatedOperand.TypeMapping)!;
279+
}
280+
281+
isOperandInSearchConditionContext = true;
282+
isSearchConditionExpression = true;
283+
break;
284+
}
285+
286+
case ExpressionType.Not:
287+
isOperandInSearchConditionContext = false;
288+
isSearchConditionExpression = false;
289+
break;
290+
291+
case ExpressionType.Convert:
292+
case ExpressionType.Negate:
293+
isOperandInSearchConditionContext = false;
294+
isSearchConditionExpression = false;
295+
break;
296+
297+
case ExpressionType.Equal:
298+
case ExpressionType.NotEqual:
299+
isOperandInSearchConditionContext = false;
300+
isSearchConditionExpression = true;
301+
break;
302+
303+
default:
304+
throw new InvalidOperationException(
305+
RelationalStrings.UnsupportedOperatorForSqlExpression(
306+
sqlUnaryExpression.OperatorType, typeof(SqlUnaryExpression)));
307+
}
308+
309+
var operand = (SqlExpression)Visit(sqlUnaryExpression.Operand, isOperandInSearchConditionContext);
310+
311+
return SimplifyNegatedBinary(
312+
ApplyConversion(
313+
sqlUnaryExpression.Update(operand),
314+
inSearchConditionContext,
315+
isSearchConditionExpression));
316+
}
317+
}

0 commit comments

Comments
 (0)