Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved nested SelectMany with DefaultIfEmpty #34110

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,7 @@ private sealed class CorrelationFindingExpressionVisitor : ExpressionVisitor
private ParameterExpression? _outerParameter;
private bool _correlated;
private bool _defaultIfEmpty;
private bool _topLevelSelectMany;

public (LambdaExpression, bool, bool) IsCorrelated(LambdaExpression lambdaExpression)
{
Expand All @@ -1146,6 +1147,7 @@ private sealed class CorrelationFindingExpressionVisitor : ExpressionVisitor

_correlated = false;
_defaultIfEmpty = false;
_topLevelSelectMany = true;
_outerParameter = lambdaExpression.Parameters[0];

var result = Visit(lambdaExpression.Body);
Expand All @@ -1165,8 +1167,18 @@ protected override Expression VisitParameter(ParameterExpression parameterExpres

protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
if (methodCallExpression.Method.IsGenericMethod
&& methodCallExpression.Method.GetGenericMethodDefinition() == QueryableMethods.DefaultIfEmptyWithoutArgument)
if (methodCallExpression.Method.Name == nameof(Queryable.SelectMany))
{
var levelBuffer = _topLevelSelectMany;
_topLevelSelectMany = false;
var result = base.VisitMethodCall(methodCallExpression);
_topLevelSelectMany = levelBuffer;
return result;
}

if (_topLevelSelectMany
&& methodCallExpression.Method.IsGenericMethod
&& methodCallExpression.Method.GetGenericMethodDefinition() == QueryableMethods.DefaultIfEmptyWithoutArgument)
{
_defaultIfEmpty = true;
return Visit(methodCallExpression.Arguments[0]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2454,4 +2454,32 @@ public virtual Task Set_operation_in_pending_collection(bool async)
}).Take(5),
assertOrder: true,
elementAsserter: (e, a) => AssertCollection(e.OrderIds, a.OrderIds, elementSorter: ee => ee));

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Nested_SelectMany_with_DefaultIfEmpty(bool async) => AssertQuery(
async,
ss => ss.Set<Customer>().SelectMany(
ownerCustomer => ss.Set<Customer>()
.Where(neighbor => ownerCustomer.City == neighbor.City)
.Select(x => new { x.City, x.Address }).SelectMany(
customer => ss.Set<Customer>()
.Where(branch => branch.Address == customer.Address)
.DefaultIfEmpty()))
.Select(x => new { x.City, x.Address }));


[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Nested_SelectMany_with_anonymous_type_and_DefaultIfEmpty(bool async) => AssertQuery(
async,
ss => ss.Set<Customer>().SelectMany(
ownerCustomer => ss.Set<Customer>()
.Where(neighbor => ownerCustomer.City == neighbor.City)
.Select(x => new { x.City, x.Address }).SelectMany(
customer => ss.Set<Customer>()
.Where(branch => branch.Address == customer.Address)
.DefaultIfEmpty(), (left, right) => new { left, right }),
(left, right) => new { left, right })
.Select(x => new { x.left.City, x.left.Address }));
}
Original file line number Diff line number Diff line change
Expand Up @@ -3853,24 +3853,30 @@ FROM [LevelOne] AS [l]
public override async Task Nested_SelectMany_correlated_with_join_table_correctly_translated_to_apply(bool async)
{
// DefaultIfEmpty on child collection. Issue #19095.
await Assert.ThrowsAsync<EqualException>(
await Assert.ThrowsAsync<TrueException>(
async () => await base.Nested_SelectMany_correlated_with_join_table_correctly_translated_to_apply(async));

AssertSql(
"""
SELECT [s0].[l1Name], [s0].[l2Name], [s0].[l3Name]
FROM [LevelOne] AS [l]
OUTER APPLY (
CROSS APPLY (
SELECT [s].[l1Name], [s].[l2Name], [s].[l3Name]
FROM [LevelTwo] AS [l0]
LEFT JOIN [LevelThree] AS [l1] ON [l0].[Id] = [l1].[Id]
CROSS APPLY (
SELECT [l].[Name] AS [l1Name], [l1].[Name] AS [l2Name], [l3].[Name] AS [l3Name]
FROM [LevelFour] AS [l2]
LEFT JOIN [LevelThree] AS [l3] ON [l2].[OneToOne_Optional_PK_Inverse4Id] = [l3].[Id]
WHERE [l1].[Id] IS NOT NULL AND [l1].[Id] = [l2].[OneToMany_Optional_Inverse4Id]
FROM (
SELECT 1 AS empty
) AS [e]
LEFT JOIN (
SELECT [l0].[Id]
FROM [LevelTwo] AS [l0]
WHERE [l].[Id] = [l0].[OneToMany_Optional_Inverse2Id]
) AS [l1] ON 1 = 1
LEFT JOIN [LevelThree] AS [l2] ON [l1].[Id] = [l2].[Id]
OUTER APPLY (
SELECT [l].[Name] AS [l1Name], [l2].[Name] AS [l2Name], [l4].[Name] AS [l3Name]
FROM [LevelFour] AS [l3]
LEFT JOIN [LevelThree] AS [l4] ON [l3].[OneToOne_Optional_PK_Inverse4Id] = [l4].[Id]
WHERE [l2].[Id] IS NOT NULL AND [l2].[Id] = [l3].[OneToMany_Optional_Inverse4Id]
) AS [s]
WHERE [l].[Id] = [l0].[OneToMany_Optional_Inverse2Id]
) AS [s0]
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,44 +440,50 @@ END IS NULL
public override async Task Nested_SelectMany_correlated_with_join_table_correctly_translated_to_apply(bool async)
{
// DefaultIfEmpty on child collection. Issue #19095.
await Assert.ThrowsAsync<EqualException>(
await Assert.ThrowsAsync<TrueException>(
async () => await base.Nested_SelectMany_correlated_with_join_table_correctly_translated_to_apply(async));

AssertSql(
"""
SELECT [s0].[l1Name], [s0].[l2Name], [s0].[l3Name]
FROM [Level1] AS [l]
OUTER APPLY (
CROSS APPLY (
SELECT [s].[l1Name], [s].[l2Name], [s].[l3Name]
FROM [Level1] AS [l0]
FROM (
SELECT 1 AS empty
) AS [e]
LEFT JOIN (
SELECT [l1].[Id], [l1].[Level2_Required_Id], [l1].[Level3_Name], [l1].[OneToMany_Required_Inverse3Id]
FROM [Level1] AS [l1]
WHERE [l1].[Level2_Required_Id] IS NOT NULL AND [l1].[OneToMany_Required_Inverse3Id] IS NOT NULL
) AS [l2] ON CASE
WHEN [l0].[OneToOne_Required_PK_Date] IS NOT NULL AND [l0].[Level1_Required_Id] IS NOT NULL AND [l0].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [l0].[Id]
SELECT [l0].[Id], [l0].[OneToOne_Required_PK_Date], [l0].[Level1_Required_Id], [l0].[OneToMany_Required_Inverse2Id]
FROM [Level1] AS [l0]
WHERE [l0].[OneToOne_Required_PK_Date] IS NOT NULL AND [l0].[Level1_Required_Id] IS NOT NULL AND [l0].[OneToMany_Required_Inverse2Id] IS NOT NULL AND [l].[Id] = [l0].[OneToMany_Optional_Inverse2Id]
) AS [l1] ON 1 = 1
LEFT JOIN (
SELECT [l2].[Id], [l2].[Level2_Required_Id], [l2].[Level3_Name], [l2].[OneToMany_Required_Inverse3Id]
FROM [Level1] AS [l2]
WHERE [l2].[Level2_Required_Id] IS NOT NULL AND [l2].[OneToMany_Required_Inverse3Id] IS NOT NULL
) AS [l3] ON CASE
WHEN [l1].[OneToOne_Required_PK_Date] IS NOT NULL AND [l1].[Level1_Required_Id] IS NOT NULL AND [l1].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [l1].[Id]
END = CASE
WHEN [l2].[Level2_Required_Id] IS NOT NULL AND [l2].[OneToMany_Required_Inverse3Id] IS NOT NULL THEN [l2].[Id]
WHEN [l3].[Level2_Required_Id] IS NOT NULL AND [l3].[OneToMany_Required_Inverse3Id] IS NOT NULL THEN [l3].[Id]
END
CROSS APPLY (
SELECT [l].[Name] AS [l1Name], [l2].[Level3_Name] AS [l2Name], [l5].[Level3_Name] AS [l3Name]
FROM [Level1] AS [l3]
OUTER APPLY (
SELECT [l].[Name] AS [l1Name], [l3].[Level3_Name] AS [l2Name], [l6].[Level3_Name] AS [l3Name]
FROM [Level1] AS [l4]
LEFT JOIN (
SELECT [l4].[Id], [l4].[Level2_Required_Id], [l4].[Level3_Name], [l4].[OneToMany_Required_Inverse3Id]
FROM [Level1] AS [l4]
WHERE [l4].[Level2_Required_Id] IS NOT NULL AND [l4].[OneToMany_Required_Inverse3Id] IS NOT NULL
) AS [l5] ON [l3].[OneToOne_Optional_PK_Inverse4Id] = CASE
WHEN [l5].[Level2_Required_Id] IS NOT NULL AND [l5].[OneToMany_Required_Inverse3Id] IS NOT NULL THEN [l5].[Id]
SELECT [l5].[Id], [l5].[Level2_Required_Id], [l5].[Level3_Name], [l5].[OneToMany_Required_Inverse3Id]
FROM [Level1] AS [l5]
WHERE [l5].[Level2_Required_Id] IS NOT NULL AND [l5].[OneToMany_Required_Inverse3Id] IS NOT NULL
) AS [l6] ON [l4].[OneToOne_Optional_PK_Inverse4Id] = CASE
WHEN [l6].[Level2_Required_Id] IS NOT NULL AND [l6].[OneToMany_Required_Inverse3Id] IS NOT NULL THEN [l6].[Id]
END
WHERE [l3].[Level3_Required_Id] IS NOT NULL AND [l3].[OneToMany_Required_Inverse4Id] IS NOT NULL AND CASE
WHEN [l2].[Level2_Required_Id] IS NOT NULL AND [l2].[OneToMany_Required_Inverse3Id] IS NOT NULL THEN [l2].[Id]
WHERE [l4].[Level3_Required_Id] IS NOT NULL AND [l4].[OneToMany_Required_Inverse4Id] IS NOT NULL AND CASE
WHEN [l3].[Level2_Required_Id] IS NOT NULL AND [l3].[OneToMany_Required_Inverse3Id] IS NOT NULL THEN [l3].[Id]
END IS NOT NULL AND (CASE
WHEN [l2].[Level2_Required_Id] IS NOT NULL AND [l2].[OneToMany_Required_Inverse3Id] IS NOT NULL THEN [l2].[Id]
END = [l3].[OneToMany_Optional_Inverse4Id] OR (CASE
WHEN [l2].[Level2_Required_Id] IS NOT NULL AND [l2].[OneToMany_Required_Inverse3Id] IS NOT NULL THEN [l2].[Id]
END IS NULL AND [l3].[OneToMany_Optional_Inverse4Id] IS NULL))
WHEN [l3].[Level2_Required_Id] IS NOT NULL AND [l3].[OneToMany_Required_Inverse3Id] IS NOT NULL THEN [l3].[Id]
END = [l4].[OneToMany_Optional_Inverse4Id] OR (CASE
WHEN [l3].[Level2_Required_Id] IS NOT NULL AND [l3].[OneToMany_Required_Inverse3Id] IS NOT NULL THEN [l3].[Id]
END IS NULL AND [l4].[OneToMany_Optional_Inverse4Id] IS NULL))
) AS [s]
WHERE [l0].[OneToOne_Required_PK_Date] IS NOT NULL AND [l0].[Level1_Required_Id] IS NOT NULL AND [l0].[OneToMany_Required_Inverse2Id] IS NOT NULL AND [l].[Id] = [l0].[OneToMany_Optional_Inverse2Id]
) AS [s0]
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2771,6 +2771,34 @@ ORDER BY [c0].[CustomerID]
""");
}

public override async Task Nested_SelectMany_with_DefaultIfEmpty(bool async)
{
await base.Nested_SelectMany_with_DefaultIfEmpty(async);

AssertSql(
@"SELECT [s].[City], [s].[Address]
FROM [Customers] AS [c]
INNER JOIN (
SELECT [c1].[Address], [c1].[City], [c0].[City] AS [City0]
FROM [Customers] AS [c0]
LEFT JOIN [Customers] AS [c1] ON [c0].[Address] = [c1].[Address]
) AS [s] ON [c].[City] = [s].[City0]");
}

public override async Task Nested_SelectMany_with_anonymous_type_and_DefaultIfEmpty(bool async)
{
await base.Nested_SelectMany_with_anonymous_type_and_DefaultIfEmpty(async);

AssertSql(
@"SELECT [c].[City], [c].[Address]
FROM [Customers] AS [c]
INNER JOIN (
SELECT [c0].[City]
FROM [Customers] AS [c0]
LEFT JOIN [Customers] AS [c1] ON [c0].[Address] = [c1].[Address]
) AS [s] ON [c].[City] = [s].[City]");
}

private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,35 @@ SELECT rtrim(rtrim(strftime('%H:%M:%f', "o"."OrderDate"), '0'), '.')
""");
}


public override async Task Nested_SelectMany_with_DefaultIfEmpty(bool async)
{
await base.Nested_SelectMany_with_DefaultIfEmpty(async);

AssertSql(
@"SELECT ""s"".""City"", ""s"".""Address""
FROM ""Customers"" AS ""c""
INNER JOIN (
SELECT ""c1"".""Address"", ""c1"".""City"", ""c0"".""City"" AS ""City0""
FROM ""Customers"" AS ""c0""
LEFT JOIN ""Customers"" AS ""c1"" ON ""c0"".""Address"" = ""c1"".""Address""
) AS ""s"" ON ""c"".""City"" = ""s"".""City0""");
}

public override async Task Nested_SelectMany_with_anonymous_type_and_DefaultIfEmpty(bool async)
{
await base.Nested_SelectMany_with_anonymous_type_and_DefaultIfEmpty(async);

AssertSql(
@"SELECT ""c"".""City"", ""c"".""Address""
FROM ""Customers"" AS ""c""
INNER JOIN (
SELECT ""c0"".""City""
FROM ""Customers"" AS ""c0""
LEFT JOIN ""Customers"" AS ""c1"" ON ""c0"".""Address"" = ""c1"".""Address""
) AS ""s"" ON ""c"".""City"" = ""s"".""City""");
}

public override async Task
SelectMany_with_collection_being_correlated_subquery_which_references_non_mapped_properties_from_inner_and_outer_entity(
bool async)
Expand Down