Skip to content

Commit 62d1bb9

Browse files
rojiAndriySvyryd
authored andcommitted
Translate to NULLIF
Closes dotnet#31682
1 parent 870a449 commit 62d1bb9

14 files changed

Lines changed: 397 additions & 73 deletions

File tree

EFCore.slnx.DotSettings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ The .NET Foundation licenses this file to you under the MIT license.
357357
<s:Boolean x:Key="/Default/UserDictionary/Words/=subquery/@EntryIndexedValue">True</s:Boolean>
358358
<s:Boolean x:Key="/Default/UserDictionary/Words/=subquery_0027s/@EntryIndexedValue">True</s:Boolean>
359359
<s:Boolean x:Key="/Default/UserDictionary/Words/=transactionality/@EntryIndexedValue">True</s:Boolean>
360+
<s:Boolean x:Key="/Default/UserDictionary/Words/=uncoalesce/@EntryIndexedValue">True</s:Boolean>
361+
<s:Boolean x:Key="/Default/UserDictionary/Words/=uncoalescing/@EntryIndexedValue">True</s:Boolean>
360362
<s:Boolean x:Key="/Default/UserDictionary/Words/=unconfigured/@EntryIndexedValue">True</s:Boolean>
361363
<s:Boolean x:Key="/Default/UserDictionary/Words/=unignore/@EntryIndexedValue">True</s:Boolean>
362364
<s:Boolean x:Key="/Default/UserDictionary/Words/=fixup/@EntryIndexedValue">True</s:Boolean>

