Skip to content

Commit 19ae80d

Browse files
committed
Implement right join support
Closes #35367
1 parent 01ae6cb commit 19ae80d

File tree

43 files changed

+817
-129
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+817
-129
lines changed

src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,23 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou
807807
return null;
808808
}
809809

810+
/// <summary>
811+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
812+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
813+
/// any release. You should only use it directly in your code with extreme caution and knowing that
814+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
815+
/// </summary>
816+
protected override ShapedQueryExpression? TranslateRightJoin(
817+
ShapedQueryExpression outer,
818+
ShapedQueryExpression inner,
819+
LambdaExpression outerKeySelector,
820+
LambdaExpression innerKeySelector,
821+
LambdaExpression resultSelector)
822+
{
823+
AddTranslationErrorDetails(CosmosStrings.CrossDocumentJoinNotSupported);
824+
return null;
825+
}
826+
810827
/// <summary>
811828
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
812829
/// the same compatibility standards as public APIs. It may be changed or removed without notice in

src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,20 @@ static bool IsConvertedToNullable(Expression outer, Expression inner)
833833
return source;
834834
}
835835

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

src/EFCore.Relational/Query/Internal/RelationalValueConverterCompensatingExpressionVisitor.cs

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ public RelationalValueConverterCompensatingExpressionVisitor(
3535
protected override Expression VisitExtension(Expression extensionExpression)
3636
=> extensionExpression switch
3737
{
38-
ShapedQueryExpression shapedQueryExpression => VisitShapedQueryExpression(shapedQueryExpression),
39-
CaseExpression caseExpression => VisitCase(caseExpression),
40-
SelectExpression selectExpression => VisitSelect(selectExpression),
41-
InnerJoinExpression innerJoinExpression => VisitInnerJoin(innerJoinExpression),
42-
LeftJoinExpression leftJoinExpression => VisitLeftJoin(leftJoinExpression),
38+
ShapedQueryExpression shapedQuery => VisitShapedQueryExpression(shapedQuery),
39+
CaseExpression @case => VisitCase(@case),
40+
SelectExpression select => VisitSelect(select),
41+
PredicateJoinExpressionBase join => VisitJoin(join),
42+
4343
_ => base.VisitExtension(extensionExpression)
4444
};
4545

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

89-
private Expression VisitInnerJoin(InnerJoinExpression innerJoinExpression)
90-
{
91-
var table = (TableExpressionBase)Visit(innerJoinExpression.Table);
92-
var joinPredicate = TryCompensateForBoolWithValueConverter((SqlExpression)Visit(innerJoinExpression.JoinPredicate));
93-
94-
return innerJoinExpression.Update(table, joinPredicate);
95-
}
96-
97-
private Expression VisitLeftJoin(LeftJoinExpression leftJoinExpression)
89+
private Expression VisitJoin(PredicateJoinExpressionBase joinExpression)
9890
{
99-
var table = (TableExpressionBase)Visit(leftJoinExpression.Table);
100-
var joinPredicate = TryCompensateForBoolWithValueConverter((SqlExpression)Visit(leftJoinExpression.JoinPredicate));
91+
var table = (TableExpressionBase)Visit(joinExpression.Table);
92+
var joinPredicate = TryCompensateForBoolWithValueConverter((SqlExpression)Visit(joinExpression.JoinPredicate));
10193

102-
return leftJoinExpression.Update(table, joinPredicate);
94+
return joinExpression.Update(table, joinPredicate);
10395
}
10496

10597
[return: NotNullIfNotNull(nameof(sqlExpression))]

src/EFCore.Relational/Query/QuerySqlGenerator.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,20 @@ protected override Expression VisitLeftJoin(LeftJoinExpression leftJoinExpressio
12501250
return leftJoinExpression;
12511251
}
12521252

1253+
/// <summary>
1254+
/// Generates SQL for a right join.
1255+
/// </summary>
1256+
/// <param name="rightJoinExpression">The <see cref="RightJoinExpression" /> for which to generate SQL.</param>
1257+
protected override Expression VisitRightJoin(RightJoinExpression rightJoinExpression)
1258+
{
1259+
_relationalCommandBuilder.Append("RIGHT JOIN ");
1260+
Visit(rightJoinExpression.Table);
1261+
_relationalCommandBuilder.Append(" ON ");
1262+
Visit(rightJoinExpression.JoinPredicate);
1263+
1264+
return rightJoinExpression;
1265+
}
1266+
12531267
/// <summary>
12541268
/// Generates SQL for a scalar subquery.
12551269
/// </summary>

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,27 @@ protected override ShapedQueryExpression TranslateIntersect(ShapedQueryExpressio
876876
return null;
877877
}
878878

879+
/// <inheritdoc />
880+
protected override ShapedQueryExpression? TranslateRightJoin(
881+
ShapedQueryExpression outer,
882+
ShapedQueryExpression inner,
883+
LambdaExpression outerKeySelector,
884+
LambdaExpression innerKeySelector,
885+
LambdaExpression resultSelector)
886+
{
887+
var joinPredicate = CreateJoinPredicate(outer, outerKeySelector, inner, innerKeySelector);
888+
if (joinPredicate != null)
889+
{
890+
var outerSelectExpression = (SelectExpression)outer.QueryExpression;
891+
var outerShaperExpression = outerSelectExpression.AddRightJoin(inner, joinPredicate, outer.ShaperExpression);
892+
outer = outer.UpdateShaperExpression(outerShaperExpression);
893+
894+
return TranslateTwoParameterSelector(outer, resultSelector);
895+
}
896+
897+
return null;
898+
}
899+
879900
private SqlExpression CreateJoinPredicate(
880901
ShapedQueryExpression outer,
881902
LambdaExpression outerKeySelector,

src/EFCore.Relational/Query/SqlExpressionVisitor.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ ShapedQueryExpression shapedQueryExpression
4242
OuterApplyExpression outerApplyExpression => VisitOuterApply(outerApplyExpression),
4343
ProjectionExpression projectionExpression => VisitProjection(projectionExpression),
4444
TableValuedFunctionExpression tableValuedFunctionExpression => VisitTableValuedFunction(tableValuedFunctionExpression),
45+
RightJoinExpression rightJoinExpression => VisitRightJoin(rightJoinExpression),
4546
RowNumberExpression rowNumberExpression => VisitRowNumber(rowNumberExpression),
4647
RowValueExpression rowValueExpression => VisitRowValue(rowValueExpression),
4748
ScalarSubqueryExpression scalarSubqueryExpression => VisitScalarSubquery(scalarSubqueryExpression),
@@ -193,6 +194,13 @@ ShapedQueryExpression shapedQueryExpression
193194
/// <returns>The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.</returns>
194195
protected abstract Expression VisitProjection(ProjectionExpression projectionExpression);
195196

197+
/// <summary>
198+
/// Visits the children of the right join expression.
199+
/// </summary>
200+
/// <param name="rightJoinExpression">The expression to visit.</param>
201+
/// <returns>The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.</returns>
202+
protected abstract Expression VisitRightJoin(RightJoinExpression rightJoinExpression);
203+
196204
/// <summary>
197205
/// Visits the children of the table valued function expression.
198206
/// </summary>

src/EFCore.Relational/Query/SqlExpressions/InnerJoinExpression.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,12 @@ public InnerJoinExpression(
5050
/// <param name="table">The <see cref="JoinExpressionBase.Table" /> property of the result.</param>
5151
/// <param name="joinPredicate">The <see cref="PredicateJoinExpressionBase.JoinPredicate" /> property of the result.</param>
5252
/// <returns>This expression if no children changed, or an expression with the updated children.</returns>
53-
public override InnerJoinExpression Update(TableExpressionBase table, SqlExpression joinPredicate)
54-
=> table != Table || joinPredicate != JoinPredicate
55-
? new InnerJoinExpression(table, joinPredicate, IsPrunable, Annotations)
56-
: this;
53+
public override JoinExpressionBase Update(TableExpressionBase table, SqlExpression joinPredicate)
54+
=> table == Table && joinPredicate == JoinPredicate
55+
? this
56+
: joinPredicate is SqlConstantExpression { Value: true }
57+
? new CrossJoinExpression(table)
58+
: new InnerJoinExpression(table, joinPredicate, IsPrunable, Annotations);
5759

5860
/// <summary>
5961
/// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will

src/EFCore.Relational/Query/SqlExpressions/PredicateJoinExpressionBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
5050
/// <param name="table">The <see cref="JoinExpressionBase.Table" /> property of the result.</param>
5151
/// <param name="joinPredicate">The <see cref="PredicateJoinExpressionBase.JoinPredicate" /> property of the result.</param>
5252
/// <returns>This expression if no children changed, or an expression with the updated children.</returns>
53-
public abstract PredicateJoinExpressionBase Update(TableExpressionBase table, SqlExpression joinPredicate);
53+
public abstract JoinExpressionBase Update(TableExpressionBase table, SqlExpression joinPredicate);
5454

5555
/// <inheritdoc />
5656
public override bool Equals(object? obj)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
5+
6+
/// <summary>
7+
/// <para>
8+
/// An expression that represents a RIGHT JOIN in a SQL tree.
9+
/// </para>
10+
/// <para>
11+
/// This type is typically used by database providers (and other extensions). It is generally
12+
/// not used in application code.
13+
/// </para>
14+
/// </summary>
15+
public class RightJoinExpression : PredicateJoinExpressionBase
16+
{
17+
private static ConstructorInfo? _quotingConstructor;
18+
19+
/// <summary>
20+
/// Creates a new instance of the <see cref="LeftJoinExpression" /> class.
21+
/// </summary>
22+
/// <param name="table">A table source to LEFT JOIN with.</param>
23+
/// <param name="joinPredicate">A predicate to use for the join.</param>
24+
/// <param name="prunable">Whether this join expression may be pruned if nothing references a column on it.</param>
25+
public RightJoinExpression(TableExpressionBase table, SqlExpression joinPredicate, bool prunable = false)
26+
: this(table, joinPredicate, prunable, annotations: null)
27+
{
28+
}
29+
30+
/// <summary>
31+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
32+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
33+
/// any release. You should only use it directly in your code with extreme caution and knowing that
34+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
35+
/// </summary>
36+
[EntityFrameworkInternal] // For precompiled queries
37+
public RightJoinExpression(
38+
TableExpressionBase table,
39+
SqlExpression joinPredicate,
40+
bool prunable,
41+
IReadOnlyDictionary<string, IAnnotation>? annotations = null)
42+
: base(table, joinPredicate, prunable, annotations)
43+
{
44+
}
45+
46+
/// <summary>
47+
/// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
48+
/// return this expression.
49+
/// </summary>
50+
/// <param name="table">The <see cref="JoinExpressionBase.Table" /> property of the result.</param>
51+
/// <param name="joinPredicate">The <see cref="PredicateJoinExpressionBase.JoinPredicate" /> property of the result.</param>
52+
/// <returns>This expression if no children changed, or an expression with the updated children.</returns>
53+
public override RightJoinExpression Update(TableExpressionBase table, SqlExpression joinPredicate)
54+
=> table != Table || joinPredicate != JoinPredicate
55+
? new RightJoinExpression(table, joinPredicate, IsPrunable, Annotations)
56+
: this;
57+
58+
/// <summary>
59+
/// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
60+
/// return this expression.
61+
/// </summary>
62+
/// <param name="table">The <see cref="JoinExpressionBase.Table" /> property of the result.</param>
63+
/// <returns>This expression if no children changed, or an expression with the updated children.</returns>
64+
public override RightJoinExpression Update(TableExpressionBase table)
65+
=> table != Table
66+
? new RightJoinExpression(table, JoinPredicate, IsPrunable, Annotations)
67+
: this;
68+
69+
/// <inheritdoc />
70+
protected override RightJoinExpression WithAnnotations(IReadOnlyDictionary<string, IAnnotation> annotations)
71+
=> new(Table, JoinPredicate, IsPrunable, annotations);
72+
73+
/// <inheritdoc />
74+
public override Expression Quote()
75+
=> New(
76+
_quotingConstructor ??= typeof(RightJoinExpression).GetConstructor(
77+
[typeof(TableExpressionBase), typeof(SqlExpression), typeof(bool), typeof(IReadOnlyDictionary<string, IAnnotation>)])!,
78+
Table.Quote(),
79+
JoinPredicate.Quote(),
80+
Constant(IsPrunable),
81+
RelationalExpressionQuotingUtilities.QuoteAnnotations(Annotations));
82+
83+
/// <inheritdoc />
84+
protected override void Print(ExpressionPrinter expressionPrinter)
85+
{
86+
expressionPrinter.Append("RIGHT JOIN ");
87+
expressionPrinter.Visit(Table);
88+
expressionPrinter.Append(" ON ");
89+
expressionPrinter.Visit(JoinPredicate);
90+
PrintAnnotations(expressionPrinter);
91+
}
92+
93+
/// <inheritdoc />
94+
public override bool Equals(object? obj)
95+
=> obj != null
96+
&& (ReferenceEquals(this, obj)
97+
|| obj is RightJoinExpression rightJoinExpression
98+
&& Equals(rightJoinExpression));
99+
100+
private bool Equals(RightJoinExpression rightJoinExpression)
101+
=> base.Equals(rightJoinExpression);
102+
103+
/// <inheritdoc />
104+
public override int GetHashCode()
105+
=> base.GetHashCode();
106+
}

0 commit comments

Comments
 (0)