Skip to content

Commit 03fd843

Browse files
committed
Translate to NULLIF
Closes #31682
1 parent c53bbac commit 03fd843

File tree

15 files changed

+414
-73
lines changed

15 files changed

+414
-73
lines changed

EFCore.sln.DotSettings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,8 @@ The .NET Foundation licenses this file to you under the MIT license.
356356
<s:Boolean x:Key="/Default/UserDictionary/Words/=subquery/@EntryIndexedValue">True</s:Boolean>
357357
<s:Boolean x:Key="/Default/UserDictionary/Words/=subquery_0027s/@EntryIndexedValue">True</s:Boolean>
358358
<s:Boolean x:Key="/Default/UserDictionary/Words/=transactionality/@EntryIndexedValue">True</s:Boolean>
359+
<s:Boolean x:Key="/Default/UserDictionary/Words/=uncoalesce/@EntryIndexedValue">True</s:Boolean>
360+
<s:Boolean x:Key="/Default/UserDictionary/Words/=uncoalescing/@EntryIndexedValue">True</s:Boolean>
359361
<s:Boolean x:Key="/Default/UserDictionary/Words/=unconfigured/@EntryIndexedValue">True</s:Boolean>
360362
<s:Boolean x:Key="/Default/UserDictionary/Words/=unignore/@EntryIndexedValue">True</s:Boolean>
361363
<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
@@ -685,10 +685,37 @@ public virtual SqlExpression Condition(SqlExpression test, SqlExpression ifTrue,
685685
{
686686
var typeMapping = ExpressionExtensions.InferTypeMapping(ifTrue, ifFalse);
687687

688-
return new SqlConditionalExpression(
689-
ApplyTypeMapping(test, _boolTypeMapping),
690-
ApplyTypeMapping(ifTrue, typeMapping),
691-
ApplyTypeMapping(ifFalse, typeMapping));
688+
test = ApplyTypeMapping(test, _boolTypeMapping);
689+
ifTrue = ApplyTypeMapping(ifTrue, typeMapping);
690+
ifFalse = ApplyTypeMapping(ifFalse, typeMapping);
691+
692+
// Simplify:
693+
// a == b ? b : a -> a
694+
// a != b ? a : b -> a
695+
if (test is SqlBinaryExpression
696+
{
697+
OperatorType: ExpressionType.Equal or ExpressionType.NotEqual,
698+
Left: var left,
699+
Right: var right
700+
} binary)
701+
{
702+
// Reverse ifEqual/ifNotEqual for ExpressionType.NotEqual for easier reasoning below
703+
var (ifEqual, ifNotEqual) = binary.OperatorType is ExpressionType.Equal ? (ifTrue, ifFalse) : (ifFalse, ifTrue);
704+
705+
// a == b ? b : a -> a
706+
if (left.Equals(ifNotEqual) && right.Equals(ifEqual))
707+
{
708+
return left;
709+
}
710+
711+
// b == a ? b : a -> a
712+
if (right.Equals(ifNotEqual) && left.Equals(ifEqual))
713+
{
714+
return right;
715+
}
716+
}
717+
718+
return new SqlConditionalExpression(test, ifTrue, ifFalse);
692719
}
693720

694721
/// <summary>

src/EFCore.Relational/Query/SqlExpressionFactory.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,58 @@ public virtual SqlExpression Case(
825825
elseResult = lastCase.ElseResult;
826826
}
827827

828+
// Simplify:
829+
// a == b ? b : a -> a
830+
// a != b ? a : b -> a
831+
// And lift:
832+
// a == b ? null : a -> NULLIF(a, b)
833+
// a != b ? a : null -> NULLIF(a, b)
834+
if (operand is null
835+
&& typeMappedWhenClauses is
836+
[
837+
{
838+
Test: SqlBinaryExpression
839+
{
840+
OperatorType: ExpressionType.Equal or ExpressionType.NotEqual,
841+
Left: var left,
842+
Right: var right
843+
} binary,
844+
Result: var result
845+
}
846+
])
847+
{
848+
// Reverse ifEqual/ifNotEqual for ExpressionType.NotEqual for easier reasoning below
849+
var (ifEqual, ifNotEqual) = binary.OperatorType is ExpressionType.Equal
850+
? (result, elseResult ?? Constant(null, result.Type, result.TypeMapping))
851+
: (elseResult ?? Constant(null, result.Type, result.TypeMapping), result);
852+
853+
if (left.Equals(ifNotEqual))
854+
{
855+
switch (ifEqual)
856+
{
857+
// a == b ? b : a -> a
858+
case var _ when ifEqual.Equals(right):
859+
return left;
860+
// a == b ? null : a -> NULLIF(a, b)
861+
case SqlConstantExpression { Value: null }:
862+
return Function("NULLIF", [left, right], nullable: true, [false, false], left.Type, left.TypeMapping);
863+
}
864+
}
865+
866+
if (right.Equals(ifNotEqual))
867+
{
868+
switch (ifEqual)
869+
{
870+
// b == a ? b : a -> a
871+
case var _ when ifEqual.Equals(left):
872+
return right;
873+
// b == a ? null : a -> NULLIF(a, b)
874+
case SqlConstantExpression { Value: null }:
875+
return Function("NULLIF", [right, left], nullable: true, [false, false], right.Type, right.TypeMapping);
876+
}
877+
}
878+
}
879+
828880
return existingExpression is CaseExpression expr
829881
&& operand == expr.Operand
830882
&& typeMappedWhenClauses.SequenceEqual(expr.WhenClauses)

test/EFCore.Cosmos.FunctionalTests/Query/Translations/OperatorTranslationsCosmosTest.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,94 @@ public OperatorTranslationsCosmosTest(BasicTypesQueryCosmosFixture fixture, ITes
1414
Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
1515
}
1616

17+
#region Conditional
18+
19+
public override Task Conditional_simplifiable_equality(bool async)
20+
=> Fixture.NoSyncTest(
21+
async, async a =>
22+
{
23+
await base.Conditional_simplifiable_equality(a);
24+
25+
AssertSql(
26+
"""
27+
SELECT VALUE c
28+
FROM root c
29+
WHERE (c["Int"] > 1)
30+
""");
31+
});
32+
33+
public override Task Conditional_simplifiable_inequality(bool async)
34+
=> Fixture.NoSyncTest(
35+
async, async a =>
36+
{
37+
await base.Conditional_simplifiable_inequality(a);
38+
39+
AssertSql(
40+
"""
41+
SELECT VALUE c
42+
FROM root c
43+
WHERE (c["Int"] > 1)
44+
""");
45+
});
46+
47+
public override Task Conditional_uncoalesce_with_equality_left(bool async)
48+
=> Fixture.NoSyncTest(
49+
async, async a =>
50+
{
51+
await base.Conditional_uncoalesce_with_equality_left(a);
52+
53+
AssertSql(
54+
"""
55+
SELECT VALUE c
56+
FROM root c
57+
WHERE (((c["Int"] = 9) ? null : c["Int"]) > 1)
58+
""");
59+
});
60+
61+
public override Task Conditional_uncoalesce_with_equality_right(bool async)
62+
=> Fixture.NoSyncTest(
63+
async, async a =>
64+
{
65+
await base.Conditional_uncoalesce_with_equality_right(a);
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 Task Conditional_uncoalesce_with_unequality_left(bool async)
76+
=> Fixture.NoSyncTest(
77+
async, async a =>
78+
{
79+
await base.Conditional_uncoalesce_with_unequality_left(a);
80+
81+
AssertSql(
82+
"""
83+
SELECT VALUE c
84+
FROM root c
85+
WHERE (((c["Int"] != 9) ? c["Int"] : null) > 1)
86+
""");
87+
});
88+
89+
public override Task Conditional_uncoalesce_with_inequality_right(bool async)
90+
=> Fixture.NoSyncTest(
91+
async, async a =>
92+
{
93+
await base.Conditional_uncoalesce_with_inequality_right(a);
94+
95+
AssertSql(
96+
"""
97+
SELECT VALUE c
98+
FROM root c
99+
WHERE (((9 != c["Int"]) ? c["Int"] : null) > 1)
100+
""");
101+
});
102+
103+
#endregion Conditional
104+
17105
#region Bitwise
18106

19107
public override Task Bitwise_or(bool async)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ public virtual Task Where_equal_with_conditional(bool async)
753753
ss => ss.Set<NullSemanticsEntity1>().Where(
754754
e => (e.NullableStringA == e.NullableStringB
755755
? e.NullableStringA
756-
: e.NullableStringB)
756+
: e.NullableStringC)
757757
== e.NullableStringC).Select(e => e.Id));
758758

759759
[ConditionalTheory]
@@ -765,7 +765,7 @@ public virtual Task Where_not_equal_with_conditional(bool async)
765765
e => e.NullableStringC
766766
!= (e.NullableStringA == e.NullableStringB
767767
? e.NullableStringA
768-
: e.NullableStringB)).Select(e => e.Id));
768+
: e.NullableStringC)).Select(e => e.Id));
769769

