diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index 6718c5d3619..22724d93a0b 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -1548,6 +1548,14 @@ static IReadOnlyList GetMappedKeyProperties(IKey key) public void ReplaceProjection(IReadOnlyDictionary projectionMapping) { _projectionMapping.Clear(); + + // A projection is represented either as a projection-member mapping or as a client-projection list, never both + // (see GetProjection). When switching to a member mapping, any client projections left over from a previous + // projection (e.g. a prior Select that fell back to index-based binding) must be cleared, otherwise the stale + // client projections take precedence in ApplyProjection and the member-based shaper fails to remap. See #31209. + _clientProjections.Clear(); + _aliasForClientProjections.Clear(); + foreach (var (projectionMember, expression) in projectionMapping) { Check.DebugAssert( diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindGroupByQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindGroupByQueryInMemoryTest.cs index f9a1f8ff177..18be4614e3d 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindGroupByQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindGroupByQueryInMemoryTest.cs @@ -84,4 +84,28 @@ public override Task GroupBy_Select_Entire_Entity_Order(bool async) [Theory(Skip = "Issue#31209")] public override Task GroupBy_Select_Entire_Entity_Where(bool async) => base.GroupBy_Select_Entire_Entity_Where(async); + + [Theory(Skip = "Issue#31209")] + public override Task GroupBy_Select_Entire_Entity_Select(bool async) + => base.GroupBy_Select_Entire_Entity_Select(async); + + [Theory(Skip = "Issue#31209")] + public override Task GroupBy_Select_Entire_Entity_Where_Select(bool async) + => base.GroupBy_Select_Entire_Entity_Where_Select(async); + + [Theory(Skip = "Issue#31209")] + public override Task GroupBy_Select_Entire_Entity_FirstOrDefault_Where(bool async) + => base.GroupBy_Select_Entire_Entity_FirstOrDefault_Where(async); + + [Theory(Skip = "Issue#31209")] + public override Task GroupBy_ResultSelector_Entire_Entity_Where(bool async) + => base.GroupBy_ResultSelector_Entire_Entity_Where(async); + + [Theory(Skip = "Issue#31209")] + public override Task GroupBy_Select_Entire_Entity_GroupBy(bool async) + => base.GroupBy_Select_Entire_Entity_GroupBy(async); + + [Theory(Skip = "Issue#31209")] + public override Task GroupBy_Select_Entire_Entity_composite_key_Select(bool async) + => base.GroupBy_Select_Entire_Entity_composite_key_Select(async); } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs index 559dc517251..4bfdbaf25ff 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs @@ -1957,7 +1957,7 @@ public virtual Task GroupBy_Select_Entire_Entity_Where(bool async) // #31209 .Select(a => a.First()) .Where(x => x.EmployeeID == 6u)); - [Theory(Skip = "Issue#31209"), MemberData(nameof(IsAsyncData))] + [Theory, MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_Select_Entire_Entity_Where_Select(bool async) // #31209 => AssertQuery( async, @@ -1966,7 +1966,7 @@ public virtual Task GroupBy_Select_Entire_Entity_Where_Select(bool async) // #31 .Where(x => x.OrderID > 10) .Select(r => r.EmployeeID)); - [Theory(Skip = "Issue#31209"), MemberData(nameof(IsAsyncData))] + [Theory, MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_Select_Entire_Entity_Select(bool async) // #31209 => AssertQuery( async, @@ -1995,6 +1995,41 @@ public virtual Task GroupBy_Select_Anonymous_Type_With_Entire_Entity(bool async) Item = g.OrderByDescending(x => x.OrderDate).FirstOrDefault(), }).Where(x => x.Item != null)); + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_Select_Entire_Entity_FirstOrDefault_Where(bool async) // #31209 + => AssertQuery( + async, + ss => ss.Set().GroupBy(o => o.CustomerID) + .Select(g => g.OrderByDescending(o => o.OrderDate).FirstOrDefault()) + .Where(r => r.EmployeeID == 5u)); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_ResultSelector_Entire_Entity_Where(bool async) // #31209 + => AssertQuery( + async, + ss => ss.Set().GroupBy( + o => o.CustomerID, + (k, es) => es.OrderByDescending(o => o.OrderDate).First()) + .Where(r => r.EmployeeID == 6u)); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_Select_Entire_Entity_GroupBy(bool async) // #31209 + => AssertQuery( + async, + ss => ss.Set().GroupBy(o => new { o.CustomerID, o.EmployeeID }) + .Select(g => g.First()) + .GroupBy(o => o.EmployeeID) + .Select(g2 => new { g2.Key, Count = g2.Count() }), + elementSorter: e => e.Key); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_Select_Entire_Entity_composite_key_Select(bool async) // #26748 + => AssertQuery( + async, + ss => ss.Set().GroupBy(o => new { o.CustomerID, o.EmployeeID }) + .Select(g => g.First()) + .Select(p => p.OrderID)); + [Theory, MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_aggregate_join_with_group_result(bool async) => AssertQuery( diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs index 3f37ac5a1c8..459367a3c05 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs @@ -3085,14 +3085,127 @@ public override async Task GroupBy_Select_Entire_Entity_Where_Select(bool async) { await base.GroupBy_Select_Entire_Entity_Where_Select(async); - AssertSql(); + AssertSql( +""" +SELECT ( + SELECT TOP(1) [o1].[EmployeeID] + FROM [Orders] AS [o1] + WHERE [o].[OrderID] = [o1].[OrderID]) +FROM [Orders] AS [o] +GROUP BY [o].[OrderID] +HAVING ( + SELECT TOP(1) [o0].[OrderID] + FROM [Orders] AS [o0] + WHERE [o].[OrderID] = [o0].[OrderID]) > 10 +"""); } public override async Task GroupBy_Select_Entire_Entity_Select(bool async) { await base.GroupBy_Select_Entire_Entity_Select(async); - AssertSql(); + AssertSql( +""" +SELECT ( + SELECT TOP(1) [o0].[EmployeeID] + FROM [Orders] AS [o0] + WHERE [o].[OrderID] = [o0].[OrderID]) +FROM [Orders] AS [o] +GROUP BY [o].[OrderID] +"""); + } + + public override async Task GroupBy_Select_Entire_Entity_FirstOrDefault_Where(bool async) + { + await base.GroupBy_Select_Entire_Entity_FirstOrDefault_Where(async); + + AssertSql( +""" +SELECT [o4].[OrderID], [o4].[CustomerID], [o4].[EmployeeID], [o4].[OrderDate] +FROM ( + SELECT [o].[CustomerID] + FROM [Orders] AS [o] + GROUP BY [o].[CustomerID] + HAVING ( + SELECT TOP(1) [o1].[EmployeeID] + FROM [Orders] AS [o1] + WHERE [o].[CustomerID] = [o1].[CustomerID] OR ([o].[CustomerID] IS NULL AND [o1].[CustomerID] IS NULL) + ORDER BY [o1].[OrderDate] DESC) = 5 +) AS [o2] +LEFT JOIN ( + SELECT [o3].[OrderID], [o3].[CustomerID], [o3].[EmployeeID], [o3].[OrderDate] + FROM ( + SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], ROW_NUMBER() OVER(PARTITION BY [o0].[CustomerID] ORDER BY [o0].[OrderDate] DESC) AS [row] + FROM [Orders] AS [o0] + ) AS [o3] + WHERE [o3].[row] <= 1 +) AS [o4] ON [o2].[CustomerID] = [o4].[CustomerID] +"""); + } + + public override async Task GroupBy_ResultSelector_Entire_Entity_Where(bool async) + { + await base.GroupBy_ResultSelector_Entire_Entity_Where(async); + + AssertSql( +""" +SELECT [o4].[OrderID], [o4].[CustomerID], [o4].[EmployeeID], [o4].[OrderDate] +FROM ( + SELECT [o].[CustomerID] + FROM [Orders] AS [o] + GROUP BY [o].[CustomerID] + HAVING ( + SELECT TOP(1) [o1].[EmployeeID] + FROM [Orders] AS [o1] + WHERE [o].[CustomerID] = [o1].[CustomerID] OR ([o].[CustomerID] IS NULL AND [o1].[CustomerID] IS NULL) + ORDER BY [o1].[OrderDate] DESC) = 6 +) AS [o2] +LEFT JOIN ( + SELECT [o3].[OrderID], [o3].[CustomerID], [o3].[EmployeeID], [o3].[OrderDate] + FROM ( + SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], ROW_NUMBER() OVER(PARTITION BY [o0].[CustomerID] ORDER BY [o0].[OrderDate] DESC) AS [row] + FROM [Orders] AS [o0] + ) AS [o3] + WHERE [o3].[row] <= 1 +) AS [o4] ON [o2].[CustomerID] = [o4].[CustomerID] +"""); + } + + public override async Task GroupBy_Select_Entire_Entity_GroupBy(bool async) + { + await base.GroupBy_Select_Entire_Entity_GroupBy(async); + + AssertSql( +""" +SELECT [o2].[Key], COUNT(*) AS [Count] +FROM ( + SELECT ( + SELECT TOP(1) [o1].[EmployeeID] + FROM [Orders] AS [o1] + WHERE ([o0].[CustomerID] = [o1].[CustomerID] OR ([o0].[CustomerID] IS NULL AND [o1].[CustomerID] IS NULL)) AND ([o0].[EmployeeID] = [o1].[EmployeeID] OR ([o0].[EmployeeID] IS NULL AND [o1].[EmployeeID] IS NULL))) AS [Key] + FROM ( + SELECT [o].[CustomerID], [o].[EmployeeID] + FROM [Orders] AS [o] + GROUP BY [o].[CustomerID], [o].[EmployeeID] + ) AS [o0] +) AS [o2] +GROUP BY [o2].[Key] +"""); + } + + public override async Task GroupBy_Select_Entire_Entity_composite_key_Select(bool async) + { + await base.GroupBy_Select_Entire_Entity_composite_key_Select(async); + + AssertSql( +""" +SELECT ( + SELECT TOP(1) [o0].[OrderID] + FROM [Orders] AS [o0] + WHERE ([o].[CustomerID] = [o0].[CustomerID] OR ([o].[CustomerID] IS NULL AND [o0].[CustomerID] IS NULL)) AND ([o].[EmployeeID] = [o0].[EmployeeID] OR ([o].[EmployeeID] IS NULL AND [o0].[EmployeeID] IS NULL))) +FROM [Orders] AS [o] +GROUP BY [o].[CustomerID], [o].[EmployeeID] +"""); } public override async Task GroupBy_Select_Entire_Entity_Order(bool async)