From 1285b4f732adccac49d739f63ff3f1ff5e14c57c Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 28 Jun 2019 17:52:11 +0200 Subject: [PATCH] Set operation cleanup * We no longer allow ColumnExpression without table alias * Set operations over operands with different types have been disabled for now. * We now push down set operations into a subquery on Orderby, Skip or Take, which shouldn't be necessary (#16244). * Some test consolidation and cleanup. Continues #6812 --- .../Query/Pipeline/QuerySqlGenerator.cs | 8 +- .../SqlExpressions/ColumnExpression.cs | 8 +- .../SqlExpressions/SelectExpression.cs | 190 +++++--------- .../NavigationExpansion/NavigationTreeNode.cs | 11 +- .../SimpleQueryCosmosTest.ResultOperators.cs | 10 - .../SimpleQueryCosmosTest.SetOperations.cs | 83 +++--- .../Query/InheritanceTestBase.cs | 6 +- .../SimpleQueryTestBase.ResultOperators.cs | 236 ------------------ .../SimpleQueryTestBase.SetOperations.cs | 152 +++++++++-- ...impleQuerySqlServerTest.ResultOperators.cs | 14 -- .../SimpleQuerySqlServerTest.SetOperations.cs | 120 ++++++--- 11 files changed, 332 insertions(+), 506 deletions(-) diff --git a/src/EFCore.Relational/Query/Pipeline/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/Pipeline/QuerySqlGenerator.cs index fe3937fce86..ea528ba5e10 100644 --- a/src/EFCore.Relational/Query/Pipeline/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/Pipeline/QuerySqlGenerator.cs @@ -236,13 +236,9 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction protected override Expression VisitColumn(ColumnExpression columnExpression) { - if (columnExpression.Table.Alias != null) - { - _relationalCommandBuilder - .Append(_sqlGenerationHelper.DelimitIdentifier(columnExpression.Table.Alias)) - .Append("."); - } _relationalCommandBuilder + .Append(_sqlGenerationHelper.DelimitIdentifier(columnExpression.Table.Alias)) + .Append(".") .Append(_sqlGenerationHelper.DelimitIdentifier(columnExpression.Name)); return columnExpression; diff --git a/src/EFCore.Relational/Query/Pipeline/SqlExpressions/ColumnExpression.cs b/src/EFCore.Relational/Query/Pipeline/SqlExpressions/ColumnExpression.cs index 9ee29ddd44c..eb9e8a5e366 100644 --- a/src/EFCore.Relational/Query/Pipeline/SqlExpressions/ColumnExpression.cs +++ b/src/EFCore.Relational/Query/Pipeline/SqlExpressions/ColumnExpression.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions { @@ -27,6 +28,10 @@ internal ColumnExpression(ProjectionExpression subqueryProjection, TableExpressi private ColumnExpression(string name, TableExpressionBase table, Type type, RelationalTypeMapping typeMapping, bool nullable) : base(type, typeMapping) { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(table, nameof(table)); + Check.NotEmpty(table.Alias, $"{nameof(table)}.{nameof(table.Alias)}"); + Name = name; Table = table; Nullable = nullable; @@ -71,7 +76,6 @@ private bool Equals(ColumnExpression columnExpression) public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Name, Table, Nullable); - private string DebuggerDisplay() - => Table.Alias == null ? Name : $"{Table.Alias}.{Name}"; + private string DebuggerDisplay() => $"{Table.Alias}.{Name}"; } } diff --git a/src/EFCore.Relational/Query/Pipeline/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/Pipeline/SqlExpressions/SelectExpression.cs index 758da346792..f849abfd410 100644 --- a/src/EFCore.Relational/Query/Pipeline/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/Pipeline/SqlExpressions/SelectExpression.cs @@ -8,6 +8,7 @@ using System.Reflection; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.Internal; @@ -258,9 +259,8 @@ public void ApplyPredicate(SqlExpression expression) public void ApplyOrdering(OrderingExpression orderingExpression) { - if (IsDistinct - || Limit != null - || Offset != null) + // TODO: We should not be pushing down set operations, see #16244 + if (IsDistinct || Limit != null || Offset != null || IsSetOperation) { orderingExpression = orderingExpression.Update( new SqlRemappingVisitor(PushdownIntoSubquery()) @@ -281,7 +281,8 @@ public void AppendOrdering(OrderingExpression orderingExpression) public void ApplyLimit(SqlExpression sqlExpression) { - if (Limit != null) + // TODO: We should not be pushing down set operations, see #16244 + if (Limit != null || IsSetOperation) { PushdownIntoSubquery(); } @@ -291,8 +292,8 @@ public void ApplyLimit(SqlExpression sqlExpression) public void ApplyOffset(SqlExpression sqlExpression) { - if (Limit != null - || Offset != null) + // TODO: We should not be pushing down set operations, see #16244 + if (Limit != null || Offset != null || IsSetOperation) { PushdownIntoSubquery(); } @@ -367,11 +368,14 @@ public Expression ApplySetOperation( select1._projectionMapping = new Dictionary(_projectionMapping); _projectionMapping.Clear(); + select1._identifier.AddRange(_identifier); + _identifier.Clear(); + var select2 = otherSelectExpression; if (_projection.Any()) { - throw new NotImplementedException("Set operation on SelectExpression with populated _projection"); + throw new InvalidOperationException("Can't process set operations after client evaluation, consider moving the operation before the last Select() call (see issue #16243)"); } else { @@ -387,106 +391,24 @@ public Expression ApplySetOperation( kv => kv.Key, (kv1, kv2) => (kv1.Key, Value1: kv1.Value, Value2: kv2.Value))) { - if (joinedMapping.Value1 is EntityProjectionExpression entityProjection1 && joinedMapping.Value2 is EntityProjectionExpression entityProjection2) { - var propertyExpressions = new Dictionary(); - - if (entityProjection1.EntityType == entityProjection2.EntityType) - { - foreach (var property in GetAllPropertiesInHierarchy(entityProjection1.EntityType)) - { - propertyExpressions[property] = AddSetOperationColumnProjections( - property, - select1, entityProjection1.GetProperty(property), - select2, entityProjection2.GetProperty(property)); - } - - _projectionMapping[joinedMapping.Key] = new EntityProjectionExpression(entityProjection1.EntityType, propertyExpressions); - continue; - } - - // We're doing a set operation over two different entity types (within the same hierarchy). - // Since both sides of the set operations must produce the same result shape, find the - // closest common ancestor and load all the columns for that, adding null projections where - // necessary. Note this means we add null projections for properties which neither sibling - // actually needs, since the shaper doesn't know that only those sibling types will be coming - // back. - var commonParentEntityType = entityProjection1.EntityType.GetClosestCommonParent(entityProjection2.EntityType); - - if (commonParentEntityType == null) - { - throw new NotSupportedException(RelationalStrings.SetOperationNotWithinEntityTypeHierarchy); - } - - var properties1 = GetAllPropertiesInHierarchy(entityProjection1.EntityType).ToArray(); - var properties2 = GetAllPropertiesInHierarchy(entityProjection2.EntityType).ToArray(); - - foreach (var property in properties1.Intersect(properties2)) - { - propertyExpressions[property] = AddSetOperationColumnProjections( - property, - select1, entityProjection1.GetProperty(property), - select2, entityProjection2.GetProperty(property)); - } - - foreach (var property in properties1.Except(properties2)) - { - propertyExpressions[property] = AddSetOperationColumnProjections( - property, - select1, entityProjection1.GetProperty(property), - select2, null); - } - - foreach (var property in properties2.Except(properties1)) - { - propertyExpressions[property] = AddSetOperationColumnProjections( - property, - select1, null, - select2, entityProjection2.GetProperty(property)); - } - - foreach (var property in GetAllPropertiesInHierarchy(commonParentEntityType) - .Except(properties1).Except(properties2)) - { - propertyExpressions[property] = AddSetOperationColumnProjections( - property, - select1, null, - select2, null); - } - - _projectionMapping[joinedMapping.Key] = new EntityProjectionExpression(commonParentEntityType, propertyExpressions); - - if (commonParentEntityType != entityProjection1.EntityType) - { - if (!(shaperExpression.RemoveConvert() is EntityShaperExpression entityShaperExpression)) - { - throw new Exception("Non-entity shaper expression while handling set operation over siblings."); - } - - shaperExpression = new EntityShaperExpression( - commonParentEntityType, entityShaperExpression.ValueBufferExpression, entityShaperExpression.Nullable); - } - + HandleEntityMapping(joinedMapping.Key, select1, entityProjection1, select2, entityProjection2); continue; } - if (joinedMapping.Value1 is ColumnExpression innerColumn1 - && joinedMapping.Value2 is ColumnExpression innerColumn2) + if (joinedMapping.Value1 is ColumnExpression && joinedMapping.Value2 is ColumnExpression + || joinedMapping.Value1 is SubSelectExpression && joinedMapping.Value2 is SubSelectExpression) { - // The actual columns may actually be different, but we don't care as long as the type and alias - // coming out of the two operands are the same - var alias = joinedMapping.Key.LastMember?.Name; - var index = select1.AddToProjection(innerColumn1, alias); - var projectionExpression1 = select1._projection[index]; - select2.AddToProjection(innerColumn2, alias); - var outerColumn = new ColumnExpression(projectionExpression1, select1, IsNullableProjection(projectionExpression1)); - _projectionMapping[joinedMapping.Key] = outerColumn; + HandleColumnMapping( + joinedMapping.Key, + select1, (SqlExpression)joinedMapping.Value1, + select2, (SqlExpression)joinedMapping.Value2); continue; } - throw new NotSupportedException("Non-matching or unknown projection mapping type in set operation"); + throw new InvalidOperationException("Non-matching or unknown projection mapping type in set operation"); } } @@ -502,34 +424,59 @@ public Expression ApplySetOperation( return shaperExpression; - static ColumnExpression AddSetOperationColumnProjections( + void HandleEntityMapping( + ProjectionMember projectionMember, + SelectExpression select1, EntityProjectionExpression projection1, + SelectExpression select2, EntityProjectionExpression projection2) + { + var propertyExpressions = new Dictionary(); + + if (projection1.EntityType == projection2.EntityType) + { + foreach (var property in GetAllPropertiesInHierarchy(projection1.EntityType)) + { + propertyExpressions[property] = AddSetOperationColumnProjections( + property, + select1, projection1.GetProperty(property), + select2, projection2.GetProperty(property)); + } + + _projectionMapping[projectionMember] = new EntityProjectionExpression(projection1.EntityType, propertyExpressions); + return; + } + + throw new InvalidOperationException("Set operations over different entity types are currently unsupported (see #16298)"); + } + + ColumnExpression AddSetOperationColumnProjections( IProperty property, SelectExpression select1, ColumnExpression column1, SelectExpression select2, ColumnExpression column2) { - var columnName = column1?.Name ?? column2?.Name ?? property.Name; - var baseColumnName = columnName; - var counter = 0; - while (select1._projection.Any(pe => string.Equals(pe.Alias, columnName, StringComparison.OrdinalIgnoreCase))) - { - columnName = $"{baseColumnName}{counter++}"; - } + var columnName = column1.Name; - var typeMapping = column1?.TypeMapping ?? column2?.TypeMapping ?? property.FindRelationalMapping(); + select1._projection.Add(new ProjectionExpression(column1, columnName)); + select2._projection.Add(new ProjectionExpression(column2, columnName)); - select1._projection.Add(new ProjectionExpression(column1 != null - ? (SqlExpression)column1 - : new SqlConstantExpression(Constant(null), typeMapping), - columnName)); + if (select1._identifier.Contains(column1)) + { + _identifier.Add(column1); + } - select2._projection.Add(new ProjectionExpression(column2 != null - ? (SqlExpression)column2 - : new SqlConstantExpression(Constant(null), typeMapping), - columnName)); + return column1; + } - var projectionExpression = select1._projection[select1._projection.Count - 1]; - var outerColumn = new ColumnExpression(projectionExpression, select1, IsNullableProjection(projectionExpression)); - return outerColumn; + void HandleColumnMapping( + ProjectionMember projectionMember, + SelectExpression select1, SqlExpression innerColumn1, + SelectExpression select2, SqlExpression innerColumn2) + { + // The actual columns may actually be different, but we don't care as long as the type and alias + // coming out of the two operands are the same + var alias = projectionMember.LastMember?.Name; + select1.AddToProjection(innerColumn1, alias); + select2.AddToProjection(innerColumn2, alias); + _projectionMapping[projectionMember] = innerColumn1; } } @@ -901,9 +848,7 @@ public void AddInnerJoin(SelectExpression innerSelectExpression, SqlExpression j public void AddLeftJoin(SelectExpression innerSelectExpression, SqlExpression joinPredicate, Type transparentIdentifierType) { - if (Limit != null - || Offset != null - || IsDistinct) + if (Limit != null || Offset != null || IsDistinct || IsSetOperation) { joinPredicate = new SqlRemappingVisitor(PushdownIntoSubquery()) .Remap(joinPredicate); @@ -951,10 +896,7 @@ public void AddLeftJoin(SelectExpression innerSelectExpression, SqlExpression jo public void AddCrossJoin(SelectExpression innerSelectExpression, Type transparentIdentifierType) { - if (Limit != null - || Offset != null - || IsDistinct - || Predicate != null) + if (Limit != null || Offset != null || IsDistinct || Predicate != null || IsSetOperation) { PushdownIntoSubquery(); } 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/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.ResultOperators.cs b/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.ResultOperators.cs index 1a9910ce14b..dc65f700d6f 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.ResultOperators.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.ResultOperators.cs @@ -12,16 +12,6 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query { public partial class SimpleQueryCosmosTest { - public override async Task Union_with_custom_projection(bool isAsync) - { - await base.Union_with_custom_projection(isAsync); - - AssertSql( - @"SELECT c -FROM root c -WHERE (c[""Discriminator""] = ""Customer"")"); - } - [ConditionalTheory(Skip = "Issue#14935")] public override void Select_All() { diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.SetOperations.cs b/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.SetOperations.cs index fd8de9181bb..2b8a8f0cf36 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.SetOperations.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.SetOperations.cs @@ -8,60 +8,33 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query { public partial class SimpleQueryCosmosTest { - [ConditionalTheory(Skip = "Issue #12086")] - public override Task Union(bool isAsync) - => base.Union(isAsync); - - [ConditionalTheory(Skip = "Issue #12086")] - public override Task Concat(bool isAsync) - => base.Concat(isAsync); - - [ConditionalTheory(Skip = "Issue #12086")] - public override Task Intersect(bool isAsync) - => base.Intersect(isAsync); - - [ConditionalTheory(Skip = "Issue #12086")] - public override Task Except(bool isAsync) - => base.Except(isAsync); - - [ConditionalTheory(Skip = "Issue #12086")] - public override Task Union_OrderBy_Skip_Take(bool isAsync) - => base.Union_OrderBy_Skip_Take(isAsync); - - [ConditionalTheory(Skip = "Issue #12086")] - public override Task Union_Where(bool isAsync) - => base.Union_Where(isAsync); - - [ConditionalTheory(Skip = "Issue #12086")] - public override Task Union_Skip_Take_OrderBy_ThenBy_Where(bool isAsync) - => base.Union_Skip_Take_OrderBy_ThenBy_Where(isAsync); - - [ConditionalTheory(Skip = "Issue #12086")] - public override Task Union_Union(bool isAsync) - => base.Union_Union(isAsync); - - [ConditionalTheory(Skip = "Issue #12086")] - public override Task Union_Intersect(bool isAsync) - => base.Union_Intersect(isAsync); - - [ConditionalTheory(Skip = "Issue #12086")] - public override Task Union_Take_Union_Take(bool isAsync) - => base.Union_Take_Union_Take(isAsync); - - [ConditionalTheory(Skip = "Issue #12086")] - public override Task Select_Union(bool isAsync) - => base.Select_Union(isAsync); - - [ConditionalTheory(Skip = "Issue #12086")] - public override Task Union_Select(bool isAsync) - => base.Union_Select(isAsync); - - [ConditionalTheory(Skip = "Issue #12086")] - public override Task Select_Union_unrelated(bool isAsync) - => base.Select_Union_unrelated(isAsync); - - [ConditionalTheory(Skip = "Issue #12086")] - public override Task Select_Union_different_fields_in_anonymous_with_subquery(bool isAsync) - => base.Select_Union_different_fields_in_anonymous_with_subquery(isAsync); + // Set operations aren't supported on Cosmos + public override Task Concat(bool isAsync) => Task.CompletedTask; + public override Task Concat_nested(bool isAsync) => Task.CompletedTask; + public override Task Concat_non_entity(bool isAsync) => Task.CompletedTask; + public override Task Except(bool isAsync) => Task.CompletedTask; + public override Task Except_simple_followed_by_projecting_constant(bool isAsync) => Task.CompletedTask; + public override Task Except_nested(bool isAsync) => Task.CompletedTask; + public override Task Except_non_entity(bool isAsync) => Task.CompletedTask; + public override Task Intersect(bool isAsync) => Task.CompletedTask; + public override Task Intersect_nested(bool isAsync) => Task.CompletedTask; + public override Task Intersect_non_entity(bool isAsync) => Task.CompletedTask; + public override Task Union(bool isAsync) => Task.CompletedTask; + public override Task Union_nested(bool isAsync) => Task.CompletedTask; + public override void Union_non_entity(bool isAsync) {} + public override Task Union_OrderBy_Skip_Take(bool isAsync) => Task.CompletedTask; + public override Task Union_Where(bool isAsync) => Task.CompletedTask; + public override Task Union_Skip_Take_OrderBy_ThenBy_Where(bool isAsync) => Task.CompletedTask; + public override Task Union_Union(bool isAsync) => Task.CompletedTask; + public override Task Union_Intersect(bool isAsync) => Task.CompletedTask; + public override Task Union_Take_Union_Take(bool isAsync) => Task.CompletedTask; + public override Task Select_Union(bool isAsync) => Task.CompletedTask; + public override Task Union_Select(bool isAsync) => Task.CompletedTask; + public override Task Union_with_anonymous_type_projection(bool isAsync) => Task.CompletedTask; + public override Task Select_Union_unrelated(bool isAsync) => Task.CompletedTask; + public override Task Select_Union_different_fields_in_anonymous_with_subquery(bool isAsync) => Task.CompletedTask; + public override Task Select_Except_reference_projection(bool isAsync) => Task.CompletedTask; + public override Task SubSelect_Union(bool isAsync) => Task.CompletedTask; + public override Task Client_eval_Union_FirstOrDefault(bool isAsync) => Task.CompletedTask; } } diff --git a/test/EFCore.Specification.Tests/Query/InheritanceTestBase.cs b/test/EFCore.Specification.Tests/Query/InheritanceTestBase.cs index 45cc674cba3..27580a03185 100644 --- a/test/EFCore.Specification.Tests/Query/InheritanceTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/InheritanceTestBase.cs @@ -478,7 +478,7 @@ protected virtual void UseTransaction(DatabaseFacade facade, IDbContextTransacti { } - [ConditionalFact] + [ConditionalFact(Skip = "#16298")] public virtual void Union_siblings_with_duplicate_property_in_subquery() { // Coke and Tea both have CaffeineGrams, which both need to be projected out on each side and so @@ -498,7 +498,7 @@ public virtual void Union_siblings_with_duplicate_property_in_subquery() } } - [ConditionalFact] + [ConditionalFact(Skip = "#16298")] public virtual void OfType_Union_subquery() { using (var context = CreateContext()) @@ -525,7 +525,7 @@ public virtual void OfType_Union_OfType() } } - [ConditionalFact] + [ConditionalFact(Skip = "#16298")] public virtual void Union_entity_equality() { using (var context = CreateContext()) 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 acfd6c1d8c8..241d2c10f43 100644 --- a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.SetOperations.cs +++ b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.SetOperations.cs @@ -1,6 +1,7 @@ // 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.TestModels.Northwind; @@ -14,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] @@ -136,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) @@ -146,7 +235,7 @@ public virtual Task Select_Union_unrelated(bool isAsync) .OrderBy(x => x), assertOrder: true); - [ConditionalTheory] + [ConditionalTheory(Skip = "Very similar to #16298")] [MemberData(nameof(IsAsyncData))] public virtual Task Select_Union_different_fields_in_anonymous_with_subquery(bool isAsync) => AssertQuery(isAsync, cs => cs @@ -160,5 +249,34 @@ public virtual Task Select_Union_different_fields_in_anonymous_with_subquery(boo .Take(10) .Where(x => x.Foo == "Berlin"), entryCount: 1); + + [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); + + [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 }) + ), + 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)); + + 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 7505ce3d234..b7e9b957e81 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.SetOperations.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.SetOperations.cs @@ -71,14 +71,17 @@ public override async Task Union_OrderBy_Skip_Take(bool isAsync) AssertSql( @"@__p_0='1' -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].[City] = N'Berlin') AND [c].[City] IS NOT NULL -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].[City] = N'London') AND [c0].[City] IS NOT NULL -ORDER BY [ContactName] +SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region] +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 [Customers] AS [c] + WHERE ([c].[City] = N'Berlin') AND [c].[City] IS NOT NULL + 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].[City] = N'London') AND [c0].[City] IS NOT NULL +) AS [t] +ORDER BY [t].[ContactName] OFFSET @__p_0 ROWS FETCH NEXT @__p_0 ROWS ONLY"); } @@ -107,20 +110,23 @@ public override async Task Union_Skip_Take_OrderBy_ThenBy_Where(bool isAsync) AssertSql( @"@__p_0='0' -SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region] +SELECT [t0].[CustomerID], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region] 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 [Customers] AS [c] - WHERE ([c].[City] = N'Berlin') AND [c].[City] IS NOT NULL - 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].[City] = N'London') AND [c0].[City] IS NOT NULL - ORDER BY [Region], [City] + SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region] + 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 [Customers] AS [c] + WHERE ([c].[City] = N'Berlin') AND [c].[City] IS NOT NULL + 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].[City] = N'London') AND [c0].[City] IS NOT NULL + ) AS [t] + ORDER BY [t].[Region], [t].[City] OFFSET @__p_0 ROWS -) AS [t] -WHERE CHARINDEX(N'Thomas', [t].[ContactName]) > 0 -ORDER BY [t].[Region], [t].[City]"); +) AS [t0] +WHERE CHARINDEX(N'Thomas', [t0].[ContactName]) > 0 +ORDER BY [t0].[Region], [t0].[City]"); } public override async Task Union_Union(bool isAsync) @@ -200,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); @@ -225,20 +245,56 @@ public override async Task Select_Union_different_fields_in_anonymous_with_subqu @"@__p_0='1' @__p_1='10' -SELECT [t].[Foo], [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region] +SELECT [t0].[Foo], [t0].[CustomerID], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region] FROM ( - SELECT [c].[City] AS [Foo], [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].[City] = N'Berlin') AND [c].[City] IS NOT NULL - UNION - SELECT [c0].[Region] AS [Foo], [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region] - FROM [Customers] AS [c0] - WHERE ([c0].[City] = N'London') AND [c0].[City] IS NOT NULL - ORDER BY [Foo] + SELECT [t].[Foo], [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region] + FROM ( + SELECT [c].[City] AS [Foo], [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].[City] = N'Berlin') AND [c].[City] IS NOT NULL + UNION + SELECT [c0].[Region] AS [Foo], [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region] + FROM [Customers] AS [c0] + WHERE ([c0].[City] = N'London') AND [c0].[City] IS NOT NULL + ) AS [t] + ORDER BY [t].[Foo] OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY -) AS [t] -WHERE ([t].[Foo] = N'Berlin') AND [t].[Foo] IS NOT NULL -ORDER BY [t].[Foo]"); +) AS [t0] +WHERE ([t0].[Foo] = N'Berlin') AND [t0].[Foo] IS NOT NULL +ORDER BY [t0].[Foo]"); + } + + 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); + + 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 COUNT(*) + FROM [Orders] AS [o] + WHERE ([c].[CustomerID] = [o].[CustomerID]) AND [o].[CustomerID] IS NOT NULL) AS [Orders] +FROM [Customers] AS [c] +UNION +SELECT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region], ( + SELECT COUNT(*) + FROM [Orders] AS [o0] + WHERE ([c0].[CustomerID] = [o0].[CustomerID]) AND [o0].[CustomerID] IS NOT NULL) AS [Orders] +FROM [Customers] AS [c0]"); } } }