From da0db88b9783a87b96539589ffcfd84445695dff Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Tue, 23 Mar 2021 18:09:38 -0700 Subject: [PATCH] Query: Update column references in pending collections - Don't apply Include on entities with Include already applied - Update table references when pushing down select into left for set operation - Update identifiers after applying set operation if the projection removed exiting identifiers - Update SQL references in pending collection during push down Fix for the repro in #17337 Resolves #18738 Resolves #19763 Resolves #19947 Resolves #20813 Resolves #21026 Resolves #22222 Resolves #23676 Resolves #23720 Resolves #24216 --- .../Query/SqlExpressions/SelectExpression.cs | 64 +- ...ingExpressionVisitor.ExpressionVisitors.cs | 1 + .../Query/NorthwindSelectQueryCosmosTest.cs | 18 + .../Query/ComplexNavigationsQueryTestBase.cs | 76 +++ .../Query/NorthwindSelectQueryTestBase.cs | 72 +- .../NorthwindSetOperationsQueryTestBase.cs | 139 +++- .../ComplexNavigationsQuerySqlServerTest.cs | 54 ++ ...NavigationsSharedTypeQuerySqlServerTest.cs | 56 ++ .../NorthwindSelectQuerySqlServerTest.cs | 80 +++ ...orthwindSetOperationsQuerySqlServerTest.cs | 42 ++ .../Query/QueryBugsTest.cs | 621 +++++++++++++++++- .../ComplexNavigationsQuerySqliteTest.cs | 12 + ...lexNavigationsSharedTypeQuerySqliteTest.cs | 12 + .../Query/NorthwindSelectQuerySqliteTest.cs | 18 + 14 files changed, 1221 insertions(+), 44 deletions(-) diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index a0cabe3856b..e063afa08c7 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -1190,11 +1190,23 @@ private void ApplySetOperation(SetOperationType setOperationType, SelectExpressi select1._projectionMapping = new Dictionary(_projectionMapping); _projectionMapping.Clear(); + // Remap tableReferences in select1 + foreach (var tableReference in select1._tableReferences) + { + tableReference.UpdateTableReference(this, select1); + } + + var tableReferenceUpdatingExpressionVisitor = new TableReferenceUpdatingExpressionVisitor(this, select1); + tableReferenceUpdatingExpressionVisitor.Visit(select1); + select1._identifier.AddRange(_identifier); _identifier.Clear(); var outerIdentifiers = select1._identifier.Count == select2._identifier.Count ? new ColumnExpression?[select1._identifier.Count] : Array.Empty(); + var entityProjectionIdentifiers = new List(); + var entityProjectionValueComparers = new List(); + var otherExpressions = new List(); if (select1.Orderings.Count != 0 || select1.Limit != null @@ -1306,14 +1318,34 @@ private void ApplySetOperation(SetOperationType setOperationType, SelectExpressi outerIdentifiers = Array.Empty(); } } + + otherExpressions.Add(outerProjection); } } + // We should apply _identifiers only when it is distinct and actual select expression had identifiers. if (distinct - && outerIdentifiers.Length > 0 - && outerIdentifiers.All(e => e != null)) + && outerIdentifiers.Length > 0) { - _identifier.AddRange(outerIdentifiers.Zip(select1._identifier, (c, i) => (c!, i.Comparer))); + // If we find matching identifier in outer level then we just use them. + if (outerIdentifiers.All(e => e != null)) + { + _identifier.AddRange(outerIdentifiers.Zip(select1._identifier, (c, i) => (c!, i.Comparer))); + } + else + { + _identifier.Clear(); + if (otherExpressions.Count == 0) + { + // If there are no other expressions then we can use all entityProjectionIdentifiers + _identifier.AddRange(entityProjectionIdentifiers.Zip(entityProjectionValueComparers)); + } + else if (otherExpressions.All(e => e is ColumnExpression)) + { + _identifier.AddRange(entityProjectionIdentifiers.Zip(entityProjectionValueComparers)); + _identifier.AddRange(otherExpressions.Select(e => ((ColumnExpression)e, e.TypeMapping!.KeyComparer))); + } + } } void HandleEntityProjection( @@ -1377,8 +1409,22 @@ void HandleEntityProjection( discriminatorExpression = new ConcreteColumnExpression(innerProjection, tableReferenceExpression); } - _projectionMapping[projectionMember] = new EntityProjectionExpression( - projection1.EntityType, propertyExpressions, discriminatorExpression); + var entityProjection = new EntityProjectionExpression(projection1.EntityType, propertyExpressions, discriminatorExpression); + + if (outerIdentifiers.Length > 0) + { + var primaryKey = entityProjection.EntityType.FindPrimaryKey(); + // If there are any existing identifier then all entity projection must have a key + // else keyless entity would have wiped identifier when generating join. + Check.DebugAssert(primaryKey != null, "primary key is null."); + foreach (var property in primaryKey.Properties) + { + entityProjectionIdentifiers.Add(entityProjection.BindProperty(property)); + entityProjectionValueComparers.Add(property.GetKeyValueComparer()); + } + } + + _projectionMapping[projectionMember] = entityProjection; } string GenerateUniqueColumnAlias(string baseAlias) @@ -2831,6 +2877,14 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) Offset = (SqlExpression?)visitor.Visit(Offset); Limit = (SqlExpression?)visitor.Visit(Limit); + if (visitor is SqlRemappingVisitor) + { + // We have to traverse pending collections for remapping so that columns from outer are updated. + var pendingCollections = _pendingCollections.ToList(); + _pendingCollections.Clear(); + _pendingCollections.AddRange(pendingCollections.Select(e => (SelectExpression)visitor.Visit(e)!)); + } + return this; } var changed = false; diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs index 395fbf1abb3..8f40882d3c9 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs @@ -538,6 +538,7 @@ protected override Expression VisitExtension(Expression extensionExpression) return ExpandInclude(ownedNavigationReference, ownedNavigationReference.EntityReference); case MaterializeCollectionNavigationExpression _: + case IncludeExpression _: return extensionExpression; } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs index 14bb309cb72..8982c6a4fae 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs @@ -1284,6 +1284,24 @@ public override Task Collection_include_over_result_of_single_non_scalar(bool as return base.Collection_include_over_result_of_single_non_scalar(async); } + [ConditionalTheory(Skip = "Cross collection join Issue#17246")] + public override Task Collection_projection_selecting_outer_element_followed_by_take(bool async) + { + return base.Collection_projection_selecting_outer_element_followed_by_take(async); + } + + [ConditionalTheory(Skip = "Cross collection join Issue#17246")] + public override Task Take_on_top_level_and_on_collection_projection_with_outer_apply(bool async) + { + return base.Take_on_top_level_and_on_collection_projection_with_outer_apply(async); + } + + [ConditionalTheory(Skip = "Cross collection join Issue#17246")] + public override Task Take_on_correlated_collection_in_first(bool async) + { + return base.Take_on_correlated_collection_in_first(async); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs index 26a2aa4c477..1537dfb2394 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs @@ -6093,5 +6093,81 @@ orderby l2.OneToOne_Optional_FK2.MaybeScalar(e => e.Id) AssertEqual(e.Property, a.Property); }); } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Take_Select_collection_Take(bool async) + { + return AssertQuery( + async, + ss => ss.Set().OrderBy(l1 => l1.Id).Take(1) + .Select(l1 => new + { + Id = l1.Id, + Name = l1.Name, + Level2s = l1.OneToMany_Required1.OrderBy(l2 => l2.Id).Take(3) + .Select(l2 => new + { + Id = l2.Id, + Name = l2.Name, + Level1Id = EF.Property(l2, "OneToMany_Required_Inverse2Id"), + Level2Id = l2.Level1_Required_Id, + Level2 = l2.OneToOne_Required_FK_Inverse2 + }) + }), + assertOrder: true, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + Assert.Equal(e.Name, a.Name); + AssertCollection(e.Level2s, a.Level2s, ordered: true, + elementAsserter: (ee, aa) => + { + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.Name, aa.Name); + Assert.Equal(ee.Level1Id, aa.Level1Id); + Assert.Equal(ee.Level2Id, aa.Level2Id); + AssertEqual(ee.Level2, aa.Level2); + }); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Skip_Take_Select_collection_Skip_Take(bool async) + { + return AssertQuery( + async, + ss => ss.Set().OrderBy(l1 => l1.Id).Skip(1).Take(1) + .Select(l1 => new + { + Id = l1.Id, + Name = l1.Name, + Level2s = l1.OneToMany_Required1.OrderBy(l2 => l2.Id).Skip(1).Take(3) + .Select(l2 => new + { + Id = l2.Id, + Name = l2.Name, + Level1Id = EF.Property(l2, "OneToMany_Required_Inverse2Id"), + Level2Id = l2.Level1_Required_Id, + Level2 = l2.OneToOne_Required_FK_Inverse2 + }) + }), + assertOrder: true, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + Assert.Equal(e.Name, a.Name); + AssertCollection(e.Level2s, a.Level2s, ordered: true, + elementAsserter: (ee, aa) => + { + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.Name, aa.Name); + Assert.Equal(ee.Level1Id, aa.Level1Id); + Assert.Equal(ee.Level2Id, aa.Level2Id); + AssertEqual(ee.Level2, aa.Level2); + }); + }); + } } } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs index 8cece5c2fa2..ff898ead293 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs @@ -2168,6 +2168,8 @@ public virtual Task Ternary_in_client_eval_assigns_correct_types(bool async) }); } + private static string ClientMethod(string s) => s; + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task VisitLambda_should_not_be_visited_trivially(bool async) @@ -2246,6 +2248,74 @@ public virtual Task Collection_include_over_result_of_single_non_scalar(bool asy entryCount: 235); } - private static string ClientMethod(string s) => s; + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Collection_projection_selecting_outer_element_followed_by_take(bool async) + { + return AssertQuery( + async, + ss => + ss.Set().Include(c => c.Orders) + .Where(c => c.CustomerID.StartsWith("F")) + .OrderBy(e => e.CustomerID) + .Select(c => new + { + Customer = c.Orders.Select(o => c) + }) + .Take(10), + assertOrder: true, + elementAsserter: (e, a) => + { + AssertCollection(e.Customer, a.Customer, + elementAsserter: (ee, aa) => AssertInclude(ee, aa, new ExpectedInclude(i => i.Orders))); + }, + entryCount: 70); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Take_on_top_level_and_on_collection_projection_with_outer_apply(bool async) + { + return AssertFirstOrDefault( + async, + ss => + ss.Set() + .Where(o => o.CustomerID.StartsWith("F")) + .Select(o => new Order + { + OrderID = o.OrderID, + OrderDate = o.OrderDate, + OrderDetails = o.OrderDetails.Select(e => new OrderDetail + { + OrderID = e.OrderID, + Product = e.Product, + UnitPrice = e.UnitPrice + }) + .OrderByDescending(e => e.OrderID) + .Skip(0) + .Take(10) + .ToList() + }), + entryCount: 2); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Take_on_correlated_collection_in_first(bool async) + { + return AssertFirstOrDefault( + async, + ss => + ss.Set() + .Where(o => o.CustomerID.StartsWith("F")) + .OrderBy(e => e.CustomerID) + .Select(o => new + { + Orders = o.Orders.OrderBy(a => a.OrderDate).Take(1) + .Select(e => new { Title = e.CustomerID == e.Customer.CustomerID ? "A" : "B" }).ToList() + }), + asserter: (e, a) => AssertCollection(e.Orders, a.Orders, ordered: true, + elementAsserter: (ee, aa) => AssertEqual(ee.Title, aa.Title))); + } } } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindSetOperationsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindSetOperationsQueryTestBase.cs index 688d08541c9..d2a67e9ad6a 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindSetOperationsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindSetOperationsQueryTestBase.cs @@ -32,7 +32,8 @@ protected virtual void ClearLog() public virtual Task Concat(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "Berlin") .Concat(ss.Set().Where(c => c.City == "London")), entryCount: 7); @@ -43,7 +44,8 @@ public virtual Task Concat(bool async) public virtual Task Concat_nested(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "México D.F.") .Concat(ss.Set().Where(s => s.City == "Berlin")) .Concat(ss.Set().Where(e => e.City == "London")), @@ -55,7 +57,8 @@ public virtual Task Concat_nested(bool async) public virtual Task Concat_non_entity(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "México D.F.") .Select(c => c.CustomerID) .Concat( @@ -69,7 +72,8 @@ public virtual Task Concat_non_entity(bool async) public virtual Task Except(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "London") .Except(ss.Set().Where(c => c.ContactName.Contains("Thomas"))), entryCount: 5); @@ -80,7 +84,8 @@ public virtual Task Except(bool async) public virtual Task Except_simple_followed_by_projecting_constant(bool async) { return AssertQueryScalar( - async, ss => ss.Set() + async, + ss => ss.Set() .Except(ss.Set()) .Select(e => 1)); } @@ -90,7 +95,8 @@ public virtual Task Except_simple_followed_by_projecting_constant(bool async) public virtual Task Except_nested(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(s => s.ContactTitle == "Owner") .Except(ss.Set().Where(s => s.City == "México D.F.")) .Except(ss.Set().Where(e => e.City == "Seattle")), @@ -102,7 +108,8 @@ public virtual Task Except_nested(bool async) public virtual Task Except_non_entity(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(s => s.ContactTitle == "Owner") .Select(c => c.CustomerID) .Except( @@ -116,7 +123,8 @@ public virtual Task Except_non_entity(bool async) public virtual Task Intersect(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "London") .Intersect(ss.Set().Where(c => c.ContactName.Contains("Thomas"))), entryCount: 1); @@ -127,7 +135,8 @@ public virtual Task Intersect(bool async) public virtual Task Intersect_nested(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "México D.F.") .Intersect(ss.Set().Where(s => s.ContactTitle == "Owner")) .Intersect(ss.Set().Where(e => e.Fax != null)), @@ -139,7 +148,8 @@ public virtual Task Intersect_nested(bool async) public virtual Task Intersect_non_entity(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "México D.F.") .Select(c => c.CustomerID) .Intersect( @@ -153,7 +163,8 @@ public virtual Task Intersect_non_entity(bool async) public virtual Task Union(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "Berlin") .Union(ss.Set().Where(c => c.City == "London")), entryCount: 7); @@ -164,7 +175,8 @@ public virtual Task Union(bool async) public virtual Task Union_nested(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(s => s.ContactTitle == "Owner") .Union(ss.Set().Where(s => s.City == "México D.F.")) .Union(ss.Set().Where(e => e.City == "London")), @@ -176,7 +188,8 @@ public virtual Task Union_nested(bool async) public virtual Task Union_non_entity(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(s => s.ContactTitle == "Owner") .Select(c => c.CustomerID) .Union( @@ -191,7 +204,8 @@ public virtual Task Union_non_entity(bool async) public virtual Task Union_OrderBy_Skip_Take(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "Berlin") .Union(ss.Set().Where(c => c.City == "London")) .OrderBy(c => c.ContactName) @@ -207,7 +221,8 @@ public virtual Task Union_OrderBy_Skip_Take(bool async) public virtual Task Union_Where(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "Berlin") .Union(ss.Set().Where(c => c.City == "London")) .Where(c => c.ContactName.Contains("Thomas")), // pushdown @@ -220,7 +235,8 @@ public virtual Task Union_Where(bool async) public virtual Task Union_Skip_Take_OrderBy_ThenBy_Where(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "Berlin") .Union(ss.Set().Where(c => c.City == "London")) .OrderBy(c => c.Region) @@ -236,7 +252,8 @@ public virtual Task Union_Skip_Take_OrderBy_ThenBy_Where(bool async) public virtual Task Union_Union(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "Berlin") .Union(ss.Set().Where(c => c.City == "London")) .Union(ss.Set().Where(c => c.City == "Mannheim")), @@ -250,7 +267,8 @@ public virtual Task Union_Union(bool async) public virtual Task Union_Intersect(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "Berlin") .Union(ss.Set().Where(c => c.City == "London")) .Intersect(ss.Set().Where(c => c.ContactName.Contains("Thomas"))), @@ -262,7 +280,8 @@ public virtual Task Union_Intersect(bool async) public virtual Task Union_Take_Union_Take(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "Berlin") .Union(ss.Set().Where(c => c.City == "London")) .OrderBy(c => c.CustomerID) @@ -278,7 +297,8 @@ public virtual Task Union_Take_Union_Take(bool async) public virtual Task Select_Union(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "Berlin") .Select(c => c.Address) .Union( @@ -292,7 +312,8 @@ public virtual Task Select_Union(bool async) public virtual Task Union_Select(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "Berlin") .Union(ss.Set().Where(c => c.City == "London")) .Select(c => c.Address) @@ -304,7 +325,8 @@ public virtual Task Union_Select(bool async) public virtual Task Union_Select_scalar(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Except(ss.Set()) .Select(c => (object)1)); } @@ -314,7 +336,8 @@ public virtual Task Union_Select_scalar(bool async) public virtual Task Union_with_anonymous_type_projection(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.CompanyName.StartsWith("A")) .Union(ss.Set().Where(c => c.CompanyName.StartsWith("B"))) .Select(c => new CustomerDeets { Id = c.CustomerID })); @@ -360,7 +383,8 @@ public virtual Task Select_Union_unrelated(bool async) public virtual Task Select_Union_different_fields_in_anonymous_with_subquery(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "Berlin") .Select(c => new { Foo = c.City, Customer = c }) // Foo is City .Union( @@ -379,7 +403,8 @@ public virtual Task Select_Union_different_fields_in_anonymous_with_subquery(boo public virtual Task Union_Include(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "Berlin") .Union(ss.Set().Where(c => c.City == "London")) .Include(c => c.Orders), @@ -391,7 +416,8 @@ public virtual Task Union_Include(bool async) public virtual Task Include_Union(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "Berlin") .Include(c => c.Orders) .Union( @@ -406,7 +432,8 @@ public virtual Task Include_Union(bool async) public virtual Task Select_Except_reference_projection(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Select(o => o.Customer) .Except( ss.Set() @@ -486,7 +513,8 @@ private static Customer ClientSideMethod(Customer c) public virtual Task GroupBy_Select_Union(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "Berlin") .GroupBy(c => c.CustomerID) .Select(g => new { CustomerID = g.Key, Count = g.Count() }) @@ -502,7 +530,8 @@ public virtual Task GroupBy_Select_Union(bool async) public virtual Task Union_over_columns_with_different_nullability(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Select(c => "NonNullableConstant") .Concat( ss.Set() @@ -559,7 +588,8 @@ from rightType in _supportedOperandExpressionType public virtual Task OrderBy_Take_Union(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .OrderBy(c => c.ContactName) .Take(1) .Union( @@ -575,7 +605,8 @@ public virtual Task OrderBy_Take_Union(bool async) public virtual Task Collection_projection_after_set_operation(bool async) { return AssertQuery( - async, ss => ss.Set().Where(c => c.City == "Seatte") + async, + ss => ss.Set().Where(c => c.City == "Seatte") .Union(ss.Set().Where(c => c.CustomerID.StartsWith("F"))) .Select(c => new { @@ -596,7 +627,8 @@ public virtual Task Collection_projection_after_set_operation(bool async) public virtual Task Collection_projection_after_set_operation_fails_if_distinct(bool async) { return AssertQuery( - async, ss => ss.Set().Where(c => c.City == "Seatte") + async, + ss => ss.Set().Where(c => c.City == "Seatte") .Concat(ss.Set().Where(c => c.CustomerID.StartsWith("F"))) .Select(c => new { @@ -617,7 +649,8 @@ public virtual Task Collection_projection_after_set_operation_fails_if_distinct( public virtual Task Collection_projection_before_set_operation_fails(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.City == "Seatte") .Select(c => new { @@ -642,7 +675,8 @@ public virtual Task Collection_projection_before_set_operation_fails(bool async) public virtual Task Concat_with_one_side_being_GroupBy_aggregate(bool async) { return AssertQuery( - async, ss => ss.Set() + async, + ss => ss.Set() .Where(c => c.Customer.City == "Seatte") .Select(c => new { @@ -660,5 +694,42 @@ public virtual Task Concat_with_one_side_being_GroupBy_aggregate(bool async) AssertEqual(e.OrderDate, a.OrderDate); }); } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_on_entity_with_correlated_collection(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Customer.City == "Seatte").Select(c => c.Customer) + .Union(ss.Set().Where(o => o.OrderID < 10250).Select(c => c.Customer)) + .OrderBy(c => c.CustomerID) + .Select(c => c.Orders), + assertOrder: true, + elementAsserter: (e, a) => + { + AssertCollection(e, a); + }, + entryCount: 11); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_on_entity_plus_other_column_with_correlated_collection(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Customer.City == "Seatte").Select(c => new { c.Customer, c.OrderDate }) + .Union(ss.Set().Where(o => o.OrderID < 10250).Select(c => new { c.Customer, c.OrderDate })) + .OrderBy(c => c.Customer.CustomerID) + .Select(c => new { c.OrderDate, Orders = ss.Set().Where(o => o.CustomerID == c.Customer.CustomerID).ToList() }), + assertOrder: true, + elementAsserter: (e, a) => + { + AssertEqual(e.OrderDate, a.OrderDate); + AssertCollection(e.Orders, a.Orders); + }, + entryCount: 11); + } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index b2b5fe34e05..390ba4d56ec 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -6226,6 +6226,60 @@ WHERE [l15].[Id] <> 42 ORDER BY [l12].[Id], [l].[Id], [l0].[Id], [l1].[Id], [l2].[Id], [t].[Id], [t].[Id0], [t].[Id1], [t].[Id2], [t0].[Id], [t0].[Id0], [t0].[Id1], [t0].[Id2], [l11].[Id], [l13].[Id], [l14].[Id], [t1].[Id]"); } + public override async Task Take_Select_collection_Take(bool async) + { + await base.Take_Select_collection_Take(async); + + AssertSql( + @"@__p_0='1' + +SELECT [t].[Id], [t].[Name], [t0].[Id], [t0].[Name], [t0].[Level1Id], [t0].[Level2Id], [t0].[Id0], [t0].[Date], [t0].[Name0], [t0].[OneToMany_Optional_Self_Inverse1Id], [t0].[OneToMany_Required_Self_Inverse1Id], [t0].[OneToOne_Optional_Self1Id] +FROM ( + SELECT TOP(@__p_0) [l].[Id], [l].[Name] + FROM [LevelOne] AS [l] + ORDER BY [l].[Id] +) AS [t] +OUTER APPLY ( + SELECT [t1].[Id], [t1].[Name], [t1].[OneToMany_Required_Inverse2Id] AS [Level1Id], [t1].[Level1_Required_Id] AS [Level2Id], [l0].[Id] AS [Id0], [l0].[Date], [l0].[Name] AS [Name0], [l0].[OneToMany_Optional_Self_Inverse1Id], [l0].[OneToMany_Required_Self_Inverse1Id], [l0].[OneToOne_Optional_Self1Id] + FROM ( + SELECT TOP(3) [l1].[Id], [l1].[Level1_Required_Id], [l1].[Name], [l1].[OneToMany_Required_Inverse2Id] + FROM [LevelTwo] AS [l1] + WHERE [t].[Id] = [l1].[OneToMany_Required_Inverse2Id] + ORDER BY [l1].[Id] + ) AS [t1] + INNER JOIN [LevelOne] AS [l0] ON [t1].[Level1_Required_Id] = [l0].[Id] +) AS [t0] +ORDER BY [t].[Id], [t0].[Id], [t0].[Id0]"); + } + + public override async Task Skip_Take_Select_collection_Skip_Take(bool async) + { + await base.Skip_Take_Select_collection_Skip_Take(async); + + AssertSql( + @"@__p_0='1' + +SELECT [t].[Id], [t].[Name], [t0].[Id], [t0].[Name], [t0].[Level1Id], [t0].[Level2Id], [t0].[Id0], [t0].[Date], [t0].[Name0], [t0].[OneToMany_Optional_Self_Inverse1Id], [t0].[OneToMany_Required_Self_Inverse1Id], [t0].[OneToOne_Optional_Self1Id] +FROM ( + SELECT [l].[Id], [l].[Name] + FROM [LevelOne] AS [l] + ORDER BY [l].[Id] + OFFSET @__p_0 ROWS FETCH NEXT @__p_0 ROWS ONLY +) AS [t] +OUTER APPLY ( + SELECT [t1].[Id], [t1].[Name], [t1].[OneToMany_Required_Inverse2Id] AS [Level1Id], [t1].[Level1_Required_Id] AS [Level2Id], [l0].[Id] AS [Id0], [l0].[Date], [l0].[Name] AS [Name0], [l0].[OneToMany_Optional_Self_Inverse1Id], [l0].[OneToMany_Required_Self_Inverse1Id], [l0].[OneToOne_Optional_Self1Id] + FROM ( + SELECT [l1].[Id], [l1].[Level1_Required_Id], [l1].[Name], [l1].[OneToMany_Required_Inverse2Id] + FROM [LevelTwo] AS [l1] + WHERE [t].[Id] = [l1].[OneToMany_Required_Inverse2Id] + ORDER BY [l1].[Id] + OFFSET 1 ROWS FETCH NEXT 3 ROWS ONLY + ) AS [t1] + INNER JOIN [LevelOne] AS [l0] ON [t1].[Level1_Required_Id] = [l0].[Id] +) AS [t0] +ORDER BY [t].[Id], [t0].[Id], [t0].[Id0]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs index 8ebd6ff5245..6230aa89b42 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs @@ -324,6 +324,62 @@ WHERE [l2].[OneToMany_Required_Inverse2Id] IS NOT NULL AND ([l2].[Level1_Require ORDER BY [l].[Id], [t].[Id], [t].[Name], [t].[FK], [t0].[Id], [t0].[Id0]"); } + public override async Task Take_Select_collection_Take(bool async) + { + await base.Take_Select_collection_Take(async); + + AssertSql( + @"@__p_0='1' + +SELECT [t].[Id], [t].[Name], [t0].[Id], [t0].[Name], [t0].[Level1Id], [t0].[Level2Id], [t0].[Id0], [t0].[Date], [t0].[Name0], [t0].[Id00] +FROM ( + SELECT TOP(@__p_0) [l].[Id], [l].[Name] + FROM [Level1] AS [l] + ORDER BY [l].[Id] +) AS [t] +OUTER APPLY ( + SELECT [t1].[Id], [t1].[Level2_Name] AS [Name], [t1].[OneToMany_Required_Inverse2Id] AS [Level1Id], [t1].[Level1_Required_Id] AS [Level2Id], [l1].[Id] AS [Id0], [l1].[Date], [l1].[Name] AS [Name0], [t1].[Id0] AS [Id00] + FROM ( + SELECT TOP(3) [l0].[Id], [l0].[Level1_Required_Id], [l0].[Level2_Name], [l0].[OneToMany_Required_Inverse2Id], [l2].[Id] AS [Id0] + FROM [Level1] AS [l0] + INNER JOIN [Level1] AS [l2] ON [l0].[Id] = [l2].[Id] + WHERE ([l0].[OneToMany_Required_Inverse2Id] IS NOT NULL AND ([l0].[Level1_Required_Id] IS NOT NULL AND [l0].[OneToOne_Required_PK_Date] IS NOT NULL)) AND ([t].[Id] = [l0].[OneToMany_Required_Inverse2Id]) + ORDER BY [l0].[Id] + ) AS [t1] + INNER JOIN [Level1] AS [l1] ON [t1].[Level1_Required_Id] = [l1].[Id] +) AS [t0] +ORDER BY [t].[Id], [t0].[Id], [t0].[Id00], [t0].[Id0]"); + } + + public override async Task Skip_Take_Select_collection_Skip_Take(bool async) + { + await base.Skip_Take_Select_collection_Skip_Take(async); + + AssertSql( + @"@__p_0='1' + +SELECT [t].[Id], [t].[Name], [t0].[Id], [t0].[Name], [t0].[Level1Id], [t0].[Level2Id], [t0].[Id0], [t0].[Date], [t0].[Name0], [t0].[Id00] +FROM ( + SELECT [l].[Id], [l].[Name] + FROM [Level1] AS [l] + ORDER BY [l].[Id] + OFFSET @__p_0 ROWS FETCH NEXT @__p_0 ROWS ONLY +) AS [t] +OUTER APPLY ( + SELECT [t1].[Id], [t1].[Level2_Name] AS [Name], [t1].[OneToMany_Required_Inverse2Id] AS [Level1Id], [t1].[Level1_Required_Id] AS [Level2Id], [l1].[Id] AS [Id0], [l1].[Date], [l1].[Name] AS [Name0], [t1].[Id0] AS [Id00] + FROM ( + SELECT [l0].[Id], [l0].[Level1_Required_Id], [l0].[Level2_Name], [l0].[OneToMany_Required_Inverse2Id], [l2].[Id] AS [Id0] + FROM [Level1] AS [l0] + INNER JOIN [Level1] AS [l2] ON [l0].[Id] = [l2].[Id] + WHERE ([l0].[OneToMany_Required_Inverse2Id] IS NOT NULL AND ([l0].[Level1_Required_Id] IS NOT NULL AND [l0].[OneToOne_Required_PK_Date] IS NOT NULL)) AND ([t].[Id] = [l0].[OneToMany_Required_Inverse2Id]) + ORDER BY [l0].[Id] + OFFSET 1 ROWS FETCH NEXT 3 ROWS ONLY + ) AS [t1] + INNER JOIN [Level1] AS [l1] ON [t1].[Level1_Required_Id] = [l1].[Id] +) AS [t0] +ORDER BY [t].[Id], [t0].[Id], [t0].[Id00], [t0].[Id0]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs index 3c54dd1a8fd..75e9584bd26 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs @@ -1744,6 +1744,86 @@ WHERE [c].[CustomerID] LIKE N'F%' ORDER BY [c].[CustomerID], [t0].[OrderID], [t1].[OrderID], [t1].[OrderID0], [t1].[ProductID], [o2].[OrderID], [o2].[ProductID]"); } + public override async Task Collection_projection_selecting_outer_element_followed_by_take(bool async) + { + await base.Collection_projection_selecting_outer_element_followed_by_take(async); + + AssertSql( + @"@__p_0='10' + +SELECT [t].[CustomerID], [t0].[CustomerID], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region], [t0].[OrderID], [t0].[OrderID0], [t0].[CustomerID0], [t0].[EmployeeID], [t0].[OrderDate] +FROM ( + SELECT TOP(@__p_0) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + WHERE [c].[CustomerID] LIKE N'F%' + ORDER BY [c].[CustomerID] +) AS [t] +OUTER APPLY ( + SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [t1].[OrderID] AS [OrderID0], [t1].[CustomerID] AS [CustomerID0], [t1].[EmployeeID], [t1].[OrderDate] + FROM [Orders] AS [o] + OUTER APPLY ( + SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] + FROM [Orders] AS [o0] + WHERE [t].[CustomerID] = [o0].[CustomerID] + ) AS [t1] + WHERE [t].[CustomerID] = [o].[CustomerID] +) AS [t0] +ORDER BY [t].[CustomerID], [t0].[OrderID], [t0].[OrderID0]"); + } + + public override async Task Take_on_top_level_and_on_collection_projection_with_outer_apply(bool async) + { + await base.Take_on_top_level_and_on_collection_projection_with_outer_apply(async); + + AssertSql( + @"SELECT [t].[OrderID], [t].[OrderDate], [t0].[OrderID], [t0].[ProductID], [t0].[Discontinued], [t0].[ProductName], [t0].[SupplierID], [t0].[UnitPrice], [t0].[UnitsInStock], [t0].[UnitPrice0] AS [UnitPrice], [t0].[ProductID0] +FROM ( + SELECT TOP(1) [o].[OrderID], [o].[OrderDate] + FROM [Orders] AS [o] + WHERE [o].[CustomerID] IS NOT NULL AND ([o].[CustomerID] LIKE N'F%') +) AS [t] +OUTER APPLY ( + SELECT [t1].[OrderID], [p].[ProductID], [p].[Discontinued], [p].[ProductName], [p].[SupplierID], [p].[UnitPrice], [p].[UnitsInStock], [t1].[UnitPrice] AS [UnitPrice0], [t1].[ProductID] AS [ProductID0] + FROM ( + SELECT [o0].[OrderID], [o0].[ProductID], [o0].[UnitPrice] + FROM [Order Details] AS [o0] + WHERE [t].[OrderID] = [o0].[OrderID] + ORDER BY [o0].[OrderID] DESC + OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY + ) AS [t1] + INNER JOIN [Products] AS [p] ON [t1].[ProductID] = [p].[ProductID] +) AS [t0] +ORDER BY [t].[OrderID], [t0].[OrderID] DESC, [t0].[ProductID0], [t0].[ProductID]"); + } + + public override async Task Take_on_correlated_collection_in_first(bool async) + { + await base.Take_on_correlated_collection_in_first(async); + + AssertSql( + @"SELECT [t].[CustomerID], [t0].[Title], [t0].[OrderID], [t0].[CustomerID] +FROM ( + SELECT TOP(1) [c].[CustomerID] + FROM [Customers] AS [c] + WHERE [c].[CustomerID] LIKE N'F%' + ORDER BY [c].[CustomerID] +) AS [t] +OUTER APPLY ( + SELECT CASE + WHEN ([t1].[CustomerID] = [c0].[CustomerID]) OR ([t1].[CustomerID] IS NULL AND [c0].[CustomerID] IS NULL) THEN N'A' + ELSE N'B' + END AS [Title], [t1].[OrderID], [c0].[CustomerID], [t1].[OrderDate] + FROM ( + SELECT TOP(1) [o].[OrderID], [o].[CustomerID], [o].[OrderDate] + FROM [Orders] AS [o] + WHERE [t].[CustomerID] = [o].[CustomerID] + ORDER BY [o].[OrderDate] + ) AS [t1] + LEFT JOIN [Customers] AS [c0] ON [t1].[CustomerID] = [c0].[CustomerID] +) AS [t0] +ORDER BY [t].[CustomerID], [t0].[OrderDate], [t0].[OrderID], [t0].[CustomerID]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSetOperationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSetOperationsQuerySqlServerTest.cs index bf2153020ca..b0d53d4319f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSetOperationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSetOperationsQuerySqlServerTest.cs @@ -576,6 +576,48 @@ FROM [Orders] AS [o0] GROUP BY [o0].[CustomerID]"); } + public override async Task Union_on_entity_with_correlated_collection(bool async) + { + await base.Union_on_entity_with_correlated_collection(async); + + AssertSql( + @"SELECT [t].[CustomerID], [o1].[OrderID], [o1].[CustomerID], [o1].[EmployeeID], [o1].[OrderDate] +FROM ( + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Orders] AS [o] + LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] + WHERE [c].[City] = N'Seatte' + UNION + SELECT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region] + FROM [Orders] AS [o0] + LEFT JOIN [Customers] AS [c0] ON [o0].[CustomerID] = [c0].[CustomerID] + WHERE [o0].[OrderID] < 10250 +) AS [t] +LEFT JOIN [Orders] AS [o1] ON [t].[CustomerID] = [o1].[CustomerID] +ORDER BY [t].[CustomerID], [o1].[OrderID]"); + } + + public override async Task Union_on_entity_plus_other_column_with_correlated_collection(bool async) + { + await base.Union_on_entity_plus_other_column_with_correlated_collection(async); + + AssertSql( + @"SELECT [t].[OrderDate], [t].[CustomerID], [o1].[OrderID], [o1].[CustomerID], [o1].[EmployeeID], [o1].[OrderDate] +FROM ( + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderDate] + FROM [Orders] AS [o] + LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] + WHERE [c].[City] = N'Seatte' + UNION + SELECT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region], [o0].[OrderDate] + FROM [Orders] AS [o0] + LEFT JOIN [Customers] AS [c0] ON [o0].[CustomerID] = [c0].[CustomerID] + WHERE [o0].[OrderID] < 10250 +) AS [t] +LEFT JOIN [Orders] AS [o1] ON [t].[CustomerID] = [o1].[CustomerID] +ORDER BY [t].[CustomerID], [t].[OrderDate], [o1].[OrderID]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index 3939bafb6ff..531db16e307 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -8377,8 +8377,8 @@ public virtual async Task Projecting_correlated_collection_along_with_non_mapped var result = context.Blogs.Select( e => new { - Id = e.Id, - Title = e.Title, + e.Id, + e.Title, FirstPostName = e.Posts.Where(i => i.Name.Contains("2")).ToList() }).ToList(); @@ -8399,8 +8399,8 @@ WHERE [p].[Name] LIKE N'%2%' var result = context.Blogs.Select( e => new { - Id = e.Id, - Title = e.Title, + e.Id, + e.Title, FirstPostName = e.Posts.OrderBy(i => i.Id).FirstOrDefault().Name }).ToList(); @@ -9410,6 +9410,619 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) #endregion + #region Issue23676 + + [ConditionalFact] + public virtual async Task Projection_with_multiple_includes_and_subquery_with_set_operation() + { + var contextFactory = await InitializeAsync(); + + using var context = contextFactory.CreateContext(); + var id = 1; + var person = await context.Persons + .Include(p => p.Images) + .Include(p => p.Actor) + .ThenInclude(a => a.Movies) + .ThenInclude(p => p.Movie) + .Include(p => p.Director) + .ThenInclude(a => a.Movies) + .ThenInclude(p => p.Movie) + .Select(x => new + { + x.Id, + x.Name, + x.Surname, + x.Birthday, + x.Hometown, + x.Bio, + x.AvatarUrl, + + Images = x.Images + .Select(i => new + { + i.Id, + i.ImageUrl, + i.Height, + i.Width + }).ToList(), + + KnownByFilms = x.Actor.Movies + .Select(m => m.Movie) + .Union(x.Director.Movies + .Select(m => m.Movie)) + .Select(m => new + { + m.Id, + m.Name, + m.PosterUrl, + m.Rating + }).ToList() + + }) + .FirstOrDefaultAsync(x => x.Id == id); + + // Verify the valid generated SQL + AssertSql( + @"@__id_0='1' + +SELECT [t].[Id], [t].[Name], [t].[Surname], [t].[Birthday], [t].[Hometown], [t].[Bio], [t].[AvatarUrl], [t].[Id0], [t].[Id1], [p0].[Id], [p0].[ImageUrl], [p0].[Height], [p0].[Width], [t0].[Id], [t0].[Name], [t0].[PosterUrl], [t0].[Rating] +FROM ( + SELECT TOP(1) [p].[Id], [p].[Name], [p].[Surname], [p].[Birthday], [p].[Hometown], [p].[Bio], [p].[AvatarUrl], [a].[Id] AS [Id0], [d].[Id] AS [Id1] + FROM [Persons] AS [p] + LEFT JOIN [ActorEntity] AS [a] ON [p].[Id] = [a].[PersonId] + LEFT JOIN [DirectorEntity] AS [d] ON [p].[Id] = [d].[PersonId] + WHERE [p].[Id] = @__id_0 +) AS [t] +LEFT JOIN [PersonImageEntity] AS [p0] ON [t].[Id] = [p0].[PersonId] +OUTER APPLY ( + SELECT [m0].[Id], [m0].[Budget], [m0].[Description], [m0].[DurationInMins], [m0].[Name], [m0].[PosterUrl], [m0].[Rating], [m0].[ReleaseDate], [m0].[Revenue] + FROM [MovieActorEntity] AS [m] + INNER JOIN [MovieEntity] AS [m0] ON [m].[MovieId] = [m0].[Id] + WHERE [t].[Id0] IS NOT NULL AND ([t].[Id0] = [m].[ActorId]) + UNION + SELECT [m2].[Id], [m2].[Budget], [m2].[Description], [m2].[DurationInMins], [m2].[Name], [m2].[PosterUrl], [m2].[Rating], [m2].[ReleaseDate], [m2].[Revenue] + FROM [MovieDirectorEntity] AS [m1] + INNER JOIN [MovieEntity] AS [m2] ON [m1].[MovieId] = [m2].[Id] + WHERE [t].[Id1] IS NOT NULL AND ([t].[Id1] = [m1].[DirectorId]) +) AS [t0] +ORDER BY [t].[Id], [t].[Id0], [t].[Id1], [p0].[Id], [t0].[Id]"); + } + + private class PersonEntity + { + public int Id { get; set; } + public string Name { get; set; } + public string Surname { get; set; } + public DateTime Birthday { get; set; } + public string Hometown { get; set; } + public string Bio { get; set; } + public string AvatarUrl { get; set; } + + public ActorEntity Actor { get; set; } + public DirectorEntity Director { get; set; } + public IList Images { get; set; } = new List(); + } + private class PersonImageEntity + { + public int Id { get; set; } + public string ImageUrl { get; set; } + public int Height { get; set; } + public int Width { get; set; } + public PersonEntity Person { get; set; } + } + + private class ActorEntity + { + public int Id { get; set; } + public int PersonId { get; set; } + public PersonEntity Person { get; set; } + + public IList Movies { get; set; } = new List(); + } + + private class MovieActorEntity + { + public int Id { get; set; } + public int ActorId { get; set; } + public ActorEntity Actor { get; set; } + + public int MovieId { get; set; } + public MovieEntity Movie { get; set; } + + public string RoleInFilm { get; set; } + + public int Order { get; set; } + } + + private class DirectorEntity + { + public int Id { get; set; } + public int PersonId { get; set; } + public PersonEntity Person { get; set; } + + public IList Movies { get; set; } = new List(); + } + + private class MovieDirectorEntity + { + public int Id { get; set; } + public int DirectorId { get; set; } + public DirectorEntity Director { get; set; } + + public int MovieId { get; set; } + public MovieEntity Movie { get; set; } + } + + private class MovieEntity + { + public int Id { get; set; } + public string Name { get; set; } + public double Rating { get; set; } + public string Description { get; set; } + public DateTime ReleaseDate { get; set; } + public int DurationInMins { get; set; } + public int Budget { get; set; } + public int Revenue { get; set; } + public string PosterUrl { get; set; } + + public IList Directors { get; set; } = new List(); + public IList Actors { get; set; } = new List(); + } + + private class MyContext23676 : DbContext + { + public MyContext23676(DbContextOptions options) + : base(options) + { + } + + public DbSet Persons { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + } + } + + #endregion + + #region Issue19947 + + [ConditionalFact] + public virtual async Task Multiple_select_many_in_projection() + { + var contextFactory = await InitializeAsync(); + + using var context = contextFactory.CreateContext(); + var query = context.Users.Select(captain => new + { + CaptainRateDtos = captain.Cars + .SelectMany(car0 => car0.Taxis) + .OrderByDescending(taxi => taxi.DateArrived).Take(12) + .Select(taxi => new + { + Rate = taxi.UserRate.Value, + UserRateText = taxi.UserTextRate, + UserId = taxi.UserEUser.Id, + }).ToList(), + + ReportCount = captain.Cars + .SelectMany(car1 => car1.Taxis).Count(taxi0 => taxi0.ReportText != ""), + }).SingleOrDefault(); + + // Verify the valid generated SQL + AssertSql( + @"SELECT [t0].[c], [t0].[Id], [t1].[Rate], [t1].[UserRateText], [t1].[UserId], [t1].[Id], [t1].[Id0] +FROM ( + SELECT TOP(2) ( + SELECT COUNT(*) + FROM [Cars] AS [c] + INNER JOIN [Taxis] AS [t] ON [c].[Id] = [t].[CarId] + WHERE ([u].[Id] = [c].[EUserId]) AND (([t].[ReportText] <> N'') OR [t].[ReportText] IS NULL)) AS [c], [u].[Id] + FROM [Users] AS [u] +) AS [t0] +OUTER APPLY ( + SELECT [t2].[UserRate] AS [Rate], [t2].[UserTextRate] AS [UserRateText], [u0].[Id] AS [UserId], [t2].[Id], [t2].[Id0], [t2].[DateArrived] + FROM ( + SELECT TOP(12) [c0].[Id], [t3].[Id] AS [Id0], [t3].[DateArrived], [t3].[UserEUserId], [t3].[UserRate], [t3].[UserTextRate] + FROM [Cars] AS [c0] + INNER JOIN [Taxis] AS [t3] ON [c0].[Id] = [t3].[CarId] + WHERE [t0].[Id] = [c0].[EUserId] + ORDER BY [t3].[DateArrived] DESC + ) AS [t2] + LEFT JOIN [Users] AS [u0] ON [t2].[UserEUserId] = [u0].[Id] +) AS [t1] +ORDER BY [t0].[Id], [t1].[DateArrived] DESC, [t1].[Id], [t1].[Id0], [t1].[UserId]"); + } + + [ConditionalFact] + public virtual async Task Single_select_many_in_projection_with_take() + { + var contextFactory = await InitializeAsync(); + + using var context = contextFactory.CreateContext(); + var query = context.Users.Select(captain => new + { + CaptainRateDtos = captain.Cars + .SelectMany(car0 => car0.Taxis) + .OrderByDescending(taxi => taxi.DateArrived).Take(12) + .Select(taxi => new + { + Rate = taxi.UserRate.Value, + UserRateText = taxi.UserTextRate, + UserId = taxi.UserEUser.Id, + }).ToList() + }).SingleOrDefault(); + + // Verify the valid generated SQL + AssertSql( + @"SELECT [t].[Id], [t0].[Rate], [t0].[UserRateText], [t0].[UserId], [t0].[Id], [t0].[Id0] +FROM ( + SELECT TOP(2) [u].[Id] + FROM [Users] AS [u] +) AS [t] +OUTER APPLY ( + SELECT [t1].[UserRate] AS [Rate], [t1].[UserTextRate] AS [UserRateText], [u0].[Id] AS [UserId], [t1].[Id], [t1].[Id0], [t1].[DateArrived] + FROM ( + SELECT TOP(12) [c].[Id], [t2].[Id] AS [Id0], [t2].[DateArrived], [t2].[UserEUserId], [t2].[UserRate], [t2].[UserTextRate] + FROM [Cars] AS [c] + INNER JOIN [Taxis] AS [t2] ON [c].[Id] = [t2].[CarId] + WHERE [t].[Id] = [c].[EUserId] + ORDER BY [t2].[DateArrived] DESC + ) AS [t1] + LEFT JOIN [Users] AS [u0] ON [t1].[UserEUserId] = [u0].[Id] +) AS [t0] +ORDER BY [t].[Id], [t0].[DateArrived] DESC, [t0].[Id], [t0].[Id0], [t0].[UserId]"); + } + + private class EUser + { + public int Id { get; set; } + + public ICollection Cars { get; set; } + } + + private class Taxi + { + public int Id { get; set; } + public DateTime? DateArrived { get; set; } + public int? UserRate { get; set; } + public string UserTextRate { get; set; } + public string ReportText { get; set; } + public EUser UserEUser { get; set; } + } + + private class Car + { + public int Id { get; set; } + public ICollection Taxis { get; set; } + } + + private class MyContext19947 : DbContext + { + public MyContext19947(DbContextOptions options) + : base(options) + { + } + + public DbSet Users { get; set; } + public DbSet Cars { get; set; } + public DbSet Taxis { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + } + } + + #endregion + + #region Issue20813 + + [ConditionalFact] + public virtual async Task SelectMany_and_collection_in_projection_in_FirstOrDefault() + { + var contextFactory = await InitializeAsync(); + + using var context = contextFactory.CreateContext(); + var referenceId = "a"; + var customerId = new Guid("1115c816-6c4c-4016-94df-d8b60a22ffa1"); + var query = context.Orders + .Where(o => o.ExternalReferenceId == referenceId && o.CustomerId == customerId) + .Select(o => new + { + IdentityDocuments = o.IdentityDocuments.Select(id => new + { + Images = o.IdentityDocuments + .SelectMany(id => id.Images) + .Select(i => new + { + Image = i.Image + }), + }) + }).SingleOrDefault(); + + // Verify the valid generated SQL + AssertSql( + @"@__referenceId_0='a' (Size = 4000) +@__customerId_1='1115c816-6c4c-4016-94df-d8b60a22ffa1' + +SELECT [t].[Id], [t0].[Id], [t0].[Image], [t0].[Id0], [t0].[Id00] +FROM ( + SELECT TOP(2) [o].[Id] + FROM [Orders] AS [o] + WHERE ([o].[ExternalReferenceId] = @__referenceId_0) AND ([o].[CustomerId] = @__customerId_1) +) AS [t] +OUTER APPLY ( + SELECT [i].[Id], [t1].[Image], [t1].[Id] AS [Id0], [t1].[Id0] AS [Id00] + FROM [IdentityDocument] AS [i] + OUTER APPLY ( + SELECT [i1].[Image], [i0].[Id], [i1].[Id] AS [Id0] + FROM [IdentityDocument] AS [i0] + INNER JOIN [IdentityDocumentImage] AS [i1] ON [i0].[Id] = [i1].[IdentityDocumentId] + WHERE [t].[Id] = [i0].[OrderId] + ) AS [t1] + WHERE [t].[Id] = [i].[OrderId] +) AS [t0] +ORDER BY [t].[Id], [t0].[Id], [t0].[Id0], [t0].[Id00]"); + } + + private class Order + { + private ICollection _identityDocuments; + + public Guid Id { get; set; } + + public Guid CustomerId { get; set; } + + public string ExternalReferenceId { get; set; } + + public ICollection IdentityDocuments + { + get => _identityDocuments = _identityDocuments ?? new Collection(); + set => _identityDocuments = value; + } + } + + private class IdentityDocument + { + private ICollection _images; + + public Guid Id { get; set; } + + [ForeignKey(nameof(Order))] + public Guid OrderId { get; set; } + + public Order Order { get; set; } + + public ICollection Images + { + get => _images = _images ?? new Collection(); + set => _images = value; + } + } + + private class IdentityDocumentImage + { + public Guid Id { get; set; } + + [ForeignKey(nameof(IdentityDocument))] + public Guid IdentityDocumentId { get; set; } + + public byte[] Image { get; set; } + + public IdentityDocument IdentityDocument { get; set; } + } + + private class MyContext20813 : DbContext + { + public MyContext20813(DbContextOptions options) + : base(options) + { + } + + public DbSet Orders { get; set; } + } + + #endregion + + #region Issue18738 + + [ConditionalFact] + public virtual async Task Set_operation_in_pending_collection() + { + var contextFactory = await InitializeAsync(); + + using var context = contextFactory.CreateContext(); + var resultCollection = context.StudentGameMapper + .OrderBy(s => s.Id) + .Select(s => new StudentGameResult + { + SportsList = ( + from inDoorSports in context.InDoorSports + where inDoorSports.Id == s.InCategoryId + select inDoorSports.Name) + .Union( + from outDoorSports in context.OutDoorSports + where outDoorSports.Id == s.OutCategoryId + select outDoorSports.Name) + .ToList() + }) + .Take(5) // Without this line the query works + .ToList(); + + // Verify the valid generated SQL + AssertSql( + @"@__p_0='5' + +SELECT [t].[Id], [t0].[Name] +FROM ( + SELECT TOP(@__p_0) [s].[Id], [s].[InCategoryId], [s].[OutCategoryId] + FROM [StudentGameMapper] AS [s] + ORDER BY [s].[Id] +) AS [t] +OUTER APPLY ( + SELECT [i].[Name] + FROM [InDoorSports] AS [i] + WHERE [i].[Id] = [t].[InCategoryId] + UNION + SELECT [o].[Name] + FROM [OutDoorSports] AS [o] + WHERE [o].[Id] = [t].[OutCategoryId] +) AS [t0] +ORDER BY [t].[Id], [t0].[Name]"); + } + + private class StudentGameMapper + { + public int Id { get; set; } + public int InCategoryId { get; set; } + public int OutCategoryId { get; set; } + } + + private class InDoorSports + { + public int Id { get; set; } + public string Name { get; set; } + } + + private class OutDoorSports + { + public int Id { get; set; } + public string Name { get; set; } + } + + private class StudentGameResult + { + public int GroupId { get; set; } + public int StudentId { get; set; } + public List SportsList { get; set; } + } + + private class MyContext18738 : DbContext + { + public MyContext18738(DbContextOptions options) + : base(options) + { + } + + public DbSet StudentGameMapper { get; set; } + public DbSet InDoorSports { get; set; } + public DbSet OutDoorSports { get; set; } + } + + #endregion + + #region Issue24216 + + [ConditionalFact] + public virtual async Task Subquery_take_SelectMany_with_TVF() + { + var contextFactory = await InitializeAsync(); + + using var context = contextFactory.CreateContext(); + + context.Database.ExecuteSqlRaw( + @"create function [dbo].[GetPersonStatusAsOf] (@personId bigint, @timestamp datetime2) + returns @personStatus table + ( + Id bigint not null, + PersonId bigint not null, + GenderId bigint not null, + StatusMessage nvarchar(max) + ) + as + begin + insert into @personStatus + select [m].[Id], [m].[PersonId], [m].[PersonId], null + from [Message] as [m] + where [m].[PersonId] = @personId and [m].[TimeStamp] = @timestamp + return + end"); + + ClearLog(); + + var q = from m in context.Message + orderby m.Id + select m; + + var q2 = + from m in q.Take(10) + from asof in context.GetPersonStatusAsOf(m.PersonId, m.Timestamp) + select new + { + Gender = (from g in context.Gender where g.Id == asof.GenderId select g.Description).Single() + }; + + q2.ToList(); + + // Verify the valid generated SQL + AssertSql( + @"@__p_0='10' + +SELECT ( + SELECT TOP(1) [g0].[Description] + FROM [Gender] AS [g0] + WHERE [g0].[Id] = [g].[GenderId]) AS [Gender] +FROM ( + SELECT TOP(@__p_0) [m].[Id], [m].[PersonId], [m].[Timestamp] + FROM [Message] AS [m] + ORDER BY [m].[Id] +) AS [t] +CROSS APPLY [dbo].[GetPersonStatusAsOf]([t].[PersonId], [t].[Timestamp]) AS [g] +ORDER BY [t].[Id]"); + } + + private class Gender + { + public long Id { get; set; } + + public string Description { get; set; } + } + + private class Message + { + public long Id { get; set; } + + public long PersonId { get; set; } + + public DateTime Timestamp { get; set; } + } + + private class PersonStatus + { + public long Id { get; set; } + + public long PersonId { get; set; } + + public long GenderId { get; set; } + + public string StatusMessage { get; set; } + } + + private class MyContext24216 : DbContext + { + public MyContext24216(DbContextOptions options) + : base(options) + { + } + + public DbSet Gender { get; set; } + + public DbSet Message { get; set; } + + public IQueryable GetPersonStatusAsOf(long personId, DateTime asOf) + => FromExpression(() => GetPersonStatusAsOf(personId, asOf)); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.HasDbFunction(typeof(MyContext24216).GetMethod(nameof(GetPersonStatusAsOf), + new[] { typeof(long), typeof(DateTime) })); + } + } + + #endregion + protected override string StoreName => "QueryBugsTest"; protected TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsQuerySqliteTest.cs index 50b657dd703..24297cdb512 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsQuerySqliteTest.cs @@ -94,5 +94,17 @@ public override async Task Complex_query_with_let_collection_projection_FirstOrD SqliteStrings.ApplyNotSupported, (await Assert.ThrowsAsync( () => base.Complex_query_with_let_collection_projection_FirstOrDefault(async))).Message); + + public override async Task Take_Select_collection_Take(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Take_Select_collection_Take(async))).Message); + + public override async Task Skip_Take_Select_collection_Skip_Take(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Skip_Take_Select_collection_Skip_Take(async))).Message); } } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqliteTest.cs index 967d48aabe9..a3755db71d9 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqliteTest.cs @@ -84,5 +84,17 @@ public override async Task Filtered_include_same_filter_set_on_same_navigation_t SqliteStrings.ApplyNotSupported, (await Assert.ThrowsAsync( () => base.Filtered_include_same_filter_set_on_same_navigation_twice_followed_by_ThenIncludes_split(async))).Message); + + public override async Task Take_Select_collection_Take(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Take_Select_collection_Take(async))).Message); + + public override async Task Skip_Take_Select_collection_Skip_Take(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Skip_Take_Select_collection_Skip_Take(async))).Message); } } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSelectQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSelectQuerySqliteTest.cs index a0e6c8904ac..2847d12f919 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSelectQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSelectQuerySqliteTest.cs @@ -241,6 +241,24 @@ public override Task SelectMany_with_collection_being_correlated_subquery_which_ return base.SelectMany_with_collection_being_correlated_subquery_which_references_inner_and_outer_entity(async); } + public override async Task Collection_projection_selecting_outer_element_followed_by_take(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Collection_projection_selecting_outer_element_followed_by_take(async))).Message); + + public override async Task Take_on_top_level_and_on_collection_projection_with_outer_apply(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Take_on_top_level_and_on_collection_projection_with_outer_apply(async))).Message); + + public override async Task Take_on_correlated_collection_in_first(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Take_on_correlated_collection_in_first(async))).Message); + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); }