Skip to content

Commit a4f37c5

Browse files
authored
SQL Server: Don't transform equality to bitwise operations in predicate contexts (#36809)
Fixes #36291
1 parent 050591e commit a4f37c5

File tree

4 files changed

+66
-17
lines changed

4 files changed

+66
-17
lines changed

src/EFCore.SqlServer/Query/Internal/SearchConditionConverter.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public class SearchConditionConverter(ISqlExpressionFactory sqlExpressionFactory
3232
/// </summary>
3333
[return: NotNullIfNotNull(nameof(expression))]
3434
public override Expression? Visit(Expression? expression)
35-
=> Visit(expression, inSearchConditionContext: false);
35+
=> Visit(expression, inSearchConditionContext: false, allowNullFalseEquivalence: false);
3636

3737
/// <summary>
3838
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -41,12 +41,12 @@ public class SearchConditionConverter(ISqlExpressionFactory sqlExpressionFactory
4141
/// doing so can result in application failures when updating to a new Entity Framework Core release.
4242
/// </summary>
4343
[return: NotNullIfNotNull(nameof(expression))]
44-
protected virtual Expression? Visit(Expression? expression, bool inSearchConditionContext)
44+
protected virtual Expression? Visit(Expression? expression, bool inSearchConditionContext, bool allowNullFalseEquivalence)
4545
=> expression switch
4646
{
47-
CaseExpression e => VisitCase(e, inSearchConditionContext),
47+
CaseExpression e => VisitCase(e, inSearchConditionContext, allowNullFalseEquivalence),
4848
SelectExpression e => VisitSelect(e),
49-
SqlBinaryExpression e => VisitSqlBinary(e, inSearchConditionContext),
49+
SqlBinaryExpression e => VisitSqlBinary(e, inSearchConditionContext, allowNullFalseEquivalence),
5050
SqlUnaryExpression e => VisitSqlUnary(e, inSearchConditionContext),
5151
PredicateJoinExpressionBase e => VisitPredicateJoin(e),
5252

@@ -139,19 +139,19 @@ private SqlExpression SimplifyNegatedBinary(SqlExpression sqlExpression)
139139
/// any release. You should only use it directly in your code with extreme caution and knowing that
140140
/// doing so can result in application failures when updating to a new Entity Framework Core release.
141141
/// </summary>
142-
protected virtual Expression VisitCase(CaseExpression caseExpression, bool inSearchConditionContext)
142+
protected virtual Expression VisitCase(CaseExpression caseExpression, bool inSearchConditionContext, bool allowNullFalseEquivalence)
143143
{
144144
var testIsCondition = caseExpression.Operand is null;
145145
var operand = (SqlExpression?)Visit(caseExpression.Operand);
146146
var whenClauses = new List<CaseWhenClause>();
147147
foreach (var whenClause in caseExpression.WhenClauses)
148148
{
149-
var test = (SqlExpression)Visit(whenClause.Test, testIsCondition);
150-
var result = (SqlExpression)Visit(whenClause.Result);
149+
var test = (SqlExpression)Visit(whenClause.Test, testIsCondition, testIsCondition);
150+
var result = (SqlExpression)Visit(whenClause.Result, inSearchConditionContext: false, allowNullFalseEquivalence);
151151
whenClauses.Add(new CaseWhenClause(test, result));
152152
}
153153

154-
var elseResult = (SqlExpression?)Visit(caseExpression.ElseResult);
154+
var elseResult = (SqlExpression?)Visit(caseExpression.ElseResult, inSearchConditionContext: false, allowNullFalseEquivalence);
155155

156156
return ApplyConversion(
157157
sqlExpressionFactory.Case(operand, whenClauses, elseResult, caseExpression),
@@ -168,7 +168,7 @@ protected virtual Expression VisitCase(CaseExpression caseExpression, bool inSea
168168
protected virtual Expression VisitPredicateJoin(PredicateJoinExpressionBase join)
169169
=> join.Update(
170170
(TableExpressionBase)Visit(join.Table),
171-
(SqlExpression)Visit(join.JoinPredicate, inSearchConditionContext: true));
171+
(SqlExpression)Visit(join.JoinPredicate, inSearchConditionContext: true, allowNullFalseEquivalence: true));
172172

173173
/// <summary>
174174
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -179,9 +179,9 @@ protected virtual Expression VisitPredicateJoin(PredicateJoinExpressionBase join
179179
protected virtual Expression VisitSelect(SelectExpression select)
180180
{
181181
var tables = this.VisitAndConvert(select.Tables);
182-
var predicate = (SqlExpression?)Visit(select.Predicate, inSearchConditionContext: true);
182+
var predicate = (SqlExpression?)Visit(select.Predicate, inSearchConditionContext: true, allowNullFalseEquivalence: true);
183183
var groupBy = this.VisitAndConvert(select.GroupBy);
184-
var havingExpression = (SqlExpression?)Visit(select.Having, inSearchConditionContext: true);
184+
var havingExpression = (SqlExpression?)Visit(select.Having, inSearchConditionContext: true, allowNullFalseEquivalence: true);
185185
var projections = this.VisitAndConvert(select.Projection);
186186
var orderings = this.VisitAndConvert(select.Orderings);
187187
var offset = (SqlExpression?)Visit(select.Offset);
@@ -196,19 +196,19 @@ protected virtual Expression VisitSelect(SelectExpression select)
196196
/// any release. You should only use it directly in your code with extreme caution and knowing that
197197
/// doing so can result in application failures when updating to a new Entity Framework Core release.
198198
/// </summary>
199-
protected virtual Expression VisitSqlBinary(SqlBinaryExpression binary, bool inSearchConditionContext)
199+
protected virtual Expression VisitSqlBinary(SqlBinaryExpression binary, bool inSearchConditionContext, bool allowNullFalseEquivalence)
200200
{
201201
// Only logical operations need conditions on both sides
202202
var areOperandsInSearchConditionContext = binary.OperatorType is ExpressionType.AndAlso or ExpressionType.OrElse;
203203

204-
var newLeft = (SqlExpression)Visit(binary.Left, areOperandsInSearchConditionContext);
205-
var newRight = (SqlExpression)Visit(binary.Right, areOperandsInSearchConditionContext);
204+
var newLeft = (SqlExpression)Visit(binary.Left, areOperandsInSearchConditionContext, allowNullFalseEquivalence: false);
205+
var newRight = (SqlExpression)Visit(binary.Right, areOperandsInSearchConditionContext, allowNullFalseEquivalence: false);
206206

207207
if (binary.OperatorType is ExpressionType.NotEqual or ExpressionType.Equal)
208208
{
209209
var leftType = newLeft.TypeMapping?.Converter?.ProviderClrType ?? newLeft.Type;
210210
var rightType = newRight.TypeMapping?.Converter?.ProviderClrType ?? newRight.Type;
211-
if (!inSearchConditionContext
211+
if (!inSearchConditionContext && !allowNullFalseEquivalence
212212
&& (leftType == typeof(bool) || leftType.IsInteger())
213213
&& (rightType == typeof(bool) || rightType.IsInteger()))
214214
{
@@ -309,7 +309,7 @@ protected virtual Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpression
309309
sqlUnaryExpression.OperatorType, typeof(SqlUnaryExpression)));
310310
}
311311

312-
var operand = (SqlExpression)Visit(sqlUnaryExpression.Operand, isOperandInSearchConditionContext);
312+
var operand = (SqlExpression)Visit(sqlUnaryExpression.Operand, isOperandInSearchConditionContext, allowNullFalseEquivalence: false);
313313

314314
return SimplifyNegatedBinary(
315315
ApplyConversion(

test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,17 @@ public virtual void Where_not_equal_using_relational_null_semantics_complex_with
911911
.Select(e => e.Id).ToList();
912912
}
913913

914+
[ConditionalFact]
915+
public virtual void Where_not_equal_using_relational_null_semantics_complex_in_equals()
916+
{
917+
using var context = CreateContext(useRelationalNulls: true);
918+
var l = context.Entities1
919+
.Where(e => (e.NullableBoolA != e.NullableBoolB) == e.NullableBoolC)
920+
.Select(e => e.Id).ToList();
921+
922+
Assert.Equal(l.OrderBy(e => e), [1, 5, 11, 13]);
923+
}
924+
914925
[ConditionalTheory, MemberData(nameof(IsAsyncData))]
915926
public virtual async Task Where_comparison_null_constant_and_null_parameter(bool async)
916927
{
@@ -1161,6 +1172,11 @@ await AssertQueryScalar(
11611172
await AssertQueryScalar(
11621173
async,
11631174
ss => ss.Set<NullSemanticsEntity1>().Select(e => (e.BoolA ? e.NullableIntA : e.IntB) > e.IntC));
1175+
1176+
await AssertQueryScalar(
1177+
async,
1178+
ss => ss.Set<NullSemanticsEntity1>().Where(e => (e.BoolA ? e.NullableBoolB : !e.NullableBoolC) == null).Select(e => e.Id)
1179+
);
11641180
}
11651181

11661182
[ConditionalTheory, MemberData(nameof(IsAsyncData))]

test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3252,6 +3252,18 @@ WHERE [e].[NullableBoolA] <> [e].[NullableBoolB]
32523252
""");
32533253
}
32543254

3255+
public override void Where_not_equal_using_relational_null_semantics_complex_in_equals()
3256+
{
3257+
base.Where_not_equal_using_relational_null_semantics_complex_in_equals();
3258+
3259+
AssertSql(
3260+
"""
3261+
SELECT [e].[Id]
3262+
FROM [Entities1] AS [e]
3263+
WHERE [e].[NullableBoolA] ^ [e].[NullableBoolB] = [e].[NullableBoolC]
3264+
""");
3265+
}
3266+
32553267
public override async Task Where_comparison_null_constant_and_null_parameter(bool async)
32563268
{
32573269
await base.Where_comparison_null_constant_and_null_parameter(async);
@@ -3583,6 +3595,15 @@ ELSE [e].[IntB]
35833595
ELSE CAST(0 AS bit)
35843596
END
35853597
FROM [Entities1] AS [e]
3598+
""",
3599+
//
3600+
"""
3601+
SELECT [e].[Id]
3602+
FROM [Entities1] AS [e]
3603+
WHERE CASE
3604+
WHEN [e].[BoolA] = CAST(1 AS bit) THEN [e].[NullableBoolB]
3605+
ELSE ~[e].[NullableBoolC]
3606+
END IS NULL
35863607
""");
35873608
}
35883609

@@ -5087,7 +5108,10 @@ public override async Task Is_null_on_column_followed_by_OrElse_optimizes_nullab
50875108
SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC]
50885109
FROM [Entities1] AS [e]
50895110
WHERE CASE
5090-
WHEN [e].[NullableBoolA] IS NULL THEN ~([e].[BoolA] ^ [e].[BoolB])
5111+
WHEN [e].[NullableBoolA] IS NULL THEN CASE
5112+
WHEN [e].[BoolA] = [e].[BoolB] THEN CAST(1 AS bit)
5113+
ELSE CAST(0 AS bit)
5114+
END
50915115
WHEN [e].[NullableBoolC] IS NULL THEN CASE
50925116
WHEN ([e].[NullableBoolA] <> [e].[NullableBoolC] OR [e].[NullableBoolA] IS NULL OR [e].[NullableBoolC] IS NULL) AND ([e].[NullableBoolA] IS NOT NULL OR [e].[NullableBoolC] IS NOT NULL) THEN CAST(1 AS bit)
50935117
ELSE CAST(0 AS bit)

test/EFCore.Sqlite.FunctionalTests/Query/NullSemanticsQuerySqliteTest.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1703,6 +1703,15 @@ WHEN CASE
17031703
ELSE 0
17041704
END
17051705
FROM "Entities1" AS "e"
1706+
""",
1707+
//
1708+
"""
1709+
SELECT "e"."Id"
1710+
FROM "Entities1" AS "e"
1711+
WHERE CASE
1712+
WHEN "e"."BoolA" THEN "e"."NullableBoolB"
1713+
ELSE NOT ("e"."NullableBoolC")
1714+
END IS NULL
17061715
""");
17071716
}
17081717

0 commit comments

Comments
 (0)