From b1778723670814e7d119b9d2124a0a4e2b11cf47 Mon Sep 17 00:00:00 2001 From: Maurycy Markowski Date: Tue, 24 Oct 2017 18:10:34 -0700 Subject: [PATCH] Fix to #9282 - Query: improve include pipeline so it can be reused for queries projecting composed collection navigations (filters and projections) This feature optimizes a number of queries that project correlated collections. Previously those would produce N+1 queries. Now, we rewrite queries similarly to how Include pipeline does it, producing only two queries and correlating them on the client. To enable the feature the inner subquery needs to be wrapped around ToList() or ToArray() call. Current limitations: - only works for sync queries, - child entities are not being tracked, - no fixup between parent and child, - doesn't work if the parent query results in a CROSS JOIN, - doesn't work with result operators (i.e. Skip/Take/Distinct) - doesn't work if outer query needs client evaluation anywhere outside projection (e.g. order by or filter by NonMapped property) - doesn't work if inner query is correlated with query two (or more) levels up, (e.g. customers.Select(c => c.Orders.Select(o => o.OrderDetails.Where(od => od.Name == c.Name).ToList()).ToList()) - doesn't work in nested scenarios where the outer collection is streaming (e.g. customers.Select(c => c.Orders.Select(o => o.OrderDetails.Where(od => od.Name != "Foo").ToList())) - to make it work, outer collection must also be wrapped in ToList(). However it is OK to stream inner collection - in that case outer collection will take advantage of the optimization. Optimization process: original query: from c in ctx.Customers where c.CustomerID != "ALFKI" orderby c.City descending select (from o in c.Orders where o.OrderID > 100 orderby o.EmployeeID select new { o.OrderID, o.CustomerID }).ToList() nav rewrite converts it to: from c in customers where c.CustomerID != "ALFKI" order by c.City descending select (from o in orders where o.OrderID > 100 order by o.EmployeeID where c.CustomerID ?= o.CustomerID select new { o.OrderID, o.CustomerID }).ToList() which gets rewritten to (simplified): from c in customers where c.CustomerID != "ALFKI" order by c.City desc, c.CustomerID asc select CorrelateSubquery( outerKey: new { c.CustomerID }, correlationPredicate: (outer, inner) => outer.GetValue(0) == null || inner.GetValue(0) == null ? false : outer.GetValue(0) == inner.GetValue(0) correlatedCollectionFactory: () => from o in orders where o.OrderID > 100 join _c in from c in customers where c.CustomerID != "ALFKI" select new { c.City, c.CustomerID } on o.CustomerID equals _c.GetValue(1) order by _c.GetValue(0) descending, _c.GetValue(1), o.EmployeeID select new { InnerResult = new { o.OrderID, o.CustomerID } InnerKey = new { o.CustomerID }, OriginKey = new { _c.GetValue(1) } }).ToList() CorrelateSubquery is the method that combines results of outer and inner queries. Because order for both queries is the same we can perform only one pass thru inner query. We use correlation predicate (between outerKey parameter passed to CorrelateSubquery and InnerKey which is part of the final result) to determine whether giver result of the inner query belongs to the outer. We also remember latest origin key (i.e. PK of the outer, which is not always the same as outer key). If the origin key changes, it means that all inners for that outer have already been encountered. --- .../RelationalProjectionExpressionVisitor.cs | 3 +- .../SqlTranslatingExpressionVisitor.cs | 6 +- .../Query/Expressions/SelectExpression.cs | 17 +- .../Query/RelationalQueryModelVisitor.cs | 49 + .../Query/ComplexNavigationsQueryTestBase.cs | 2 +- .../Query/GearsOfWarQueryTestBase.cs | 759 +++++++++- .../Query/QueryNavigationsTestBase.cs | 21 +- .../Query/SimpleQueryTestBase.Select.cs | 2 +- .../Internal/ExpressionExtensions.cs | 28 + .../Infrastructure/CoreOptionsExtension.cs | 4 +- .../Query/CorrelatedSubqueryMetadata.cs | 35 + src/EFCore/Query/EntityQueryModelVisitor.cs | 96 +- ...tionNavigationIncludeExpressionRewriter.cs | 1 + .../CollectionNavigationSubqueryInjector.cs | 10 - ...latedCollectionFindingExpressionVisitor.cs | 249 ++++ .../CorrelatedCollectionOptimizingVisitor.cs | 594 ++++++++ ...ntityEqualityRewritingExpressionVisitor.cs | 12 +- .../NavigationRewritingExpressionVisitor.cs | 37 +- ...ceReferenceFindingExpressionTreeVisitor.cs | 36 + .../Query/Internal/ExpressionPrinter.cs | 9 + src/EFCore/Query/Internal/IQueryBuffer.cs | 11 + ...ionQueryModelRewritingExpressionVisitor.cs | 37 +- .../Internal/MaterializedAnonymousObject.cs | 120 ++ src/EFCore/Query/Internal/QueryBuffer.cs | 109 ++ src/EFCore/Query/QueryCompilationContext.cs | 5 + .../ComplexNavigationsQuerySqlServerTest.cs | 87 +- .../Query/GearsOfWarQuerySqlServerTest.cs | 1312 ++++++++++++++++- .../Query/QueryNavigationsSqlServerTest.cs | 38 +- .../Query/SimpleQuerySqlServerTest.Select.cs | 18 + 29 files changed, 3527 insertions(+), 180 deletions(-) create mode 100644 src/EFCore/Query/CorrelatedSubqueryMetadata.cs create mode 100644 src/EFCore/Query/ExpressionVisitors/Internal/CorrelatedCollectionFindingExpressionVisitor.cs create mode 100644 src/EFCore/Query/ExpressionVisitors/Internal/CorrelatedCollectionOptimizingVisitor.cs create mode 100644 src/EFCore/Query/ExpressionVisitors/Internal/QuerySourceReferenceFindingExpressionTreeVisitor.cs create mode 100644 src/EFCore/Query/Internal/MaterializedAnonymousObject.cs diff --git a/src/EFCore.Relational/Query/ExpressionVisitors/RelationalProjectionExpressionVisitor.cs b/src/EFCore.Relational/Query/ExpressionVisitors/RelationalProjectionExpressionVisitor.cs index d8f866586e3..9e58edaa0ec 100644 --- a/src/EFCore.Relational/Query/ExpressionVisitors/RelationalProjectionExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/ExpressionVisitors/RelationalProjectionExpressionVisitor.cs @@ -139,7 +139,8 @@ protected override Expression VisitNew(NewExpression newExpression) { Check.NotNull(newExpression, nameof(newExpression)); - if (newExpression.Type == typeof(AnonymousObject)) + if (newExpression.Type == typeof(AnonymousObject) + || newExpression.Type == typeof(MaterializedAnonymousObject)) { var propertyCallExpressions = ((NewArrayExpression)newExpression.Arguments.Single()).Expressions; diff --git a/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs index bd2e2982db7..9d8e842f14c 100644 --- a/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs @@ -649,7 +649,8 @@ var boundExpression } } - if (AnonymousObject.IsGetValueExpression(methodCallExpression, out var querySourceReferenceExpression)) + if (AnonymousObject.IsGetValueExpression(methodCallExpression, out var querySourceReferenceExpression) + || MaterializedAnonymousObject.IsGetValueExpression(methodCallExpression, out querySourceReferenceExpression)) { var selectExpression = _queryModelVisitor.TryGetQuery(querySourceReferenceExpression.ReferencedQuerySource); @@ -863,7 +864,8 @@ var memberBindings return Expression.Constant(memberBindings); } } - else if (expression.Type == typeof(AnonymousObject)) + else if (expression.Type == typeof(AnonymousObject) + || expression.Type == typeof(MaterializedAnonymousObject)) { var propertyCallExpressions = ((NewArrayExpression)expression.Arguments.Single()).Expressions; diff --git a/src/EFCore.Relational/Query/Expressions/SelectExpression.cs b/src/EFCore.Relational/Query/Expressions/SelectExpression.cs index 39832dceff1..8738cb057aa 100644 --- a/src/EFCore.Relational/Query/Expressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/Expressions/SelectExpression.cs @@ -9,6 +9,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query.Expressions.Internal; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.Sql; using Microsoft.EntityFrameworkCore.Storage; @@ -910,13 +911,10 @@ var existingOrdering private bool OrderingExpressionComparison(Ordering ordering, Expression expressionToMatch) { - return _expressionEqualityComparer.Equals(ordering.Expression, expressionToMatch) - || _expressionEqualityComparer.Equals( - UnwrapNullableExpression(ordering.Expression.RemoveConvert()).RemoveConvert(), - expressionToMatch) - || _expressionEqualityComparer.Equals( - UnwrapNullableExpression(expressionToMatch.RemoveConvert()).RemoveConvert(), - ordering.Expression); + var unwrappedOrderingExpression = UnwrapNullableExpression(ordering.Expression.RemoveConvert()).RemoveConvert(); + var unwrappedExpressionToMatch = UnwrapNullableExpression(expressionToMatch.RemoveConvert()).RemoveConvert(); + + return _expressionEqualityComparer.Equals(unwrappedOrderingExpression, unwrappedExpressionToMatch); } private Expression UnwrapNullableExpression(Expression expression) @@ -926,6 +924,11 @@ private Expression UnwrapNullableExpression(Expression expression) return nullableExpression.Operand; } + if (expression is NullConditionalExpression nullConditionalExpression) + { + return nullConditionalExpression.AccessOperation; + } + return expression; } diff --git a/src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs index 3fb845a1dc4..71e67326a7f 100644 --- a/src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs @@ -1058,6 +1058,54 @@ var sqlOrderingExpression } } + /// + /// Determines whether correlated collections (if any) can be optimized. + /// + /// True if optimization is allowed, false otherwise. + protected override bool CanOptimizeCorrelatedCollections() + { + if (!base.CanOptimizeCorrelatedCollections()) + { + return false; + } + + if (RequiresClientEval + || RequiresClientFilter + || RequiresClientJoin + || RequiresClientOrderBy + || RequiresClientSelectMany) + { + return false; + } + + var injectParametersFinder = new InjectParametersFindingVisitor(QueryCompilationContext.QueryMethodProvider.InjectParametersMethod); + injectParametersFinder.Visit(Expression); + + return !injectParametersFinder.InjectParametersFound; + } + + private class InjectParametersFindingVisitor : ExpressionVisitorBase + { + private MethodInfo _injectParametersMethod; + + public InjectParametersFindingVisitor(MethodInfo injectParametersMethod) + { + _injectParametersMethod = injectParametersMethod; + } + + public bool InjectParametersFound { get; private set; } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.MethodIsClosedFormOf(_injectParametersMethod)) + { + InjectParametersFound = true; + } + + return base.VisitMethodCall(node); + } + } + /// /// Visits nodes. /// @@ -1449,6 +1497,7 @@ var predicate var projection = QueryCompilationContext.QuerySourceRequiresMaterialization(joinClause) + //|| joinClause.ItemType == typeof(MaterializedAnonymousObject) ? innerSelectExpression.Projection : Enumerable.Empty(); diff --git a/src/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs b/src/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs index 7093b902adf..f6488171b21 100644 --- a/src/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs +++ b/src/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs @@ -3516,7 +3516,7 @@ public virtual void Project_collection_navigation_composed() AssertQuery( l1s => from l1 in l1s where l1.Id < 3 - select new { l1.Id, collection = l1.OneToMany_Optional.Where(l2 => l2.Name != "Foo") }, + select new { l1.Id, collection = l1.OneToMany_Optional.Where(l2 => l2.Name != "Foo").ToList() }, elementSorter: e => e.Id, elementAsserter: (e, a) => { diff --git a/src/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/src/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index 5627f3d919d..a91139f2595 100644 --- a/src/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/src/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -1760,7 +1760,7 @@ public virtual void Select_correlated_filtered_collection() gs => gs .Where(g => g.CityOfBirth.Name == "Ephyra" || g.CityOfBirth.Name == "Hanover") .OrderBy(g => g.Nickname) - .Select(g => g.Weapons.Where(w => w.Name != "Lancer")), + .Select(g => g.Weapons.Where(w => w.Name != "Lancer").ToList()), assertOrder: true, elementAsserter: CollectionAsserter(e => e.Id, (e, a) => Assert.Equal(e.Id, a.Id))); } @@ -1769,7 +1769,7 @@ public virtual void Select_correlated_filtered_collection() public virtual void Select_correlated_filtered_collection_with_composite_key() { AssertQuery( - gs => gs.OfType().OrderBy(g => g.Nickname).Select(g => g.Reports.Where(r => r.Nickname != "Dom")), + gs => gs.OfType().OrderBy(g => g.Nickname).Select(g => g.Reports.Where(r => r.Nickname != "Dom").ToList()), assertOrder: true, elementAsserter: CollectionAsserter(e => e.Nickname, (e, a) => Assert.Equal(e.Nickname, a.Nickname))); } @@ -3235,11 +3235,762 @@ public virtual void Enum_ToString_is_client_eval() .Select(g => g.Rank.ToString())); } - protected GearsOfWarContext CreateContext() + [ConditionalFact] + public virtual void Correlated_collections_basic_projection() + { + AssertQuery( + gs => from g in gs + where g.Nickname != "Marcus" + orderby g.Nickname + select (from w in g.Weapons + where w.IsAutomatic || w.Name != "foo" + select w).ToList(), + assertOrder: true, + elementAsserter: CollectionAsserter(e => e.Id, (e, a) => Assert.Equal(e.Id, a.Id))); + } + + [ConditionalFact] + public virtual void Correlated_collections_basic_projection_explicit_to_list() + { + AssertQuery( + gs => from g in gs + where g.Nickname != "Marcus" + orderby g.Nickname + select (from w in g.Weapons + where w.IsAutomatic || w.Name != "foo" + select w).ToList(), + assertOrder: true, + elementAsserter: CollectionAsserter(e => e.Id, (e, a) => Assert.Equal(e.Id, a.Id))); + } + + [ConditionalFact] + public virtual void Correlated_collections_basic_projection_explicit_to_array() + { + AssertQuery( + gs => from g in gs + where g.Nickname != "Marcus" + orderby g.Nickname + select (from w in g.Weapons + where w.IsAutomatic || w.Name != "foo" + select w).ToArray(), + assertOrder: true, + elementAsserter: CollectionAsserter(e => e.Id, (e, a) => Assert.Equal(e.Id, a.Id))); + } + + [ConditionalFact] + public virtual void Correlated_collections_basic_projection_ordered() + { + AssertQuery( + gs => from g in gs + where g.Nickname != "Marcus" + orderby g.Nickname + select (from w in g.Weapons + where w.IsAutomatic || w.Name != "foo" + orderby w.Name descending + select w).ToList(), + assertOrder: true, + elementAsserter: CollectionAsserter(elementAsserter: (e, a) => Assert.Equal(e.Id, a.Id))); + } + + [ConditionalFact] + public virtual void Correlated_collections_basic_projection_composite_key() + { + AssertQuery(gs => + from o in gs.OfType() + where o.Nickname != "Foo" + select new + { + o.Nickname, + Collection = (from r in o.Reports + where !r.HasSoulPatch + select new { r.Nickname, r.FullName }).ToArray() + }, + elementSorter: e => e.Nickname, + elementAsserter: (e, a) => + { + Assert.Equal(e.Nickname, a.Nickname); + CollectionAsserter(elementSorter: ee => ee.FullName)(e.Collection, a.Collection); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_basic_projecting_single_property() + { + AssertQuery( + gs => from g in gs + where g.Nickname != "Marcus" + orderby g.Nickname + select (from w in g.Weapons + where w.IsAutomatic || w.Name != "foo" + select w.Name).ToList(), + assertOrder: true, + elementAsserter: CollectionAsserter()); + } + + [ConditionalFact] + public virtual void Correlated_collections_basic_projecting_constant() + { + AssertQuery( + gs => from g in gs + where g.Nickname != "Marcus" + orderby g.Nickname + select (from w in g.Weapons + where w.IsAutomatic || w.Name != "foo" + select "BFG").ToList(), + assertOrder: true, + elementAsserter: CollectionAsserter()); + } + + [ConditionalFact] + public virtual void Correlated_collections_projection_of_collection_thru_navigation() + { + AssertQuery( + gs => from g in gs + orderby g.FullName + where g.Nickname != "Marcus" + select g.Squad.Missions.Where(m => m.MissionId != 17).ToList(), + assertOrder: true, + elementAsserter: CollectionAsserter( + e => e.MissionId + " " + e.SquadId, + (e, a) => + { + Assert.Equal(e.MissionId, a.MissionId); + Assert.Equal(e.SquadId, a.SquadId); + })); + } + + [ConditionalFact] + public virtual void Correlated_collections_project_anonymous_collection_result() + { + AssertQuery( + ss => from s in ss + where s.Id < 20 + select new + { + s.Name, + Collection = (from m in s.Members + select new { m.FullName, m.Rank }).ToList() + }, + elementSorter: e => e.Name, + elementAsserter: (e, a) => + { + Assert.Equal(e.Name, a.Name); + CollectionAsserter(ee => ee.FullName + " " + ee.Rank)(e.Collection, a.Collection); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_nested() + { + AssertQuery( + ss => from s in ss + select (from m in s.Missions + where m.MissionId < 42 + select (from ps in m.Mission.ParticipatingSquads + where ps.SquadId < 7 + select ps).ToList()).ToList(), + elementSorter: CollectionSorter(), + elementAsserter: (e, a) => + { + CollectionAsserter( + CollectionSorter(), + CollectionAsserter( + ee => ee.SquadId + " " + ee.MissionId, + (ee, aa) => + { + Assert.Equal(ee.SquadId, aa.SquadId); + Assert.Equal(ee.MissionId, aa.MissionId); + }))(e, a); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_nested_mixed_streaming_with_buffer1() + { + AssertQuery( + ss => from s in ss + select (from m in s.Missions + where m.MissionId < 3 + select (from ps in m.Mission.ParticipatingSquads + where ps.SquadId < 2 + select ps).ToList()), + elementSorter: CollectionSorter(), + elementAsserter: (e, a) => + { + CollectionAsserter( + CollectionSorter(), + CollectionAsserter( + ee => ee.SquadId + " " + ee.MissionId, + (ee, aa) => + { + Assert.Equal(ee.SquadId, aa.SquadId); + Assert.Equal(ee.MissionId, aa.MissionId); + }))(e, a); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_nested_mixed_streaming_with_buffer2() + { + AssertQuery( + ss => from s in ss + select (from m in s.Missions + where m.MissionId < 42 + select (from ps in m.Mission.ParticipatingSquads + where ps.SquadId < 7 + select ps)).ToList(), + elementSorter: CollectionSorter(), + elementAsserter: (e, a) => + { + CollectionAsserter( + CollectionSorter(), + CollectionAsserter( + ee => ee.SquadId + " " + ee.MissionId, + (ee, aa) => + { + Assert.Equal(ee.SquadId, aa.SquadId); + Assert.Equal(ee.MissionId, aa.MissionId); + }))(e, a); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_nested_with_custom_ordering() + { + AssertQuery( + gs => gs + .OfType() + .OrderByDescending(o => o.HasSoulPatch) + .Select(o => new + { + o.FullName, + OuterCollection = o.Reports + .Where(r => r.FullName != "Foo") + .OrderBy(r => r.Rank) + .Select(g => new + { + g.FullName, + InnerCollection = g.Weapons + .Where(w => w.Name != "Bar") + .OrderBy(w => w.IsAutomatic).ToList() + }).ToList() + }), + elementSorter: e => e.FullName, + elementAsserter: (e, a) => + { + Assert.Equal(e.FullName, a.FullName); + CollectionAsserter( + ee => ee.FullName, + (ee, aa) => + { + Assert.Equal(ee.FullName, aa.FullName); + CollectionAsserter( + eee => eee.Name, + (eee, aaa) => Assert.Equal(eee.Name, aaa.Name))(ee.InnerCollection, aa.InnerCollection); + })(e.OuterCollection, a.OuterCollection); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_same_collection_projected_multiple_times() + { + AssertQuery( + gs => + from g in gs + select new + { + g.FullName, + First = g.Weapons.Where(w1 => w1.IsAutomatic).ToList(), + Second = g.Weapons.Where(w2 => w2.IsAutomatic).ToList() + }, + elementSorter: e => e.FullName, + elementAsserter: (e, a) => + { + Assert.Equal(e.FullName, a.FullName); + CollectionAsserter(ee => ee.Id, (ee, aa) => Assert.Equal(ee.Id, aa.Id))(e.First, a.First); + CollectionAsserter(ee => ee.Id, (ee, aa) => Assert.Equal(ee.Id, aa.Id))(e.Second, a.Second); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_similar_collection_projected_multiple_times() + { + AssertQuery( + gs => + from g in gs + orderby g.Rank + select new + { + g.FullName, + First = g.Weapons.OrderBy(w1 => w1.OwnerFullName).Where(w1 => w1.IsAutomatic).ToList(), + Second = g.Weapons.OrderBy(w2 => w2.IsAutomatic).Where(w2 => !w2.IsAutomatic).ToArray() + }, + elementSorter: e => e.FullName, + elementAsserter: (e, a) => + { + Assert.Equal(e.FullName, a.FullName); + CollectionAsserter(ee => ee.Id, (ee, aa) => Assert.Equal(ee.Id, aa.Id))(e.First, a.First); + CollectionAsserter(ee => ee.Id, (ee, aa) => Assert.Equal(ee.Id, aa.Id))(e.Second, a.Second); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_different_collections_projected() + { + AssertQuery( + gs => + from o in gs.OfType() + orderby o.FullName + select new + { + o.Nickname, + First = o.Weapons.Where(w => w.IsAutomatic).Select(w => new { w.Name, w.IsAutomatic }).ToArray(), + Second = o.Reports.OrderBy(r => r.FullName).Select(r => new { r.Nickname, r.Rank }).ToList() + }, + assertOrder: true, + elementAsserter: (e, a) => + { + Assert.Equal(e.Nickname, a.Nickname); + CollectionAsserter()(e.First, a.First); + CollectionAsserter()(e.Second, a.Second); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_multiple_nested_complex_collections() + { + AssertQuery( + gs => + from o in gs.OfType() + orderby o.HasSoulPatch descending, o.Tag.Note + where o.Reports.Any() + select new + { + o.FullName, + OuterCollection = (from r in o.Reports + where r.FullName != "Foo" + orderby r.Rank + select new + { + r.FullName, + InnerCollection = (from w in r.Weapons + where w.Name != "Bar" + orderby w.IsAutomatic + select new + { + w.Id, + InnerFirst = w.Owner.Weapons.Select(ww => new { ww.Name, ww.IsAutomatic }).ToList(), + InnerSecond = w.Owner.Squad.Members.OrderBy(mm => mm.Nickname).Select(mm => new { mm.Nickname, mm.HasSoulPatch }).ToList() + }).ToList() + }).ToList(), + OuterCollection2 = (from www in o.Tag.Gear.Weapons + orderby www.IsAutomatic, www.Owner.Nickname descending + select www).ToList() + }, + elementSorter: e => e.FullName, + elementAsserter: (e, a) => + { + Assert.Equal(e.FullName, a.FullName); + + CollectionAsserter( + ee => ee.FullName, + (ee, aa) => + { + Assert.Equal(ee.FullName, aa.FullName); + CollectionAsserter( + eee => eee.Id, + (eee, aaa) => + { + Assert.Equal(eee.Id, aaa.Id); + CollectionAsserter(eeee => eeee.Name)(eee.InnerFirst, aaa.InnerFirst); + CollectionAsserter()(eee.InnerSecond, aaa.InnerSecond); + })(ee.InnerCollection, aa.InnerCollection); + })(e.OuterCollection, a.OuterCollection); + + CollectionAsserter( + ee => ee.Id, + (ee, aa) => Assert.Equal(ee.Id, aa.Id))(e.OuterCollection2, a.OuterCollection2); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_inner_subquery_selector_references_outer_qsre() + { + AssertQuery( + gs => + from o in gs.OfType() + select new + { + o.FullName, + Collection = from r in o.Reports + select new { ReportName = r.FullName, OfficerName = o.FullName } + }, + elementSorter: e => e.FullName, + elementAsserter: (e, a) => + { + Assert.Equal(e.FullName, a.FullName); + CollectionAsserter(ee => ee.ReportName)(e.Collection, a.Collection); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_inner_subquery_predicate_references_outer_qsre() + { + AssertQuery( + gs => + from o in gs.OfType() + select new + { + o.FullName, + Collection = from r in o.Reports + where o.FullName != "Foo" + select new { ReportName = r.FullName } + }, + elementSorter: e => e.FullName, + elementAsserter: (e, a) => + { + Assert.Equal(e.FullName, a.FullName); + CollectionAsserter(ee => ee.ReportName)(e.Collection, a.Collection); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_nested_inner_subquery_references_outer_qsre_one_level_up() + { + AssertQuery( + gs => + from o in gs.OfType() + select new + { + o.FullName, + OuterCollection = (from r in o.Reports + where r.FullName != "Foo" + select new + { + r.FullName, + InnerCollection = (from w in r.Weapons + where w.Name != "Bar" + select new + { + w.Name, + r.Nickname + }).ToList() + }).ToList(), + }, + elementSorter: e => e.FullName, + elementAsserter: (e, a) => + { + Assert.Equal(e.FullName, a.FullName); + CollectionAsserter( + ee => ee.FullName, + (ee, aa) => + { + Assert.Equal(ee.FullName, aa.FullName); + CollectionAsserter(eee => eee.Name)(ee.InnerCollection, aa.InnerCollection); + })(e.OuterCollection, a.OuterCollection); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_nested_inner_subquery_references_outer_qsre_two_levels_up() + { + AssertQuery( + gs => + from o in gs.OfType() + select new + { + o.FullName, + OuterCollection = from r in o.Reports + where r.FullName != "Foo" + select new + { + r.FullName, + InnerCollection = from w in r.Weapons + where w.Name != "Bar" + select new + { + w.Name, + o.Nickname + } + }, + }, + elementSorter: e => e.FullName, + elementAsserter: (e, a) => + { + Assert.Equal(e.FullName, a.FullName); + CollectionAsserter( + ee => ee.FullName, + (ee, aa) => + { + Assert.Equal(ee.FullName, aa.FullName); + CollectionAsserter(eee => eee.Name)(ee.InnerCollection, aa.InnerCollection); + })(e.OuterCollection, a.OuterCollection); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_on_select_many() + { + AssertQuery( + (gs, ss) => + from g in gs + from s in ss + where g.HasSoulPatch + orderby g.Nickname, s.Id descending + select new + { + GearNickname = g.Nickname, + SquadName = s.Name, + Collection1 = from w in g.Weapons + where w.IsAutomatic || w.Name != "foo" + select w, + Collection2 = from m in s.Members + where !m.HasSoulPatch + select m + }, + assertOrder: true, + elementAsserter: (e, a) => + { + Assert.Equal(e.GearNickname, e.GearNickname); + Assert.Equal(e.SquadName, e.SquadName); + + CollectionAsserter(ee => ee.Id, (ee, aa) => Assert.Equal(ee.Id, aa.Id))(e.Collection1, a.Collection1); + CollectionAsserter(ee => ee.Nickname, (ee, aa) => Assert.Equal(ee.Nickname, aa.Nickname))(e.Collection2, a.Collection2); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_with_Skip() + { + AssertQuery( + ss => ss.OrderBy(s => s.Name).Select(s => s.Members.OrderBy(m => m.Nickname).Skip(1)), + assertOrder: true, + elementAsserter: (e, a) => + { + CollectionAsserter(elementAsserter: (ee, aa) => Assert.Equal(ee.Nickname, aa.Nickname))(e, a); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_with_Take() + { + AssertQuery( + ss => ss.OrderBy(s => s.Name).Select(s => s.Members.OrderBy(m => m.Nickname).Take(2)), + assertOrder: true, + elementAsserter: (e, a) => + { + CollectionAsserter(elementAsserter: (ee, aa) => Assert.Equal(ee.Nickname, aa.Nickname))(e, a); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_with_Distinct() + { + AssertQuery( + ss => ss.OrderBy(s => s.Name).Select(s => s.Members.OrderBy(m => m.Nickname).Distinct()), + assertOrder: true, + elementAsserter: (e, a) => + { + CollectionAsserter(elementAsserter: (ee, aa) => Assert.Equal(ee.Nickname, aa.Nickname))(e, a); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_with_FirstOrDefault() + { + AssertQuery( + ss => ss.OrderBy(s => s.Name).Select(s => s.Members.OrderBy(m => m.Nickname).Select(m => m.FullName).FirstOrDefault()), + assertOrder: true, + elementAsserter: (e, a) => + { + CollectionAsserter(elementAsserter: (ee, aa) => Assert.Equal(ee.Nickname, aa.Nickname)); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_on_left_join_with_predicate() + { + AssertQuery( + (ts, gs) => + from t in ts + join g in gs on t.GearNickName equals g.Nickname into grouping + from g in grouping.DefaultIfEmpty() + where !g.HasSoulPatch + select new { g.Nickname, WeaponNames = g.Weapons.Select(w => w.Name).ToList() }, + (ts, gs) => + from t in ts + join g in gs on t.GearNickName equals g.Nickname into grouping + from g in grouping.DefaultIfEmpty() + where !MaybeScalar(g, () => g.HasSoulPatch) == true || g == null + select new { Nickname = Maybe(g, () => g.Nickname), WeaponNames = g == null ? new List() : g.Weapons.Select(w => w.Name) }, + elementSorter: e => e.Nickname, + elementAsserter: (e, a) => + { + Assert.Equal(e.Nickname, a.Nickname); + CollectionAsserter(ee => ee)(e.WeaponNames, a.WeaponNames); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_on_left_join_with_null_value() + { + AssertQuery( + (ts, gs) => + from t in ts + join g in gs on t.GearNickName equals g.Nickname into grouping + from g in grouping.DefaultIfEmpty() + orderby t.Note + select g.Weapons.Select(w => w.Name).ToList(), + (ts, gs) => + from t in ts + join g in gs on t.GearNickName equals g.Nickname into grouping + from g in grouping.DefaultIfEmpty() + orderby t.Note + select g != null ? g.Weapons.Select(w => w.Name) : new List(), + assertOrder: true, + elementAsserter: (e, a) => CollectionAsserter(ee => ee)); + } + + [ConditionalFact] + public virtual void Correlated_collections_left_join_with_self_reference() { - return Fixture.CreateContext(); + AssertQuery( + (ts, gs) => + from t in ts + join o in gs.OfType() on t.GearNickName equals o.Nickname into grouping + from o in grouping.DefaultIfEmpty() + select new { t.Note, ReportNames = o.Reports.Select(r => r.FullName).ToList() }, + (ts, gs) => + from t in ts + join o in gs.OfType() on t.GearNickName equals o.Nickname into grouping + from o in grouping.DefaultIfEmpty() + select new { t.Note, ReportNames = o != null ? o.Reports.Select(r => r.FullName) : new List() }, + elementSorter: e => e.Note, + elementAsserter: (e, a) => + { + Assert.Equal(e.Note, a.Note); + CollectionAsserter(ee => ee)(e.ReportNames, a.ReportNames); + }); } + [ConditionalFact] + public virtual void Correlated_collections_deeply_nested_left_join() + { + AssertQuery( + (ts, gs) => + from t in ts + join g in gs on t.GearNickName equals g.Nickname into grouping + from g in grouping.DefaultIfEmpty() + orderby t.Note, g.Nickname descending + select g.Squad.Members.Where(m => m.HasSoulPatch).Select(m => new { m.Nickname, AutomaticWeapons = m.Weapons.Where(w => w.IsAutomatic).ToList() }).ToList(), + (ts, gs) => + from t in ts + join g in gs on t.GearNickName equals g.Nickname into grouping + from g in grouping.DefaultIfEmpty() + orderby t.Note, Maybe(g, () => g.Nickname) descending + select g != null ? g.Squad.Members.Where(m => m.HasSoulPatch).OrderBy(m => m.Nickname).Select(m => m.Weapons.Where(w => w.IsAutomatic)) : new List>(), + assertOrder: true, + elementAsserter: (e, a) => + CollectionAsserter( + elementAsserter: (ee, aa) => CollectionAsserter(eee => eee.Id, (eee, aaa) => Assert.Equal(eee.Id, aaa.Id)))); + } + + [ConditionalFact] + public virtual void Correlated_collections_from_left_join_with_additional_elements_projected_of_that_join() + { + AssertQuery( + ws => ws.OrderBy(w => w.Name).Select(w => w.Owner.Squad.Members.OrderByDescending(m => m.FullName).Select(m => new { Weapons = m.Weapons.Where(ww => !ww.IsAutomatic).OrderBy(ww => ww.Id).ToList(), m.Rank }).ToList()), + ws => ws.OrderBy(w => w.Name).Select(w => w.Owner != null + ? w.Owner.Squad.Members.OrderByDescending(m => m.FullName).Select(m => new Tuple, MilitaryRank>(m.Weapons.Where(ww => !ww.IsAutomatic).OrderBy(ww => ww.Id), m.Rank)) + : new List, MilitaryRank>>()), + assertOrder: true, + elementAsserter: (e, a) => + { + CollectionAsserter( + elementAsserter: (ee, aa) => + { + Assert.Equal(ee.Item2, aa.Rank); + CollectionAsserter( + elementAsserter: (eee, aaa) => Assert.Equal(eee.Id, aaa.Id))(ee.Item1, aa.Weapons); + })(e, a); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_complex_scenario1() + { + AssertQuery( + gs => + from r in gs + select new + { + r.FullName, + OuterCollection = (from w in r.Weapons + select new + { + w.Id, + InnerCollection = w.Owner.Squad.Members.OrderBy(mm => mm.Nickname).Select(mm => new { mm.Nickname, mm.HasSoulPatch }).ToList() + }).ToList() + }, + elementSorter: e => e.FullName, + elementAsserter: (e, a) => + { + Assert.Equal(e.FullName, a.FullName); + + CollectionAsserter( + ee => ee.Id, + (ee, aa) => + { + Assert.Equal(ee.Id, aa.Id); + CollectionAsserter(eee => eee.Nickname)(ee.InnerCollection, aa.InnerCollection); + })(e.OuterCollection, a.OuterCollection); + }); + } + + [ConditionalFact] + public virtual void Correlated_collections_complex_scenario2() + { + AssertQuery( + gs => + from o in gs.OfType() + select new + { + o.FullName, + OuterCollection = (from r in o.Reports + select new + { + r.FullName, + InnerCollection = (from w in r.Weapons + select new + { + w.Id, + InnerSecond = w.Owner.Squad.Members.OrderBy(mm => mm.Nickname).Select(mm => new { mm.Nickname, mm.HasSoulPatch }).ToList() + }).ToList() + }).ToList(), + }, + elementSorter: e => e.FullName, + elementAsserter: (e, a) => + { + Assert.Equal(e.FullName, a.FullName); + + CollectionAsserter( + ee => ee.FullName, + (ee, aa) => + { + Assert.Equal(ee.FullName, aa.FullName); + CollectionAsserter( + eee => eee.Id, + (eee, aaa) => + { + Assert.Equal(eee.Id, aaa.Id); + CollectionAsserter()(eee.InnerSecond, aaa.InnerSecond); + })(ee.InnerCollection, aa.InnerCollection); + })(e.OuterCollection, a.OuterCollection); + }); + } + + protected GearsOfWarContext CreateContext() => Fixture.CreateContext(); + protected virtual void ClearLog() { } diff --git a/src/EFCore.Specification.Tests/Query/QueryNavigationsTestBase.cs b/src/EFCore.Specification.Tests/Query/QueryNavigationsTestBase.cs index 017b61b9688..d39224f77b7 100644 --- a/src/EFCore.Specification.Tests/Query/QueryNavigationsTestBase.cs +++ b/src/EFCore.Specification.Tests/Query/QueryNavigationsTestBase.cs @@ -421,6 +421,7 @@ public virtual void Select_Navigations_Where_Navigations() [ConditionalFact] public virtual void Select_collection_navigation_simple() { + // TODO: temporarily tracking is disabled for correlated collections AssertQuery( cs => from c in cs where c.CustomerID.StartsWith("A") @@ -431,13 +432,13 @@ orderby c.CustomerID { Assert.Equal(e.CustomerID, a.CustomerID); CollectionAsserter(o => o.OrderID, (ee, aa) => Assert.Equal(ee.OrderID, aa.OrderID))(e.Orders, a.Orders); - }, - entryCount: 34); + }); } [ConditionalFact] public virtual void Select_collection_navigation_multi_part() { + // TODO: temporarily tracking is disabled for correlated collections AssertQuery( os => from o in os where o.CustomerID == "ALFKI" @@ -447,8 +448,20 @@ public virtual void Select_collection_navigation_multi_part() { Assert.Equal(e.OrderID, a.OrderID); CollectionAsserter(o => o.OrderID, (ee, aa) => Assert.Equal(ee.OrderID, aa.OrderID))(e.Orders, a.Orders); - }, - entryCount: 7); + }); + } + + [ConditionalFact] + public virtual void Select_collection_navigation_multi_part2() + { + AssertQuery( + ods => + from od in ods + orderby od.OrderID, od.ProductID + where od.Order.CustomerID == "ALFKI" || od.Order.CustomerID == "ANTON" + select new { od.Order.Customer.Orders }, + assertOrder: true, + elementAsserter: (e, a) => CollectionAsserter(ee => ee.OrderID, (ee, aa) => Assert.Equal(ee.OrderID, aa.OrderID))); } [ConditionalFact] diff --git a/src/EFCore.Specification.Tests/Query/SimpleQueryTestBase.Select.cs b/src/EFCore.Specification.Tests/Query/SimpleQueryTestBase.Select.cs index bb5019246b2..c7cbb7457b7 100644 --- a/src/EFCore.Specification.Tests/Query/SimpleQueryTestBase.Select.cs +++ b/src/EFCore.Specification.Tests/Query/SimpleQueryTestBase.Select.cs @@ -45,7 +45,7 @@ public virtual void Projection_when_null_value() public virtual void Projection_when_client_evald_subquery() { AssertQuery( - cs => cs.Select(c => string.Join(", ", c.Orders.Select(o => o.CustomerID)))); + cs => cs.Select(c => string.Join(", ", c.Orders.Select(o => o.CustomerID).ToList()))); } [ConditionalFact] diff --git a/src/EFCore/Extensions/Internal/ExpressionExtensions.cs b/src/EFCore/Extensions/Internal/ExpressionExtensions.cs index f9c4815d451..0e94d0f77a3 100644 --- a/src/EFCore/Extensions/Internal/ExpressionExtensions.cs +++ b/src/EFCore/Extensions/Internal/ExpressionExtensions.cs @@ -8,6 +8,8 @@ using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Extensions.Internal; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Utilities; using Remotion.Linq.Clauses; @@ -366,5 +368,31 @@ public static bool IsNullPropagationCandidate( return true; } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static Expression CreateKeyAccessExpression( + [NotNull] this Expression target, + [NotNull] IReadOnlyList properties) + { + Check.NotNull(target, nameof(target)); + Check.NotNull(properties, nameof(properties)); + + return properties.Count == 1 + ? target.CreateEFPropertyExpression(properties[0]) + : Expression.New( + AnonymousObject.AnonymousObjectCtor, + Expression.NewArrayInit( + typeof(object), + properties + .Select(p => + Expression.Convert( + target.CreateEFPropertyExpression(p), + typeof(object))) + .Cast() + .ToArray())); + } } } diff --git a/src/EFCore/Infrastructure/CoreOptionsExtension.cs b/src/EFCore/Infrastructure/CoreOptionsExtension.cs index 35c6e04e914..5744e8ded42 100644 --- a/src/EFCore/Infrastructure/CoreOptionsExtension.cs +++ b/src/EFCore/Infrastructure/CoreOptionsExtension.cs @@ -39,8 +39,8 @@ public class CoreOptionsExtension : IDbContextOptionsExtension private long? _serviceProviderHash; private string _logFragment; - private WarningsConfiguration _warningsConfiguration - = new WarningsConfiguration().TryWithExplicit(CoreEventId.LazyLoadOnDisposedContextWarning, WarningBehavior.Throw); + private WarningsConfiguration _warningsConfiguration + = new WarningsConfiguration().TryWithExplicit(CoreEventId.LazyLoadOnDisposedContextWarning, WarningBehavior.Throw); /// /// Creates a new set of options with everything set to default values. /// diff --git a/src/EFCore/Query/CorrelatedSubqueryMetadata.cs b/src/EFCore/Query/CorrelatedSubqueryMetadata.cs new file mode 100644 index 00000000000..ec940c80426 --- /dev/null +++ b/src/EFCore/Query/CorrelatedSubqueryMetadata.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.Metadata; +using JetBrains.Annotations; +using Remotion.Linq.Clauses; + +namespace Microsoft.EntityFrameworkCore.Query +{ + /// + /// Structure to store metadata needed for correlated collection optimizations. + /// + public class CorrelatedSubqueryMetadata + { + /// + /// Id associated with the collection that is being optimized. + /// + public virtual int Index { get; set; } + + /// + /// First navigation in the chain leading to collection navigation that is being optimized. + /// + public virtual INavigation FirstNavigation { get; [param: NotNull] set; } + + /// + /// Collection navigation that is being optimized. + /// + public virtual INavigation CollectionNavigation { get; [param: NotNull] set; } + + /// + /// Query source that is origin of the collection navigation. + /// + public virtual IQuerySource ParentQuerySource { get; [param: NotNull] set; } + } +} diff --git a/src/EFCore/Query/EntityQueryModelVisitor.cs b/src/EFCore/Query/EntityQueryModelVisitor.cs index 415401bf606..8e840d7ade3 100644 --- a/src/EFCore/Query/EntityQueryModelVisitor.cs +++ b/src/EFCore/Query/EntityQueryModelVisitor.cs @@ -13,6 +13,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Extensions.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -235,6 +236,7 @@ protected virtual void InterceptExceptions() /// Rewrites collection navigation projections so that they can be handled by the Include pipeline. /// /// The query. + [Obsolete("This is now handled by correlated collection optimization.")] protected virtual void RewriteProjectedCollectionNavigationsToIncludes([NotNull] QueryModel queryModel) { Check.NotNull(queryModel, nameof(queryModel)); @@ -277,16 +279,21 @@ protected virtual void OptimizeQueryModel( // Rewrite includes/navigations - RewriteProjectedCollectionNavigationsToIncludes(queryModel); - var includeCompiler = new IncludeCompiler(QueryCompilationContext, _querySourceTracingExpressionVisitorFactory); - includeCompiler.CompileIncludes(queryModel, TrackResults(queryModel), asyncQuery); queryModel.TransformExpressions(new CollectionNavigationSubqueryInjector(this).Visit); queryModel.TransformExpressions(new CollectionNavigationSetOperatorSubqueryInjector(this).Visit); var navigationRewritingExpressionVisitor = _navigationRewritingExpressionVisitorFactory.Create(this); + navigationRewritingExpressionVisitor.InjectSubqueryToCollectionsInProjection(queryModel); + + // TODO: for now correlated collection optimization only works for sync queries + if (!asyncQuery) + { + var correlatedCollectionFinder = new CorrelatedCollectionFindingExpressionVisitor(this); + queryModel.SelectClause.TransformExpressions(correlatedCollectionFinder.Visit); + } navigationRewritingExpressionVisitor.Rewrite(queryModel, parentQueryModel: null); @@ -510,7 +517,6 @@ var entityTrackingInfos MethodInfo trackingMethod; if (isGrouping) - { trackingMethod = LinqOperatorProvider.TrackGroupedEntities @@ -1017,6 +1023,82 @@ public override void VisitOrdering( Expression.Constant(ordering.OrderingDirection)); } + + private bool TryOptimizeCorrelatedCollections([NotNull] QueryModel queryModel) + { + //// TODO: disabled for cross joins - problem is outer query containig cross join can produce duplicate results + if (queryModel.BodyClauses.OfType().Where(c => !IsPartOfLeftJoinPattern(c, queryModel)).Any()) + { + return false; + } + + var correlatedCollectionOptimizer = new CorrelatedCollectionOptimizingVisitor( + this, + queryModel); + + var newSelector = correlatedCollectionOptimizer.Visit(queryModel.SelectClause.Selector); + if (newSelector != queryModel.SelectClause.Selector) + { + queryModel.SelectClause.Selector = newSelector; + + if (correlatedCollectionOptimizer.ParentOrderings.Any()) + { + var existingOrderByClauses = queryModel.BodyClauses.OfType().ToList(); + foreach (var existingOrderByClause in existingOrderByClauses) + { + queryModel.BodyClauses.Remove(existingOrderByClause); + } + + var orderByClause = new OrderByClause(); + + foreach (var ordering in correlatedCollectionOptimizer.ParentOrderings) + { + orderByClause.Orderings.Add(ordering); + } + + queryModel.BodyClauses.Add(orderByClause); + + VisitOrderByClause(orderByClause, queryModel, queryModel.BodyClauses.IndexOf(orderByClause)); + } + + return true; + } + + return false; + } + + private bool IsPartOfLeftJoinPattern(AdditionalFromClause additionalFromClause, QueryModel queryModel) + { + var index = queryModel.BodyClauses.IndexOf(additionalFromClause); + var groupJoinClause = queryModel.BodyClauses.ElementAtOrDefault(index - 1) as GroupJoinClause; + + var subQueryModel + = (additionalFromClause?.FromExpression as SubQueryExpression) + ?.QueryModel; + + var referencedQuerySource + = subQueryModel?.MainFromClause.FromExpression.TryGetReferencedQuerySource(); + + if (groupJoinClause != null + && groupJoinClause == referencedQuerySource + && queryModel.CountQuerySourceReferences(groupJoinClause) == 1 + && subQueryModel.BodyClauses.Count == 0 + && subQueryModel.ResultOperators.Count == 1 + && subQueryModel.ResultOperators[0] is DefaultIfEmptyResultOperator) + { + return true; + } + + return false; + } + + /// + /// Determines whether correlated collections (if any) can be optimized. + /// + /// True if optimization is allowed, false otherwise. + protected virtual bool CanOptimizeCorrelatedCollections() + => true; + /// /// Visits nodes. /// @@ -1035,6 +1117,12 @@ public override void VisitSelectClause( return; } + // TODO: for now optimization only works for sync queries + if (CanOptimizeCorrelatedCollections()) + { + TryOptimizeCorrelatedCollections(queryModel); + } + var selector = ReplaceClauseReferences( _projectionExpressionVisitorFactory diff --git a/src/EFCore/Query/ExpressionVisitors/Internal/CollectionNavigationIncludeExpressionRewriter.cs b/src/EFCore/Query/ExpressionVisitors/Internal/CollectionNavigationIncludeExpressionRewriter.cs index 425ff1ee682..f86342a88ec 100644 --- a/src/EFCore/Query/ExpressionVisitors/Internal/CollectionNavigationIncludeExpressionRewriter.cs +++ b/src/EFCore/Query/ExpressionVisitors/Internal/CollectionNavigationIncludeExpressionRewriter.cs @@ -22,6 +22,7 @@ namespace Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// + [Obsolete("This is now handled by correlated collection optimization.")] public class CollectionNavigationIncludeExpressionRewriter : ExpressionVisitorBase { private readonly EntityQueryModelVisitor _queryModelVisitor; diff --git a/src/EFCore/Query/ExpressionVisitors/Internal/CollectionNavigationSubqueryInjector.cs b/src/EFCore/Query/ExpressionVisitors/Internal/CollectionNavigationSubqueryInjector.cs index cd0ad96dc08..d38656fe6ca 100644 --- a/src/EFCore/Query/ExpressionVisitors/Internal/CollectionNavigationSubqueryInjector.cs +++ b/src/EFCore/Query/ExpressionVisitors/Internal/CollectionNavigationSubqueryInjector.cs @@ -104,16 +104,6 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp return methodCallExpression; } - if (methodCallExpression.Method.MethodIsClosedFormOf( - CollectionNavigationIncludeExpressionRewriter.ProjectCollectionNavigationMethodInfo)) - { - var newArgument = Visit(methodCallExpression.Arguments[0]); - - return newArgument != methodCallExpression.Arguments[0] - ? methodCallExpression.Update(methodCallExpression.Object, new[] { newArgument, methodCallExpression.Arguments[1] }) - : methodCallExpression; - } - var shouldInject = ShouldInject; if (!methodCallExpression.Method.IsEFPropertyMethod() && !_collectionMaterializingMethods.Any(m => methodCallExpression.Method.MethodIsClosedFormOf(m))) diff --git a/src/EFCore/Query/ExpressionVisitors/Internal/CorrelatedCollectionFindingExpressionVisitor.cs b/src/EFCore/Query/ExpressionVisitors/Internal/CorrelatedCollectionFindingExpressionVisitor.cs new file mode 100644 index 00000000000..14cd210c9be --- /dev/null +++ b/src/EFCore/Query/ExpressionVisitors/Internal/CorrelatedCollectionFindingExpressionVisitor.cs @@ -0,0 +1,249 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Extensions.Internal; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query.Internal; +using JetBrains.Annotations; +using Remotion.Linq.Clauses.Expressions; +using Remotion.Linq.Parsing; +using Remotion.Linq; +using Remotion.Linq.Clauses; + +namespace Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class CorrelatedCollectionFindingExpressionVisitor : RelinqExpressionVisitor + { + private EntityQueryModelVisitor _queryModelVisitor; + private CorrelatedSubqueryOptimizationValidator _validator; + + private static readonly MethodInfo _toListMethodInfo + = typeof(Enumerable).GetTypeInfo().GetDeclaredMethod(nameof(Enumerable.ToList)); + + private static readonly MethodInfo _toArrayMethodInfo + = typeof(Enumerable).GetTypeInfo().GetDeclaredMethod(nameof(Enumerable.ToArray)); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public CorrelatedCollectionFindingExpressionVisitor([NotNull] EntityQueryModelVisitor queryModelVisitor) + { + _queryModelVisitor = queryModelVisitor; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name.StartsWith(nameof(IQueryBuffer.IncludeCollection), StringComparison.Ordinal)) + { + return node; + } + + SubQueryExpression subQueryExpression = null; + if ((node.Method.MethodIsClosedFormOf(_toListMethodInfo) || node.Method.MethodIsClosedFormOf(_toArrayMethodInfo)) + && node.Arguments[0] is SubQueryExpression) + { + subQueryExpression = (SubQueryExpression)node.Arguments[0]; + } + + if (node.Method.MethodIsClosedFormOf(CollectionNavigationSubqueryInjector.MaterializeCollectionNavigationMethodInfo) + && node.Arguments[1] is SubQueryExpression) + { + subQueryExpression = (SubQueryExpression)node.Arguments[1]; + } + + if (subQueryExpression != null) + { + TryMarkSubQuery(subQueryExpression); + + return node; + } + + return base.VisitMethodCall(node); + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override Expression VisitSubQuery(SubQueryExpression expression) + // prune subqueries (and potential subqueries inside them) that are not wrapped around ToList/ToArray + // we can't optimize correlated collection if it's parent is streaming + => expression; + + private void TryMarkSubQuery(SubQueryExpression expression) + { + //if (_queryModelVisitor.QueryCompilationContext.IsAsyncQuery) + //{ + // return; + //} + + var subQueryModel = expression.QueryModel; + + subQueryModel.SelectClause.TransformExpressions(Visit); + + if (_validator == null) + { + _validator = new CorrelatedSubqueryOptimizationValidator(); + } + + if (_validator.CanTryOptimizeCorreltedSubquery(subQueryModel)) + { + // if the query passes validation it becomes a candidate for future optimization + // optimiation can't always be performed, e.g. when client-eval is needed + // but we need to collect metadata (i.e. INavigations) before nav rewrite converts them into joins + _queryModelVisitor.BindNavigationPathPropertyExpression( + subQueryModel.MainFromClause.FromExpression, + (properties, querySource) => + { + var collectionNavigation = properties.OfType().SingleOrDefault(n => n.IsCollection()); + + if (collectionNavigation != null) + { + _queryModelVisitor.QueryCompilationContext.CorrelatedSubqueryMetadataMap[subQueryModel.MainFromClause] = new CorrelatedSubqueryMetadata + { + Index = _queryModelVisitor.QueryCompilationContext.CorrelatedSubqueryMetadataMap.Count, + FirstNavigation = properties.OfType().First(), + CollectionNavigation = collectionNavigation, + ParentQuerySource = querySource + }; + + return expression; + } + + return default; + }); + } + } + + private class CorrelatedSubqueryOptimizationValidator + { + public bool CanTryOptimizeCorreltedSubquery(QueryModel queryModel) + { + if (queryModel.ResultOperators.Any()) + { + return false; + } + + // first pass finds all the query sources defined in this scope (i.e. from clauses) + var declaredQuerySourcesFinder = new DefinedQuerySourcesFindingVisitor(); + declaredQuerySourcesFinder.VisitQueryModel(queryModel); + + // second pass makes sure that all qsres reference only query sources that were discovered in the first step, i.e. nothing from the outside + var qsreScopeValidator = new ReferencedQuerySourcesScopeValidatingVisitor(queryModel.MainFromClause, declaredQuerySourcesFinder.QuerySources); + qsreScopeValidator.VisitQueryModel(queryModel); + + return qsreScopeValidator.AllQuerySourceReferencesInScope; + } + + private class DefinedQuerySourcesFindingVisitor : QueryModelVisitorBase + { + public ISet QuerySources { get; } = new HashSet(); + + public override void VisitQueryModel(QueryModel queryModel) + { + queryModel.TransformExpressions(new TransformingQueryModelExpressionVisitor(this).Visit); + + base.VisitQueryModel(queryModel); + } + + public override void VisitMainFromClause(MainFromClause fromClause, QueryModel queryModel) + { + QuerySources.Add(fromClause); + + base.VisitMainFromClause(fromClause, queryModel); + } + + public override void VisitAdditionalFromClause(AdditionalFromClause fromClause, QueryModel queryModel, int index) + { + QuerySources.Add(fromClause); + + base.VisitAdditionalFromClause(fromClause, queryModel, index); + } + } + + private class ReferencedQuerySourcesScopeValidatingVisitor : QueryModelVisitorBase + { + private class InnerVisitor : TransformingQueryModelExpressionVisitor + { + private ISet _querySourcesInScope; + + public InnerVisitor(ISet querySourcesInScope, ReferencedQuerySourcesScopeValidatingVisitor transformingQueryModelVisitor) + : base(transformingQueryModelVisitor) + { + _querySourcesInScope = querySourcesInScope; + } + + public bool AllQuerySourceReferencesInScope { get; private set; } = true; + + protected override Expression VisitQuerySourceReference(QuerySourceReferenceExpression expression) + { + if (!_querySourcesInScope.Contains(expression.ReferencedQuerySource)) + { + AllQuerySourceReferencesInScope = false; + } + + return base.VisitQuerySourceReference(expression); + } + } + + // query source that can reference something outside the scope, e.g. main from clause that contains the correlated navigation + private IQuerySource _exemptQuerySource; + private InnerVisitor _innerVisitor; + + public ReferencedQuerySourcesScopeValidatingVisitor(IQuerySource exemptQuerySource, ISet querySourcesInScope) + { + _exemptQuerySource = exemptQuerySource; + _innerVisitor = new InnerVisitor(querySourcesInScope, this); + } + + public bool AllQuerySourceReferencesInScope => _innerVisitor.AllQuerySourceReferencesInScope; + + public override void VisitMainFromClause(MainFromClause fromClause, QueryModel queryModel) + { + if (fromClause != _exemptQuerySource) + { + fromClause.TransformExpressions(_innerVisitor.Visit); + } + } + + protected override void VisitBodyClauses(ObservableCollection bodyClauses, QueryModel queryModel) + { + foreach (var bodyClause in bodyClauses) + { + if (bodyClause != _exemptQuerySource) + { + bodyClause.TransformExpressions(_innerVisitor.Visit); + } + } + } + + public override void VisitSelectClause(SelectClause selectClause, QueryModel queryModel) + { + selectClause.TransformExpressions(_innerVisitor.Visit); + } + + public override void VisitResultOperator(ResultOperatorBase resultOperator, QueryModel queryModel, int index) + { + // it is not necessary to visit result ops at the moment, since we don't optimize subqueries that contain any result ops + // however, we might support some result ops in the future + resultOperator.TransformExpressions(_innerVisitor.Visit); + } + } + } + } +} diff --git a/src/EFCore/Query/ExpressionVisitors/Internal/CorrelatedCollectionOptimizingVisitor.cs b/src/EFCore/Query/ExpressionVisitors/Internal/CorrelatedCollectionOptimizingVisitor.cs new file mode 100644 index 00000000000..78278f14555 --- /dev/null +++ b/src/EFCore/Query/ExpressionVisitors/Internal/CorrelatedCollectionOptimizingVisitor.cs @@ -0,0 +1,594 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.EntityFrameworkCore.Extensions.Internal; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Query.Expressions.Internal; +using Microsoft.EntityFrameworkCore.Query.Internal; +using JetBrains.Annotations; +using Remotion.Linq; +using Remotion.Linq.Clauses; +using Remotion.Linq.Clauses.Expressions; +using Remotion.Linq.Clauses.ExpressionVisitors; + +namespace Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class CorrelatedCollectionOptimizingVisitor : ExpressionVisitorBase + { + private readonly EntityQueryModelVisitor _queryModelVisitor; + private readonly QueryCompilationContext _queryCompilationContext; + private readonly QueryModel _parentQueryModel; + + private static readonly ExpressionEqualityComparer _expressionEqualityComparer + = new ExpressionEqualityComparer(); + + private static MethodInfo _correlateSubqueryMethodInfo + = typeof(IQueryBuffer).GetMethod(nameof(IQueryBuffer.CorrelateSubquery)); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public CorrelatedCollectionOptimizingVisitor( + [NotNull] EntityQueryModelVisitor queryModelVisitor, + [NotNull] QueryModel parentQueryModel) + { + _queryModelVisitor = queryModelVisitor; + _queryCompilationContext = queryModelVisitor.QueryCompilationContext; + _parentQueryModel = parentQueryModel; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual List ParentOrderings { get; } = new List(); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override Expression VisitSubQuery(SubQueryExpression subQueryExpression) + { + if (_queryCompilationContext.CorrelatedSubqueryMetadataMap.TryGetValue(subQueryExpression.QueryModel.MainFromClause, out var correlatedSubqueryMetadata)) + { + var parentQsre = new QuerySourceReferenceExpression(correlatedSubqueryMetadata.ParentQuerySource); + var result = Rewrite(correlatedSubqueryMetadata.Index, subQueryExpression.QueryModel, correlatedSubqueryMetadata.CollectionNavigation, parentQsre); + + return result; + } + + return base.VisitSubQuery(subQueryExpression); + } + + private Expression Rewrite(int correlatedCollectionIndex, QueryModel collectionQueryModel, INavigation navigation, QuerySourceReferenceExpression originQuerySource) + { + var querySourceReferenceFindingExpressionTreeVisitor + = new QuerySourceReferenceFindingExpressionTreeVisitor(); + + var originalCorrelationPredicate = collectionQueryModel.BodyClauses.OfType().Single(c => c.Predicate is NullConditionalEqualExpression); + collectionQueryModel.BodyClauses.Remove(originalCorrelationPredicate); + + originalCorrelationPredicate.TransformExpressions(querySourceReferenceFindingExpressionTreeVisitor.Visit); + var parentQuerySourceReferenceExpression = querySourceReferenceFindingExpressionTreeVisitor.QuerySourceReferenceExpression; + + querySourceReferenceFindingExpressionTreeVisitor = new QuerySourceReferenceFindingExpressionTreeVisitor(); + querySourceReferenceFindingExpressionTreeVisitor.Visit(((NullConditionalEqualExpression)originalCorrelationPredicate.Predicate).InnerKey); + + var currentKey = BuildKeyAccess(navigation.ForeignKey.Properties, querySourceReferenceFindingExpressionTreeVisitor.QuerySourceReferenceExpression); + + // PK of the parent qsre + var originKey = BuildKeyAccess(_queryCompilationContext.Model.FindEntityType(originQuerySource.Type).FindPrimaryKey().Properties, originQuerySource); + + // principal side of the FK relationship between parent and this collection + var outerKey = BuildKeyAccess(navigation.ForeignKey.PrincipalKey.Properties, parentQuerySourceReferenceExpression); + + var parentQuerySource = parentQuerySourceReferenceExpression.ReferencedQuerySource; + + // ordering priority for parent: + // - user specified orderings + // - parent PK + // - principal side of the FK between parent and child + + // ordering priority for child: + // - user specified orderings on parent (from join) + // - parent PK (from join) + // - dependent side of the FK between parent and child + // - customer specified orderings on child + + var parentOrderings = new List(); + var exisingParentOrderByClause = _parentQueryModel.BodyClauses.OfType().LastOrDefault(); + if (exisingParentOrderByClause != null) + { + parentOrderings.AddRange(exisingParentOrderByClause.Orderings); + } + + var originEntityType = _queryCompilationContext.Model.FindEntityType(originQuerySource.Type); + foreach (var property in originEntityType.FindPrimaryKey().Properties) + { + TryAddPropertyToOrderings(property, originQuerySource, parentOrderings); + } + + foreach (var property in navigation.ForeignKey.PrincipalKey.Properties) + { + TryAddPropertyToOrderings(property, parentQuerySourceReferenceExpression, parentOrderings); + } + + ParentOrderings.AddRange(parentOrderings); + + //// if selector contains multiple correlated collections, visiting the first one changes that collections QM (changing it's type) + //// which makes the parent QM inconsistent temporarily. QM's type is different but the CorrelateCollections method that fixes the result type + //// is not part of the QM and it's added only when the entire Selector is replaced - i.e. after all it's components have been visited + + //// since when we clone the parent QM, we don't care about it's original selector anyway (it's being discarded) + //// we avoid cloning the selector in the first place and avoid all the potential problem with temporarily mismatched types of the subqueries inside + var parentSelectClause = _parentQueryModel.SelectClause; + _parentQueryModel.SelectClause = new SelectClause(Expression.Default(parentSelectClause.Selector.Type)); + + var querySourceMapping = new QuerySourceMapping(); + var clonedParentQueryModel = _parentQueryModel.Clone(querySourceMapping); + + _parentQueryModel.SelectClause = parentSelectClause; + + _queryCompilationContext.UpdateMapping(querySourceMapping); + _queryCompilationContext.CloneAnnotations(querySourceMapping, clonedParentQueryModel); + + var clonedParentQuerySourceReferenceExpression + = (QuerySourceReferenceExpression)querySourceMapping.GetExpression(parentQuerySource); + + var clonedParentQuerySource + = clonedParentQuerySourceReferenceExpression.ReferencedQuerySource; + + var parentItemName + = parentQuerySource.HasGeneratedItemName() + ? navigation.DeclaringEntityType.DisplayName()[0].ToString().ToLowerInvariant() + : parentQuerySource.ItemName; + + collectionQueryModel.MainFromClause.ItemName = $"{parentItemName}.{navigation.Name}"; + + var collectionQuerySourceReferenceExpression + = new QuerySourceReferenceExpression(collectionQueryModel.MainFromClause); + + var subQueryProjection = new List(); + subQueryProjection.AddRange(parentOrderings.Select(o => CloningExpressionVisitor.AdjustExpressionAfterCloning(o.Expression, querySourceMapping))); + + var joinQuerySourceReferenceExpression + = CreateJoinToParentQuery( + clonedParentQueryModel, + clonedParentQuerySourceReferenceExpression, + collectionQuerySourceReferenceExpression, + navigation.ForeignKey, + collectionQueryModel, + subQueryProjection); + + ApplyParentOrderings( + parentOrderings, + clonedParentQueryModel, + querySourceMapping); + + LiftOrderBy( + clonedParentQuerySource, + joinQuerySourceReferenceExpression, + clonedParentQueryModel, + collectionQueryModel, + subQueryProjection); + + clonedParentQueryModel.SelectClause.Selector + = Expression.New( + MaterializedAnonymousObject.AnonymousObjectCtor, + Expression.NewArrayInit( + typeof(object), + subQueryProjection.Select(e => Expression.Convert(e, typeof(object))))); + + clonedParentQueryModel.ResultTypeOverride = typeof(IQueryable<>).MakeGenericType(clonedParentQueryModel.SelectClause.Selector.Type); + + var newOriginKey = CloningExpressionVisitor + .AdjustExpressionAfterCloning(originKey, querySourceMapping); + + var newOriginKeyElements = ((NewArrayExpression)(((NewExpression)newOriginKey).Arguments[0])).Expressions; + var remappedOriginKeyElements = RemapOriginKeyExpressions(newOriginKeyElements, joinQuerySourceReferenceExpression, subQueryProjection); + + var tupleCtor = typeof(Tuple<,,>).MakeGenericType( + collectionQueryModel.SelectClause.Selector.Type, + typeof(MaterializedAnonymousObject), + typeof(MaterializedAnonymousObject)).GetConstructors().FirstOrDefault(); + + var correlateSubqueryMethod = _correlateSubqueryMethodInfo.MakeGenericMethod(collectionQueryModel.SelectClause.Selector.Type); + + collectionQueryModel.SelectClause.Selector + = Expression.New( + tupleCtor, + new Expression[] + { + collectionQueryModel.SelectClause.Selector, + currentKey, + Expression.New( + MaterializedAnonymousObject.AnonymousObjectCtor, + Expression.NewArrayInit( + typeof(object), + remappedOriginKeyElements)) + }); + + // Enumerable or OrderedEnumerable + collectionQueryModel.ResultTypeOverride = collectionQueryModel.BodyClauses.OfType().Any() + ? typeof(IOrderedEnumerable<>).MakeGenericType(collectionQueryModel.SelectClause.Selector.Type) + : typeof(IEnumerable<>).MakeGenericType(collectionQueryModel.SelectClause.Selector.Type); + + // since we cloned QM, we need to check if it's query sources require materialization (e.g. TypeIs operation for InMemory) + _queryCompilationContext.FindQuerySourcesRequiringMaterialization(_queryModelVisitor, collectionQueryModel); + + var correlationPredicate = CreateCorrelationPredicate(navigation); + + var arguments = new List + { + Expression.Constant(correlatedCollectionIndex), + Expression.Constant(navigation), + outerKey, + Expression.Lambda(new SubQueryExpression(collectionQueryModel)), + correlationPredicate + }; + + var result = Expression.Call( + Expression.Property( + EntityQueryModelVisitor.QueryContextParameter, + nameof(QueryContext.QueryBuffer)), + correlateSubqueryMethod, + arguments); + + if (collectionQueryModel.ResultTypeOverride.GetGenericTypeDefinition() == typeof(IOrderedEnumerable<>)) + { + return + Expression.Call( + _queryCompilationContext.LinqOperatorProvider.ToOrdered + .MakeGenericMethod(result.Type.GetSequenceType()), + result); + } + + return result; + } + + private static Expression BuildKeyAccess(IEnumerable keyProperties, Expression qsre) + { + var keyAccessExpressions = keyProperties.Select(p => new NullConditionalExpression(qsre, qsre.CreateEFPropertyExpression(p))).ToArray(); + + return Expression.New( + MaterializedAnonymousObject.AnonymousObjectCtor, + Expression.NewArrayInit( + typeof(object), + keyAccessExpressions.Select(k => Expression.Convert(k, typeof(object))))); + } + + private static Expression CreateCorrelationPredicate(INavigation navigation) + { + var foreignKey = navigation.ForeignKey; + var primaryKeyProperties = foreignKey.PrincipalKey.Properties; + var foreignKeyProperties = foreignKey.Properties; + + var outerKeyParameter = Expression.Parameter(typeof(MaterializedAnonymousObject), "o"); + var innerKeyParameter = Expression.Parameter(typeof(MaterializedAnonymousObject), "i"); + + return Expression.Lambda( + primaryKeyProperties + .Select((pk, i) => new { pk, i }) + .Zip( + foreignKeyProperties, + (outer, inner) => + { + var outerKeyAccess = + Expression.Call( + outerKeyParameter, + MaterializedAnonymousObject.GetValueMethodInfo, + Expression.Constant(outer.i)); + + var typedOuterKeyAccess = + Expression.Convert( + outerKeyAccess, + primaryKeyProperties[outer.i].ClrType); + + var innerKeyAccess = + Expression.Call( + innerKeyParameter, + MaterializedAnonymousObject.GetValueMethodInfo, + Expression.Constant(outer.i)); + + var typedInnerKeyAccess = + Expression.Convert( + innerKeyAccess, + foreignKeyProperties[outer.i].ClrType); + + Expression equalityExpression; + if (typedOuterKeyAccess.Type != typedInnerKeyAccess.Type) + { + if (typedOuterKeyAccess.Type.IsNullableType()) + { + typedInnerKeyAccess = Expression.Convert(typedInnerKeyAccess, typedOuterKeyAccess.Type); + } + else + { + typedOuterKeyAccess = Expression.Convert(typedOuterKeyAccess, typedInnerKeyAccess.Type); + } + } + + equalityExpression = Expression.Equal(typedOuterKeyAccess, typedInnerKeyAccess); + + return + (Expression)Expression.Condition( + Expression.OrElse( + Expression.Equal(innerKeyAccess, Expression.Default(innerKeyAccess.Type)), + Expression.Equal(outerKeyAccess, Expression.Default(outerKeyAccess.Type))), + Expression.Constant(false), + equalityExpression); + }) + .Aggregate((e1, e2) => Expression.AndAlso(e1, e2)), + outerKeyParameter, + innerKeyParameter); + } + + private void TryAddPropertyToOrderings( + IProperty property, + QuerySourceReferenceExpression propertyQsre, + ICollection orderings) + { + var propertyExpression = propertyQsre.CreateEFPropertyExpression(property); + + var orderingExpression = Expression.Convert( + new NullConditionalExpression( + propertyQsre, + propertyExpression), + propertyExpression.Type); + + + if (!orderings.Any( + o => _expressionEqualityComparer.Equals(o.Expression, orderingExpression) + || AreEquivalentPropertyExpressions(o.Expression, orderingExpression))) + { + orderings.Add(new Ordering(orderingExpression, OrderingDirection.Asc)); + } + } + + private static bool AreEquivalentPropertyExpressions(Expression expression1, Expression expression2) + { + var expressionWithoutConvert1 = expression1.RemoveConvert(); + var expressionWithoutNullConditional1 = (expressionWithoutConvert1 as NullConditionalExpression)?.AccessOperation + ?? expressionWithoutConvert1; + + var expressionWithoutConvert2 = expression2.RemoveConvert(); + var expressionWithoutNullConditional2 = (expressionWithoutConvert2 as NullConditionalExpression)?.AccessOperation + ?? expressionWithoutConvert2; + + QuerySourceReferenceExpression qsre1 = null; + QuerySourceReferenceExpression qsre2 = null; + string propertyName1 = null; + string propertyName2 = null; + + if (expressionWithoutNullConditional1 is MethodCallExpression methodCallExpression1 + && methodCallExpression1.IsEFProperty()) + { + qsre1 = methodCallExpression1.Arguments[0] as QuerySourceReferenceExpression; + propertyName1 = (methodCallExpression1.Arguments[1] as ConstantExpression)?.Value as string; + } + else if (expressionWithoutNullConditional1 is MemberExpression memberExpression1) + { + qsre1 = memberExpression1.Expression as QuerySourceReferenceExpression; + propertyName1 = memberExpression1.Member.Name; + } + + if (expressionWithoutNullConditional2 is MethodCallExpression methodCallExpression2 + && methodCallExpression2.IsEFProperty()) + { + qsre2 = methodCallExpression2.Arguments[0] as QuerySourceReferenceExpression; + propertyName2 = (methodCallExpression2.Arguments[1] as ConstantExpression)?.Value as string; + } + else if (expressionWithoutNullConditional2 is MemberExpression memberExpression2) + { + qsre2 = memberExpression2.Expression as QuerySourceReferenceExpression; + propertyName2 = memberExpression2.Member.Name; + } + + return qsre1?.ReferencedQuerySource == qsre2?.ReferencedQuerySource + && propertyName1 == propertyName2; + } + + private QuerySourceReferenceExpression CreateJoinToParentQuery( + QueryModel parentQueryModel, + QuerySourceReferenceExpression parentQuerySourceReferenceExpression, + Expression outerTargetExpression, + IForeignKey foreignKey, + QueryModel targetQueryModel, + List subQueryProjection) + { + var subQueryExpression = new SubQueryExpression(parentQueryModel); + var parentQuerySource = parentQuerySourceReferenceExpression.ReferencedQuerySource; + + var joinClause + = new JoinClause( + "_" + parentQuerySource.ItemName, + typeof(MaterializedAnonymousObject), + subQueryExpression, + outerTargetExpression.CreateKeyAccessExpression(foreignKey.Properties), + Expression.Constant(null)); + + var joinQuerySourceReferenceExpression = new QuerySourceReferenceExpression(joinClause); + var innerKeyExpressions = new List(); + + foreach (var principalKeyProperty in foreignKey.PrincipalKey.Properties) + { + var index = subQueryProjection.FindIndex( + e => + { + var expressionWithoutConvert = e.RemoveConvert(); + var projectionExpression = (expressionWithoutConvert as NullConditionalExpression)?.AccessOperation + ?? expressionWithoutConvert; + + if (projectionExpression is MethodCallExpression methodCall + && methodCall.Method.IsEFPropertyMethod()) + { + var propertyQsre = (QuerySourceReferenceExpression)methodCall.Arguments[0]; + var propertyName = (string)((ConstantExpression)methodCall.Arguments[1]).Value; + var propertyQsreEntityType = _queryCompilationContext.FindEntityType(propertyQsre.ReferencedQuerySource) + ?? _queryCompilationContext.Model.FindEntityType(propertyQsre.Type); + + return propertyQsreEntityType.RootType() == principalKeyProperty.DeclaringEntityType.RootType() + && propertyName == principalKeyProperty.Name; + } + + if (projectionExpression is MemberExpression projectionMemberExpression) + { + var projectionMemberQsre = (QuerySourceReferenceExpression)projectionMemberExpression.Expression; + var projectionMemberQsreEntityType = _queryCompilationContext.FindEntityType(projectionMemberQsre.ReferencedQuerySource) + ?? _queryCompilationContext.Model.FindEntityType(projectionMemberQsre.Type); + + return projectionMemberQsreEntityType.RootType() == principalKeyProperty.DeclaringEntityType.RootType() + && projectionMemberExpression.Member.Name == principalKeyProperty.Name; + } + + return false; + }); + + Debug.Assert(index != -1); + + innerKeyExpressions.Add( + Expression.Convert( + Expression.Call( + joinQuerySourceReferenceExpression, + MaterializedAnonymousObject.GetValueMethodInfo, + Expression.Constant(index)), + principalKeyProperty.ClrType.MakeNullable())); + + var propertyExpression + = parentQuerySourceReferenceExpression.CreateEFPropertyExpression(principalKeyProperty); + } + + joinClause.InnerKeySelector + = innerKeyExpressions.Count == 1 + ? innerKeyExpressions[0] + : Expression.New( + AnonymousObject.AnonymousObjectCtor, + Expression.NewArrayInit( + typeof(object), + innerKeyExpressions.Select(e => Expression.Convert(e, typeof(object))))); + + targetQueryModel.BodyClauses.Add(joinClause); + + return joinQuerySourceReferenceExpression; + } + + private static void ApplyParentOrderings( + IEnumerable parentOrderings, + QueryModel queryModel, + QuerySourceMapping querySourceMapping) + { + var orderByClause = queryModel.BodyClauses.OfType().LastOrDefault(); + + if (orderByClause == null) + { + queryModel.BodyClauses.Add(orderByClause = new OrderByClause()); + } + + // all exisiting order by clauses are guaranteed to be present in the parent ordering list, + // so we can safely remove them from the original order by clause + orderByClause.Orderings.Clear(); + + foreach (var ordering in parentOrderings) + { + var newExpression + = CloningExpressionVisitor + .AdjustExpressionAfterCloning(ordering.Expression, querySourceMapping); + + if (newExpression is MethodCallExpression methodCallExpression + && methodCallExpression.Method.IsEFPropertyMethod()) + { + newExpression + = new NullConditionalExpression( + methodCallExpression.Arguments[0], + methodCallExpression); + } + + orderByClause.Orderings + .Add(new Ordering(newExpression, ordering.OrderingDirection)); + } + } + + private static void LiftOrderBy( + IQuerySource querySource, + Expression targetExpression, + QueryModel fromQueryModel, + QueryModel toQueryModel, + List subQueryProjection) + { + foreach (var orderByClause + in fromQueryModel.BodyClauses.OfType().ToArray()) + { + var outerOrderByClause = new OrderByClause(); + for (var i = 0; i < orderByClause.Orderings.Count; i++) + { + var newExpression + = Expression.Call( + targetExpression, + MaterializedAnonymousObject.GetValueMethodInfo, + Expression.Constant(i)); + + outerOrderByClause.Orderings + .Add(new Ordering(newExpression, orderByClause.Orderings[i].OrderingDirection)); + } + + // after we lifted the orderings, we need to append the orderings that were applied to the query originally + // they should come after the ones that were lifted - we want to order by lifted properties first + var toQueryModelPreviousOrderByClause = toQueryModel.BodyClauses.OfType().LastOrDefault(); + if (toQueryModelPreviousOrderByClause != null) + { + foreach (var toQueryModelPreviousOrdering in toQueryModelPreviousOrderByClause.Orderings) + { + outerOrderByClause.Orderings.Add(toQueryModelPreviousOrdering); + } + + toQueryModel.BodyClauses.Remove(toQueryModelPreviousOrderByClause); + } + + toQueryModel.BodyClauses.Add(outerOrderByClause); + fromQueryModel.BodyClauses.Remove(orderByClause); + } + } + + private static List RemapOriginKeyExpressions( + IEnumerable originKeyExpressions, + QuerySourceReferenceExpression targetQsre, + List targetExpressions) + { + var remappedKeys = new List(); + + int projectionIndex; + foreach (var originKeyExpression in originKeyExpressions) + { + projectionIndex + = targetExpressions + .FindIndex( + e => AreEquivalentPropertyExpressions(e, originKeyExpression)); + + Debug.Assert(projectionIndex != -1); + + var remappedKey + = Expression.Call( + targetQsre, + MaterializedAnonymousObject.GetValueMethodInfo, + Expression.Constant(projectionIndex)); + + remappedKeys.Add(remappedKey); + } + + return remappedKeys; + } + } +} diff --git a/src/EFCore/Query/ExpressionVisitors/Internal/EntityEqualityRewritingExpressionVisitor.cs b/src/EFCore/Query/ExpressionVisitors/Internal/EntityEqualityRewritingExpressionVisitor.cs index f585ece1e27..582f38d929d 100644 --- a/src/EFCore/Query/ExpressionVisitors/Internal/EntityEqualityRewritingExpressionVisitor.cs +++ b/src/EFCore/Query/ExpressionVisitors/Internal/EntityEqualityRewritingExpressionVisitor.cs @@ -335,17 +335,7 @@ private static Expression CreateKeyAccessExpression( // If comparing with null then we need only first PK property return properties.Count == 1 || nullComparison ? target.CreateEFPropertyExpression(properties[0]) - : Expression.New( - AnonymousObject.AnonymousObjectCtor, - Expression.NewArrayInit( - typeof(object), - properties - .Select( - p => Expression.Convert( - target.CreateEFPropertyExpression(p), - typeof(object))) - .Cast() - .ToArray())); + : target.CreateKeyAccessExpression(properties); } } } diff --git a/src/EFCore/Query/ExpressionVisitors/Internal/NavigationRewritingExpressionVisitor.cs b/src/EFCore/Query/ExpressionVisitors/Internal/NavigationRewritingExpressionVisitor.cs index 2c6f21805a8..49670c724c6 100644 --- a/src/EFCore/Query/ExpressionVisitors/Internal/NavigationRewritingExpressionVisitor.cs +++ b/src/EFCore/Query/ExpressionVisitors/Internal/NavigationRewritingExpressionVisitor.cs @@ -224,6 +224,16 @@ public NavigationRewritingExpressionVisitor([NotNull] EntityQueryModelVisitor qu _navigationRewritingQueryModelVisitor = new NavigationRewritingQueryModelVisitor(this, _queryModelVisitor, navigationExpansionSubquery); } + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual void InjectSubqueryToCollectionsInProjection([NotNull] QueryModel queryModel) + { + var visitor = new ProjectionSubqueryInjectingQueryModelVisitor(_queryModelVisitor); + visitor.VisitQueryModel(queryModel); + } + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. @@ -569,16 +579,6 @@ protected override Expression VisitMethodCall(MethodCallExpression node) { Check.NotNull(node, nameof(node)); - if (node.Method.MethodIsClosedFormOf( - CollectionNavigationIncludeExpressionRewriter.ProjectCollectionNavigationMethodInfo)) - { - var newArgument = Visit(node.Arguments[0]); - - return newArgument != node.Arguments[0] - ? node.Update(node.Object, new[] { newArgument, node.Arguments[1] }) - : node; - } - if (node.Method.IsEFPropertyMethod()) { var result = _queryModelVisitor.BindNavigationPathPropertyExpression( @@ -1596,5 +1596,22 @@ private void VisitAndAdjustResultOperatorType( adjuster(resultOperator, translatedExpression); } } + + private class ProjectionSubqueryInjectingQueryModelVisitor : QueryModelVisitorBase + { + private readonly CollectionNavigationSubqueryInjector _subqueryInjector; + + public ProjectionSubqueryInjectingQueryModelVisitor(EntityQueryModelVisitor queryModelVisitor) + { + _subqueryInjector = new CollectionNavigationSubqueryInjector(queryModelVisitor, shouldInject: true); + } + + public override void VisitSelectClause(SelectClause selectClause, QueryModel queryModel) + { + selectClause.Selector = _subqueryInjector.Visit(selectClause.Selector); + + base.VisitSelectClause(selectClause, queryModel); + } + } } } diff --git a/src/EFCore/Query/ExpressionVisitors/Internal/QuerySourceReferenceFindingExpressionTreeVisitor.cs b/src/EFCore/Query/ExpressionVisitors/Internal/QuerySourceReferenceFindingExpressionTreeVisitor.cs new file mode 100644 index 00000000000..9614faf3ea2 --- /dev/null +++ b/src/EFCore/Query/ExpressionVisitors/Internal/QuerySourceReferenceFindingExpressionTreeVisitor.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq.Expressions; +using Remotion.Linq.Clauses.Expressions; +using Remotion.Linq.Parsing; + +namespace Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class QuerySourceReferenceFindingExpressionTreeVisitor : RelinqExpressionVisitor + { + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual QuerySourceReferenceExpression QuerySourceReferenceExpression { get; private set; } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override Expression VisitQuerySourceReference(QuerySourceReferenceExpression querySourceReferenceExpression) + { + if (QuerySourceReferenceExpression == null) + { + QuerySourceReferenceExpression = querySourceReferenceExpression; + } + + return querySourceReferenceExpression; + } + } +} diff --git a/src/EFCore/Query/Internal/ExpressionPrinter.cs b/src/EFCore/Query/Internal/ExpressionPrinter.cs index 816e5304988..997f53f590f 100644 --- a/src/EFCore/Query/Internal/ExpressionPrinter.cs +++ b/src/EFCore/Query/Internal/ExpressionPrinter.cs @@ -770,6 +770,10 @@ protected override Expression VisitExtension(Expression extensionExpression) VisitNullConditionalEqualExpression(nullConditionalEqualExpression); break; + case SubQueryExpression subqueryExpression: + VisitSubqueryExpression(subqueryExpression); + break; + default: UnhandledExpressionType(extensionExpression); break; @@ -823,6 +827,11 @@ private void VisitNullConditionalEqualExpression(NullConditionalEqualExpression Visit(nullConditionalEqualExpression.InnerKey); } + private void VisitSubqueryExpression(SubQueryExpression subqueryExpression) + { + _stringBuilder.Append(subqueryExpression.QueryModel.Print()); + } + private void VisitArguments(IList arguments, Action appendAction, string lastSeparator = "") { for (var i = 0; i < arguments.Count; i++) diff --git a/src/EFCore/Query/Internal/IQueryBuffer.cs b/src/EFCore/Query/Internal/IQueryBuffer.cs index 34b55927d6d..945c580fe8f 100644 --- a/src/EFCore/Query/Internal/IQueryBuffer.cs +++ b/src/EFCore/Query/Internal/IQueryBuffer.cs @@ -85,5 +85,16 @@ Task IncludeCollectionAsync( [CanBeNull] Func joinPredicate, CancellationToken cancellationToken) where TRelated : TElement; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + IEnumerable CorrelateSubquery( + int correlatedCollectionId, + [NotNull] INavigation navigation, + MaterializedAnonymousObject outerKey, + [NotNull] Func>> correlatedCollectionFactory, + [NotNull] Func correlationPredicate); } } diff --git a/src/EFCore/Query/Internal/IncludeCompiler.CollectionQueryModelRewritingExpressionVisitor.cs b/src/EFCore/Query/Internal/IncludeCompiler.CollectionQueryModelRewritingExpressionVisitor.cs index 7eeb405c729..97c7084c008 100644 --- a/src/EFCore/Query/Internal/IncludeCompiler.CollectionQueryModelRewritingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/IncludeCompiler.CollectionQueryModelRewritingExpressionVisitor.cs @@ -11,6 +11,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.Expressions.Internal; +using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal; using Remotion.Linq; using Remotion.Linq.Clauses; using Remotion.Linq.Clauses.Expressions; @@ -395,9 +396,7 @@ var joinClause "_" + parentQuerySource.ItemName, typeof(AnonymousObject), subQueryExpression, - CreateKeyAccessExpression( - outerTargetExpression, - foreignKey.Properties), + outerTargetExpression.CreateKeyAccessExpression(foreignKey.Properties), Expression.Constant(null)); var joinQuerySourceReferenceExpression = new QuerySourceReferenceExpression(joinClause); @@ -438,23 +437,6 @@ var propertyExpression return joinQuerySourceReferenceExpression; } - // TODO: Unify this with other versions - private static Expression CreateKeyAccessExpression(Expression target, IReadOnlyList properties) - => properties.Count == 1 - ? target.CreateEFPropertyExpression(properties[0]) - : Expression.New( - AnonymousObject.AnonymousObjectCtor, - Expression.NewArrayInit( - typeof(object), - properties - .Select( - p => - Expression.Convert( - target.CreateEFPropertyExpression(p), - typeof(object))) - .Cast() - .ToArray())); - private static void ApplyParentOrderings( IEnumerable parentOrderings, QueryModel queryModel, @@ -598,21 +580,6 @@ var newExpression } } - private class QuerySourceReferenceFindingExpressionTreeVisitor : RelinqExpressionVisitor - { - public QuerySourceReferenceExpression QuerySourceReferenceExpression { get; private set; } - - protected override Expression VisitQuerySourceReference(QuerySourceReferenceExpression querySourceReferenceExpression) - { - if (QuerySourceReferenceExpression == null) - { - QuerySourceReferenceExpression = querySourceReferenceExpression; - } - - return querySourceReferenceExpression; - } - } - protected override Expression VisitSubQuery(SubQueryExpression subQueryExpression) { subQueryExpression.QueryModel.TransformExpressions(Visit); diff --git a/src/EFCore/Query/Internal/MaterializedAnonymousObject.cs b/src/EFCore/Query/Internal/MaterializedAnonymousObject.cs new file mode 100644 index 00000000000..32262c2f9dd --- /dev/null +++ b/src/EFCore/Query/Internal/MaterializedAnonymousObject.cs @@ -0,0 +1,120 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using JetBrains.Annotations; +using Remotion.Linq.Clauses.Expressions; + +namespace Microsoft.EntityFrameworkCore.Query.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public struct MaterializedAnonymousObject + { + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static bool IsGetValueExpression( + [NotNull] MethodCallExpression methodCallExpression, + out QuerySourceReferenceExpression querySourceReferenceExpression) + { + querySourceReferenceExpression = null; + + if (methodCallExpression.Object?.Type == typeof(MaterializedAnonymousObject) + && methodCallExpression.Method.Equals(GetValueMethodInfo) + && methodCallExpression.Object is QuerySourceReferenceExpression qsre) + { + querySourceReferenceExpression = qsre; + + return true; + } + + return false; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static readonly ConstructorInfo AnonymousObjectCtor + = typeof(MaterializedAnonymousObject).GetTypeInfo() + .DeclaredConstructors + .Single(c => c.GetParameters().Length == 1); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static readonly MethodInfo GetValueMethodInfo + = typeof(MaterializedAnonymousObject).GetTypeInfo() + .GetDeclaredMethod(nameof(GetValue)); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static bool operator ==(MaterializedAnonymousObject x, MaterializedAnonymousObject y) => x.Equals(y); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static bool operator !=(MaterializedAnonymousObject x, MaterializedAnonymousObject y) => !x.Equals(y); + + private readonly object[] _values; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + [UsedImplicitly] + public MaterializedAnonymousObject([NotNull] object[] values) => _values = values; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public bool IsDefault() => _values == null; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is MaterializedAnonymousObject anonymousObject + && _values.SequenceEqual(anonymousObject._values); + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override int GetHashCode() + { + unchecked + { + return _values.Aggregate( + 0, + (current, argument) + => current + ((current * 397) ^ (argument?.GetHashCode() ?? 0))); + } + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public object GetValue(int index) => _values[index]; + } +} diff --git a/src/EFCore/Query/Internal/QueryBuffer.cs b/src/EFCore/Query/Internal/QueryBuffer.cs index 6a8dc526229..b4f8bf9bd4d 100644 --- a/src/EFCore/Query/Internal/QueryBuffer.cs +++ b/src/EFCore/Query/Internal/QueryBuffer.cs @@ -32,6 +32,23 @@ private readonly ConditionalWeakTable _valueBuffers private readonly Dictionary _includedCollections = new Dictionary(); // IDisposable as IEnumerable/IAsyncEnumerable + private Dictionary _correlatedCollectionMetadata + = new Dictionary(); + + private struct CorrelatedCollectionMetadataElement + { + public CorrelatedCollectionMetadataElement( + IDisposable enumerator, + MaterializedAnonymousObject previousOriginKey) + { + Enumerator = enumerator; + PreviousOriginKey = previousOriginKey; + } + + public IDisposable Enumerator { get; set; } + public MaterializedAnonymousObject PreviousOriginKey { get; set; } + } + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. @@ -473,12 +490,104 @@ private IWeakReferenceIdentityMap GetOrCreateIdentityMap(IKey key) return identityMap; } + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual IEnumerable CorrelateSubquery( + int correlatedCollectionId, + INavigation navigation, + MaterializedAnonymousObject outerKey, + Func>> correlatedCollectionFactory, + Func correlationPredicate) + { + IDisposable untypedEnumerator = null; + IEnumerator> enumerator = null; + + if (!_correlatedCollectionMetadata.TryGetValue(correlatedCollectionId, out var correlatedCollectionMetadataElement)) + { + enumerator = correlatedCollectionFactory().GetEnumerator(); + + if (!enumerator.MoveNext()) + { + enumerator.Dispose(); + enumerator = null; + } + + correlatedCollectionMetadataElement = new CorrelatedCollectionMetadataElement(enumerator, default); + _correlatedCollectionMetadata[correlatedCollectionId] = correlatedCollectionMetadataElement; + } + else + { + untypedEnumerator = correlatedCollectionMetadataElement.Enumerator; + } + + if (enumerator == null) + { + if (untypedEnumerator == null) + { + yield break; + } + + enumerator = (IEnumerator>)untypedEnumerator; + } + + while (true) + { + if (enumerator == null) + { + yield break; + } + + var shouldCorrelate = correlationPredicate(outerKey, enumerator.Current.Item2); + if (shouldCorrelate) + { + // if origin key changed, we need to yield break, even if the correlation predicate matches + // e.g. orders.Select(o => o.Customer.Addresses) - if there are 10 orders but only 5 customers, we still need 10 collections of addresses, even though some of the addresses belong to same customer + if (!correlatedCollectionMetadataElement.PreviousOriginKey.IsDefault() + && !enumerator.Current.Item3.Equals(correlatedCollectionMetadataElement.PreviousOriginKey)) + { + correlatedCollectionMetadataElement.PreviousOriginKey = default; + _correlatedCollectionMetadata[correlatedCollectionId] = correlatedCollectionMetadataElement; + + yield break; + } + + var result = enumerator.Current.Item1; + + correlatedCollectionMetadataElement.PreviousOriginKey = enumerator.Current.Item3; + _correlatedCollectionMetadata[correlatedCollectionId] = correlatedCollectionMetadataElement; + + if (!enumerator.MoveNext()) + { + enumerator.Dispose(); + enumerator = null; + _correlatedCollectionMetadata[correlatedCollectionId] = default; + } + + yield return result; + } + else + { + correlatedCollectionMetadataElement.PreviousOriginKey = default; + _correlatedCollectionMetadata[correlatedCollectionId] = correlatedCollectionMetadataElement; + + yield break; + } + } + } + void IDisposable.Dispose() { foreach (var kv in _includedCollections) { kv.Value?.Dispose(); } + + foreach (var kv in _correlatedCollectionMetadata) + { + kv.Value.Enumerator?.Dispose(); + } } } } diff --git a/src/EFCore/Query/QueryCompilationContext.cs b/src/EFCore/Query/QueryCompilationContext.cs index 8dfb4322a70..5f7e891a7be 100644 --- a/src/EFCore/Query/QueryCompilationContext.cs +++ b/src/EFCore/Query/QueryCompilationContext.cs @@ -63,6 +63,11 @@ public QueryCompilationContext( TrackQueryResults = trackQueryResults; } + /// + /// Mapping between correlated collection query modles and metadata needed to process them + /// + public virtual Dictionary CorrelatedSubqueryMetadataMap { get; } = new Dictionary(); + /// /// Gets the model. /// diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index 9d74faf2801..497f784d6e3 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -2989,11 +2989,11 @@ public override void Project_collection_navigation() base.Project_collection_navigation(); AssertSql( - @"SELECT [l1].[Id], [l1].[Date], [l1].[Name], [l1].[OneToMany_Optional_Self_InverseId], [l1].[OneToMany_Required_Self_InverseId], [l1].[OneToOne_Optional_SelfId] + @"SELECT [l1].[Id] FROM [LevelOne] AS [l1] ORDER BY [l1].[Id]", // - @"SELECT [l1.OneToMany_Optional].[Id], [l1.OneToMany_Optional].[Date], [l1.OneToMany_Optional].[Level1_Optional_Id], [l1.OneToMany_Optional].[Level1_Required_Id], [l1.OneToMany_Optional].[Name], [l1.OneToMany_Optional].[OneToMany_Optional_InverseId], [l1.OneToMany_Optional].[OneToMany_Optional_Self_InverseId], [l1.OneToMany_Optional].[OneToMany_Required_InverseId], [l1.OneToMany_Optional].[OneToMany_Required_Self_InverseId], [l1.OneToMany_Optional].[OneToOne_Optional_PK_InverseId], [l1.OneToMany_Optional].[OneToOne_Optional_SelfId] + @"SELECT [l1.OneToMany_Optional].[Id], [l1.OneToMany_Optional].[Date], [l1.OneToMany_Optional].[Level1_Optional_Id], [l1.OneToMany_Optional].[Level1_Required_Id], [l1.OneToMany_Optional].[Name], [l1.OneToMany_Optional].[OneToMany_Optional_InverseId], [l1.OneToMany_Optional].[OneToMany_Optional_Self_InverseId], [l1.OneToMany_Optional].[OneToMany_Required_InverseId], [l1.OneToMany_Optional].[OneToMany_Required_Self_InverseId], [l1.OneToMany_Optional].[OneToOne_Optional_PK_InverseId], [l1.OneToMany_Optional].[OneToOne_Optional_SelfId], [t].[Id] FROM [LevelTwo] AS [l1.OneToMany_Optional] INNER JOIN ( SELECT [l10].[Id] @@ -3007,19 +3007,19 @@ public override void Project_collection_navigation_nested() base.Project_collection_navigation_nested(); AssertSql( - @"SELECT [l1].[Id], [l1].[Date], [l1].[Name], [l1].[OneToMany_Optional_Self_InverseId], [l1].[OneToMany_Required_Self_InverseId], [l1].[OneToOne_Optional_SelfId], [l1.OneToOne_Optional_FK].[Id], [l1.OneToOne_Optional_FK].[Date], [l1.OneToOne_Optional_FK].[Level1_Optional_Id], [l1.OneToOne_Optional_FK].[Level1_Required_Id], [l1.OneToOne_Optional_FK].[Name], [l1.OneToOne_Optional_FK].[OneToMany_Optional_InverseId], [l1.OneToOne_Optional_FK].[OneToMany_Optional_Self_InverseId], [l1.OneToOne_Optional_FK].[OneToMany_Required_InverseId], [l1.OneToOne_Optional_FK].[OneToMany_Required_Self_InverseId], [l1.OneToOne_Optional_FK].[OneToOne_Optional_PK_InverseId], [l1.OneToOne_Optional_FK].[OneToOne_Optional_SelfId] + @"SELECT [l1.OneToOne_Optional_FK].[Id] FROM [LevelOne] AS [l1] LEFT JOIN [LevelTwo] AS [l1.OneToOne_Optional_FK] ON [l1].[Id] = [l1.OneToOne_Optional_FK].[Level1_Optional_Id] -ORDER BY [l1.OneToOne_Optional_FK].[Id]", +ORDER BY [l1].[Id], [l1.OneToOne_Optional_FK].[Id]", // - @"SELECT [l1.OneToOne_Optional_FK.OneToMany_Optional].[Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Level2_Optional_Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Level2_Required_Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Name], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_Self_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Required_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Required_Self_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToOne_Optional_PK_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToOne_Optional_SelfId] + @"SELECT [l1.OneToOne_Optional_FK.OneToMany_Optional].[Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Level2_Optional_Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Level2_Required_Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Name], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_Self_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Required_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Required_Self_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToOne_Optional_PK_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToOne_Optional_SelfId], [t].[Id], [t].[Id0] FROM [LevelThree] AS [l1.OneToOne_Optional_FK.OneToMany_Optional] INNER JOIN ( - SELECT DISTINCT [l1.OneToOne_Optional_FK0].[Id] + SELECT [l10].[Id], [l1.OneToOne_Optional_FK0].[Id] AS [Id0] FROM [LevelOne] AS [l10] LEFT JOIN [LevelTwo] AS [l1.OneToOne_Optional_FK0] ON [l10].[Id] = [l1.OneToOne_Optional_FK0].[Level1_Optional_Id] -) AS [t] ON [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_InverseId] = [t].[Id] -ORDER BY [t].[Id]"); +) AS [t] ON [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_InverseId] = [t].[Id0] +ORDER BY [t].[Id], [t].[Id0]"); } public override void Project_collection_navigation_using_ef_property() @@ -3027,19 +3027,19 @@ public override void Project_collection_navigation_using_ef_property() base.Project_collection_navigation_using_ef_property(); AssertSql( - @"SELECT [l1].[Id], [l1].[Date], [l1].[Name], [l1].[OneToMany_Optional_Self_InverseId], [l1].[OneToMany_Required_Self_InverseId], [l1].[OneToOne_Optional_SelfId], [l1.OneToOne_Optional_FK].[Id], [l1.OneToOne_Optional_FK].[Date], [l1.OneToOne_Optional_FK].[Level1_Optional_Id], [l1.OneToOne_Optional_FK].[Level1_Required_Id], [l1.OneToOne_Optional_FK].[Name], [l1.OneToOne_Optional_FK].[OneToMany_Optional_InverseId], [l1.OneToOne_Optional_FK].[OneToMany_Optional_Self_InverseId], [l1.OneToOne_Optional_FK].[OneToMany_Required_InverseId], [l1.OneToOne_Optional_FK].[OneToMany_Required_Self_InverseId], [l1.OneToOne_Optional_FK].[OneToOne_Optional_PK_InverseId], [l1.OneToOne_Optional_FK].[OneToOne_Optional_SelfId] + @"SELECT [l1.OneToOne_Optional_FK].[Id] FROM [LevelOne] AS [l1] LEFT JOIN [LevelTwo] AS [l1.OneToOne_Optional_FK] ON [l1].[Id] = [l1.OneToOne_Optional_FK].[Level1_Optional_Id] -ORDER BY [l1.OneToOne_Optional_FK].[Id]", +ORDER BY [l1].[Id], [l1.OneToOne_Optional_FK].[Id]", // - @"SELECT [l1.OneToOne_Optional_FK.OneToMany_Optional].[Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Level2_Optional_Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Level2_Required_Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Name], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_Self_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Required_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Required_Self_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToOne_Optional_PK_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToOne_Optional_SelfId] + @"SELECT [l1.OneToOne_Optional_FK.OneToMany_Optional].[Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Level2_Optional_Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Level2_Required_Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Name], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_Self_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Required_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Required_Self_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToOne_Optional_PK_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToOne_Optional_SelfId], [t].[Id], [t].[Id0] FROM [LevelThree] AS [l1.OneToOne_Optional_FK.OneToMany_Optional] INNER JOIN ( - SELECT DISTINCT [l1.OneToOne_Optional_FK0].[Id] + SELECT [l10].[Id], [l1.OneToOne_Optional_FK0].[Id] AS [Id0] FROM [LevelOne] AS [l10] LEFT JOIN [LevelTwo] AS [l1.OneToOne_Optional_FK0] ON [l10].[Id] = [l1.OneToOne_Optional_FK0].[Level1_Optional_Id] -) AS [t] ON [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_InverseId] = [t].[Id] -ORDER BY [t].[Id]"); +) AS [t] ON [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_InverseId] = [t].[Id0] +ORDER BY [t].[Id], [t].[Id0]"); } public override void Project_collection_navigation_nested_anonymous() @@ -3047,19 +3047,19 @@ public override void Project_collection_navigation_nested_anonymous() base.Project_collection_navigation_nested_anonymous(); AssertSql( - @"SELECT [l1].[Id] AS [Id0], [l1].[Date], [l1].[Name], [l1].[OneToMany_Optional_Self_InverseId], [l1].[OneToMany_Required_Self_InverseId], [l1].[OneToOne_Optional_SelfId], [l1.OneToOne_Optional_FK].[Id], [l1.OneToOne_Optional_FK].[Date], [l1.OneToOne_Optional_FK].[Level1_Optional_Id], [l1.OneToOne_Optional_FK].[Level1_Required_Id], [l1.OneToOne_Optional_FK].[Name], [l1.OneToOne_Optional_FK].[OneToMany_Optional_InverseId], [l1.OneToOne_Optional_FK].[OneToMany_Optional_Self_InverseId], [l1.OneToOne_Optional_FK].[OneToMany_Required_InverseId], [l1.OneToOne_Optional_FK].[OneToMany_Required_Self_InverseId], [l1.OneToOne_Optional_FK].[OneToOne_Optional_PK_InverseId], [l1.OneToOne_Optional_FK].[OneToOne_Optional_SelfId] + @"SELECT [l1].[Id] AS [Id0], [l1.OneToOne_Optional_FK].[Id] FROM [LevelOne] AS [l1] LEFT JOIN [LevelTwo] AS [l1.OneToOne_Optional_FK] ON [l1].[Id] = [l1.OneToOne_Optional_FK].[Level1_Optional_Id] -ORDER BY [l1.OneToOne_Optional_FK].[Id]", +ORDER BY [l1].[Id], [l1.OneToOne_Optional_FK].[Id]", // - @"SELECT [l1.OneToOne_Optional_FK.OneToMany_Optional].[Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Level2_Optional_Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Level2_Required_Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Name], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_Self_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Required_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Required_Self_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToOne_Optional_PK_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToOne_Optional_SelfId] + @"SELECT [l1.OneToOne_Optional_FK.OneToMany_Optional].[Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Level2_Optional_Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Level2_Required_Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Name], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_Self_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Required_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Required_Self_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToOne_Optional_PK_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToOne_Optional_SelfId], [t].[Id], [t].[Id0] FROM [LevelThree] AS [l1.OneToOne_Optional_FK.OneToMany_Optional] INNER JOIN ( - SELECT DISTINCT [l1.OneToOne_Optional_FK0].[Id] + SELECT [l10].[Id], [l1.OneToOne_Optional_FK0].[Id] AS [Id0] FROM [LevelOne] AS [l10] LEFT JOIN [LevelTwo] AS [l1.OneToOne_Optional_FK0] ON [l10].[Id] = [l1.OneToOne_Optional_FK0].[Level1_Optional_Id] -) AS [t] ON [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_InverseId] = [t].[Id] -ORDER BY [t].[Id]"); +) AS [t] ON [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_InverseId] = [t].[Id0] +ORDER BY [t].[Id], [t].[Id0]"); } public override void Project_collection_navigation_count() @@ -3083,19 +3083,18 @@ public override void Project_collection_navigation_composed() AssertSql( @"SELECT [l1].[Id] FROM [LevelOne] AS [l1] -WHERE [l1].[Id] < 3", - // - @"@_outer_Id='1' - -SELECT [l2].[Id], [l2].[Date], [l2].[Level1_Optional_Id], [l2].[Level1_Required_Id], [l2].[Name], [l2].[OneToMany_Optional_InverseId], [l2].[OneToMany_Optional_Self_InverseId], [l2].[OneToMany_Required_InverseId], [l2].[OneToMany_Required_Self_InverseId], [l2].[OneToOne_Optional_PK_InverseId], [l2].[OneToOne_Optional_SelfId] -FROM [LevelTwo] AS [l2] -WHERE (([l2].[Name] <> N'Foo') OR [l2].[Name] IS NULL) AND (@_outer_Id = [l2].[OneToMany_Optional_InverseId])", +WHERE [l1].[Id] < 3 +ORDER BY [l1].[Id]", // - @"@_outer_Id='2' - -SELECT [l2].[Id], [l2].[Date], [l2].[Level1_Optional_Id], [l2].[Level1_Required_Id], [l2].[Name], [l2].[OneToMany_Optional_InverseId], [l2].[OneToMany_Optional_Self_InverseId], [l2].[OneToMany_Required_InverseId], [l2].[OneToMany_Required_Self_InverseId], [l2].[OneToOne_Optional_PK_InverseId], [l2].[OneToOne_Optional_SelfId] -FROM [LevelTwo] AS [l2] -WHERE (([l2].[Name] <> N'Foo') OR [l2].[Name] IS NULL) AND (@_outer_Id = [l2].[OneToMany_Optional_InverseId])"); + @"SELECT [l1.OneToMany_Optional].[Id], [l1.OneToMany_Optional].[Date], [l1.OneToMany_Optional].[Level1_Optional_Id], [l1.OneToMany_Optional].[Level1_Required_Id], [l1.OneToMany_Optional].[Name], [l1.OneToMany_Optional].[OneToMany_Optional_InverseId], [l1.OneToMany_Optional].[OneToMany_Optional_Self_InverseId], [l1.OneToMany_Optional].[OneToMany_Required_InverseId], [l1.OneToMany_Optional].[OneToMany_Required_Self_InverseId], [l1.OneToMany_Optional].[OneToOne_Optional_PK_InverseId], [l1.OneToMany_Optional].[OneToOne_Optional_SelfId], [t].[Id] +FROM [LevelTwo] AS [l1.OneToMany_Optional] +INNER JOIN ( + SELECT [l10].[Id] + FROM [LevelOne] AS [l10] + WHERE [l10].[Id] < 3 +) AS [t] ON [l1.OneToMany_Optional].[OneToMany_Optional_InverseId] = [t].[Id] +WHERE ([l1.OneToMany_Optional].[Name] <> N'Foo') OR [l1.OneToMany_Optional].[Name] IS NULL +ORDER BY [t].[Id]"); } public override void Project_collection_and_root_entity() @@ -3107,7 +3106,7 @@ public override void Project_collection_and_root_entity() FROM [LevelOne] AS [l1] ORDER BY [l1].[Id]", // - @"SELECT [l1.OneToMany_Optional].[Id], [l1.OneToMany_Optional].[Date], [l1.OneToMany_Optional].[Level1_Optional_Id], [l1.OneToMany_Optional].[Level1_Required_Id], [l1.OneToMany_Optional].[Name], [l1.OneToMany_Optional].[OneToMany_Optional_InverseId], [l1.OneToMany_Optional].[OneToMany_Optional_Self_InverseId], [l1.OneToMany_Optional].[OneToMany_Required_InverseId], [l1.OneToMany_Optional].[OneToMany_Required_Self_InverseId], [l1.OneToMany_Optional].[OneToOne_Optional_PK_InverseId], [l1.OneToMany_Optional].[OneToOne_Optional_SelfId] + @"SELECT [l1.OneToMany_Optional].[Id], [l1.OneToMany_Optional].[Date], [l1.OneToMany_Optional].[Level1_Optional_Id], [l1.OneToMany_Optional].[Level1_Required_Id], [l1.OneToMany_Optional].[Name], [l1.OneToMany_Optional].[OneToMany_Optional_InverseId], [l1.OneToMany_Optional].[OneToMany_Optional_Self_InverseId], [l1.OneToMany_Optional].[OneToMany_Required_InverseId], [l1.OneToMany_Optional].[OneToMany_Required_Self_InverseId], [l1.OneToMany_Optional].[OneToOne_Optional_PK_InverseId], [l1.OneToMany_Optional].[OneToOne_Optional_SelfId], [t].[Id] FROM [LevelTwo] AS [l1.OneToMany_Optional] INNER JOIN ( SELECT [l10].[Id] @@ -3125,7 +3124,15 @@ public override void Project_collection_and_include() FROM [LevelOne] AS [l] ORDER BY [l].[Id]", // - @"SELECT [l.OneToMany_Optional].[Id], [l.OneToMany_Optional].[Date], [l.OneToMany_Optional].[Level1_Optional_Id], [l.OneToMany_Optional].[Level1_Required_Id], [l.OneToMany_Optional].[Name], [l.OneToMany_Optional].[OneToMany_Optional_InverseId], [l.OneToMany_Optional].[OneToMany_Optional_Self_InverseId], [l.OneToMany_Optional].[OneToMany_Required_InverseId], [l.OneToMany_Optional].[OneToMany_Required_Self_InverseId], [l.OneToMany_Optional].[OneToOne_Optional_PK_InverseId], [l.OneToMany_Optional].[OneToOne_Optional_SelfId] + @"SELECT [l.OneToMany_Optional0].[Id], [l.OneToMany_Optional0].[Date], [l.OneToMany_Optional0].[Level1_Optional_Id], [l.OneToMany_Optional0].[Level1_Required_Id], [l.OneToMany_Optional0].[Name], [l.OneToMany_Optional0].[OneToMany_Optional_InverseId], [l.OneToMany_Optional0].[OneToMany_Optional_Self_InverseId], [l.OneToMany_Optional0].[OneToMany_Required_InverseId], [l.OneToMany_Optional0].[OneToMany_Required_Self_InverseId], [l.OneToMany_Optional0].[OneToOne_Optional_PK_InverseId], [l.OneToMany_Optional0].[OneToOne_Optional_SelfId] +FROM [LevelTwo] AS [l.OneToMany_Optional0] +INNER JOIN ( + SELECT [l1].[Id] + FROM [LevelOne] AS [l1] +) AS [t0] ON [l.OneToMany_Optional0].[OneToMany_Optional_InverseId] = [t0].[Id] +ORDER BY [t0].[Id]", + // + @"SELECT [l.OneToMany_Optional].[Id], [l.OneToMany_Optional].[Date], [l.OneToMany_Optional].[Level1_Optional_Id], [l.OneToMany_Optional].[Level1_Required_Id], [l.OneToMany_Optional].[Name], [l.OneToMany_Optional].[OneToMany_Optional_InverseId], [l.OneToMany_Optional].[OneToMany_Optional_Self_InverseId], [l.OneToMany_Optional].[OneToMany_Required_InverseId], [l.OneToMany_Optional].[OneToMany_Required_Self_InverseId], [l.OneToMany_Optional].[OneToOne_Optional_PK_InverseId], [l.OneToMany_Optional].[OneToOne_Optional_SelfId], [t].[Id] FROM [LevelTwo] AS [l.OneToMany_Optional] INNER JOIN ( SELECT [l0].[Id] @@ -3139,19 +3146,19 @@ public override void Project_navigation_and_collection() base.Project_navigation_and_collection(); AssertSql( - @"SELECT [l1].[Id], [l1].[Date], [l1].[Name], [l1].[OneToMany_Optional_Self_InverseId], [l1].[OneToMany_Required_Self_InverseId], [l1].[OneToOne_Optional_SelfId], [l1.OneToOne_Optional_FK].[Id], [l1.OneToOne_Optional_FK].[Date], [l1.OneToOne_Optional_FK].[Level1_Optional_Id], [l1.OneToOne_Optional_FK].[Level1_Required_Id], [l1.OneToOne_Optional_FK].[Name], [l1.OneToOne_Optional_FK].[OneToMany_Optional_InverseId], [l1.OneToOne_Optional_FK].[OneToMany_Optional_Self_InverseId], [l1.OneToOne_Optional_FK].[OneToMany_Required_InverseId], [l1.OneToOne_Optional_FK].[OneToMany_Required_Self_InverseId], [l1.OneToOne_Optional_FK].[OneToOne_Optional_PK_InverseId], [l1.OneToOne_Optional_FK].[OneToOne_Optional_SelfId] + @"SELECT [l1.OneToOne_Optional_FK].[Id], [l1.OneToOne_Optional_FK].[Date], [l1.OneToOne_Optional_FK].[Level1_Optional_Id], [l1.OneToOne_Optional_FK].[Level1_Required_Id], [l1.OneToOne_Optional_FK].[Name], [l1.OneToOne_Optional_FK].[OneToMany_Optional_InverseId], [l1.OneToOne_Optional_FK].[OneToMany_Optional_Self_InverseId], [l1.OneToOne_Optional_FK].[OneToMany_Required_InverseId], [l1.OneToOne_Optional_FK].[OneToMany_Required_Self_InverseId], [l1.OneToOne_Optional_FK].[OneToOne_Optional_PK_InverseId], [l1.OneToOne_Optional_FK].[OneToOne_Optional_SelfId] FROM [LevelOne] AS [l1] LEFT JOIN [LevelTwo] AS [l1.OneToOne_Optional_FK] ON [l1].[Id] = [l1.OneToOne_Optional_FK].[Level1_Optional_Id] -ORDER BY [l1.OneToOne_Optional_FK].[Id]", +ORDER BY [l1].[Id], [l1.OneToOne_Optional_FK].[Id]", // - @"SELECT [l1.OneToOne_Optional_FK.OneToMany_Optional].[Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Level2_Optional_Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Level2_Required_Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Name], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_Self_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Required_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Required_Self_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToOne_Optional_PK_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToOne_Optional_SelfId] + @"SELECT [l1.OneToOne_Optional_FK.OneToMany_Optional].[Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Level2_Optional_Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Level2_Required_Id], [l1.OneToOne_Optional_FK.OneToMany_Optional].[Name], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_Self_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Required_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Required_Self_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToOne_Optional_PK_InverseId], [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToOne_Optional_SelfId], [t].[Id], [t].[Id0] FROM [LevelThree] AS [l1.OneToOne_Optional_FK.OneToMany_Optional] INNER JOIN ( - SELECT DISTINCT [l1.OneToOne_Optional_FK0].[Id] + SELECT [l10].[Id], [l1.OneToOne_Optional_FK0].[Id] AS [Id0] FROM [LevelOne] AS [l10] LEFT JOIN [LevelTwo] AS [l1.OneToOne_Optional_FK0] ON [l10].[Id] = [l1.OneToOne_Optional_FK0].[Level1_Optional_Id] -) AS [t] ON [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_InverseId] = [t].[Id] -ORDER BY [t].[Id]"); +) AS [t] ON [l1.OneToOne_Optional_FK.OneToMany_Optional].[OneToMany_Optional_InverseId] = [t].[Id0] +ORDER BY [t].[Id], [t].[Id0]"); } public override void Include_inside_subquery() diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index 8003f5fe954..9dc6e430bc3 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -2655,19 +2655,17 @@ public override void Select_correlated_filtered_collection() @"SELECT [g].[FullName] FROM [Gears] AS [g] WHERE [g].[Discriminator] IN (N'Officer', N'Gear') AND [g].[CityOrBirthName] IN (N'Ephyra', N'Hanover') -ORDER BY [g].[Nickname]", - // - @"@_outer_FullName='Augustus Cole' (Size = 450) - -SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] -FROM [Weapons] AS [w] -WHERE (([w].[Name] <> N'Lancer') OR [w].[Name] IS NULL) AND (@_outer_FullName = [w].[OwnerFullName])", +ORDER BY [g].[Nickname], [g].[SquadId], [g].[FullName]", // - @"@_outer_FullName='Dominic Santiago' (Size = 450) - -SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] -FROM [Weapons] AS [w] -WHERE (([w].[Name] <> N'Lancer') OR [w].[Name] IS NULL) AND (@_outer_FullName = [w].[OwnerFullName])"); + @"SELECT [g.Weapons].[Id], [g.Weapons].[AmmunitionType], [g.Weapons].[IsAutomatic], [g.Weapons].[Name], [g.Weapons].[OwnerFullName], [g.Weapons].[SynergyWithId], [t].[Nickname], [t].[SquadId], [t].[FullName] +FROM [Weapons] AS [g.Weapons] +INNER JOIN ( + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[FullName] + FROM [Gears] AS [g0] + WHERE [g0].[Discriminator] IN (N'Officer', N'Gear') AND [g0].[CityOrBirthName] IN (N'Ephyra', N'Hanover') +) AS [t] ON [g.Weapons].[OwnerFullName] = [t].[FullName] +WHERE ([g.Weapons].[Name] <> N'Lancer') OR [g.Weapons].[Name] IS NULL +ORDER BY [t].[Nickname], [t].[SquadId], [t].[FullName]"); } public override void Select_correlated_filtered_collection_with_composite_key() @@ -2678,21 +2676,17 @@ public override void Select_correlated_filtered_collection_with_composite_key() @"SELECT [g].[Nickname], [g].[SquadId] FROM [Gears] AS [g] WHERE [g].[Discriminator] = N'Officer' -ORDER BY [g].[Nickname]", - // - @"@_outer_Nickname='Baird' (Size = 450) -@_outer_SquadId='1' - -SELECT [r].[Nickname], [r].[SquadId], [r].[AssignedCityName], [r].[CityOrBirthName], [r].[Discriminator], [r].[FullName], [r].[HasSoulPatch], [r].[LeaderNickname], [r].[LeaderSquadId], [r].[Rank] -FROM [Gears] AS [r] -WHERE ([r].[Discriminator] IN (N'Officer', N'Gear') AND ([r].[Nickname] <> N'Dom')) AND ((@_outer_Nickname = [r].[LeaderNickname]) AND (@_outer_SquadId = [r].[LeaderSquadId]))", +ORDER BY [g].[Nickname], [g].[SquadId]", // - @"@_outer_Nickname='Marcus' (Size = 450) -@_outer_SquadId='1' - -SELECT [r].[Nickname], [r].[SquadId], [r].[AssignedCityName], [r].[CityOrBirthName], [r].[Discriminator], [r].[FullName], [r].[HasSoulPatch], [r].[LeaderNickname], [r].[LeaderSquadId], [r].[Rank] -FROM [Gears] AS [r] -WHERE ([r].[Discriminator] IN (N'Officer', N'Gear') AND ([r].[Nickname] <> N'Dom')) AND ((@_outer_Nickname = [r].[LeaderNickname]) AND (@_outer_SquadId = [r].[LeaderSquadId]))"); + @"SELECT [g.Reports].[Nickname], [g.Reports].[SquadId], [g.Reports].[AssignedCityName], [g.Reports].[CityOrBirthName], [g.Reports].[Discriminator], [g.Reports].[FullName], [g.Reports].[HasSoulPatch], [g.Reports].[LeaderNickname], [g.Reports].[LeaderSquadId], [g.Reports].[Rank], [t].[Nickname], [t].[SquadId] +FROM [Gears] AS [g.Reports] +INNER JOIN ( + SELECT [g0].[Nickname], [g0].[SquadId] + FROM [Gears] AS [g0] + WHERE [g0].[Discriminator] = N'Officer' +) AS [t] ON ([g.Reports].[LeaderNickname] = [t].[Nickname]) AND ([g.Reports].[LeaderSquadId] = [t].[SquadId]) +WHERE [g.Reports].[Discriminator] IN (N'Officer', N'Gear') AND ([g.Reports].[Nickname] <> N'Dom') +ORDER BY [t].[Nickname], [t].[SquadId]"); } public override void Select_correlated_filtered_collection_works_with_caching() @@ -3918,7 +3912,7 @@ public override void Project_collection_navigation_with_inheritance1() base.Project_collection_navigation_with_inheritance1(); AssertSql( - @"SELECT [h].[Id] AS [Id0], [h].[CapitalName], [h].[Discriminator], [h].[Name], [h].[CommanderName], [h].[Eradicated], [t].[Name], [t].[Discriminator], [t].[LocustHordeId], [t].[ThreatLevel], [t].[DefeatedByNickname], [t].[DefeatedBySquadId], [t0].[Id], [t0].[CapitalName], [t0].[Discriminator], [t0].[Name], [t0].[CommanderName], [t0].[Eradicated] + @"SELECT [h].[Id] AS [Id0], [t0].[Id] FROM [Factions] AS [h] LEFT JOIN ( SELECT [h.Commander].* @@ -3931,12 +3925,12 @@ FROM [Factions] AS [h.Commander.CommandingFaction] WHERE [h.Commander.CommandingFaction].[Discriminator] = N'LocustHorde' ) AS [t0] ON [t].[Name] = [t0].[CommanderName] WHERE [h].[Discriminator] = N'LocustHorde' -ORDER BY [t0].[Id]", +ORDER BY [h].[Id], [t0].[Id]", // - @"SELECT [h.Commander.CommandingFaction.Leaders].[Name], [h.Commander.CommandingFaction.Leaders].[Discriminator], [h.Commander.CommandingFaction.Leaders].[LocustHordeId], [h.Commander.CommandingFaction.Leaders].[ThreatLevel], [h.Commander.CommandingFaction.Leaders].[DefeatedByNickname], [h.Commander.CommandingFaction.Leaders].[DefeatedBySquadId] + @"SELECT [h.Commander.CommandingFaction.Leaders].[Name], [h.Commander.CommandingFaction.Leaders].[Discriminator], [h.Commander.CommandingFaction.Leaders].[LocustHordeId], [h.Commander.CommandingFaction.Leaders].[ThreatLevel], [h.Commander.CommandingFaction.Leaders].[DefeatedByNickname], [h.Commander.CommandingFaction.Leaders].[DefeatedBySquadId], [t3].[Id], [t3].[Id0] FROM [LocustLeaders] AS [h.Commander.CommandingFaction.Leaders] INNER JOIN ( - SELECT DISTINCT [t2].[Id] + SELECT [h0].[Id], [t2].[Id] AS [Id0] FROM [Factions] AS [h0] LEFT JOIN ( SELECT [h.Commander0].* @@ -3951,7 +3945,7 @@ FROM [Factions] AS [h.Commander.CommandingFaction0] WHERE [h0].[Discriminator] = N'LocustHorde' ) AS [t3] ON [h.Commander.CommandingFaction.Leaders].[LocustHordeId] = [t3].[Id] WHERE [h.Commander.CommandingFaction.Leaders].[Discriminator] IN (N'LocustCommander', N'LocustLeader') -ORDER BY [t3].[Id]"); +ORDER BY [t3].[Id], [t3].[Id0]"); } public override void Project_collection_navigation_with_inheritance2() @@ -3959,7 +3953,7 @@ public override void Project_collection_navigation_with_inheritance2() base.Project_collection_navigation_with_inheritance2(); AssertSql( - @"SELECT [h].[Id], [h].[CapitalName], [h].[Discriminator], [h].[Name], [h].[CommanderName], [h].[Eradicated], [t].[Name], [t].[Discriminator], [t].[LocustHordeId], [t].[ThreatLevel], [t].[DefeatedByNickname], [t].[DefeatedBySquadId], [t0].[Nickname], [t0].[SquadId], [t0].[AssignedCityName], [t0].[CityOrBirthName], [t0].[Discriminator], [t0].[FullName], [t0].[HasSoulPatch], [t0].[LeaderNickname], [t0].[LeaderSquadId], [t0].[Rank] + @"SELECT [h].[Id], [t0].[Nickname], [t0].[SquadId] FROM [Factions] AS [h] LEFT JOIN ( SELECT [h.Commander].* @@ -3972,12 +3966,12 @@ FROM [Gears] AS [h.Commander.DefeatedBy] WHERE [h.Commander.DefeatedBy].[Discriminator] IN (N'Officer', N'Gear') ) AS [t0] ON ([t].[DefeatedByNickname] = [t0].[Nickname]) AND ([t].[DefeatedBySquadId] = [t0].[SquadId]) WHERE [h].[Discriminator] = N'LocustHorde' -ORDER BY [t0].[Nickname], [t0].[SquadId]", +ORDER BY [h].[Id], [t0].[Nickname], [t0].[SquadId]", // - @"SELECT [h.Commander.DefeatedBy.Reports].[Nickname], [h.Commander.DefeatedBy.Reports].[SquadId], [h.Commander.DefeatedBy.Reports].[AssignedCityName], [h.Commander.DefeatedBy.Reports].[CityOrBirthName], [h.Commander.DefeatedBy.Reports].[Discriminator], [h.Commander.DefeatedBy.Reports].[FullName], [h.Commander.DefeatedBy.Reports].[HasSoulPatch], [h.Commander.DefeatedBy.Reports].[LeaderNickname], [h.Commander.DefeatedBy.Reports].[LeaderSquadId], [h.Commander.DefeatedBy.Reports].[Rank] + @"SELECT [h.Commander.DefeatedBy.Reports].[Nickname], [h.Commander.DefeatedBy.Reports].[SquadId], [h.Commander.DefeatedBy.Reports].[AssignedCityName], [h.Commander.DefeatedBy.Reports].[CityOrBirthName], [h.Commander.DefeatedBy.Reports].[Discriminator], [h.Commander.DefeatedBy.Reports].[FullName], [h.Commander.DefeatedBy.Reports].[HasSoulPatch], [h.Commander.DefeatedBy.Reports].[LeaderNickname], [h.Commander.DefeatedBy.Reports].[LeaderSquadId], [h.Commander.DefeatedBy.Reports].[Rank], [t3].[Id], [t3].[Nickname], [t3].[SquadId] FROM [Gears] AS [h.Commander.DefeatedBy.Reports] INNER JOIN ( - SELECT DISTINCT [t2].[Nickname], [t2].[SquadId] + SELECT [h0].[Id], [t2].[Nickname], [t2].[SquadId] FROM [Factions] AS [h0] LEFT JOIN ( SELECT [h.Commander0].* @@ -3992,7 +3986,7 @@ WHERE [h.Commander.DefeatedBy0].[Discriminator] IN (N'Officer', N'Gear') WHERE [h0].[Discriminator] = N'LocustHorde' ) AS [t3] ON ([h.Commander.DefeatedBy.Reports].[LeaderNickname] = [t3].[Nickname]) AND ([h.Commander.DefeatedBy.Reports].[LeaderSquadId] = [t3].[SquadId]) WHERE [h.Commander.DefeatedBy.Reports].[Discriminator] IN (N'Officer', N'Gear') -ORDER BY [t3].[Nickname], [t3].[SquadId]"); +ORDER BY [t3].[Id], [t3].[Nickname], [t3].[SquadId]"); } public override void Project_collection_navigation_with_inheritance3() @@ -4000,7 +3994,7 @@ public override void Project_collection_navigation_with_inheritance3() base.Project_collection_navigation_with_inheritance3(); AssertSql( - @"SELECT [f].[Id], [f].[CapitalName], [f].[Discriminator], [f].[Name], [f].[CommanderName], [f].[Eradicated], [t].[Name], [t].[Discriminator], [t].[LocustHordeId], [t].[ThreatLevel], [t].[DefeatedByNickname], [t].[DefeatedBySquadId], [t0].[Nickname], [t0].[SquadId], [t0].[AssignedCityName], [t0].[CityOrBirthName], [t0].[Discriminator], [t0].[FullName], [t0].[HasSoulPatch], [t0].[LeaderNickname], [t0].[LeaderSquadId], [t0].[Rank] + @"SELECT [f].[Id], [t0].[Nickname], [t0].[SquadId] FROM [Factions] AS [f] LEFT JOIN ( SELECT [f.Commander].* @@ -4016,12 +4010,12 @@ FROM [Gears] AS [f.Commander.DefeatedBy] WHERE [f.Commander.DefeatedBy].[Discriminator] IN (N'Officer', N'Gear') ) AS [t0] ON ([t].[DefeatedByNickname] = [t0].[Nickname]) AND ([t].[DefeatedBySquadId] = [t0].[SquadId]) WHERE ([f].[Discriminator] = N'LocustHorde') AND ([f].[Discriminator] = N'LocustHorde') -ORDER BY [t0].[Nickname], [t0].[SquadId]", +ORDER BY [f].[Id], [t0].[Nickname], [t0].[SquadId]", // - @"SELECT [f.Commander.DefeatedBy.Reports].[Nickname], [f.Commander.DefeatedBy.Reports].[SquadId], [f.Commander.DefeatedBy.Reports].[AssignedCityName], [f.Commander.DefeatedBy.Reports].[CityOrBirthName], [f.Commander.DefeatedBy.Reports].[Discriminator], [f.Commander.DefeatedBy.Reports].[FullName], [f.Commander.DefeatedBy.Reports].[HasSoulPatch], [f.Commander.DefeatedBy.Reports].[LeaderNickname], [f.Commander.DefeatedBy.Reports].[LeaderSquadId], [f.Commander.DefeatedBy.Reports].[Rank] + @"SELECT [f.Commander.DefeatedBy.Reports].[Nickname], [f.Commander.DefeatedBy.Reports].[SquadId], [f.Commander.DefeatedBy.Reports].[AssignedCityName], [f.Commander.DefeatedBy.Reports].[CityOrBirthName], [f.Commander.DefeatedBy.Reports].[Discriminator], [f.Commander.DefeatedBy.Reports].[FullName], [f.Commander.DefeatedBy.Reports].[HasSoulPatch], [f.Commander.DefeatedBy.Reports].[LeaderNickname], [f.Commander.DefeatedBy.Reports].[LeaderSquadId], [f.Commander.DefeatedBy.Reports].[Rank], [t3].[Id], [t3].[Nickname], [t3].[SquadId] FROM [Gears] AS [f.Commander.DefeatedBy.Reports] INNER JOIN ( - SELECT DISTINCT [t2].[Nickname], [t2].[SquadId] + SELECT [f0].[Id], [t2].[Nickname], [t2].[SquadId] FROM [Factions] AS [f0] LEFT JOIN ( SELECT [f.Commander0].* @@ -4039,7 +4033,7 @@ WHERE [f.Commander.DefeatedBy0].[Discriminator] IN (N'Officer', N'Gear') WHERE ([f0].[Discriminator] = N'LocustHorde') AND ([f0].[Discriminator] = N'LocustHorde') ) AS [t3] ON ([f.Commander.DefeatedBy.Reports].[LeaderNickname] = [t3].[Nickname]) AND ([f.Commander.DefeatedBy.Reports].[LeaderSquadId] = [t3].[SquadId]) WHERE [f.Commander.DefeatedBy.Reports].[Discriminator] IN (N'Officer', N'Gear') -ORDER BY [t3].[Nickname], [t3].[SquadId]"); +ORDER BY [t3].[Id], [t3].[Nickname], [t3].[SquadId]"); } public override void Include_reference_on_derived_type_using_string() @@ -4532,6 +4526,1242 @@ WHERE [g].[Discriminator] IN (N'Officer', N'Gear') ORDER BY [g].[SquadId], [g].[Nickname]"); } + public override void Correlated_collections_basic_projection() + { + base.Correlated_collections_basic_projection(); + + AssertSql( + @"SELECT [g].[FullName] +FROM [Gears] AS [g] +WHERE [g].[Discriminator] IN (N'Officer', N'Gear') AND ([g].[Nickname] <> N'Marcus') +ORDER BY [g].[Nickname], [g].[SquadId], [g].[FullName]", + // + @"SELECT [g.Weapons].[Id], [g.Weapons].[AmmunitionType], [g.Weapons].[IsAutomatic], [g.Weapons].[Name], [g.Weapons].[OwnerFullName], [g.Weapons].[SynergyWithId], [t].[Nickname], [t].[SquadId], [t].[FullName] +FROM [Weapons] AS [g.Weapons] +INNER JOIN ( + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[FullName] + FROM [Gears] AS [g0] + WHERE [g0].[Discriminator] IN (N'Officer', N'Gear') AND ([g0].[Nickname] <> N'Marcus') +) AS [t] ON [g.Weapons].[OwnerFullName] = [t].[FullName] +WHERE ([g.Weapons].[IsAutomatic] = 1) OR (([g.Weapons].[Name] <> N'foo') OR [g.Weapons].[Name] IS NULL) +ORDER BY [t].[Nickname], [t].[SquadId], [t].[FullName]"); + } + + public override void Correlated_collections_basic_projection_explicit_to_list() + { + base.Correlated_collections_basic_projection_explicit_to_list(); + + AssertSql( + @"SELECT [g].[FullName] +FROM [Gears] AS [g] +WHERE [g].[Discriminator] IN (N'Officer', N'Gear') AND ([g].[Nickname] <> N'Marcus') +ORDER BY [g].[Nickname], [g].[SquadId], [g].[FullName]", + // + @"SELECT [g.Weapons].[Id], [g.Weapons].[AmmunitionType], [g.Weapons].[IsAutomatic], [g.Weapons].[Name], [g.Weapons].[OwnerFullName], [g.Weapons].[SynergyWithId], [t].[Nickname], [t].[SquadId], [t].[FullName] +FROM [Weapons] AS [g.Weapons] +INNER JOIN ( + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[FullName] + FROM [Gears] AS [g0] + WHERE [g0].[Discriminator] IN (N'Officer', N'Gear') AND ([g0].[Nickname] <> N'Marcus') +) AS [t] ON [g.Weapons].[OwnerFullName] = [t].[FullName] +WHERE ([g.Weapons].[IsAutomatic] = 1) OR (([g.Weapons].[Name] <> N'foo') OR [g.Weapons].[Name] IS NULL) +ORDER BY [t].[Nickname], [t].[SquadId], [t].[FullName]"); + } + + public override void Correlated_collections_basic_projection_explicit_to_array() + { + base.Correlated_collections_basic_projection_explicit_to_array(); + + AssertSql( + @"SELECT [g].[FullName] +FROM [Gears] AS [g] +WHERE [g].[Discriminator] IN (N'Officer', N'Gear') AND ([g].[Nickname] <> N'Marcus') +ORDER BY [g].[Nickname], [g].[SquadId], [g].[FullName]", + // + @"SELECT [g.Weapons].[Id], [g.Weapons].[AmmunitionType], [g.Weapons].[IsAutomatic], [g.Weapons].[Name], [g.Weapons].[OwnerFullName], [g.Weapons].[SynergyWithId], [t].[Nickname], [t].[SquadId], [t].[FullName] +FROM [Weapons] AS [g.Weapons] +INNER JOIN ( + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[FullName] + FROM [Gears] AS [g0] + WHERE [g0].[Discriminator] IN (N'Officer', N'Gear') AND ([g0].[Nickname] <> N'Marcus') +) AS [t] ON [g.Weapons].[OwnerFullName] = [t].[FullName] +WHERE ([g.Weapons].[IsAutomatic] = 1) OR (([g.Weapons].[Name] <> N'foo') OR [g.Weapons].[Name] IS NULL) +ORDER BY [t].[Nickname], [t].[SquadId], [t].[FullName]"); + } + + public override void Correlated_collections_basic_projection_ordered() + { + base.Correlated_collections_basic_projection_ordered(); + + AssertSql( + @"SELECT [g].[FullName] +FROM [Gears] AS [g] +WHERE [g].[Discriminator] IN (N'Officer', N'Gear') AND ([g].[Nickname] <> N'Marcus') +ORDER BY [g].[Nickname], [g].[SquadId], [g].[FullName]", + // + @"SELECT [g.Weapons].[Id], [g.Weapons].[AmmunitionType], [g.Weapons].[IsAutomatic], [g.Weapons].[Name], [g.Weapons].[OwnerFullName], [g.Weapons].[SynergyWithId], [t].[Nickname], [t].[SquadId], [t].[FullName] +FROM [Weapons] AS [g.Weapons] +INNER JOIN ( + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[FullName] + FROM [Gears] AS [g0] + WHERE [g0].[Discriminator] IN (N'Officer', N'Gear') AND ([g0].[Nickname] <> N'Marcus') +) AS [t] ON [g.Weapons].[OwnerFullName] = [t].[FullName] +WHERE ([g.Weapons].[IsAutomatic] = 1) OR (([g.Weapons].[Name] <> N'foo') OR [g.Weapons].[Name] IS NULL) +ORDER BY [t].[Nickname], [t].[SquadId], [t].[FullName], [g.Weapons].[Name] DESC"); + } + + public override void Correlated_collections_basic_projection_composite_key() + { + base.Correlated_collections_basic_projection_composite_key(); + + AssertSql( + @"SELECT [o].[Nickname], [o].[SquadId] +FROM [Gears] AS [o] +WHERE ([o].[Discriminator] = N'Officer') AND ([o].[Nickname] <> N'Foo') +ORDER BY [o].[Nickname], [o].[SquadId]", + // + @"SELECT [t].[Nickname], [t].[SquadId], [o.Reports].[Nickname] AS [Nickname0], [o.Reports].[FullName], [o.Reports].[LeaderNickname], [o.Reports].[LeaderSquadId] +FROM [Gears] AS [o.Reports] +INNER JOIN ( + SELECT [o0].[Nickname], [o0].[SquadId] + FROM [Gears] AS [o0] + WHERE ([o0].[Discriminator] = N'Officer') AND ([o0].[Nickname] <> N'Foo') +) AS [t] ON ([o.Reports].[LeaderNickname] = [t].[Nickname]) AND ([o.Reports].[LeaderSquadId] = [t].[SquadId]) +WHERE [o.Reports].[Discriminator] IN (N'Officer', N'Gear') AND ([o.Reports].[HasSoulPatch] = 0) +ORDER BY [t].[Nickname], [t].[SquadId]"); + } + + public override void Correlated_collections_basic_projecting_single_property() + { + base.Correlated_collections_basic_projecting_single_property(); + + AssertSql( + @"SELECT [g].[FullName] +FROM [Gears] AS [g] +WHERE [g].[Discriminator] IN (N'Officer', N'Gear') AND ([g].[Nickname] <> N'Marcus') +ORDER BY [g].[Nickname], [g].[SquadId], [g].[FullName]", + // + @"SELECT [t].[Nickname], [t].[SquadId], [t].[FullName], [g.Weapons].[Name], [g.Weapons].[OwnerFullName] +FROM [Weapons] AS [g.Weapons] +INNER JOIN ( + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[FullName] + FROM [Gears] AS [g0] + WHERE [g0].[Discriminator] IN (N'Officer', N'Gear') AND ([g0].[Nickname] <> N'Marcus') +) AS [t] ON [g.Weapons].[OwnerFullName] = [t].[FullName] +WHERE ([g.Weapons].[IsAutomatic] = 1) OR (([g.Weapons].[Name] <> N'foo') OR [g.Weapons].[Name] IS NULL) +ORDER BY [t].[Nickname], [t].[SquadId], [t].[FullName]"); + } + + public override void Correlated_collections_basic_projecting_constant() + { + base.Correlated_collections_basic_projecting_constant(); + + AssertSql( + @"SELECT [g].[FullName] +FROM [Gears] AS [g] +WHERE [g].[Discriminator] IN (N'Officer', N'Gear') AND ([g].[Nickname] <> N'Marcus') +ORDER BY [g].[Nickname], [g].[SquadId], [g].[FullName]", + // + @"SELECT [t].[Nickname], [t].[SquadId], [t].[FullName], [g.Weapons].[OwnerFullName] +FROM [Weapons] AS [g.Weapons] +INNER JOIN ( + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[FullName] + FROM [Gears] AS [g0] + WHERE [g0].[Discriminator] IN (N'Officer', N'Gear') AND ([g0].[Nickname] <> N'Marcus') +) AS [t] ON [g.Weapons].[OwnerFullName] = [t].[FullName] +WHERE ([g.Weapons].[IsAutomatic] = 1) OR (([g.Weapons].[Name] <> N'foo') OR [g.Weapons].[Name] IS NULL) +ORDER BY [t].[Nickname], [t].[SquadId], [t].[FullName]"); + } + + public override void Correlated_collections_projection_of_collection_thru_navigation() + { + base.Correlated_collections_projection_of_collection_thru_navigation(); + + AssertSql( + @"SELECT [g.Squad].[Id] +FROM [Gears] AS [g] +INNER JOIN [Squads] AS [g.Squad] ON [g].[SquadId] = [g.Squad].[Id] +WHERE [g].[Discriminator] IN (N'Officer', N'Gear') AND ([g].[Nickname] <> N'Marcus') +ORDER BY [g].[FullName], [g].[Nickname], [g].[SquadId], [g.Squad].[Id]", + // + @"SELECT [g.Squad.Missions].[SquadId], [g.Squad.Missions].[MissionId], [t].[FullName], [t].[Nickname], [t].[SquadId], [t].[Id] +FROM [SquadMissions] AS [g.Squad.Missions] +INNER JOIN ( + SELECT [g0].[FullName], [g0].[Nickname], [g0].[SquadId], [g.Squad0].[Id] + FROM [Gears] AS [g0] + INNER JOIN [Squads] AS [g.Squad0] ON [g0].[SquadId] = [g.Squad0].[Id] + WHERE [g0].[Discriminator] IN (N'Officer', N'Gear') AND ([g0].[Nickname] <> N'Marcus') +) AS [t] ON [g.Squad.Missions].[SquadId] = [t].[Id] +WHERE [g.Squad.Missions].[MissionId] <> 17 +ORDER BY [t].[FullName], [t].[Nickname], [t].[SquadId], [t].[Id]"); + } + + public override void Correlated_collections_project_anonymous_collection_result() + { + base.Correlated_collections_project_anonymous_collection_result(); + + AssertSql( + @"SELECT [s].[Name], [s].[Id] +FROM [Squads] AS [s] +WHERE [s].[Id] < 20 +ORDER BY [s].[Id]", + // + @"SELECT [t].[Id], [s.Members].[FullName], [s.Members].[Rank], [s.Members].[SquadId] +FROM [Gears] AS [s.Members] +INNER JOIN ( + SELECT [s0].[Id] + FROM [Squads] AS [s0] + WHERE [s0].[Id] < 20 +) AS [t] ON [s.Members].[SquadId] = [t].[Id] +WHERE [s.Members].[Discriminator] IN (N'Officer', N'Gear') +ORDER BY [t].[Id]"); + } + + public override void Correlated_collections_nested() + { + base.Correlated_collections_nested(); + + AssertSql( + @"SELECT [s].[Id] +FROM [Squads] AS [s] +ORDER BY [s].[Id]", + // + @"SELECT [t].[Id], [m.Mission].[Id], [s.Missions].[SquadId] +FROM [SquadMissions] AS [s.Missions] +INNER JOIN [Missions] AS [m.Mission] ON [s.Missions].[MissionId] = [m.Mission].[Id] +INNER JOIN ( + SELECT [s0].[Id] + FROM [Squads] AS [s0] +) AS [t] ON [s.Missions].[SquadId] = [t].[Id] +WHERE [s.Missions].[MissionId] < 42 +ORDER BY [t].[Id], [s.Missions].[SquadId], [s.Missions].[MissionId], [m.Mission].[Id]", + // + @"SELECT [m.Mission.ParticipatingSquads].[SquadId], [m.Mission.ParticipatingSquads].[MissionId], [t1].[Id], [t1].[SquadId], [t1].[MissionId], [t1].[Id0] +FROM [SquadMissions] AS [m.Mission.ParticipatingSquads] +INNER JOIN ( + SELECT [t0].[Id], [s.Missions0].[SquadId], [s.Missions0].[MissionId], [m.Mission0].[Id] AS [Id0] + FROM [SquadMissions] AS [s.Missions0] + INNER JOIN [Missions] AS [m.Mission0] ON [s.Missions0].[MissionId] = [m.Mission0].[Id] + INNER JOIN ( + SELECT [s1].[Id] + FROM [Squads] AS [s1] + ) AS [t0] ON [s.Missions0].[SquadId] = [t0].[Id] + WHERE [s.Missions0].[MissionId] < 42 +) AS [t1] ON [m.Mission.ParticipatingSquads].[MissionId] = [t1].[Id0] +WHERE [m.Mission.ParticipatingSquads].[SquadId] < 7 +ORDER BY [t1].[Id], [t1].[SquadId], [t1].[MissionId], [t1].[Id0]"); + } + + public override void Correlated_collections_nested_mixed_streaming_with_buffer1() + { + base.Correlated_collections_nested_mixed_streaming_with_buffer1(); + + AssertSql( + @"SELECT [s].[Id] +FROM [Squads] AS [s]", + // + @"@_outer_Id='1' + +SELECT [m.Mission].[Id] +FROM [SquadMissions] AS [m] +INNER JOIN [Missions] AS [m.Mission] ON [m].[MissionId] = [m.Mission].[Id] +WHERE ([m].[MissionId] < 3) AND (@_outer_Id = [m].[SquadId])", + // + @"@_outer_Id1='1' + +SELECT [ps].[SquadId], [ps].[MissionId] +FROM [SquadMissions] AS [ps] +WHERE ([ps].[SquadId] < 2) AND (@_outer_Id1 = [ps].[MissionId])", + // + @"@_outer_Id1='2' + +SELECT [ps].[SquadId], [ps].[MissionId] +FROM [SquadMissions] AS [ps] +WHERE ([ps].[SquadId] < 2) AND (@_outer_Id1 = [ps].[MissionId])", + // + @"@_outer_Id='2' + +SELECT [m.Mission].[Id] +FROM [SquadMissions] AS [m] +INNER JOIN [Missions] AS [m.Mission] ON [m].[MissionId] = [m.Mission].[Id] +WHERE ([m].[MissionId] < 3) AND (@_outer_Id = [m].[SquadId])", + // + @"@_outer_Id='2' + +SELECT [m.Mission].[Id] +FROM [SquadMissions] AS [m] +INNER JOIN [Missions] AS [m.Mission] ON [m].[MissionId] = [m.Mission].[Id] +WHERE ([m].[MissionId] < 3) AND (@_outer_Id = [m].[SquadId])", + // + @"@_outer_Id='1' + +SELECT [m.Mission].[Id] +FROM [SquadMissions] AS [m] +INNER JOIN [Missions] AS [m.Mission] ON [m].[MissionId] = [m.Mission].[Id] +WHERE ([m].[MissionId] < 3) AND (@_outer_Id = [m].[SquadId])", + // + @"@_outer_Id1='1' + +SELECT [ps].[SquadId], [ps].[MissionId] +FROM [SquadMissions] AS [ps] +WHERE ([ps].[SquadId] < 2) AND (@_outer_Id1 = [ps].[MissionId])", + // + @"@_outer_Id1='2' + +SELECT [ps].[SquadId], [ps].[MissionId] +FROM [SquadMissions] AS [ps] +WHERE ([ps].[SquadId] < 2) AND (@_outer_Id1 = [ps].[MissionId])"); + } + + public override void Correlated_collections_nested_mixed_streaming_with_buffer2() + { + base.Correlated_collections_nested_mixed_streaming_with_buffer2(); + + AssertSql( + @"SELECT [s].[Id] +FROM [Squads] AS [s] +ORDER BY [s].[Id]", + // + @"SELECT [t].[Id], [m.Mission].[Id], [s.Missions].[SquadId] +FROM [SquadMissions] AS [s.Missions] +INNER JOIN [Missions] AS [m.Mission] ON [s.Missions].[MissionId] = [m.Mission].[Id] +INNER JOIN ( + SELECT [s0].[Id] + FROM [Squads] AS [s0] +) AS [t] ON [s.Missions].[SquadId] = [t].[Id] +WHERE [s.Missions].[MissionId] < 42 +ORDER BY [t].[Id]", + // + @"@_outer_Id='3' + +SELECT [ps].[SquadId], [ps].[MissionId] +FROM [SquadMissions] AS [ps] +WHERE ([ps].[SquadId] < 7) AND (@_outer_Id = [ps].[MissionId])", + // + @"@_outer_Id='3' + +SELECT [ps].[SquadId], [ps].[MissionId] +FROM [SquadMissions] AS [ps] +WHERE ([ps].[SquadId] < 7) AND (@_outer_Id = [ps].[MissionId])", + // + @"@_outer_Id='1' + +SELECT [ps].[SquadId], [ps].[MissionId] +FROM [SquadMissions] AS [ps] +WHERE ([ps].[SquadId] < 7) AND (@_outer_Id = [ps].[MissionId])", + // + @"@_outer_Id='2' + +SELECT [ps].[SquadId], [ps].[MissionId] +FROM [SquadMissions] AS [ps] +WHERE ([ps].[SquadId] < 7) AND (@_outer_Id = [ps].[MissionId])", + // + @"@_outer_Id='1' + +SELECT [ps].[SquadId], [ps].[MissionId] +FROM [SquadMissions] AS [ps] +WHERE ([ps].[SquadId] < 7) AND (@_outer_Id = [ps].[MissionId])", + // + @"@_outer_Id='2' + +SELECT [ps].[SquadId], [ps].[MissionId] +FROM [SquadMissions] AS [ps] +WHERE ([ps].[SquadId] < 7) AND (@_outer_Id = [ps].[MissionId])"); + } + + public override void Correlated_collections_nested_with_custom_ordering() + { + base.Correlated_collections_nested_with_custom_ordering(); + + AssertSql( + @"SELECT [o].[FullName], [o].[Nickname], [o].[SquadId] +FROM [Gears] AS [o] +WHERE [o].[Discriminator] = N'Officer' +ORDER BY [o].[HasSoulPatch] DESC, [o].[Nickname], [o].[SquadId]", + // + @"SELECT [t].[HasSoulPatch], [t].[Nickname], [t].[SquadId], [o.Reports].[FullName], [o.Reports].[LeaderNickname], [o.Reports].[LeaderSquadId] +FROM [Gears] AS [o.Reports] +INNER JOIN ( + SELECT [o0].[HasSoulPatch], [o0].[Nickname], [o0].[SquadId] + FROM [Gears] AS [o0] + WHERE [o0].[Discriminator] = N'Officer' +) AS [t] ON ([o.Reports].[LeaderNickname] = [t].[Nickname]) AND ([o.Reports].[LeaderSquadId] = [t].[SquadId]) +WHERE [o.Reports].[Discriminator] IN (N'Officer', N'Gear') AND ([o.Reports].[FullName] <> N'Foo') +ORDER BY [t].[HasSoulPatch] DESC, [t].[Nickname], [t].[SquadId], [o.Reports].[Rank], [o.Reports].[Nickname], [o.Reports].[SquadId], [o.Reports].[FullName]", + // + @"SELECT [o.Reports.Weapons].[Id], [o.Reports.Weapons].[AmmunitionType], [o.Reports.Weapons].[IsAutomatic], [o.Reports.Weapons].[Name], [o.Reports.Weapons].[OwnerFullName], [o.Reports.Weapons].[SynergyWithId], [t1].[HasSoulPatch], [t1].[Nickname], [t1].[SquadId], [t1].[Rank], [t1].[Nickname0], [t1].[SquadId0], [t1].[FullName] +FROM [Weapons] AS [o.Reports.Weapons] +INNER JOIN ( + SELECT [t0].[HasSoulPatch], [t0].[Nickname], [t0].[SquadId], [o.Reports0].[Rank], [o.Reports0].[Nickname] AS [Nickname0], [o.Reports0].[SquadId] AS [SquadId0], [o.Reports0].[FullName] + FROM [Gears] AS [o.Reports0] + INNER JOIN ( + SELECT [o1].[HasSoulPatch], [o1].[Nickname], [o1].[SquadId] + FROM [Gears] AS [o1] + WHERE [o1].[Discriminator] = N'Officer' + ) AS [t0] ON ([o.Reports0].[LeaderNickname] = [t0].[Nickname]) AND ([o.Reports0].[LeaderSquadId] = [t0].[SquadId]) + WHERE [o.Reports0].[Discriminator] IN (N'Officer', N'Gear') AND ([o.Reports0].[FullName] <> N'Foo') +) AS [t1] ON [o.Reports.Weapons].[OwnerFullName] = [t1].[FullName] +WHERE ([o.Reports.Weapons].[Name] <> N'Bar') OR [o.Reports.Weapons].[Name] IS NULL +ORDER BY [t1].[HasSoulPatch] DESC, [t1].[Nickname], [t1].[SquadId], [t1].[Rank], [t1].[Nickname0], [t1].[SquadId0], [t1].[FullName], [o.Reports.Weapons].[IsAutomatic]"); + } + + public override void Correlated_collections_same_collection_projected_multiple_times() + { + base.Correlated_collections_same_collection_projected_multiple_times(); + + AssertSql( + @"SELECT [g].[FullName] +FROM [Gears] AS [g] +WHERE [g].[Discriminator] IN (N'Officer', N'Gear') +ORDER BY [g].[Nickname], [g].[SquadId], [g].[FullName]", + // + @"SELECT [g.Weapons].[Id], [g.Weapons].[AmmunitionType], [g.Weapons].[IsAutomatic], [g.Weapons].[Name], [g.Weapons].[OwnerFullName], [g.Weapons].[SynergyWithId], [t].[Nickname], [t].[SquadId], [t].[FullName] +FROM [Weapons] AS [g.Weapons] +INNER JOIN ( + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[FullName] + FROM [Gears] AS [g0] + WHERE [g0].[Discriminator] IN (N'Officer', N'Gear') +) AS [t] ON [g.Weapons].[OwnerFullName] = [t].[FullName] +WHERE [g.Weapons].[IsAutomatic] = 1 +ORDER BY [t].[Nickname], [t].[SquadId], [t].[FullName]", + // + @"SELECT [g.Weapons0].[Id], [g.Weapons0].[AmmunitionType], [g.Weapons0].[IsAutomatic], [g.Weapons0].[Name], [g.Weapons0].[OwnerFullName], [g.Weapons0].[SynergyWithId], [t0].[Nickname], [t0].[SquadId], [t0].[FullName] +FROM [Weapons] AS [g.Weapons0] +INNER JOIN ( + SELECT [g1].[Nickname], [g1].[SquadId], [g1].[FullName] + FROM [Gears] AS [g1] + WHERE [g1].[Discriminator] IN (N'Officer', N'Gear') +) AS [t0] ON [g.Weapons0].[OwnerFullName] = [t0].[FullName] +WHERE [g.Weapons0].[IsAutomatic] = 1 +ORDER BY [t0].[Nickname], [t0].[SquadId], [t0].[FullName]"); + } + + public override void Correlated_collections_similar_collection_projected_multiple_times() + { + base.Correlated_collections_similar_collection_projected_multiple_times(); + + AssertSql( + @"SELECT [g].[FullName] +FROM [Gears] AS [g] +WHERE [g].[Discriminator] IN (N'Officer', N'Gear') +ORDER BY [g].[Rank], [g].[Nickname], [g].[SquadId], [g].[FullName]", + // + @"SELECT [g.Weapons].[Id], [g.Weapons].[AmmunitionType], [g.Weapons].[IsAutomatic], [g.Weapons].[Name], [g.Weapons].[OwnerFullName], [g.Weapons].[SynergyWithId], [t].[Rank], [t].[Nickname], [t].[SquadId], [t].[FullName] +FROM [Weapons] AS [g.Weapons] +INNER JOIN ( + SELECT [g0].[Rank], [g0].[Nickname], [g0].[SquadId], [g0].[FullName] + FROM [Gears] AS [g0] + WHERE [g0].[Discriminator] IN (N'Officer', N'Gear') +) AS [t] ON [g.Weapons].[OwnerFullName] = [t].[FullName] +WHERE [g.Weapons].[IsAutomatic] = 1 +ORDER BY [t].[Rank], [t].[Nickname], [t].[SquadId], [t].[FullName], [g.Weapons].[OwnerFullName]", + // + @"SELECT [g.Weapons0].[Id], [g.Weapons0].[AmmunitionType], [g.Weapons0].[IsAutomatic], [g.Weapons0].[Name], [g.Weapons0].[OwnerFullName], [g.Weapons0].[SynergyWithId], [t0].[Rank], [t0].[Nickname], [t0].[SquadId], [t0].[FullName] +FROM [Weapons] AS [g.Weapons0] +INNER JOIN ( + SELECT [g1].[Rank], [g1].[Nickname], [g1].[SquadId], [g1].[FullName] + FROM [Gears] AS [g1] + WHERE [g1].[Discriminator] IN (N'Officer', N'Gear') +) AS [t0] ON [g.Weapons0].[OwnerFullName] = [t0].[FullName] +WHERE [g.Weapons0].[IsAutomatic] = 0 +ORDER BY [t0].[Rank], [t0].[Nickname], [t0].[SquadId], [t0].[FullName], [g.Weapons0].[IsAutomatic]"); + } + + public override void Correlated_collections_different_collections_projected() + { + base.Correlated_collections_different_collections_projected(); + + AssertSql( + @"SELECT [o].[Nickname], [o].[FullName], [o].[SquadId] +FROM [Gears] AS [o] +WHERE [o].[Discriminator] = N'Officer' +ORDER BY [o].[FullName], [o].[Nickname], [o].[SquadId]", + // + @"SELECT [t].[FullName], [t].[Nickname], [t].[SquadId], [o.Weapons].[Name], [o.Weapons].[IsAutomatic], [o.Weapons].[OwnerFullName] +FROM [Weapons] AS [o.Weapons] +INNER JOIN ( + SELECT [o0].[FullName], [o0].[Nickname], [o0].[SquadId] + FROM [Gears] AS [o0] + WHERE [o0].[Discriminator] = N'Officer' +) AS [t] ON [o.Weapons].[OwnerFullName] = [t].[FullName] +WHERE [o.Weapons].[IsAutomatic] = 1 +ORDER BY [t].[FullName], [t].[Nickname], [t].[SquadId]", + // + @"SELECT [t0].[FullName], [t0].[Nickname], [t0].[SquadId], [o.Reports].[Nickname] AS [Nickname0], [o.Reports].[Rank], [o.Reports].[LeaderNickname], [o.Reports].[LeaderSquadId] +FROM [Gears] AS [o.Reports] +INNER JOIN ( + SELECT [o1].[FullName], [o1].[Nickname], [o1].[SquadId] + FROM [Gears] AS [o1] + WHERE [o1].[Discriminator] = N'Officer' +) AS [t0] ON ([o.Reports].[LeaderNickname] = [t0].[Nickname]) AND ([o.Reports].[LeaderSquadId] = [t0].[SquadId]) +WHERE [o.Reports].[Discriminator] IN (N'Officer', N'Gear') +ORDER BY [t0].[FullName], [t0].[Nickname], [t0].[SquadId], [o.Reports].[FullName]"); + } + + public override void Correlated_collections_multiple_nested_complex_collections() + { + base.Correlated_collections_multiple_nested_complex_collections(); + + AssertSql( + @"SELECT [o].[FullName] AS [FullName0], [o].[Nickname], [o].[SquadId], [t].[FullName] +FROM [Gears] AS [o] +LEFT JOIN [Tags] AS [o.Tag] ON ([o].[Nickname] = [o.Tag].[GearNickName]) AND ([o].[SquadId] = [o.Tag].[GearSquadId]) +LEFT JOIN ( + SELECT [o.Tag.Gear].* + FROM [Gears] AS [o.Tag.Gear] + WHERE [o.Tag.Gear].[Discriminator] IN (N'Officer', N'Gear') +) AS [t] ON ([o.Tag].[GearNickName] = [t].[Nickname]) AND ([o.Tag].[GearSquadId] = [t].[SquadId]) +WHERE ([o].[Discriminator] = N'Officer') AND EXISTS ( + SELECT 1 + FROM [Gears] AS [g] + WHERE [g].[Discriminator] IN (N'Officer', N'Gear') AND (([o].[Nickname] = [g].[LeaderNickname]) AND ([o].[SquadId] = [g].[LeaderSquadId]))) +ORDER BY [o].[HasSoulPatch] DESC, [o.Tag].[Note], [o].[Nickname], [o].[SquadId], [t].[FullName]", + // + @"SELECT [t1].[HasSoulPatch], [t1].[Note], [t1].[Nickname], [t1].[SquadId], [o.Reports].[FullName], [o.Reports].[LeaderNickname], [o.Reports].[LeaderSquadId] +FROM [Gears] AS [o.Reports] +INNER JOIN ( + SELECT [o0].[HasSoulPatch], [o.Tag0].[Note], [o0].[Nickname], [o0].[SquadId] + FROM [Gears] AS [o0] + LEFT JOIN [Tags] AS [o.Tag0] ON ([o0].[Nickname] = [o.Tag0].[GearNickName]) AND ([o0].[SquadId] = [o.Tag0].[GearSquadId]) + LEFT JOIN ( + SELECT [o.Tag.Gear0].* + FROM [Gears] AS [o.Tag.Gear0] + WHERE [o.Tag.Gear0].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t0] ON ([o.Tag0].[GearNickName] = [t0].[Nickname]) AND ([o.Tag0].[GearSquadId] = [t0].[SquadId]) + WHERE ([o0].[Discriminator] = N'Officer') AND EXISTS ( + SELECT 1 + FROM [Gears] AS [g0] + WHERE [g0].[Discriminator] IN (N'Officer', N'Gear') AND (([o0].[Nickname] = [g0].[LeaderNickname]) AND ([o0].[SquadId] = [g0].[LeaderSquadId]))) +) AS [t1] ON ([o.Reports].[LeaderNickname] = [t1].[Nickname]) AND ([o.Reports].[LeaderSquadId] = [t1].[SquadId]) +WHERE [o.Reports].[Discriminator] IN (N'Officer', N'Gear') AND ([o.Reports].[FullName] <> N'Foo') +ORDER BY [t1].[HasSoulPatch] DESC, [t1].[Note], [t1].[Nickname], [t1].[SquadId], [o.Reports].[Rank], [o.Reports].[Nickname], [o.Reports].[SquadId], [o.Reports].[FullName]", + // + @"SELECT [t5].[HasSoulPatch], [t5].[Note], [t5].[Nickname], [t5].[SquadId], [t5].[Rank], [t5].[Nickname0], [t5].[SquadId0], [t5].[FullName], [o.Reports.Weapons].[Id] AS [Id0], [t2].[FullName], [w.Owner.Squad].[Id], [o.Reports.Weapons].[OwnerFullName] +FROM [Weapons] AS [o.Reports.Weapons] +LEFT JOIN ( + SELECT [w.Owner].* + FROM [Gears] AS [w.Owner] + WHERE [w.Owner].[Discriminator] IN (N'Officer', N'Gear') +) AS [t2] ON [o.Reports.Weapons].[OwnerFullName] = [t2].[FullName] +LEFT JOIN [Squads] AS [w.Owner.Squad] ON [t2].[SquadId] = [w.Owner.Squad].[Id] +INNER JOIN ( + SELECT [t4].[HasSoulPatch], [t4].[Note], [t4].[Nickname], [t4].[SquadId], [o.Reports0].[Rank], [o.Reports0].[Nickname] AS [Nickname0], [o.Reports0].[SquadId] AS [SquadId0], [o.Reports0].[FullName] + FROM [Gears] AS [o.Reports0] + INNER JOIN ( + SELECT [o1].[HasSoulPatch], [o.Tag1].[Note], [o1].[Nickname], [o1].[SquadId] + FROM [Gears] AS [o1] + LEFT JOIN [Tags] AS [o.Tag1] ON ([o1].[Nickname] = [o.Tag1].[GearNickName]) AND ([o1].[SquadId] = [o.Tag1].[GearSquadId]) + LEFT JOIN ( + SELECT [o.Tag.Gear1].* + FROM [Gears] AS [o.Tag.Gear1] + WHERE [o.Tag.Gear1].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t3] ON ([o.Tag1].[GearNickName] = [t3].[Nickname]) AND ([o.Tag1].[GearSquadId] = [t3].[SquadId]) + WHERE ([o1].[Discriminator] = N'Officer') AND EXISTS ( + SELECT 1 + FROM [Gears] AS [g1] + WHERE [g1].[Discriminator] IN (N'Officer', N'Gear') AND (([o1].[Nickname] = [g1].[LeaderNickname]) AND ([o1].[SquadId] = [g1].[LeaderSquadId]))) + ) AS [t4] ON ([o.Reports0].[LeaderNickname] = [t4].[Nickname]) AND ([o.Reports0].[LeaderSquadId] = [t4].[SquadId]) + WHERE [o.Reports0].[Discriminator] IN (N'Officer', N'Gear') AND ([o.Reports0].[FullName] <> N'Foo') +) AS [t5] ON [o.Reports.Weapons].[OwnerFullName] = [t5].[FullName] +WHERE ([o.Reports.Weapons].[Name] <> N'Bar') OR [o.Reports.Weapons].[Name] IS NULL +ORDER BY [t5].[HasSoulPatch] DESC, [t5].[Note], [t5].[Nickname], [t5].[SquadId], [t5].[Rank], [t5].[Nickname0], [t5].[SquadId0], [t5].[FullName], [o.Reports.Weapons].[IsAutomatic], [o.Reports.Weapons].[Id], [t2].[FullName], [w.Owner.Squad].[Id]", + // + @"SELECT [t10].[HasSoulPatch], [t10].[Note], [t10].[Nickname], [t10].[SquadId], [t10].[Rank], [t10].[Nickname0], [t10].[SquadId0], [t10].[FullName], [t10].[IsAutomatic], [t10].[Id], [t10].[FullName0], [w.Owner.Weapons].[Name], [w.Owner.Weapons].[IsAutomatic] AS [IsAutomatic0], [w.Owner.Weapons].[OwnerFullName] +FROM [Weapons] AS [w.Owner.Weapons] +INNER JOIN ( + SELECT [t9].[HasSoulPatch], [t9].[Note], [t9].[Nickname], [t9].[SquadId], [t9].[Rank], [t9].[Nickname0], [t9].[SquadId0], [t9].[FullName], [o.Reports.Weapons0].[IsAutomatic], [o.Reports.Weapons0].[Id], [t6].[FullName] AS [FullName0] + FROM [Weapons] AS [o.Reports.Weapons0] + LEFT JOIN ( + SELECT [w.Owner0].* + FROM [Gears] AS [w.Owner0] + WHERE [w.Owner0].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t6] ON [o.Reports.Weapons0].[OwnerFullName] = [t6].[FullName] + LEFT JOIN [Squads] AS [w.Owner.Squad0] ON [t6].[SquadId] = [w.Owner.Squad0].[Id] + INNER JOIN ( + SELECT [t8].[HasSoulPatch], [t8].[Note], [t8].[Nickname], [t8].[SquadId], [o.Reports1].[Rank], [o.Reports1].[Nickname] AS [Nickname0], [o.Reports1].[SquadId] AS [SquadId0], [o.Reports1].[FullName] + FROM [Gears] AS [o.Reports1] + INNER JOIN ( + SELECT [o2].[HasSoulPatch], [o.Tag2].[Note], [o2].[Nickname], [o2].[SquadId] + FROM [Gears] AS [o2] + LEFT JOIN [Tags] AS [o.Tag2] ON ([o2].[Nickname] = [o.Tag2].[GearNickName]) AND ([o2].[SquadId] = [o.Tag2].[GearSquadId]) + LEFT JOIN ( + SELECT [o.Tag.Gear2].* + FROM [Gears] AS [o.Tag.Gear2] + WHERE [o.Tag.Gear2].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t7] ON ([o.Tag2].[GearNickName] = [t7].[Nickname]) AND ([o.Tag2].[GearSquadId] = [t7].[SquadId]) + WHERE ([o2].[Discriminator] = N'Officer') AND EXISTS ( + SELECT 1 + FROM [Gears] AS [g2] + WHERE [g2].[Discriminator] IN (N'Officer', N'Gear') AND (([o2].[Nickname] = [g2].[LeaderNickname]) AND ([o2].[SquadId] = [g2].[LeaderSquadId]))) + ) AS [t8] ON ([o.Reports1].[LeaderNickname] = [t8].[Nickname]) AND ([o.Reports1].[LeaderSquadId] = [t8].[SquadId]) + WHERE [o.Reports1].[Discriminator] IN (N'Officer', N'Gear') AND ([o.Reports1].[FullName] <> N'Foo') + ) AS [t9] ON [o.Reports.Weapons0].[OwnerFullName] = [t9].[FullName] + WHERE ([o.Reports.Weapons0].[Name] <> N'Bar') OR [o.Reports.Weapons0].[Name] IS NULL +) AS [t10] ON [w.Owner.Weapons].[OwnerFullName] = [t10].[FullName0] +ORDER BY [t10].[HasSoulPatch] DESC, [t10].[Note], [t10].[Nickname], [t10].[SquadId], [t10].[Rank], [t10].[Nickname0], [t10].[SquadId0], [t10].[FullName], [t10].[IsAutomatic], [t10].[Id], [t10].[FullName0]", + // + @"SELECT [t15].[HasSoulPatch], [t15].[Note], [t15].[Nickname], [t15].[SquadId], [t15].[Rank], [t15].[Nickname0], [t15].[SquadId0], [t15].[FullName], [t15].[IsAutomatic], [t15].[Id], [t15].[Id0], [w.Owner.Squad.Members].[Nickname] AS [Nickname1], [w.Owner.Squad.Members].[HasSoulPatch] AS [HasSoulPatch0], [w.Owner.Squad.Members].[SquadId] +FROM [Gears] AS [w.Owner.Squad.Members] +INNER JOIN ( + SELECT [t14].[HasSoulPatch], [t14].[Note], [t14].[Nickname], [t14].[SquadId], [t14].[Rank], [t14].[Nickname0], [t14].[SquadId0], [t14].[FullName], [o.Reports.Weapons1].[IsAutomatic], [o.Reports.Weapons1].[Id], [w.Owner.Squad1].[Id] AS [Id0] + FROM [Weapons] AS [o.Reports.Weapons1] + LEFT JOIN ( + SELECT [w.Owner1].* + FROM [Gears] AS [w.Owner1] + WHERE [w.Owner1].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t11] ON [o.Reports.Weapons1].[OwnerFullName] = [t11].[FullName] + LEFT JOIN [Squads] AS [w.Owner.Squad1] ON [t11].[SquadId] = [w.Owner.Squad1].[Id] + INNER JOIN ( + SELECT [t13].[HasSoulPatch], [t13].[Note], [t13].[Nickname], [t13].[SquadId], [o.Reports2].[Rank], [o.Reports2].[Nickname] AS [Nickname0], [o.Reports2].[SquadId] AS [SquadId0], [o.Reports2].[FullName] + FROM [Gears] AS [o.Reports2] + INNER JOIN ( + SELECT [o3].[HasSoulPatch], [o.Tag3].[Note], [o3].[Nickname], [o3].[SquadId] + FROM [Gears] AS [o3] + LEFT JOIN [Tags] AS [o.Tag3] ON ([o3].[Nickname] = [o.Tag3].[GearNickName]) AND ([o3].[SquadId] = [o.Tag3].[GearSquadId]) + LEFT JOIN ( + SELECT [o.Tag.Gear3].* + FROM [Gears] AS [o.Tag.Gear3] + WHERE [o.Tag.Gear3].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t12] ON ([o.Tag3].[GearNickName] = [t12].[Nickname]) AND ([o.Tag3].[GearSquadId] = [t12].[SquadId]) + WHERE ([o3].[Discriminator] = N'Officer') AND EXISTS ( + SELECT 1 + FROM [Gears] AS [g3] + WHERE [g3].[Discriminator] IN (N'Officer', N'Gear') AND (([o3].[Nickname] = [g3].[LeaderNickname]) AND ([o3].[SquadId] = [g3].[LeaderSquadId]))) + ) AS [t13] ON ([o.Reports2].[LeaderNickname] = [t13].[Nickname]) AND ([o.Reports2].[LeaderSquadId] = [t13].[SquadId]) + WHERE [o.Reports2].[Discriminator] IN (N'Officer', N'Gear') AND ([o.Reports2].[FullName] <> N'Foo') + ) AS [t14] ON [o.Reports.Weapons1].[OwnerFullName] = [t14].[FullName] + WHERE ([o.Reports.Weapons1].[Name] <> N'Bar') OR [o.Reports.Weapons1].[Name] IS NULL +) AS [t15] ON [w.Owner.Squad.Members].[SquadId] = [t15].[Id0] +WHERE [w.Owner.Squad.Members].[Discriminator] IN (N'Officer', N'Gear') +ORDER BY [t15].[HasSoulPatch] DESC, [t15].[Note], [t15].[Nickname], [t15].[SquadId], [t15].[Rank], [t15].[Nickname0], [t15].[SquadId0], [t15].[FullName], [t15].[IsAutomatic], [t15].[Id], [t15].[Id0], [Nickname1]", + // + @"SELECT [o.Tag.Gear.Weapons].[Id], [o.Tag.Gear.Weapons].[AmmunitionType], [o.Tag.Gear.Weapons].[IsAutomatic], [o.Tag.Gear.Weapons].[Name], [o.Tag.Gear.Weapons].[OwnerFullName], [o.Tag.Gear.Weapons].[SynergyWithId], [t18].[HasSoulPatch], [t18].[Note], [t18].[Nickname], [t18].[SquadId], [t18].[FullName] +FROM [Weapons] AS [o.Tag.Gear.Weapons] +LEFT JOIN ( + SELECT [www.Owner].* + FROM [Gears] AS [www.Owner] + WHERE [www.Owner].[Discriminator] IN (N'Officer', N'Gear') +) AS [t16] ON [o.Tag.Gear.Weapons].[OwnerFullName] = [t16].[FullName] +INNER JOIN ( + SELECT [o4].[HasSoulPatch], [o.Tag4].[Note], [o4].[Nickname], [o4].[SquadId], [t17].[FullName] + FROM [Gears] AS [o4] + LEFT JOIN [Tags] AS [o.Tag4] ON ([o4].[Nickname] = [o.Tag4].[GearNickName]) AND ([o4].[SquadId] = [o.Tag4].[GearSquadId]) + LEFT JOIN ( + SELECT [o.Tag.Gear4].* + FROM [Gears] AS [o.Tag.Gear4] + WHERE [o.Tag.Gear4].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t17] ON ([o.Tag4].[GearNickName] = [t17].[Nickname]) AND ([o.Tag4].[GearSquadId] = [t17].[SquadId]) + WHERE ([o4].[Discriminator] = N'Officer') AND EXISTS ( + SELECT 1 + FROM [Gears] AS [g4] + WHERE [g4].[Discriminator] IN (N'Officer', N'Gear') AND (([o4].[Nickname] = [g4].[LeaderNickname]) AND ([o4].[SquadId] = [g4].[LeaderSquadId]))) +) AS [t18] ON [o.Tag.Gear.Weapons].[OwnerFullName] = [t18].[FullName] +ORDER BY [t18].[HasSoulPatch] DESC, [t18].[Note], [t18].[Nickname], [t18].[SquadId], [t18].[FullName], [o.Tag.Gear.Weapons].[IsAutomatic], [t16].[Nickname] DESC"); + } + + public override void Correlated_collections_inner_subquery_selector_references_outer_qsre() + { + base.Correlated_collections_inner_subquery_selector_references_outer_qsre(); + + AssertSql( + @"SELECT [o].[FullName], [o].[Nickname], [o].[SquadId] +FROM [Gears] AS [o] +WHERE [o].[Discriminator] = N'Officer'", + // + @"@_outer_FullName='Damon Baird' (Size = 4000) +@_outer_Nickname='Baird' (Size = 450) +@_outer_SquadId='1' + +SELECT [r].[FullName] AS [ReportName], @_outer_FullName AS [OfficerName] +FROM [Gears] AS [r] +WHERE [r].[Discriminator] IN (N'Officer', N'Gear') AND ((@_outer_Nickname = [r].[LeaderNickname]) AND (@_outer_SquadId = [r].[LeaderSquadId]))", + // + @"@_outer_FullName='Marcus Fenix' (Size = 4000) +@_outer_Nickname='Marcus' (Size = 450) +@_outer_SquadId='1' + +SELECT [r].[FullName] AS [ReportName], @_outer_FullName AS [OfficerName] +FROM [Gears] AS [r] +WHERE [r].[Discriminator] IN (N'Officer', N'Gear') AND ((@_outer_Nickname = [r].[LeaderNickname]) AND (@_outer_SquadId = [r].[LeaderSquadId]))"); + } + + public override void Correlated_collections_inner_subquery_predicate_references_outer_qsre() + { + base.Correlated_collections_inner_subquery_predicate_references_outer_qsre(); + + AssertSql( + @"SELECT [o].[FullName], [o].[Nickname], [o].[SquadId] +FROM [Gears] AS [o] +WHERE [o].[Discriminator] = N'Officer'", + // + @"@_outer_FullName='Damon Baird' (Size = 4000) +@_outer_Nickname='Baird' (Size = 450) +@_outer_SquadId='1' + +SELECT [r].[FullName] AS [ReportName] +FROM [Gears] AS [r] +WHERE ([r].[Discriminator] IN (N'Officer', N'Gear') AND (@_outer_FullName <> N'Foo')) AND ((@_outer_Nickname = [r].[LeaderNickname]) AND (@_outer_SquadId = [r].[LeaderSquadId]))", + // + @"@_outer_FullName='Marcus Fenix' (Size = 4000) +@_outer_Nickname='Marcus' (Size = 450) +@_outer_SquadId='1' + +SELECT [r].[FullName] AS [ReportName] +FROM [Gears] AS [r] +WHERE ([r].[Discriminator] IN (N'Officer', N'Gear') AND (@_outer_FullName <> N'Foo')) AND ((@_outer_Nickname = [r].[LeaderNickname]) AND (@_outer_SquadId = [r].[LeaderSquadId]))"); + } + + public override void Correlated_collections_nested_inner_subquery_references_outer_qsre_one_level_up() + { + base.Correlated_collections_nested_inner_subquery_references_outer_qsre_one_level_up(); + + AssertSql( + @"SELECT [o].[FullName], [o].[Nickname], [o].[SquadId] +FROM [Gears] AS [o] +WHERE [o].[Discriminator] = N'Officer' +ORDER BY [o].[Nickname], [o].[SquadId]", + // + @"SELECT [t].[Nickname], [t].[SquadId], [o.Reports].[FullName], [o.Reports].[Nickname], [o.Reports].[LeaderNickname], [o.Reports].[LeaderSquadId] +FROM [Gears] AS [o.Reports] +INNER JOIN ( + SELECT [o0].[Nickname], [o0].[SquadId] + FROM [Gears] AS [o0] + WHERE [o0].[Discriminator] = N'Officer' +) AS [t] ON ([o.Reports].[LeaderNickname] = [t].[Nickname]) AND ([o.Reports].[LeaderSquadId] = [t].[SquadId]) +WHERE [o.Reports].[Discriminator] IN (N'Officer', N'Gear') AND ([o.Reports].[FullName] <> N'Foo') +ORDER BY [t].[Nickname], [t].[SquadId]", + // + @"@_outer_Nickname='Paduk' (Size = 4000) +@_outer_FullName='Garron Paduk' (Size = 450) + +SELECT [w].[Name], @_outer_Nickname AS [Nickname] +FROM [Weapons] AS [w] +WHERE (([w].[Name] <> N'Bar') OR [w].[Name] IS NULL) AND (@_outer_FullName = [w].[OwnerFullName])", + // + @"@_outer_Nickname='Baird' (Size = 4000) +@_outer_FullName='Damon Baird' (Size = 450) + +SELECT [w].[Name], @_outer_Nickname AS [Nickname] +FROM [Weapons] AS [w] +WHERE (([w].[Name] <> N'Bar') OR [w].[Name] IS NULL) AND (@_outer_FullName = [w].[OwnerFullName])", + // + @"@_outer_Nickname='Cole Train' (Size = 4000) +@_outer_FullName='Augustus Cole' (Size = 450) + +SELECT [w].[Name], @_outer_Nickname AS [Nickname] +FROM [Weapons] AS [w] +WHERE (([w].[Name] <> N'Bar') OR [w].[Name] IS NULL) AND (@_outer_FullName = [w].[OwnerFullName])", + // + @"@_outer_Nickname='Dom' (Size = 4000) +@_outer_FullName='Dominic Santiago' (Size = 450) + +SELECT [w].[Name], @_outer_Nickname AS [Nickname] +FROM [Weapons] AS [w] +WHERE (([w].[Name] <> N'Bar') OR [w].[Name] IS NULL) AND (@_outer_FullName = [w].[OwnerFullName])"); + } + + public override void Correlated_collections_nested_inner_subquery_references_outer_qsre_two_levels_up() + { + base.Correlated_collections_nested_inner_subquery_references_outer_qsre_two_levels_up(); + + AssertSql( + @"SELECT [o].[FullName], [o].[Nickname], [o].[SquadId] +FROM [Gears] AS [o] +WHERE [o].[Discriminator] = N'Officer'", + // + @"@_outer_Nickname='Baird' (Size = 450) +@_outer_SquadId='1' + +SELECT [r].[FullName] +FROM [Gears] AS [r] +WHERE ([r].[Discriminator] IN (N'Officer', N'Gear') AND ([r].[FullName] <> N'Foo')) AND ((@_outer_Nickname = [r].[LeaderNickname]) AND (@_outer_SquadId = [r].[LeaderSquadId]))", + // + @"@_outer_Nickname1='Baird' (Size = 4000) +@_outer_FullName='Garron Paduk' (Size = 450) + +SELECT [w].[Name], @_outer_Nickname1 AS [Nickname] +FROM [Weapons] AS [w] +WHERE (([w].[Name] <> N'Bar') OR [w].[Name] IS NULL) AND (@_outer_FullName = [w].[OwnerFullName])", + // + @"@_outer_Nickname='Marcus' (Size = 450) +@_outer_SquadId='1' + +SELECT [r].[FullName] +FROM [Gears] AS [r] +WHERE ([r].[Discriminator] IN (N'Officer', N'Gear') AND ([r].[FullName] <> N'Foo')) AND ((@_outer_Nickname = [r].[LeaderNickname]) AND (@_outer_SquadId = [r].[LeaderSquadId]))", + // + @"@_outer_Nickname1='Marcus' (Size = 4000) +@_outer_FullName='Augustus Cole' (Size = 450) + +SELECT [w].[Name], @_outer_Nickname1 AS [Nickname] +FROM [Weapons] AS [w] +WHERE (([w].[Name] <> N'Bar') OR [w].[Name] IS NULL) AND (@_outer_FullName = [w].[OwnerFullName])", + // + @"@_outer_Nickname1='Marcus' (Size = 4000) +@_outer_FullName='Damon Baird' (Size = 450) + +SELECT [w].[Name], @_outer_Nickname1 AS [Nickname] +FROM [Weapons] AS [w] +WHERE (([w].[Name] <> N'Bar') OR [w].[Name] IS NULL) AND (@_outer_FullName = [w].[OwnerFullName])", + // + @"@_outer_Nickname1='Marcus' (Size = 4000) +@_outer_FullName='Dominic Santiago' (Size = 450) + +SELECT [w].[Name], @_outer_Nickname1 AS [Nickname] +FROM [Weapons] AS [w] +WHERE (([w].[Name] <> N'Bar') OR [w].[Name] IS NULL) AND (@_outer_FullName = [w].[OwnerFullName])"); + } + + public override void Correlated_collections_on_select_many() + { + base.Correlated_collections_on_select_many(); + + AssertSql( + @"SELECT [g].[Nickname] AS [GearNickname], [s].[Name] AS [SquadName], [g].[FullName], [s].[Id] +FROM [Gears] AS [g] +CROSS JOIN [Squads] AS [s] +WHERE [g].[Discriminator] IN (N'Officer', N'Gear') AND ([g].[HasSoulPatch] = 1) +ORDER BY [GearNickname], [s].[Id] DESC", + // + @"@_outer_FullName='Damon Baird' (Size = 450) + +SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] +FROM [Weapons] AS [w] +WHERE (([w].[IsAutomatic] = 1) OR (([w].[Name] <> N'foo') OR [w].[Name] IS NULL)) AND (@_outer_FullName = [w].[OwnerFullName])", + // + @"@_outer_Id='2' + +SELECT [m].[Nickname], [m].[SquadId], [m].[AssignedCityName], [m].[CityOrBirthName], [m].[Discriminator], [m].[FullName], [m].[HasSoulPatch], [m].[LeaderNickname], [m].[LeaderSquadId], [m].[Rank] +FROM [Gears] AS [m] +WHERE ([m].[Discriminator] IN (N'Officer', N'Gear') AND ([m].[HasSoulPatch] = 0)) AND (@_outer_Id = [m].[SquadId])", + // + @"@_outer_FullName='Damon Baird' (Size = 450) + +SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] +FROM [Weapons] AS [w] +WHERE (([w].[IsAutomatic] = 1) OR (([w].[Name] <> N'foo') OR [w].[Name] IS NULL)) AND (@_outer_FullName = [w].[OwnerFullName])", + // + @"@_outer_Id='1' + +SELECT [m].[Nickname], [m].[SquadId], [m].[AssignedCityName], [m].[CityOrBirthName], [m].[Discriminator], [m].[FullName], [m].[HasSoulPatch], [m].[LeaderNickname], [m].[LeaderSquadId], [m].[Rank] +FROM [Gears] AS [m] +WHERE ([m].[Discriminator] IN (N'Officer', N'Gear') AND ([m].[HasSoulPatch] = 0)) AND (@_outer_Id = [m].[SquadId])", + // + @"@_outer_FullName='Marcus Fenix' (Size = 450) + +SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] +FROM [Weapons] AS [w] +WHERE (([w].[IsAutomatic] = 1) OR (([w].[Name] <> N'foo') OR [w].[Name] IS NULL)) AND (@_outer_FullName = [w].[OwnerFullName])", + // + @"@_outer_Id='2' + +SELECT [m].[Nickname], [m].[SquadId], [m].[AssignedCityName], [m].[CityOrBirthName], [m].[Discriminator], [m].[FullName], [m].[HasSoulPatch], [m].[LeaderNickname], [m].[LeaderSquadId], [m].[Rank] +FROM [Gears] AS [m] +WHERE ([m].[Discriminator] IN (N'Officer', N'Gear') AND ([m].[HasSoulPatch] = 0)) AND (@_outer_Id = [m].[SquadId])", + // + @"@_outer_FullName='Marcus Fenix' (Size = 450) + +SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] +FROM [Weapons] AS [w] +WHERE (([w].[IsAutomatic] = 1) OR (([w].[Name] <> N'foo') OR [w].[Name] IS NULL)) AND (@_outer_FullName = [w].[OwnerFullName])", + // + @"@_outer_Id='1' + +SELECT [m].[Nickname], [m].[SquadId], [m].[AssignedCityName], [m].[CityOrBirthName], [m].[Discriminator], [m].[FullName], [m].[HasSoulPatch], [m].[LeaderNickname], [m].[LeaderSquadId], [m].[Rank] +FROM [Gears] AS [m] +WHERE ([m].[Discriminator] IN (N'Officer', N'Gear') AND ([m].[HasSoulPatch] = 0)) AND (@_outer_Id = [m].[SquadId])"); + } + + public override void Correlated_collections_with_Skip() + { + base.Correlated_collections_with_Skip(); + + AssertSql( + @"SELECT [s].[Id] +FROM [Squads] AS [s] +ORDER BY [s].[Name]", + // + @"@_outer_Id='1' + +SELECT [m].[Nickname], [m].[SquadId], [m].[AssignedCityName], [m].[CityOrBirthName], [m].[Discriminator], [m].[FullName], [m].[HasSoulPatch], [m].[LeaderNickname], [m].[LeaderSquadId], [m].[Rank] +FROM [Gears] AS [m] +WHERE [m].[Discriminator] IN (N'Officer', N'Gear') AND (@_outer_Id = [m].[SquadId]) +ORDER BY [m].[Nickname] +OFFSET 1 ROWS", + // + @"@_outer_Id='2' + +SELECT [m].[Nickname], [m].[SquadId], [m].[AssignedCityName], [m].[CityOrBirthName], [m].[Discriminator], [m].[FullName], [m].[HasSoulPatch], [m].[LeaderNickname], [m].[LeaderSquadId], [m].[Rank] +FROM [Gears] AS [m] +WHERE [m].[Discriminator] IN (N'Officer', N'Gear') AND (@_outer_Id = [m].[SquadId]) +ORDER BY [m].[Nickname] +OFFSET 1 ROWS"); + } + + public override void Correlated_collections_with_Take() + { + base.Correlated_collections_with_Take(); + + AssertSql( + @"SELECT [s].[Id] +FROM [Squads] AS [s] +ORDER BY [s].[Name]", + // + @"@_outer_Id='1' + +SELECT TOP(2) [m].[Nickname], [m].[SquadId], [m].[AssignedCityName], [m].[CityOrBirthName], [m].[Discriminator], [m].[FullName], [m].[HasSoulPatch], [m].[LeaderNickname], [m].[LeaderSquadId], [m].[Rank] +FROM [Gears] AS [m] +WHERE [m].[Discriminator] IN (N'Officer', N'Gear') AND (@_outer_Id = [m].[SquadId]) +ORDER BY [m].[Nickname]", + // + @"@_outer_Id='2' + +SELECT TOP(2) [m].[Nickname], [m].[SquadId], [m].[AssignedCityName], [m].[CityOrBirthName], [m].[Discriminator], [m].[FullName], [m].[HasSoulPatch], [m].[LeaderNickname], [m].[LeaderSquadId], [m].[Rank] +FROM [Gears] AS [m] +WHERE [m].[Discriminator] IN (N'Officer', N'Gear') AND (@_outer_Id = [m].[SquadId]) +ORDER BY [m].[Nickname]"); + } + + public override void Correlated_collections_with_Distinct() + { + base.Correlated_collections_with_Distinct(); + + AssertSql( + @"SELECT [s].[Id] +FROM [Squads] AS [s] +ORDER BY [s].[Name]", + // + @"@_outer_Id='1' + +SELECT DISTINCT [m].[Nickname], [m].[SquadId], [m].[AssignedCityName], [m].[CityOrBirthName], [m].[Discriminator], [m].[FullName], [m].[HasSoulPatch], [m].[LeaderNickname], [m].[LeaderSquadId], [m].[Rank] +FROM [Gears] AS [m] +WHERE [m].[Discriminator] IN (N'Officer', N'Gear') AND (@_outer_Id = [m].[SquadId]) +ORDER BY [m].[Nickname]", + // + @"@_outer_Id='2' + +SELECT DISTINCT [m].[Nickname], [m].[SquadId], [m].[AssignedCityName], [m].[CityOrBirthName], [m].[Discriminator], [m].[FullName], [m].[HasSoulPatch], [m].[LeaderNickname], [m].[LeaderSquadId], [m].[Rank] +FROM [Gears] AS [m] +WHERE [m].[Discriminator] IN (N'Officer', N'Gear') AND (@_outer_Id = [m].[SquadId]) +ORDER BY [m].[Nickname]"); + } + + public override void Correlated_collections_with_FirstOrDefault() + { + base.Correlated_collections_with_FirstOrDefault(); + + AssertSql( + @"SELECT ( + SELECT TOP(1) [m].[FullName] + FROM [Gears] AS [m] + WHERE [m].[Discriminator] IN (N'Officer', N'Gear') AND ([s].[Id] = [m].[SquadId]) + ORDER BY [m].[Nickname] +) +FROM [Squads] AS [s] +ORDER BY [s].[Name]"); + } + + public override void Correlated_collections_on_left_join_with_predicate() + { + base.Correlated_collections_on_left_join_with_predicate(); + + AssertSql( + @"SELECT [t0].[Nickname], [t0].[FullName] +FROM [Tags] AS [t] +LEFT JOIN ( + SELECT [g].* + FROM [Gears] AS [g] + WHERE [g].[Discriminator] IN (N'Officer', N'Gear') +) AS [t0] ON [t].[GearNickName] = [t0].[Nickname] +WHERE ([t0].[HasSoulPatch] <> 1) OR [t0].[HasSoulPatch] IS NULL +ORDER BY [t0].[Nickname], [t0].[SquadId], [t0].[FullName]", + // + @"SELECT [t3].[Nickname], [t3].[SquadId], [t3].[FullName], [g.Weapons].[Name], [g.Weapons].[OwnerFullName] +FROM [Weapons] AS [g.Weapons] +INNER JOIN ( + SELECT [t2].[Nickname], [t2].[SquadId], [t2].[FullName] + FROM [Tags] AS [t1] + LEFT JOIN ( + SELECT [g0].* + FROM [Gears] AS [g0] + WHERE [g0].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t2] ON [t1].[GearNickName] = [t2].[Nickname] + WHERE ([t2].[HasSoulPatch] <> 1) OR [t2].[HasSoulPatch] IS NULL +) AS [t3] ON [g.Weapons].[OwnerFullName] = [t3].[FullName] +ORDER BY [t3].[Nickname], [t3].[SquadId], [t3].[FullName]"); + } + + public override void Correlated_collections_on_left_join_with_null_value() + { + base.Correlated_collections_on_left_join_with_null_value(); + + AssertSql( + @"SELECT [t0].[FullName] +FROM [Tags] AS [t] +LEFT JOIN ( + SELECT [g].* + FROM [Gears] AS [g] + WHERE [g].[Discriminator] IN (N'Officer', N'Gear') +) AS [t0] ON [t].[GearNickName] = [t0].[Nickname] +ORDER BY [t].[Note], [t0].[Nickname], [t0].[SquadId], [t0].[FullName]", + // + @"SELECT [t3].[Note], [t3].[Nickname], [t3].[SquadId], [t3].[FullName], [g.Weapons].[Name], [g.Weapons].[OwnerFullName] +FROM [Weapons] AS [g.Weapons] +INNER JOIN ( + SELECT [t1].[Note], [t2].[Nickname], [t2].[SquadId], [t2].[FullName] + FROM [Tags] AS [t1] + LEFT JOIN ( + SELECT [g0].* + FROM [Gears] AS [g0] + WHERE [g0].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t2] ON [t1].[GearNickName] = [t2].[Nickname] +) AS [t3] ON [g.Weapons].[OwnerFullName] = [t3].[FullName] +ORDER BY [t3].[Note], [t3].[Nickname], [t3].[SquadId], [t3].[FullName]"); + } + + public override void Correlated_collections_left_join_with_self_reference() + { + base.Correlated_collections_left_join_with_self_reference(); + + AssertSql( + @"SELECT [t].[Note], [t0].[Nickname], [t0].[SquadId] +FROM [Tags] AS [t] +LEFT JOIN ( + SELECT [o].* + FROM [Gears] AS [o] + WHERE [o].[Discriminator] = N'Officer' +) AS [t0] ON [t].[GearNickName] = [t0].[Nickname] +ORDER BY [t0].[Nickname], [t0].[SquadId]", + // + @"SELECT [t3].[Nickname], [t3].[SquadId], [o.Reports].[FullName], [o.Reports].[LeaderNickname], [o.Reports].[LeaderSquadId] +FROM [Gears] AS [o.Reports] +INNER JOIN ( + SELECT [t2].[Nickname], [t2].[SquadId] + FROM [Tags] AS [t1] + LEFT JOIN ( + SELECT [o0].* + FROM [Gears] AS [o0] + WHERE [o0].[Discriminator] = N'Officer' + ) AS [t2] ON [t1].[GearNickName] = [t2].[Nickname] +) AS [t3] ON ([o.Reports].[LeaderNickname] = [t3].[Nickname]) AND ([o.Reports].[LeaderSquadId] = [t3].[SquadId]) +WHERE [o.Reports].[Discriminator] IN (N'Officer', N'Gear') +ORDER BY [t3].[Nickname], [t3].[SquadId]"); + } + + public override void Correlated_collections_deeply_nested_left_join() + { + base.Correlated_collections_deeply_nested_left_join(); + + AssertSql( + @"SELECT [g.Squad].[Id] +FROM [Tags] AS [t] +LEFT JOIN ( + SELECT [g].* + FROM [Gears] AS [g] + WHERE [g].[Discriminator] IN (N'Officer', N'Gear') +) AS [t0] ON [t].[GearNickName] = [t0].[Nickname] +LEFT JOIN [Squads] AS [g.Squad] ON [t0].[SquadId] = [g.Squad].[Id] +ORDER BY [t].[Note], [t0].[Nickname] DESC, [t0].[SquadId], [g.Squad].[Id]", + // + @"SELECT [t3].[Note], [t3].[Nickname], [t3].[SquadId], [t3].[Id], [g.Squad.Members].[Nickname] AS [Nickname0], [g.Squad.Members].[FullName], [g.Squad.Members].[SquadId] +FROM [Gears] AS [g.Squad.Members] +INNER JOIN ( + SELECT [t1].[Note], [t2].[Nickname], [t2].[SquadId], [g.Squad0].[Id] + FROM [Tags] AS [t1] + LEFT JOIN ( + SELECT [g0].* + FROM [Gears] AS [g0] + WHERE [g0].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t2] ON [t1].[GearNickName] = [t2].[Nickname] + LEFT JOIN [Squads] AS [g.Squad0] ON [t2].[SquadId] = [g.Squad0].[Id] +) AS [t3] ON [g.Squad.Members].[SquadId] = [t3].[Id] +WHERE [g.Squad.Members].[Discriminator] IN (N'Officer', N'Gear') AND ([g.Squad.Members].[HasSoulPatch] = 1) +ORDER BY [t3].[Note], [t3].[Nickname] DESC, [t3].[SquadId], [t3].[Id], [g.Squad.Members].[Nickname], [g.Squad.Members].[SquadId], [g.Squad.Members].[FullName]", + // + @"SELECT [g.Squad.Members.Weapons].[Id], [g.Squad.Members.Weapons].[AmmunitionType], [g.Squad.Members.Weapons].[IsAutomatic], [g.Squad.Members.Weapons].[Name], [g.Squad.Members.Weapons].[OwnerFullName], [g.Squad.Members.Weapons].[SynergyWithId], [t7].[Note], [t7].[Nickname], [t7].[SquadId], [t7].[Id], [t7].[Nickname0], [t7].[SquadId0], [t7].[FullName] +FROM [Weapons] AS [g.Squad.Members.Weapons] +INNER JOIN ( + SELECT [t6].[Note], [t6].[Nickname], [t6].[SquadId], [t6].[Id], [g.Squad.Members0].[Nickname] AS [Nickname0], [g.Squad.Members0].[SquadId] AS [SquadId0], [g.Squad.Members0].[FullName] + FROM [Gears] AS [g.Squad.Members0] + INNER JOIN ( + SELECT [t4].[Note], [t5].[Nickname], [t5].[SquadId], [g.Squad1].[Id] + FROM [Tags] AS [t4] + LEFT JOIN ( + SELECT [g1].* + FROM [Gears] AS [g1] + WHERE [g1].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t5] ON [t4].[GearNickName] = [t5].[Nickname] + LEFT JOIN [Squads] AS [g.Squad1] ON [t5].[SquadId] = [g.Squad1].[Id] + ) AS [t6] ON [g.Squad.Members0].[SquadId] = [t6].[Id] + WHERE [g.Squad.Members0].[Discriminator] IN (N'Officer', N'Gear') AND ([g.Squad.Members0].[HasSoulPatch] = 1) +) AS [t7] ON [g.Squad.Members.Weapons].[OwnerFullName] = [t7].[FullName] +WHERE [g.Squad.Members.Weapons].[IsAutomatic] = 1 +ORDER BY [t7].[Note], [t7].[Nickname] DESC, [t7].[SquadId], [t7].[Id], [t7].[Nickname0], [t7].[SquadId0], [t7].[FullName]"); + } + + public override void Correlated_collections_from_left_join_with_additional_elements_projected_of_that_join() + { + base.Correlated_collections_from_left_join_with_additional_elements_projected_of_that_join(); + + AssertSql( + @"SELECT [w.Owner.Squad].[Id] +FROM [Weapons] AS [w] +LEFT JOIN ( + SELECT [w.Owner].* + FROM [Gears] AS [w.Owner] + WHERE [w.Owner].[Discriminator] IN (N'Officer', N'Gear') +) AS [t] ON [w].[OwnerFullName] = [t].[FullName] +LEFT JOIN [Squads] AS [w.Owner.Squad] ON [t].[SquadId] = [w.Owner.Squad].[Id] +ORDER BY [w].[Name], [w].[Id], [w.Owner.Squad].[Id]", + // + @"SELECT [t1].[Name], [t1].[Id], [t1].[Id0], [w.Owner.Squad.Members].[FullName], [w.Owner.Squad.Members].[Rank], [w.Owner.Squad.Members].[SquadId] +FROM [Gears] AS [w.Owner.Squad.Members] +INNER JOIN ( + SELECT [w0].[Name], [w0].[Id], [w.Owner.Squad0].[Id] AS [Id0] + FROM [Weapons] AS [w0] + LEFT JOIN ( + SELECT [w.Owner0].* + FROM [Gears] AS [w.Owner0] + WHERE [w.Owner0].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t0] ON [w0].[OwnerFullName] = [t0].[FullName] + LEFT JOIN [Squads] AS [w.Owner.Squad0] ON [t0].[SquadId] = [w.Owner.Squad0].[Id] +) AS [t1] ON [w.Owner.Squad.Members].[SquadId] = [t1].[Id0] +WHERE [w.Owner.Squad.Members].[Discriminator] IN (N'Officer', N'Gear') +ORDER BY [t1].[Name], [t1].[Id], [t1].[Id0], [w.Owner.Squad.Members].[FullName] DESC, [w.Owner.Squad.Members].[Nickname], [w.Owner.Squad.Members].[SquadId]", + // + @"SELECT [w.Owner.Squad.Members.Weapons].[Id], [w.Owner.Squad.Members.Weapons].[AmmunitionType], [w.Owner.Squad.Members.Weapons].[IsAutomatic], [w.Owner.Squad.Members.Weapons].[Name], [w.Owner.Squad.Members.Weapons].[OwnerFullName], [w.Owner.Squad.Members.Weapons].[SynergyWithId], [t4].[Name], [t4].[Id], [t4].[Id0], [t4].[FullName], [t4].[Nickname], [t4].[SquadId] +FROM [Weapons] AS [w.Owner.Squad.Members.Weapons] +INNER JOIN ( + SELECT [t3].[Name], [t3].[Id], [t3].[Id0], [w.Owner.Squad.Members0].[FullName], [w.Owner.Squad.Members0].[Nickname], [w.Owner.Squad.Members0].[SquadId] + FROM [Gears] AS [w.Owner.Squad.Members0] + INNER JOIN ( + SELECT [w1].[Name], [w1].[Id], [w.Owner.Squad1].[Id] AS [Id0] + FROM [Weapons] AS [w1] + LEFT JOIN ( + SELECT [w.Owner1].* + FROM [Gears] AS [w.Owner1] + WHERE [w.Owner1].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t2] ON [w1].[OwnerFullName] = [t2].[FullName] + LEFT JOIN [Squads] AS [w.Owner.Squad1] ON [t2].[SquadId] = [w.Owner.Squad1].[Id] + ) AS [t3] ON [w.Owner.Squad.Members0].[SquadId] = [t3].[Id0] + WHERE [w.Owner.Squad.Members0].[Discriminator] IN (N'Officer', N'Gear') +) AS [t4] ON [w.Owner.Squad.Members.Weapons].[OwnerFullName] = [t4].[FullName] +WHERE [w.Owner.Squad.Members.Weapons].[IsAutomatic] = 0 +ORDER BY [t4].[Name], [t4].[Id], [t4].[Id0], [t4].[FullName] DESC, [t4].[Nickname], [t4].[SquadId], [w.Owner.Squad.Members.Weapons].[Id]"); + } + + public override void Correlated_collections_complex_scenario1() + { + base.Correlated_collections_complex_scenario1(); + + AssertSql( + @"SELECT [r].[FullName] +FROM [Gears] AS [r] +WHERE [r].[Discriminator] IN (N'Officer', N'Gear') +ORDER BY [r].[Nickname], [r].[SquadId], [r].[FullName]", + // + @"SELECT [t0].[Nickname], [t0].[SquadId], [t0].[FullName], [r.Weapons].[Id] AS [Id0], [w.Owner.Squad].[Id], [r.Weapons].[OwnerFullName] +FROM [Weapons] AS [r.Weapons] +LEFT JOIN ( + SELECT [w.Owner].* + FROM [Gears] AS [w.Owner] + WHERE [w.Owner].[Discriminator] IN (N'Officer', N'Gear') +) AS [t] ON [r.Weapons].[OwnerFullName] = [t].[FullName] +LEFT JOIN [Squads] AS [w.Owner.Squad] ON [t].[SquadId] = [w.Owner.Squad].[Id] +INNER JOIN ( + SELECT [r0].[Nickname], [r0].[SquadId], [r0].[FullName] + FROM [Gears] AS [r0] + WHERE [r0].[Discriminator] IN (N'Officer', N'Gear') +) AS [t0] ON [r.Weapons].[OwnerFullName] = [t0].[FullName] +ORDER BY [t0].[Nickname], [t0].[SquadId], [t0].[FullName], [r.Weapons].[Id], [w.Owner.Squad].[Id]", + // + @"SELECT [t3].[Nickname], [t3].[SquadId], [t3].[FullName], [t3].[Id], [t3].[Id0], [w.Owner.Squad.Members].[Nickname] AS [Nickname0], [w.Owner.Squad.Members].[HasSoulPatch], [w.Owner.Squad.Members].[SquadId] +FROM [Gears] AS [w.Owner.Squad.Members] +INNER JOIN ( + SELECT [t2].[Nickname], [t2].[SquadId], [t2].[FullName], [r.Weapons0].[Id], [w.Owner.Squad0].[Id] AS [Id0] + FROM [Weapons] AS [r.Weapons0] + LEFT JOIN ( + SELECT [w.Owner0].* + FROM [Gears] AS [w.Owner0] + WHERE [w.Owner0].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t1] ON [r.Weapons0].[OwnerFullName] = [t1].[FullName] + LEFT JOIN [Squads] AS [w.Owner.Squad0] ON [t1].[SquadId] = [w.Owner.Squad0].[Id] + INNER JOIN ( + SELECT [r1].[Nickname], [r1].[SquadId], [r1].[FullName] + FROM [Gears] AS [r1] + WHERE [r1].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t2] ON [r.Weapons0].[OwnerFullName] = [t2].[FullName] +) AS [t3] ON [w.Owner.Squad.Members].[SquadId] = [t3].[Id0] +WHERE [w.Owner.Squad.Members].[Discriminator] IN (N'Officer', N'Gear') +ORDER BY [t3].[Nickname], [t3].[SquadId], [t3].[FullName], [t3].[Id], [t3].[Id0], [Nickname0]"); + } + + public override void Correlated_collections_complex_scenario2() + { + base.Correlated_collections_complex_scenario2(); + + AssertSql( + @"SELECT [o].[FullName], [o].[Nickname], [o].[SquadId] +FROM [Gears] AS [o] +WHERE [o].[Discriminator] = N'Officer' +ORDER BY [o].[Nickname], [o].[SquadId]", + // + @"SELECT [t].[Nickname], [t].[SquadId], [o.Reports].[FullName], [o.Reports].[LeaderNickname], [o.Reports].[LeaderSquadId] +FROM [Gears] AS [o.Reports] +INNER JOIN ( + SELECT [o0].[Nickname], [o0].[SquadId] + FROM [Gears] AS [o0] + WHERE [o0].[Discriminator] = N'Officer' +) AS [t] ON ([o.Reports].[LeaderNickname] = [t].[Nickname]) AND ([o.Reports].[LeaderSquadId] = [t].[SquadId]) +WHERE [o.Reports].[Discriminator] IN (N'Officer', N'Gear') +ORDER BY [t].[Nickname], [t].[SquadId], [o.Reports].[Nickname], [o.Reports].[SquadId], [o.Reports].[FullName]", + // + @"SELECT [t2].[Nickname], [t2].[SquadId], [t2].[Nickname0], [t2].[SquadId0], [t2].[FullName], [o.Reports.Weapons].[Id] AS [Id0], [w.Owner.Squad].[Id], [o.Reports.Weapons].[OwnerFullName] +FROM [Weapons] AS [o.Reports.Weapons] +LEFT JOIN ( + SELECT [w.Owner].* + FROM [Gears] AS [w.Owner] + WHERE [w.Owner].[Discriminator] IN (N'Officer', N'Gear') +) AS [t0] ON [o.Reports.Weapons].[OwnerFullName] = [t0].[FullName] +LEFT JOIN [Squads] AS [w.Owner.Squad] ON [t0].[SquadId] = [w.Owner.Squad].[Id] +INNER JOIN ( + SELECT [t1].[Nickname], [t1].[SquadId], [o.Reports0].[Nickname] AS [Nickname0], [o.Reports0].[SquadId] AS [SquadId0], [o.Reports0].[FullName] + FROM [Gears] AS [o.Reports0] + INNER JOIN ( + SELECT [o1].[Nickname], [o1].[SquadId] + FROM [Gears] AS [o1] + WHERE [o1].[Discriminator] = N'Officer' + ) AS [t1] ON ([o.Reports0].[LeaderNickname] = [t1].[Nickname]) AND ([o.Reports0].[LeaderSquadId] = [t1].[SquadId]) + WHERE [o.Reports0].[Discriminator] IN (N'Officer', N'Gear') +) AS [t2] ON [o.Reports.Weapons].[OwnerFullName] = [t2].[FullName] +ORDER BY [t2].[Nickname], [t2].[SquadId], [t2].[Nickname0], [t2].[SquadId0], [t2].[FullName], [o.Reports.Weapons].[Id], [w.Owner.Squad].[Id]", + // + @"SELECT [t6].[Nickname], [t6].[SquadId], [t6].[Nickname0], [t6].[SquadId0], [t6].[FullName], [t6].[Id], [t6].[Id0], [w.Owner.Squad.Members].[Nickname] AS [Nickname1], [w.Owner.Squad.Members].[HasSoulPatch], [w.Owner.Squad.Members].[SquadId] +FROM [Gears] AS [w.Owner.Squad.Members] +INNER JOIN ( + SELECT [t5].[Nickname], [t5].[SquadId], [t5].[Nickname0], [t5].[SquadId0], [t5].[FullName], [o.Reports.Weapons0].[Id], [w.Owner.Squad0].[Id] AS [Id0] + FROM [Weapons] AS [o.Reports.Weapons0] + LEFT JOIN ( + SELECT [w.Owner0].* + FROM [Gears] AS [w.Owner0] + WHERE [w.Owner0].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t3] ON [o.Reports.Weapons0].[OwnerFullName] = [t3].[FullName] + LEFT JOIN [Squads] AS [w.Owner.Squad0] ON [t3].[SquadId] = [w.Owner.Squad0].[Id] + INNER JOIN ( + SELECT [t4].[Nickname], [t4].[SquadId], [o.Reports1].[Nickname] AS [Nickname0], [o.Reports1].[SquadId] AS [SquadId0], [o.Reports1].[FullName] + FROM [Gears] AS [o.Reports1] + INNER JOIN ( + SELECT [o2].[Nickname], [o2].[SquadId] + FROM [Gears] AS [o2] + WHERE [o2].[Discriminator] = N'Officer' + ) AS [t4] ON ([o.Reports1].[LeaderNickname] = [t4].[Nickname]) AND ([o.Reports1].[LeaderSquadId] = [t4].[SquadId]) + WHERE [o.Reports1].[Discriminator] IN (N'Officer', N'Gear') + ) AS [t5] ON [o.Reports.Weapons0].[OwnerFullName] = [t5].[FullName] +) AS [t6] ON [w.Owner.Squad.Members].[SquadId] = [t6].[Id0] +WHERE [w.Owner.Squad.Members].[Discriminator] IN (N'Officer', N'Gear') +ORDER BY [t6].[Nickname], [t6].[SquadId], [t6].[Nickname0], [t6].[SquadId0], [t6].[FullName], [t6].[Id], [t6].[Id0], [Nickname1]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryNavigationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryNavigationsSqlServerTest.cs index c099fd2d445..0ab200105b1 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryNavigationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryNavigationsSqlServerTest.cs @@ -483,12 +483,12 @@ public override void Select_collection_navigation_simple() base.Select_collection_navigation_simple(); AssertSql( - @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + @"SELECT [c].[CustomerID] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A' + N'%' AND (LEFT([c].[CustomerID], LEN(N'A')) = N'A') ORDER BY [c].[CustomerID]", // - @"SELECT [c.Orders].[OrderID], [c.Orders].[CustomerID], [c.Orders].[EmployeeID], [c.Orders].[OrderDate] + @"SELECT [c.Orders].[OrderID], [c.Orders].[CustomerID], [c.Orders].[EmployeeID], [c.Orders].[OrderDate], [t].[CustomerID] FROM [Orders] AS [c.Orders] INNER JOIN ( SELECT [c0].[CustomerID] @@ -503,21 +503,45 @@ public override void Select_collection_navigation_multi_part() base.Select_collection_navigation_multi_part(); AssertSql( - @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [o.Customer].[CustomerID], [o.Customer].[Address], [o.Customer].[City], [o.Customer].[CompanyName], [o.Customer].[ContactName], [o.Customer].[ContactTitle], [o.Customer].[Country], [o.Customer].[Fax], [o.Customer].[Phone], [o.Customer].[PostalCode], [o.Customer].[Region] + @"SELECT [o].[OrderID], [o.Customer].[CustomerID] FROM [Orders] AS [o] LEFT JOIN [Customers] AS [o.Customer] ON [o].[CustomerID] = [o.Customer].[CustomerID] WHERE [o].[CustomerID] = N'ALFKI' -ORDER BY [o.Customer].[CustomerID]", +ORDER BY [o].[OrderID], [o.Customer].[CustomerID]", // - @"SELECT [o.Customer.Orders].[OrderID], [o.Customer.Orders].[CustomerID], [o.Customer.Orders].[EmployeeID], [o.Customer.Orders].[OrderDate] + @"SELECT [o.Customer.Orders].[OrderID], [o.Customer.Orders].[CustomerID], [o.Customer.Orders].[EmployeeID], [o.Customer.Orders].[OrderDate], [t].[OrderID], [t].[CustomerID] FROM [Orders] AS [o.Customer.Orders] INNER JOIN ( - SELECT DISTINCT [o.Customer0].[CustomerID] + SELECT [o0].[OrderID], [o.Customer0].[CustomerID] FROM [Orders] AS [o0] LEFT JOIN [Customers] AS [o.Customer0] ON [o0].[CustomerID] = [o.Customer0].[CustomerID] WHERE [o0].[CustomerID] = N'ALFKI' ) AS [t] ON [o.Customer.Orders].[CustomerID] = [t].[CustomerID] -ORDER BY [t].[CustomerID]"); +ORDER BY [t].[OrderID], [t].[CustomerID]"); + } + + public override void Select_collection_navigation_multi_part2() + { + base.Select_collection_navigation_multi_part2(); + + AssertSql( + @"SELECT [od.Order.Customer].[CustomerID] +FROM [Order Details] AS [od] +INNER JOIN [Orders] AS [od.Order] ON [od].[OrderID] = [od.Order].[OrderID] +LEFT JOIN [Customers] AS [od.Order.Customer] ON [od.Order].[CustomerID] = [od.Order.Customer].[CustomerID] +WHERE [od.Order].[CustomerID] IN (N'ALFKI', N'ANTON') +ORDER BY [od].[OrderID], [od].[ProductID], [od.Order.Customer].[CustomerID]", + // + @"SELECT [od.Order.Customer.Orders].[OrderID], [od.Order.Customer.Orders].[CustomerID], [od.Order.Customer.Orders].[EmployeeID], [od.Order.Customer.Orders].[OrderDate], [t].[OrderID], [t].[ProductID], [t].[CustomerID] +FROM [Orders] AS [od.Order.Customer.Orders] +INNER JOIN ( + SELECT [od0].[OrderID], [od0].[ProductID], [od.Order.Customer0].[CustomerID] + FROM [Order Details] AS [od0] + INNER JOIN [Orders] AS [od.Order0] ON [od0].[OrderID] = [od.Order0].[OrderID] + LEFT JOIN [Customers] AS [od.Order.Customer0] ON [od.Order0].[CustomerID] = [od.Order.Customer0].[CustomerID] + WHERE [od.Order0].[CustomerID] IN (N'ALFKI', N'ANTON') +) AS [t] ON [od.Order.Customer.Orders].[CustomerID] = [t].[CustomerID] +ORDER BY [t].[OrderID], [t].[ProductID], [t].[CustomerID]"); } public override void Collection_select_nav_prop_any() diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.Select.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.Select.cs index 02cd7a6a4d3..b65636e6940 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.Select.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.Select.cs @@ -79,6 +79,24 @@ public override void Projection_when_null_value() FROM [Customers] AS [c]"); } + public override void Projection_when_client_evald_subquery() + { + base.Projection_when_client_evald_subquery(); + + AssertSql( + @"SELECT [c].[CustomerID] +FROM [Customers] AS [c] +ORDER BY [c].[CustomerID]", + // + @"SELECT [t].[CustomerID], [c.Orders].[CustomerID] +FROM [Orders] AS [c.Orders] +INNER JOIN ( + SELECT [c0].[CustomerID] + FROM [Customers] AS [c0] +) AS [t] ON [c.Orders].[CustomerID] = [t].[CustomerID] +ORDER BY [t].[CustomerID]"); + } + public override void Project_to_object_array() { base.Project_to_object_array();