Skip to content

Commit

Permalink
Implement right join support
Browse files Browse the repository at this point in the history
  • Loading branch information
roji committed Jan 15, 2025
1 parent 4f4bd31 commit 73c1528
Show file tree
Hide file tree
Showing 43 changed files with 817 additions and 129 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,23 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou
return null;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override ShapedQueryExpression? TranslateRightJoin(
ShapedQueryExpression outer,
ShapedQueryExpression inner,
LambdaExpression outerKeySelector,
LambdaExpression innerKeySelector,
LambdaExpression resultSelector)
{
AddTranslationErrorDetails(CosmosStrings.CrossDocumentJoinNotSupported);
return null;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,20 @@ static bool IsConvertedToNullable(Expression outer, Expression inner)
return source;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override ShapedQueryExpression? TranslateRightJoin(
ShapedQueryExpression outer,
ShapedQueryExpression inner,
LambdaExpression outerKeySelector,
LambdaExpression innerKeySelector,
LambdaExpression resultSelector)
=> null;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ public RelationalValueConverterCompensatingExpressionVisitor(
protected override Expression VisitExtension(Expression extensionExpression)
=> extensionExpression switch
{
ShapedQueryExpression shapedQueryExpression => VisitShapedQueryExpression(shapedQueryExpression),
CaseExpression caseExpression => VisitCase(caseExpression),
SelectExpression selectExpression => VisitSelect(selectExpression),
InnerJoinExpression innerJoinExpression => VisitInnerJoin(innerJoinExpression),
LeftJoinExpression leftJoinExpression => VisitLeftJoin(leftJoinExpression),
ShapedQueryExpression shapedQuery => VisitShapedQueryExpression(shapedQuery),
CaseExpression @case => VisitCase(@case),
SelectExpression select => VisitSelect(select),
PredicateJoinExpressionBase join => VisitJoin(join),

_ => base.VisitExtension(extensionExpression)
};

Expand Down Expand Up @@ -86,20 +86,12 @@ private Expression VisitSelect(SelectExpression selectExpression)
return selectExpression.Update(tables, predicate, groupBy, having, projections, orderings, offset, limit);
}

private Expression VisitInnerJoin(InnerJoinExpression innerJoinExpression)
{
var table = (TableExpressionBase)Visit(innerJoinExpression.Table);
var joinPredicate = TryCompensateForBoolWithValueConverter((SqlExpression)Visit(innerJoinExpression.JoinPredicate));

return innerJoinExpression.Update(table, joinPredicate);
}

private Expression VisitLeftJoin(LeftJoinExpression leftJoinExpression)
private Expression VisitJoin(PredicateJoinExpressionBase joinExpression)
{
var table = (TableExpressionBase)Visit(leftJoinExpression.Table);
var joinPredicate = TryCompensateForBoolWithValueConverter((SqlExpression)Visit(leftJoinExpression.JoinPredicate));
var table = (TableExpressionBase)Visit(joinExpression.Table);
var joinPredicate = TryCompensateForBoolWithValueConverter((SqlExpression)Visit(joinExpression.JoinPredicate));

return leftJoinExpression.Update(table, joinPredicate);
return joinExpression.Update(table, joinPredicate);
}

[return: NotNullIfNotNull(nameof(sqlExpression))]
Expand Down
14 changes: 14 additions & 0 deletions src/EFCore.Relational/Query/QuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1250,6 +1250,20 @@ protected override Expression VisitLeftJoin(LeftJoinExpression leftJoinExpressio
return leftJoinExpression;
}

/// <summary>
/// Generates SQL for a right join.
/// </summary>
/// <param name="rightJoinExpression">The <see cref="RightJoinExpression" /> for which to generate SQL.</param>
protected override Expression VisitRightJoin(RightJoinExpression rightJoinExpression)
{
_relationalCommandBuilder.Append("RIGHT JOIN ");
Visit(rightJoinExpression.Table);
_relationalCommandBuilder.Append(" ON ");
Visit(rightJoinExpression.JoinPredicate);

return rightJoinExpression;
}

/// <summary>
/// Generates SQL for a scalar subquery.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,27 @@ protected override ShapedQueryExpression TranslateIntersect(ShapedQueryExpressio
return null;
}

