From 4f54ae909efc295bebd12b51547b64650ccfa026 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 26 Jun 2019 14:04:33 +0200 Subject: [PATCH] Addressed review comments Also found some more skipped set operation tests, unskipped and rearranged them. Fixes #12568 --- .../SqlExpressions/SelectExpression.cs | 39 ++- src/EFCore/Properties/CoreStrings.Designer.cs | 5 + src/EFCore/Properties/CoreStrings.resx | 5 +- .../NavigationExpansion/NavigationTreeNode.cs | 11 +- .../NavigationExpandingVisitor_MethodCall.cs | 30 +++ .../SimpleQueryTestBase.ResultOperators.cs | 236 ------------------ .../SimpleQueryTestBase.SetOperations.cs | 182 ++++++++++++-- ...impleQuerySqlServerTest.ResultOperators.cs | 14 -- .../SimpleQuerySqlServerTest.SetOperations.cs | 29 +++ 9 files changed, 250 insertions(+), 301 deletions(-) diff --git a/src/EFCore.Relational/Query/Pipeline/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/Pipeline/SqlExpressions/SelectExpression.cs index 0d3739259ff..549ea967a06 100644 --- a/src/EFCore.Relational/Query/Pipeline/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/Pipeline/SqlExpressions/SelectExpression.cs @@ -502,18 +502,8 @@ void HandleEntityMapping( if (commonParentEntityType != projection1.EntityType) { // The first source has been up-cast by the set operation, so we also need to change the shaper expression. - var entityShaperExpression = - shaperExpression as EntityShaperExpression ?? ( - shaperExpression is UnaryExpression unary - && unary.NodeType == ExpressionType.Convert - && unary.Type == commonParentEntityType.ClrType - ? unary.Operand as EntityShaperExpression : null); - - if (entityShaperExpression != null) - { - shaperExpression = new EntityShaperExpression( - commonParentEntityType, entityShaperExpression.ValueBufferExpression, entityShaperExpression.Nullable); - } + // The EntityShaperExpression may be buried under Convert nodes produced by Cast operators, preserve those. + shaperExpression = UpdateEntityShaperEntityType(shaperExpression, commonParentEntityType); } } @@ -567,6 +557,21 @@ void HandleColumnMapping( var outerColumn = new ColumnExpression(projectionExpression1, select1, IsNullableProjection(projectionExpression1)); _projectionMapping[projectionMember] = outerColumn; } + + static Expression UpdateEntityShaperEntityType(Expression shaperExpression, IEntityType newEntityType) + { + switch (shaperExpression) + { + case EntityShaperExpression entityShaperExpression: + return new EntityShaperExpression(newEntityType, entityShaperExpression.ValueBufferExpression, entityShaperExpression.Nullable); + case UnaryExpression unary when unary.NodeType == ExpressionType.Convert: + return Convert(UpdateEntityShaperEntityType(unary.Operand, newEntityType), unary.Type); + case UnaryExpression unary when unary.NodeType == ExpressionType.ConvertChecked: + return ConvertChecked(UpdateEntityShaperEntityType(unary.Operand, newEntityType), unary.Type); + default: + throw new Exception($"Unexpected expression type {shaperExpression.GetType().Name} encountered in {nameof(UpdateEntityShaperEntityType)}"); + } + } } public IDictionary PushdownIntoSubquery() @@ -716,8 +721,7 @@ public RelationalCollectionShaperExpression ApplyCollectionJoin(int collectionId { if (IsDistinct || Limit != null || Offset != null || IsSetOperation) { - var pushdown = PushdownIntoSubquery(); - outer = new SqlRemappingVisitor(pushdown).Remap(outer); + outer = new SqlRemappingVisitor(PushdownIntoSubquery()).Remap(outer); } if (innerSelectExpression.Offset != null @@ -1332,12 +1336,7 @@ public enum SetOperationType /// /// Represents an SQL EXCEPT set operation. /// - Except = 4, - - /// - /// Represents a custom, provider-specific set operation. - /// - Other = 9999 + Except = 4 } } diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index e05ea096d11..bb345cc6eaa 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -2148,6 +2148,11 @@ public static string UnableToDiscriminate([CanBeNull] object entityType, [CanBeN GetString("UnableToDiscriminate", nameof(entityType), nameof(discriminator)), entityType, discriminator); + /// When performing a set operation, both operands must have the same Include operations. + /// + public static string SetOperationWithDifferentIncludesInOperands + => GetString("SetOperationWithDifferentIncludesInOperands"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 6a6b0a7030b..9afbf82d8f8 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1186,4 +1186,7 @@ Unable to materialize entity of type '{entityType}'. No discriminators matched '{discriminator}'. - \ No newline at end of file + + When performing a set operation, both operands must have the same Include operations. + + diff --git a/src/EFCore/Query/NavigationExpansion/NavigationTreeNode.cs b/src/EFCore/Query/NavigationExpansion/NavigationTreeNode.cs index e1e266d7d3b..c18e0446e9c 100644 --- a/src/EFCore/Query/NavigationExpansion/NavigationTreeNode.cs +++ b/src/EFCore/Query/NavigationExpansion/NavigationTreeNode.cs @@ -132,17 +132,14 @@ public static NavigationTreeNode Create( return result; } - public List Flatten() + public IEnumerable Flatten() { - var result = new List(); - result.Add(this); + yield return this; - foreach (var child in Children) + foreach (var child in Children.SelectMany(c => c.Flatten())) { - result.AddRange(child.Flatten()); + yield return child; } - - return result; } // TODO: just make property settable? diff --git a/src/EFCore/Query/NavigationExpansion/Visitors/NavigationExpandingVisitor_MethodCall.cs b/src/EFCore/Query/NavigationExpansion/Visitors/NavigationExpandingVisitor_MethodCall.cs index 4f3dc58371e..01c9eef6ede 100644 --- a/src/EFCore/Query/NavigationExpansion/Visitors/NavigationExpandingVisitor_MethodCall.cs +++ b/src/EFCore/Query/NavigationExpansion/Visitors/NavigationExpandingVisitor_MethodCall.cs @@ -7,6 +7,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Xml; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -873,6 +874,35 @@ private Expression ProcessSetOperation(MethodCallExpression methodCallExpression var source2 = VisitSourceExpression(methodCallExpression.Arguments[1]); var preProcessResult2 = PreProcessTerminatingOperation(source2); + // Compare the include chains from each side to make sure they're identical. We don't allow set operations over + // operands with different include chains. + var current1 = preProcessResult1.state.PendingIncludeChain?.NavigationTreeNode; + var current2 = preProcessResult2.state.PendingIncludeChain?.NavigationTreeNode; + while (true) + { + if (current1 == null) + { + if (current2 == null) + { + break; + } + throw new NotSupportedException(CoreStrings.SetOperationWithDifferentIncludesInOperands); + } + + if (current2 == null) + { + throw new NotSupportedException(CoreStrings.SetOperationWithDifferentIncludesInOperands); + } + + if (current1.FromMappings.Zip(current2.FromMappings, (m1, m2) => (m1, m2)) + .Any(t => !t.m1.SequenceEqual(t.m2))) + { + throw new NotSupportedException(CoreStrings.SetOperationWithDifferentIncludesInOperands); + } + + (current1, current2) = (current1.Parent, current2.Parent); + } + // If the siblings are different types, one is derived from the other the set operation returns the less derived type. // Find that. var clrType1 = preProcessResult1.state.CurrentParameter.Type; diff --git a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.ResultOperators.cs b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.ResultOperators.cs index 73e6e9b79b3..d68def7bb88 100644 --- a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.ResultOperators.cs +++ b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.ResultOperators.cs @@ -22,21 +22,6 @@ namespace Microsoft.EntityFrameworkCore.Query { public abstract partial class SimpleQueryTestBase { - [ConditionalTheory(Skip = "Issue #6812. Cannot eval 'Union({from Customer c in value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer]) where [c].CompanyName.StartsWith(\"B\") select [c]})'")] - [MemberData(nameof(IsAsyncData))] - public virtual Task Union_with_custom_projection(bool isAsync) - { - return AssertQuery( - isAsync, - cs => cs.Where(c => c.CompanyName.StartsWith("A")) - .Union(cs.Where(c => c.CompanyName.StartsWith("B"))) - .Select( - c => new CustomerDeets - { - Id = c.CustomerID - })); - } - public class CustomerDeets { public string Id { get; set; } @@ -1460,227 +1445,6 @@ public virtual void OfType_Select_OfType_Select() } } - [ConditionalFact(Skip = "Issue #6812. Cannot eval 'Concat({value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer])})'")] - public virtual void Concat_dbset() - { - using (var context = CreateContext()) - { - var query = context.Set() - .Where(c => c.City == "México D.F.") - .Concat(context.Set()) - .ToList(); - - Assert.Equal(96, query.Count); - } - } - - [ConditionalFact(Skip = "Issue #6812. Cannot eval 'Concat({from Customer s in value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer]) where ([s].ContactTitle == \"Owner\") select [s]})'")] - public virtual void Concat_simple() - { - using (var context = CreateContext()) - { - var query = context.Set() - .Where(c => c.City == "México D.F.") - .Concat( - context.Set() - .Where(s => s.ContactTitle == "Owner")) - .ToList(); - - Assert.Equal(22, query.Count); - } - } - - [ConditionalTheory(Skip = "Issue #6812. Cannot eval 'Concat({from Customer s in value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer]) where ([s].City == \"Berlin\") select [s]})'")] - [MemberData(nameof(IsAsyncData))] - public virtual Task Concat_nested(bool isAsync) - { - return AssertQuery( - isAsync, - cs => cs.Where(c => c.City == "México D.F.") - .Concat(cs.Where(s => s.City == "Berlin")) - .Concat(cs.Where(e => e.City == "London")), - entryCount: 12); - } - - [ConditionalFact(Skip = "Issue #6812. Cannot eval 'Concat({from Customer s in value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer]) where ([s].ContactTitle == \"Owner\") select [s].CustomerID})'")] - public virtual void Concat_non_entity() - { - using (var context = CreateContext()) - { - var query = context.Set() - .Where(c => c.City == "México D.F.") - .Select(c => c.CustomerID) - .Concat( - context.Set() - .Where(s => s.ContactTitle == "Owner") - .Select(c => c.CustomerID)) - .ToList(); - - Assert.Equal(22, query.Count); - } - } - - [ConditionalTheory(Skip = "Issue #6812. Cannot eval 'Except({value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer])})'")] - [MemberData(nameof(IsAsyncData))] - public virtual Task Except_dbset(bool isAsync) - { - return AssertQuery( - isAsync, - cs => cs.Where(s => s.ContactTitle == "Owner").Except(cs)); - } - - [ConditionalTheory(Skip = "Issue #6812. Cannot eval 'Except({from Customer c in value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer]) where ([c].City == \"México D.F.\") select [c]})'")] - [MemberData(nameof(IsAsyncData))] - public virtual Task Except_simple(bool isAsync) - { - return AssertQuery( - isAsync, - cs => cs.Where(s => s.ContactTitle == "Owner") - .Except(cs.Where(c => c.City == "México D.F.")), - entryCount: 14); - } - - [ConditionalTheory(Skip = "Issue#12568")] - [MemberData(nameof(IsAsyncData))] - public virtual Task Except_simple_followed_by_projecting_constant(bool isAsync) - { - return AssertQueryScalar( - isAsync, - cs => cs.Except(cs).Select(e => 1)); - } - - [ConditionalTheory(Skip = "Issue #6812. Cannot eval 'Except({from Customer s in value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer]) where ([s].City == \"México D.F.\") select [s]})'")] - [MemberData(nameof(IsAsyncData))] - public virtual Task Except_nested(bool isAsync) - { - return AssertQuery( - isAsync, - cs => cs.Where(s => s.ContactTitle == "Owner") - .Except(cs.Where(s => s.City == "México D.F.")) - .Except(cs.Where(e => e.City == "Seattle")), - entryCount: 13); - } - - [ConditionalFact(Skip = "Issue #6812. Cannot eval 'Except({from Customer c in value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer]) where ([c].City == \"México D.F.\") select [c].CustomerID})'")] - public virtual void Except_non_entity() - { - using (var context = CreateContext()) - { - var query = context.Set() - .Where(s => s.ContactTitle == "Owner") - .Select(c => c.CustomerID) - .Except( - context.Set() - .Where(c => c.City == "México D.F.") - .Select(c => c.CustomerID)) - .ToList(); - - Assert.Equal(14, query.Count); - } - } - - [ConditionalTheory(Skip = "Issue #6812. Cannot eval 'Intersect({value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer])})'")] - [MemberData(nameof(IsAsyncData))] - public virtual Task Intersect_dbset(bool isAsync) - { - return AssertQuery( - isAsync, - cs => cs.Where(c => c.City == "México D.F.").Intersect(cs), - entryCount: 5); - } - - [ConditionalTheory(Skip = "Issue #6812. Cannot eval 'Intersect({from Customer s in value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer]) where ([s].ContactTitle == \"Owner\") select [s]})'")] - [MemberData(nameof(IsAsyncData))] - public virtual Task Intersect_simple(bool isAsync) - { - return AssertQuery( - isAsync, - cs => cs.Where(c => c.City == "México D.F.") - .Intersect(cs.Where(s => s.ContactTitle == "Owner")), - entryCount: 3); - } - - [ConditionalTheory(Skip = "Issue #6812. Cannot eval 'Intersect({from Customer s in value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer]) where ([s].ContactTitle == \"Owner\") select [s]})'")] - [MemberData(nameof(IsAsyncData))] - public virtual Task Intersect_nested(bool isAsync) - { - return AssertQuery( - isAsync, - cs => cs.Where(c => c.City == "México D.F.") - .Intersect(cs.Where(s => s.ContactTitle == "Owner")) - .Intersect(cs.Where(e => e.Fax != null)), - entryCount: 1); - } - - [ConditionalFact(Skip = "Issue #6812. Cannot eval 'Intersect({from Customer s in value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer]) where ([s].ContactTitle == \"Owner\") select [s].CustomerID})'")] - public virtual void Intersect_non_entity() - { - using (var context = CreateContext()) - { - var query = context.Set() - .Where(c => c.City == "México D.F.") - .Select(c => c.CustomerID) - .Intersect( - context.Set() - .Where(s => s.ContactTitle == "Owner") - .Select(c => c.CustomerID)) - .ToList(); - - Assert.Equal(3, query.Count); - } - } - - [ConditionalTheory(Skip = "Issue #6812. Cannot eval 'Union({value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer])})'")] - [MemberData(nameof(IsAsyncData))] - public virtual Task Union_dbset(bool isAsync) - { - return AssertQuery( - isAsync, - cs => cs.Where(s => s.ContactTitle == "Owner").Union(cs), - entryCount: 91); - } - - [ConditionalTheory(Skip = "Issue #6812. Cannot eval 'Union({from Customer c in value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer]) where ([c].City == \"México D.F.\") select [c]})'")] - [MemberData(nameof(IsAsyncData))] - public virtual Task Union_simple(bool isAsync) - { - return AssertQuery( - isAsync, - cs => cs.Where(s => s.ContactTitle == "Owner") - .Union(cs.Where(c => c.City == "México D.F.")), - entryCount: 19); - } - - [ConditionalTheory(Skip = "Issue #6812. Cannot eval 'Union({from Customer s in value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer]) where ([s].City == \"México D.F.\") select [s]})'")] - [MemberData(nameof(IsAsyncData))] - public virtual Task Union_nested(bool isAsync) - { - return AssertQuery( - isAsync, - cs => cs.Where(s => s.ContactTitle == "Owner") - .Union(cs.Where(s => s.City == "México D.F.")) - .Union(cs.Where(e => e.City == "London")), - entryCount: 25); - } - - [ConditionalFact(Skip = "Issue #6812. Cannot eval 'Union({from Customer c in value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Customer]) where ([c].City == \"México D.F.\") select [c].CustomerID})'")] - public virtual void Union_non_entity() - { - using (var context = CreateContext()) - { - var query = context.Set() - .Where(s => s.ContactTitle == "Owner") - .Select(c => c.CustomerID) - .Union( - context.Set() - .Where(c => c.City == "México D.F.") - .Select(c => c.CustomerID)) - .ToList(); - - Assert.Equal(19, query.Count); - } - } - [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Average_with_non_matching_types_in_projection_doesnt_produce_second_explicit_cast(bool isAsync) diff --git a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.SetOperations.cs b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.SetOperations.cs index 3a352ab335a..d27beac45a0 100644 --- a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.SetOperations.cs +++ b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.SetOperations.cs @@ -1,9 +1,9 @@ // 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.Linq; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.TestModels.Northwind; using Xunit; @@ -15,39 +15,119 @@ public abstract partial class SimpleQueryTestBase { [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Union(bool isAsync) + public virtual Task Concat(bool isAsync) => AssertQuery(isAsync, cs => cs .Where(c => c.City == "Berlin") - .Union(cs.Where(c => c.City == "London")), + .Concat(cs.Where(c => c.City == "London")), entryCount: 7); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Concat(bool isAsync) + public virtual Task Concat_nested(bool isAsync) => AssertQuery(isAsync, cs => cs - .Where(c => c.City == "Berlin") - .Concat(cs.Where(c => c.City == "London")), - entryCount: 7); + .Where(c => c.City == "México D.F.") + .Concat(cs.Where(s => s.City == "Berlin")) + .Concat(cs.Where(e => e.City == "London")), + entryCount: 12); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Intersect(bool isAsync) - { - return AssertQuery(isAsync, cs => cs - .Where(c => c.City == "London") - .Intersect(cs.Where(c => c.ContactName.Contains("Thomas"))), - entryCount: 1); - } + public virtual Task Concat_non_entity(bool isAsync) + => AssertQuery(isAsync, cs => cs + .Where(c => c.City == "México D.F.") + .Select(c => c.CustomerID) + .Concat(cs + .Where(s => s.ContactTitle == "Owner") + .Select(c => c.CustomerID))); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Except(bool isAsync) - { - return AssertQuery(isAsync, cs => cs + => AssertQuery(isAsync, cs => cs .Where(c => c.City == "London") .Except(cs.Where(c => c.ContactName.Contains("Thomas"))), entryCount: 5); - } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Except_simple_followed_by_projecting_constant(bool isAsync) + => AssertQueryScalar(isAsync, cs => cs + .Except(cs) + .Select(e => 1)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Except_nested(bool isAsync) + => AssertQuery(isAsync, cs => cs + .Where(s => s.ContactTitle == "Owner") + .Except(cs.Where(s => s.City == "México D.F.")) + .Except(cs.Where(e => e.City == "Seattle")), + entryCount: 13); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Except_non_entity(bool isAsync) + => AssertQuery(isAsync, cs => cs + .Where(s => s.ContactTitle == "Owner") + .Select(c => c.CustomerID) + .Except( + cs + .Where(c => c.City == "México D.F.") + .Select(c => c.CustomerID))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Intersect(bool isAsync) + => AssertQuery(isAsync, cs => cs + .Where(c => c.City == "London") + .Intersect(cs.Where(c => c.ContactName.Contains("Thomas"))), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Intersect_nested(bool isAsync) + => AssertQuery(isAsync, cs => cs + .Where(c => c.City == "México D.F.") + .Intersect(cs.Where(s => s.ContactTitle == "Owner")) + .Intersect(cs.Where(e => e.Fax != null)), + entryCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Intersect_non_entity(bool isAsync) + => AssertQuery(isAsync, cs => cs + .Where(c => c.City == "México D.F.") + .Select(c => c.CustomerID) + .Intersect(cs + .Where(s => s.ContactTitle == "Owner") + .Select(c => c.CustomerID))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union(bool isAsync) + => AssertQuery(isAsync, cs => cs + .Where(c => c.City == "Berlin") + .Union(cs.Where(c => c.City == "London")), + entryCount: 7); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_nested(bool isAsync) + => AssertQuery(isAsync, cs => cs + .Where(s => s.ContactTitle == "Owner") + .Union(cs.Where(s => s.City == "México D.F.")) + .Union(cs.Where(e => e.City == "London")), + entryCount: 25); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual void Union_non_entity(bool isAsync) + => AssertQuery(isAsync, cs => cs + .Where(s => s.ContactTitle == "Owner") + .Select(c => c.CustomerID) + .Union(cs + .Where(c => c.City == "México D.F.") + .Select(c => c.CustomerID))); // OrderBy, Skip and Take are typically supported on the set operation itself (no need for query pushdown) [ConditionalTheory] @@ -137,6 +217,14 @@ public virtual Task Union_Select(bool isAsync) .Select(c => c.Address) .Where(a => a.Contains("Hanover"))); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_with_anonymous_type_projection(bool isAsync) + => AssertQuery(isAsync, cs => cs + .Where(c => c.CompanyName.StartsWith("A")) + .Union(cs.Where(c => c.CompanyName.StartsWith("B"))) + .Select(c => new CustomerDeets { Id = c.CustomerID })); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Select_Union_unrelated(bool isAsync) @@ -182,23 +270,71 @@ public virtual Task Include_Union(bool isAsync) .Include(c => c.Orders)), entryCount: 59); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Select_Except_reference_projection(bool isAsync) + => AssertQuery(isAsync, od => od + .Select(o => o.Customer) + .Except(od + .Where(o => o.CustomerID == "ALFKI") + .Select(o => o.Customer)), + entryCount: 88); + + [ConditionalFact] + public virtual void Include_Union_only_on_one_side_throws() + { + using (var ctx = CreateContext()) + { + Assert.Throws(() => + ctx.Customers + .Where(c => c.City == "Berlin") + .Include(c => c.Orders) + .Union(ctx.Customers.Where(c => c.City == "London")) + .ToList()); + + Assert.Throws(() => + ctx.Customers + .Where(c => c.City == "Berlin") + .Union(ctx.Customers + .Where(c => c.City == "London") + .Include(c => c.Orders)) + .ToList()); + } + } + + [ConditionalFact] + public virtual void Include_Union_different_includes_throws() + { + using (var ctx = CreateContext()) + { + Assert.Throws(() => + ctx.Customers + .Where(c => c.City == "Berlin") + .Include(c => c.Orders) + .Union(ctx.Customers + .Where(c => c.City == "London") + .Include(c => c.Orders) + .ThenInclude(o => o.OrderDetails)) + .ToList()); + } + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task SubSelect_Union(bool isAsync) => AssertQuery(isAsync, cs => cs .Select(c => new { Customer = c, Orders = c.Orders.Count }) .Union(cs - .Select(c => new { Customer = c, Orders = c.Orders.Count }) + .Select(c => new { Customer = c, Orders = c.Orders.Count }) ), entryCount: 91); [ConditionalTheory(Skip = "#16243")] [MemberData(nameof(IsAsyncData))] -public virtual Task Client_eval_Union_FirstOrDefault(bool isAsync) - => AssertFirstOrDefault( - isAsync, cs => cs - .Select(c => ClientSideMethod(c)) - .Union(cs)); + public virtual Task Client_eval_Union_FirstOrDefault(bool isAsync) + => AssertFirstOrDefault(isAsync, cs => cs + .Select(c => ClientSideMethod(c)) + .Union(cs)); static Customer ClientSideMethod(Customer c) => c; } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.ResultOperators.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.ResultOperators.cs index 5a7a1879510..8c5e0885f24 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.ResultOperators.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.ResultOperators.cs @@ -12,20 +12,6 @@ namespace Microsoft.EntityFrameworkCore.Query { public partial class SimpleQuerySqlServerTest { - public override async Task Union_with_custom_projection(bool isAsync) - { - await base.Union_with_custom_projection(isAsync); - - AssertSql( - @"SELECT [c1].[CustomerID], [c1].[Address], [c1].[City], [c1].[CompanyName], [c1].[ContactName], [c1].[ContactTitle], [c1].[Country], [c1].[Fax], [c1].[Phone], [c1].[PostalCode], [c1].[Region] -FROM [Customers] AS [c1] -WHERE [c1].[CompanyName] LIKE N'A%'", - // - @"SELECT [c2].[CustomerID], [c2].[Address], [c2].[City], [c2].[CompanyName], [c2].[ContactName], [c2].[ContactTitle], [c2].[Country], [c2].[Fax], [c2].[Phone], [c2].[PostalCode], [c2].[Region] -FROM [Customers] AS [c2] -WHERE [c2].[CompanyName] LIKE N'B%'"); - } - public override void Select_All() { base.Select_All(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.SetOperations.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.SetOperations.cs index 1a34e36222e..34aeff32a8f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.SetOperations.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.SetOperations.cs @@ -206,6 +206,20 @@ FROM [Customers] AS [c0] WHERE CHARINDEX(N'Hanover', [t].[Address]) > 0"); } + public override async Task Union_with_anonymous_type_projection(bool isAsync) + { + await base.Union_with_anonymous_type_projection(isAsync); + + AssertSql( + @"SELECT [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].[CompanyName] IS NOT NULL AND ([c].[CompanyName] LIKE N'A%') +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 [Customers] AS [c0] +WHERE [c0].[CompanyName] IS NOT NULL AND ([c0].[CompanyName] LIKE N'B%')"); + } + public override async Task Select_Union_unrelated(bool isAsync) { await base.Select_Union_unrelated(isAsync); @@ -286,6 +300,21 @@ FROM [Customers] AS [c0] LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID]"); } + public override async Task Select_Except_reference_projection(bool isAsync) + { + await base.Select_Except_reference_projection(isAsync); + + AssertSql( + @"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] +EXCEPT +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].[CustomerID] = N'ALFKI') AND [o0].[CustomerID] IS NOT NULL"); + } + public override async Task SubSelect_Union(bool isAsync) { await base.SubSelect_Union(isAsync);