src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -707,10 +707,37 @@ public virtual SqlExpression Condition(SqlExpression test, SqlExpression ifTrue,
707707
{
708708
var typeMapping = ExpressionExtensions.InferTypeMapping(ifTrue, ifFalse);
709709

710-
return new SqlConditionalExpression(
711-
ApplyTypeMapping(test, _boolTypeMapping),
712-
ApplyTypeMapping(ifTrue, typeMapping),
713-
ApplyTypeMapping(ifFalse, typeMapping));
710+
test = ApplyTypeMapping(test, _boolTypeMapping);
711+
ifTrue = ApplyTypeMapping(ifTrue, typeMapping);
712+
ifFalse = ApplyTypeMapping(ifFalse, typeMapping);
713+
714+
// Simplify:
715+
// a == b ? b : a -> a
716+
// a != b ? a : b -> a
717+
if (test is SqlBinaryExpression
718+
{
719+
OperatorType: ExpressionType.Equal or ExpressionType.NotEqual,
720+
Left: SqlExpression left,
721+
Right: SqlExpression right
722+
} binary)
723+
{
724+
// Swap ifTrue/ifFalse for NotEqual so we can reason uniformly about ifEqual/ifNotEqual.
725+
var (ifEqual, ifNotEqual) = binary.OperatorType is ExpressionType.Equal ? (ifTrue, ifFalse) : (ifFalse, ifTrue);
726+
727+
// 'left' survives: a == b ? b : a -> a
728+
if (left.Equals(ifNotEqual) && right.Equals(ifEqual))
729+
{
730+
return left;
731+
}
732+
733+
// 'right' survives: a == b ? a : b -> b
734+
if (right.Equals(ifNotEqual) && left.Equals(ifEqual))
735+
{
736+
return right;
737+
}
738+
}
739+
740+
return new SqlConditionalExpression(test, ifTrue, ifFalse);
714741
}
715742

716743
/// <summary>

src/EFCore.Relational/Query/SqlExpressionFactory.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,61 @@ public virtual SqlExpression Case(
837837
elseResult = lastCase.ElseResult;
838838
}
839839

840+
// Simplify:
841+
// a == b ? b : a -> a
842+
// a != b ? a : b -> a
843+
// And lift:
844+
// a == b ? null : a -> NULLIF(a, b)
845+
// a != b ? a : null -> NULLIF(a, b)
846+
if (operand is null
847+
&& typeMappedWhenClauses is
848+
[
849+
{
850+
Test: SqlBinaryExpression
851+
{
852+
OperatorType: ExpressionType.Equal or ExpressionType.NotEqual,
853+
Left: var left,
854+
Right: var right
855+
} binary,
856+
Result: var result
857+
}
858+
])
859+
{
860+
// Swap result/elseResult for NotEqual so we can reason uniformly about ifEqual/ifNotEqual.
861+
// A missing ELSE clause is equivalent to ELSE NULL, so normalize to a null constant for matching.
862+
var (ifEqual, ifNotEqual) = binary.OperatorType is ExpressionType.Equal
863+
? (result, elseResult ?? Constant(null, result.Type, result.TypeMapping))
864+
: (elseResult ?? Constant(null, result.Type, result.TypeMapping), result);
865+
866+
// 'left' survives the conditional.
867+
if (left.Equals(ifNotEqual))
868+
{
869+
switch (ifEqual)
870+
{
871+
// a == b ? b : a -> a (also collapses NULLIF(a, NULL) when both are null constants)
872+
case var _ when ifEqual.Equals(right):
873+
return left;
874+
// a == b ? null : a -> NULLIF(a, b)
875+
case SqlConstantExpression { Value: null }:
876+
return Function("NULLIF", [left, right], nullable: true, [true, false], left.Type, left.TypeMapping);
877+
}
878+
}
879+
880+
// 'right' survives the conditional (operand-swapped equivalents of the patterns above).
881+
if (right.Equals(ifNotEqual))
882+
{
883+
switch (ifEqual)
884+
{
885+
// a == b ? a : b -> b
886+
case var _ when ifEqual.Equals(left):
887+
return right;
888+
// a == b ? null : b -> NULLIF(b, a)
889+
case SqlConstantExpression { Value: null }:
890+
return Function("NULLIF", [right, left], nullable: true, [true, false], right.Type, right.TypeMapping);
891+
}
892+
}
893+
}
894+
840895
return existingExpression is CaseExpression expr
841896
&& operand == expr.Operand
842897
&& typeMappedWhenClauses.SequenceEqual(expr.WhenClauses)

test/EFCore.Cosmos.FunctionalTests/Query/Translations/Operators/MiscellaneousOperatorTranslationsCosmosTest.cs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,90 @@ FROM root c
2424
""");
2525
}
2626

27+
public override async Task Conditional_simplifiable_equality()
28+
{
29+
await base.Conditional_simplifiable_equality();
30+
31+
AssertSql(
32+
"""
33+
SELECT VALUE c
34+
FROM root c
35+
WHERE (c["Int"] > 1)
36+
""");
37+
}
38+
39+
public override async Task Conditional_simplifiable_inequality()
40+
{
41+
await base.Conditional_simplifiable_inequality();
42+
43+
AssertSql(
44+
"""
45+
SELECT VALUE c
46+
FROM root c
47+
WHERE (c["Int"] > 1)
48+
""");
49+
}
50+
51+
public override async Task Conditional_uncoalesce_with_equality_left()
52+
{
53+
await base.Conditional_uncoalesce_with_equality_left();
54+
55+
AssertSql(
56+
"""
57+
SELECT VALUE c
58+
FROM root c
59+
WHERE (((c["Int"] = 9) ? null : c["Int"]) > 1)
60+
""");
61+
}
62+
63+
public override async Task Conditional_uncoalesce_with_equality_right()
64+
{
65+
await base.Conditional_uncoalesce_with_equality_right();
66+
67+
AssertSql(
68+
"""
69+
SELECT VALUE c
70+
FROM root c
71+
WHERE (((9 = c["Int"]) ? null : c["Int"]) > 1)
72+
""");
73+
}
74+
75+
public override async Task Conditional_uncoalesce_with_inequality_left()
76+
{
77+
await base.Conditional_uncoalesce_with_inequality_left();
78+
79+
AssertSql(
80+
"""
81+
SELECT VALUE c
82+
FROM root c
83+
WHERE (((c["Int"] != 9) ? c["Int"] : null) > 1)
84+
""");
85+
}
86+
87+
public override async Task Conditional_uncoalesce_with_inequality_right()
88+
{
89+
await base.Conditional_uncoalesce_with_inequality_right();
90+
91+
AssertSql(
92+
"""
93+
SELECT VALUE c
94+
FROM root c
95+
WHERE (((9 != c["Int"]) ? c["Int"] : null) > 1)
96+
""");
97+
}
98+
99+
public override async Task Conditional_uncoalesce_with_string()
100+
{
101+
await base.Conditional_uncoalesce_with_string();
102+
103+
AssertSql(
104+
"""
105+
SELECT VALUE c
106+
FROM root c
107+
WHERE (((c["String"] = "Seattle") ? null : c["String"]) = "London")
108+
""");
109+
}
110+
27111
public override async Task Coalesce()
28112
{
29113
await base.Coalesce();

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,7 @@ public virtual Task Where_equal_with_conditional(bool async)
711711
async,
712712
ss => ss.Set<NullSemanticsEntity1>().Where(e => (e.NullableStringA == e.NullableStringB
713713
? e.NullableStringA
714-
: e.NullableStringB)
714+
: e.NullableStringC)
715715
== e.NullableStringC).Select(e => e.Id));
716716

717717
[Theory, MemberData(nameof(IsAsyncData))]
@@ -721,7 +721,7 @@ public virtual Task Where_not_equal_with_conditional(bool async)
721721
ss => ss.Set<NullSemanticsEntity1>().Where(e => e.NullableStringC
722722
!= (e.NullableStringA == e.NullableStringB
723723
? e.NullableStringA
724-
: e.NullableStringB)).Select(e => e.Id));
724+
: e.NullableStringC)).Select(e => e.Id));
725725

726726
[Theory, MemberData(nameof(IsAsyncData))]
727727
public virtual Task Where_equal_with_conditional_non_nullable(bool async)

test/EFCore.Specification.Tests/Query/Translations/Operators/MiscellaneousOperatorTranslationsTestBase.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,38 @@ public abstract class MiscellaneousOperatorTranslationsTestBase<TFixture>(TFixtu
1212
public virtual async Task Conditional()
1313
=> await AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(b => (b.Int == 8 ? b.String : "Foo") == "Seattle"));
1414

15+
[Fact]
16+
public virtual async Task Conditional_simplifiable_equality()
17+
// ReSharper disable once MergeConditionalExpression
18+
=> await AssertQuery(ss => ss.Set<NullableBasicTypesEntity>().Where(x => (x.Int == 9 ? 9 : x.Int) > 1));
19+
20+
[Fact]
21+
public virtual async Task Conditional_simplifiable_inequality()
22+
// ReSharper disable once MergeConditionalExpression
23+
=> await AssertQuery(ss => ss.Set<NullableBasicTypesEntity>().Where(x => (x.Int != 8 ? x.Int : 8) > 1));
24+
25+
// In relational providers, x == a ? null : x ("un-coalescing conditional") is translated to SQL NULLIF
26+
27+
[Fact]
28+
public virtual async Task Conditional_uncoalesce_with_equality_left()
29+
=> await AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(x => (x.Int == 9 ? null : x.Int) > 1));
30+
31+
[Fact]
32+
public virtual async Task Conditional_uncoalesce_with_equality_right()
33+
=> await AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(x => (9 == x.Int ? null : x.Int) > 1));
34+
35+
[Fact]
36+
public virtual async Task Conditional_uncoalesce_with_inequality_left()
37+
=> await AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(x => (x.Int != 9 ? x.Int : null) > 1));
38+
39+
[Fact]
40+
public virtual async Task Conditional_uncoalesce_with_inequality_right()
41+
=> await AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(x => (9 != x.Int ? x.Int : null) > 1));
42+
43+
[Fact]
44+
public virtual async Task Conditional_uncoalesce_with_string()
45+
=> await AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(x => (x.String == "Seattle" ? null : x.String) == "London"));
46+
1547
[Fact]
1648
public virtual async Task Coalesce()
1749
=> await AssertQuery(ss => ss.Set<NullableBasicTypesEntity>().Where(b => (b.String ?? "Unknown") == "Seattle"));

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

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -873,9 +873,7 @@ public override async Task Select_null_propagation_works_for_multiple_navigation
873873

874874
AssertSql(
875875
"""
876-
SELECT CASE
877-
WHEN [c].[Name] IS NOT NULL THEN [c].[Name]
878-
END
876+
SELECT [c].[Name]
879877
FROM [Tags] AS [t]
880878
LEFT JOIN [Gears] AS [g] ON [t].[GearNickName] = [g].[Nickname] AND [t].[GearSquadId] = [g].[SquadId]
881879
LEFT JOIN [Tags] AS [t0] ON ([g].[Nickname] = [t0].[GearNickName] OR ([g].[Nickname] IS NULL AND [t0].[GearNickName] IS NULL)) AND ([g].[SquadId] = [t0].[GearSquadId] OR ([g].[SquadId] IS NULL AND [t0].[GearSquadId] IS NULL))
@@ -2048,10 +2046,7 @@ public override async Task Optional_navigation_type_compensation_works_with_pred
20482046
SELECT [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note]
20492047
FROM [Tags] AS [t]
20502048
LEFT JOIN [Gears] AS [g] ON [t].[GearNickName] = [g].[Nickname] AND [t].[GearSquadId] = [g].[SquadId]
2051-
WHERE CASE
2052-
WHEN [g].[HasSoulPatch] = CAST(1 AS bit) THEN CAST(1 AS bit)
2053-
ELSE [g].[HasSoulPatch]
2054-
END = CAST(0 AS bit)
2049+
WHERE [g].[HasSoulPatch] = CAST(0 AS bit)
20552050
""");
20562051
}
20572052

@@ -2064,10 +2059,7 @@ public override async Task Optional_navigation_type_compensation_works_with_pred
20642059
SELECT [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note]
20652060
FROM [Tags] AS [t]
20662061
LEFT JOIN [Gears] AS [g] ON [t].[GearNickName] = [g].[Nickname] AND [t].[GearSquadId] = [g].[SquadId]
2067-
WHERE CASE
2068-
WHEN [g].[HasSoulPatch] = CAST(0 AS bit) THEN CAST(0 AS bit)
2069-
ELSE [g].[HasSoulPatch]
2070-
END = CAST(0 AS bit)
2062+
WHERE [g].[HasSoulPatch] = CAST(0 AS bit)
20712063
""");
20722064
}
20732065

