diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs index b8987a9ad08..e8d1a15c047 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs @@ -374,6 +374,18 @@ protected override Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpressio case ExpressionType.Not when sqlUnaryExpression.Type == typeof(bool): { + // when possible, avoid converting to/from predicate form + if (!_isSearchCondition && sqlUnaryExpression.Operand is not (ExistsExpression or InExpression or LikeExpression)) + { + var negatedOperand = (SqlExpression)Visit(sqlUnaryExpression.Operand); + return _sqlExpressionFactory.MakeBinary( + ExpressionType.ExclusiveOr, + negatedOperand, + _sqlExpressionFactory.Constant(true, negatedOperand.TypeMapping), + negatedOperand.TypeMapping + )!; + } + _isSearchCondition = true; resultCondition = true; break; diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index 22ed658812d..efb32442eaa 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -710,6 +710,14 @@ public virtual Task Select_inverted_boolean(bool async) w => new { w.Id, Manual = !w.IsAutomatic }), elementSorter: e => e.Id); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Select_inverted_nullable_boolean(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(w => new { w.Id, Alive = !w.Eradicated }), + elementSorter: e => e.Id); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task Select_comparison_with_null(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs index 7dbdedb982d..de969e5374e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs @@ -1676,10 +1676,7 @@ public override async Task Conditional_expression_with_conditions_does_not_colla AssertSql( """ SELECT CASE - WHEN [c0].[Id] IS NOT NULL THEN CASE - WHEN [c0].[Processed] = CAST(0 AS bit) THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) - END + WHEN [c0].[Id] IS NOT NULL THEN [c0].[Processed] ^ CAST(1 AS bit) ELSE NULL END AS [Processing] FROM [Carts] AS [c] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index 2d227cda094..e970afe5674 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -770,15 +770,23 @@ public override async Task Select_inverted_boolean(bool async) AssertSql( """ -SELECT [w].[Id], CASE - WHEN [w].[IsAutomatic] = CAST(0 AS bit) THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END AS [Manual] +SELECT [w].[Id], [w].[IsAutomatic] ^ CAST(1 AS bit) AS [Manual] FROM [Weapons] AS [w] WHERE [w].[IsAutomatic] = CAST(1 AS bit) """); } + public override async Task Select_inverted_nullable_boolean(bool async) + { + await base.Select_inverted_nullable_boolean(async); + + AssertSql( + """ +SELECT [f].[Id], [f].[Eradicated] ^ CAST(1 AS bit) AS [Alive] +FROM [Factions] AS [f] +"""); + } + public override async Task Select_comparison_with_null(bool async) { await base.Select_comparison_with_null(async); @@ -5168,12 +5176,9 @@ public override async Task Negated_bool_ternary_inside_anonymous_type_in_project AssertSql( """ SELECT CASE - WHEN CASE - WHEN [g].[HasSoulPatch] = CAST(1 AS bit) THEN CAST(1 AS bit) - ELSE COALESCE([g].[HasSoulPatch], CAST(1 AS bit)) - END = CAST(0 AS bit) THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END AS [c] + WHEN [g].[HasSoulPatch] = CAST(1 AS bit) THEN CAST(1 AS bit) + ELSE COALESCE([g].[HasSoulPatch], CAST(1 AS bit)) +END ^ CAST(1 AS bit) AS [c] FROM [Tags] AS [t] LEFT JOIN [Gears] AS [g] ON [t].[GearNickName] = [g].[Nickname] AND [t].[GearSquadId] = [g].[SquadId] """); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs index dc52f3c63aa..9eff9030c3c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs @@ -2344,10 +2344,7 @@ public override async Task Json_boolean_projection_negated(bool async) AssertSql( """ -SELECT CASE - WHEN CAST(JSON_VALUE([j].[Reference], '$.TestBoolean') AS bit) = CAST(0 AS bit) THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END +SELECT CAST(JSON_VALUE([j].[Reference], '$.TestBoolean') AS bit) ^ CAST(1 AS bit) FROM [JsonEntitiesAllTypes] AS [j] """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs index db58d1dbe4c..af2b5cb528d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs @@ -1138,15 +1138,23 @@ public override async Task Select_inverted_boolean(bool async) AssertSql( """ -SELECT [w].[Id], CASE - WHEN [w].[IsAutomatic] = CAST(0 AS bit) THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END AS [Manual] +SELECT [w].[Id], [w].[IsAutomatic] ^ CAST(1 AS bit) AS [Manual] FROM [Weapons] AS [w] WHERE [w].[IsAutomatic] = CAST(1 AS bit) """); } + public override async Task Select_inverted_nullable_boolean(bool async) + { + await base.Select_inverted_nullable_boolean(async); + + AssertSql( + """ +SELECT [l].[Id], [l].[Eradicated] ^ CAST(1 AS bit) AS [Alive] +FROM [LocustHordes] AS [l] +"""); + } + public override async Task Select_comparison_with_null(bool async) { await base.Select_comparison_with_null(async); @@ -7050,12 +7058,9 @@ public override async Task Negated_bool_ternary_inside_anonymous_type_in_project AssertSql( """ SELECT CASE - WHEN CASE - WHEN [u].[HasSoulPatch] = CAST(1 AS bit) THEN CAST(1 AS bit) - ELSE COALESCE([u].[HasSoulPatch], CAST(1 AS bit)) - END = CAST(0 AS bit) THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END AS [c] + WHEN [u].[HasSoulPatch] = CAST(1 AS bit) THEN CAST(1 AS bit) + ELSE COALESCE([u].[HasSoulPatch], CAST(1 AS bit)) +END ^ CAST(1 AS bit) AS [c] FROM [Tags] AS [t] LEFT JOIN ( SELECT [g].[Nickname], [g].[SquadId], [g].[HasSoulPatch] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index 889b19103e4..6e857565770 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -970,15 +970,24 @@ public override async Task Select_inverted_boolean(bool async) AssertSql( """ -SELECT [w].[Id], CASE - WHEN [w].[IsAutomatic] = CAST(0 AS bit) THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END AS [Manual] +SELECT [w].[Id], [w].[IsAutomatic] ^ CAST(1 AS bit) AS [Manual] FROM [Weapons] AS [w] WHERE [w].[IsAutomatic] = CAST(1 AS bit) """); } + public override async Task Select_inverted_nullable_boolean(bool async) + { + await base.Select_inverted_nullable_boolean(async); + + AssertSql( + """ +SELECT [f].[Id], [l].[Eradicated] ^ CAST(1 AS bit) AS [Alive] +FROM [Factions] AS [f] +INNER JOIN [LocustHordes] AS [l] ON [f].[Id] = [l].[Id] +"""); + } + public override async Task Select_comparison_with_null(bool async) { await base.Select_comparison_with_null(async); @@ -5959,12 +5968,9 @@ public override async Task Negated_bool_ternary_inside_anonymous_type_in_project AssertSql( """ SELECT CASE - WHEN CASE - WHEN [s].[HasSoulPatch] = CAST(1 AS bit) THEN CAST(1 AS bit) - ELSE COALESCE([s].[HasSoulPatch], CAST(1 AS bit)) - END = CAST(0 AS bit) THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END AS [c] + WHEN [s].[HasSoulPatch] = CAST(1 AS bit) THEN CAST(1 AS bit) + ELSE COALESCE([s].[HasSoulPatch], CAST(1 AS bit)) +END ^ CAST(1 AS bit) AS [c] FROM [Tags] AS [t] LEFT JOIN ( SELECT [g].[Nickname], [g].[SquadId], [g].[HasSoulPatch] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index f0de906a778..7ee56fa0222 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -3012,15 +3012,23 @@ public override async Task Select_inverted_boolean(bool async) AssertSql( """ -SELECT [w].[Id], CASE - WHEN [w].[IsAutomatic] = CAST(0 AS bit) THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END AS [Manual] +SELECT [w].[Id], [w].[IsAutomatic] ^ CAST(1 AS bit) AS [Manual] FROM [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w] WHERE [w].[IsAutomatic] = CAST(1 AS bit) """); } + public override async Task Select_inverted_nullable_boolean(bool async) + { + await base.Select_inverted_nullable_boolean(async); + + AssertSql( + """ +SELECT [f].[Id], [f].[Eradicated] ^ CAST(1 AS bit) AS [Alive] +FROM [Factions] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [f] +"""); + } + public override async Task Where_datetimeoffset_millisecond_component(bool async) { await base.Where_datetimeoffset_millisecond_component(async); @@ -4413,12 +4421,9 @@ public override async Task Negated_bool_ternary_inside_anonymous_type_in_project AssertSql( """ SELECT CASE - WHEN CASE - WHEN [g].[HasSoulPatch] = CAST(1 AS bit) THEN CAST(1 AS bit) - ELSE COALESCE([g].[HasSoulPatch], CAST(1 AS bit)) - END = CAST(0 AS bit) THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END AS [c] + WHEN [g].[HasSoulPatch] = CAST(1 AS bit) THEN CAST(1 AS bit) + ELSE COALESCE([g].[HasSoulPatch], CAST(1 AS bit)) +END ^ CAST(1 AS bit) AS [c] FROM [Tags] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [t] LEFT JOIN [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] ON [t].[GearNickName] = [g].[Nickname] AND [t].[GearSquadId] = [g].[SquadId] """); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index 451bc96bf38..7edb07d101a 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -6157,6 +6157,17 @@ public override async Task Select_inverted_boolean(bool async) """); } + public override async Task Select_inverted_nullable_boolean(bool async) + { + await base.Select_inverted_nullable_boolean(async); + + AssertSql( + """ +SELECT "f"."Id", NOT ("f"."Eradicated") AS "Alive" +FROM "Factions" AS "f" +"""); + } + public override async Task Multiple_orderby_with_navigation_expansion_on_one_of_the_order_bys(bool async) { await base.Multiple_orderby_with_navigation_expansion_on_one_of_the_order_bys(async);