Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix to #23472 - !boolean in query should convert to boolean == false rather than boolean != true where possible #23711

Merged
merged 1 commit into from
Dec 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,34 @@ private SqlExpression OptimizeNonNullableNotExpression(SqlUnaryExpression sqlUna
sqlBinaryOperand.TypeMapping)!);
}

// use equality where possible
// !(a == true) -> a == false
// !(a == false) -> a == true
// !(true == a) -> false == a
// !(false == a) -> true == a
if (sqlBinaryOperand.OperatorType == ExpressionType.Equal)
{
if (sqlBinaryOperand.Left is SqlConstantExpression leftConstant
&& leftConstant.Type == typeof(bool))
{
return _sqlExpressionFactory.MakeBinary(
ExpressionType.Equal,
_sqlExpressionFactory.Constant(!(bool)leftConstant.Value!, leftConstant.TypeMapping),
sqlBinaryOperand.Right,
sqlBinaryOperand.TypeMapping)!;
}

if (sqlBinaryOperand.Right is SqlConstantExpression rightConstant
&& rightConstant.Type == typeof(bool))
{
return _sqlExpressionFactory.MakeBinary(
ExpressionType.Equal,
sqlBinaryOperand.Left,
_sqlExpressionFactory.Constant(!(bool)rightConstant.Value!, rightConstant.TypeMapping),
sqlBinaryOperand.TypeMapping)!;
}
}

// !(a == b) -> a != b
// !(a != b) -> a == b
// !(a > b) -> a <= b
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,22 +74,50 @@ private SqlExpression BuildCompareToExpression(SqlExpression sqlExpression)
sqlExpression,
_sqlExpressionFactory.Constant(true));

// !(a == b) -> (a != b)
// !(a != b) -> (a == b)

private SqlExpression SimplifyNegatedBinary(SqlExpression sqlExpression)
=> sqlExpression is SqlUnaryExpression sqlUnaryExpression
{
if (sqlExpression is SqlUnaryExpression sqlUnaryExpression
&& sqlUnaryExpression.OperatorType == ExpressionType.Not
&& sqlUnaryExpression.Type == typeof(bool)
&& sqlUnaryExpression.Operand is SqlBinaryExpression sqlBinaryOperand
&& (sqlBinaryOperand.OperatorType == ExpressionType.Equal || sqlBinaryOperand.OperatorType == ExpressionType.NotEqual)
? _sqlExpressionFactory.MakeBinary(
&& (sqlBinaryOperand.OperatorType == ExpressionType.Equal || sqlBinaryOperand.OperatorType == ExpressionType.NotEqual))
{
if (sqlBinaryOperand.Left.Type == typeof(bool)
&& sqlBinaryOperand.Right.Type == typeof(bool)
&& (sqlBinaryOperand.Left is SqlConstantExpression
|| sqlBinaryOperand.Right is SqlConstantExpression))
{
var constant = sqlBinaryOperand.Left as SqlConstantExpression ?? (SqlConstantExpression)sqlBinaryOperand.Right;
if (sqlBinaryOperand.Left is SqlConstantExpression)
{
return _sqlExpressionFactory.MakeBinary(
ExpressionType.Equal,
_sqlExpressionFactory.Constant(!(bool)constant.Value!, constant.TypeMapping),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that the value should be negated only if sqlBinaryOperand.OperatorType == ExpressionType.Equal
if sqlBinaryOperand.OperatorType == ExpressionType.NotEqual, the constant should be constructed with constant.Value (no negation) since the operator is already being negated

also, I am quite confident that the NotEqual case is dead code; would it make sense to remove it? (considering that it is wrong and unreachable)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/cc @maumar

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense - good catch!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #33943

sqlBinaryOperand.Right,
sqlBinaryOperand.TypeMapping)!;
}
else
{
return _sqlExpressionFactory.MakeBinary(
ExpressionType.Equal,
sqlBinaryOperand.Left,
_sqlExpressionFactory.Constant(!(bool)constant.Value!, constant.TypeMapping),
sqlBinaryOperand.TypeMapping)!;
}
}

return _sqlExpressionFactory.MakeBinary(
sqlBinaryOperand.OperatorType == ExpressionType.Equal
? ExpressionType.NotEqual
: ExpressionType.Equal,
sqlBinaryOperand.Left,
sqlBinaryOperand.Right,
sqlBinaryOperand.TypeMapping)!
: sqlExpression;
sqlBinaryOperand.TypeMapping)!;
}

return sqlExpression;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
11 changes: 11 additions & 0 deletions test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@ FROM root c
WHERE (c[""Discriminator""] IN (""Blog"", ""RssBlog"") AND (c[""IsVisible""] = ""Y""))");
}

[ConditionalFact]
public override void Where_negated_bool_gets_converted_to_equality_when_value_conversion_is_used()
{
base.Where_negated_bool_gets_converted_to_equality_when_value_conversion_is_used();

AssertSql(
@"SELECT c
FROM root c
WHERE (c[""Discriminator""] IN (""Blog"", ""RssBlog"") AND NOT((c[""IsVisible""] = ""Y"")))");
}

[ConditionalFact]
public override void Where_bool_gets_converted_to_equality_when_value_conversion_is_used_using_EFProperty()
{
Expand Down
10 changes: 10 additions & 0 deletions test/EFCore.Specification.Tests/CustomConvertersTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,16 @@ public virtual void Where_bool_gets_converted_to_equality_when_value_conversion_
Assert.Equal("http://blog.com", result.Url);
}

[ConditionalFact]
public virtual void Where_negated_bool_gets_converted_to_equality_when_value_conversion_is_used()
{
using var context = CreateContext();
var query = context.Set<Blog>().Where(b => !b.IsVisible).ToList();

var result = Assert.Single(query);
Assert.Equal("http://rssblog.com", result.Url);
}

[ConditionalFact]
public virtual void Where_bool_with_value_conversion_inside_comparison_doesnt_get_converted_twice()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,17 @@ FROM [Blog] AS [b]
WHERE [b].[IsVisible] = N'Y'");
}

[ConditionalFact]
public override void Where_negated_bool_gets_converted_to_equality_when_value_conversion_is_used()
{
base.Where_negated_bool_gets_converted_to_equality_when_value_conversion_is_used();

AssertSql(
@"SELECT [b].[BlogId], [b].[Discriminator], [b].[IndexerVisible], [b].[IsVisible], [b].[Url], [b].[RssUrl]
FROM [Blog] AS [b]
WHERE [b].[IsVisible] = N'N'");
}

public override void Where_bool_gets_converted_to_equality_when_value_conversion_is_used_using_EFProperty()
{
base.Where_bool_gets_converted_to_equality_when_value_conversion_is_used_using_EFProperty();
Expand All @@ -260,7 +271,7 @@ public override void Where_bool_gets_converted_to_equality_when_value_conversion
AssertSql(
@"SELECT [b].[BlogId], [b].[Discriminator], [b].[IndexerVisible], [b].[IsVisible], [b].[Url], [b].[RssUrl]
FROM [Blog] AS [b]
WHERE [b].[IndexerVisible] <> N'Aye'");
WHERE [b].[IndexerVisible] = N'Nay'");
}

public override void Object_to_string_conversion()
Expand Down
Loading