Skip to content

Commit

Permalink
Implement set operations (Union, Intersect...).
Browse files Browse the repository at this point in the history
Navigation/include support not included.

Fixes #6812
Fixes #12549
  • Loading branch information
roji committed Jun 25, 2019
1 parent 9e970d9 commit 8d1f9cb
Show file tree
Hide file tree
Showing 16 changed files with 1,012 additions and 95 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -481,4 +481,7 @@
<data name="PendingAmbientTransaction" xml:space="preserve">
<value>This connection was used with an ambient transaction. The original ambient transaction needs to be completed before this connection can be used outside of it.</value>
</data>
</root>
<data name="SetOperationNotWithinEntityTypeHierarchy" xml:space="preserve">
<value>Set operations (Union, Concat, Intersect, Except) are only supported over entity types within the same type hierarchy.</value>
</data>
</root>
124 changes: 96 additions & 28 deletions src/EFCore.Relational/Query/Pipeline/QuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions;
Expand Down Expand Up @@ -79,6 +80,28 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
subQueryIndent = _relationalCommandBuilder.Indent();
}

if (selectExpression.IsSetOperation)
{
GenerateSetOperation(selectExpression);
}
else
{
GenerateSelect(selectExpression);
}

if (selectExpression.Alias != null)
{
subQueryIndent.Dispose();

_relationalCommandBuilder.AppendLine()
.Append(") AS " + _sqlGenerationHelper.DelimitIdentifier(selectExpression.Alias));
}

return selectExpression;
}

protected virtual void GenerateSelect(SelectExpression selectExpression)
{
_relationalCommandBuilder.Append("SELECT ");

if (selectExpression.IsDistinct)
Expand Down Expand Up @@ -111,40 +134,55 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
Visit(selectExpression.Predicate);
}

if (selectExpression.Orderings.Any())
{
var orderings = selectExpression.Orderings.ToList();
GenerateOrderings(selectExpression);
GenerateLimitOffset(selectExpression);
}

if (selectExpression.Limit == null
&& selectExpression.Offset == null)
{
orderings.RemoveAll(oe => oe.Expression is SqlConstantExpression || oe.Expression is SqlParameterExpression);
}
protected virtual void GenerateSetOperation(SelectExpression setOperationExpression)
{
Debug.Assert(setOperationExpression.Tables.Count == 2,
$"{nameof(SelectExpression)} with {setOperationExpression.Tables.Count} tables, must be 2");

if (orderings.Count > 0)
{
_relationalCommandBuilder.AppendLine()
.Append("ORDER BY ");
GenerateSetOperationOperand(setOperationExpression, (SelectExpression)setOperationExpression.Tables[0]);

GenerateList(orderings, e => Visit(e));
}
}
else if (selectExpression.Offset != null)
{
_relationalCommandBuilder.AppendLine().Append("ORDER BY (SELECT 1)");
}
_relationalCommandBuilder
.AppendLine()
.AppendLine(GenerateSetOperationType(setOperationExpression.SetOperationType));

GenerateLimitOffset(selectExpression);
GenerateSetOperationOperand(setOperationExpression, (SelectExpression)setOperationExpression.Tables[1]);

if (selectExpression.Alias != null)
{
subQueryIndent.Dispose();
GenerateOrderings(setOperationExpression);
GenerateLimitOffset(setOperationExpression);
}

_relationalCommandBuilder.AppendLine()
.Append(") AS " + _sqlGenerationHelper.DelimitIdentifier(selectExpression.Alias));
private static string GenerateSetOperationType(SetOperationType setOperationType)
=> setOperationType switch {
SetOperationType.Union => "UNION",
SetOperationType.UnionAll => "UNION ALL",
SetOperationType.Intersect => "INTERSECT",
SetOperationType.Except => "EXCEPT",
_ => throw new NotSupportedException($"Invalid {nameof(SetOperationType)}: {setOperationType}")
};

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)
{
_relationalCommandBuilder.AppendLine("(");
using (_relationalCommandBuilder.Indent())
{
Visit(operandExpression);
}
_relationalCommandBuilder.AppendLine().Append(")");
return;
}

return selectExpression;
Visit(operandExpression);
}

