Skip to content

Commit 503255c

Browse files
committed
Fix to #23990 - Relational: IS (NOT) NULL requires parenthesis when not in left side of an equality
We were generating incorrect sql when comparing bool value to a nullableBool.HasValue. Fix is to add parentheses to the right side when we detect the pattern: - left side doesn't already have parentheses - right side is a IS NULL / IS NOT NULL check - argument to that null check is bool Note: this is not really an issue for sql server because of search conditions, but sqlite would yield incorrect data for those queries. Fixes #23990
1 parent 4624a2d commit 503255c

File tree

5 files changed

+337
-38
lines changed

5 files changed

+337
-38
lines changed

src/EFCore.Relational/Query/QuerySqlGenerator.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,16 @@ protected override Expression VisitSqlBinary(SqlBinaryExpression sqlBinaryExpres
538538

539539
_relationalCommandBuilder.Append(GetOperator(sqlBinaryExpression));
540540

541-
requiresBrackets = RequiresBrackets(sqlBinaryExpression.Right);
541+
// IS NULL / IS NOT NULL on bool requires brackets if it's on the right side
542+
// e.g. TRUE = (someBool IS NULL) rather than TRUE = someBool IS NULL
543+
var requiresBracketsForNullCheckOnBool = !requiresBrackets
544+
&& sqlBinaryExpression.Right is SqlUnaryExpression sqlUnaryRight
545+
&& (sqlUnaryRight.OperatorType == ExpressionType.Equal
546+
|| sqlUnaryRight.OperatorType == ExpressionType.NotEqual)
547+
&& sqlUnaryRight.Operand.Type == typeof(bool);
548+
549+
requiresBrackets = RequiresBrackets(sqlBinaryExpression.Right)
550+
|| requiresBracketsForNullCheckOnBool;
542551

543552
if (requiresBrackets)
544553
{

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1546,6 +1546,88 @@ public virtual Task Multiple_non_equality_comparisons_with_null_in_the_middle(bo
15461546
ss => ss.Set<NullSemanticsEntity1>().Where(e => e.NullableIntA != 1 && e.NullableIntA != null && e.NullableIntA != 2));
15471547
}
15481548

1549+
[ConditionalTheory]
1550+
[MemberData(nameof(IsAsyncData))]
1551+
public virtual async Task Bool_equal_nullable_bool_HasValue(bool async)
1552+
{
1553+
await AssertQuery(
1554+
async,
1555+
ss => ss.Set<NullSemanticsEntity1>().Where(e => true == e.NullableBoolA.HasValue));
1556+
1557+
var prm = false;
1558+
await AssertQuery(
1559+
async,
1560+
ss => ss.Set<NullSemanticsEntity1>().Where(e => prm == e.NullableBoolA.HasValue));
1561+
1562+
await AssertQuery(
1563+
async,
1564+
ss => ss.Set<NullSemanticsEntity1>().Where(e => e.BoolB == e.NullableBoolA.HasValue));
1565+
}
1566+
1567+
[ConditionalTheory]
1568+
[MemberData(nameof(IsAsyncData))]
1569+
public virtual async Task Bool_equal_nullable_bool_compared_to_null(bool async)
1570+
{
1571+
await AssertQuery(
1572+
async,
1573+
ss => ss.Set<NullSemanticsEntity1>().Where(e => true == (e.NullableBoolA == null)));
1574+
1575+
var prm = false;
1576+
await AssertQuery(
1577+
async,
1578+
ss => ss.Set<NullSemanticsEntity1>().Where(e => prm == (e.NullableBoolA != null)));
1579+
}
1580+
1581+
[ConditionalTheory]
1582+
[MemberData(nameof(IsAsyncData))]
1583+
public virtual async Task Bool_not_equal_nullable_bool_HasValue(bool async)
1584+
{
1585+
await AssertQuery(
1586+
async,
1587+
ss => ss.Set<NullSemanticsEntity1>().Where(e => true != e.NullableBoolA.HasValue));
1588+
1589+
var prm = false;
1590+
await AssertQuery(
1591+
async,
1592+
ss => ss.Set<NullSemanticsEntity1>().Where(e => prm != e.NullableBoolA.HasValue));
1593+
1594+
await AssertQuery(
1595+
async,
1596+
ss => ss.Set<NullSemanticsEntity1>().Where(e => e.BoolB != e.NullableBoolA.HasValue));
1597+
}
1598+
1599+
[ConditionalTheory]
1600+
[MemberData(nameof(IsAsyncData))]
1601+
public virtual async Task Bool_not_equal_nullable_bool_compared_to_null(bool async)
1602+
{
1603+
await AssertQuery(
1604+
async,
1605+
ss => ss.Set<NullSemanticsEntity1>().Where(e => true != (e.NullableBoolA == null)));
1606+
1607+
var prm = false;
1608+
await AssertQuery(
1609+
async,
1610+
ss => ss.Set<NullSemanticsEntity1>().Where(e => prm != (e.NullableBoolA != null)));
1611+
}
1612+
1613+
[ConditionalTheory]
1614+
[MemberData(nameof(IsAsyncData))]
1615+
public virtual async Task Bool_logical_operation_with_nullable_bool_HasValue(bool async)
1616+
{
1617+
await AssertQuery(
1618+
async,
1619+
ss => ss.Set<NullSemanticsEntity1>().Where(e => true || e.NullableBoolA.HasValue));
1620+
1621+
var prm = false;
1622+
await AssertQuery(
1623+
async,
1624+
ss => ss.Set<NullSemanticsEntity1>().Where(e => prm && e.NullableBoolA.HasValue));
1625+
1626+
await AssertQuery(
1627+
async,
1628+
ss => ss.Set<NullSemanticsEntity1>().Where(e => e.BoolB | e.NullableBoolA.HasValue));
1629+
}
1630+
15491631
[ConditionalTheory]
15501632
[MemberData(nameof(IsAsyncData))]
15511633
public virtual async Task Multiple_non_equality_comparisons_including_null_comparison_work_for_relational_null_semantics(bool async)

test/EFCore.Specification.Tests/TestUtilities/ExpectedQueryRewritingVisitor.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,12 @@ private Expression AddNullProtectionForNonNullableMemberAccess(Expression expres
302302
instance.Type,
303303
memberExpression.Type.UnwrapNullableType());
304304

305-
return Expression.Call(methodInfo, instance, maybeLambda);
305+
var maybeMethodCall = Expression.Call(methodInfo, instance, maybeLambda);
306+
307+
return memberExpression.Member.DeclaringType.IsNullableType()
308+
&& memberExpression.Member.Name == "HasValue"
309+
? Expression.Coalesce(maybeMethodCall, Expression.Constant(false))
310+
: maybeMethodCall;
306311
}
307312

308313
return Visit(expression);

0 commit comments

Comments
 (0)