Skip to content

Commit

Permalink
Use bitwise NOT on BIT SqlServer expressions (#34303)
Browse files Browse the repository at this point in the history
Since #34080 the SqlServer provider uses XOR to avoid converting to/from predicates when negating BIT expressions.
It is actually possible to simply use ~ on BIT values and this changeset implements this.

Fixes #34213

* Add minimal support for `ExpressionType.OnesComplement` It is currently only supported in the final part of SqlServer translation pipeline.
* Use `~` for negating boolean values Instead of `x ^ 1`, use the `~x` expression when translating boolean values in SqlServer.
  • Loading branch information
ranma42 authored Jul 29, 2024
1 parent 5aa72df commit 1e828fd
Show file tree
Hide file tree
Showing 13 changed files with 95 additions and 77 deletions.
3 changes: 2 additions & 1 deletion src/EFCore.Relational/Query/QuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,7 @@ protected override Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpressio
break;
}

case ExpressionType.OnesComplement:
case ExpressionType.Not:
{
_relationalCommandBuilder.Append("~");
Expand Down Expand Up @@ -1099,7 +1100,7 @@ protected virtual string GetOperator(SqlBinaryExpression binaryExpression)
ExpressionType.Or => " | ",
ExpressionType.ExclusiveOr => " ^ ",

_ => throw new UnreachableException($"Unsupported unary OperatorType: {binaryExpression.OperatorType}")
_ => throw new UnreachableException($"Unsupported binary OperatorType: {binaryExpression.OperatorType}")
};

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/EFCore.Relational/Query/SqlExpressionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ private SqlExpression ApplyTypeMappingOnSqlUnary(

case ExpressionType.Not:
case ExpressionType.Negate:
case ExpressionType.OnesComplement:
resultTypeMapping = typeMapping;
// While Not is logical, negate is numeric hence we use clrType from TypeMapping
resultType = resultTypeMapping?.ClrType ?? sqlUnaryExpression.Type;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public class SqlUnaryExpression : SqlExpression
ExpressionType.NotEqual,
ExpressionType.Convert,
ExpressionType.Not,
ExpressionType.Negate
ExpressionType.Negate,
ExpressionType.OnesComplement
};