@@ -3114,9 +3106,7 @@ public override async Task Select_null_conditional_with_inheritance(bool async)
31143106

31153107
AssertSql(
31163108
"""
3117-
SELECT CASE
3118-
WHEN [f].[CommanderName] IS NOT NULL THEN [f].[CommanderName]
3119-
END
3109+
SELECT [f].[CommanderName]
31203110
FROM [Factions] AS [f]
31213111
""");
31223112
}

test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCGearsOfWarQuerySqlServerTest.cs

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,9 +1175,7 @@ public override async Task Select_null_propagation_works_for_multiple_navigation
11751175

11761176
AssertSql(
11771177
"""
1178-
SELECT CASE
1179-
WHEN [c].[Name] IS NOT NULL THEN [c].[Name]
1180-
END
1178+
SELECT [c].[Name]
11811179
FROM [Tags] AS [t]
11821180
LEFT JOIN (
11831181
SELECT [g].[Nickname], [g].[SquadId]
@@ -2822,10 +2820,7 @@ UNION ALL
28222820
SELECT [o].[Nickname], [o].[SquadId], [o].[HasSoulPatch]
28232821
FROM [Officers] AS [o]
28242822
) AS [u] ON [t].[GearNickName] = [u].[Nickname] AND [t].[GearSquadId] = [u].[SquadId]
2825-
WHERE CASE
2826-
WHEN [u].[HasSoulPatch] = CAST(1 AS bit) THEN CAST(1 AS bit)
2827-
ELSE [u].[HasSoulPatch]
2828-
END = CAST(0 AS bit)
2823+
WHERE [u].[HasSoulPatch] = CAST(0 AS bit)
28292824
""");
28302825
}
28312826

