From 19ae80d9150398491f182f1235c54aa3f57fd9cb Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 13 Jan 2025 11:39:15 +0100 Subject: [PATCH] Implement right join support Closes #35367 --- ...yableMethodTranslatingExpressionVisitor.cs | 17 +++ ...yableMethodTranslatingExpressionVisitor.cs | 14 +++ ...eConverterCompensatingExpressionVisitor.cs | 26 ++--- .../Query/QuerySqlGenerator.cs | 14 +++ ...yableMethodTranslatingExpressionVisitor.cs | 21 ++++ .../Query/SqlExpressionVisitor.cs | 8 ++ .../SqlExpressions/InnerJoinExpression.cs | 10 +- .../PredicateJoinExpressionBase.cs | 2 +- .../SqlExpressions/RightJoinExpression.cs | 106 ++++++++++++++++++ .../Query/SqlExpressions/SelectExpression.cs | 64 ++++++++--- .../Query/SqlNullabilityProcessor.cs | 18 +-- .../NavigationExpandingExpressionVisitor.cs | 96 +++++++--------- ...yableMethodTranslatingExpressionVisitor.cs | 34 ++++++ src/EFCore/Query/QueryableMethods.cs | 18 +++ .../Query/JsonQueryCosmosTest.cs | 9 +- .../Query/GearsOfWarQueryInMemoryTest.cs | 4 + ...hwindEFPropertyIncludeQueryInMemoryTest.cs | 7 +- ...hwindIncludeNoTrackingQueryInMemoryTest.cs | 7 +- .../NorthwindIncludeQueryInMemoryTest.cs | 7 +- .../Query/NorthwindJoinQueryInMemoryTest.cs | 4 + ...NorthwindStringIncludeQueryInMemoryTest.cs | 7 +- .../NorthwindBulkUpdatesTestBase.cs | 29 +++++ .../Query/GearsOfWarQueryTestBase.cs | 28 +++++ .../Query/JsonQueryTestBase.cs | 26 ++++- .../Query/NorthwindIncludeQueryTestBase.cs | 12 ++ .../Query/NorthwindJoinQueryTestBase.cs | 13 +++ .../Query/OwnedQueryTestBase.cs | 9 +- .../NorthwindBulkUpdatesSqlServerTest.cs | 42 +++++++ .../Query/GearsOfWarQuerySqlServerTest.cs | 15 +++ .../Query/JsonQueryJsonTypeSqlServerTest.cs | 31 ++++- .../Query/JsonQuerySqlServerTest.cs | 16 ++- ...windEFPropertyIncludeQuerySqlServerTest.cs | 15 +++ ...windIncludeNoTrackingQuerySqlServerTest.cs | 15 +++ .../NorthwindIncludeQuerySqlServerTest.cs | 15 +++ .../Query/NorthwindJoinQuerySqlServerTest.cs | 12 ++ ...plitIncludeNoTrackingQuerySqlServerTest.cs | 23 ++++ ...NorthwindSplitIncludeQuerySqlServerTest.cs | 23 ++++ ...orthwindStringIncludeQuerySqlServerTest.cs | 15 +++ .../Query/TPCGearsOfWarQuerySqlServerTest.cs | 21 ++++ .../Query/TPTGearsOfWarQuerySqlServerTest.cs | 15 +++ .../TemporalGearsOfWarQuerySqlServerTest.cs | 15 +++ .../NorthwindBulkUpdatesSqliteTest.cs | 48 ++++++++ .../Query/GearsOfWarQuerySqliteTest.cs | 15 +++ 43 files changed, 817 insertions(+), 129 deletions(-) create mode 100644 src/EFCore.Relational/Query/SqlExpressions/RightJoinExpression.cs diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index e3d81672bf0..7083e6978ea 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -807,6 +807,23 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou return null; } + /// + /// 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. + /// + protected override ShapedQueryExpression? TranslateRightJoin( + ShapedQueryExpression outer, + ShapedQueryExpression inner, + LambdaExpression outerKeySelector, + LambdaExpression innerKeySelector, + LambdaExpression resultSelector) + { + AddTranslationErrorDetails(CosmosStrings.CrossDocumentJoinNotSupported); + return null; + } + /// /// 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 diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs index cfc13924df9..b259d6aa501 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs @@ -833,6 +833,20 @@ static bool IsConvertedToNullable(Expression outer, Expression inner) return source; } + /// + /// 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. + /// + protected override ShapedQueryExpression? TranslateRightJoin( + ShapedQueryExpression outer, + ShapedQueryExpression inner, + LambdaExpression outerKeySelector, + LambdaExpression innerKeySelector, + LambdaExpression resultSelector) + => null; + /// /// 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 diff --git a/src/EFCore.Relational/Query/Internal/RelationalValueConverterCompensatingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/RelationalValueConverterCompensatingExpressionVisitor.cs index 6fa06292acc..67425048db5 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalValueConverterCompensatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalValueConverterCompensatingExpressionVisitor.cs @@ -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) }; @@ -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))] diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs index 0c9f38b96a8..b0a7ad8fb7c 100644 --- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs @@ -1250,6 +1250,20 @@ protected override Expression VisitLeftJoin(LeftJoinExpression leftJoinExpressio return leftJoinExpression; } + /// + /// Generates SQL for a right join. + /// + /// The for which to generate SQL. + protected override Expression VisitRightJoin(RightJoinExpression rightJoinExpression) + { + _relationalCommandBuilder.Append("RIGHT JOIN "); + Visit(rightJoinExpression.Table); + _relationalCommandBuilder.Append(" ON "); + Visit(rightJoinExpression.JoinPredicate); + + return rightJoinExpression; + } + /// /// Generates SQL for a scalar subquery. /// diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 91b7b4c3cd1..3c7ca4a7f14 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -876,6 +876,27 @@ protected override ShapedQueryExpression TranslateIntersect(ShapedQueryExpressio return null; } + /// + 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, diff --git a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs index bb4c44c9307..82e9ccdf1ec 100644 --- a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs @@ -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), @@ -193,6 +194,13 @@ ShapedQueryExpression shapedQueryExpression /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. protected abstract Expression VisitProjection(ProjectionExpression projectionExpression); + /// + /// Visits the children of the right join expression. + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. + protected abstract Expression VisitRightJoin(RightJoinExpression rightJoinExpression); + /// /// Visits the children of the table valued function expression. /// diff --git a/src/EFCore.Relational/Query/SqlExpressions/InnerJoinExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/InnerJoinExpression.cs index b1050066e0f..2dcfbb41523 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/InnerJoinExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/InnerJoinExpression.cs @@ -50,10 +50,12 @@ public InnerJoinExpression( /// The property of the result. /// The property of the result. /// This expression if no children changed, or an expression with the updated children. - 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); /// /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will diff --git a/src/EFCore.Relational/Query/SqlExpressions/PredicateJoinExpressionBase.cs b/src/EFCore.Relational/Query/SqlExpressions/PredicateJoinExpressionBase.cs index baac3db4435..598528cdda5 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/PredicateJoinExpressionBase.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/PredicateJoinExpressionBase.cs @@ -50,7 +50,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// The property of the result. /// The property of the result. /// This expression if no children changed, or an expression with the updated children. - public abstract PredicateJoinExpressionBase Update(TableExpressionBase table, SqlExpression joinPredicate); + public abstract JoinExpressionBase Update(TableExpressionBase table, SqlExpression joinPredicate); /// public override bool Equals(object? obj) diff --git a/src/EFCore.Relational/Query/SqlExpressions/RightJoinExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/RightJoinExpression.cs new file mode 100644 index 00000000000..6a024043c20 --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/RightJoinExpression.cs @@ -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; + +/// +/// +/// An expression that represents a RIGHT JOIN in a SQL tree. +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +public class RightJoinExpression : PredicateJoinExpressionBase +{ + private static ConstructorInfo? _quotingConstructor; + + /// + /// Creates a new instance of the class. + /// + /// A table source to LEFT JOIN with. + /// A predicate to use for the join. + /// Whether this join expression may be pruned if nothing references a column on it. + public RightJoinExpression(TableExpressionBase table, SqlExpression joinPredicate, bool prunable = false) + : this(table, joinPredicate, prunable, annotations: null) + { + } + + /// + /// 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. + /// + [EntityFrameworkInternal] // For precompiled queries + public RightJoinExpression( + TableExpressionBase table, + SqlExpression joinPredicate, + bool prunable, + IReadOnlyDictionary? annotations = null) + : base(table, joinPredicate, prunable, annotations) + { + } + + /// + /// 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. + /// + /// The property of the result. + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. + public override RightJoinExpression Update(TableExpressionBase table, SqlExpression joinPredicate) + => table != Table || joinPredicate != JoinPredicate + ? new RightJoinExpression(table, joinPredicate, IsPrunable, Annotations) + : this; + + /// + /// 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. + /// + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. + public override RightJoinExpression Update(TableExpressionBase table) + => table != Table + ? new RightJoinExpression(table, JoinPredicate, IsPrunable, Annotations) + : this; + + /// + protected override RightJoinExpression WithAnnotations(IReadOnlyDictionary annotations) + => new(Table, JoinPredicate, IsPrunable, annotations); + + /// + public override Expression Quote() + => New( + _quotingConstructor ??= typeof(RightJoinExpression).GetConstructor( + [typeof(TableExpressionBase), typeof(SqlExpression), typeof(bool), typeof(IReadOnlyDictionary)])!, + Table.Quote(), + JoinPredicate.Quote(), + Constant(IsPrunable), + RelationalExpressionQuotingUtilities.QuoteAnnotations(Annotations)); + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append("RIGHT JOIN "); + expressionPrinter.Visit(Table); + expressionPrinter.Append(" ON "); + expressionPrinter.Visit(JoinPredicate); + PrintAnnotations(expressionPrinter); + } + + /// + 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); + + /// + public override int GetHashCode() + => base.GetHashCode(); +} diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index b510c493bd2..94021bf7a7f 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -2773,6 +2773,7 @@ private enum JoinType { InnerJoin, LeftJoin, + RightJoin, CrossJoin, CrossApply, OuterApply @@ -2792,10 +2793,19 @@ private Expression AddJoin( var innerMemberInfo = transparentIdentifierType.GetTypeInfo().GetDeclaredField("Inner")!; var outerClientEval = _clientProjections.Count > 0; var innerClientEval = innerSelect._clientProjections.Count > 0; + var outerNullable = joinType is JoinType.RightJoin; var innerNullable = joinType is JoinType.LeftJoin or JoinType.OuterApply; if (outerClientEval) { + if (outerNullable) + { + for (var i = 0; i < _clientProjections.Count; i++) + { + _clientProjections[i] = MakeNullable(_clientProjections[i], true); + } + } + // Outer projection are already populated if (innerClientEval) { @@ -2828,7 +2838,7 @@ private Expression AddJoin( if (innerClientEval) { // Since inner projections are populated, we need to populate outer also - var mapping = ConvertProjectionMappingToClientProjections(_projectionMapping); + var mapping = ConvertProjectionMappingToClientProjections(_projectionMapping, outerNullable); outerShaper = new ProjectionMemberToIndexConvertingExpressionVisitor(this, mapping).Visit(outerShaper); var indexMap = new int[innerSelect._clientProjections.Count]; @@ -2855,7 +2865,7 @@ private Expression AddJoin( { var remappedProjectionMember = projectionMember.Prepend(outerMemberInfo); mapping[projectionMember] = remappedProjectionMember; - projectionMapping[remappedProjectionMember] = expression; + projectionMapping[remappedProjectionMember] = MakeNullable(expression, outerNullable); } outerShaper = new ProjectionMemberRemappingExpressionVisitor(this, mapping).Visit(outerShaper); @@ -2865,9 +2875,7 @@ private Expression AddJoin( { var remappedProjectionMember = projectionMember.Prepend(innerMemberInfo); mapping[projectionMember] = remappedProjectionMember; - var projectionToAdd = expression; - projectionToAdd = MakeNullable(projectionToAdd, innerNullable); - projectionMapping[remappedProjectionMember] = projectionToAdd; + projectionMapping[remappedProjectionMember] = MakeNullable(expression, innerNullable); } innerShaper = new ProjectionMemberRemappingExpressionVisitor(this, mapping).Visit(innerShaper); @@ -2876,6 +2884,11 @@ private Expression AddJoin( } } + if (outerNullable) + { + outerShaper = new EntityShaperNullableMarkingExpressionVisitor().Visit(outerShaper); + } + if (innerNullable) { innerShaper = new EntityShaperNullableMarkingExpressionVisitor().Visit(innerShaper); @@ -2883,7 +2896,7 @@ private Expression AddJoin( return New( transparentIdentifierType.GetTypeInfo().DeclaredConstructors.Single(), - new[] { outerShaper, innerShaper }, outerMemberInfo, innerMemberInfo); + [outerShaper, innerShaper], outerMemberInfo, innerMemberInfo); } private void AddJoin( @@ -3025,16 +3038,24 @@ private void AddJoin( innerPushdownOccurred = true; } - if (_identifier.Count > 0 - && innerSelect._identifier.Count > 0) + if (_identifier.Count > 0 && innerSelect._identifier.Count > 0) { - if (joinType is JoinType.LeftJoin or JoinType.OuterApply) - { - _identifier.AddRange(innerSelect._identifier.Select(e => (e.Column.MakeNullable(), e.Comparer))); - } - else + switch (joinType) { - _identifier.AddRange(innerSelect._identifier); + case JoinType.LeftJoin or JoinType.OuterApply: + _identifier.AddRange(innerSelect._identifier.Select(e => (e.Column.MakeNullable(), e.Comparer))); + break; + + case JoinType.RightJoin: + var nullableOuterIdentifier = _identifier.Select(e => (e.Column.MakeNullable(), e.Comparer)).ToList(); + _identifier.Clear(); + _identifier.AddRange(nullableOuterIdentifier); + _identifier.AddRange(innerSelect._identifier); + break; + + default: + _identifier.AddRange(innerSelect._identifier); + break; } } else @@ -3050,6 +3071,7 @@ private void AddJoin( { JoinType.InnerJoin => new InnerJoinExpression(innerTable, joinPredicate!), JoinType.LeftJoin => new LeftJoinExpression(innerTable, joinPredicate!), + JoinType.RightJoin => new RightJoinExpression(innerTable, joinPredicate!), JoinType.CrossJoin => new CrossJoinExpression(innerTable), JoinType.CrossApply => new CrossApplyExpression(innerTable), JoinType.OuterApply => (TableExpressionBase)new OuterApplyExpression(innerTable), @@ -3434,6 +3456,20 @@ public Expression AddLeftJoin( => AddJoin( JoinType.LeftJoin, (SelectExpression)innerSource.QueryExpression, outerShaper, innerSource.ShaperExpression, joinPredicate); + /// + /// Adds the query expression of the given to table sources using RIGHT JOIN and combine shapers. + /// + /// A to join with. + /// A predicate to use for the join. + /// An expression for outer shaper. + /// An expression which shapes the result of this join. + public Expression AddRightJoin( + ShapedQueryExpression innerSource, + SqlExpression joinPredicate, + Expression outerShaper) + => AddJoin( + JoinType.RightJoin, (SelectExpression)innerSource.QueryExpression, outerShaper, innerSource.ShaperExpression, joinPredicate); + /// /// Adds the query expression of the given to table sources using CROSS JOIN and combine shapers. /// diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index d4924f562f8..3e7d3d72158 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -105,22 +105,12 @@ protected override Expression VisitExtension(Expression node) case SelectExpression select: return Visit(select); - case InnerJoinExpression innerJoinExpression: + case PredicateJoinExpressionBase join: { - var newTable = VisitAndConvert(innerJoinExpression.Table, nameof(VisitExtension)); - var newJoinPredicate = ProcessJoinPredicate(innerJoinExpression.JoinPredicate); + var newTable = VisitAndConvert(join.Table, nameof(VisitExtension)); + var newJoinPredicate = ProcessJoinPredicate(join.JoinPredicate); - return IsTrue(newJoinPredicate) - ? new CrossJoinExpression(newTable) - : innerJoinExpression.Update(newTable, newJoinPredicate); - } - - case LeftJoinExpression leftJoinExpression: - { - var newTable = VisitAndConvert(leftJoinExpression.Table, nameof(VisitExtension)); - var newJoinPredicate = ProcessJoinPredicate(leftJoinExpression.JoinPredicate); - - return leftJoinExpression.Update(newTable, newJoinPredicate); + return join.Update(newTable, newJoinPredicate); } case ValuesExpression { ValuesParameter: SqlParameterExpression valuesParameter } valuesExpression: diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index 9601ea9abf9..0c4d3874bfe 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -466,7 +466,8 @@ when QueryableMethods.IsSumWithSelector(method): innerSource, methodCallExpression.Arguments[2].UnwrapLambdaFromQuote(), methodCallExpression.Arguments[3].UnwrapLambdaFromQuote(), - methodCallExpression.Arguments[4].UnwrapLambdaFromQuote()); + methodCallExpression.Arguments[4].UnwrapLambdaFromQuote(), + QueryableMethods.Join); } goto default; @@ -479,12 +480,32 @@ when QueryableMethods.IsSumWithSelector(method): secondArgument = UnwrapCollectionMaterialization(secondArgument); if (secondArgument is NavigationExpansionExpression innerSource) { - return ProcessLeftJoin( + return ProcessJoin( + source, + innerSource, + methodCallExpression.Arguments[2].UnwrapLambdaFromQuote(), + methodCallExpression.Arguments[3].UnwrapLambdaFromQuote(), + methodCallExpression.Arguments[4].UnwrapLambdaFromQuote(), + QueryableMethods.LeftJoin); + } + + goto default; + } + + case nameof(Queryable.RightJoin) + when genericMethod == QueryableMethods.RightJoin: + { + var secondArgument = Visit(methodCallExpression.Arguments[1]); + secondArgument = UnwrapCollectionMaterialization(secondArgument); + if (secondArgument is NavigationExpansionExpression innerSource) + { + return ProcessJoin( source, innerSource, methodCallExpression.Arguments[2].UnwrapLambdaFromQuote(), methodCallExpression.Arguments[3].UnwrapLambdaFromQuote(), - methodCallExpression.Arguments[4].UnwrapLambdaFromQuote()); + methodCallExpression.Arguments[4].UnwrapLambdaFromQuote(), + QueryableMethods.RightJoin); } goto default; @@ -1236,8 +1257,13 @@ private NavigationExpansionExpression ProcessJoin( NavigationExpansionExpression innerSource, LambdaExpression outerKeySelector, LambdaExpression innerKeySelector, - LambdaExpression resultSelector) + LambdaExpression resultSelector, + MethodInfo joinMethod) { + Check.DebugAssert( + joinMethod == QueryableMethods.Join || joinMethod == QueryableMethods.LeftJoin || joinMethod == QueryableMethods.RightJoin, + "Join method required"); + if (innerSource.PendingOrderings.Any()) { ApplyPendingOrderings(innerSource); @@ -1254,14 +1280,14 @@ private NavigationExpansionExpression ProcessJoin( var newResultSelector = Expression.Lambda( Expression.New( transparentIdentifierType.GetConstructors().Single(), - new[] { outerSource.CurrentParameter, innerSource.CurrentParameter }, + [outerSource.CurrentParameter, innerSource.CurrentParameter], transparentIdentifierOuterMemberInfo, transparentIdentifierInnerMemberInfo), outerSource.CurrentParameter, innerSource.CurrentParameter); var source = Expression.Call( - QueryableMethods.Join.MakeGenericMethod( + joinMethod.MakeGenericMethod( outerSource.SourceElementType, innerSource.SourceElementType, outerKeySelector.ReturnType, newResultSelector.ReturnType), outerSource.Source, @@ -1270,62 +1296,22 @@ private NavigationExpansionExpression ProcessJoin( Expression.Quote(innerKeySelector), Expression.Quote(newResultSelector)); - var currentTree = new NavigationTreeNode(outerSource.CurrentTree, innerSource.CurrentTree); - var pendingSelector = new ReplacingExpressionVisitor( - new Expression[] { resultSelector.Parameters[0], resultSelector.Parameters[1] }, - new[] { outerSource.PendingSelector, innerSource.PendingSelector }) - .Visit(resultSelector.Body); - var parameterName = GetParameterName("ti"); - - return new NavigationExpansionExpression(source, currentTree, pendingSelector, parameterName); - } - - private NavigationExpansionExpression ProcessLeftJoin( - NavigationExpansionExpression outerSource, - NavigationExpansionExpression innerSource, - LambdaExpression outerKeySelector, - LambdaExpression innerKeySelector, - LambdaExpression resultSelector) - { - if (innerSource.PendingOrderings.Any()) + var outerPendingSelector = outerSource.PendingSelector; + if (joinMethod == QueryableMethods.RightJoin) { - ApplyPendingOrderings(innerSource); + outerPendingSelector = _entityReferenceOptionalMarkingExpressionVisitor.Visit(outerPendingSelector); } - (outerKeySelector, innerKeySelector) = ProcessJoinConditions(outerSource, innerSource, outerKeySelector, innerKeySelector); - - var transparentIdentifierType = TransparentIdentifierFactory.Create( - outerSource.SourceElementType, innerSource.SourceElementType); - - var transparentIdentifierOuterMemberInfo = transparentIdentifierType.GetTypeInfo().GetDeclaredField("Outer")!; - var transparentIdentifierInnerMemberInfo = transparentIdentifierType.GetTypeInfo().GetDeclaredField("Inner")!; - - var newResultSelector = Expression.Lambda( - Expression.New( - transparentIdentifierType.GetConstructors().Single(), - new[] { outerSource.CurrentParameter, innerSource.CurrentParameter }, - transparentIdentifierOuterMemberInfo, - transparentIdentifierInnerMemberInfo), - outerSource.CurrentParameter, - innerSource.CurrentParameter); - - var source = Expression.Call( - QueryableMethods.LeftJoin.MakeGenericMethod( - outerSource.SourceElementType, innerSource.SourceElementType, outerKeySelector.ReturnType, - newResultSelector.ReturnType), - outerSource.Source, - innerSource.Source, - Expression.Quote(outerKeySelector), - Expression.Quote(innerKeySelector), - Expression.Quote(newResultSelector)); - var innerPendingSelector = innerSource.PendingSelector; - innerPendingSelector = _entityReferenceOptionalMarkingExpressionVisitor.Visit(innerPendingSelector); + if (joinMethod == QueryableMethods.LeftJoin) + { + innerPendingSelector = _entityReferenceOptionalMarkingExpressionVisitor.Visit(innerPendingSelector); + } var currentTree = new NavigationTreeNode(outerSource.CurrentTree, innerSource.CurrentTree); var pendingSelector = new ReplacingExpressionVisitor( - new Expression[] { resultSelector.Parameters[0], resultSelector.Parameters[1] }, - new[] { outerSource.PendingSelector, innerPendingSelector }) + [resultSelector.Parameters[0], resultSelector.Parameters[1]], + [outerPendingSelector, innerPendingSelector]) .Visit(resultSelector.Body); var parameterName = GetParameterName("ti"); diff --git a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs index 4202bd6ad5f..5d64a66ed8f 100644 --- a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs @@ -407,6 +407,20 @@ when QueryableMethods.IsAverageWithSelector(method): break; } + case nameof(Queryable.RightJoin) + when genericMethod == QueryableMethods.RightJoin: + { + if (Visit(methodCallExpression.Arguments[1]) is ShapedQueryExpression innerShapedQueryExpression) + { + return CheckTranslated( + TranslateRightJoin( + shapedQueryExpression, innerShapedQueryExpression, GetLambdaExpressionFromArgument(2), + GetLambdaExpressionFromArgument(3), GetLambdaExpressionFromArgument(4))); + } + + break; + } + case nameof(Queryable.Last) when genericMethod == QueryableMethods.LastWithoutPredicate: shapedQueryExpression = shapedQueryExpression.UpdateResultCardinality(ResultCardinality.Single); @@ -841,6 +855,26 @@ protected virtual Expression MarkShaperNullable(Expression shaperExpression) LambdaExpression innerKeySelector, LambdaExpression resultSelector); + /// + /// Translates LeftJoin over the given source. + /// + /// + /// Certain patterns of GroupJoin-DefaultIfEmpty-SelectMany represents a left join in database. We identify such pattern + /// in advance and convert it to join like syntax. + /// + /// The shaped query on which the operator is applied. + /// The inner shaped query to perform join with. + /// The key selector for the outer source. + /// The key selector for the inner source. + /// The result selector supplied in the call. + /// The shaped query after translation. + protected abstract ShapedQueryExpression? TranslateRightJoin( + ShapedQueryExpression outer, + ShapedQueryExpression inner, + LambdaExpression outerKeySelector, + LambdaExpression innerKeySelector, + LambdaExpression resultSelector); + /// /// Translates method or /// and their other overloads over the given source. diff --git a/src/EFCore/Query/QueryableMethods.cs b/src/EFCore/Query/QueryableMethods.cs index b08c7d6c48e..9a2564e788a 100644 --- a/src/EFCore/Query/QueryableMethods.cs +++ b/src/EFCore/Query/QueryableMethods.cs @@ -275,6 +275,13 @@ public static class QueryableMethods /// public static MethodInfo Reverse { get; } + /// + /// The for + /// + /// + public static MethodInfo RightJoin { get; } + /// /// The for /// @@ -716,6 +723,17 @@ static QueryableMethods() Reverse = GetMethod(nameof(Queryable.Reverse), 1, types => [typeof(IQueryable<>).MakeGenericType(types[0])]); + RightJoin = GetMethod( + nameof(Queryable.RightJoin), 4, + types => + [ + typeof(IQueryable<>).MakeGenericType(types[0]), + typeof(IEnumerable<>).MakeGenericType(types[1]), + typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(types[0], types[2])), + typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(types[1], types[2])), + typeof(Expression<>).MakeGenericType(typeof(Func<,,>).MakeGenericType(types[0], types[1], types[3])) + ]); + Select = GetMethod( nameof(Queryable.Select), 2, types => diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs index fecdd244846..2fa6019459d 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs @@ -2275,11 +2275,16 @@ public override Task Json_with_projection_of_multiple_json_references_and_entity () => base.Json_with_projection_of_multiple_json_references_and_entity_collection(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries); - public override Task Left_join_json_entities(bool async) + public override Task LeftJoin_json_entities(bool async) => AssertTranslationFailedWithDetails( - () => base.Left_join_json_entities(async), + () => base.LeftJoin_json_entities(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(JsonEntityBasic), nameof(JsonEntitySingleOwned))); + public override Task RightJoin_json_entities(bool async) + => AssertTranslationFailedWithDetails( + () => base.RightJoin_json_entities(async), + CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(JsonEntitySingleOwned), nameof(JsonEntityBasic))); + public override Task Left_join_json_entities_complex_projection(bool async) => AssertTranslationFailedWithDetails( () => base.Left_join_json_entities_complex_projection(async), diff --git a/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs index fdd78cef5a4..6ec5a0a431d 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs @@ -143,4 +143,8 @@ public override Task Join_include_coalesce_nested(bool async) public override Task Join_include_conditional(bool async) => Task.CompletedTask; + + // Right join not supported in InMemory + public override Task Correlated_collections_on_RightJoin_with_predicate(bool async) + => AssertTranslationFailed(() => base.Correlated_collections_on_RightJoin_with_predicate(async)); } diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindEFPropertyIncludeQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindEFPropertyIncludeQueryInMemoryTest.cs index ee15f05bbd0..e9e3b395f0d 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindEFPropertyIncludeQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindEFPropertyIncludeQueryInMemoryTest.cs @@ -4,4 +4,9 @@ namespace Microsoft.EntityFrameworkCore.Query; public class NorthwindEFPropertyIncludeQueryInMemoryTest(NorthwindQueryInMemoryFixture fixture) - : NorthwindEFPropertyIncludeQueryTestBase>(fixture); + : NorthwindEFPropertyIncludeQueryTestBase>(fixture) +{ + // Right join not supported in InMemory + public override Task Include_collection_with_right_join_clause_with_filter(bool async) + => AssertTranslationFailed(() => base.Include_collection_with_right_join_clause_with_filter(async)); +} diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindIncludeNoTrackingQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindIncludeNoTrackingQueryInMemoryTest.cs index 81a7e76faba..6c3bb988a45 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindIncludeNoTrackingQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindIncludeNoTrackingQueryInMemoryTest.cs @@ -4,4 +4,9 @@ namespace Microsoft.EntityFrameworkCore.Query; public class NorthwindIncludeNoTrackingQueryInMemoryTest(NorthwindQueryInMemoryFixture fixture) - : NorthwindIncludeNoTrackingQueryTestBase>(fixture); + : NorthwindIncludeNoTrackingQueryTestBase>(fixture) +{ + // Right join not supported in InMemory + public override Task Include_collection_with_right_join_clause_with_filter(bool async) + => AssertTranslationFailed(() => base.Include_collection_with_right_join_clause_with_filter(async)); +} diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindIncludeQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindIncludeQueryInMemoryTest.cs index abcb0c87b9c..c2543be0199 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindIncludeQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindIncludeQueryInMemoryTest.cs @@ -4,4 +4,9 @@ namespace Microsoft.EntityFrameworkCore.Query; public class NorthwindIncludeQueryInMemoryTest(NorthwindQueryInMemoryFixture fixture) - : NorthwindIncludeQueryTestBase>(fixture); + : NorthwindIncludeQueryTestBase>(fixture) +{ + // Right join not supported in InMemory + public override Task Include_collection_with_right_join_clause_with_filter(bool async) + => AssertTranslationFailed(() => base.Include_collection_with_right_join_clause_with_filter(async)); +} diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs index 0437a83eb56..ad2d0c47f8e 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs @@ -24,6 +24,10 @@ public override Task SelectMany_with_client_eval_with_constructor(bool async) // Joins between sources with client eval. Issue #21200. => Assert.ThrowsAsync(() => base.SelectMany_with_client_eval_with_constructor(async)); + // Right join not supported in InMemory + public override Task RightJoin(bool async) + => AssertTranslationFailed(() => base.RightJoin(async)); + public override async Task Join_local_collection_int_closure_is_cached_correctly(bool async) { var ids = new uint[] { 1, 2 }; diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindStringIncludeQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindStringIncludeQueryInMemoryTest.cs index 5c33b1166e3..724e082f9e9 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindStringIncludeQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindStringIncludeQueryInMemoryTest.cs @@ -4,4 +4,9 @@ namespace Microsoft.EntityFrameworkCore.Query; public class NorthwindStringIncludeQueryInMemoryTest(NorthwindQueryInMemoryFixture fixture) - : NorthwindStringIncludeQueryTestBase>(fixture); + : NorthwindStringIncludeQueryTestBase>(fixture) +{ + // Right join not supported in InMemory + public override Task Include_collection_with_right_join_clause_with_filter(bool async) + => AssertTranslationFailed(() => base.Include_collection_with_right_join_clause_with_filter(async)); +} diff --git a/test/EFCore.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs b/test/EFCore.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs index 33c2c1f9b28..be92bfd6294 100644 --- a/test/EFCore.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs +++ b/test/EFCore.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs @@ -381,6 +381,19 @@ from o in ss.Set().Where(o => o.OrderID < od.OrderID).OrderBy(e => e.Orde select od, rowsAffectedCount: 74); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Delete_with_RightJoin(bool async) + => AssertDelete( + async, + ss => ss.Set().Where(e => e.OrderID < 10276) + .RightJoin( + ss.Set().Where(o => o.OrderID < 10300).OrderBy(e => e.OrderID).Skip(0).Take(100), + od => od.OrderID, + o => o.OrderID, + (od, o) => od), + rowsAffectedCount: 74); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Update_Where_set_constant_TagWith(bool async) @@ -863,6 +876,22 @@ from o in grouping.DefaultIfEmpty() rowsAffectedCount: 8, (b, a) => Assert.All(a, c => Assert.Equal("Updated", c.ContactName))); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_with_RightJoin(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(o => o.OrderID < 10300) + .RightJoin( + ss.Set().Where(c => c.CustomerID.StartsWith("F")), + o => o.CustomerID, + c => c.CustomerID, + (o, c) => new { Order = o, Customers = c }), + e => e.Order, + s => s.SetProperty(t => t.Order.OrderDate, new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc)), + rowsAffectedCount: 2, + (b, a) => Assert.All(a, o => Assert.Equal(new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc), o.OrderDate))); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Update_with_cross_join_set_constant(bool async) diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index d853af6543b..953b5cae392 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -3420,6 +3420,34 @@ from g in grouping.DefaultIfEmpty() AssertCollection(e.WeaponNames, a.WeaponNames); }); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collections_on_RightJoin_with_predicate(bool async) + => AssertQuery( + async, + ss => ss.Set() + .RightJoin( + ss.Set(), + g => g.Nickname, + t => t.GearNickName, + (g, c) => new { g, c }) + .Where(t => !t.g.HasSoulPatch) + .Select(t => new { t.g.Nickname, WeaponNames = t.g.Weapons.Select(w => w.Name).ToList() }), + ss => ss.Set() + .RightJoin( + ss.Set(), + g => g.Nickname, + t => t.GearNickName, + (g, c) => new { g, c }) + .Where(t => t.g != null && !t.g.HasSoulPatch) + .Select(t => new { t.g.Nickname, WeaponNames = t.g.Weapons.Select(w => w.Name).ToList() }), + elementSorter: e => e.Nickname, + elementAsserter: (e, a) => + { + Assert.Equal(e.Nickname, a.Nickname); + AssertCollection(e.WeaponNames, a.WeaponNames); + }); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Correlated_collections_on_left_join_with_null_value(bool async) diff --git a/test/EFCore.Specification.Tests/Query/JsonQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/JsonQueryTestBase.cs index b7cfe2ae3ce..6576ccf93f2 100644 --- a/test/EFCore.Specification.Tests/Query/JsonQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/JsonQueryTestBase.cs @@ -576,13 +576,11 @@ public virtual Task Project_entity_with_single_owned(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Left_join_json_entities(bool async) + public virtual Task LeftJoin_json_entities(bool async) => AssertQuery( async, - ss => from e1 in ss.Set() - join e2 in ss.Set() on e1.Id equals e2.Id into g - from e2 in g.DefaultIfEmpty() - select new { e1, e2 }, + ss => ss.Set() + .LeftJoin(ss.Set(), e1 => e1.Id, e2 => e2.Id, (e1, e2) => new { e1, e2 }), elementSorter: e => (e.e1.Id, e.e2?.Id), elementAsserter: (e, a) => { @@ -590,6 +588,24 @@ from e2 in g.DefaultIfEmpty() AssertEqual(e.e2, a.e2); }); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task RightJoin_json_entities(bool async) + => AssertQuery( + async, + ss => ss.Set() + .RightJoin( + ss.Set(), + e1 => e1.Id, + e2 => e2.Id, + (e1, e2) => new { e1, e2 }), + elementSorter: e => (e.e1?.Id, e.e2.Id), + elementAsserter: (e, a) => + { + AssertEqual(e.e1, a.e1); + AssertEqual(e.e2, a.e2); + }); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Left_join_json_entities_complex_projection(bool async) diff --git a/test/EFCore.Specification.Tests/Query/NorthwindIncludeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindIncludeQueryTestBase.cs index 8271ce100ce..036604132ef 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindIncludeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindIncludeQueryTestBase.cs @@ -299,6 +299,18 @@ where c.CustomerID.StartsWith("F") select c, elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(c => c.Orders))); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Include_collection_with_right_join_clause_with_filter(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Include(o => o.Orders) + .RightJoin(ss.Set(), c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }) + .Where(t => t.c.CustomerID.StartsWith("F")) + .Select(t => t.c), + elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(c => c.Orders))); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Include_collection_with_cross_join_clause_with_filter(bool async) diff --git a/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs index 9d7b095118d..af0b2ff3283 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs @@ -305,6 +305,19 @@ public virtual Task LeftJoin(bool async) (c, o) => new { c, o }), e => (e.c.CustomerID, e.o?.OrderID)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task RightJoin(bool async) + => AssertQuery( + async, + ss => ss.Set() + .RightJoin( + ss.Set(), + c => c.CustomerID, + o => o.CustomerID, + (c, o) => new { c, o }), + e => (e.c.CustomerID, e.o?.OrderID)); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupJoin_customers_employees_shadow(bool async) diff --git a/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs index 6c62e859c7a..8bafc857258 100644 --- a/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs @@ -857,14 +857,7 @@ public virtual Task Left_join_on_entity_with_owned_navigations(bool async) ss => from c1 in ss.Set() join c2 in ss.Set() on c1.Id equals c2.Id into grouping from c2 in grouping.DefaultIfEmpty() - select new - { - c1, - c2.Id, - c2, - c2.Orders, - c2.PersonAddress - }, + select new { c1, c2.Id, c2, c2.Orders, c2.PersonAddress }, elementSorter: e => (e.c1.Id, e.c2.Id), elementAsserter: (e, a) => { diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs index 38d6bd9622f..075ff9f74fe 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs @@ -627,6 +627,28 @@ WHERE [o].[OrderID] < 10276 """); } + public override async Task Delete_with_RightJoin(bool async) + { + await base.Delete_with_RightJoin(async); + + AssertSql( + """ +@p='0' +@p0='100' + +DELETE FROM [o] +FROM [Order Details] AS [o] +RIGHT JOIN ( + SELECT [o0].[OrderID] + FROM [Orders] AS [o0] + WHERE [o0].[OrderID] < 10300 + ORDER BY [o0].[OrderID] + OFFSET @p ROWS FETCH NEXT @p0 ROWS ONLY +) AS [o1] ON [o].[OrderID] = [o1].[OrderID] +WHERE [o].[OrderID] < 10276 +"""); + } + public override async Task Update_Where_set_constant_TagWith(bool async) { await base.Update_Where_set_constant_TagWith(async); @@ -1347,6 +1369,26 @@ WHERE [c].[CustomerID] LIKE N'F%' """); } + public override async Task Update_with_RightJoin(bool async) + { + await base.Update_with_RightJoin(async); + + AssertExecuteUpdateSql( + """ +@p='2020-01-01T00:00:00.0000000Z' (Nullable = true) (DbType = DateTime) + +UPDATE [o] +SET [o].[OrderDate] = @p +FROM [Orders] AS [o] +RIGHT JOIN ( + SELECT [c].[CustomerID] + FROM [Customers] AS [c] + WHERE [c].[CustomerID] LIKE N'F%' +) AS [c0] ON [o].[CustomerID] = [c0].[CustomerID] +WHERE [o].[OrderID] < 10300 +"""); + } + public override async Task Update_with_cross_join_set_constant(bool async) { await base.Update_with_cross_join_set_constant(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index e288e2482e2..ea50c4ef7c8 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -4212,6 +4212,21 @@ FROM [Tags] AS [t] """); } + public override async Task Correlated_collections_on_RightJoin_with_predicate(bool async) + { + await base.Correlated_collections_on_RightJoin_with_predicate(async); + + AssertSql( + """ +SELECT [g].[Nickname], [g].[SquadId], [t].[Id], [w].[Name], [w].[Id] +FROM [Gears] AS [g] +RIGHT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] +LEFT JOIN [Weapons] AS [w] ON [g].[FullName] = [w].[OwnerFullName] +WHERE [g].[HasSoulPatch] = CAST(0 AS bit) +ORDER BY [g].[Nickname], [g].[SquadId], [t].[Id] +"""); + } + public override async Task Correlated_collections_on_left_join_with_null_value(bool async) { await base.Correlated_collections_on_left_join_with_null_value(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerTest.cs index 2b6bf874232..e401aebbbf4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerTest.cs @@ -595,20 +595,20 @@ FROM [JsonEntitiesSingleOwned] AS [j] """); } - public override async Task Left_join_json_entities(bool async) + public override async Task LeftJoin_json_entities(bool async) { // TODO:SQLJSON Returns empty (invalid) JSON (See BadJson.cs) if (async) { Assert.Equal( "Unable to cast object of type 'System.DBNull' to type 'System.String'.", - (await Assert.ThrowsAsync(() => base.Left_join_json_entities(true))).Message); + (await Assert.ThrowsAsync(() => base.LeftJoin_json_entities(true))).Message); } else { Assert.Equal( RelationalStrings.JsonEmptyString, - (await Assert.ThrowsAsync(() => base.Left_join_json_entities(false))) + (await Assert.ThrowsAsync(() => base.LeftJoin_json_entities(false))) .Message); } @@ -620,6 +620,31 @@ FROM [JsonEntitiesSingleOwned] AS [j] """); } + public override async Task RightJoin_json_entities(bool async) + { + // TODO:SQLJSON Returns empty (invalid) JSON (See BadJson.cs) + if (async) + { + Assert.Equal( + "Unable to cast object of type 'System.DBNull' to type 'System.String'.", + (await Assert.ThrowsAsync(() => base.LeftJoin_json_entities(true))).Message); + } + else + { + Assert.Equal( + RelationalStrings.JsonEmptyString, + (await Assert.ThrowsAsync(() => base.LeftJoin_json_entities(false))) + .Message); + } + + AssertSql( + """ +SELECT [j].[Id], [j].[Name], [j].[OwnedCollection], [j0].[Id], [j0].[EntityBasicId], [j0].[Name], [j0].[OwnedCollectionRoot], [j0].[OwnedReferenceRoot] +FROM [JsonEntitiesSingleOwned] AS [j] +RIGHT JOIN [JsonEntitiesBasic] AS [j0] ON [j].[Id] = [j0].[Id] +"""); + } + public override async Task Left_join_json_entities_complex_projection(bool async) { // TODO:SQLJSON Returns empty (invalid) JSON (See BadJson.cs) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs index 37a7763fb0a..15220d2a31b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs @@ -581,9 +581,9 @@ FROM [JsonEntitiesSingleOwned] AS [j] """); } - public override async Task Left_join_json_entities(bool async) + public override async Task LeftJoin_json_entities(bool async) { - await base.Left_join_json_entities(async); + await base.LeftJoin_json_entities(async); AssertSql( """ @@ -593,6 +593,18 @@ FROM [JsonEntitiesSingleOwned] AS [j] """); } + public override async Task RightJoin_json_entities(bool async) + { + await base.RightJoin_json_entities(async); + + AssertSql( + """ +SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot], [j0].[Id], [j0].[Name], [j0].[OwnedCollection] +FROM [JsonEntitiesBasic] AS [j] +RIGHT JOIN [JsonEntitiesSingleOwned] AS [j0] ON [j].[Id] = [j0].[Id] +"""); + } + public override async Task Left_join_json_entities_complex_projection(bool async) { await base.Left_join_json_entities_complex_projection(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindEFPropertyIncludeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindEFPropertyIncludeQuerySqlServerTest.cs index 2a11a3f1caa..ff2faffb210 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindEFPropertyIncludeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindEFPropertyIncludeQuerySqlServerTest.cs @@ -491,6 +491,21 @@ WHERE [c].[CustomerID] LIKE N'F%' """); } + public override async Task Include_collection_with_right_join_clause_with_filter(bool async) + { + await base.Include_collection_with_right_join_clause_with_filter(async); + + AssertSql( + """ +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderID], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM [Customers] AS [c] +RIGHT JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +LEFT JOIN [Orders] AS [o0] ON [c].[CustomerID] = [o0].[CustomerID] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID], [o].[OrderID] +"""); + } + public override async Task Include_duplicate_collection(bool async) { await base.Include_duplicate_collection(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeNoTrackingQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeNoTrackingQuerySqlServerTest.cs index adc7782ebed..d213b26db6c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeNoTrackingQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeNoTrackingQuerySqlServerTest.cs @@ -1863,6 +1863,21 @@ WHERE [c].[CustomerID] LIKE N'F%' """); } + public override async Task Include_collection_with_right_join_clause_with_filter(bool async) + { + await base.Include_collection_with_right_join_clause_with_filter(async); + + AssertSql( + """ +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderID], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM [Customers] AS [c] +RIGHT JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +LEFT JOIN [Orders] AS [o0] ON [c].[CustomerID] = [o0].[CustomerID] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID], [o].[OrderID] +"""); + } + public override async Task Include_multiple_references_then_include_multi_level_reverse(bool async) { await base.Include_multiple_references_then_include_multi_level_reverse(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeQuerySqlServerTest.cs index 2d8324d765e..938bfc83450 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeQuerySqlServerTest.cs @@ -574,6 +574,21 @@ WHERE [c].[CustomerID] LIKE N'F%' """); } + public override async Task Include_collection_with_right_join_clause_with_filter(bool async) + { + await base.Include_collection_with_right_join_clause_with_filter(async); + + AssertSql( + """ +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderID], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM [Customers] AS [c] +RIGHT JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +LEFT JOIN [Orders] AS [o0] ON [c].[CustomerID] = [o0].[CustomerID] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID], [o].[OrderID] +"""); + } + public override async Task Include_collection_with_cross_join_clause_with_filter(bool async) { await base.Include_collection_with_cross_join_clause_with_filter(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs index 121eac4a162..ac1cddb3d45 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs @@ -247,6 +247,18 @@ FROM [Customers] AS [c] """); } + public override async Task RightJoin(bool async) + { + await base.RightJoin(async); + + AssertSql( + """ +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM [Customers] AS [c] +RIGHT JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +"""); + } + public override async Task GroupJoin_simple(bool async) { await base.GroupJoin_simple(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQuerySqlServerTest.cs index b580728d89e..306ad8b0133 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQuerySqlServerTest.cs @@ -2234,6 +2234,29 @@ WHERE [c].[CustomerID] LIKE N'F%' """); } + public override async Task Include_collection_with_right_join_clause_with_filter(bool async) + { + await base.Include_collection_with_right_join_clause_with_filter(async); + + AssertSql( + """ +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderID] +FROM [Customers] AS [c] +RIGHT JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID], [o].[OrderID] +""", + // + """ +SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], [c].[CustomerID], [o].[OrderID] +FROM [Customers] AS [c] +RIGHT JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +INNER JOIN [Orders] AS [o0] ON [c].[CustomerID] = [o0].[CustomerID] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID], [o].[OrderID] +"""); + } + public override async Task Include_reference_distinct_is_server_evaluated(bool async) { await base.Include_reference_distinct_is_server_evaluated(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs index 1b349b7234b..4f4a591a867 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs @@ -788,6 +788,29 @@ WHERE [c].[CustomerID] LIKE N'F%' """); } + public override async Task Include_collection_with_right_join_clause_with_filter(bool async) + { + await base.Include_collection_with_right_join_clause_with_filter(async); + + AssertSql( + """ +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderID] +FROM [Customers] AS [c] +RIGHT JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID], [o].[OrderID] +""", + // + """ +SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], [c].[CustomerID], [o].[OrderID] +FROM [Customers] AS [c] +RIGHT JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +INNER JOIN [Orders] AS [o0] ON [c].[CustomerID] = [o0].[CustomerID] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID], [o].[OrderID] +"""); + } + public override async Task Include_collection_with_cross_join_clause_with_filter(bool async) { await base.Include_collection_with_cross_join_clause_with_filter(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindStringIncludeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindStringIncludeQuerySqlServerTest.cs index 8b57e322c2d..3ca97ba898e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindStringIncludeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindStringIncludeQuerySqlServerTest.cs @@ -491,6 +491,21 @@ WHERE [c].[CustomerID] LIKE N'F%' """); } + public override async Task Include_collection_with_right_join_clause_with_filter(bool async) + { + await base.Include_collection_with_right_join_clause_with_filter(async); + + AssertSql( + """ +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderID], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM [Customers] AS [c] +RIGHT JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +LEFT JOIN [Orders] AS [o0] ON [c].[CustomerID] = [o0].[CustomerID] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID], [o].[OrderID] +"""); + } + public override async Task Include_duplicate_collection(bool async) { await base.Include_duplicate_collection(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs index 8bc518e14ef..817322cb0ad 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs @@ -5670,6 +5670,27 @@ FROM [Officers] AS [o] """); } + public override async Task Correlated_collections_on_RightJoin_with_predicate(bool async) + { + await base.Correlated_collections_on_RightJoin_with_predicate(async); + + AssertSql( + """ +SELECT [u].[Nickname], [u].[SquadId], [t].[Id], [w].[Name], [w].[Id] +FROM ( + SELECT [g].[Nickname], [g].[SquadId], [g].[FullName], [g].[HasSoulPatch] + FROM [Gears] AS [g] + UNION ALL + SELECT [o].[Nickname], [o].[SquadId], [o].[FullName], [o].[HasSoulPatch] + FROM [Officers] AS [o] +) AS [u] +RIGHT JOIN [Tags] AS [t] ON [u].[Nickname] = [t].[GearNickName] +LEFT JOIN [Weapons] AS [w] ON [u].[FullName] = [w].[OwnerFullName] +WHERE [u].[HasSoulPatch] = CAST(0 AS bit) +ORDER BY [u].[Nickname], [u].[SquadId], [t].[Id] +"""); + } + public override async Task Correlated_collections_on_left_join_with_null_value(bool async) { await base.Correlated_collections_on_left_join_with_null_value(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index aae0b2dba33..8943f4f0cf4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -4858,6 +4858,21 @@ FROM [Gears] AS [g] """); } + public override async Task Correlated_collections_on_RightJoin_with_predicate(bool async) + { + await base.Correlated_collections_on_RightJoin_with_predicate(async); + + AssertSql( + """ +SELECT [g].[Nickname], [g].[SquadId], [t].[Id], [w].[Name], [w].[Id] +FROM [Gears] AS [g] +RIGHT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] +LEFT JOIN [Weapons] AS [w] ON [g].[FullName] = [w].[OwnerFullName] +WHERE [g].[HasSoulPatch] = CAST(0 AS bit) +ORDER BY [g].[Nickname], [g].[SquadId], [t].[Id] +"""); + } + public override async Task Correlated_collections_on_left_join_with_null_value(bool async) { await base.Correlated_collections_on_left_join_with_null_value(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index d941059310d..b53d72db487 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -7560,6 +7560,21 @@ public override async Task Correlated_collections_on_left_join_with_predicate(bo """); } + public override async Task Correlated_collections_on_RightJoin_with_predicate(bool async) + { + await base.Correlated_collections_on_RightJoin_with_predicate(async); + + AssertSql( + """ +SELECT [g].[Nickname], [g].[SquadId], [t].[Id], [w].[Name], [w].[Id] +FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] +RIGHT JOIN [Tags] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [t] ON [g].[Nickname] = [t].[GearNickName] +LEFT JOIN [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w] ON [g].[FullName] = [w].[OwnerFullName] +WHERE [g].[HasSoulPatch] = CAST(0 AS bit) +ORDER BY [g].[Nickname], [g].[SquadId], [t].[Id] +"""); + } + public override async Task Select_null_propagation_negative4(bool async) { await base.Select_null_propagation_negative4(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs index db77f3c21d4..a7118bf2a65 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs @@ -608,6 +608,30 @@ public override async Task Delete_with_outer_apply(bool async) SqliteStrings.ApplyNotSupported, (await Assert.ThrowsAsync(() => base.Delete_with_outer_apply(async))).Message); + public override async Task Delete_with_RightJoin(bool async) + { + await base.Delete_with_RightJoin(async); + + AssertSql( + """ +@p0='100' +@p='0' + +DELETE FROM "Order Details" AS "o" +WHERE EXISTS ( + SELECT 1 + FROM "Order Details" AS "o0" + RIGHT JOIN ( + SELECT "o2"."OrderID" + FROM "Orders" AS "o2" + WHERE "o2"."OrderID" < 10300 + ORDER BY "o2"."OrderID" + LIMIT @p0 OFFSET @p + ) AS "o1" ON "o0"."OrderID" = "o1"."OrderID" + WHERE "o0"."OrderID" < 10276 AND "o0"."OrderID" = "o"."OrderID" AND "o0"."ProductID" = "o"."ProductID") +"""); + } + public override async Task Update_Where_set_constant_TagWith(bool async) { await base.Update_Where_set_constant_TagWith(async); @@ -1327,6 +1351,30 @@ LEFT JOIN ( """); } + public override async Task Update_with_RightJoin(bool async) + { + await base.Update_with_RightJoin(async); + + AssertExecuteUpdateSql( + """ +@p='2020-01-01T00:00:00.0000000Z' (Nullable = true) (DbType = DateTime) + +UPDATE "Orders" AS "o0" +SET "OrderDate" = @p +FROM ( + SELECT "o"."OrderID" + FROM "Orders" AS "o" + RIGHT JOIN ( + SELECT "c"."CustomerID" + FROM "Customers" AS "c" + WHERE "c"."CustomerID" LIKE 'F%' + ) AS "c0" ON "o"."CustomerID" = "c0"."CustomerID" + WHERE "o"."OrderID" < 10300 +) AS "s" +WHERE "o0"."OrderID" = "s"."OrderID" +"""); + } + public override async Task Update_with_cross_join_set_constant(bool async) { await base.Update_with_cross_join_set_constant(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index f1f8a560a4f..76c7db3ad8c 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -1006,6 +1006,21 @@ WHERE NOT ("g"."HasSoulPatch") """); } + public override async Task Correlated_collections_on_RightJoin_with_predicate(bool async) + { + await base.Correlated_collections_on_RightJoin_with_predicate(async); + + AssertSql( + """ +SELECT "g"."Nickname", "g"."SquadId", "t"."Id", "w"."Name", "w"."Id" +FROM "Gears" AS "g" +RIGHT JOIN "Tags" AS "t" ON "g"."Nickname" = "t"."GearNickName" +LEFT JOIN "Weapons" AS "w" ON "g"."FullName" = "w"."OwnerFullName" +WHERE NOT ("g"."HasSoulPatch") +ORDER BY "g"."Nickname", "g"."SquadId", "t"."Id" +"""); + } + public override async Task Property_access_on_derived_entity_using_cast(bool async) { await base.Property_access_on_derived_entity_using_cast(async);