diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConverter.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConverter.cs index 5a27b5e09a2..6d0d2ac69b9 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConverter.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConverter.cs @@ -24,6 +24,17 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; /// public class SearchConditionConverter(ISqlExpressionFactory sqlExpressionFactory) : ExpressionVisitor { + /// + /// Tracks whether we're in the larger context of a predicate, even the immediate context is not a search condition. + /// + /// + /// This visitor tracks whether the immediate context is a search condition by passing a boolean flag down during visitation, + /// and making adjustments so that the SQL is correct. However, in some cases it's useful to know whether we're in the + /// larger context of a predicate - even if the immediate context isn't a search condition; we use this to make sure the + /// SQL is efficient (as opposed to correct), refraining from transformations which could prevent index usage. + /// + private bool _inLargerPredicateContext; + /// /// 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 @@ -42,7 +53,14 @@ public class SearchConditionConverter(ISqlExpressionFactory sqlExpressionFactory /// [return: NotNullIfNotNull(nameof(expression))] protected virtual Expression? Visit(Expression? expression, bool inSearchConditionContext) - => expression switch + { + var parentOptimizeForPredicateContext = _inLargerPredicateContext; + if (inSearchConditionContext) + { + _inLargerPredicateContext = true; + } + + var result = expression switch { CaseExpression e => VisitCase(e, inSearchConditionContext), SelectExpression e => VisitSelect(e), @@ -62,6 +80,10 @@ SqlExpression e and _ => base.Visit(expression) }; + _inLargerPredicateContext = parentOptimizeForPredicateContext; + return result; + } + private SqlExpression ApplyConversion(SqlExpression sqlExpression, bool inSearchConditionContext, bool isExpressionSearchCondition) => (inSearchCondition: inSearchConditionContext, isExpressionSearchCondition) switch { @@ -178,6 +200,9 @@ protected virtual Expression VisitPredicateJoin(PredicateJoinExpressionBase join /// protected virtual Expression VisitSelect(SelectExpression select) { + var parentOptimizeForPredicateContext = _inLargerPredicateContext; + _inLargerPredicateContext = false; + var tables = this.VisitAndConvert(select.Tables); var predicate = (SqlExpression?)Visit(select.Predicate, inSearchConditionContext: true); var groupBy = this.VisitAndConvert(select.GroupBy); @@ -187,6 +212,8 @@ protected virtual Expression VisitSelect(SelectExpression select) var offset = (SqlExpression?)Visit(select.Offset); var limit = (SqlExpression?)Visit(select.Limit); + _inLargerPredicateContext = parentOptimizeForPredicateContext; + return select.Update(tables, predicate, groupBy, havingExpression, projections, orderings, offset, limit); } @@ -208,7 +235,14 @@ protected virtual Expression VisitSqlBinary(SqlBinaryExpression binary, bool inS { var leftType = newLeft.TypeMapping?.Converter?.ProviderClrType ?? newLeft.Type; var rightType = newRight.TypeMapping?.Converter?.ProviderClrType ?? newRight.Type; - if (!inSearchConditionContext + + // Transform equality and inequality over bool/integer to bitwise operators: + // x != y => CAST(x ^ y AS BIT) + // x == y => CAST(~(x ^ y) AS BIT) + // However, refrain from doing this if we're within a predicate - even if the immediate context is not a search condition - + // since this might prevent index usage (e.g. we don't want this to happen within CASE THEN clauses, which aren't search + // conditions but are seen through by SQL Server when in the WHERE clause - see #36291). + if (!_inLargerPredicateContext && (leftType == typeof(bool) || leftType.IsInteger()) && (rightType == typeof(bool) || rightType.IsInteger())) { diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs index bd0137646c4..9cc830a069f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs @@ -1101,9 +1101,12 @@ public override async Task Where_bool_member_and_parameter_compared_to_binary_ex SELECT [p].[ProductID], [p].[Discontinued], [p].[ProductName], [p].[SupplierID], [p].[UnitPrice], [p].[UnitsInStock] FROM [Products] AS [p] WHERE [p].[Discontinued] = CASE - WHEN [p].[ProductID] > 50 THEN CAST(1 AS bit) + WHEN CASE + WHEN [p].[ProductID] > 50 THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END <> @prm THEN CAST(1 AS bit) ELSE CAST(0 AS bit) -END ^ @prm +END """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs index 8e5143af447..14d5da01cd5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs @@ -2466,7 +2466,13 @@ public override async Task Compare_complex_equal_equal_equal(bool async) """ SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[BoolA] ^ [e].[BoolB] = CAST([e].[IntA] ^ [e].[IntB] AS bit) +WHERE CASE + WHEN [e].[BoolA] = [e].[BoolB] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END = CASE + WHEN [e].[IntA] = [e].[IntB] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END """, // """ @@ -2502,7 +2508,13 @@ public override async Task Compare_complex_equal_not_equal_equal(bool async) """ SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[BoolA] ^ [e].[BoolB] <> CAST([e].[IntA] ^ [e].[IntB] AS bit) +WHERE CASE + WHEN [e].[BoolA] = [e].[BoolB] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END <> CASE + WHEN [e].[IntA] = [e].[IntB] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END """, // """ @@ -2538,7 +2550,13 @@ public override async Task Compare_complex_not_equal_equal_equal(bool async) """ SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[BoolA] ^ [e].[BoolB] = ~CAST([e].[IntA] ^ [e].[IntB] AS bit) +WHERE CASE + WHEN [e].[BoolA] <> [e].[BoolB] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END = CASE + WHEN [e].[IntA] = [e].[IntB] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END """, // """ @@ -2574,7 +2592,13 @@ public override async Task Compare_complex_not_equal_not_equal_equal(bool async) """ SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[BoolA] ^ [e].[BoolB] <> ~CAST([e].[IntA] ^ [e].[IntB] AS bit) +WHERE CASE + WHEN [e].[BoolA] <> [e].[BoolB] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END <> CASE + WHEN [e].[IntA] = [e].[IntB] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END """, // """ @@ -2610,7 +2634,13 @@ public override async Task Compare_complex_not_equal_equal_not_equal(bool async) """ SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[BoolA] ^ [e].[BoolB] = CAST([e].[IntA] ^ [e].[IntB] AS bit) +WHERE CASE + WHEN [e].[BoolA] <> [e].[BoolB] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END = CASE + WHEN [e].[IntA] <> [e].[IntB] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END """, // """ @@ -2646,7 +2676,13 @@ public override async Task Compare_complex_not_equal_not_equal_not_equal(bool as """ SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE [e].[BoolA] ^ [e].[BoolB] <> CAST([e].[IntA] ^ [e].[IntB] AS bit) +WHERE CASE + WHEN [e].[BoolA] <> [e].[BoolB] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END <> CASE + WHEN [e].[IntA] <> [e].[IntB] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END """, // """ @@ -4934,7 +4970,10 @@ public override async Task Comparison_compared_to_null_check_on_bool(bool async) """ SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC] FROM [Entities1] AS [e] -WHERE ~CAST([e].[IntA] ^ [e].[IntB] AS bit) <> CASE +WHERE CASE + WHEN [e].[IntA] = [e].[IntB] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END <> CASE WHEN [e].[NullableBoolA] IS NOT NULL THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END @@ -4943,7 +4982,10 @@ ELSE CAST(0 AS bit) """ SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC] FROM [Entities1] AS [e] -WHERE CAST([e].[IntA] ^ [e].[IntB] AS bit) = CASE +WHERE CASE + WHEN [e].[IntA] <> [e].[IntB] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END = CASE WHEN [e].[NullableBoolA] IS NOT NULL THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END @@ -5087,7 +5129,10 @@ public override async Task Is_null_on_column_followed_by_OrElse_optimizes_nullab SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC] FROM [Entities1] AS [e] WHERE CASE - WHEN [e].[NullableBoolA] IS NULL THEN ~([e].[BoolA] ^ [e].[BoolB]) + WHEN [e].[NullableBoolA] IS NULL THEN CASE + WHEN [e].[BoolA] = [e].[BoolB] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END WHEN [e].[NullableBoolC] IS NULL THEN CASE WHEN ([e].[NullableBoolA] <> [e].[NullableBoolC] OR [e].[NullableBoolA] IS NULL OR [e].[NullableBoolC] IS NULL) AND ([e].[NullableBoolA] IS NOT NULL OR [e].[NullableBoolC] IS NOT NULL) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Translations/Operators/BitwiseOperatorTranslationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Translations/Operators/BitwiseOperatorTranslationsSqlServerTest.cs index 5ccc529caf9..0804fcdd2c5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Translations/Operators/BitwiseOperatorTranslationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Translations/Operators/BitwiseOperatorTranslationsSqlServerTest.cs @@ -123,7 +123,10 @@ public override async Task Xor_over_boolean() """ SELECT [b].[Id], [b].[Bool], [b].[Byte], [b].[ByteArray], [b].[DateOnly], [b].[DateTime], [b].[DateTimeOffset], [b].[Decimal], [b].[Double], [b].[Enum], [b].[FlagsEnum], [b].[Float], [b].[Guid], [b].[Int], [b].[Long], [b].[Short], [b].[String], [b].[TimeOnly], [b].[TimeSpan] FROM [BasicTypesEntities] AS [b] -WHERE ~CAST([b].[Int] ^ [b].[Short] AS bit) ^ CASE +WHERE CASE + WHEN [b].[Int] = [b].[Short] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END ^ CASE WHEN [b].[String] = N'Seattle' THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END = CAST(1 AS bit)