Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions EFCore.slnx.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,8 @@ The .NET Foundation licenses this file to you under the MIT license.
<s:Boolean x:Key="/Default/UserDictionary/Words/=subquery/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=subquery_0027s/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=transactionality/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=uncoalesce/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=uncoalescing/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unconfigured/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unignore/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=fixup/@EntryIndexedValue">True</s:Boolean>
Expand Down
35 changes: 31 additions & 4 deletions src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -707,10 +707,37 @@ public virtual SqlExpression Condition(SqlExpression test, SqlExpression ifTrue,
{
var typeMapping = ExpressionExtensions.InferTypeMapping(ifTrue, ifFalse);

return new SqlConditionalExpression(
ApplyTypeMapping(test, _boolTypeMapping),
ApplyTypeMapping(ifTrue, typeMapping),
ApplyTypeMapping(ifFalse, typeMapping));
test = ApplyTypeMapping(test, _boolTypeMapping);
ifTrue = ApplyTypeMapping(ifTrue, typeMapping);
ifFalse = ApplyTypeMapping(ifFalse, typeMapping);

// Simplify:
// a == b ? b : a -> a
// a != b ? a : b -> a
if (test is SqlBinaryExpression
{
OperatorType: ExpressionType.Equal or ExpressionType.NotEqual,
Left: SqlExpression left,
Right: SqlExpression right
} binary)
{
// Swap ifTrue/ifFalse for NotEqual so we can reason uniformly about ifEqual/ifNotEqual.
var (ifEqual, ifNotEqual) = binary.OperatorType is ExpressionType.Equal ? (ifTrue, ifFalse) : (ifFalse, ifTrue);

// 'left' survives: a == b ? b : a -> a
if (left.Equals(ifNotEqual) && right.Equals(ifEqual))
{
return left;
}

// 'right' survives: a == b ? a : b -> b
if (right.Equals(ifNotEqual) && left.Equals(ifEqual))
{
return right;
}
}

return new SqlConditionalExpression(test, ifTrue, ifFalse);
}

/// <summary>
Expand Down
55 changes: 55 additions & 0 deletions src/EFCore.Relational/Query/SqlExpressionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,61 @@ public virtual SqlExpression Case(
elseResult = lastCase.ElseResult;
}

// Simplify:
// a == b ? b : a -> a
// a != b ? a : b -> a
// And lift:
// a == b ? null : a -> NULLIF(a, b)
// a != b ? a : null -> NULLIF(a, b)
if (operand is null
&& typeMappedWhenClauses is
[
{
Test: SqlBinaryExpression
{
OperatorType: ExpressionType.Equal or ExpressionType.NotEqual,
Left: var left,
Right: var right
} binary,
Result: var result
}
])
{
// Swap result/elseResult for NotEqual so we can reason uniformly about ifEqual/ifNotEqual.
// A missing ELSE clause is equivalent to ELSE NULL, so normalize to a null constant for matching.
var (ifEqual, ifNotEqual) = binary.OperatorType is ExpressionType.Equal
? (result, elseResult ?? Constant(null, result.Type, result.TypeMapping))
: (elseResult ?? Constant(null, result.Type, result.TypeMapping), result);

// 'left' survives the conditional.
if (left.Equals(ifNotEqual))
{
switch (ifEqual)
{
// a == b ? b : a -> a (also collapses NULLIF(a, NULL) when both are null constants)
case var _ when ifEqual.Equals(right):
return left;
// a == b ? null : a -> NULLIF(a, b)
case SqlConstantExpression { Value: null }:
return Function("NULLIF", [left, right], nullable: true, Statics.TrueFalse, left.Type, left.TypeMapping);
}
}

// 'right' survives the conditional (operand-swapped equivalents of the patterns above).
if (right.Equals(ifNotEqual))
{
switch (ifEqual)
{
// a == b ? a : b -> b
case var _ when ifEqual.Equals(left):
return right;
// a == b ? null : b -> NULLIF(b, a)
case SqlConstantExpression { Value: null }:
return Function("NULLIF", [right, left], nullable: true, Statics.TrueFalse, right.Type, right.TypeMapping);
}
}
}

return existingExpression is CaseExpression expr
&& operand == expr.Operand
&& typeMappedWhenClauses.SequenceEqual(expr.WhenClauses)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,90 @@ FROM root c
""");
}

public override async Task Conditional_simplifiable_equality()
{
await base.Conditional_simplifiable_equality();

AssertSql(
"""
SELECT VALUE c
FROM root c
WHERE (c["Int"] > 1)
""");
}

public override async Task Conditional_simplifiable_inequality()
{
await base.Conditional_simplifiable_inequality();

AssertSql(
"""
SELECT VALUE c
FROM root c
WHERE (c["Int"] > 1)
""");
}

public override async Task Conditional_uncoalesce_with_equality_left()
{
await base.Conditional_uncoalesce_with_equality_left();

AssertSql(
"""
SELECT VALUE c
FROM root c
WHERE (((c["Int"] = 9) ? null : c["Int"]) > 1)
""");
}

public override async Task Conditional_uncoalesce_with_equality_right()
{
await base.Conditional_uncoalesce_with_equality_right();

AssertSql(
"""
SELECT VALUE c
FROM root c
WHERE (((9 = c["Int"]) ? null : c["Int"]) > 1)
""");
}

public override async Task Conditional_uncoalesce_with_inequality_left()
{
await base.Conditional_uncoalesce_with_inequality_left();

AssertSql(
"""
SELECT VALUE c
FROM root c
WHERE (((c["Int"] != 9) ? c["Int"] : null) > 1)
""");
}

public override async Task Conditional_uncoalesce_with_inequality_right()
{
await base.Conditional_uncoalesce_with_inequality_right();

AssertSql(
"""
SELECT VALUE c
FROM root c
WHERE (((9 != c["Int"]) ? c["Int"] : null) > 1)
""");
}

public override async Task Conditional_uncoalesce_with_string()
{
await base.Conditional_uncoalesce_with_string();

AssertSql(
"""
SELECT VALUE c
FROM root c
WHERE (((c["String"] = "Seattle") ? null : c["String"]) = "London")
""");
}

public override async Task Coalesce()
{
await base.Coalesce();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,7 @@ public virtual Task Where_equal_with_conditional(bool async)
async,
ss => ss.Set<NullSemanticsEntity1>().Where(e => (e.NullableStringA == e.NullableStringB
? e.NullableStringA
: e.NullableStringB)
: e.NullableStringC)
== e.NullableStringC).Select(e => e.Id));

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

[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task Where_equal_with_conditional_non_nullable(bool async)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,38 @@ public abstract class MiscellaneousOperatorTranslationsTestBase<TFixture>(TFixtu
public virtual async Task Conditional()
=> await AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(b => (b.Int == 8 ? b.String : "Foo") == "Seattle"));

[Fact]
public virtual async Task Conditional_simplifiable_equality()
// ReSharper disable once MergeConditionalExpression
=> await AssertQuery(ss => ss.Set<NullableBasicTypesEntity>().Where(x => (x.Int == 9 ? 9 : x.Int) > 1));

[Fact]
public virtual async Task Conditional_simplifiable_inequality()
// ReSharper disable once MergeConditionalExpression
=> await AssertQuery(ss => ss.Set<NullableBasicTypesEntity>().Where(x => (x.Int != 8 ? x.Int : 8) > 1));

// In relational providers, x == a ? null : x ("un-coalescing conditional") is translated to SQL NULLIF

[Fact]
public virtual async Task Conditional_uncoalesce_with_equality_left()
=> await AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(x => (x.Int == 9 ? null : x.Int) > 1));

[Fact]
public virtual async Task Conditional_uncoalesce_with_equality_right()
=> await AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(x => (9 == x.Int ? null : x.Int) > 1));

[Fact]
public virtual async Task Conditional_uncoalesce_with_inequality_left()
=> await AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(x => (x.Int != 9 ? x.Int : null) > 1));

[Fact]
public virtual async Task Conditional_uncoalesce_with_inequality_right()
=> await AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(x => (9 != x.Int ? x.Int : null) > 1));

[Fact]
public virtual async Task Conditional_uncoalesce_with_string()
=> await AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(x => (x.String == "Seattle" ? null : x.String) == "London"));

[Fact]
public virtual async Task Coalesce()
=> await AssertQuery(ss => ss.Set<NullableBasicTypesEntity>().Where(b => (b.String ?? "Unknown") == "Seattle"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -873,9 +873,7 @@ public override async Task Select_null_propagation_works_for_multiple_navigation

AssertSql(
"""
SELECT CASE
WHEN [c].[Name] IS NOT NULL THEN [c].[Name]
END
SELECT [c].[Name]
FROM [Tags] AS [t]
LEFT JOIN [Gears] AS [g] ON [t].[GearNickName] = [g].[Nickname] AND [t].[GearSquadId] = [g].[SquadId]
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))
Expand Down Expand Up @@ -2048,10 +2046,7 @@ public override async Task Optional_navigation_type_compensation_works_with_pred
SELECT [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note]
FROM [Tags] AS [t]
LEFT JOIN [Gears] AS [g] ON [t].[GearNickName] = [g].[Nickname] AND [t].[GearSquadId] = [g].[SquadId]
WHERE CASE
WHEN [g].[HasSoulPatch] = CAST(1 AS bit) THEN CAST(1 AS bit)
ELSE [g].[HasSoulPatch]
END = CAST(0 AS bit)
WHERE [g].[HasSoulPatch] = CAST(0 AS bit)
""");
}

Expand All @@ -2064,10 +2059,7 @@ public override async Task Optional_navigation_type_compensation_works_with_pred
SELECT [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note]
FROM [Tags] AS [t]
LEFT JOIN [Gears] AS [g] ON [t].[GearNickName] = [g].[Nickname] AND [t].[GearSquadId] = [g].[SquadId]
WHERE CASE
WHEN [g].[HasSoulPatch] = CAST(0 AS bit) THEN CAST(0 AS bit)
ELSE [g].[HasSoulPatch]
END = CAST(0 AS bit)
WHERE [g].[HasSoulPatch] = CAST(0 AS bit)
""");
}

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