770770
[ConditionalTheory]
771771
[MemberData(nameof(IsAsyncData))]

test/EFCore.Specification.Tests/Query/Translations/OperatorTranslationsTestBase.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,58 @@ namespace Microsoft.EntityFrameworkCore.Query.Translations;
88
public abstract class OperatorTranslationsTestBase<TFixture>(TFixture fixture) : QueryTestBase<TFixture>(fixture)
99
where TFixture : BasicTypesQueryFixtureBase, new()
1010
{
11+
// See also operators precedence tests in OperatorsQueryTestBase
12+
13+
#region Conditional
14+
15+
[ConditionalTheory]
16+
[MemberData(nameof(IsAsyncData))]
17+
public virtual Task Conditional_simplifiable_equality(bool async)
18+
=> AssertQuery(
19+
async,
20+
// ReSharper disable once MergeConditionalExpression
21+
cs => cs.Set<NullableBasicTypesEntity>().Where(x => (x.Int == 9 ? 9 : x.Int) > 1));
22+
23+
[ConditionalTheory]
24+
[MemberData(nameof(IsAsyncData))]
25+
public virtual Task Conditional_simplifiable_inequality(bool async)
26+
=> AssertQuery(
27+
async,
28+
// ReSharper disable once MergeConditionalExpression
29+
cs => cs.Set<NullableBasicTypesEntity>().Where(x => (x.Int != 8 ? x.Int : 8) > 1));
30+
31+
// In relational providers, x == a ? null : x ("un-coalescing conditional") is translated to SQL NULLIF
32+
33+
[ConditionalTheory]
34+
[MemberData(nameof(IsAsyncData))]
35+
public virtual Task Conditional_uncoalesce_with_equality_left(bool async)
36+
=> AssertQuery(
37+
async,
38+
cs => cs.Set<BasicTypesEntity>().Where(x => (x.Int == 9 ? null : x.Int) > 1));
39+
40+
[ConditionalTheory]
41+
[MemberData(nameof(IsAsyncData))]
42+
public virtual Task Conditional_uncoalesce_with_equality_right(bool async)
43+
=> AssertQuery(
44+
async,
45+
cs => cs.Set<BasicTypesEntity>().Where(x => (9 == x.Int ? null : x.Int) > 1));
46+
47+
[ConditionalTheory]
48+
[MemberData(nameof(IsAsyncData))]
49+
public virtual Task Conditional_uncoalesce_with_unequality_left(bool async)
50+
=> AssertQuery(
51+
async,
52+
cs => cs.Set<BasicTypesEntity>().Where(x => (x.Int != 9 ? x.Int : null) > 1));
53+
54+
[ConditionalTheory]
55+
[MemberData(nameof(IsAsyncData))]
56+
public virtual Task Conditional_uncoalesce_with_inequality_right(bool async)
57+
=> AssertQuery(
58+
async,
59+
cs => cs.Set<BasicTypesEntity>().Where(x => (9 != x.Int ? x.Int : null) > 1));
60+
61+
#endregion Conditional
62+
1163
#region Bitwise
1264
#pragma warning disable CS0675 // Bitwise-or operator used on a sign-extended operand
1365

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

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

856856
AssertSql(
857857
"""
858-
SELECT CASE
859-
WHEN [c].[Name] IS NOT NULL THEN [c].[Name]
860-
END
858+
SELECT [c].[Name]
861859
FROM [Tags] AS [t]
862860
LEFT JOIN [Gears] AS [g] ON [t].[GearNickName] = [g].[Nickname] AND [t].[GearSquadId] = [g].[SquadId]
863861
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))
@@ -1981,10 +1979,7 @@ public override async Task Optional_navigation_type_compensation_works_with_pred
19811979
SELECT [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note]
19821980
FROM [Tags] AS [t]
19831981
LEFT JOIN [Gears] AS [g] ON [t].[GearNickName] = [g].[Nickname] AND [t].[GearSquadId] = [g].[SquadId]
1984-
WHERE CASE
1985-
WHEN [g].[HasSoulPatch] = CAST(1 AS bit) THEN CAST(1 AS bit)
1986-
ELSE [g].[HasSoulPatch]
1987-
END = CAST(0 AS bit)
1982+
WHERE [g].[HasSoulPatch] = CAST(0 AS bit)
19881983
""");
19891984
}
19901985

