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..ebc819fe6c8 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. /// 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..bd19cfcbd1f 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,11 @@ public override void VisitSelectClause( return; } + 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();