From e790f0d45b21e9771ba1f364f0e63044ef9b54de Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 25 Jun 2019 14:02:00 +0200 Subject: [PATCH] Set operations: finalization * Include/nav rewriting is now supported. * We now push down to subquery when OrderBy, Take or Skip are applied to set operations, to avoid using the hack where ColumnExpression with no table alias was used. #16244 was opened to track for post-3.0. * Added missing support for union over subselect projection mappings. * Added Other type to SetOperationType so that providers can define extra set operations (e.g. PostgreSQL `INTERSECT ALL`, `EXCEPT ALL`). Completes #6812 Fixes #13196 Fixes #16065 Fixes #16165 --- .../SqlExpressions/SelectExpression.cs | 248 ++++++++++-------- .../NavigationExpandingVisitor_MethodCall.cs | 129 +++++---- .../SimpleQueryTestBase.SetOperations.cs | 41 ++- .../SimpleQuerySqlServerTest.SetOperations.cs | 117 +++++++-- 4 files changed, 350 insertions(+), 185 deletions(-) diff --git a/src/EFCore.Relational/Query/Pipeline/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/Pipeline/SqlExpressions/SelectExpression.cs index c4d7f635d8f..6dd589c2ac8 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; @@ -257,9 +258,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()) @@ -280,7 +280,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(); } @@ -290,8 +291,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(); } @@ -365,11 +366,14 @@ public Expression ApplySetOperation( select1._projectionMapping = new Dictionary(_projectionMapping); _projectionMapping.Clear(); + select1._identifyingProjection.AddRange(_identifyingProjection); + _identifyingProjection.Clear(); + var select2 = otherSelectExpression; if (_projection.Any()) { - throw new NotImplementedException("Set operation on SelectExpression with populated _projection"); + throw new NotSupportedException("Can't process set operations after client evaluation, consider moving the operation before the last Select() call (see issue #16243)"); } else { @@ -389,98 +393,17 @@ public Expression ApplySetOperation( 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); - } - - continue; + 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; } @@ -499,7 +422,97 @@ public Expression ApplySetOperation( SetOperationType = setOperationType; 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; + } + + // 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 = projection1.EntityType.GetClosestCommonParent(projection2.EntityType); + + if (commonParentEntityType == null) + { + throw new NotSupportedException(RelationalStrings.SetOperationNotWithinEntityTypeHierarchy); + } + + var properties1 = GetAllPropertiesInHierarchy(projection1.EntityType).ToArray(); + var properties2 = GetAllPropertiesInHierarchy(projection2.EntityType).ToArray(); + + foreach (var property in properties1.Intersect(properties2)) + { + propertyExpressions[property] = AddSetOperationColumnProjections( + property, + select1, projection1.GetProperty(property), + select2, projection2.GetProperty(property)); + } + + foreach (var property in properties1.Except(properties2)) + { + propertyExpressions[property] = AddSetOperationColumnProjections( + property, + select1,projection1.GetProperty(property), + select2, null); + } + + foreach (var property in properties2.Except(properties1)) + { + propertyExpressions[property] = AddSetOperationColumnProjections( + property, + select1, null, + select2, projection2.GetProperty(property)); + } + + foreach (var property in GetAllPropertiesInHierarchy(commonParentEntityType) + .Except(properties1).Except(properties2)) + { + propertyExpressions[property] = AddSetOperationColumnProjections( + property, + select1, null, + select2, null); + } + + _projectionMapping[projectionMember] = new EntityProjectionExpression(commonParentEntityType, propertyExpressions); + + 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); + } + } + } + + ColumnExpression AddSetOperationColumnProjections( IProperty property, SelectExpression select1, ColumnExpression column1, SelectExpression select2, ColumnExpression column2) @@ -526,8 +539,29 @@ static ColumnExpression AddSetOperationColumnProjections( var projectionExpression = select1._projection[select1._projection.Count - 1]; var outerColumn = new ColumnExpression(projectionExpression, select1, IsNullableProjection(projectionExpression)); + + if (select1._identifyingProjection.Contains(column1)) + { + _identifyingProjection.Add(outerColumn); + } + 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; + var index = select1.AddToProjection(innerColumn1, alias); + var projectionExpression1 = select1._projection[index]; + select2.AddToProjection(innerColumn2, alias); + var outerColumn = new ColumnExpression(projectionExpression1, select1, IsNullableProjection(projectionExpression1)); + _projectionMapping[projectionMember] = outerColumn; + } } public IDictionary PushdownIntoSubquery() @@ -663,9 +697,7 @@ public RelationalCollectionShaperExpression ApplyCollectionJoin( var joinPredicate = ExtractJoinKey(innerSelectExpression); if (joinPredicate != null) { - if (IsDistinct - || Limit != null - || Offset != null) + if (IsDistinct || Limit != null || Offset != null || IsSetOperation) { var remappingVisitor = new SqlRemappingVisitor(PushdownIntoSubquery()); joinPredicate = remappingVisitor.Remap(joinPredicate); @@ -900,9 +932,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); @@ -950,10 +980,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(); } @@ -1322,7 +1349,12 @@ public enum SetOperationType /// /// Represents an SQL EXCEPT set operation. /// - Except = 4 + Except = 4, + + /// + /// Represents a custom, provider-specific set operation. + /// + Other = 9999 } } diff --git a/src/EFCore/Query/NavigationExpansion/Visitors/NavigationExpandingVisitor_MethodCall.cs b/src/EFCore/Query/NavigationExpansion/Visitors/NavigationExpandingVisitor_MethodCall.cs index bd580abbd18..4f3dc58371e 100644 --- a/src/EFCore/Query/NavigationExpansion/Visitors/NavigationExpandingVisitor_MethodCall.cs +++ b/src/EFCore/Query/NavigationExpansion/Visitors/NavigationExpandingVisitor_MethodCall.cs @@ -35,74 +35,84 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp : methodCallExpression.Update(methodCallExpression.Object, new[] { newSource, methodCallExpression.Arguments[1] }); } - switch (methodCallExpression.Method.Name) + if (methodCallExpression.Method.DeclaringType == typeof(Queryable) + || methodCallExpression.Method.DeclaringType == typeof(QueryableExtensions) + || methodCallExpression.Method.DeclaringType == typeof(Enumerable) + || methodCallExpression.Method.DeclaringType == typeof(EntityFrameworkQueryableExtensions)) { - case nameof(Queryable.Where): - return ProcessWhere(methodCallExpression); + switch (methodCallExpression.Method.Name) + { + case nameof(Queryable.Where): + return ProcessWhere(methodCallExpression); - case nameof(Queryable.Select): - return ProcessSelect(methodCallExpression); + case nameof(Queryable.Select): + return ProcessSelect(methodCallExpression); - case nameof(Queryable.OrderBy): - case nameof(Queryable.OrderByDescending): - return ProcessOrderBy(methodCallExpression); + case nameof(Queryable.OrderBy): + case nameof(Queryable.OrderByDescending): + return ProcessOrderBy(methodCallExpression); - case nameof(Queryable.ThenBy): - case nameof(Queryable.ThenByDescending): - return ProcessThenByBy(methodCallExpression); + case nameof(Queryable.ThenBy): + case nameof(Queryable.ThenByDescending): + return ProcessThenByBy(methodCallExpression); - case nameof(Queryable.Join): - return ProcessJoin(methodCallExpression); + case nameof(Queryable.Join): + return ProcessJoin(methodCallExpression); - case nameof(Queryable.GroupJoin): - return ProcessGroupJoin(methodCallExpression); + case nameof(Queryable.GroupJoin): + return ProcessGroupJoin(methodCallExpression); - case nameof(Queryable.SelectMany): - return ProcessSelectMany(methodCallExpression); + case nameof(Queryable.SelectMany): + return ProcessSelectMany(methodCallExpression); - case nameof(Queryable.All): - return ProcessAll(methodCallExpression); + case nameof(Queryable.All): + return ProcessAll(methodCallExpression); - case nameof(Queryable.Any): - case nameof(Queryable.Count): - case nameof(Queryable.LongCount): - return ProcessAnyCountLongCount(methodCallExpression); + case nameof(Queryable.Any): + case nameof(Queryable.Count): + case nameof(Queryable.LongCount): + return ProcessAnyCountLongCount(methodCallExpression); - case nameof(Queryable.Average): - case nameof(Queryable.Sum): - case nameof(Queryable.Min): - case nameof(Queryable.Max): - return ProcessAverageSumMinMax(methodCallExpression); + case nameof(Queryable.Average): + case nameof(Queryable.Sum): + case nameof(Queryable.Min): + case nameof(Queryable.Max): + return ProcessAverageSumMinMax(methodCallExpression); - case nameof(Queryable.Distinct): - return ProcessDistinct(methodCallExpression); + case nameof(Queryable.Distinct): + return ProcessDistinct(methodCallExpression); - case nameof(Queryable.DefaultIfEmpty): - return ProcessDefaultIfEmpty(methodCallExpression); + case nameof(Queryable.DefaultIfEmpty): + return ProcessDefaultIfEmpty(methodCallExpression); - case nameof(Queryable.First): - case nameof(Queryable.FirstOrDefault): - case nameof(Queryable.Single): - case nameof(Queryable.SingleOrDefault): - return ProcessCardinalityReducingOperation(methodCallExpression); + case nameof(Queryable.First): + case nameof(Queryable.FirstOrDefault): + case nameof(Queryable.Single): + case nameof(Queryable.SingleOrDefault): + return ProcessCardinalityReducingOperation(methodCallExpression); - case nameof(Queryable.OfType): - return ProcessOfType(methodCallExpression); + case nameof(Queryable.OfType): + return ProcessOfType(methodCallExpression); - case nameof(Queryable.Skip): - case nameof(Queryable.Take): - return ProcessSkipTake(methodCallExpression); + case nameof(Queryable.Skip): + case nameof(Queryable.Take): + return ProcessSkipTake(methodCallExpression); - case "Include": - case "ThenInclude": - return ProcessInclude(methodCallExpression); + case nameof(Queryable.Union): + case nameof(Queryable.Intersect): + case nameof(Queryable.Except): + return ProcessSetOperation(methodCallExpression); - case nameof(EntityFrameworkQueryableExtensions.TagWith): - return ProcessWithTag(methodCallExpression); + case "Include": + case "ThenInclude": + return ProcessInclude(methodCallExpression); - default: - return ProcessUnknownMethod(methodCallExpression); + case nameof(EntityFrameworkQueryableExtensions.TagWith): + return ProcessWithTag(methodCallExpression); + } } + + return ProcessUnknownMethod(methodCallExpression); } private Expression ProcessUnknownMethod(MethodCallExpression methodCallExpression) @@ -853,6 +863,27 @@ private Expression ProcessSkipTake(MethodCallExpression methodCallExpression) return new NavigationExpansionExpression(rewritten, preProcessResult.state, methodCallExpression.Type); } + private Expression ProcessSetOperation(MethodCallExpression methodCallExpression) + { + // TODO: We shouldn't terminate if both sides are identical, #16246 + + var source1 = VisitSourceExpression(methodCallExpression.Arguments[0]); + var preProcessResult1 = PreProcessTerminatingOperation(source1); + + var source2 = VisitSourceExpression(methodCallExpression.Arguments[1]); + var preProcessResult2 = PreProcessTerminatingOperation(source2); + + // 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; + var clrType2 = preProcessResult2.state.CurrentParameter.Type; + var parentState = clrType1.IsAssignableFrom(clrType2) ? preProcessResult1.state : preProcessResult2.state; + + var rewritten = methodCallExpression.Update(null, new[] { preProcessResult1.source, preProcessResult2.source }); + + return new NavigationExpansionExpression(rewritten, parentState, methodCallExpression.Type); + } + private (Expression source, NavigationExpansionExpressionState state) PreProcessTerminatingOperation(NavigationExpansionExpression source) { var applyOrderingsResult = ApplyPendingOrderings(source.Operand, source.State); diff --git a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.SetOperations.cs b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.SetOperations.cs index bba21626c8e..3a352ab335a 100644 --- a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.SetOperations.cs +++ b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.SetOperations.cs @@ -13,7 +13,6 @@ namespace Microsoft.EntityFrameworkCore.Query { public abstract partial class SimpleQueryTestBase { - [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Union(bool isAsync) @@ -162,5 +161,45 @@ 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 Union_Include(bool isAsync) + => AssertQuery(isAsync, cs => cs + .Where(c => c.City == "Berlin") + .Union(cs.Where(c => c.City == "London")) + .Include(c => c.Orders), + entryCount: 59); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Include_Union(bool isAsync) + => AssertQuery(isAsync, cs => cs + .Where(c => c.City == "Berlin") + .Include(c => c.Orders) + .Union(cs + .Where(c => c.City == "London") + .Include(c => c.Orders)), + entryCount: 59); + + [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.SetOperations.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.SetOperations.cs index 7505ce3d234..1a34e36222e 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) @@ -225,20 +231,77 @@ 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 [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 [t0] +WHERE ([t0].[Foo] = N'Berlin') AND [t0].[Foo] IS NOT NULL +ORDER BY [t0].[Foo]"); + } + + public override async Task Union_Include(bool isAsync) + { + await base.Union_Include(isAsync); + + AssertSql( + @"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], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] 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] + 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].[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] + 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] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID]"); + } + + public override async Task Include_Union(bool isAsync) + { + await base.Include_Union(isAsync); + + AssertSql( + @"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], [o].[CustomerID], [o].[EmployeeID], [o].[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 [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 [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]"); +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID]"); + } + + 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]"); } } }