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 24, 2019
1 parent e649a72 commit ada1ac2
Show file tree
Hide file tree
Showing 15 changed files with 1,010 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>
128 changes: 103 additions & 25 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,65 @@ 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));
_relationalCommandBuilder
.AppendLine()
.AppendLine(GenerateSetOperationType(setOperationExpression.SetOperationType));

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

GenerateOrderings(setOperationExpression);
GenerateLimitOffset(setOperationExpression);
}

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)
{
var parensOpened = false;
IDisposable indent = null;
if (operandExpression.IsSetOperation)
{
// 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.SetOperationType != setOperationExpression.SetOperationType)
{
_relationalCommandBuilder.AppendLine("(");
parensOpened = true;
indent = _relationalCommandBuilder.Indent();
}
}
else if (selectExpression.Offset != null)
else
{
_relationalCommandBuilder.AppendLine().Append("ORDER BY (SELECT 1)");
indent = _relationalCommandBuilder.Indent();
}

GenerateLimitOffset(selectExpression);
Visit(operandExpression);

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

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

return selectExpression;
}

protected override Expression VisitProjection(ProjectionExpression projectionExpression)
Expand Down Expand Up @@ -198,9 +246,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 +590,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
Loading

0 comments on commit ada1ac2

Please sign in to comment.