/// <inheritdoc />
protected override ShapedQueryExpression? TranslateRightJoin(
ShapedQueryExpression outer,
ShapedQueryExpression inner,
LambdaExpression outerKeySelector,
LambdaExpression innerKeySelector,
LambdaExpression resultSelector)
{
var joinPredicate = CreateJoinPredicate(outer, outerKeySelector, inner, innerKeySelector);
if (joinPredicate != null)
{
var outerSelectExpression = (SelectExpression)outer.QueryExpression;
var outerShaperExpression = outerSelectExpression.AddRightJoin(inner, joinPredicate, outer.ShaperExpression);
outer = outer.UpdateShaperExpression(outerShaperExpression);

return TranslateTwoParameterSelector(outer, resultSelector);
}

return null;
}

private SqlExpression CreateJoinPredicate(
ShapedQueryExpression outer,
LambdaExpression outerKeySelector,
Expand Down
8 changes: 8 additions & 0 deletions src/EFCore.Relational/Query/SqlExpressionVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ ShapedQueryExpression shapedQueryExpression
OuterApplyExpression outerApplyExpression => VisitOuterApply(outerApplyExpression),
ProjectionExpression projectionExpression => VisitProjection(projectionExpression),
TableValuedFunctionExpression tableValuedFunctionExpression => VisitTableValuedFunction(tableValuedFunctionExpression),
RightJoinExpression rightJoinExpression => VisitRightJoin(rightJoinExpression),
RowNumberExpression rowNumberExpression => VisitRowNumber(rowNumberExpression),
RowValueExpression rowValueExpression => VisitRowValue(rowValueExpression),
ScalarSubqueryExpression scalarSubqueryExpression => VisitScalarSubquery(scalarSubqueryExpression),
Expand Down Expand Up @@ -193,6 +194,13 @@ ShapedQueryExpression shapedQueryExpression
/// <returns>The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.</returns>
protected abstract Expression VisitProjection(ProjectionExpression projectionExpression);

/// <summary>
/// Visits the children of the right join expression.
/// </summary>
/// <param name="rightJoinExpression">The expression to visit.</param>
/// <returns>The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.</returns>
protected abstract Expression VisitRightJoin(RightJoinExpression rightJoinExpression);

/// <summary>
/// Visits the children of the table valued function expression.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ public InnerJoinExpression(
/// <param name="table">The <see cref="JoinExpressionBase.Table" /> property of the result.</param>
/// <param name="joinPredicate">The <see cref="PredicateJoinExpressionBase.JoinPredicate" /> property of the result.</param>
/// <returns>This expression if no children changed, or an expression with the updated children.</returns>
public override InnerJoinExpression Update(TableExpressionBase table, SqlExpression joinPredicate)
=> table != Table || joinPredicate != JoinPredicate
? new InnerJoinExpression(table, joinPredicate, IsPrunable, Annotations)
: this;
public override JoinExpressionBase Update(TableExpressionBase table, SqlExpression joinPredicate)
=> table == Table && joinPredicate == JoinPredicate
? this
: joinPredicate is SqlConstantExpression { Value: true }
? new CrossJoinExpression(table)
: new InnerJoinExpression(table, joinPredicate, IsPrunable, Annotations);

/// <summary>
/// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
/// <param name="table">The <see cref="JoinExpressionBase.Table" /> property of the result.</param>
/// <param name="joinPredicate">The <see cref="PredicateJoinExpressionBase.JoinPredicate" /> property of the result.</param>
/// <returns>This expression if no children changed, or an expression with the updated children.</returns>
public abstract PredicateJoinExpressionBase Update(TableExpressionBase table, SqlExpression joinPredicate);
public abstract JoinExpressionBase Update(TableExpressionBase table, SqlExpression joinPredicate);

/// <inheritdoc />
public override bool Equals(object? obj)
Expand Down
106 changes: 106 additions & 0 deletions src/EFCore.Relational/Query/SqlExpressions/RightJoinExpression.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;

/// <summary>
/// <para>
/// An expression that represents a RIGHT JOIN in a SQL tree.
/// </para>
/// <para>
/// This type is typically used by database providers (and other extensions). It is generally
/// not used in application code.
/// </para>
/// </summary>
public class RightJoinExpression : PredicateJoinExpressionBase
{
private static ConstructorInfo? _quotingConstructor;

/// <summary>
/// Creates a new instance of the <see cref="LeftJoinExpression" /> class.
/// </summary>
/// <param name="table">A table source to LEFT JOIN with.</param>
/// <param name="joinPredicate">A predicate to use for the join.</param>
/// <param name="prunable">Whether this join expression may be pruned if nothing references a column on it.</param>
public RightJoinExpression(TableExpressionBase table, SqlExpression joinPredicate, bool prunable = false)
: this(table, joinPredicate, prunable, annotations: null)
{
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal] // For precompiled queries
public RightJoinExpression(
TableExpressionBase table,
SqlExpression joinPredicate,
bool prunable,
IReadOnlyDictionary<string, IAnnotation>? annotations = null)
: base(table, joinPredicate, prunable, annotations)
{
}

/// <summary>
/// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
/// return this expression.
/// </summary>
/// <param name="table">The <see cref="JoinExpressionBase.Table" /> property of the result.</param>
/// <param name="joinPredicate">The <see cref="PredicateJoinExpressionBase.JoinPredicate" /> property of the result.</param>
/// <returns>This expression if no children changed, or an expression with the updated children.</returns>
public override RightJoinExpression Update(TableExpressionBase table, SqlExpression joinPredicate)
=> table != Table || joinPredicate != JoinPredicate
? new RightJoinExpression(table, joinPredicate, IsPrunable, Annotations)
: this;

/// <summary>
/// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
/// return this expression.
/// </summary>
/// <param name="table">The <see cref="JoinExpressionBase.Table" /> property of the result.</param>
/// <returns>This expression if no children changed, or an expression with the updated children.</returns>
public override RightJoinExpression Update(TableExpressionBase table)
=> table != Table
? new RightJoinExpression(table, JoinPredicate, IsPrunable, Annotations)
: this;

/// <inheritdoc />
protected override RightJoinExpression WithAnnotations(IReadOnlyDictionary<string, IAnnotation> annotations)
=> new(Table, JoinPredicate, IsPrunable, annotations);

/// <inheritdoc />
public override Expression Quote()
=> New(
_quotingConstructor ??= typeof(RightJoinExpression).GetConstructor(
[typeof(TableExpressionBase), typeof(SqlExpression), typeof(bool), typeof(IReadOnlyDictionary<string, IAnnotation>)])!,
Table.Quote(),
JoinPredicate.Quote(),
Constant(IsPrunable),
RelationalExpressionQuotingUtilities.QuoteAnnotations(Annotations));

/// <inheritdoc />
protected override void Print(ExpressionPrinter expressionPrinter)
{
expressionPrinter.Append("RIGHT JOIN ");
expressionPrinter.Visit(Table);
expressionPrinter.Append(" ON ");
expressionPrinter.Visit(JoinPredicate);
PrintAnnotations(expressionPrinter);
}

/// <inheritdoc />
public override bool Equals(object? obj)
=> obj != null
&& (ReferenceEquals(this, obj)
|| obj is RightJoinExpression rightJoinExpression
&& Equals(rightJoinExpression));

private bool Equals(RightJoinExpression rightJoinExpression)
=> base.Equals(rightJoinExpression);

/// <inheritdoc />
public override int GetHashCode()
=> base.GetHashCode();
}
Loading

0 comments on commit 73c1528

Please sign in to comment.