diff --git a/src/EFCore.Cosmos/Extensions/Internal/CosmosExpressionExtensions.cs b/src/EFCore.Cosmos/Extensions/Internal/CosmosExpressionExtensions.cs new file mode 100644 index 00000000000..e4082679c02 --- /dev/null +++ b/src/EFCore.Cosmos/Extensions/Internal/CosmosExpressionExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static class CosmosExpressionExtensions + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static bool IsLogicalNot(this SqlUnaryExpression sqlUnaryExpression) + => sqlUnaryExpression.OperatorType == ExpressionType.Not + && (sqlUnaryExpression.Type == typeof(bool) + || sqlUnaryExpression.Type == typeof(bool?)); + } +} diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs index a7d0eb86eed..643c8ae1dc5 100644 --- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs +++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs @@ -7,6 +7,7 @@ using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Storage; @@ -107,16 +108,20 @@ private SqlExpression ApplyTypeMappingOnSqlUnary( { case ExpressionType.Equal: case ExpressionType.NotEqual: - case ExpressionType.Not: + case ExpressionType.Not + when sqlUnaryExpression.IsLogicalNot(): + { resultTypeMapping = _boolTypeMapping; operand = ApplyDefaultTypeMapping(sqlUnaryExpression.Operand); break; + } case ExpressionType.Convert: resultTypeMapping = typeMapping; operand = ApplyDefaultTypeMapping(sqlUnaryExpression.Operand); break; + case ExpressionType.Not: case ExpressionType.Negate: resultTypeMapping = typeMapping; operand = ApplyTypeMapping(sqlUnaryExpression.Operand, typeMapping); @@ -417,7 +422,7 @@ public virtual SqlUnaryExpression Convert(SqlExpression operand, Type type, Core /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual SqlUnaryExpression Not(SqlExpression operand) - => MakeUnary(ExpressionType.Not, operand, typeof(bool)); + => MakeUnary(ExpressionType.Not, operand, operand.Type, operand.TypeMapping); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Extensions/Internal/RelationalExpressionExtensions.cs b/src/EFCore.Relational/Extensions/Internal/RelationalExpressionExtensions.cs new file mode 100644 index 00000000000..24337c302a6 --- /dev/null +++ b/src/EFCore.Relational/Extensions/Internal/RelationalExpressionExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static class RelationalExpressionExtensions + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static bool IsLogicalNot(this SqlUnaryExpression sqlUnaryExpression) + => sqlUnaryExpression.OperatorType == ExpressionType.Not + && (sqlUnaryExpression.Type == typeof(bool) + || sqlUnaryExpression.Type == typeof(bool?)); + } +} diff --git a/src/EFCore.Relational/Query/Internal/NullSemanticsRewritingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/NullSemanticsRewritingExpressionVisitor.cs index 8497a115581..8313988ee26 100644 --- a/src/EFCore.Relational/Query/Internal/NullSemanticsRewritingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/NullSemanticsRewritingExpressionVisitor.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; namespace Microsoft.EntityFrameworkCore.Query.Internal @@ -389,8 +390,8 @@ protected override Expression VisitSqlBinary(SqlBinaryExpression sqlBinaryExpres var leftUnary = newLeft as SqlUnaryExpression; var rightUnary = newRight as SqlUnaryExpression; - var leftNegated = leftUnary?.OperatorType == ExpressionType.Not; - var rightNegated = rightUnary?.OperatorType == ExpressionType.Not; + var leftNegated = leftUnary?.IsLogicalNot() == true; + var rightNegated = rightUnary?.IsLogicalNot() == true; if (leftNegated) { diff --git a/src/EFCore.Relational/Query/Internal/SqlExpressionOptimizingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/SqlExpressionOptimizingExpressionVisitor.cs index 47ba1df1242..389fa135338 100644 --- a/src/EFCore.Relational/Query/Internal/SqlExpressionOptimizingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/SqlExpressionOptimizingExpressionVisitor.cs @@ -113,13 +113,16 @@ private SqlExpression SimplifyUnaryExpression( { switch (operatorType) { - case ExpressionType.Not: + case ExpressionType.Not + when type == typeof(bool) + || type == typeof(bool?): + { switch (operand) { // !(true) -> false // !(false) -> true case SqlConstantExpression constantOperand - when constantOperand.Value is bool value: + when constantOperand.Value is bool value: { return SqlExpressionFactory.Constant(!value, typeMapping); } @@ -142,6 +145,7 @@ private SqlExpression SimplifyUnaryExpression( case ExpressionType.NotEqual: return SqlExpressionFactory.IsNull(unaryOperand.Operand); } + break; case SqlBinaryExpression binaryOperand: @@ -165,7 +169,8 @@ private SqlExpression SimplifyUnaryExpression( // those optimizations are only valid in 2-value logic // they are safe to do here because if we apply null semantics // because null semantics removes possibility of nulls in the tree when the comparison is wrapped around NOT - if (!_useRelationalNulls && TryNegate(binaryOperand.OperatorType, out var negated)) + if (!_useRelationalNulls + && TryNegate(binaryOperand.OperatorType, out var negated)) { return SimplifyBinaryExpression( negated, @@ -177,6 +182,7 @@ private SqlExpression SimplifyUnaryExpression( break; } break; + } case ExpressionType.Equal: case ExpressionType.NotEqual: diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs index 4133df125a6..2dbb0c227fb 100644 --- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs @@ -7,6 +7,7 @@ using System.Linq.Expressions; using System.Text.RegularExpressions; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.Internal; @@ -515,7 +516,8 @@ protected override Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpressio break; } - case ExpressionType.Not: + case ExpressionType.Not + when sqlUnaryExpression.IsLogicalNot(): { _relationalCommandBuilder.Append("NOT ("); Visit(sqlUnaryExpression.Operand); @@ -523,6 +525,13 @@ protected override Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpressio break; } + case ExpressionType.Not: + { + _relationalCommandBuilder.Append("~"); + Visit(sqlUnaryExpression.Operand); + break; + } + case ExpressionType.Equal: { Visit(sqlUnaryExpression.Operand); diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index e84f8e6d66c..e72aadf325e 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; @@ -105,16 +106,20 @@ private SqlExpression ApplyTypeMappingOnSqlUnary( { case ExpressionType.Equal: case ExpressionType.NotEqual: - case ExpressionType.Not: + case ExpressionType.Not + when sqlUnaryExpression.IsLogicalNot(): + { resultTypeMapping = _boolTypeMapping; operand = ApplyDefaultTypeMapping(sqlUnaryExpression.Operand); break; + } case ExpressionType.Convert: resultTypeMapping = typeMapping; operand = ApplyDefaultTypeMapping(sqlUnaryExpression.Operand); break; + case ExpressionType.Not: case ExpressionType.Negate: resultTypeMapping = typeMapping; operand = ApplyTypeMapping(sqlUnaryExpression.Operand, typeMapping); @@ -283,7 +288,7 @@ public virtual SqlUnaryExpression Convert(SqlExpression operand, Type type, Rela => MakeUnary(ExpressionType.Convert, operand, type, typeMapping); public virtual SqlUnaryExpression Not(SqlExpression operand) - => MakeUnary(ExpressionType.Not, operand, typeof(bool)); + => MakeUnary(ExpressionType.Not, operand, operand.Type, operand.TypeMapping); public virtual SqlUnaryExpression Negate(SqlExpression operand) => MakeUnary(ExpressionType.Negate, operand, operand.Type, operand.TypeMapping); diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs index 7736fbe01a3..c206cc7ffe5 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; @@ -220,10 +221,18 @@ protected override Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpressio bool resultCondition; switch (sqlUnaryExpression.OperatorType) { - case ExpressionType.Not: + case ExpressionType.Not + when sqlUnaryExpression.IsLogicalNot(): + { _isSearchCondition = true; resultCondition = true; break; + } + + case ExpressionType.Not: + _isSearchCondition = false; + resultCondition = false; + break; case ExpressionType.Convert: case ExpressionType.Negate: diff --git a/src/EFCore/Extensions/Internal/ExpressionExtensions.cs b/src/EFCore/Extensions/Internal/ExpressionExtensions.cs index f7920138b7f..df151fdd18d 100644 --- a/src/EFCore/Extensions/Internal/ExpressionExtensions.cs +++ b/src/EFCore/Extensions/Internal/ExpressionExtensions.cs @@ -155,6 +155,17 @@ public static LambdaExpression GetLambdaOrNull(this Expression expression) ? (LambdaExpression)unary.Operand : null; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static bool IsLogicalNot(this UnaryExpression sqlUnaryExpression) + => sqlUnaryExpression.NodeType == ExpressionType.Not + && (sqlUnaryExpression.Type == typeof(bool) + || sqlUnaryExpression.Type == typeof(bool?)); + private static Expression RemoveConvert(Expression expression) { if (expression is UnaryExpression unaryExpression diff --git a/src/EFCore/Query/Internal/AllAnyToContainsRewritingExpressionVisitor.cs b/src/EFCore/Query/Internal/AllAnyToContainsRewritingExpressionVisitor.cs index 7a5aecafdd2..173c25741ed 100644 --- a/src/EFCore/Query/Internal/AllAnyToContainsRewritingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/AllAnyToContainsRewritingExpressionVisitor.cs @@ -4,6 +4,7 @@ using System; using System.Linq.Expressions; using System.Reflection; +using Microsoft.EntityFrameworkCore.Internal; namespace Microsoft.EntityFrameworkCore.Query.Internal { @@ -67,6 +68,7 @@ private bool TryExtractEqualityOperands(Expression expression, out Expression le case MethodCallExpression methodCallExpression when methodCallExpression.Method.Name == nameof(object.Equals): + { negated = false; if (methodCallExpression.Arguments.Count == 1 && methodCallExpression.Object.Type == methodCallExpression.Arguments[0].Type) @@ -80,12 +82,15 @@ private bool TryExtractEqualityOperands(Expression expression, out Expression le } return true; + } case UnaryExpression unaryExpression - when unaryExpression.NodeType == ExpressionType.Not: + when unaryExpression.IsLogicalNot(): + { var result = TryExtractEqualityOperands(unaryExpression.Operand, out left, out right, out negated); negated = !negated; return result; + } } return false; diff --git a/src/EFCore/Query/Internal/NegationOptimizingExpressionVisitor.cs b/src/EFCore/Query/Internal/NegationOptimizingExpressionVisitor.cs index efae0d840d7..3aaf1eee74b 100644 --- a/src/EFCore/Query/Internal/NegationOptimizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NegationOptimizingExpressionVisitor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Internal; namespace Microsoft.EntityFrameworkCore.Query.Internal { @@ -32,7 +33,8 @@ private static ExpressionType Negate(ExpressionType expressionType) protected override Expression VisitUnary(UnaryExpression unaryExpression) { - if (unaryExpression.NodeType == ExpressionType.Not) + if (unaryExpression.NodeType == ExpressionType.Not + && unaryExpression.IsLogicalNot()) { if (unaryExpression.Operand is ConstantExpression innerConstant && innerConstant.Value is bool value) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.cs index 0cc86a724d2..d48dfd5c7fc 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.cs @@ -2342,6 +2342,38 @@ FROM root c WHERE ((c[""Discriminator""] = ""Customer"") AND (((c[""CustomerID""] = ""ALFKI"") & (c[""CustomerID""] = ""ANATR"")) OR (c[""CustomerID""] = ""ANTON"")))"); } + public override async Task Where_bitwise_binary_not(bool isAsync) + { + await base.Where_bitwise_binary_not(isAsync); + + AssertSql( + @"@__negatedId_0='-10249' + +SELECT c +FROM root c +WHERE ((c[""Discriminator""] = ""Customer"") AND (~c[""CustomerID""] = @__negatedId_0)"); + } + + public override async Task Where_bitwise_binary_and(bool isAsync) + { + await base.Where_bitwise_binary_and(isAsync); + + AssertSql( + @"SELECT c +FROM root c +WHERE ((c[""Discriminator""] = ""Customer"") AND ((c[""CustomerID""] & 10248) = 10248)"); + } + + public override async Task Where_bitwise_binary_or(bool isAsync) + { + await base.Where_bitwise_binary_or(isAsync); + + AssertSql( + @"SELECT c +FROM root c +WHERE ((c[""Discriminator""] = ""Customer"") AND ((c[""CustomerID""] | 10248) = 10248)"); + } + [ConditionalFact(Skip = "Issue #17246")] public override void Select_bitwise_or_with_logical_or() { diff --git a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.cs index 9d8c46d0dfa..434331fed24 100644 --- a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.cs @@ -3764,6 +3764,38 @@ public virtual Task Where_bitwise_and_with_logical_or(bool isAsync) entryCount: 1); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_bitwise_binary_not(bool isAsync) + { + var negatedId = ~10248; + + return AssertQuery( + isAsync, + ss => ss.Set().Where(o => ~o.OrderID == negatedId), + entryCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_bitwise_binary_and(bool isAsync) + { + return AssertQuery( + isAsync, + ss => ss.Set().Where(o => (o.OrderID & 10248) == 10248), + entryCount: 416); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_bitwise_binary_or(bool isAsync) + { + return AssertQuery( + isAsync, + ss => ss.Set().Where(o => (o.OrderID | 10248) == 10248), + entryCount: 1); + } + [ConditionalFact] public virtual void Select_bitwise_or_with_logical_or() { diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs index 3aa2642e70e..06906ffd802 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs @@ -2887,6 +2887,38 @@ ELSE CAST(0 AS bit) END) = CAST(1 AS bit)) OR ([c].[CustomerID] = N'ANTON')"); } + public override async Task Where_bitwise_binary_not(bool isAsync) + { + await base.Where_bitwise_binary_not(isAsync); + + AssertSql( + @"@__negatedId_0='-10249' + +SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM [Orders] AS [o] +WHERE ~[o].[OrderID] = @__negatedId_0"); + } + + public override async Task Where_bitwise_binary_and(bool isAsync) + { + await base.Where_bitwise_binary_and(isAsync); + + AssertSql( + @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM [Orders] AS [o] +WHERE ([o].[OrderID] & 10248) = 10248"); + } + + public override async Task Where_bitwise_binary_or(bool isAsync) + { + await base.Where_bitwise_binary_or(isAsync); + + AssertSql( + @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM [Orders] AS [o] +WHERE ([o].[OrderID] | 10248) = 10248"); + } + public override void Select_bitwise_or_with_logical_or() { base.Select_bitwise_or_with_logical_or();