From 2bc8bfaeee4622260645171ed44ca1f1560b2b24 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 9 Apr 2024 09:40:56 +0200 Subject: [PATCH] Support GroupBy over complex type Fixes #33491 --- ...yableMethodTranslatingExpressionVisitor.cs | 17 +++++++- .../Query/SqlExpressions/SelectExpression.cs | 5 +++ .../Query/ComplexTypeQueryTestBase.cs | 33 +++++++++++++++ .../ComplexTypeModel/ComplexTypeData.cs | 41 ++++-------------- .../Query/ComplexTypeQuerySqlServerTest.cs | 42 ++++++++++++++++++- .../Query/ComplexTypeQuerySqliteTest.cs | 42 ++++++++++++++++++- 6 files changed, 144 insertions(+), 36 deletions(-) diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index d9ff7dce69b..5c06db9eecc 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -823,7 +823,7 @@ protected override ShapedQueryExpression TranslateExcept(ShapedQueryExpression s return memberInitExpression.Update(updatedNewExpression, newBindings); default: - var translation = TranslateExpression(expression); + var translation = TranslateProjection(expression); if (translation == null) { return null; @@ -1325,6 +1325,21 @@ protected override ShapedQueryExpression TranslateUnion(ShapedQueryExpression so return translation; } + private Expression? TranslateProjection(Expression expression, bool applyDefaultTypeMapping = true) + { + var translation = _sqlTranslator.TranslateProjection(expression, applyDefaultTypeMapping); + + if (translation is null) + { + if (_sqlTranslator.TranslationErrorDetails != null) + { + AddTranslationErrorDetails(_sqlTranslator.TranslationErrorDetails); + } + } + + return translation; + } + /// /// Translates the given lambda expression for the source into equivalent SQL representation. /// diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index e6156e3c736..ae3ab2d5952 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -1759,6 +1759,11 @@ private static void PopulateGroupByTerms( projection.DiscriminatorExpression, groupByTerms, groupByAliases, name: DiscriminatorColumnAlias); } + foreach (var complexProperty in projection.StructuralType.GetComplexProperties()) + { + PopulateGroupByTerms(projection.BindComplexProperty(complexProperty), groupByTerms, groupByAliases, name: null); + } + break; default: diff --git a/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs index 59600760a37..5633332ad50 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs @@ -812,6 +812,37 @@ from c2 in ss.Set() AssertEqual(e.Complex?.Two, a.Complex?.Two); }); + #region GroupBy + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_over_property_in_nested_complex_type(bool async) + => AssertQuery( + async, + ss => ss.Set().GroupBy(x => x.ShippingAddress.Country.Code).Select(g => new { Code = g.Key, Count = g.Count() }), + elementSorter: g => g.Code); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_over_complex_type(bool async) + => AssertQuery( + async, + ss => ss.Set().GroupBy(x => x.ShippingAddress).Select(g => new { Address = g.Key, Count = g.Count() }), + elementSorter: g => g.Address.ZipCode, + elementAsserter: (e, a) => + { + AssertEqual(e.Address, a.Address); + Assert.Equal(e.Count, a.Count); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_over_nested_complex_type(bool async) + => AssertQuery( + async, + ss => ss.Set().GroupBy(x => x.ShippingAddress.Country).Select(g => new { Country = g.Key, Count = g.Count() }), + elementSorter: g => g.Country.Code); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Entity_with_complex_type_with_group_by_and_first(bool async) @@ -819,6 +850,8 @@ public virtual Task Entity_with_complex_type_with_group_by_and_first(bool async) async, ss => ss.Set().GroupBy(x => x.Id).Select(x => x.First())); + #endregion GroupBy + protected DbContext CreateContext() => Fixture.CreateContext(); } diff --git a/test/EFCore.Specification.Tests/TestModels/ComplexTypeModel/ComplexTypeData.cs b/test/EFCore.Specification.Tests/TestModels/ComplexTypeModel/ComplexTypeData.cs index e1b1fc01304..e71ca95b29d 100644 --- a/test/EFCore.Specification.Tests/TestModels/ComplexTypeModel/ComplexTypeData.cs +++ b/test/EFCore.Specification.Tests/TestModels/ComplexTypeModel/ComplexTypeData.cs @@ -51,7 +51,7 @@ private static IReadOnlyList CreateCustomers() AddressLine1 = "804 S. Lakeshore Road", ZipCode = 38654, Country = new Country { FullName = "United States", Code = "US" }, - Tags = new List { "foo", "bar" } + Tags = ["foo", "bar"] }; var customer1 = new Customer @@ -71,19 +71,14 @@ private static IReadOnlyList CreateCustomers() AddressLine1 = "72 Hickory Rd.", ZipCode = 07728, Country = new Country { FullName = "Germany", Code = "DE" }, - Tags = new List { "baz" } + Tags = ["baz"] }, BillingAddress = new Address { AddressLine1 = "79 Main St.", ZipCode = 29293, Country = new Country { FullName = "Germany", Code = "DE" }, - Tags = new List - { - "a1", - "a2", - "a3" - } + Tags = ["a1", "a2", "a3"] } }; @@ -92,7 +87,7 @@ private static IReadOnlyList CreateCustomers() AddressLine1 = "79 Main St.", ZipCode = 29293, Country = new Country { FullName = "Germany", Code = "DE" }, - Tags = new List { "foo", "moo" } + Tags = ["foo", "moo"] }; var customer3 = new Customer @@ -103,12 +98,7 @@ private static IReadOnlyList CreateCustomers() BillingAddress = address3 }; - return new List - { - customer1, - customer2, - customer3 - }; + return [customer1, customer2, customer3]; } private static IReadOnlyList CreateCustomerGroups(IReadOnlyList customers) @@ -134,12 +124,7 @@ private static IReadOnlyList CreateCustomerGroups(IReadOnlyList - { - group1, - group2, - group3 - }; + return [group1, group2, group3]; } private static IReadOnlyList CreateValuedCustomers() @@ -192,12 +177,7 @@ private static IReadOnlyList CreateValuedCustomers() BillingAddress = address3 }; - return new List - { - customer1, - customer2, - customer3 - }; + return [customer1, customer2, customer3]; } private static IReadOnlyList CreateValuedCustomerGroups(IReadOnlyList customers) @@ -223,12 +203,7 @@ private static IReadOnlyList CreateValuedCustomerGroups(IRe OptionalCustomer = null }; - return new List - { - group1, - group2, - group3 - }; + return [group1, group2, group3]; } public static Task SeedAsync(PoolableDbContext context) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexTypeQuerySqlServerTest.cs index 96c07dc5fa1..a74ee887946 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexTypeQuerySqlServerTest.cs @@ -1131,12 +1131,50 @@ public override async Task Same_complex_type_projected_twice_with_pushdown_as_pa AssertSql(""); } + #region GroupBy + + public override async Task GroupBy_over_property_in_nested_complex_type(bool async) + { + await base.GroupBy_over_property_in_nested_complex_type(async); + + AssertSql( + """ +SELECT [c].[ShippingAddress_Country_Code] AS [Code], COUNT(*) AS [Count] +FROM [Customer] AS [c] +GROUP BY [c].[ShippingAddress_Country_Code] +"""); + } + + public override async Task GroupBy_over_complex_type(bool async) + { + await base.GroupBy_over_complex_type(async); + + AssertSql( + """ +SELECT [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName], COUNT(*) AS [Count] +FROM [Customer] AS [c] +GROUP BY [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] +"""); + } + + public override async Task GroupBy_over_nested_complex_type(bool async) + { + await base.GroupBy_over_nested_complex_type(async); + + AssertSql( + """ +SELECT [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName], COUNT(*) AS [Count] +FROM [Customer] AS [c] +GROUP BY [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] +"""); + } + public override async Task Entity_with_complex_type_with_group_by_and_first(bool async) { await base.Entity_with_complex_type_with_group_by_and_first(async); AssertSql( -""" + """ SELECT [c3].[Id], [c3].[Name], [c3].[BillingAddress_AddressLine1], [c3].[BillingAddress_AddressLine2], [c3].[BillingAddress_Tags], [c3].[BillingAddress_ZipCode], [c3].[BillingAddress_Country_Code], [c3].[BillingAddress_Country_FullName], [c3].[ShippingAddress_AddressLine1], [c3].[ShippingAddress_AddressLine2], [c3].[ShippingAddress_Tags], [c3].[ShippingAddress_ZipCode], [c3].[ShippingAddress_Country_Code], [c3].[ShippingAddress_Country_FullName] FROM ( SELECT [c].[Id] @@ -1154,6 +1192,8 @@ FROM [Customer] AS [c0] """); } + #endregion GroupBy + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexTypeQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexTypeQuerySqliteTest.cs index 7ca77e4bf25..d729eb76348 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexTypeQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexTypeQuerySqliteTest.cs @@ -1014,12 +1014,50 @@ public override async Task Same_complex_type_projected_twice_with_pushdown_as_pa (await Assert.ThrowsAsync( () => base.Same_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(async))).Message); + #region GroupBy + + public override async Task GroupBy_over_property_in_nested_complex_type(bool async) + { + await base.GroupBy_over_property_in_nested_complex_type(async); + + AssertSql( + """ +SELECT "c"."ShippingAddress_Country_Code" AS "Code", COUNT(*) AS "Count" +FROM "Customer" AS "c" +GROUP BY "c"."ShippingAddress_Country_Code" +"""); + } + + public override async Task GroupBy_over_complex_type(bool async) + { + await base.GroupBy_over_complex_type(async); + + AssertSql( + """ +SELECT "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName", COUNT(*) AS "Count" +FROM "Customer" AS "c" +GROUP BY "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" +"""); + } + + public override async Task GroupBy_over_nested_complex_type(bool async) + { + await base.GroupBy_over_nested_complex_type(async); + + AssertSql( + """ +SELECT "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName", COUNT(*) AS "Count" +FROM "Customer" AS "c" +GROUP BY "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" +"""); + } + public override async Task Entity_with_complex_type_with_group_by_and_first(bool async) { await base.Entity_with_complex_type_with_group_by_and_first(async); AssertSql( -""" + """ SELECT "c3"."Id", "c3"."Name", "c3"."BillingAddress_AddressLine1", "c3"."BillingAddress_AddressLine2", "c3"."BillingAddress_Tags", "c3"."BillingAddress_ZipCode", "c3"."BillingAddress_Country_Code", "c3"."BillingAddress_Country_FullName", "c3"."ShippingAddress_AddressLine1", "c3"."ShippingAddress_AddressLine2", "c3"."ShippingAddress_Tags", "c3"."ShippingAddress_ZipCode", "c3"."ShippingAddress_Country_Code", "c3"."ShippingAddress_Country_FullName" FROM ( SELECT "c"."Id" @@ -1037,6 +1075,8 @@ LEFT JOIN ( """); } + #endregion GroupBy + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType());