From 3769433557ca2ebb271f709eb63e445c25a4ba84 Mon Sep 17 00:00:00 2001 From: Andrea Canciani Date: Sat, 16 Nov 2024 10:57:34 +0100 Subject: [PATCH 1/2] Add test for projection of equality between converted values --- .../Query/GearsOfWarQueryTestBase.cs | 7 +++++++ .../Query/GearsOfWarQuerySqlServerTest.cs | 14 ++++++++++++++ .../Query/TPCGearsOfWarQuerySqlServerTest.cs | 14 ++++++++++++++ .../Query/TPTGearsOfWarQuerySqlServerTest.cs | 14 ++++++++++++++ .../Query/TemporalGearsOfWarQuerySqlServerTest.cs | 14 ++++++++++++++ .../Query/GearsOfWarQuerySqliteTest.cs | 11 +++++++++++ 6 files changed, 74 insertions(+) diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index e312398fa06..3b1a75cc60a 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -8047,6 +8047,13 @@ public virtual Task Comparison_with_value_converted_subclass(bool async) async, ss => ss.Set().Where(f => f.ServerAddress == IPAddress.Loopback)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_equality_with_value_converted_property(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(m => m.Difficulty == MissionDifficulty.Unknown)); + private static readonly IEnumerable _weaponTypes = new AmmunitionType?[] { AmmunitionType.Cartridge }; [ConditionalTheory] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index 5de2aa4b63a..02a9fddc8d5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -9380,6 +9380,20 @@ FROM [Factions] AS [f] """); } + public override async Task Project_equality_with_value_converted_property(bool async) + { + await base.Project_equality_with_value_converted_property(async); + + AssertSql( + """ +SELECT CASE + WHEN [m].[Difficulty] = N'Unknown' THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END +FROM [Missions] AS [m] +"""); + } + public override async Task Contains_on_readonly_enumerable(bool async) { await base.Contains_on_readonly_enumerable(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs index 528e501d582..ff3b8c2bd1a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs @@ -12341,6 +12341,20 @@ FROM [LocustHordes] AS [l] """); } + public override async Task Project_equality_with_value_converted_property(bool async) + { + await base.Project_equality_with_value_converted_property(async); + + AssertSql( + """ +SELECT CASE + WHEN [m].[Difficulty] = N'Unknown' THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END +FROM [Missions] AS [m] +"""); + } + public override async Task FirstOrDefault_on_empty_collection_of_DateTime_in_subquery(bool async) { await base.FirstOrDefault_on_empty_collection_of_DateTime_in_subquery(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index 696079c3a30..e04e3568f62 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -10507,6 +10507,20 @@ FROM [Factions] AS [f] """); } + public override async Task Project_equality_with_value_converted_property(bool async) + { + await base.Project_equality_with_value_converted_property(async); + + AssertSql( + """ +SELECT CASE + WHEN [m].[Difficulty] = N'Unknown' THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END +FROM [Missions] AS [m] +"""); + } + public override async Task FirstOrDefault_on_empty_collection_of_DateTime_in_subquery(bool async) { await base.FirstOrDefault_on_empty_collection_of_DateTime_in_subquery(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index 10082b2c7e2..15bcb3fd9a4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -6999,6 +6999,20 @@ public override async Task Comparison_with_value_converted_subclass(bool async) """); } + public override async Task Project_equality_with_value_converted_property(bool async) + { + await base.Project_equality_with_value_converted_property(async); + + AssertSql( + """ +SELECT CASE + WHEN [m].[Difficulty] = N'Unknown' THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END +FROM [Missions] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [m] +"""); + } + public override async Task Navigation_access_on_derived_materialized_entity_using_cast(bool async) { await base.Navigation_access_on_derived_materialized_entity_using_cast(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index 1581cbb64a4..36d2e9cab80 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -3076,6 +3076,17 @@ public override async Task Comparison_with_value_converted_subclass(bool async) """); } + public override async Task Project_equality_with_value_converted_property(bool async) + { + await base.Project_equality_with_value_converted_property(async); + + AssertSql( + """ +SELECT "m"."Difficulty" = 'Unknown' +FROM "Missions" AS "m" +"""); + } + public override async Task GetValueOrDefault_in_filter_non_nullable_column(bool async) { await base.GetValueOrDefault_in_filter_non_nullable_column(async); From 4f6e04645759703ec66bfa3dc314f3d3dde4d90e Mon Sep 17 00:00:00 2001 From: Andrea Canciani Date: Sat, 16 Nov 2024 11:47:21 +0100 Subject: [PATCH 2/2] Avoid using `^` and `~` on value-converted expressions The transformation of equality/in-equality in a (negated) XOR is only possible when the expressions are BIT or integer types on the SQL side (i.e. taking value conversion into account). Similarly, the Boolean negation `NOT` can be implemented as `~` only if the underlying expression is a BIT. Fixes #35093. --- .../Query/Internal/SearchConditionConverter.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConverter.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConverter.cs index 5e827602edc..5a27b5e09a2 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConverter.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConverter.cs @@ -206,9 +206,11 @@ protected virtual Expression VisitSqlBinary(SqlBinaryExpression binary, bool inS if (binary.OperatorType is ExpressionType.NotEqual or ExpressionType.Equal) { + var leftType = newLeft.TypeMapping?.Converter?.ProviderClrType ?? newLeft.Type; + var rightType = newRight.TypeMapping?.Converter?.ProviderClrType ?? newRight.Type; if (!inSearchConditionContext - && (newLeft.Type == typeof(bool) || newLeft.Type.IsEnum || newLeft.Type.IsInteger()) - && (newRight.Type == typeof(bool) || newRight.Type.IsEnum || newRight.Type.IsInteger())) + && (leftType == typeof(bool) || leftType.IsInteger()) + && (rightType == typeof(bool) || rightType.IsInteger())) { // "lhs != rhs" is the same as "CAST(lhs ^ rhs AS BIT)", except that // the first is a boolean, the second is a BIT @@ -262,7 +264,8 @@ protected virtual Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpression switch (sqlUnaryExpression.OperatorType) { - case ExpressionType.Not when sqlUnaryExpression.Type == typeof(bool): + case ExpressionType.Not + when (sqlUnaryExpression.TypeMapping?.Converter?.ProviderClrType ?? sqlUnaryExpression.Type) == typeof(bool): { // when possible, avoid converting to/from predicate form if (!inSearchConditionContext && sqlUnaryExpression.Operand is not (ExistsExpression or InExpression or LikeExpression))