@@ -1997,10 +1992,7 @@ public override async Task Optional_navigation_type_compensation_works_with_pred
19971992
SELECT [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note]
19981993
FROM [Tags] AS [t]
19991994
LEFT JOIN [Gears] AS [g] ON [t].[GearNickName] = [g].[Nickname] AND [t].[GearSquadId] = [g].[SquadId]
2000-
WHERE CASE
2001-
WHEN [g].[HasSoulPatch] = CAST(0 AS bit) THEN CAST(0 AS bit)
2002-
ELSE [g].[HasSoulPatch]
2003-
END = CAST(0 AS bit)
1995+
WHERE [g].[HasSoulPatch] = CAST(0 AS bit)
20041996
""");
20051997
}
20061998

@@ -3057,9 +3049,7 @@ public override async Task Select_null_conditional_with_inheritance(bool async)
30573049

30583050
AssertSql(
30593051
"""
3060-
SELECT CASE
3061-
WHEN [f].[CommanderName] IS NOT NULL THEN [f].[CommanderName]
3062-
END
3052+
SELECT [f].[CommanderName]
30633053
FROM [Factions] AS [f]
30643054
""");
30653055
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ public NorthwindFunctionsQuerySqlServer160Test(Fixture160 fixture, ITestOutputHe
2121
public virtual void Check_all_tests_overridden()
2222
=> TestHelpers.AssertAllMethodsOverridden(GetType());
2323

24+
public override async Task Client_evaluation_of_uncorrelated_method_call(bool async)
25+
{
26+
await base.Client_evaluation_of_uncorrelated_method_call(async);
27+
28+
AssertSql(
29+
"""
30+
SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice]
31+
FROM [Order Details] AS [o]
32+
WHERE [o].[UnitPrice] < 7.0 AND 10 < [o].[ProductID]
33+
""");
34+
}
35+
2436
public override async Task Sum_over_round_works_correctly_in_projection(bool async)
2537
{
2638
await base.Sum_over_round_works_correctly_in_projection(async);

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2207,10 +2207,10 @@ SELECT [e].[Id]
22072207
FROM [Entities1] AS [e]
22082208
WHERE CASE
22092209
WHEN [e].[NullableStringA] = [e].[NullableStringB] OR ([e].[NullableStringA] IS NULL AND [e].[NullableStringB] IS NULL) THEN [e].[NullableStringA]
2210-
ELSE [e].[NullableStringB]
2210+
ELSE [e].[NullableStringC]
22112211
END = [e].[NullableStringC] OR (CASE
22122212
WHEN [e].[NullableStringA] = [e].[NullableStringB] OR ([e].[NullableStringA] IS NULL AND [e].[NullableStringB] IS NULL) THEN [e].[NullableStringA]
2213-
ELSE [e].[NullableStringB]
2213+
ELSE [e].[NullableStringC]
22142214
END IS NULL AND [e].[NullableStringC] IS NULL)
22152215
""");
22162216
}
@@ -2225,13 +2225,13 @@ SELECT [e].[Id]
22252225
FROM [Entities1] AS [e]
22262226
WHERE ([e].[NullableStringC] <> CASE
22272227
WHEN [e].[NullableStringA] = [e].[NullableStringB] OR ([e].[NullableStringA] IS NULL AND [e].[NullableStringB] IS NULL) THEN [e].[NullableStringA]
2228-
ELSE [e].[NullableStringB]
2228+
ELSE [e].[NullableStringC]
22292229
END OR [e].[NullableStringC] IS NULL OR CASE
22302230
WHEN [e].[NullableStringA] = [e].[NullableStringB] OR ([e].[NullableStringA] IS NULL AND [e].[NullableStringB] IS NULL) THEN [e].[NullableStringA]
2231-
ELSE [e].[NullableStringB]
2231+
ELSE [e].[NullableStringC]
22322232
END IS NULL) AND ([e].[NullableStringC] IS NOT NULL OR CASE
22332233
WHEN [e].[NullableStringA] = [e].[NullableStringB] OR ([e].[NullableStringA] IS NULL AND [e].[NullableStringB] IS NULL) THEN [e].[NullableStringA]
2234-
ELSE [e].[NullableStringB]
2234+
ELSE [e].[NullableStringC]
22352235
END IS NOT NULL)
22362236
""");
22372237
}

0 commit comments

Comments
 (0)