AssertSql(
"""
SELECT CASE
WHEN [f].[CommanderName] IS NOT NULL THEN [f].[CommanderName]
END
SELECT [f].[CommanderName]
FROM [Factions] AS [f]
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1175,9 +1175,7 @@ public override async Task Select_null_propagation_works_for_multiple_navigation

AssertSql(
"""
SELECT CASE
WHEN [c].[Name] IS NOT NULL THEN [c].[Name]
END
SELECT [c].[Name]
FROM [Tags] AS [t]
LEFT JOIN (
SELECT [g].[Nickname], [g].[SquadId]
Expand Down Expand Up @@ -2822,10 +2820,7 @@ UNION ALL
SELECT [o].[Nickname], [o].[SquadId], [o].[HasSoulPatch]
FROM [Officers] AS [o]
) AS [u] ON [t].[GearNickName] = [u].[Nickname] AND [t].[GearSquadId] = [u].[SquadId]
WHERE CASE
WHEN [u].[HasSoulPatch] = CAST(1 AS bit) THEN CAST(1 AS bit)
ELSE [u].[HasSoulPatch]
END = CAST(0 AS bit)
WHERE [u].[HasSoulPatch] = CAST(0 AS bit)
""");
}

Expand All @@ -2844,10 +2839,7 @@ UNION ALL
SELECT [o].[Nickname], [o].[SquadId], [o].[HasSoulPatch]
FROM [Officers] AS [o]
) AS [u] ON [t].[GearNickName] = [u].[Nickname] AND [t].[GearSquadId] = [u].[SquadId]
WHERE CASE
WHEN [u].[HasSoulPatch] = CAST(0 AS bit) THEN CAST(0 AS bit)
ELSE [u].[HasSoulPatch]
END = CAST(0 AS bit)
WHERE [u].[HasSoulPatch] = CAST(0 AS bit)
""");
}

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