@@ -2844,10 +2839,7 @@ UNION ALL
28442839
SELECT [o].[Nickname], [o].[SquadId], [o].[HasSoulPatch]
28452840
FROM [Officers] AS [o]
28462841
) AS [u] ON [t].[GearNickName] = [u].[Nickname] AND [t].[GearSquadId] = [u].[SquadId]
2847-
WHERE CASE
2848-
WHEN [u].[HasSoulPatch] = CAST(0 AS bit) THEN CAST(0 AS bit)
2849-
ELSE [u].[HasSoulPatch]
2850-
END = CAST(0 AS bit)
2842+
WHERE [u].[HasSoulPatch] = CAST(0 AS bit)
28512843
""");
28522844
}
28532845

@@ -4143,9 +4135,7 @@ public override async Task Select_null_conditional_with_inheritance(bool async)
41434135

41444136
AssertSql(
41454137
"""
4146-
SELECT CASE
4147-
WHEN [l].[CommanderName] IS NOT NULL THEN [l].[CommanderName]
4148-
END
4138+
SELECT [l].[CommanderName]
41494139
FROM [LocustHordes] AS [l]
41504140
""");
41514141
}

test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPTGearsOfWarQuerySqlServerTest.cs

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,9 +1053,7 @@ public override async Task Select_null_propagation_works_for_multiple_navigation
10531053

10541054
AssertSql(
10551055
"""
1056-
SELECT CASE
1057-
WHEN [c].[Name] IS NOT NULL THEN [c].[Name]
1058-
END
1056+
SELECT [c].[Name]
10591057
FROM [Tags] AS [t]
10601058
LEFT JOIN (
10611059
SELECT [g].[Nickname], [g].[SquadId]
@@ -2434,10 +2432,7 @@ LEFT JOIN (
24342432
SELECT [g].[Nickname], [g].[SquadId], [g].[HasSoulPatch]
24352433
FROM [Gears] AS [g]
24362434
) AS [s] ON [t].[GearNickName] = [s].[Nickname] AND [t].[GearSquadId] = [s].[SquadId]
2437-
WHERE CASE
2438-
WHEN [s].[HasSoulPatch] = CAST(1 AS bit) THEN CAST(1 AS bit)
2439-
ELSE [s].[HasSoulPatch]
2440-
END = CAST(0 AS bit)
2435+
WHERE [s].[HasSoulPatch] = CAST(0 AS bit)
24412436
""");
24422437
}
24432438

@@ -2453,10 +2448,7 @@ LEFT JOIN (
24532448
SELECT [g].[Nickname], [g].[SquadId], [g].[HasSoulPatch]
24542449
FROM [Gears] AS [g]
24552450
) AS [s] ON [t].[GearNickName] = [s].[Nickname] AND [t].[GearSquadId] = [s].[SquadId]
2456-
WHERE CASE
2457-
WHEN [s].[HasSoulPatch] = CAST(0 AS bit) THEN CAST(0 AS bit)
2458-
ELSE [s].[HasSoulPatch]
2459-
END = CAST(0 AS bit)
2451+
WHERE [s].[HasSoulPatch] = CAST(0 AS bit)
24602452
""");
24612453
}
24622454

@@ -3572,9 +3564,7 @@ public override async Task Select_null_conditional_with_inheritance(bool async)
35723564

35733565
AssertSql(
35743566
"""
3575-
SELECT CASE
3576-
WHEN [l].[CommanderName] IS NOT NULL THEN [l].[CommanderName]
3577-
END
3567+
SELECT [l].[CommanderName]
35783568
FROM [Factions] AS [f]
35793569
LEFT JOIN [LocustHordes] AS [l] ON [f].[Id] = [l].[Id]
35803570
WHERE [l].[Id] IS NOT NULL

0 commit comments

Comments
 (0)