Skip to content

Commit

Permalink
Query: Add support for set operations between 2 collection navs (#23218)
Browse files Browse the repository at this point in the history
Resolves #17759
  • Loading branch information
smitpatel committed Nov 9, 2020
1 parent 27ea2d6 commit 0028103
Show file tree
Hide file tree
Showing 4 changed files with 326 additions and 121 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ when QueryableMethods.IsSumWithSelector(method):
when genericMethod == QueryableMethods.Join:
{
var secondArgument = Visit(methodCallExpression.Arguments[1]);
secondArgument = UnwrapCollectionMaterialization(secondArgument);
if (secondArgument is NavigationExpansionExpression innerSource)
{
return ProcessJoin(
Expand All @@ -399,6 +400,7 @@ when QueryableMethods.IsSumWithSelector(method):
when genericMethod == QueryableExtensions.LeftJoinMethodInfo:
{
var secondArgument = Visit(methodCallExpression.Arguments[1]);
secondArgument = UnwrapCollectionMaterialization(secondArgument);
if (secondArgument is NavigationExpansionExpression innerSource)
{
return ProcessLeftJoin(
Expand Down Expand Up @@ -436,6 +438,7 @@ when QueryableMethods.IsSumWithSelector(method):
when genericMethod == QueryableMethods.Union:
{
var secondArgument = Visit(methodCallExpression.Arguments[1]);
secondArgument = UnwrapCollectionMaterialization(secondArgument);
if (secondArgument is NavigationExpansionExpression innerSource)
{
return ProcessSetOperation(source, genericMethod, innerSource);
Expand Down Expand Up @@ -1130,10 +1133,7 @@ private NavigationExpansionExpression ProcessSelectMany(
LambdaExpression? resultSelector)
{
var collectionSelectorBody = ExpandNavigationsForSource(source, RemapLambdaExpression(source, collectionSelector));
if (collectionSelectorBody is MaterializeCollectionNavigationExpression materializeCollectionNavigationExpression)
{
collectionSelectorBody = materializeCollectionNavigationExpression.Subquery;
}
collectionSelectorBody = UnwrapCollectionMaterialization(collectionSelectorBody);

if (collectionSelectorBody is NavigationExpansionExpression collectionSource)
{
Expand Down
122 changes: 48 additions & 74 deletions test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1487,7 +1487,7 @@ public virtual Task Where_subquery_distinct_orderby_firstordefault_boolean_with_
ss => ss.Set<Gear>().Where(g => g.HasSoulPatch && g.Weapons.Distinct().OrderBy(w => w.Id).FirstOrDefault().IsAutomatic));
}

[ConditionalTheory(Skip = "Issue#17759")]
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Where_subquery_union_firstordefault_boolean(bool async)
{
Expand All @@ -1499,13 +1499,36 @@ public virtual Task Where_subquery_union_firstordefault_boolean(bool async)

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_subquery_concat_firstordefault_boolean(bool async)
public virtual Task Where_subquery_join_firstordefault_boolean(bool async)
{
var message = (await Assert.ThrowsAsync<InvalidOperationException>(
() => AssertQuery(
async,
ss => ss.Set<Gear>().Where(
g => g.HasSoulPatch && g.Weapons.Concat(g.Weapons).OrderBy(w => w.Id).FirstOrDefault().IsAutomatic)))).Message;
return AssertQuery(
async,
ss => ss.Set<Gear>().Where(
g => g.HasSoulPatch && g.Weapons.Join(g.Weapons, e => e.Id, e => e.Id, (e1, e2) => e1).OrderBy(w => w.Id).FirstOrDefault().IsAutomatic));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Where_subquery_left_join_firstordefault_boolean(bool async)
{
return AssertQuery(
async,
ss => ss.Set<Gear>().Where(
g => g.HasSoulPatch
&& (from o in g.Weapons
join i in g.Weapons on o.Id equals i.Id into grouping
from i in grouping.DefaultIfEmpty()
select o).OrderBy(w => w.Id).FirstOrDefault().IsAutomatic));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Where_subquery_concat_firstordefault_boolean(bool async)
{
return AssertQuery(
async,
ss => ss.Set<Gear>().Where(
g => g.HasSoulPatch && g.Weapons.Concat(g.Weapons).OrderBy(w => w.Id).FirstOrDefault().IsAutomatic));
}

[ConditionalTheory]
Expand Down Expand Up @@ -1551,78 +1574,29 @@ public virtual Task Concat_with_scalar_projection(bool async)

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Select_navigation_with_concat_and_count(bool async)
public virtual Task Select_navigation_with_concat_and_count(bool async)
{
var message = (await Assert.ThrowsAsync<InvalidOperationException>(
() => AssertQueryScalar(
async,
ss => ss.Set<Gear>().Where(g => !g.HasSoulPatch).Select(g => g.Weapons.Concat(g.Weapons).Count())))).Message;
return AssertQueryScalar(
async,
ss => ss.Set<Gear>().Where(g => !g.HasSoulPatch).Select(g => g.Weapons.Concat(g.Weapons).Count()));
}

Assert.Equal(
CoreStrings.TranslationFailed(
@"MaterializeCollectionNavigation(
Navigation: Gear.Weapons,
subquery: DbSet<Weapon>()
.Where(w => EF.Property<string>(g, ""FullName"") != null && object.Equals(
objA: EF.Property<string>(g, ""FullName""),
objB: EF.Property<string>(w, ""OwnerFullName"")))
.Where(i => EF.Property<string>(g, ""FullName"") != null && object.Equals(
objA: EF.Property<string>(g, ""FullName""),
objB: EF.Property<string>(i, ""OwnerFullName"")))
.AsQueryable()
.Concat(MaterializeCollectionNavigation(
Navigation: Gear.Weapons,
subquery: DbSet<Weapon>()
.Where(w0 => EF.Property<string>(g, ""FullName"") != null && object.Equals(
objA: EF.Property<string>(g, ""FullName""),
objB: EF.Property<string>(w0, ""OwnerFullName"")))
.Where(i => EF.Property<string>(g, ""FullName"") != null && object.Equals(
objA: EF.Property<string>(g, ""FullName""),
objB: EF.Property<string>(i, ""OwnerFullName""))))"),
message, ignoreLineEndingDifferences: true);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Concat_with_collection_navigations(bool async)
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Concat_with_collection_navigations(bool async)
{
var message = (await Assert.ThrowsAsync<InvalidOperationException>(
() => AssertQueryScalar(
async,
ss => ss.Set<Gear>().Where(g => g.HasSoulPatch).Select(g => g.Weapons.Union(g.Weapons).Count())))).Message;
return AssertQueryScalar(
async,
ss => ss.Set<Gear>().Where(g => g.HasSoulPatch).Select(g => g.Weapons.Union(g.Weapons).Count()));
}

Assert.Equal(
CoreStrings.TranslationFailed(
@"MaterializeCollectionNavigation(
Navigation: Gear.Weapons,
subquery: DbSet<Weapon>()
.Where(w => EF.Property<string>(g, ""FullName"") != null && object.Equals(
objA: EF.Property<string>(g, ""FullName""),
objB: EF.Property<string>(w, ""OwnerFullName"")))
.Where(i => EF.Property<string>(g, ""FullName"") != null && object.Equals(
objA: EF.Property<string>(g, ""FullName""),
objB: EF.Property<string>(i, ""OwnerFullName"")))
.AsQueryable()
.Union(MaterializeCollectionNavigation(
Navigation: Gear.Weapons,
subquery: DbSet<Weapon>()
.Where(w0 => EF.Property<string>(g, ""FullName"") != null && object.Equals(
objA: EF.Property<string>(g, ""FullName""),
objB: EF.Property<string>(w0, ""OwnerFullName"")))
.Where(i => EF.Property<string>(g, ""FullName"") != null && object.Equals(
objA: EF.Property<string>(g, ""FullName""),
objB: EF.Property<string>(i, ""OwnerFullName""))))"),
message, ignoreLineEndingDifferences: true);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Union_with_collection_navigations(bool async)
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Union_with_collection_navigations(bool async)
{
var message = (await Assert.ThrowsAsync<InvalidOperationException>(
() => AssertQueryScalar(
async,
ss => ss.Set<Gear>().OfType<Officer>().Select(o => o.Reports.Union(o.Reports).Count())))).Message;
return AssertQueryScalar(
async,
ss => ss.Set<Gear>().OfType<Officer>().Select(o => o.Reports.Union(o.Reports).Count()));
}

[ConditionalTheory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1488,31 +1488,77 @@ public override async Task Where_subquery_union_firstordefault_boolean(bool asyn
AssertSql(
@"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Gears] AS [g]
WHERE [g].[HasSoulPatch] = 1",
//
@"@_outer_FullName6='Damon Baird' (Size = 450)
WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND ((
SELECT TOP(1) [t].[IsAutomatic]
FROM (
SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId]
FROM [Weapons] AS [w]
WHERE [g].[FullName] = [w].[OwnerFullName]
UNION
SELECT [w0].[Id], [w0].[AmmunitionType], [w0].[IsAutomatic], [w0].[Name], [w0].[OwnerFullName], [w0].[SynergyWithId]
FROM [Weapons] AS [w0]
WHERE [g].[FullName] = [w0].[OwnerFullName]
) AS [t]
ORDER BY [t].[Id]) = CAST(1 AS bit))");
}

SELECT [w6].[Id], [w6].[AmmunitionType], [w6].[IsAutomatic], [w6].[Name], [w6].[OwnerFullName], [w6].[SynergyWithId]
FROM [Weapons] AS [w6]
WHERE @_outer_FullName6 = [w6].[OwnerFullName]",
//
@"@_outer_FullName5='Damon Baird' (Size = 450)
public override async Task Where_subquery_join_firstordefault_boolean(bool async)
{
await base.Where_subquery_join_firstordefault_boolean(async);

SELECT [w5].[Id], [w5].[AmmunitionType], [w5].[IsAutomatic], [w5].[Name], [w5].[OwnerFullName], [w5].[SynergyWithId]
FROM [Weapons] AS [w5]
WHERE @_outer_FullName5 = [w5].[OwnerFullName]",
//
@"@_outer_FullName6='Marcus Fenix' (Size = 450)
AssertSql(
@"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Gears] AS [g]
WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND ((
SELECT TOP(1) [w].[IsAutomatic]
FROM [Weapons] AS [w]
INNER JOIN (
SELECT [w0].[Id], [w0].[AmmunitionType], [w0].[IsAutomatic], [w0].[Name], [w0].[OwnerFullName], [w0].[SynergyWithId]
FROM [Weapons] AS [w0]
WHERE [g].[FullName] = [w0].[OwnerFullName]
) AS [t] ON [w].[Id] = [t].[Id]
WHERE [g].[FullName] = [w].[OwnerFullName]
ORDER BY [w].[Id]) = CAST(1 AS bit))");
}

SELECT [w6].[Id], [w6].[AmmunitionType], [w6].[IsAutomatic], [w6].[Name], [w6].[OwnerFullName], [w6].[SynergyWithId]
FROM [Weapons] AS [w6]
WHERE @_outer_FullName6 = [w6].[OwnerFullName]",
//
@"@_outer_FullName5='Marcus Fenix' (Size = 450)
public override async Task Where_subquery_left_join_firstordefault_boolean(bool async)
{
await base.Where_subquery_left_join_firstordefault_boolean(async);

SELECT [w5].[Id], [w5].[AmmunitionType], [w5].[IsAutomatic], [w5].[Name], [w5].[OwnerFullName], [w5].[SynergyWithId]
FROM [Weapons] AS [w5]
WHERE @_outer_FullName5 = [w5].[OwnerFullName]");
AssertSql(
@"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Gears] AS [g]
WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND ((
SELECT TOP(1) [w].[IsAutomatic]
FROM [Weapons] AS [w]
LEFT JOIN (
SELECT [w0].[Id], [w0].[AmmunitionType], [w0].[IsAutomatic], [w0].[Name], [w0].[OwnerFullName], [w0].[SynergyWithId]
FROM [Weapons] AS [w0]
WHERE [g].[FullName] = [w0].[OwnerFullName]
) AS [t] ON [w].[Id] = [t].[Id]
WHERE [g].[FullName] = [w].[OwnerFullName]
ORDER BY [w].[Id]) = CAST(1 AS bit))");
}

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

AssertSql(
@"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Gears] AS [g]
WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND ((
SELECT TOP(1) [t].[IsAutomatic]
FROM (
SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId]
FROM [Weapons] AS [w]
WHERE [g].[FullName] = [w].[OwnerFullName]
UNION ALL
SELECT [w0].[Id], [w0].[AmmunitionType], [w0].[IsAutomatic], [w0].[Name], [w0].[OwnerFullName], [w0].[SynergyWithId]
FROM [Weapons] AS [w0]
WHERE [g].[FullName] = [w0].[OwnerFullName]
) AS [t]
ORDER BY [t].[Id]) = CAST(1 AS bit))");
}

public override async Task Concat_with_count(bool async)
Expand Down Expand Up @@ -1575,6 +1621,66 @@ FROM [Gears] AS [g0]
) AS [t]");
}

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

AssertSql(
@"SELECT (
SELECT COUNT(*)
FROM (
SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId]
FROM [Weapons] AS [w]
WHERE [g].[FullName] = [w].[OwnerFullName]
UNION ALL
SELECT [w0].[Id], [w0].[AmmunitionType], [w0].[IsAutomatic], [w0].[Name], [w0].[OwnerFullName], [w0].[SynergyWithId]
FROM [Weapons] AS [w0]
WHERE [g].[FullName] = [w0].[OwnerFullName]
) AS [t])
FROM [Gears] AS [g]
WHERE [g].[HasSoulPatch] <> CAST(1 AS bit)");
}

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

AssertSql(
@"SELECT (
SELECT COUNT(*)
FROM (
SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId]
FROM [Weapons] AS [w]
WHERE [g].[FullName] = [w].[OwnerFullName]
UNION
SELECT [w0].[Id], [w0].[AmmunitionType], [w0].[IsAutomatic], [w0].[Name], [w0].[OwnerFullName], [w0].[SynergyWithId]
FROM [Weapons] AS [w0]
WHERE [g].[FullName] = [w0].[OwnerFullName]
) AS [t])
FROM [Gears] AS [g]
WHERE [g].[HasSoulPatch] = CAST(1 AS bit)");
}

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

AssertSql(
@"SELECT (
SELECT COUNT(*)
FROM (
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Gears] AS [g]
WHERE ([g1].[Nickname] = [g].[LeaderNickname]) AND ([g1].[SquadId] = [g].[LeaderSquadId])
UNION
SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[Discriminator], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank]
FROM [Gears] AS [g0]
WHERE ([g1].[Nickname] = [g0].[LeaderNickname]) AND ([g1].[SquadId] = [g0].[LeaderSquadId])
) AS [t])
FROM [Gears] AS [g1]
WHERE [g1].[Discriminator] = N'Officer'");
}

public override async Task Select_subquery_distinct_firstordefault(bool async)
{
await base.Select_subquery_distinct_firstordefault(async);
Expand Down
Loading

0 comments on commit 0028103

Please sign in to comment.