protected override Expression VisitProjection(ProjectionExpression projectionExpression)
Expand Down Expand Up @@ -198,9 +236,13 @@ 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;
Expand Down Expand Up @@ -538,6 +580,32 @@ protected virtual void GenerateTop(SelectExpression selectExpression)
{
}

protected virtual void GenerateOrderings(SelectExpression selectExpression)
{
if (selectExpression.Orderings.Any())
{
var orderings = selectExpression.Orderings.ToList();

if (selectExpression.Limit == null
&& selectExpression.Offset == null)
{
orderings.RemoveAll(oe => oe.Expression is SqlConstantExpression || oe.Expression is SqlParameterExpression);
}

if (orderings.Count > 0)
{
_relationalCommandBuilder.AppendLine()
.Append("ORDER BY ");

GenerateList(orderings, e => Visit(e));
}
}
else if (selectExpression.Offset != null)
{
_relationalCommandBuilder.AppendLine().Append("ORDER BY (SELECT 1)");
}
}

protected virtual void GenerateLimitOffset(SelectExpression selectExpression)
{
// The below implements ISO SQL:2008
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.EntityFrameworkCore.Query.Pipeline;
using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions;
using System.Diagnostics;
using Microsoft.EntityFrameworkCore.Storage;

namespace Microsoft.EntityFrameworkCore.Relational.Query.Pipeline
{
Expand Down Expand Up @@ -150,7 +151,13 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou
return source;
}

protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression source1, ShapedQueryExpression source2) => throw new NotImplementedException();
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);
return source1;
}

protected override ShapedQueryExpression TranslateContains(ShapedQueryExpression source, Expression item)
{
Expand Down Expand Up @@ -212,7 +219,13 @@ protected override ShapedQueryExpression TranslateDistinct(ShapedQueryExpression

protected override ShapedQueryExpression TranslateElementAtOrDefault(ShapedQueryExpression source, Expression index, bool returnDefault) => throw new NotImplementedException();

protected override ShapedQueryExpression TranslateExcept(ShapedQueryExpression source1, ShapedQueryExpression source2) => throw new NotImplementedException();
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);
return source1;
}

protected override ShapedQueryExpression TranslateFirstOrDefault(ShapedQueryExpression source, LambdaExpression predicate, Type returnType, bool returnDefault)
{
Expand Down Expand Up @@ -279,7 +292,13 @@ protected override ShapedQueryExpression TranslateGroupJoin(ShapedQueryExpressio
throw new NotImplementedException();
}

protected override ShapedQueryExpression TranslateIntersect(ShapedQueryExpression source1, ShapedQueryExpression source2) => throw new NotImplementedException();
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);
return source1;
}

protected override ShapedQueryExpression TranslateJoin(
ShapedQueryExpression outer,
Expand Down Expand Up @@ -730,7 +749,13 @@ protected override ShapedQueryExpression TranslateThenBy(ShapedQueryExpression s
throw new InvalidOperationException();
}

protected override ShapedQueryExpression TranslateUnion(ShapedQueryExpression source1, ShapedQueryExpression source2) => throw new NotImplementedException();
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);
return source1;
}

protected override ShapedQueryExpression TranslateWhere(ShapedQueryExpression source, LambdaExpression predicate)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,14 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
public ColumnExpression MakeNullable()
=> new ColumnExpression(Name, Table, Type.MakeNullable(), TypeMapping, true);


public override void Print(ExpressionPrinter expressionPrinter)
=> expressionPrinter.StringBuilder.Append(Table.Alias).Append(".").Append(Name);
{
if (Table.Alias != null)
{
expressionPrinter.StringBuilder.Append(Table.Alias).Append(".");
}
expressionPrinter.StringBuilder.Append(Name);
}

public override bool Equals(object obj)
=> obj != null
Expand All @@ -66,6 +71,7 @@ private bool Equals(ColumnExpression columnExpression)

public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Name, Table, Nullable);

private string DebuggerDisplay() => $"{Table.Alias}.{Name}";
private string DebuggerDisplay()
=> Table.Alias == null ? Name : $"{Table.Alias}.{Name}";
}
}
Loading

0 comments on commit 8d1f9cb

Please sign in to comment.