Skip to content

Commit

Permalink
Draft work on set operation support
Browse files Browse the repository at this point in the history
  • Loading branch information
roji committed Jun 17, 2019
1 parent d58787e commit ac538d4
Show file tree
Hide file tree
Showing 7 changed files with 885 additions and 403 deletions.
116 changes: 93 additions & 23 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.SetOperationType == SetOperationType.None)
{
GenerateSelect(selectExpression);
}
else
{
GenerateSetOperation(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,61 @@ 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();
_relationalCommandBuilder.AppendLine(setOperationExpression.SetOperationType switch {
SetOperationType.Union => "UNION",
SetOperationType.UnionAll => "UNION ALL",
SetOperationType.Intersect => "INTERSECT",
SetOperationType.Except => "EXCEPT",
_ => throw new NotSupportedException($"Invalid {nameof(SetOperationType)}: {setOperationExpression.SetOperationType}")
});

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

GenerateOrderings(setOperationExpression);
GenerateLimitOffset(setOperationExpression);
}

protected virtual void GenerateSetOperationOperand(
SelectExpression setOperationExpression,
SelectExpression operand1)
{
var parensOpened = false;
IDisposable indent = null;
if (operand1.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 (operand1.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(operand1);

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 @@ -542,6 +586,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)
{
if (selectExpression.Offset != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,36 @@ private RelationalQueryableMethodTranslatingExpressionVisitor(
_sqlExpressionFactory = sqlExpressionFactory;
}

protected override Expression TranslateQueryableMethodCall(
MethodCallExpression methodCallExpression,
ShapedQueryExpression source)
{
var selectExpression = (SelectExpression)source.QueryExpression;

if (selectExpression.IsSetOperation && IsSetOperationPushdownRequired(methodCallExpression))
{
selectExpression.PushdownIntoSubquery();
}

return base.TranslateQueryableMethodCall(methodCallExpression, source);
}

/// <summary>
/// Most LINQ operators over a set operation cause a pushdown into a subquery (e.g. ("SELECT * FROM (a UNION b) WHERE ...")),
/// but some operators are supported directly on the set operation (e.g. ("a UNION b ORDER BY x")). This method is
/// responsible for performing pushdown as necessary.
/// </summary>
protected virtual bool IsSetOperationPushdownRequired(MethodCallExpression methodCallExpression)
=> methodCallExpression.Method.Name switch {
nameof(Queryable.Union) => false,
nameof(Queryable.Intersect) => false,
nameof(Queryable.Except) => false,
nameof(Queryable.OrderBy) => false,
nameof(Queryable.Take) => false,
nameof(Queryable.Skip) => false,
_ => true
};

public override ShapedQueryExpression TranslateSubquery(Expression expression)
{
return (ShapedQueryExpression)new RelationalQueryableMethodTranslatingExpressionVisitor(
Expand Down Expand Up @@ -153,7 +183,14 @@ 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)
{
// TODO: Make sure we're doing the operation over entity types from the same hierarchy
var operand1 = (SelectExpression)source1.QueryExpression;
var operand2 = (SelectExpression)source2.QueryExpression;
operand1.WrapWithSetOperation(SetOperationType.UnionAll, operand2);
return source1;
}

protected override ShapedQueryExpression TranslateContains(ShapedQueryExpression source, Expression item)
{
Expand Down Expand Up @@ -215,7 +252,14 @@ 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)
{
// TODO: Make sure we're doing the operation over entity types from the same hierarchy
var operand1 = (SelectExpression)source1.QueryExpression;
var operand2 = (SelectExpression)source2.QueryExpression;
operand1.WrapWithSetOperation(SetOperationType.Except, operand2);
return source1;
}

protected override ShapedQueryExpression TranslateFirstOrDefault(ShapedQueryExpression source, LambdaExpression predicate, Type returnType, bool returnDefault)
{
Expand Down Expand Up @@ -282,7 +326,14 @@ 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)
{
// TODO: Make sure we're doing the operation over entity types from the same hierarchy
var operand1 = (SelectExpression)source1.QueryExpression;
var operand2 = (SelectExpression)source2.QueryExpression;
operand1.WrapWithSetOperation(SetOperationType.Intersect, operand2);
return source1;
}

protected override ShapedQueryExpression TranslateJoin(
ShapedQueryExpression outer,
Expand Down Expand Up @@ -733,7 +784,14 @@ 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)
{
// TODO: Make sure we're doing the operation over entity types from the same hierarchy
var operand1 = (SelectExpression)source1.QueryExpression;
var operand2 = (SelectExpression)source2.QueryExpression;
operand1.WrapWithSetOperation(SetOperationType.Union, operand2);
return source1;
}

protected override ShapedQueryExpression TranslateWhere(ShapedQueryExpression source, LambdaExpression predicate)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ private readonly IDictionary<EntityProjectionExpression, IDictionary<IProperty,
public SqlExpression Offset { get; private set; }
public bool IsDistinct { get; private set; }

/// <summary>
/// Marks this <see cref="SelectExpression"/> as representing an SQL set operation, such as a UNION.
/// For regular SQL SELECT expressions, contains <c>None</c>.
/// </summary>
public SetOperationType SetOperationType { get; private set; }

/// <summary>
/// Returns whether this <see cref="SelectExpression"/> represents an SQL set operation, such as a UNION.
/// </summary>
public bool IsSetOperation => SetOperationType != SetOperationType.None;

internal SelectExpression(
string alias,
List<ProjectionExpression> projections,
Expand Down Expand Up @@ -330,14 +341,42 @@ public void ClearOrdering()
_orderings.Clear();
}

public void WrapWithSetOperation(
SetOperationType setOperationType,
SelectExpression otherSelectExpression)
{
var select1 = new SelectExpression(null, new List<ProjectionExpression>(), _tables.ToList(), _orderings.ToList())
{
IsDistinct = IsDistinct,
Predicate = Predicate,
Offset = Offset,
Limit = Limit,
SetOperationType = SetOperationType
};

select1._projectionMapping = new Dictionary<ProjectionMember, Expression>(_projectionMapping);
select1._identifyingProjection.AddRange(_identifyingProjection);

Offset = null;
Limit = null;
IsDistinct = false;
Predicate = null;
_orderings.Clear();
_tables.Clear();
_tables.Add(select1);
_tables.Add(otherSelectExpression);
SetOperationType = setOperationType;
}

public IDictionary<SqlExpression, ColumnExpression> PushdownIntoSubquery()
{
var subquery = new SelectExpression("t", new List<ProjectionExpression>(), _tables.ToList(), _orderings.ToList())
{
IsDistinct = IsDistinct,
Predicate = Predicate,
Offset = Offset,
Limit = Limit
Limit = Limit,
SetOperationType = SetOperationType
};

if (subquery.Limit == null && subquery.Offset == null)
Expand Down Expand Up @@ -422,6 +461,7 @@ public IDictionary<SqlExpression, ColumnExpression> PushdownIntoSubquery()
Limit = null;
IsDistinct = false;
Predicate = null;
SetOperationType = SetOperationType.None;
_tables.Clear();
_tables.Add(subquery);

Expand Down Expand Up @@ -848,7 +888,8 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
Predicate = predicate,
Offset = offset,
Limit = limit,
IsDistinct = IsDistinct
IsDistinct = IsDistinct,
SetOperationType = SetOperationType
};

return newSelectExpression;
Expand Down Expand Up @@ -1064,4 +1105,36 @@ public override void Print(ExpressionPrinter expressionPrinter)
}
}
}

/// <summary>
/// Marks a <see cref="SelectExpression"/> as representing an SQL set operation, such as a UNION.
/// </summary>
public enum SetOperationType
{
/// <summary>
/// Represents a regular SQL SELECT expression that isn't a set operation.
/// </summary>
None = 0,

/// <summary>
/// Represents an SQL UNION set operation.
/// </summary>
Union = 1,

/// <summary>
/// Represents an SQL UNION ALL set operation.
/// </summary>
UnionAll = 2,

/// <summary>
/// Represents an SQL INTERSECT set operation.
/// </summary>
Intersect = 3,

/// <summary>
/// Represents an SQL EXCEPT set operation.
/// </summary>
Except = 4
}
}

Loading

0 comments on commit ac538d4

Please sign in to comment.