AssertSql(
"""
SELECT CASE
WHEN [l].[CommanderName] IS NOT NULL THEN [l].[CommanderName]
END
SELECT [l].[CommanderName]
FROM [LocustHordes] AS [l]
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1053,9 +1053,7 @@ public override async Task Select_null_propagation_works_for_multiple_navigation

AssertSql(
"""
SELECT CASE
WHEN [c].[Name] IS NOT NULL THEN [c].[Name]
END
SELECT [c].[Name]
FROM [Tags] AS [t]
LEFT JOIN (
SELECT [g].[Nickname], [g].[SquadId]
Expand Down Expand Up @@ -2434,10 +2432,7 @@ LEFT JOIN (
SELECT [g].[Nickname], [g].[SquadId], [g].[HasSoulPatch]
FROM [Gears] AS [g]
) AS [s] ON [t].[GearNickName] = [s].[Nickname] AND [t].[GearSquadId] = [s].[SquadId]
WHERE CASE
WHEN [s].[HasSoulPatch] = CAST(1 AS bit) THEN CAST(1 AS bit)
ELSE [s].[HasSoulPatch]
END = CAST(0 AS bit)
WHERE [s].[HasSoulPatch] = CAST(0 AS bit)
""");
}

Expand All @@ -2453,10 +2448,7 @@ LEFT JOIN (
SELECT [g].[Nickname], [g].[SquadId], [g].[HasSoulPatch]
FROM [Gears] AS [g]
) AS [s] ON [t].[GearNickName] = [s].[Nickname] AND [t].[GearSquadId] = [s].[SquadId]
WHERE CASE
WHEN [s].[HasSoulPatch] = CAST(0 AS bit) THEN CAST(0 AS bit)
ELSE [s].[HasSoulPatch]
END = CAST(0 AS bit)
WHERE [s].[HasSoulPatch] = CAST(0 AS bit)
""");
}

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

AssertSql(
"""
SELECT CASE
WHEN [l].[CommanderName] IS NOT NULL THEN [l].[CommanderName]
END
SELECT [l].[CommanderName]
FROM [Factions] AS [f]
LEFT JOIN [LocustHordes] AS [l] ON [f].[Id] = [l].[Id]
WHERE [l].[Id] IS NOT NULL
Expand Down
Loading
Loading