diff --git a/src/EFCore.Relational/Query/Internal/CollectionJoinApplyingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/CollectionJoinApplyingExpressionVisitor.cs index 31d16f0e4b3..11e18268c17 100644 --- a/src/EFCore.Relational/Query/Internal/CollectionJoinApplyingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/CollectionJoinApplyingExpressionVisitor.cs @@ -21,7 +21,6 @@ protected override Expression VisitExtension(Expression extensionExpression) if (selectExpression.IsDistinct || selectExpression.Limit != null || selectExpression.Offset != null - || selectExpression.IsSetOperation || selectExpression.GroupBy.Count > 1) { selectExpression.PushdownIntoSubquery(); diff --git a/src/EFCore.Relational/Query/Internal/NullSemanticsRewritingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/NullSemanticsRewritingExpressionVisitor.cs index dff62c3bb2c..d7e5a7b918b 100644 --- a/src/EFCore.Relational/Query/Internal/NullSemanticsRewritingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/NullSemanticsRewritingExpressionVisitor.cs @@ -73,9 +73,10 @@ private SqlConstantExpression VisitSqlConstantExpression(SqlConstantExpression s private ColumnExpression VisitColumnExpression(ColumnExpression columnExpression) { + var newTable = (TableExpressionBase)Visit(columnExpression.Table); _isNullable = columnExpression.IsNullable; - return columnExpression; + return columnExpression.Update(newTable); } private SqlParameterExpression VisitSqlParameterExpression(SqlParameterExpression sqlParameterExpression) diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs index 8e2932d4c63..885e13e4c67 100644 --- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs @@ -92,38 +92,40 @@ protected override Expression VisitSqlFragment(SqlFragmentExpression sqlFragment return sqlFragmentExpression; } + private bool IsNonComposedSetOperation(SelectExpression selectExpression) + => selectExpression.Offset == null + && selectExpression.Limit == null + && !selectExpression.IsDistinct + && selectExpression.Predicate == null + && selectExpression.Having == null + && selectExpression.Orderings.Count == 0 + && selectExpression.GroupBy.Count == 0 + && selectExpression.Tables.Count == 1 + && selectExpression.Tables[0] is SetOperationBase setOperation + && selectExpression.Projection.Count == setOperation.Source1.Projection.Count + && selectExpression.Projection.Select((pe, index) => pe.Expression is ColumnExpression column + && column.Table.Equals(setOperation) + && string.Equals(column.Name, setOperation.Source1.Projection[index].Alias, StringComparison.OrdinalIgnoreCase)) + .All(e => e); + protected override Expression VisitSelect(SelectExpression selectExpression) { - IDisposable subQueryIndent = null; - - if (selectExpression.Alias != null) + if (IsNonComposedSetOperation(selectExpression)) { - _relationalCommandBuilder.AppendLine("("); - subQueryIndent = _relationalCommandBuilder.Indent(); - } + // Naked set operation + GenerateSetOperation((SetOperationBase)selectExpression.Tables[0]); - if (selectExpression.IsSetOperation) - { - GenerateSetOperation(selectExpression); - } - else - { - GenerateSelect(selectExpression); + return selectExpression; } + IDisposable subQueryIndent = null; + if (selectExpression.Alias != null) { - subQueryIndent.Dispose(); - - _relationalCommandBuilder.AppendLine() - .Append(")" + AliasSeparator + _sqlGenerationHelper.DelimitIdentifier(selectExpression.Alias)); + _relationalCommandBuilder.AppendLine("("); + subQueryIndent = _relationalCommandBuilder.Indent(); } - return selectExpression; - } - - protected virtual void GenerateSelect(SelectExpression selectExpression) - { _relationalCommandBuilder.Append("SELECT "); if (selectExpression.IsDistinct) @@ -172,54 +174,16 @@ protected virtual void GenerateSelect(SelectExpression selectExpression) GenerateOrderings(selectExpression); GenerateLimitOffset(selectExpression); - } - - protected virtual void GenerateSetOperation(SelectExpression setOperationExpression) - { - Debug.Assert(setOperationExpression.Tables.Count == 2, - $"{nameof(SelectExpression)} with {setOperationExpression.Tables.Count} tables, must be 2"); - - static string GenerateSetOperationType(SetOperationType setOperationType) - => setOperationType switch - { - SetOperationType.Union => "UNION", - SetOperationType.UnionAll => "UNION ALL", - SetOperationType.Intersect => "INTERSECT", - SetOperationType.Except => "EXCEPT", - _ => throw new InvalidOperationException($"Invalid {nameof(SetOperationType)}: {setOperationType}") - }; - - GenerateSetOperationOperand(setOperationExpression, (SelectExpression)setOperationExpression.Tables[0]); - - _relationalCommandBuilder - .AppendLine() - .AppendLine(GenerateSetOperationType(setOperationExpression.SetOperationType)); - - GenerateSetOperationOperand(setOperationExpression, (SelectExpression)setOperationExpression.Tables[1]); - - GenerateOrderings(setOperationExpression); - GenerateLimitOffset(setOperationExpression); - } - protected virtual void GenerateSetOperationOperand( - SelectExpression setOperationExpression, - SelectExpression operandExpression) - { - // INTERSECT has higher precedence over UNION and EXCEPT, but otherwise evaluation is left-to-right. - // To preserve meaning, add parentheses whenever a set operation is nested within a different set operation. - if (operandExpression.IsSetOperation - && operandExpression.SetOperationType != setOperationExpression.SetOperationType) + if (selectExpression.Alias != null) { - _relationalCommandBuilder.AppendLine("("); - using (_relationalCommandBuilder.Indent()) - { - Visit(operandExpression); - } - _relationalCommandBuilder.AppendLine().Append(")"); - return; + subQueryIndent.Dispose(); + + _relationalCommandBuilder.AppendLine() + .Append(")" + AliasSeparator + _sqlGenerationHelper.DelimitIdentifier(selectExpression.Alias)); } - Visit(operandExpression); + return selectExpression; } protected override Expression VisitProjection(ProjectionExpression projectionExpression) @@ -621,9 +585,7 @@ protected override Expression VisitIn(InExpression inExpression) } protected virtual string GenerateOperator(SqlBinaryExpression binaryExpression) - { - return _operatorMap[binaryExpression.OperatorType]; - } + => _operatorMap[binaryExpression.OperatorType]; protected virtual void GenerateTop(SelectExpression selectExpression) { @@ -657,8 +619,6 @@ protected virtual void GenerateOrderings(SelectExpression selectExpression) protected virtual void GenerateLimitOffset(SelectExpression selectExpression) { - // The below implements ISO SQL:2008 - if (selectExpression.Offset != null) { _relationalCommandBuilder.AppendLine() @@ -779,5 +739,75 @@ protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpres return rowNumberExpression; } + + protected virtual void GenerateSetOperation(SetOperationBase setOperation) + { + string getSetOperation() => setOperation switch + { + ExceptExpression _ => "EXCEPT", + IntersectExpression _ => "INTERSECT", + UnionExpression _ => "UNION", + _ => throw new InvalidOperationException("Unknown SetOperationType."), + }; + + GenerateSetOperationOperand(setOperation, setOperation.Source1); + _relationalCommandBuilder.AppendLine(); + _relationalCommandBuilder.AppendLine($"{getSetOperation()}{(setOperation.IsDistinct ? "" : " ALL")}"); + GenerateSetOperationOperand(setOperation, setOperation.Source2); + } + + protected virtual void GenerateSetOperationOperand(SetOperationBase setOperation, SelectExpression operand) + { + // INTERSECT has higher precedence over UNION and EXCEPT, but otherwise evaluation is left-to-right. + // To preserve meaning, add parentheses whenever a set operation is nested within a different set operation. + if (IsNonComposedSetOperation(operand) + && operand.Tables[0].GetType() != setOperation.GetType()) + { + _relationalCommandBuilder.AppendLine("("); + using (_relationalCommandBuilder.Indent()) + { + Visit(operand); + } + _relationalCommandBuilder.AppendLine().Append(")"); + } + else + { + Visit(operand); + } + } + + private void GenerateSetOperationHelper(SetOperationBase setOperation) + { + _relationalCommandBuilder.AppendLine("("); + using (_relationalCommandBuilder.Indent()) + { + GenerateSetOperation(setOperation); + } + _relationalCommandBuilder.AppendLine() + .Append(")") + .Append(AliasSeparator) + .Append(_sqlGenerationHelper.DelimitIdentifier(setOperation.Alias)); + } + + protected override Expression VisitExcept(ExceptExpression exceptExpression) + { + GenerateSetOperationHelper(exceptExpression); + + return exceptExpression; + } + + protected override Expression VisitIntersect(IntersectExpression intersectExpression) + { + GenerateSetOperationHelper(intersectExpression); + + return intersectExpression; + } + + protected override Expression VisitUnion(UnionExpression unionExpression) + { + GenerateSetOperationHelper(unionExpression); + + return unionExpression; + } } } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index f9fa606ad04..dac06beb796 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -183,9 +183,8 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression source1, ShapedQueryExpression source2) { - var operand1 = (SelectExpression)source1.QueryExpression; - var operand2 = (SelectExpression)source2.QueryExpression; - source1.ShaperExpression = operand1.ApplySetOperation(SetOperationType.UnionAll, operand2, source1.ShaperExpression); + ((SelectExpression)source1.QueryExpression).ApplyUnion((SelectExpression)source2.QueryExpression, distinct: false); + return source1; } @@ -262,9 +261,7 @@ protected override ShapedQueryExpression TranslateElementAtOrDefault(ShapedQuery protected override ShapedQueryExpression TranslateExcept(ShapedQueryExpression source1, ShapedQueryExpression source2) { - var operand1 = (SelectExpression)source1.QueryExpression; - var operand2 = (SelectExpression)source2.QueryExpression; - source1.ShaperExpression = operand1.ApplySetOperation(SetOperationType.Except, operand2, source1.ShaperExpression); + ((SelectExpression)source1.QueryExpression).ApplyExcept((SelectExpression)source2.QueryExpression, distinct: true); return source1; } @@ -444,9 +441,7 @@ protected override ShapedQueryExpression TranslateGroupJoin(ShapedQueryExpressio protected override ShapedQueryExpression TranslateIntersect(ShapedQueryExpression source1, ShapedQueryExpression source2) { - var operand1 = (SelectExpression)source1.QueryExpression; - var operand2 = (SelectExpression)source2.QueryExpression; - source1.ShaperExpression = operand1.ApplySetOperation(SetOperationType.Intersect, operand2, source1.ShaperExpression); + ((SelectExpression)source1.QueryExpression).ApplyIntersect((SelectExpression)source2.QueryExpression, distinct: true); return source1; } @@ -937,9 +932,7 @@ protected override ShapedQueryExpression TranslateThenBy(ShapedQueryExpression s protected override ShapedQueryExpression TranslateUnion(ShapedQueryExpression source1, ShapedQueryExpression source2) { - var operand1 = (SelectExpression)source1.QueryExpression; - var operand2 = (SelectExpression)source2.QueryExpression; - source1.ShaperExpression = operand1.ApplySetOperation(SetOperationType.Union, operand2, source1.ShaperExpression); + ((SelectExpression)source1.QueryExpression).ApplyUnion((SelectExpression)source2.QueryExpression, distinct: true); return source1; } diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index f6bf8182872..130a591443d 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -500,7 +500,7 @@ private void AddConditions( var otherSelectExpression = new SelectExpression(entityType); AddInnerJoin(otherSelectExpression, otherFk, sharingTypes, skipInnerJoins: false); - selectExpression.ApplySetOperation(SetOperationType.Union, otherSelectExpression, null); + selectExpression.ApplyUnion(otherSelectExpression, distinct: true); } } } @@ -629,7 +629,7 @@ private void AddOptionalDependentConditions( AddInnerJoin(otherSelectExpression, referencingFk, sameTable ? sharingTypes : null, skipInnerJoins: sameTable); - selectExpression.ApplySetOperation(SetOperationType.Union, otherSelectExpression, null); + selectExpression.ApplyUnion(otherSelectExpression, distinct: true); } } } diff --git a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs index ca2c4c2f6e5..1a48b6bf809 100644 --- a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs @@ -80,12 +80,24 @@ protected override Expression VisitExtension(Expression extensionExpression) case TableExpression tableExpression: return VisitTable(tableExpression); + + case ExceptExpression exceptExpression: + return VisitExcept(exceptExpression); + + case IntersectExpression intersectExpression: + return VisitIntersect(intersectExpression); + + case UnionExpression unionExpression: + return VisitUnion(unionExpression); } return base.VisitExtension(extensionExpression); } protected abstract Expression VisitRowNumber(RowNumberExpression rowNumberExpression); + protected abstract Expression VisitExcept(ExceptExpression exceptExpression); + protected abstract Expression VisitIntersect(IntersectExpression intersectExpression); + protected abstract Expression VisitUnion(UnionExpression unionExpression); protected abstract Expression VisitExists(ExistsExpression existsExpression); protected abstract Expression VisitIn(InExpression inExpression); protected abstract Expression VisitCrossJoin(CrossJoinExpression crossJoinExpression); diff --git a/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs index ac51acafaa0..96c1088c720 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs @@ -51,13 +51,12 @@ private ColumnExpression(string name, TableExpressionBase table, Type type, Rela public bool IsNullable { get; } protected override Expression VisitChildren(ExpressionVisitor visitor) - { - var newTable = (TableExpressionBase)visitor.Visit(Table); + => Update((TableExpressionBase)visitor.Visit(Table)); - return newTable != Table - ? new ColumnExpression(Name, newTable, Type, TypeMapping, IsNullable) + public virtual ColumnExpression Update(TableExpressionBase table) + => table != Table + ? new ColumnExpression(Name, table, Type, TypeMapping, IsNullable) : this; - } public ColumnExpression MakeNullable() => new ColumnExpression(Name, Table, Type.MakeNullable(), TypeMapping, true); diff --git a/src/EFCore.Relational/Query/SqlExpressions/ExceptExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ExceptExpression.cs new file mode 100644 index 00000000000..a983f6790da --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/ExceptExpression.cs @@ -0,0 +1,59 @@ +// 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.Expressions; + +namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions +{ + public class ExceptExpression : SetOperationBase + { + public ExceptExpression(string alias, SelectExpression source1, SelectExpression source2, bool distinct) + : base(alias, source1, source2, distinct) + { + } + + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var source1 = (SelectExpression)visitor.Visit(Source1); + var source2 = (SelectExpression)visitor.Visit(Source2); + + return Update(source1, source2); + } + + public virtual ExceptExpression Update(SelectExpression source1, SelectExpression source2) + => source1 != Source1 || source2 != Source2 + ? new ExceptExpression(Alias, source1, source2, IsDistinct) + : this; + + public override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append("("); + using (expressionPrinter.Indent()) + { + expressionPrinter.Visit(Source1); + expressionPrinter.AppendLine(); + expressionPrinter.Append("EXCEPT"); + if (!IsDistinct) + { + expressionPrinter.AppendLine(" ALL"); + } + expressionPrinter.Visit(Source2); + } + expressionPrinter.AppendLine() + .AppendLine($") AS {Alias}"); + } + + public override bool Equals(object obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is ExceptExpression exceptExpression + && Equals(exceptExpression)); + + private bool Equals(ExceptExpression exceptExpression) + => base.Equals(exceptExpression); + + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), GetType()); + } +} diff --git a/src/EFCore.Relational/Query/SqlExpressions/IntersectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/IntersectExpression.cs new file mode 100644 index 00000000000..2bb7e8e4b0b --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/IntersectExpression.cs @@ -0,0 +1,59 @@ +// 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.Expressions; + +namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions +{ + public class IntersectExpression : SetOperationBase + { + public IntersectExpression(string alias, SelectExpression source1, SelectExpression source2, bool distinct) + : base(alias, source1, source2, distinct) + { + } + + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var source1 = (SelectExpression)visitor.Visit(Source1); + var source2 = (SelectExpression)visitor.Visit(Source2); + + return Update(source1, source2); + } + + public virtual IntersectExpression Update(SelectExpression source1, SelectExpression source2) + => source1 != Source1 || source2 != Source2 + ? new IntersectExpression(Alias, source1, source2, IsDistinct) + : this; + + public override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append("("); + using (expressionPrinter.Indent()) + { + expressionPrinter.Visit(Source1); + expressionPrinter.AppendLine(); + expressionPrinter.Append("INTERSECT"); + if (!IsDistinct) + { + expressionPrinter.AppendLine(" ALL"); + } + expressionPrinter.Visit(Source2); + } + expressionPrinter.AppendLine() + .AppendLine($") AS {Alias}"); + } + + public override bool Equals(object obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is IntersectExpression intersectExpression + && Equals(intersectExpression)); + + private bool Equals(IntersectExpression intersectExpression) + => base.Equals(intersectExpression); + + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), GetType()); + } +} diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index 1974429f883..661d59d8bc0 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -42,17 +42,6 @@ public void ApplyTags(ISet tags) Tags = tags; } - /// - /// Marks this as representing an SQL set operation, such as a UNION. - /// For regular SQL SELECT expressions, contains None. - /// - public SetOperationType SetOperationType { get; private set; } - - /// - /// Returns whether this represents an SQL set operation, such as a UNION. - /// - public bool IsSetOperation => SetOperationType != SetOperationType.None; - internal SelectExpression( string alias, List projections, @@ -202,7 +191,7 @@ public IDictionary AddToProjection(EntityProjectionExpression en public void PrepareForAggregate() { - if (IsDistinct || Limit != null || Offset != null || IsSetOperation || GroupBy.Count > 0) + if (IsDistinct || Limit != null || Offset != null || GroupBy.Count > 0) { PushdownIntoSubquery(); } @@ -216,7 +205,7 @@ public void ApplyPredicate(SqlExpression expression) return; } - if (Limit != null || Offset != null || IsSetOperation) + if (Limit != null || Offset != null) { expression = new SqlRemappingVisitor(PushdownIntoSubquery(), (SelectExpression)Tables[0]).Remap(expression); } @@ -293,8 +282,7 @@ private void AppendGroupBy(Expression keySelector) public void ApplyOrdering(OrderingExpression orderingExpression) { - // TODO: We should not be pushing down set operations, see #16244 - if (IsDistinct || Limit != null || Offset != null || IsSetOperation) + if (IsDistinct || Limit != null || Offset != null) { orderingExpression = orderingExpression.Update( new SqlRemappingVisitor(PushdownIntoSubquery(), (SelectExpression)Tables[0]) @@ -315,8 +303,7 @@ public void AppendOrdering(OrderingExpression orderingExpression) public void ApplyLimit(SqlExpression sqlExpression) { - // TODO: We should not be pushing down set operations, see #16244 - if (Limit != null || IsSetOperation) + if (Limit != null) { PushdownIntoSubquery(); } @@ -326,8 +313,7 @@ public void ApplyLimit(SqlExpression sqlExpression) public void ApplyOffset(SqlExpression sqlExpression) { - // TODO: We should not be pushing down set operations, see #16244 - if (Limit != null || Offset != null || IsSetOperation) + if (Limit != null || Offset != null) { PushdownIntoSubquery(); } @@ -358,7 +344,7 @@ public void ReverseOrderings() public void ApplyDistinct() { - if (Limit != null || Offset != null || IsSetOperation) + if (Limit != null || Offset != null) { PushdownIntoSubquery(); } @@ -421,32 +407,33 @@ public void ClearOrdering() _orderings.Clear(); } - /// - /// Applies a set operation (e.g. Union, Intersect) on this query, pushing it down and - /// down to be the set operands. - /// - /// The type of set operation to be applied. - /// The other expression to participate as an operate in the operation (along with this one). - /// The shaper expression currently in use. - /// - /// A shaper expression to be used. This will be the same as , unless the set operation - /// modified the return type (i.e. upcast to common ancestor). - /// - public Expression ApplySetOperation( - SetOperationType setOperationType, - SelectExpression otherSelectExpression, - Expression shaperExpression) + private enum SetOperationType + { + Except, + Intersect, + Union + } + + public void ApplyExcept(SelectExpression source2, bool distinct) + => ApplySetOperation(SetOperationType.Except, source2, distinct); + public void ApplyIntersect(SelectExpression source2, bool distinct) + => ApplySetOperation(SetOperationType.Intersect, source2, distinct); + public void ApplyUnion(SelectExpression source2, bool distinct) + => ApplySetOperation(SetOperationType.Union, source2, distinct); + + private void ApplySetOperation(SetOperationType setOperationType, SelectExpression select2, bool distinct) { // TODO: throw if there are pending collection joins // TODO: What happens when applying set operations on 2 queries with one of them being grouping - var select1 = new SelectExpression(null, new List(), _tables.ToList(), _groupBy.ToList(), _orderings.ToList()) + + var select1 = new SelectExpression( + null, new List(), _tables.ToList(), _groupBy.ToList(), _orderings.ToList()) { IsDistinct = IsDistinct, Predicate = Predicate, Having = Having, Offset = Offset, - Limit = Limit, - SetOperationType = SetOperationType + Limit = Limit }; select1._projectionMapping = new Dictionary(_projectionMapping); @@ -455,18 +442,26 @@ public Expression ApplySetOperation( select1._identifier.AddRange(_identifier); _identifier.Clear(); - var select2 = otherSelectExpression; + var setExpression = (SetOperationBase)(setOperationType switch + { + SetOperationType.Except => new ExceptExpression("t", select1, select2, distinct), + SetOperationType.Intersect => new IntersectExpression("t", select1, select2, distinct), + SetOperationType.Union => new UnionExpression("t", select1, select2, distinct), + _ => throw new InvalidOperationException($"Invalid {nameof(setOperationType)}: {setOperationType}") + }); - if (_projection.Any()) + if (_projection.Any() || select2._projection.Any()) { - throw new InvalidOperationException("Can't process set operations after client evaluation, consider moving the operation before the last Select() call (see issue #16243)"); + throw new InvalidOperationException( + "Can't process set operations after client evaluation, consider moving the operation" + + " before the last Select() call (see issue #16243)"); } else { if (select1._projectionMapping.Count != select2._projectionMapping.Count) { // Should not be possible after compiler checks - throw new Exception("Different projection mapping count in set operation"); + throw new InvalidOperationException("Different projection mapping count in set operation"); } foreach (var joinedMapping in select1._projectionMapping.Join( @@ -492,10 +487,14 @@ public Expression ApplySetOperation( throw new InvalidOperationException("Set operations over different store types are currently unsupported"); } - var alias = joinedMapping.Key.Last?.Name; - select1.AddToProjection(innerColumn1, alias); - select2.AddToProjection(innerColumn2, alias); - _projectionMapping[joinedMapping.Key] = innerColumn1; + var alias = generateUniqueAlias(joinedMapping.Key.Last?.Name + ?? (innerColumn1 as ColumnExpression)?.Name + ?? "c"); + + var innerProjection = new ProjectionExpression(innerColumn1, alias); + select1._projection.Add(innerProjection); + select2._projection.Add(new ProjectionExpression(innerColumn2, alias)); + _projectionMapping[joinedMapping.Key] = new ColumnExpression(innerProjection, setExpression); continue; } @@ -508,54 +507,59 @@ public Expression ApplySetOperation( IsDistinct = false; Predicate = null; Having = null; + _groupBy.Clear(); _orderings.Clear(); _tables.Clear(); - _tables.Add(select1); - _tables.Add(otherSelectExpression); - SetOperationType = setOperationType; - - return shaperExpression; + _tables.Add(setExpression); void handleEntityMapping( ProjectionMember projectionMember, SelectExpression select1, EntityProjectionExpression projection1, SelectExpression select2, EntityProjectionExpression projection2) { - var propertyExpressions = new Dictionary(); - - if (projection1.EntityType == projection2.EntityType) + if (projection1.EntityType != projection2.EntityType) { - foreach (var property in GetAllPropertiesInHierarchy(projection1.EntityType)) - { - propertyExpressions[property] = addSetOperationColumnProjections( - property, - select1, projection1.BindProperty(property), - select2, projection2.BindProperty(property)); - } + throw new InvalidOperationException("Set operations over different entity types are currently unsupported (see #16298)"); + } - _projectionMapping[projectionMember] = new EntityProjectionExpression(projection1.EntityType, propertyExpressions); - return; + var propertyExpressions = new Dictionary(); + foreach (var property in GetAllPropertiesInHierarchy(projection1.EntityType)) + { + propertyExpressions[property] = addSetOperationColumnProjections( + select1, projection1.BindProperty(property), + select2, projection2.BindProperty(property)); } - throw new InvalidOperationException("Set operations over different entity types are currently unsupported (see #16298)"); + _projectionMapping[projectionMember] = new EntityProjectionExpression(projection1.EntityType, propertyExpressions); } ColumnExpression addSetOperationColumnProjections( - IProperty property, SelectExpression select1, ColumnExpression column1, SelectExpression select2, ColumnExpression column2) { - var columnName = column1.Name; + var alias = generateUniqueAlias(column1.Name); + var innerProjection = new ProjectionExpression(column1, alias); + select1._projection.Add(innerProjection); + select2._projection.Add(new ProjectionExpression(column2, alias)); + var outerProjection = new ColumnExpression(innerProjection, setExpression); + if (select1._identifier.Contains(column1)) + { + _identifier.Add(outerProjection); + } - select1._projection.Add(new ProjectionExpression(column1, columnName)); - select2._projection.Add(new ProjectionExpression(column2, columnName)); + return outerProjection; + } - if (select1._identifier.Contains(column1)) + string generateUniqueAlias(string baseAlias) + { + var currentAlias = baseAlias ?? ""; + var counter = 0; + while (select1._projection.Any(pe => string.Equals(pe.Alias, currentAlias, StringComparison.OrdinalIgnoreCase))) { - _identifier.Add(column1); + currentAlias = $"{baseAlias}{counter++}"; } - return column1; + return currentAlias; } } @@ -575,7 +579,6 @@ public IDictionary PushdownIntoSubquery() Having = Having, Offset = Offset, Limit = Limit, - SetOperationType = SetOperationType }; var projectionMap = new Dictionary(); @@ -700,7 +703,6 @@ EntityProjectionExpression liftEntityProjectionFromSubquery(EntityProjectionExpr IsDistinct = false; Predicate = null; Having = null; - SetOperationType = SetOperationType.None; _tables.Clear(); _tables.Add(subquery); _groupBy.Clear(); @@ -964,12 +966,8 @@ private SqlBinaryExpression ValidateKeyComparison(SelectExpression inner, SqlBin return null; } - // We treat a set operation as a transparent wrapper over its left operand (the ColumnExpression projection mappings - // found on a set operation SelectExpression are actually those of its left operand). private bool ContainsTableReference(TableExpressionBase table) - => IsSetOperation - ? ((SelectExpression)Tables[0]).ContainsTableReference(table) - : Tables.Any(te => ReferenceEquals(te is JoinExpressionBase jeb ? jeb.Table : te, table)); + => Tables.Any(te => ReferenceEquals(te is JoinExpressionBase jeb ? jeb.Table : te, table)); private class SelectExpressionCorrelationFindingExpressionVisitor : ExpressionVisitor { @@ -1094,7 +1092,7 @@ private void AddJoin( } // Verify what are the cases of pushdown for inner & outer both sides - if (Limit != null || Offset != null || IsDistinct || IsSetOperation || GroupBy.Count > 1) + if (Limit != null || Offset != null || IsDistinct || GroupBy.Count > 1) { var sqlRemappingVisitor = new SqlRemappingVisitor(PushdownIntoSubquery(), (SelectExpression)Tables[0]); innerSelectExpression = sqlRemappingVisitor.Remap(innerSelectExpression); @@ -1338,7 +1336,6 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) Offset = offset, Limit = limit, IsDistinct = IsDistinct, - SetOperationType = SetOperationType }; newSelectExpression._identifier.AddRange(_identifier); @@ -1364,11 +1361,6 @@ private bool Equals(SelectExpression selectExpression) return false; } - if (SetOperationType != selectExpression.SetOperationType) - { - return false; - } - if (_projectionMapping.Count != selectExpression._projectionMapping.Count) { return false; @@ -1461,7 +1453,6 @@ public SelectExpression Update( Offset = offset, Limit = limit, IsDistinct = distinct, - SetOperationType = SetOperationType }; } @@ -1470,8 +1461,6 @@ public override int GetHashCode() var hash = new HashCode(); hash.Add(base.GetHashCode()); - hash.Add(SetOperationType); - foreach (var projectionMapping in _projectionMapping) { hash.Add(projectionMapping.Key); @@ -1520,69 +1509,59 @@ public override void Print(ExpressionPrinter expressionPrinter) expressionPrinter.AppendLine(); IDisposable indent = null; - if (IsSetOperation) + if (Alias != null) { - expressionPrinter.Visit(Tables[0]); - expressionPrinter.AppendLine() - .AppendLine(SetOperationType.ToString().ToUpperInvariant()); - expressionPrinter.Visit(Tables[1]); + expressionPrinter.AppendLine("("); + indent = expressionPrinter.Indent(); } - else - { - if (Alias != null) - { - expressionPrinter.AppendLine("("); - indent = expressionPrinter.Indent(); - } - expressionPrinter.Append("SELECT "); + expressionPrinter.Append("SELECT "); - if (IsDistinct) - { - expressionPrinter.Append("DISTINCT "); - } + if (IsDistinct) + { + expressionPrinter.Append("DISTINCT "); + } - if (Limit != null - && Offset == null) - { - expressionPrinter.Append("TOP("); - expressionPrinter.Visit(Limit); - expressionPrinter.Append(") "); - } + if (Limit != null + && Offset == null) + { + expressionPrinter.Append("TOP("); + expressionPrinter.Visit(Limit); + expressionPrinter.Append(") "); + } - if (Projection.Any()) - { - expressionPrinter.VisitList(Projection); - } - else - { - expressionPrinter.Append("1"); - } + if (Projection.Any()) + { + expressionPrinter.VisitList(Projection); + } + else + { + expressionPrinter.Append("1"); + } - if (Tables.Any()) - { - expressionPrinter.AppendLine().Append("FROM "); + if (Tables.Any()) + { + expressionPrinter.AppendLine().Append("FROM "); - expressionPrinter.VisitList(Tables, p => p.AppendLine()); - } + expressionPrinter.VisitList(Tables, p => p.AppendLine()); + } - if (Predicate != null) - { - expressionPrinter.AppendLine().Append("WHERE "); - expressionPrinter.Visit(Predicate); - } + if (Predicate != null) + { + expressionPrinter.AppendLine().Append("WHERE "); + expressionPrinter.Visit(Predicate); + } - if (GroupBy.Any()) - { - expressionPrinter.AppendLine().Append("GROUP BY "); - expressionPrinter.VisitList(GroupBy); - } + if (GroupBy.Any()) + { + expressionPrinter.AppendLine().Append("GROUP BY "); + expressionPrinter.VisitList(GroupBy); + } - if (Having != null) - { - expressionPrinter.AppendLine().Append("HAVING "); - expressionPrinter.Visit(Having); - } + if (Having != null) + { + expressionPrinter.AppendLine().Append("HAVING "); + expressionPrinter.Visit(Having); } if (Orderings.Any()) @@ -1616,36 +1595,5 @@ public override void Print(ExpressionPrinter expressionPrinter) } } } - - /// - /// Marks a as representing an SQL set operation, such as a UNION. - /// - public enum SetOperationType - { - /// - /// Represents a regular SQL SELECT expression that isn't a set operation. - /// - None = 0, - - /// - /// Represents an SQL UNION set operation. - /// - Union = 1, - - /// - /// Represents an SQL UNION ALL set operation. - /// - UnionAll = 2, - - /// - /// Represents an SQL INTERSECT set operation. - /// - Intersect = 3, - - /// - /// Represents an SQL EXCEPT set operation. - /// - Except = 4 - } } diff --git a/src/EFCore.Relational/Query/SqlExpressions/SetOperationBase.cs b/src/EFCore.Relational/Query/SqlExpressions/SetOperationBase.cs new file mode 100644 index 00000000000..6ebc8a6f5c7 --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/SetOperationBase.cs @@ -0,0 +1,38 @@ +// 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 JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions +{ + public abstract class SetOperationBase : TableExpressionBase + { + protected SetOperationBase([NotNull] string alias, SelectExpression source1, SelectExpression source2, bool distinct) + : base(Check.NotEmpty(alias, nameof(alias))) + { + IsDistinct = distinct; + Source1 = source1; + Source2 = source2; + } + + public virtual bool IsDistinct { get; } + public virtual SelectExpression Source1 { get; } + public virtual SelectExpression Source2 { get; } + + public override bool Equals(object obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is SetOperationBase setOperationBase + && Equals(setOperationBase)); + + private bool Equals(SetOperationBase setOperationBase) + => IsDistinct == setOperationBase.IsDistinct + && Source1.Equals(setOperationBase.Source1) + && Source2.Equals(setOperationBase.Source2); + + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), IsDistinct, Source1, Source2); + } +} diff --git a/src/EFCore.Relational/Query/SqlExpressions/SqlConstantExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SqlConstantExpression.cs index f51a340822e..6e5870bfed6 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SqlConstantExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SqlConstantExpression.cs @@ -2,6 +2,8 @@ // 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.Collections; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Storage; @@ -35,9 +37,36 @@ public override bool Equals(object obj) private bool Equals(SqlConstantExpression sqlConstantExpression) => base.Equals(sqlConstantExpression) - && (Value == null - ? sqlConstantExpression.Value == null - : Value.Equals(sqlConstantExpression.Value)); + && ValueEquals(Value, sqlConstantExpression.Value); + + private bool ValueEquals(object value1, object value2) + { + if (value1 == null) + { + return value2 == null; + } + + if (value1 is IList list1 + && value2 is IList list2) + { + if (list1.Count != list2.Count) + { + return false; + } + + for (var i = 0; i < list1.Count; i++) + { + if (!ValueEquals(list1[i], list2[i])) + { + return false; + } + } + + return true; + } + + return value1.Equals(value2); + } public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Value); } diff --git a/src/EFCore.Relational/Query/SqlExpressions/TableExpressionBase.cs b/src/EFCore.Relational/Query/SqlExpressions/TableExpressionBase.cs index 290eb68d594..2c4ee690583 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/TableExpressionBase.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/TableExpressionBase.cs @@ -33,14 +33,6 @@ public override bool Equals(object obj) private bool Equals(TableExpressionBase tableExpressionBase) => string.Equals(Alias, tableExpressionBase.Alias); - public override int GetHashCode() - { - unchecked - { - var hashCode = (Alias?.GetHashCode() ?? 0); - - return hashCode; - } - } + public override int GetHashCode() => HashCode.Combine(Alias); } } diff --git a/src/EFCore.Relational/Query/SqlExpressions/UnionExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/UnionExpression.cs new file mode 100644 index 00000000000..94ce272bfab --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/UnionExpression.cs @@ -0,0 +1,59 @@ +// 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.Expressions; + +namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions +{ + public class UnionExpression : SetOperationBase + { + public UnionExpression(string alias, SelectExpression source1, SelectExpression source2, bool distinct) + : base(alias, source1, source2, distinct) + { + } + + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var source1 = (SelectExpression)visitor.Visit(Source1); + var source2 = (SelectExpression)visitor.Visit(Source2); + + return Update(source1, source2); + } + + public virtual UnionExpression Update(SelectExpression source1, SelectExpression source2) + => source1 != Source1 || source2 != Source2 + ? new UnionExpression(Alias, source1, source2, IsDistinct) + : this; + + public override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append("("); + using (expressionPrinter.Indent()) + { + expressionPrinter.Visit(Source1); + expressionPrinter.AppendLine(); + expressionPrinter.Append("UNION"); + if (!IsDistinct) + { + expressionPrinter.AppendLine(" ALL"); + } + expressionPrinter.Visit(Source2); + } + expressionPrinter.AppendLine() + .AppendLine($") AS {Alias}"); + } + + public override bool Equals(object obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is UnionExpression unionExpression + && Equals(unionExpression)); + + private bool Equals(UnionExpression unionExpression) + => base.Equals(unionExpression); + + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), GetType()); + } +} diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs index 488045538ab..890d66170ab 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs @@ -387,5 +387,38 @@ protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpres return ApplyConversion(rowNumberExpression.Update(partitions, orderings), condition: false); } + + protected override Expression VisitExcept(ExceptExpression exceptExpression) + { + var parentSearchCondition = _isSearchCondition; + _isSearchCondition = false; + var source1 = (SelectExpression)Visit(exceptExpression.Source1); + var source2 = (SelectExpression)Visit(exceptExpression.Source2); + _isSearchCondition = parentSearchCondition; + + return exceptExpression.Update(source1, source2); + } + + protected override Expression VisitIntersect(IntersectExpression intersectExpression) + { + var parentSearchCondition = _isSearchCondition; + _isSearchCondition = false; + var source1 = (SelectExpression)Visit(intersectExpression.Source1); + var source2 = (SelectExpression)Visit(intersectExpression.Source2); + _isSearchCondition = parentSearchCondition; + + return intersectExpression.Update(source1, source2); + } + + protected override Expression VisitUnion(UnionExpression unionExpression) + { + var parentSearchCondition = _isSearchCondition; + _isSearchCondition = false; + var source1 = (SelectExpression)Visit(unionExpression.Source1); + var source2 = (SelectExpression)Visit(unionExpression.Source2); + _isSearchCondition = parentSearchCondition; + + return unionExpression.Update(source1, source2); + } } } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs index d5cf5956ef6..374234907d6 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs @@ -45,21 +45,10 @@ protected override void GenerateLimitOffset(SelectExpression selectExpression) } } - protected override void GenerateSetOperationOperand( - SelectExpression setOperationExpression, - SelectExpression operandExpression) + protected override void GenerateSetOperationOperand(SetOperationBase setOperation, SelectExpression operand) { // Sqlite doesn't support parentheses around set operation operands - - IDisposable indent = null; - if (!operandExpression.IsSetOperation) - { - indent = Sql.Indent(); - } - - Visit(operandExpression); - - indent?.Dispose(); + Visit(operand); } } } diff --git a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.SetOperations.cs b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.SetOperations.cs index cf97c7cdcfb..eaffe99abf0 100644 --- a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.SetOperations.cs +++ b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.SetOperations.cs @@ -353,7 +353,7 @@ public virtual Task GroupBy_Select_Union(bool isAsync) .GroupBy(c => c.CustomerID) .Select(g => new { CustomerID = g.Key, Count = g.Count() }))); - [ConditionalTheory] + [ConditionalTheory(Skip = "Issue#17339")] [MemberData(nameof(GetSetOperandTestCases))] public virtual Task Union_over_different_projection_types(bool isAsync, string leftType, string rightType) { diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index 0eddf5ad25f..e2c242e7a5a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -1615,7 +1615,7 @@ SELECT [g].[Nickname] FROM [Gears] AS [g] WHERE [g].[Discriminator] IN (N'Gear', N'Officer') UNION ALL - SELECT [g0].[FullName] + SELECT [g0].[FullName] AS [Nickname] FROM [Gears] AS [g0] WHERE [g0].[Discriminator] IN (N'Gear', N'Officer') ) AS [t]"); @@ -1628,11 +1628,11 @@ public override async Task Concat_anonymous_with_count(bool isAsync) AssertSql( @"SELECT COUNT(*) FROM ( - SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOrBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] + SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOrBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [g].[Nickname] AS [Name] FROM [Gears] AS [g] WHERE [g].[Discriminator] IN (N'Gear', N'Officer') UNION ALL - SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOrBirthName], [g0].[Discriminator], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank] + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOrBirthName], [g0].[Discriminator], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank], [g0].[FullName] AS [Name] FROM [Gears] AS [g0] WHERE [g0].[Discriminator] IN (N'Gear', N'Officer') ) AS [t]"); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.SetOperations.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.SetOperations.cs index 78e8b20c0b1..7dc8f9b9b9a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.SetOperations.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.SetOperations.cs @@ -167,13 +167,36 @@ FROM [Customers] AS [c1] WHERE CHARINDEX(N'Thomas', [c1].[ContactName]) > 0"); } - [ConditionalTheory(Skip = "Need to push down set operation on take without orderby+skip on SQL Server, waiting on design")] + [ConditionalTheory] public override async Task Union_Take_Union_Take(bool isAsync) { await base.Union_Take_Union_Take(isAsync); - throw new NotImplementedException("Take is being ignored"); - //AssertSql(@""); + AssertSql( + @"@__p_0='1' + +SELECT [t1].[CustomerID], [t1].[Address], [t1].[City], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Country], [t1].[Fax], [t1].[Phone], [t1].[PostalCode], [t1].[Region] +FROM ( + SELECT TOP(@__p_0) [t0].[CustomerID], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region] + FROM ( + SELECT TOP(@__p_0) [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].[CustomerID] + UNION + 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].[City] = N'Mannheim') AND [c1].[City] IS NOT NULL + ) AS [t0] +) AS [t1] +ORDER BY [t1].[CustomerID]"); } public override async Task Select_Union(bool isAsync) @@ -211,13 +234,16 @@ 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%')"); + @"SELECT [t].[CustomerID] AS [Id] +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].[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%') +) AS [t]"); } public override async Task Select_Union_unrelated(bool isAsync) @@ -230,7 +256,7 @@ public override async Task Select_Union_unrelated(bool isAsync) SELECT [c].[ContactName] FROM [Customers] AS [c] UNION - SELECT [p].[ProductName] + SELECT [p].[ProductName] AS [ContactName] FROM [Products] AS [p] ) AS [t] WHERE [t].[ContactName] IS NOT NULL AND ([t].[ContactName] LIKE N'C%')