internal static bool IsValidOperator(ExpressionType operatorType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,20 +364,28 @@ protected override Expression VisitSqlBinary(SqlBinaryExpression sqlBinaryExpres
result = _sqlExpressionFactory.Convert(result, typeof(bool), sqlBinaryExpression.TypeMapping);
}

// "lhs == rhs" is the same as "NOT(lhs == rhs)" aka "lhs ^ rhs ^ 1"
// "lhs == rhs" is the same as "NOT(lhs != rhs)" aka "~(lhs ^ rhs)"
if (sqlBinaryExpression.OperatorType is ExpressionType.Equal)
{
result = _sqlExpressionFactory.MakeBinary(
ExpressionType.ExclusiveOr,
result = _sqlExpressionFactory.MakeUnary(
ExpressionType.OnesComplement,
result,
_sqlExpressionFactory.Constant(true, result.TypeMapping),
result.Type,
result.TypeMapping
)!;
}

return result;
}

if (sqlBinaryExpression.OperatorType is ExpressionType.NotEqual or ExpressionType.Equal
&& newLeft is SqlUnaryExpression { OperatorType: ExpressionType.OnesComplement } negatedLeft
&& newRight is SqlUnaryExpression { OperatorType: ExpressionType.OnesComplement } negatedRight)
{
newLeft = negatedLeft.Operand;
newRight = negatedRight.Operand;
}

sqlBinaryExpression = sqlBinaryExpression.Update(newLeft, newRight);
var condition = sqlBinaryExpression.OperatorType is ExpressionType.AndAlso
or ExpressionType.OrElse
Expand Down Expand Up @@ -410,10 +418,16 @@ protected override Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpressio
if (!_isSearchCondition && sqlUnaryExpression.Operand is not (ExistsExpression or InExpression or LikeExpression))
{
var negatedOperand = (SqlExpression)Visit(sqlUnaryExpression.Operand);
return _sqlExpressionFactory.MakeBinary(
ExpressionType.ExclusiveOr,

if (negatedOperand is SqlUnaryExpression { OperatorType: ExpressionType.OnesComplement } unary)
{
return unary.Operand;
}

return _sqlExpressionFactory.MakeUnary(
ExpressionType.OnesComplement,
negatedOperand,
_sqlExpressionFactory.Constant(true, negatedOperand.TypeMapping),
negatedOperand.Type,
negatedOperand.TypeMapping
)!;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,7 @@ protected override bool TryGetOperatorInfo(SqlExpression expression, out int pre
SqlUnaryExpression sqlUnaryExpression => sqlUnaryExpression.OperatorType switch
{
ExpressionType.Convert => (1300, false),
ExpressionType.OnesComplement => (1200, false),
ExpressionType.Not when sqlUnaryExpression.Type != typeof(bool) => (1200, false),
ExpressionType.Negate => (1100, false),
ExpressionType.Equal => (500, false), // IS NULL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1676,7 +1676,7 @@ public override async Task Conditional_expression_with_conditions_does_not_colla
AssertSql(
"""
SELECT CASE
WHEN [c0].[Id] IS NOT NULL THEN [c0].[Processed] ^ CAST(1 AS bit)
WHEN [c0].[Id] IS NOT NULL THEN ~[c0].[Processed]
END AS [Processing]
FROM [Carts] AS [c]
LEFT JOIN [Configuration] AS [c0] ON [c].[ConfigurationId] = [c0].[Id]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ public override async Task Bitwise_projects_values_in_select(bool async)

AssertSql(
"""
SELECT TOP(1) CAST(([g].[Rank] & 2) ^ 2 AS bit) ^ CAST(1 AS bit) AS [BitwiseTrue], CAST(([g].[Rank] & 2) ^ 4 AS bit) ^ CAST(1 AS bit) AS [BitwiseFalse], [g].[Rank] & 2 AS [BitwiseValue]
SELECT TOP(1) ~CAST(([g].[Rank] & 2) ^ 2 AS bit) AS [BitwiseTrue], ~CAST(([g].[Rank] & 2) ^ 4 AS bit) AS [BitwiseFalse], [g].[Rank] & 2 AS [BitwiseValue]
FROM [Gears] AS [g]
WHERE [g].[Rank] & 2 = 2
""");
Expand Down Expand Up @@ -716,7 +716,7 @@ public override async Task Select_enum_has_flag(bool async)

AssertSql(
"""
SELECT TOP(1) CAST(([g].[Rank] & 2) ^ 2 AS bit) ^ CAST(1 AS bit) AS [hasFlagTrue], CAST(([g].[Rank] & 4) ^ 4 AS bit) ^ CAST(1 AS bit) AS [hasFlagFalse]
SELECT TOP(1) ~CAST(([g].[Rank] & 2) ^ 2 AS bit) AS [hasFlagTrue], ~CAST(([g].[Rank] & 4) ^ 4 AS bit) AS [hasFlagFalse]
FROM [Gears] AS [g]
WHERE [g].[Rank] & 2 = 2
""");
Expand Down Expand Up @@ -758,7 +758,7 @@ public override async Task Select_inverted_boolean(bool async)

AssertSql(
"""
SELECT [w].[Id], [w].[IsAutomatic] ^ CAST(1 AS bit) AS [Manual]
SELECT [w].[Id], ~[w].[IsAutomatic] AS [Manual]
FROM [Weapons] AS [w]
WHERE [w].[IsAutomatic] = CAST(1 AS bit)
""");
Expand All @@ -770,7 +770,7 @@ public override async Task Select_inverted_nullable_boolean(bool async)

AssertSql(
"""
SELECT [f].[Id], [f].[Eradicated] ^ CAST(1 AS bit) AS [Alive]
SELECT [f].[Id], ~[f].[Eradicated] AS [Alive]
FROM [Factions] AS [f]
""");
}
Expand Down Expand Up @@ -1069,7 +1069,7 @@ public override async Task Select_null_propagation_negative1(bool async)
AssertSql(
"""
SELECT CASE
WHEN [g].[LeaderNickname] IS NOT NULL THEN CAST(CAST(LEN([g].[Nickname]) AS int) ^ 5 AS bit) ^ CAST(1 AS bit)
WHEN [g].[LeaderNickname] IS NOT NULL THEN ~CAST(CAST(LEN([g].[Nickname]) AS int) ^ 5 AS bit)
END
FROM [Gears] AS [g]
""");
Expand Down Expand Up @@ -1188,7 +1188,7 @@ public override async Task Select_null_propagation_negative9(bool async)
AssertSql(
"""
SELECT CASE
WHEN [g].[LeaderNickname] IS NOT NULL THEN CAST(CAST(LEN([g].[Nickname]) AS int) ^ 5 AS bit) ^ CAST(1 AS bit)
WHEN [g].[LeaderNickname] IS NOT NULL THEN ~CAST(CAST(LEN([g].[Nickname]) AS int) ^ 5 AS bit)
END
FROM [Gears] AS [g]
""");
Expand Down Expand Up @@ -5160,10 +5160,10 @@ public override async Task Negated_bool_ternary_inside_anonymous_type_in_project

AssertSql(
"""
SELECT CASE
SELECT ~CASE
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]
END AS [c]
FROM [Tags] AS [t]
LEFT JOIN [Gears] AS [g] ON [t].[GearNickName] = [g].[Nickname] AND [t].[GearSquadId] = [g].[SquadId]
""");
Expand Down Expand Up @@ -6317,12 +6317,12 @@ public override async Task OrderBy_same_expression_containing_IsNull_correctly_d
AssertSql(
"""
SELECT CASE
WHEN [g].[LeaderNickname] IS NOT NULL THEN CAST(CAST(LEN([g].[Nickname]) AS int) ^ 5 AS bit) ^ CAST(1 AS bit)
WHEN [g].[LeaderNickname] IS NOT NULL THEN ~CAST(CAST(LEN([g].[Nickname]) AS int) ^ 5 AS bit)
END
FROM [Gears] AS [g]
ORDER BY CASE
WHEN CASE
WHEN [g].[LeaderNickname] IS NOT NULL THEN CAST(CAST(LEN([g].[Nickname]) AS int) ^ 5 AS bit) ^ CAST(1 AS bit)
WHEN [g].[LeaderNickname] IS NOT NULL THEN ~CAST(CAST(LEN([g].[Nickname]) AS int) ^ 5 AS bit)
END IS NOT NULL THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
Expand Down Expand Up @@ -7488,14 +7488,14 @@ WHERE [g].[Rank] & @__ranks_0 <> 0
"""
@__ranks_0='134'

SELECT CAST(([g].[Rank] | @__ranks_0) ^ @__ranks_0 AS bit) ^ CAST(1 AS bit)
SELECT ~CAST(([g].[Rank] | @__ranks_0) ^ @__ranks_0 AS bit)
FROM [Gears] AS [g]
""",
//
"""
@__ranks_0='134'

SELECT CAST(([g].[Rank] | [g].[Rank] | @__ranks_0 | [g].[Rank] | @__ranks_0) ^ @__ranks_0 AS bit) ^ CAST(1 AS bit)
SELECT ~CAST(([g].[Rank] | [g].[Rank] | @__ranks_0 | [g].[Rank] | @__ranks_0) ^ @__ranks_0 AS bit)
FROM [Gears] AS [g]
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2344,7 +2344,7 @@ public override async Task Json_boolean_projection_negated(bool async)

AssertSql(
"""
SELECT CAST(JSON_VALUE([j].[Reference], '$.TestBoolean') AS bit) ^ CAST(1 AS bit)
SELECT ~CAST(JSON_VALUE([j].[Reference], '$.TestBoolean') AS bit)
FROM [JsonEntitiesAllTypes] AS [j]
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -926,7 +926,7 @@ await AssertQueryScalar(

AssertSql(
"""
SELECT CAST(ISNUMERIC(COALESCE(CONVERT(varchar(100), [o].[OrderDate]), '')) ^ 1 AS bit) ^ CAST(1 AS bit)
SELECT ~CAST(ISNUMERIC(COALESCE(CONVERT(varchar(100), [o].[OrderDate]), '')) ^ 1 AS bit)
FROM [Orders] AS [o]
WHERE ISNUMERIC(COALESCE(CONVERT(varchar(100), [o].[OrderDate]), '')) <> 1
""");
Expand All @@ -945,7 +945,7 @@ await AssertQueryScalar(

AssertSql(
"""
SELECT CAST(ISNUMERIC(CONVERT(varchar(100), [o].[UnitPrice])) ^ 1 AS bit) ^ CAST(1 AS bit)
SELECT ~CAST(ISNUMERIC(CONVERT(varchar(100), [o].[UnitPrice])) ^ 1 AS bit)
FROM [Order Details] AS [o]
WHERE ISNUMERIC(CONVERT(varchar(100), [o].[UnitPrice])) = 1
""");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public override async Task Rewrite_compare_int_with_int(bool async)

AssertSql(
"""
SELECT [e].[Id], CAST([e].[IntA] ^ [e].[IntB] AS bit) ^ CAST(1 AS bit) AS [X]
SELECT [e].[Id], ~CAST([e].[IntA] ^ [e].[IntB] AS bit) AS [X]
FROM [Entities1] AS [e]
""",
//
Expand Down Expand Up @@ -181,7 +181,7 @@ FROM [Entities1] AS [e]
""",
//
"""
SELECT [e].[Id], CAST([e].[IntA] ^ [e].[IntB] AS bit) ^ CAST(1 AS bit) AS [X]
SELECT [e].[Id], ~CAST([e].[IntA] ^ [e].[IntB] AS bit) AS [X]
FROM [Entities1] AS [e]
""",
//
Expand Down Expand Up @@ -240,7 +240,7 @@ public override async Task Rewrite_compare_bool_with_bool(bool async)

AssertSql(
"""
SELECT [e].[Id], [e].[BoolA] ^ [e].[BoolB] ^ CAST(1 AS bit) AS [X]
SELECT [e].[Id], ~([e].[BoolA] ^ [e].[BoolB]) AS [X]
FROM [Entities1] AS [e]
""",
//
Expand Down Expand Up @@ -371,7 +371,7 @@ WHERE [e].[BoolA] <> [e].[NullableBoolB]
""",
//
"""
SELECT [e].[Id], [e].[BoolA] ^ [e].[BoolB] ^ CAST(1 AS bit) AS [X]
SELECT [e].[Id], ~([e].[BoolA] ^ [e].[BoolB]) AS [X]
FROM [Entities1] AS [e]
""",
//
Expand Down Expand Up @@ -477,7 +477,7 @@ WHERE [e].[BoolA] <> [e].[NullableBoolB] OR [e].[NullableBoolB] IS NULL
""",
//
"""
SELECT [e].[Id], [e].[BoolA] ^ [e].[BoolB] ^ CAST(1 AS bit) AS [X]
SELECT [e].[Id], ~([e].[BoolA] ^ [e].[BoolB]) AS [X]
FROM [Entities1] AS [e]
""",
//
Expand Down Expand Up @@ -558,7 +558,7 @@ FROM [Entities1] AS [e]
""",
//
"""
SELECT [e].[Id], [e].[BoolA] ^ [e].[BoolB] ^ CAST(1 AS bit) AS [X]
SELECT [e].[Id], ~([e].[BoolA] ^ [e].[BoolB]) AS [X]
FROM [Entities1] AS [e]
""",
//
Expand Down Expand Up @@ -689,7 +689,7 @@ WHERE [e].[BoolA] <> [e].[NullableBoolB] OR [e].[NullableBoolB] IS NULL
""",
//
"""
SELECT [e].[Id], [e].[BoolA] ^ [e].[BoolB] ^ CAST(1 AS bit) AS [X]
SELECT [e].[Id], ~([e].[BoolA] ^ [e].[BoolB]) AS [X]
FROM [Entities1] AS [e]
""",
//
Expand Down Expand Up @@ -770,7 +770,7 @@ FROM [Entities1] AS [e]
""",
//
"""
SELECT [e].[Id], [e].[BoolA] ^ [e].[BoolB] ^ CAST(1 AS bit) AS [X]
SELECT [e].[Id], ~([e].[BoolA] ^ [e].[BoolB]) AS [X]
FROM [Entities1] AS [e]
""",
//
Expand Down Expand Up @@ -876,7 +876,7 @@ FROM [Entities1] AS [e]
""",
//
"""
SELECT [e].[Id], [e].[BoolA] ^ [e].[BoolB] ^ CAST(1 AS bit) AS [X]
SELECT [e].[Id], ~([e].[BoolA] ^ [e].[BoolB]) AS [X]
FROM [Entities1] AS [e]
""",
//
Expand Down Expand Up @@ -1007,7 +1007,7 @@ WHERE [e].[BoolA] <> [e].[NullableBoolB]
""",
//
"""
SELECT [e].[Id], [e].[BoolA] ^ [e].[BoolB] ^ CAST(1 AS bit) AS [X]
SELECT [e].[Id], ~([e].[BoolA] ^ [e].[BoolB]) AS [X]
FROM [Entities1] AS [e]
""",
//
Expand Down Expand Up @@ -1696,7 +1696,7 @@ public override async Task Compare_complex_equal_equal_equal(bool async)
"""
SELECT [e].[Id]
FROM [Entities1] AS [e]
WHERE [e].[BoolA] ^ [e].[BoolB] ^ CAST(1 AS bit) = CAST([e].[IntA] ^ [e].[IntB] AS bit) ^ CAST(1 AS bit)
WHERE [e].[BoolA] ^ [e].[BoolB] = CAST([e].[IntA] ^ [e].[IntB] AS bit)
""",
//
"""
Expand Down Expand Up @@ -1732,7 +1732,7 @@ 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(1 AS bit) <> CAST([e].[IntA] ^ [e].[IntB] AS bit) ^ CAST(1 AS bit)
WHERE [e].[BoolA] ^ [e].[BoolB] <> CAST([e].[IntA] ^ [e].[IntB] AS bit)
""",
//
"""
Expand Down Expand Up @@ -1768,7 +1768,7 @@ 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) ^ CAST(1 AS bit)
WHERE [e].[BoolA] ^ [e].[BoolB] = ~CAST([e].[IntA] ^ [e].[IntB] AS bit)
""",
//
"""
Expand Down Expand Up @@ -1804,7 +1804,7 @@ 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) ^ CAST(1 AS bit)
WHERE [e].[BoolA] ^ [e].[BoolB] <> ~CAST([e].[IntA] ^ [e].[IntB] AS bit)
""",
//
"""
Expand Down Expand Up @@ -4247,7 +4247,7 @@ 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) ^ CAST(1 AS bit) <> CASE
WHERE ~CAST([e].[IntA] ^ [e].[IntB] AS bit) <> CASE
WHEN [e].[NullableBoolA] IS NOT NULL THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
Expand Down Expand Up @@ -4400,7 +4400,7 @@ 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] ^ CAST(1 AS bit)
WHEN [e].[NullableBoolA] IS NULL THEN ~([e].[BoolA] ^ [e].[BoolB])
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)
Expand Down
Loading

0 comments on commit 1e828fd

Please sign in to comment.