From 7fb4578c1275977516711d5a5e8eeb47667c12da Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Tue, 6 Sep 2022 12:21:04 -0700 Subject: [PATCH] Query: Convert unflattened GroupJoin to correlated subquery Resolves #19930 --- ...yableMethodNormalizingExpressionVisitor.cs | 93 +++++++++++++++++- .../Query/QueryTranslationPreprocessor.cs | 2 +- .../Query/QueryNoClientEvalTestBase.cs | 7 +- .../Query/ComplexNavigationsQueryTestBase.cs | 28 +++--- .../Query/Ef6GroupByTestBase.cs | 19 ++-- .../Query/GearsOfWarQueryTestBase.cs | 33 ++++--- .../Query/NorthwindJoinQueryTestBase.cs | 94 ++++++++++++++----- .../ComplexNavigationsQuerySqlServerTest.cs | 20 +++- ...NavigationsSharedTypeQuerySqlServerTest.cs | 46 ++++++++- .../Query/Ef6GroupBySqlServerTest.cs | 11 ++- .../Query/GearsOfWarQuerySqlServerTest.cs | 9 +- .../Query/NorthwindJoinQuerySqlServerTest.cs | 72 +++++++++++++- .../Query/TPCGearsOfWarQuerySqlServerTest.cs | 9 +- .../Query/TPTGearsOfWarQuerySqlServerTest.cs | 9 +- .../TemporalGearsOfWarQuerySqlServerTest.cs | 9 +- .../ComplexNavigationsQuerySqliteTest.cs | 12 +++ ...lexNavigationsSharedTypeQuerySqliteTest.cs | 12 +++ .../Query/Ef6GroupBySqliteTest.cs | 6 ++ .../Query/GearsOfWarQuerySqliteTest.cs | 9 +- .../Query/NorthwindJoinQuerySqliteTest.cs | 30 ++++++ 20 files changed, 447 insertions(+), 83 deletions(-) diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index c4d139d862a..659fd7cdb2d 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -15,8 +15,8 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal; public class QueryableMethodNormalizingExpressionVisitor : ExpressionVisitor { private readonly QueryCompilationContext _queryCompilationContext; - private readonly SelectManyVerifyingExpressionVisitor _selectManyVerifyingExpressionVisitor = new(); + private readonly GroupJoinConvertingExpressionVisitor _groupJoinConvertingExpressionVisitor = new(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -29,6 +29,19 @@ public QueryableMethodNormalizingExpressionVisitor(QueryCompilationContext query _queryCompilationContext = queryCompilationContext; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual Expression Normalize(Expression expression) + { + var result = Visit(expression); + + return _groupJoinConvertingExpressionVisitor.Visit(result); + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -332,9 +345,9 @@ private Expression TryConvertEnumerableToQueryable(MethodCallExpression methodCa || innerQueryableElementType != genericType) { while (innerArgument is UnaryExpression - { - NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs - } unaryExpression + { + NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs + } unaryExpression && unaryExpression.Type.TryGetElementType(typeof(IEnumerable<>)) != null) { innerArgument = unaryExpression.Operand; @@ -418,7 +431,7 @@ private static bool CanConvertEnumerableToQueryable(Type enumerableType, Type qu || enumerableType == typeof(IOrderedEnumerable<>) && queryableType == typeof(IOrderedQueryable<>); } - private Expression TryFlattenGroupJoinSelectMany(MethodCallExpression methodCallExpression) + private MethodCallExpression TryFlattenGroupJoinSelectMany(MethodCallExpression methodCallExpression) { var genericMethod = methodCallExpression.Method.GetGenericMethodDefinition(); if (genericMethod == QueryableMethods.SelectManyWithCollectionSelector) @@ -590,6 +603,76 @@ private Expression TryFlattenGroupJoinSelectMany(MethodCallExpression methodCall return methodCallExpression; } + private sealed class GroupJoinConvertingExpressionVisitor : ExpressionVisitor + { + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) + { + if (methodCallExpression.Method.DeclaringType == typeof(Queryable) + && methodCallExpression.Method.IsGenericMethod + && methodCallExpression.Method.GetGenericMethodDefinition() == QueryableMethods.GroupJoin) + { + var genericArguments = methodCallExpression.Method.GetGenericArguments(); + var outerSource = methodCallExpression.Arguments[0]; + var innerSource = methodCallExpression.Arguments[1]; + var outerKeySelector = methodCallExpression.Arguments[2].UnwrapLambdaFromQuote(); + var innerKeySelector = methodCallExpression.Arguments[3].UnwrapLambdaFromQuote(); + var resultSelector = methodCallExpression.Arguments[4].UnwrapLambdaFromQuote(); + + if (innerSource.Type.IsGenericType + && innerSource.Type.GetGenericTypeDefinition() != typeof(IQueryable<>)) + { + // In case of collection navigation it can be of enumerable or other type. + innerSource = Expression.Call( + QueryableMethods.AsQueryable.MakeGenericMethod(innerSource.Type.GetSequenceType()), + innerSource); + } + + var correlationPredicate = ReplacingExpressionVisitor.Replace( + outerKeySelector.Parameters[0], + resultSelector.Parameters[0], + Expression.AndAlso( + Infrastructure.ExpressionExtensions.CreateEqualsExpression( + outerKeySelector.Body, + Expression.Constant(null), + negated: true), + Infrastructure.ExpressionExtensions.CreateEqualsExpression( + outerKeySelector.Body, + innerKeySelector.Body))); + + innerSource = Expression.Call( + QueryableMethods.Where.MakeGenericMethod(genericArguments[1]), + innerSource, + Expression.Quote( + Expression.Lambda( + correlationPredicate, + innerKeySelector.Parameters))); + + var selector = ReplacingExpressionVisitor.Replace( + resultSelector.Parameters[1], + innerSource, + resultSelector.Body); + + if (genericArguments[3].IsGenericType + && genericArguments[3].GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + selector = Expression.Call( + EnumerableMethods.AsEnumerable.MakeGenericMethod(genericArguments[3].GetSequenceType()), + selector); + } + + return Expression.Call( + QueryableMethods.Select.MakeGenericMethod(genericArguments[0], genericArguments[3]), + outerSource, + Expression.Quote( + Expression.Lambda( + selector, + resultSelector.Parameters[0]))); + } + + return base.VisitMethodCall(methodCallExpression); + } + } + private sealed class SelectManyVerifyingExpressionVisitor : ExpressionVisitor { private readonly List _allowedParameters = new(); diff --git a/src/EFCore/Query/QueryTranslationPreprocessor.cs b/src/EFCore/Query/QueryTranslationPreprocessor.cs index af49d2d7a53..1f2a0cc307b 100644 --- a/src/EFCore/Query/QueryTranslationPreprocessor.cs +++ b/src/EFCore/Query/QueryTranslationPreprocessor.cs @@ -78,5 +78,5 @@ public virtual Expression Process(Expression query) /// A query expression after normalization has been done. public virtual Expression NormalizeQueryableMethod(Expression expression) => new QueryableMethodNormalizingExpressionVisitor(QueryCompilationContext) - .Visit(expression); + .Normalize(expression); } diff --git a/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs index 36e83728d92..e63dffbd772 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs @@ -132,14 +132,13 @@ public virtual void Throws_when_join() } [ConditionalFact] - public virtual void Throws_when_group_join() + public virtual void Does_not_throws_when_group_join() { using var context = CreateContext(); - AssertTranslationFailed( - () => (from e1 in context.Employees + (from e1 in context.Employees join i in new uint[] { 1, 2, 3 } on e1.EmployeeID equals i into g select e1) - .ToList()); + .ToList(); } [ConditionalFact(Skip = "Issue#18923")] diff --git a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs index cb866d1035f..cea1656b7f6 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs @@ -2073,26 +2073,22 @@ from l2 in groupJoin.Select(gg => gg) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupJoin_with_subquery_on_inner(bool async) - // SelectMany Skip/Take. Issue #19015. - => AssertTranslationFailed( - () => AssertQueryScalar( - async, - ss => from l1 in ss.Set() - join l2 in ss.Set() on l1.Id equals l2.Level1_Optional_Id into groupJoin - from l2 in groupJoin.Where(gg => gg.Id > 0).OrderBy(gg => gg.Id).Take(10).DefaultIfEmpty() - select l1.Id)); + => AssertQueryScalar( + async, + ss => from l1 in ss.Set() + join l2 in ss.Set() on l1.Id equals l2.Level1_Optional_Id into groupJoin + from l2 in groupJoin.Where(gg => gg.Id > 0).OrderBy(gg => gg.Id).Take(10).DefaultIfEmpty() + select l1.Id); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupJoin_with_subquery_on_inner_and_no_DefaultIfEmpty(bool async) - // SelectMany Skip/Take. Issue #19015. - => AssertTranslationFailed( - () => AssertQueryScalar( - async, - ss => from l1 in ss.Set() - join l2 in ss.Set() on l1.Id equals l2.Level1_Optional_Id into groupJoin - from l2 in groupJoin.Where(gg => gg.Id > 0).OrderBy(gg => gg.Id).Take(10) - select l1.Id)); + => AssertQueryScalar( + async, + ss => from l1 in ss.Set() + join l2 in ss.Set() on l1.Id equals l2.Level1_Optional_Id into groupJoin + from l2 in groupJoin.Where(gg => gg.Id > 0).OrderBy(gg => gg.Id).Take(10) + select l1.Id); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] diff --git a/test/EFCore.Specification.Tests/Query/Ef6GroupByTestBase.cs b/test/EFCore.Specification.Tests/Query/Ef6GroupByTestBase.cs index f859f622a15..c528e2722ba 100644 --- a/test/EFCore.Specification.Tests/Query/Ef6GroupByTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/Ef6GroupByTestBase.cs @@ -420,13 +420,18 @@ into g [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Group_Join_from_LINQ_101(bool async) - // GroupJoin final operator. Issue #19930. - => AssertTranslationFailed( - () => AssertQuery( - async, - ss => from c in ss.Set() - join o in ss.Set() on c equals o.Customer into ps - select new { Customer = c, Products = ps })); + => AssertQuery( + async, + ss => from c in ss.Set() + join o in ss.Set() on c equals o.Customer into ps + select new { Customer = c, Products = ps }, + elementSorter: e => e.Customer.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Customer, a.Customer); + AssertCollection(e.Products, a.Products); + }, + entryCount: 11); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index 17ad3425ca2..da1eb9b0884 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -4410,23 +4410,22 @@ from w in grouping.DefaultIfEmpty() [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Join_with_complex_key_selector(bool async) - => AssertTranslationFailed( - () => AssertQuery( - async, - ss => ss.Set() - .Join(ss.Set().Where(t => t.Note == "Marcus' Tag"), o => true, i => true, (o, i) => new { o, i }) - .GroupJoin( - ss.Set(), - oo => oo.o.Members.FirstOrDefault(v => v.Tag == oo.i), - ii => ii, - (k, g) => new - { - k.o, - k.i, - value = g.OrderBy(gg => gg.FullName).FirstOrDefault() - }) - .Select(r => new { r.o.Id, TagId = r.i.Id }), - elementSorter: e => (e.Id, e.TagId))); + => AssertQuery( + async, + ss => ss.Set() + .Join(ss.Set().Where(t => t.Note == "Marcus' Tag"), o => true, i => true, (o, i) => new { o, i }) + .GroupJoin( + ss.Set(), + oo => oo.o.Members.FirstOrDefault(v => v.Tag == oo.i), + ii => ii, + (k, g) => new + { + k.o, + k.i, + value = g.OrderBy(gg => gg.FullName).FirstOrDefault() + }) + .Select(r => new { r.o.Id, TagId = r.i.Id }), + elementSorter: e => (e.Id, e.TagId)); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] diff --git a/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs index 7565958ec6a..ca678bdefb3 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs @@ -446,6 +446,62 @@ from o2 in orders }, e => (e.A, e.B, e.C)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task GroupJoin_as_final_operator(bool async) + => AssertQuery( + async, + ss => + from c in ss.Set().Where(c => c.CustomerID.StartsWith("F")) + join o in ss.Set() on c.CustomerID equals o.CustomerID into orders + select new { c, orders }, + e => e.c.CustomerID, + elementAsserter: (e, a) => + { + AssertEqual(e.c, a.c); + AssertCollection(e.orders, a.orders); + }, + entryCount: 71); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Unflattened_GroupJoin_composed(bool async) + => AssertQuery( + async, + ss => + from i in (from c in ss.Set().Where(c => c.CustomerID.StartsWith("F")) + join o in ss.Set() on c.CustomerID equals o.CustomerID into orders + select new { c, orders }) + where i.c.City == "Lisboa" + select i, + e => e.c.CustomerID, + elementAsserter: (e, a) => + { + AssertEqual(e.c, a.c); + AssertCollection(e.orders, a.orders); + }, + entryCount: 9); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Unflattened_GroupJoin_composed_2(bool async) + => AssertQuery( + async, + ss => + from i in (from c in ss.Set().Where(c => c.CustomerID.StartsWith("F")) + join o in ss.Set() on c.CustomerID equals o.CustomerID into orders + select new { c, orders }) + join c2 in ss.Set().Where(n => n.City == "Lisboa") on i.c.CustomerID equals c2.CustomerID + select new { i, c2 }, + e => e.i.c.CustomerID, + elementAsserter: (e, a) => + { + AssertEqual(e.c2, a.c2); + AssertEqual(e.i.c, a.i.c); + AssertCollection(e.i.orders, a.i.orders); + }, + entryCount: 9); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupJoin_DefaultIfEmpty(bool async) @@ -590,16 +646,14 @@ from o in lo.Where(x => x.OrderID > 5) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupJoin_SelectMany_subquery_with_filter_orderby(bool async) - // SelectMany Skip/Take. Issue #19015. - => AssertTranslationFailed( - () => AssertQuery( - async, - ss => - from c in ss.Set() - join o in ss.Set() on c.CustomerID equals o.CustomerID into lo - from o in lo.Where(x => x.OrderID > 5).OrderBy(x => x.OrderDate) - select new { c.ContactName, o.OrderID }, - e => (e.ContactName, e.OrderID))); + => AssertQuery( + async, + ss => + from c in ss.Set() + join o in ss.Set() on c.CustomerID equals o.CustomerID into lo + from o in lo.Where(x => x.OrderID > 5).OrderBy(x => x.OrderDate) + select new { c.ContactName, o.OrderID }, + e => (e.ContactName, e.OrderID)); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -617,17 +671,15 @@ from o in lo.Where(x => x.OrderID > 5).DefaultIfEmpty() [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupJoin_SelectMany_subquery_with_filter_orderby_and_DefaultIfEmpty(bool async) - // SelectMany Skip/Take. Issue #19015. - => AssertTranslationFailed( - () => AssertQuery( - async, - ss => - from c in ss.Set().Where(c => c.CustomerID.StartsWith("F")) - join o in ss.Set() on c.CustomerID equals o.CustomerID into lo - from o in lo.Where(x => x.OrderID > 5).OrderBy(x => x.OrderDate).DefaultIfEmpty() - select new { c.ContactName, o }, - e => (e.ContactName, e.o?.OrderID), - entryCount: 23)); + => AssertQuery( + async, + ss => + from c in ss.Set().Where(c => c.CustomerID.StartsWith("F")) + join o in ss.Set() on c.CustomerID equals o.CustomerID into lo + from o in lo.Where(x => x.OrderID > 5).OrderBy(x => x.OrderDate).DefaultIfEmpty() + select new { c.ContactName, o }, + e => (e.ContactName, e.o?.OrderID), + entryCount: 63); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index 8b02e9c1134..4e1064c0ab6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -2114,14 +2114,30 @@ public override async Task GroupJoin_with_subquery_on_inner(bool async) { await base.GroupJoin_with_subquery_on_inner(async); - AssertSql(); + AssertSql( + @"SELECT [l].[Id] +FROM [LevelOne] AS [l] +OUTER APPLY ( + SELECT TOP(10) [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] + FROM [LevelTwo] AS [l0] + WHERE [l].[Id] = [l0].[Level1_Optional_Id] AND [l0].[Id] > 0 + ORDER BY [l0].[Id] +) AS [t]"); } public override async Task GroupJoin_with_subquery_on_inner_and_no_DefaultIfEmpty(bool async) { await base.GroupJoin_with_subquery_on_inner_and_no_DefaultIfEmpty(async); - AssertSql(); + AssertSql( + @"SELECT [l].[Id] +FROM [LevelOne] AS [l] +CROSS APPLY ( + SELECT TOP(10) [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] + FROM [LevelTwo] AS [l0] + WHERE [l].[Id] = [l0].[Level1_Optional_Id] AND [l0].[Id] > 0 + ORDER BY [l0].[Id] +) AS [t]"); } public override async Task Optional_navigation_in_subquery_with_unrelated_projection(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs index b6eb10b53f8..6622b9d1144 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs @@ -7103,14 +7103,56 @@ public override async Task GroupJoin_with_subquery_on_inner(bool async) { await base.GroupJoin_with_subquery_on_inner(async); - AssertSql(); + AssertSql( + @"SELECT [l].[Id] +FROM [Level1] AS [l] +OUTER APPLY ( + SELECT TOP(10) [t].[Id], [t].[OneToOne_Required_PK_Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Level2_Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [l0].[Id] AS [Id0], CASE + WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t].[Id] + END AS [c] + FROM [Level1] AS [l0] + LEFT JOIN ( + SELECT [l1].[Id], [l1].[OneToOne_Required_PK_Date], [l1].[Level1_Optional_Id], [l1].[Level1_Required_Id], [l1].[Level2_Name], [l1].[OneToMany_Optional_Inverse2Id], [l1].[OneToMany_Required_Inverse2Id], [l1].[OneToOne_Optional_PK_Inverse2Id] + FROM [Level1] AS [l1] + WHERE [l1].[OneToOne_Required_PK_Date] IS NOT NULL AND [l1].[Level1_Required_Id] IS NOT NULL AND [l1].[OneToMany_Required_Inverse2Id] IS NOT NULL + ) AS [t] ON [l0].[Id] = CASE + WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t].[Id] + END + WHERE [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL AND [l].[Id] = [t].[Level1_Optional_Id] AND CASE + WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t].[Id] + END > 0 + ORDER BY CASE + WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t].[Id] + END +) AS [t0]"); } public override async Task GroupJoin_with_subquery_on_inner_and_no_DefaultIfEmpty(bool async) { await base.GroupJoin_with_subquery_on_inner_and_no_DefaultIfEmpty(async); - AssertSql(); + AssertSql( + @"SELECT [l].[Id] +FROM [Level1] AS [l] +CROSS APPLY ( + SELECT TOP(10) [t].[Id], [t].[OneToOne_Required_PK_Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Level2_Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [l0].[Id] AS [Id0], CASE + WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t].[Id] + END AS [c] + FROM [Level1] AS [l0] + LEFT JOIN ( + SELECT [l1].[Id], [l1].[OneToOne_Required_PK_Date], [l1].[Level1_Optional_Id], [l1].[Level1_Required_Id], [l1].[Level2_Name], [l1].[OneToMany_Optional_Inverse2Id], [l1].[OneToMany_Required_Inverse2Id], [l1].[OneToOne_Optional_PK_Inverse2Id] + FROM [Level1] AS [l1] + WHERE [l1].[OneToOne_Required_PK_Date] IS NOT NULL AND [l1].[Level1_Required_Id] IS NOT NULL AND [l1].[OneToMany_Required_Inverse2Id] IS NOT NULL + ) AS [t] ON [l0].[Id] = CASE + WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t].[Id] + END + WHERE [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL AND [l].[Id] = [t].[Level1_Optional_Id] AND CASE + WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t].[Id] + END > 0 + ORDER BY CASE + WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t].[Id] + END +) AS [t0]"); } public override async Task Level4_Include(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Ef6GroupBySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Ef6GroupBySqlServerTest.cs index 8418b4d3370..5fdabb34d1c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Ef6GroupBySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Ef6GroupBySqlServerTest.cs @@ -525,7 +525,16 @@ public override async Task Group_Join_from_LINQ_101(bool async) { await base.Group_Join_from_LINQ_101(async); - AssertSql(); + AssertSql( + @"SELECT [c].[Id], [c].[CompanyName], [c].[Region], [t].[Id], [t].[CustomerId], [t].[OrderDate], [t].[Total], [t].[Id0] +FROM [CustomerForLinq] AS [c] +OUTER APPLY ( + SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate], [o].[Total], [c0].[Id] AS [Id0] + FROM [OrderForLinq] AS [o] + LEFT JOIN [CustomerForLinq] AS [c0] ON [o].[CustomerId] = [c0].[Id] + WHERE [c].[Id] = [c0].[Id] +) AS [t] +ORDER BY [c].[Id], [t].[Id]"); } public override async Task Whats_new_2021_sample_3(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index 7a1417f5d38..d4585b24e50 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -8553,7 +8553,14 @@ public override async Task Join_with_complex_key_selector(bool async) { await base.Join_with_complex_key_selector(async); - AssertSql(); + AssertSql( + @"SELECT [s].[Id], [t0].[Id] AS [TagId] +FROM [Squads] AS [s] +CROSS JOIN ( + SELECT [t].[Id] + FROM [Tags] AS [t] + WHERE [t].[Note] = N'Marcus'' Tag' +) AS [t0]"); } public override async Task Streaming_correlated_collection_issue_11403_returning_ordered_enumerable_throws(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs index 511610c6592..43a54f97a26 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs @@ -267,6 +267,59 @@ ORDER BY [o].[OrderID] ) AS [t] ON [c].[CustomerID] = [t].[CustomerID]"); } + public override async Task GroupJoin_as_final_operator(bool async) + { + await base.GroupJoin_as_final_operator(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [t].[OrderID], [t].[CustomerID], [t].[EmployeeID], [t].[OrderDate] +FROM [Customers] AS [c] +OUTER APPLY ( + SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] +) AS [t] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID]"); + } + + public override async Task Unflattened_GroupJoin_composed(bool async) + { + await base.Unflattened_GroupJoin_composed(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [t].[OrderID], [t].[CustomerID], [t].[EmployeeID], [t].[OrderDate] +FROM [Customers] AS [c] +OUTER APPLY ( + SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] +) AS [t] +WHERE ([c].[CustomerID] LIKE N'F%') AND [c].[City] = N'Lisboa' +ORDER BY [c].[CustomerID]"); + } + + public override async Task Unflattened_GroupJoin_composed_2(bool async) + { + await base.Unflattened_GroupJoin_composed_2(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [t].[CustomerID], [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region] +FROM [Customers] AS [c] +INNER JOIN ( + SELECT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region] + FROM [Customers] AS [c0] + WHERE [c0].[City] = N'Lisboa' +) AS [t] ON [c].[CustomerID] = [t].[CustomerID] +OUTER APPLY ( + SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] +) AS [t0] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID], [t].[CustomerID]"); + } + public override async Task GroupJoin_DefaultIfEmpty(bool async) { await base.GroupJoin_DefaultIfEmpty(async); @@ -395,7 +448,14 @@ public override async Task GroupJoin_SelectMany_subquery_with_filter_orderby(boo { await base.GroupJoin_SelectMany_subquery_with_filter_orderby(async); - AssertSql(); + AssertSql( + @"SELECT [c].[ContactName], [t].[OrderID] +FROM [Customers] AS [c] +CROSS APPLY ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] AND [o].[OrderID] > 5 +) AS [t]"); } public override async Task GroupJoin_SelectMany_subquery_with_filter_and_DefaultIfEmpty(bool async) @@ -417,7 +477,15 @@ public override async Task GroupJoin_SelectMany_subquery_with_filter_orderby_and { await base.GroupJoin_SelectMany_subquery_with_filter_orderby_and_DefaultIfEmpty(async); - AssertSql(); + AssertSql( + @"SELECT [c].[ContactName], [t].[OrderID], [t].[CustomerID], [t].[EmployeeID], [t].[OrderDate] +FROM [Customers] AS [c] +OUTER APPLY ( + SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] AND [o].[OrderID] > 5 +) AS [t] +WHERE [c].[CustomerID] LIKE N'F%'"); } public override async Task GroupJoin_Subquery_with_Take_Then_SelectMany_Where(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs index 90564132857..f58fc9f9b3c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs @@ -11611,7 +11611,14 @@ public override async Task Join_with_complex_key_selector(bool async) { await base.Join_with_complex_key_selector(async); - AssertSql(); + AssertSql( + @"SELECT [s].[Id], [t0].[Id] AS [TagId] +FROM [Squads] AS [s] +CROSS JOIN ( + SELECT [t].[Id] + FROM [Tags] AS [t] + WHERE [t].[Note] = N'Marcus'' Tag' +) AS [t0]"); } public override async Task Streaming_correlated_collection_issue_11403_returning_ordered_enumerable_throws(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index 0a12babd7f5..1c9c7a80bb2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -9801,7 +9801,14 @@ public override async Task Join_with_complex_key_selector(bool async) { await base.Join_with_complex_key_selector(async); - AssertSql(); + AssertSql( + @"SELECT [s].[Id], [t0].[Id] AS [TagId] +FROM [Squads] AS [s] +CROSS JOIN ( + SELECT [t].[Id] + FROM [Tags] AS [t] + WHERE [t].[Note] = N'Marcus'' Tag' +) AS [t0]"); } public override async Task Streaming_correlated_collection_issue_11403_returning_ordered_enumerable_throws(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index d27959659d4..8a7ef84a51f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -8387,7 +8387,14 @@ public override async Task Join_with_complex_key_selector(bool async) { await base.Join_with_complex_key_selector(async); - AssertSql(); + AssertSql( + @"SELECT [s].[Id], [t0].[Id] AS [TagId] +FROM [Squads] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [s] +CROSS JOIN ( + SELECT [t].[Id] + FROM [Tags] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [t] + WHERE [t].[Note] = N'Marcus'' Tag' +) AS [t0]"); } public override async Task Streaming_correlated_collection_issue_11403_returning_ordered_enumerable_throws(bool async) diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsQuerySqliteTest.cs index 49e9f867ae9..e487e5796e2 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsQuerySqliteTest.cs @@ -42,4 +42,16 @@ public override Task GroupJoin_client_method_in_OrderBy(bool async) CoreStrings.QueryUnableToTranslateMethod( "Microsoft.EntityFrameworkCore.Query.ComplexNavigationsQueryTestBase", "ClientMethodNullableInt")); + + public override async Task GroupJoin_with_subquery_on_inner(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.GroupJoin_with_subquery_on_inner(async))).Message); + + public override async Task GroupJoin_with_subquery_on_inner_and_no_DefaultIfEmpty(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.GroupJoin_with_subquery_on_inner_and_no_DefaultIfEmpty(async))).Message); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqliteTest.cs index 12ef9a44389..9a3fd4fafee 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqliteTest.cs @@ -53,4 +53,16 @@ public override Task GroupBy_aggregate_where_required_relationship(bool async) [ConditionalTheory(Skip = "Issue#26104")] public override Task GroupBy_aggregate_where_required_relationship_2(bool async) => base.GroupBy_aggregate_where_required_relationship_2(async); + + public override async Task GroupJoin_with_subquery_on_inner(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.GroupJoin_with_subquery_on_inner(async))).Message); + + public override async Task GroupJoin_with_subquery_on_inner_and_no_DefaultIfEmpty(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.GroupJoin_with_subquery_on_inner_and_no_DefaultIfEmpty(async))).Message); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Ef6GroupBySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Ef6GroupBySqliteTest.cs index 5c475a2bcd6..dd684826cfa 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/Ef6GroupBySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/Ef6GroupBySqliteTest.cs @@ -65,6 +65,12 @@ public override async Task Whats_new_2021_sample_6(bool async) => await base.Whats_new_2021_sample_6(async); #endif + public override async Task Group_Join_from_LINQ_101(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Group_Join_from_LINQ_101(async))).Message); + public class Ef6GroupBySqliteFixture : Ef6GroupByFixtureBase { public TestSqlLoggerFactory TestSqlLoggerFactory diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index 557b2561891..fe92ce567a6 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -8004,7 +8004,14 @@ public override async Task Join_with_complex_key_selector(bool async) { await base.Join_with_complex_key_selector(async); - AssertSql(); + AssertSql( + @"SELECT ""s"".""Id"", ""t0"".""Id"" AS ""TagId"" +FROM ""Squads"" AS ""s"" +CROSS JOIN ( + SELECT ""t"".""Id"" + FROM ""Tags"" AS ""t"" + WHERE ""t"".""Note"" = 'Marcus'' Tag' +) AS ""t0"""); } public override async Task Streaming_correlated_collection_issue_11403_returning_ordered_enumerable_throws(bool async) diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindJoinQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindJoinQuerySqliteTest.cs index 925b8e98a3f..c1fbef26245 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindJoinQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindJoinQuerySqliteTest.cs @@ -55,4 +55,34 @@ public override async Task Take_in_collection_projection_with_FirstOrDefault_on_ SqliteStrings.ApplyNotSupported, (await Assert.ThrowsAsync( () => base.Take_in_collection_projection_with_FirstOrDefault_on_top_level(async))).Message); + + public override async Task GroupJoin_as_final_operator(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.GroupJoin_as_final_operator(async))).Message); + + public override async Task Unflattened_GroupJoin_composed(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Unflattened_GroupJoin_composed(async))).Message); + + public override async Task Unflattened_GroupJoin_composed_2(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Unflattened_GroupJoin_composed_2(async))).Message); + + public override async Task GroupJoin_SelectMany_subquery_with_filter_orderby(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.GroupJoin_SelectMany_subquery_with_filter_orderby(async))).Message); + + public override async Task GroupJoin_SelectMany_subquery_with_filter_orderby_and_DefaultIfEmpty(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.GroupJoin_SelectMany_subquery_with_filter_orderby_and_DefaultIfEmpty(async))).Message); }