Skip to content

Commit 5b75cca

Browse files
committed
Simpler/safer patch fix for #35393 that doesn't compensate for some changes
1 parent cc16006 commit 5b75cca

13 files changed

+1870
-292
lines changed

src/EFCore.Relational/Query/SqlExpressionFactory.cs

+29-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ namespace Microsoft.EntityFrameworkCore.Query;
99
/// <inheritdoc />
1010
public class SqlExpressionFactory : ISqlExpressionFactory
1111
{
12+
private static readonly bool UseOldBehavior35393 =
13+
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35393", out var enabled35393) && enabled35393;
14+
1215
private readonly IRelationalTypeMappingSource _typeMappingSource;
1316
private readonly RelationalTypeMapping _boolTypeMapping;
1417

@@ -660,20 +663,45 @@ private SqlExpression Not(SqlExpression operand, SqlExpression? existingExpressi
660663
SqlBinaryExpression { OperatorType: ExpressionType.OrElse } binary
661664
=> AndAlso(Not(binary.Left), Not(binary.Right)),
662665

663-
// use equality where possible
666+
// use equality where possible - we can only do this when we know a is not null
667+
// at this point we are limited to constants, parameters and columns
668+
// more comprehensive optimization is done during null expansion
664669
// !(a == true) -> a == false
665670
// !(a == false) -> a == true
666671
SqlBinaryExpression { OperatorType: ExpressionType.Equal, Right: SqlConstantExpression { Value: bool } } binary
672+
when UseOldBehavior35393
673+
=> Equal(binary.Left, Not(binary.Right)),
674+
675+
SqlBinaryExpression
676+
{
677+
OperatorType: ExpressionType.Equal,
678+
Right: SqlConstantExpression { Value: bool },
679+
Left: SqlConstantExpression { Value: bool }
680+
or SqlParameterExpression { IsNullable: false }
681+
or ColumnExpression { IsNullable: false }
682+
} binary
667683
=> Equal(binary.Left, Not(binary.Right)),
668684

669685
// !(true == a) -> false == a
670686
// !(false == a) -> true == a
671687
SqlBinaryExpression { OperatorType: ExpressionType.Equal, Left: SqlConstantExpression { Value: bool } } binary
688+
when UseOldBehavior35393
689+
=> Equal(Not(binary.Left), binary.Right),
690+
691+
SqlBinaryExpression
692+
{
693+
OperatorType: ExpressionType.Equal,
694+
Left: SqlConstantExpression { Value: bool },
695+
Right: SqlConstantExpression { Value: bool }
696+
or SqlParameterExpression { IsNullable: false }
697+
or ColumnExpression { IsNullable: false }
698+
} binary
672699
=> Equal(Not(binary.Left), binary.Right),
673700

674701
// !(a == b) -> a != b
675702
SqlBinaryExpression { OperatorType: ExpressionType.Equal } sqlBinaryOperand => NotEqual(
676703
sqlBinaryOperand.Left, sqlBinaryOperand.Right),
704+
677705
// !(a != b) -> a == b
678706
SqlBinaryExpression { OperatorType: ExpressionType.NotEqual } sqlBinaryOperand => Equal(
679707
sqlBinaryOperand.Left, sqlBinaryOperand.Right),

src/EFCore.Relational/Query/SqlNullabilityProcessor.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ namespace Microsoft.EntityFrameworkCore.Query;
1919
/// </summary>
2020
public class SqlNullabilityProcessor
2121
{
22+
private static readonly bool UseOldBehavior35393 =
23+
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35393", out var enabled35393) && enabled35393;
24+
2225
private readonly List<ColumnExpression> _nonNullableColumns;
2326
private readonly List<ColumnExpression> _nullValueColumns;
2427
private readonly ISqlExpressionFactory _sqlExpressionFactory;
@@ -1343,7 +1346,7 @@ protected virtual SqlExpression VisitSqlBinary(
13431346
// we assume that NullSemantics rewrite is only needed (on the current level)
13441347
// if the optimization didn't make any changes.
13451348
// Reason is that optimization can/will change the nullability of the resulting expression
1346-
// and that inforation is not tracked/stored anywhere
1349+
// and that information is not tracked/stored anywhere
13471350
// so we can no longer rely on nullabilities that we computed earlier (leftNullable, rightNullable)
13481351
// when performing null semantics rewrite.
13491352
// It should be fine because current optimizations *radically* change the expression

test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs

+53-20
Original file line numberDiff line numberDiff line change
@@ -92,22 +92,26 @@ public virtual async Task Rewrite_compare_int_with_int(bool async)
9292
public virtual async Task Rewrite_compare_bool_with_bool(bool async)
9393
{
9494
var bools = new[] { false, true };
95+
var onetwothree = new[] { 1, 2, 3 };
9596

9697
foreach (var neq in bools)
9798
{
9899
foreach (var negated in bools)
99100
{
100101
foreach (var negateB in bools)
101102
{
102-
foreach (var nullableA in bools)
103+
foreach (var nullableA in onetwothree)
103104
{
104105
foreach (var negateA in bools)
105106
{
106-
foreach (var nullableB in bools)
107+
foreach (var nullableB in onetwothree)
107108
{
109+
// filter out tests comparing two constants
110+
if (nullableA == 3 && nullableB == 3) continue;
111+
108112
var queryBuilder = (ISetSource ss) =>
109113
{
110-
var data = nullableA
114+
var data = nullableA == 2
111115
? ss.Set<NullSemanticsEntity1>().Select(
112116
e => new
113117
{
@@ -116,30 +120,47 @@ public virtual async Task Rewrite_compare_bool_with_bool(bool async)
116120
e.BoolB,
117121
e.NullableBoolB
118122
})
119-
: ss.Set<NullSemanticsEntity1>().Select(
120-
e => new
121-
{
122-
e.Id,
123-
A = (bool?)e.BoolA,
124-
e.BoolB,
125-
e.NullableBoolB
126-
});
127-
128-
var query = nullableB
123+
: nullableA == 1
124+
? ss.Set<NullSemanticsEntity1>().Select(
125+
e => new
126+
{
127+
e.Id,
128+
A = (bool?)e.BoolA,
129+
e.BoolB,
130+
e.NullableBoolB
131+
})
132+
: ss.Set<NullSemanticsEntity1>().Select(
133+
e => new
134+
{
135+
e.Id,
136+
A = (bool?)true,
137+
e.BoolB,
138+
e.NullableBoolB
139+
});
140+
141+
var query = nullableB == 2
129142
? data.Select(
130143
e => new
131144
{
132145
e.Id,
133146
e.A,
134147
B = e.NullableBoolB
135148
})
136-
: data.Select(
137-
e => new
138-
{
139-
e.Id,
140-
e.A,
141-
B = (bool?)e.BoolB
142-
});
149+
: nullableB == 1
150+
? data.Select(
151+
e => new
152+
{
153+
e.Id,
154+
e.A,
155+
B = (bool?)e.BoolB
156+
})
157+
: data.Select(
158+
e => new
159+
{
160+
e.Id,
161+
e.A,
162+
B = (bool?)true
163+
});
143164

144165
query = negateA
145166
? query.Select(
@@ -2462,6 +2483,18 @@ await AssertQueryScalar(
24622483
ss => ss.Set<NullSemanticsEntity1>().Where(e => true).Select(e => e.Id));
24632484
}
24642485

2486+
[ConditionalTheory]
2487+
[MemberData(nameof(IsAsyncData))]
2488+
public virtual async Task Compare_constant_true_to_expression_which_evaluates_to_null(bool async)
2489+
{
2490+
var prm = default(bool?);
2491+
2492+
await AssertQueryScalar(
2493+
async,
2494+
ss => ss.Set<NullSemanticsEntity1>().Where(x => x.NullableBoolA != null
2495+
&& !object.Equals(true, x.NullableBoolA == null ? null : prm)).Select(x => x.Id));
2496+
}
2497+
24652498
// We can't client-evaluate Like (for the expected results).
24662499
// However, since the test data has no LIKE wildcards, it effectively functions like equality - except that 'null like null' returns
24672500
// false instead of true. So we have this "lite" implementation which doesn't support wildcards.

test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs

+18
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,24 @@ join o in ss.Set<Order>().OrderBy(o => o.OrderID).Take(100) on c.CustomerID equa
654654
from o in lo.Where(x => x.CustomerID.StartsWith("A"))
655655
select new { c.CustomerID, o.OrderID });
656656

657+
[ConditionalTheory]
658+
[MemberData(nameof(IsAsyncData))]
659+
public virtual Task GroupJoin_on_true_equal_true(bool async)
660+
=> AssertQuery(
661+
async,
662+
ss => ss.Set<Customer>().GroupJoin(
663+
ss.Set<Order>(),
664+
x => true,
665+
x => true,
666+
(c, g) => new { c, g })
667+
.Select(x => new { x.c.CustomerID, Orders = x.g }),
668+
elementSorter: e => e.CustomerID,
669+
elementAsserter: (e, a) =>
670+
{
671+
Assert.Equal(e.CustomerID, a.CustomerID);
672+
AssertCollection(e.Orders, a.Orders, elementSorter: ee => ee.OrderID);
673+
});
674+
657675
[ConditionalTheory]
658676
[MemberData(nameof(IsAsyncData))]
659677
public virtual Task Inner_join_with_tautology_predicate_converts_to_cross_join(bool async)

test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs

+13-13
Original file line numberDiff line numberDiff line change
@@ -5021,7 +5021,7 @@ INNER JOIN (
50215021
FROM [Factions] AS [f]
50225022
WHERE [f].[Name] = N'Swarm'
50235023
) AS [f0] ON [l].[Name] = [f0].[CommanderName]
5024-
WHERE [f0].[Eradicated] = CAST(0 AS bit) OR [f0].[Eradicated] IS NULL
5024+
WHERE [f0].[Eradicated] <> CAST(1 AS bit) OR [f0].[Eradicated] IS NULL
50255025
""");
50265026
}
50275027

@@ -5038,7 +5038,7 @@ LEFT JOIN (
50385038
FROM [Factions] AS [f]
50395039
WHERE [f].[Name] = N'Swarm'
50405040
) AS [f0] ON [l].[Name] = [f0].[CommanderName]
5041-
WHERE [f0].[Eradicated] = CAST(0 AS bit) OR [f0].[Eradicated] IS NULL
5041+
WHERE [f0].[Eradicated] <> CAST(1 AS bit) OR [f0].[Eradicated] IS NULL
50425042
""");
50435043
}
50445044

@@ -7585,17 +7585,17 @@ public override async Task Join_inner_source_custom_projection_followed_by_filte
75857585

75867586
AssertSql(
75877587
"""
7588-
SELECT CASE
7589-
WHEN [f].[Name] = N'Locust' THEN CAST(1 AS bit)
7590-
END AS [IsEradicated], [f].[CommanderName], [f].[Name]
7591-
FROM [LocustLeaders] AS [l]
7592-
INNER JOIN [Factions] AS [f] ON [l].[Name] = [f].[CommanderName]
7593-
WHERE CASE
7594-
WHEN [f].[Name] = N'Locust' THEN CAST(1 AS bit)
7595-
END = CAST(0 AS bit) OR CASE
7596-
WHEN [f].[Name] = N'Locust' THEN CAST(1 AS bit)
7597-
END IS NULL
7598-
""");
7588+
SELECT CASE
7589+
WHEN [f].[Name] = N'Locust' THEN CAST(1 AS bit)
7590+
END AS [IsEradicated], [f].[CommanderName], [f].[Name]
7591+
FROM [LocustLeaders] AS [l]
7592+
INNER JOIN [Factions] AS [f] ON [l].[Name] = [f].[CommanderName]
7593+
WHERE CASE
7594+
WHEN [f].[Name] = N'Locust' THEN CAST(1 AS bit)
7595+
END <> CAST(1 AS bit) OR CASE
7596+
WHEN [f].[Name] = N'Locust' THEN CAST(1 AS bit)
7597+
END IS NULL
7598+
""");
75997599
}
76007600

76017601
public override async Task Byte_array_contains_literal(bool async)

test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs

+16
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,22 @@ ORDER BY [o].[OrderID]
992992
""");
993993
}
994994

995+
public override async Task GroupJoin_on_true_equal_true(bool async)
996+
{
997+
await base.GroupJoin_on_true_equal_true(async);
998+
999+
AssertSql(
1000+
"""
1001+
SELECT [c].[CustomerID], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate]
1002+
FROM [Customers] AS [c]
1003+
OUTER APPLY (
1004+
SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
1005+
FROM [Orders] AS [o]
1006+
) AS [o0]
1007+
ORDER BY [c].[CustomerID]
1008+
""");
1009+
}
1010+
9951011
private void AssertSql(params string[] expected)
9961012
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
9971013

0 commit comments

Comments
 (0)