From c5ca9ff15c418d3b917e581f6029f5aa09ce2f77 Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Thu, 11 Jun 2020 12:52:45 -0700 Subject: [PATCH] Query: Perform join operations when one of the source has client eval (#21201) Resolves #19247 Resolves #17763 Lays ground work for #20291 Required for #20892 --- ...yableMethodTranslatingExpressionVisitor.cs | 6 + ...ionalProjectionBindingExpressionVisitor.cs | 33 + ...yableMethodTranslatingExpressionVisitor.cs | 106 ++- .../Query/SqlExpressionFactory.cs | 2 +- .../Query/SqlExpressions/SelectExpression.cs | 716 ++++++++++++++++-- ...yableMethodTranslatingExpressionVisitor.cs | 5 + .../Query/NorthwindJoinQueryCosmosTest.cs | 12 + .../Query/NorthwindJoinQueryInMemoryTest.cs | 14 + .../Query/NorthwindJoinQueryTestBase.cs | 97 ++- .../Query/NorthwindJoinQuerySqlServerTest.cs | 34 + .../Query/NorthwindJoinQuerySqliteTest.cs | 4 + 11 files changed, 923 insertions(+), 106 deletions(-) diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs index 694de799264..2c6fe5ef480 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs @@ -548,11 +548,13 @@ protected override ShapedQueryExpression TranslateJoin( innerKeySelector, transparentIdentifierType); +#pragma warning disable CS0618 // Type or member is obsolete See issue#21200 return TranslateResultSelectorForJoin( outer, resultSelector, inner.ShaperExpression, transparentIdentifierType); +#pragma warning restore CS0618 // Type or member is obsolete } private (LambdaExpression OuterKeySelector, LambdaExpression InnerKeySelector) ProcessJoinKeySelector( @@ -703,11 +705,13 @@ protected override ShapedQueryExpression TranslateLeftJoin( innerKeySelector, transparentIdentifierType); +#pragma warning disable CS0618 // Type or member is obsolete See issue#21200 return TranslateResultSelectorForJoin( outer, resultSelector, MarkShaperNullable(inner.ShaperExpression), transparentIdentifierType); +#pragma warning restore CS0618 // Type or member is obsolete } /// @@ -961,11 +965,13 @@ protected override ShapedQueryExpression TranslateSelectMany( ((InMemoryQueryExpression)source.QueryExpression).AddSelectMany( (InMemoryQueryExpression)inner.QueryExpression, transparentIdentifierType, defaultIfEmpty); +#pragma warning disable CS0618 // Type or member is obsolete See issue#21200 return TranslateResultSelectorForJoin( source, resultSelector, innerShaperExpression, transparentIdentifierType); +#pragma warning restore CS0618 // Type or member is obsolete } return null; diff --git a/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs index 21a719a316d..cd0546d5f2f 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs @@ -9,6 +9,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; @@ -27,6 +28,7 @@ public class RelationalProjectionBindingExpressionVisitor : ExpressionVisitor private readonly RelationalSqlTranslatingExpressionVisitor _sqlTranslator; private SelectExpression _selectExpression; + private SqlExpression[] _existingProjections; private bool _clientEval; private readonly IDictionary _projectionMapping @@ -69,6 +71,8 @@ public virtual Expression Translate([NotNull] SelectExpression selectExpression, _clientEval = true; expandedExpression = _queryableMethodTranslatingExpressionVisitor.ExpandWeakEntities(_selectExpression, expression); + _existingProjections = _selectExpression.Projection.Select(e => e.Expression).ToArray(); + _selectExpression.ClearProjection(); result = Visit(expandedExpression); _projectionMapping.Clear(); @@ -115,6 +119,16 @@ public override Expression Visit(Expression expression) case ConstantExpression _: return expression; + case ProjectionBindingExpression projectionBindingExpression: + if (projectionBindingExpression.Index is int index) + { + var newIndex = _selectExpression.AddToProjection(_existingProjections[index]); + + return new ProjectionBindingExpression(_selectExpression, newIndex, expression.Type); + } + + throw new InvalidOperationException(CoreStrings.TranslationFailed(projectionBindingExpression.Print())); + case ParameterExpression parameterExpression: if (parameterExpression.Name?.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal) == true) { @@ -227,10 +241,29 @@ protected override Expression VisitExtension(Expression extensionExpression) if (extensionExpression is EntityShaperExpression entityShaperExpression) { + // TODO: Make this easier to understand some day. EntityProjectionExpression entityProjectionExpression; if (entityShaperExpression.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression) { VerifySelectExpression(projectionBindingExpression); + // If projectionBinding is not mapped then SelectExpression has client projection + // Hence force client eval + if (projectionBindingExpression.ProjectionMember == null) + { + if (_clientEval) + { + var indexMap = new Dictionary(); + foreach (var item in projectionBindingExpression.IndexMap) + { + indexMap[item.Key] = _selectExpression.AddToProjection(_existingProjections[item.Value]); + } + + return entityShaperExpression.Update(new ProjectionBindingExpression(_selectExpression, indexMap)); + } + + return null; + } + entityProjectionExpression = (EntityProjectionExpression)_selectExpression.GetMappedProjection( projectionBindingExpression.ProjectionMember); } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 72f4457a054..9d8fd6c752f 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -535,18 +536,11 @@ protected override ShapedQueryExpression TranslateJoin( var joinPredicate = CreateJoinPredicate(outer, outerKeySelector, inner, innerKeySelector); if (joinPredicate != null) { - var transparentIdentifierType = TransparentIdentifierFactory.Create( - resultSelector.Parameters[0].Type, - resultSelector.Parameters[1].Type); - - ((SelectExpression)outer.QueryExpression).AddInnerJoin( - (SelectExpression)inner.QueryExpression, joinPredicate, transparentIdentifierType); - - return TranslateResultSelectorForJoin( - outer, - resultSelector, - inner.ShaperExpression, - transparentIdentifierType); + var outerSelectExpression = (SelectExpression)outer.QueryExpression; + var outerShaperExpression = outerSelectExpression.AddInnerJoin(inner, joinPredicate, outer.ShaperExpression); + outer = outer.UpdateShaperExpression(outerShaperExpression); + + return TranslateTwoParameterSelector(outer, resultSelector); } return null; @@ -567,18 +561,12 @@ protected override ShapedQueryExpression TranslateLeftJoin( var joinPredicate = CreateJoinPredicate(outer, outerKeySelector, inner, innerKeySelector); if (joinPredicate != null) { - var transparentIdentifierType = TransparentIdentifierFactory.Create( - resultSelector.Parameters[0].Type, - resultSelector.Parameters[1].Type); - - ((SelectExpression)outer.QueryExpression).AddLeftJoin( - (SelectExpression)inner.QueryExpression, joinPredicate, transparentIdentifierType); - - return TranslateResultSelectorForJoin( - outer, - resultSelector, - MarkShaperNullable(inner.ShaperExpression), - transparentIdentifierType); + var outerSelectExpression = (SelectExpression)outer.QueryExpression; + inner = inner.UpdateShaperExpression(MarkShaperNullable(inner.ShaperExpression)); + var outerShaperExpression = outerSelectExpression.AddLeftJoin(inner, joinPredicate, outer.ShaperExpression); + outer = outer.UpdateShaperExpression(outerShaperExpression); + + return TranslateTwoParameterSelector(outer, resultSelector); } return null; @@ -880,28 +868,14 @@ protected override ShapedQueryExpression TranslateSelectMany( var collectionSelectorBody = RemapLambdaBody(source, newCollectionSelector); if (Visit(collectionSelectorBody) is ShapedQueryExpression inner) { - var transparentIdentifierType = TransparentIdentifierFactory.Create( - resultSelector.Parameters[0].Type, - resultSelector.Parameters[1].Type); - - var innerShaperExpression = inner.ShaperExpression; - if (defaultIfEmpty) - { - ((SelectExpression)source.QueryExpression).AddOuterApply( - (SelectExpression)inner.QueryExpression, transparentIdentifierType); - innerShaperExpression = MarkShaperNullable(innerShaperExpression); - } - else - { - ((SelectExpression)source.QueryExpression).AddCrossApply( - (SelectExpression)inner.QueryExpression, transparentIdentifierType); - } - - return TranslateResultSelectorForJoin( - source, - resultSelector, - innerShaperExpression, - transparentIdentifierType); + var innerSelectExpression = (SelectExpression)source.QueryExpression; + var shaper = defaultIfEmpty + ? innerSelectExpression.AddOuterApply( + inner.UpdateShaperExpression(MarkShaperNullable(inner.ShaperExpression)), + source.ShaperExpression) + : innerSelectExpression.AddCrossApply(inner, source.ShaperExpression); + + return TranslateTwoParameterSelector(source.UpdateShaperExpression(shaper), resultSelector); } } else @@ -917,18 +891,10 @@ protected override ShapedQueryExpression TranslateSelectMany( } } - var transparentIdentifierType = TransparentIdentifierFactory.Create( - resultSelector.Parameters[0].Type, - resultSelector.Parameters[1].Type); + var innerSelectExpression = (SelectExpression)source.QueryExpression; + var shaper = innerSelectExpression.AddCrossJoin(inner, source.ShaperExpression); - ((SelectExpression)source.QueryExpression).AddCrossJoin( - (SelectExpression)inner.QueryExpression, transparentIdentifierType); - - return TranslateResultSelectorForJoin( - source, - resultSelector, - inner.ShaperExpression, - transparentIdentifierType); + return TranslateTwoParameterSelector(source.UpdateShaperExpression(shaper), resultSelector); } } @@ -1358,7 +1324,7 @@ private Expression TryExpand(Expression source, MemberIdentity member) makeNullable); var joinPredicate = _sqlTranslator.Translate(Expression.Equal(outerKey, innerKey)); - _selectExpression.AddLeftJoin(innerSelectExpression, joinPredicate, null); + _selectExpression.AddLeftJoin(innerSelectExpression, joinPredicate); var leftJoinTable = ((LeftJoinExpression)_selectExpression.Tables.Last()).Table; innerShaper = new RelationalEntityShaperExpression( targetEntityType, @@ -1415,6 +1381,30 @@ private static IDictionary GetPropertyExpressionsFr } } + private ShapedQueryExpression TranslateTwoParameterSelector(ShapedQueryExpression source, LambdaExpression resultSelector) + { + var transparentIdentifierType = source.ShaperExpression.Type; + var transparentIdentifierParameter = Expression.Parameter(transparentIdentifierType); + + Expression original1 = resultSelector.Parameters[0]; + var replacement1 = AccessField(transparentIdentifierType, transparentIdentifierParameter, "Outer"); + Expression original2 = resultSelector.Parameters[1]; + var replacement2 = AccessField(transparentIdentifierType, transparentIdentifierParameter, "Inner"); + var newResultSelector = Expression.Lambda( + new ReplacingExpressionVisitor( + new[] { original1, original2 }, new[] { replacement1, replacement2 }) + .Visit(resultSelector.Body), + transparentIdentifierParameter); + + return TranslateSelect(source, newResultSelector); + } + + private static Expression AccessField( + Type transparentIdentifierType, + Expression targetExpression, + string fieldName) + => Expression.Field(targetExpression, transparentIdentifierType.GetTypeInfo().GetDeclaredField(fieldName)); + private ShapedQueryExpression AggregateResultShaper( ShapedQueryExpression source, Expression projection, bool throwWhenEmpty, Type resultType) { diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index 0918af8a0a6..2a3d92dfda9 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -846,7 +846,7 @@ private void AddInnerJoin( { var joinPredicate = GenerateJoinPredicate(selectExpression, foreignKey, table, skipInnerJoins, out var innerSelect); - selectExpression.AddInnerJoin(innerSelect, joinPredicate, null); + selectExpression.AddInnerJoin(innerSelect, joinPredicate); } private SqlExpression GenerateJoinPredicate( diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index cde4131a1e4..d12bcd23e35 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -194,6 +194,14 @@ public void ApplyProjection() _projectionMapping = result; } + /// + /// Clears all existing projections. + /// + public void ClearProjection() + { + _projection.Clear(); + } + private static IEnumerable GetAllPropertiesInHierarchy(IEntityType entityType) => entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive()) .SelectMany(EntityTypeExtensions.GetDeclaredProperties); @@ -1009,7 +1017,7 @@ public Expression AddSingleProjection([NotNull] ShapedQueryExpression shapedQuer innerSelectExpression.ApplyProjection(); var projectionCount = innerSelectExpression.Projection.Count; var pendingCollectionOffset = _pendingCollections.Count; - AddOuterApply(innerSelectExpression, null); + AddOuterApply(innerSelectExpression); // Joined SelectExpression may different based on left join or outer apply // And it will always be SelectExpression because of presence of Take(1) @@ -1189,14 +1197,6 @@ public ShaperRemappingExpressionVisitor( _indexMap = indexMap; } - public ShaperRemappingExpressionVisitor( - SelectExpression queryExpression, SelectExpression innerSelectExpression, int[] indexMap) - { - _queryExpression = queryExpression; - _innerSelectExpression = innerSelectExpression; - _indexMap = indexMap; - } - protected override Expression VisitExtension(Expression extensionExpression) { Check.NotNull(extensionExpression, nameof(extensionExpression)); @@ -1449,11 +1449,662 @@ private enum JoinType OuterApply } + private Expression AddJoin( + JoinType joinType, + SelectExpression innerSelectExpression, + Expression outerShaper, + Expression innerShaper, + SqlExpression joinPredicate = null) + { + // Try to convert Apply to normal join + if (joinType == JoinType.CrossApply + || joinType == JoinType.OuterApply) + { + // Doing for limit only since limit + offset may need sum + var limit = innerSelectExpression.Limit; + innerSelectExpression.Limit = null; + + joinPredicate = TryExtractJoinKey(innerSelectExpression); + if (joinPredicate != null) + { + var containsOuterReference = new SelectExpressionCorrelationFindingExpressionVisitor(this) + .ContainsOuterReference(innerSelectExpression); + if (containsOuterReference) + { + innerSelectExpression.ApplyPredicate(joinPredicate); + if (limit != null) + { + innerSelectExpression.ApplyLimit(limit); + } + } + else + { + if (limit != null) + { + var partitions = new List(); + GetPartitions(joinPredicate, partitions); + var orderings = innerSelectExpression.Orderings.Count > 0 + ? innerSelectExpression.Orderings + : innerSelectExpression._identifier.Count > 0 + ? innerSelectExpression._identifier.Select(e => new OrderingExpression(e.Column, true)) + : new[] { new OrderingExpression(new SqlFragmentExpression("(SELECT 1)"), true) }; + + var rowNumberExpression = new RowNumberExpression(partitions, orderings.ToList(), limit.TypeMapping); + innerSelectExpression.ClearOrdering(); + + var projectionMappings = innerSelectExpression.PushdownIntoSubquery(); + var subquery = (SelectExpression)innerSelectExpression.Tables[0]; + + joinPredicate = new SqlRemappingVisitor( + projectionMappings, subquery) + .Remap(joinPredicate); + + var outerColumn = subquery.GenerateOuterColumn(rowNumberExpression, "row"); + var predicate = new SqlBinaryExpression( + ExpressionType.LessThanOrEqual, outerColumn, limit, typeof(bool), joinPredicate.TypeMapping); + innerSelectExpression.ApplyPredicate(predicate); + } + + return AddJoin(joinType == JoinType.CrossApply ? JoinType.InnerJoin : JoinType.LeftJoin, + innerSelectExpression, outerShaper, innerShaper, joinPredicate); + } + } + else + { + if (limit != null) + { + innerSelectExpression.ApplyLimit(limit); + } + } + } + + // Verify what are the cases of pushdown for inner & outer both sides + if (Limit != null + || Offset != null + || IsDistinct + || GroupBy.Count > 0) + { + var sqlRemappingVisitor = new SqlRemappingVisitor(PushdownIntoSubquery(), (SelectExpression)Tables[0]); + innerSelectExpression = sqlRemappingVisitor.Remap(innerSelectExpression); + joinPredicate = sqlRemappingVisitor.Remap(joinPredicate); + } + + if (innerSelectExpression.Orderings.Any() + || innerSelectExpression.Limit != null + || innerSelectExpression.Offset != null + || innerSelectExpression.IsDistinct + || innerSelectExpression.Predicate != null + || innerSelectExpression.Tables.Count > 1 + || innerSelectExpression.GroupBy.Count > 0) + { + joinPredicate = new SqlRemappingVisitor( + innerSelectExpression.PushdownIntoSubquery(), (SelectExpression)innerSelectExpression.Tables[0]) + .Remap(joinPredicate); + } + + if (joinType == JoinType.LeftJoin + || joinType == JoinType.OuterApply) + { + _identifier.AddRange(innerSelectExpression._identifier.Select(e => (e.Column.MakeNullable(), e.Comparer))); + } + else + { + _identifier.AddRange(innerSelectExpression._identifier); + } + + var innerTable = innerSelectExpression.Tables.Single(); + var pendingCollectionOffset = _pendingCollections.Count; + // Copy over pending collection if in join else that info would be lost. + _pendingCollections.AddRange(innerSelectExpression._pendingCollections); + + var joinTable = joinType switch + { + JoinType.InnerJoin => new InnerJoinExpression(innerTable, joinPredicate), + JoinType.LeftJoin => new LeftJoinExpression(innerTable, joinPredicate), + JoinType.CrossJoin => new CrossJoinExpression(innerTable), + JoinType.CrossApply => new CrossApplyExpression(innerTable), + JoinType.OuterApply => (TableExpressionBase)new OuterApplyExpression(innerTable), + _ => throw new InvalidOperationException(CoreStrings.InvalidSwitch(nameof(joinType), joinType)) + }; + + _tables.Add(joinTable); + + var transparentIdentifierType = TransparentIdentifierFactory.Create(outerShaper.Type, innerShaper.Type); + var outerMemberInfo = transparentIdentifierType.GetTypeInfo().GetDeclaredField("Outer"); + var innerMemberInfo = transparentIdentifierType.GetTypeInfo().GetDeclaredField("Inner"); + var outerClientEval = Projection.Count > 0; + var innerClientEval = innerSelectExpression.Projection.Count > 0; + var remapper = new ProjectionBindingExpressionRemappingExpressionVisitor(this); + var innerNullable = joinType == JoinType.LeftJoin || joinType == JoinType.OuterApply; + + if (outerClientEval) + { + if (innerClientEval) + { + var offset = Projection.Count; + foreach (var projectionExpression in innerSelectExpression.Projection) + { + var projectionToAdd = projectionExpression.Expression; + if (projectionToAdd is ColumnExpression column) + { + projectionToAdd = column.MakeNullable(); + } + + // Here we are assuming that projections will be added to the outer in the order. + // Which should be correct as inner projection should have deduped same columns. + // And outer/inner column would not have any duplicate. + AddToProjection(projectionToAdd); + } + + innerShaper = remapper.ShiftIndex(offset, innerShaper, pendingCollectionOffset); + _projectionMapping.Clear(); + } + else + { + var mapping = new Dictionary(); + foreach (var projection in innerSelectExpression._projectionMapping) + { + var projectionMember = projection.Key; + var projectionToAdd = projection.Value; + + if (projectionToAdd is EntityProjectionExpression entityProjection) + { + mapping[projectionMember] = AddToProjection(entityProjection.MakeNullable()); + } + else + { + if (projectionToAdd is ColumnExpression column) + { + projectionToAdd = column.MakeNullable(); + } + + mapping[projectionMember] = AddToProjection((SqlExpression)projectionToAdd); + } + } + + innerShaper = remapper.RemapProjectionMember(mapping, innerShaper, pendingCollectionOffset); + _projectionMapping.Clear(); + } + } + else + { + if (innerClientEval) + { + var mapping = new Dictionary(); + foreach (var projection in _projectionMapping) + { + var projectionToAdd = projection.Value; + + mapping[projection.Key] = projectionToAdd is EntityProjectionExpression entityProjection + ? AddToProjection(entityProjection) + : (object)AddToProjection((SqlExpression)projectionToAdd); + } + + outerShaper = remapper.RemapProjectionMember(mapping, outerShaper); + + var offset = Projection.Count; + foreach (var projectionExpression in innerSelectExpression.Projection) + { + var projectionToAdd = projectionExpression.Expression; + if (projectionToAdd is ColumnExpression column) + { + projectionToAdd = column.MakeNullable(); + } + + // Here we are assuming that projections will be added to the outer in the order. + // Which should be correct as inner projection should have deduped same columns. + // And outer/inner column would not have any duplicate. + AddToProjection(projectionToAdd); + } + + innerShaper = remapper.ShiftIndex(offset, innerShaper, pendingCollectionOffset); + _projectionMapping.Clear(); + } + else + { + var projectionMapping = new Dictionary(); + var mapping = new Dictionary(); + + foreach (var projection in _projectionMapping) + { + var projectionMember = projection.Key; + var remappedProjectionMember = projection.Key.Prepend(outerMemberInfo); + mapping[projectionMember] = remappedProjectionMember; + projectionMapping[remappedProjectionMember] = projection.Value; + } + + outerShaper = remapper.RemapProjectionMember(mapping, outerShaper); + mapping.Clear(); + + foreach (var projection in innerSelectExpression._projectionMapping) + { + var projectionMember = projection.Key; + var remappedProjectionMember = projection.Key.Prepend(innerMemberInfo); + mapping[projectionMember] = remappedProjectionMember; + var projectionToAdd = projection.Value; + if (innerNullable) + { + if (projectionToAdd is EntityProjectionExpression entityProjection) + { + projectionToAdd = entityProjection.MakeNullable(); + } + else if (projectionToAdd is ColumnExpression column) + { + projectionToAdd = column.MakeNullable(); + } + } + + projectionMapping[remappedProjectionMember] = projectionToAdd; + } + + innerShaper = remapper.RemapProjectionMember(mapping, innerShaper, pendingCollectionOffset); + _projectionMapping = projectionMapping; + } + } + + return New( + transparentIdentifierType.GetTypeInfo().DeclaredConstructors.Single(), + new[] { outerShaper, innerShaper }, outerMemberInfo, innerMemberInfo); + } + + private sealed class ProjectionBindingExpressionRemappingExpressionVisitor : ExpressionVisitor + { + private readonly Expression _queryExpression; + // Shifting PMs, converting PMs to index/indexMap + private IDictionary _projectionMemberMappings; + // Shifting offset + private int? _offset; + // Shift pending collection offset + private int _pendingCollectionOffset; + + public ProjectionBindingExpressionRemappingExpressionVisitor(Expression queryExpression) + { + _queryExpression = queryExpression; + } + + public Expression RemapProjectionMember( + IDictionary projectionMemberMappings, Expression expression, int pendingCollectionOffset = 0) + { + _projectionMemberMappings = projectionMemberMappings; + _offset = null; + _pendingCollectionOffset = pendingCollectionOffset; + + return Visit(expression); + } + + public Expression ShiftIndex(int offset, Expression expression, int pendingCollectionOffset = 0) + { + _projectionMemberMappings = null; + _offset = offset; + _pendingCollectionOffset = pendingCollectionOffset; + + return Visit(expression); + } + + protected override Expression VisitExtension(Expression extensionExpression) + { + return extensionExpression switch + { + ProjectionBindingExpression projectionBindingExpression => Remap(projectionBindingExpression), + CollectionShaperExpression collectionShaperExpression => Remap(collectionShaperExpression), + _ => base.VisitExtension(extensionExpression) + }; + } + + private CollectionShaperExpression Remap(CollectionShaperExpression collectionShaperExpression) + => new CollectionShaperExpression( + new ProjectionBindingExpression( + _queryExpression, + ((ProjectionBindingExpression)collectionShaperExpression.Projection).Index.Value + _pendingCollectionOffset, + typeof(object)), + collectionShaperExpression.InnerShaper, + collectionShaperExpression.Navigation, + collectionShaperExpression.ElementType); + + private ProjectionBindingExpression Remap(ProjectionBindingExpression projectionBindingExpression) + { + if (_offset is int offset) + { + if (projectionBindingExpression.Index is int index) + { + return CreateNewBinding(index + offset, projectionBindingExpression.Type); + } + else + { + var indexMap = new Dictionary(); + foreach (var item in projectionBindingExpression.IndexMap) + { + indexMap[item.Key] = item.Value + offset; + } + + return CreateNewBinding(indexMap, projectionBindingExpression.Type); + } + } + + var currentProjectionMember = projectionBindingExpression.ProjectionMember; + var newBinding = _projectionMemberMappings[currentProjectionMember]; + + return CreateNewBinding(newBinding, projectionBindingExpression.Type); + } + + private ProjectionBindingExpression CreateNewBinding(object binding, Type type) + => binding switch + { + ProjectionMember projectionMember => new ProjectionBindingExpression( + _queryExpression, projectionMember, type), + + int index => new ProjectionBindingExpression(_queryExpression, index, type), + + IDictionary indexMap => new ProjectionBindingExpression(_queryExpression, indexMap), + + _ => throw new InvalidOperationException(), + }; + } + + private void AddJoin(JoinType joinType, SelectExpression innerSelectExpression, SqlExpression joinPredicate = null) + { + // Try to convert Apply to normal join + if (joinType == JoinType.CrossApply + || joinType == JoinType.OuterApply) + { + // Doing for limit only since limit + offset may need sum + var limit = innerSelectExpression.Limit; + innerSelectExpression.Limit = null; + + joinPredicate = TryExtractJoinKey(innerSelectExpression); + if (joinPredicate != null) + { + var containsOuterReference = new SelectExpressionCorrelationFindingExpressionVisitor(this) + .ContainsOuterReference(innerSelectExpression); + if (containsOuterReference) + { + innerSelectExpression.ApplyPredicate(joinPredicate); + if (limit != null) + { + innerSelectExpression.ApplyLimit(limit); + } + } + else + { + if (limit != null) + { + var partitions = new List(); + GetPartitions(joinPredicate, partitions); + var orderings = innerSelectExpression.Orderings.Count > 0 + ? innerSelectExpression.Orderings + : innerSelectExpression._identifier.Count > 0 + ? innerSelectExpression._identifier.Select(e => new OrderingExpression(e.Column, true)) + : new[] { new OrderingExpression(new SqlFragmentExpression("(SELECT 1)"), true) }; + + var rowNumberExpression = new RowNumberExpression(partitions, orderings.ToList(), limit.TypeMapping); + innerSelectExpression.ClearOrdering(); + + var projectionMappings = innerSelectExpression.PushdownIntoSubquery(); + var subquery = (SelectExpression)innerSelectExpression.Tables[0]; + + joinPredicate = new SqlRemappingVisitor( + projectionMappings, subquery) + .Remap(joinPredicate); + + var outerColumn = subquery.GenerateOuterColumn(rowNumberExpression, "row"); + var predicate = new SqlBinaryExpression( + ExpressionType.LessThanOrEqual, outerColumn, limit, typeof(bool), joinPredicate.TypeMapping); + innerSelectExpression.ApplyPredicate(predicate); + } + + AddJoin(joinType == JoinType.CrossApply ? JoinType.InnerJoin : JoinType.LeftJoin, innerSelectExpression, joinPredicate); + return; + } + } + else + { + if (limit != null) + { + innerSelectExpression.ApplyLimit(limit); + } + } + } + + // Verify what are the cases of pushdown for inner & outer both sides + if (Limit != null + || Offset != null + || IsDistinct + || GroupBy.Count > 0) + { + var sqlRemappingVisitor = new SqlRemappingVisitor(PushdownIntoSubquery(), (SelectExpression)Tables[0]); + innerSelectExpression = sqlRemappingVisitor.Remap(innerSelectExpression); + joinPredicate = sqlRemappingVisitor.Remap(joinPredicate); + } + + if (innerSelectExpression.Orderings.Any() + || innerSelectExpression.Limit != null + || innerSelectExpression.Offset != null + || innerSelectExpression.IsDistinct + || innerSelectExpression.Predicate != null + || innerSelectExpression.Tables.Count > 1 + || innerSelectExpression.GroupBy.Count > 0) + { + joinPredicate = new SqlRemappingVisitor( + innerSelectExpression.PushdownIntoSubquery(), (SelectExpression)innerSelectExpression.Tables[0]) + .Remap(joinPredicate); + } + + if (joinType == JoinType.LeftJoin + || joinType == JoinType.OuterApply) + { + _identifier.AddRange(innerSelectExpression._identifier.Select(e => (e.Column.MakeNullable(), e.Comparer))); + } + else + { + _identifier.AddRange(innerSelectExpression._identifier); + } + + var innerTable = innerSelectExpression.Tables.Single(); + // Copy over pending collection if in join else that info would be lost. + _pendingCollections.AddRange(innerSelectExpression._pendingCollections); + + var joinTable = joinType switch + { + JoinType.InnerJoin => new InnerJoinExpression(innerTable, joinPredicate), + JoinType.LeftJoin => new LeftJoinExpression(innerTable, joinPredicate), + JoinType.CrossJoin => new CrossJoinExpression(innerTable), + JoinType.CrossApply => new CrossApplyExpression(innerTable), + JoinType.OuterApply => (TableExpressionBase)new OuterApplyExpression(innerTable), + _ => throw new InvalidOperationException(CoreStrings.InvalidSwitch(nameof(joinType), joinType)) + }; + + _tables.Add(joinTable); + } + + /// + /// Adds the given to table sources using INNER JOIN. + /// + /// A to join with. + /// A predicate to use for the join. + public void AddInnerJoin([NotNull] SelectExpression innerSelectExpression, [NotNull] SqlExpression joinPredicate) + { + Check.NotNull(innerSelectExpression, nameof(innerSelectExpression)); + Check.NotNull(joinPredicate, nameof(joinPredicate)); + + AddJoin(JoinType.InnerJoin, innerSelectExpression, joinPredicate); + } + + /// + /// Adds the given to table sources using LEFT JOIN. + /// + /// A to join with. + /// A predicate to use for the join. + public void AddLeftJoin([NotNull] SelectExpression innerSelectExpression, [NotNull] SqlExpression joinPredicate) + { + Check.NotNull(innerSelectExpression, nameof(innerSelectExpression)); + Check.NotNull(joinPredicate, nameof(joinPredicate)); + + AddJoin(JoinType.LeftJoin, innerSelectExpression, joinPredicate); + } + + /// + /// Adds the given to table sources using CROSS JOIN. + /// + /// A to join with. + public void AddCrossJoin([NotNull] SelectExpression innerSelectExpression) + { + Check.NotNull(innerSelectExpression, nameof(innerSelectExpression)); + + AddJoin(JoinType.CrossJoin, innerSelectExpression); + } + + /// + /// Adds the given to table sources using CROSS APPLY. + /// + /// A to join with. + public void AddCrossApply([NotNull] SelectExpression innerSelectExpression) + { + Check.NotNull(innerSelectExpression, nameof(innerSelectExpression)); + + AddJoin(JoinType.CrossApply, innerSelectExpression); + } + + /// + /// Adds the given to table sources using OUTER APPLY. + /// + /// A to join with. + public void AddOuterApply([NotNull] SelectExpression innerSelectExpression) + { + Check.NotNull(innerSelectExpression, nameof(innerSelectExpression)); + + AddJoin(JoinType.OuterApply, innerSelectExpression); + } + + /// + /// Adds the query expression of the given to table sources using INNER 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 AddInnerJoin( + [NotNull] ShapedQueryExpression innerSource, + [NotNull] SqlExpression joinPredicate, + [NotNull] Expression outerShaper) + { + Check.NotNull(innerSource, nameof(innerSource)); + Check.NotNull(joinPredicate, nameof(joinPredicate)); + Check.NotNull(outerShaper, nameof(outerShaper)); + + return AddJoin( + JoinType.InnerJoin, (SelectExpression)innerSource.QueryExpression, outerShaper, innerSource.ShaperExpression, joinPredicate); + } + + /// + /// Adds the query expression of the given to table sources using LEFT 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 AddLeftJoin( + [NotNull] ShapedQueryExpression innerSource, + [NotNull] SqlExpression joinPredicate, + [NotNull] Expression outerShaper) + { + Check.NotNull(innerSource, nameof(innerSource)); + Check.NotNull(joinPredicate, nameof(joinPredicate)); + Check.NotNull(outerShaper, nameof(outerShaper)); + + return AddJoin( + JoinType.LeftJoin, (SelectExpression)innerSource.QueryExpression, outerShaper, innerSource.ShaperExpression, joinPredicate); + } + + /// + /// Adds the query expression of the given to table sources using CROSS JOIN and combine shapers. + /// + /// A to join with. + /// An expression for outer shaper. + /// An expression which shapes the result of this join. + public Expression AddCrossJoin( + [NotNull] ShapedQueryExpression innerSource, + [NotNull] Expression outerShaper) + { + Check.NotNull(innerSource, nameof(innerSource)); + Check.NotNull(outerShaper, nameof(outerShaper)); + + return AddJoin(JoinType.CrossJoin, (SelectExpression)innerSource.QueryExpression, outerShaper, innerSource.ShaperExpression); + } + + /// + /// Adds the query expression of the given to table sources using CROSS APPLY and combine shapers. + /// + /// A to join with. + /// An expression for outer shaper. + /// An expression which shapes the result of this join. + public Expression AddCrossApply( + [NotNull] ShapedQueryExpression innerSource, + [NotNull] Expression outerShaper) + { + Check.NotNull(innerSource, nameof(innerSource)); + Check.NotNull(outerShaper, nameof(outerShaper)); + + return AddJoin(JoinType.CrossApply, (SelectExpression)innerSource.QueryExpression, outerShaper, innerSource.ShaperExpression); + } + + /// + /// Adds the query expression of the given to table sources using OUTER APPLY and combine shapers. + /// + /// A to join with. + /// An expression for outer shaper. + /// An expression which shapes the result of this join. + public Expression AddOuterApply( + [NotNull] ShapedQueryExpression innerSource, + [NotNull] Expression outerShaper) + { + Check.NotNull(innerSource, nameof(innerSource)); + Check.NotNull(outerShaper, nameof(outerShaper)); + + return AddJoin(JoinType.OuterApply, (SelectExpression)innerSource.QueryExpression, outerShaper, innerSource.ShaperExpression); + } + + private sealed class SqlRemappingVisitor : ExpressionVisitor + { + private readonly SelectExpression _subquery; + private readonly IDictionary _mappings; + + public SqlRemappingVisitor(IDictionary mappings, SelectExpression subquery) + { + _subquery = subquery; + _mappings = mappings; + } + + public SqlExpression Remap(SqlExpression sqlExpression) => (SqlExpression)Visit(sqlExpression); + public SelectExpression Remap(SelectExpression sqlExpression) => (SelectExpression)Visit(sqlExpression); + + public override Expression Visit(Expression expression) + { + switch (expression) + { + case SqlExpression sqlExpression + when _mappings.TryGetValue(sqlExpression, out var outer): + return outer; + + case ColumnExpression columnExpression + when _subquery.ContainsTableReference(columnExpression.Table): + var index = _subquery.AddToProjection(columnExpression); + var projectionExpression = _subquery._projection[index]; + return new ColumnExpression(projectionExpression, _subquery); + + default: + return base.Visit(expression); + } + } + } + + #region ObsoleteMethods + + [Obsolete] private void AddJoin( JoinType joinType, SelectExpression innerSelectExpression, Type transparentIdentifierType, - SqlExpression joinPredicate = null) + SqlExpression joinPredicate) { // Try to convert Apply to normal join if (joinType == JoinType.CrossApply @@ -1608,6 +2259,7 @@ private void AddJoin( /// A to join with. /// A predicate to use for the join. /// The type of the result generated after performing the join. + [Obsolete("Use the other overloads.")] public void AddInnerJoin( [NotNull] SelectExpression innerSelectExpression, [NotNull] SqlExpression joinPredicate, @@ -1625,6 +2277,7 @@ public void AddInnerJoin( /// A to join with. /// A predicate to use for the join. /// The type of the result generated after performing the join. + [Obsolete("Use the other overloads.")] public void AddLeftJoin( [NotNull] SelectExpression innerSelectExpression, [NotNull] SqlExpression joinPredicate, @@ -1641,11 +2294,12 @@ public void AddLeftJoin( /// /// A to join with. /// The type of the result generated after performing the join. + [Obsolete("Use the other overloads.")] public void AddCrossJoin([NotNull] SelectExpression innerSelectExpression, [CanBeNull] Type transparentIdentifierType) { Check.NotNull(innerSelectExpression, nameof(innerSelectExpression)); - AddJoin(JoinType.CrossJoin, innerSelectExpression, transparentIdentifierType); + AddJoin(JoinType.CrossJoin, innerSelectExpression, transparentIdentifierType, null); } /// @@ -1653,11 +2307,12 @@ public void AddCrossJoin([NotNull] SelectExpression innerSelectExpression, [CanB /// /// A to join with. /// The type of the result generated after performing the join. + [Obsolete("Use the other overloads.")] public void AddCrossApply([NotNull] SelectExpression innerSelectExpression, [CanBeNull] Type transparentIdentifierType) { Check.NotNull(innerSelectExpression, nameof(innerSelectExpression)); - AddJoin(JoinType.CrossApply, innerSelectExpression, transparentIdentifierType); + AddJoin(JoinType.CrossApply, innerSelectExpression, transparentIdentifierType, null); } /// @@ -1665,46 +2320,15 @@ public void AddCrossApply([NotNull] SelectExpression innerSelectExpression, [Can /// /// A to join with. /// The type of the result generated after performing the join. + [Obsolete("Use the other overloads.")] public void AddOuterApply([NotNull] SelectExpression innerSelectExpression, [CanBeNull] Type transparentIdentifierType) { Check.NotNull(innerSelectExpression, nameof(innerSelectExpression)); - AddJoin(JoinType.OuterApply, innerSelectExpression, transparentIdentifierType); + AddJoin(JoinType.OuterApply, innerSelectExpression, transparentIdentifierType, null); } - private sealed class SqlRemappingVisitor : ExpressionVisitor - { - private readonly SelectExpression _subquery; - private readonly IDictionary _mappings; - - public SqlRemappingVisitor(IDictionary mappings, SelectExpression subquery) - { - _subquery = subquery; - _mappings = mappings; - } - - public SqlExpression Remap(SqlExpression sqlExpression) => (SqlExpression)Visit(sqlExpression); - public SelectExpression Remap(SelectExpression sqlExpression) => (SelectExpression)Visit(sqlExpression); - - public override Expression Visit(Expression expression) - { - switch (expression) - { - case SqlExpression sqlExpression - when _mappings.TryGetValue(sqlExpression, out var outer): - return outer; - - case ColumnExpression columnExpression - when _subquery.ContainsTableReference(columnExpression.Table): - var index = _subquery.AddToProjection(columnExpression); - var projectionExpression = _subquery._projection[index]; - return new ColumnExpression(projectionExpression, _subquery); - - default: - return base.Visit(expression); - } - } - } + #endregion /// protected override Expression VisitChildren(ExpressionVisitor visitor) diff --git a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs index d7c4a640d40..9902f9665a5 100644 --- a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs @@ -528,6 +528,7 @@ protected virtual Expression MarkShaperNullable([NotNull] Expression shaperExpre /// The shaper for inner source. /// The clr type of transparent identifier created from result. /// The shaped query expression after translation of result selector. + [Obsolete("QueryExpressions should combine shapers to work in client eval scenarios.")] protected virtual ShapedQueryExpression TranslateResultSelectorForJoin( [NotNull] ShapedQueryExpression outer, [NotNull] LambdaExpression resultSelector, @@ -557,6 +558,7 @@ protected virtual ShapedQueryExpression TranslateResultSelectorForJoin( return TranslateSelect(outer, newResultSelector); } + [Obsolete] private Expression CombineShapers( Expression queryExpression, Expression outerShaper, @@ -573,6 +575,7 @@ private Expression CombineShapers( new[] { outerShaper, innerShaper }, outerMemberInfo, innerMemberInfo); } + [Obsolete] private sealed class MemberAccessShiftingExpressionVisitor : ExpressionVisitor { private readonly Expression _queryExpression; @@ -597,6 +600,7 @@ protected override Expression VisitExtension(Expression extensionExpression) } } + [Obsolete] private static Expression AccessOuterTransparentField( Type transparentIdentifierType, Expression targetExpression) @@ -606,6 +610,7 @@ private static Expression AccessOuterTransparentField( return Expression.Field(targetExpression, fieldInfo); } + [Obsolete] private static Expression AccessInnerTransparentField( Type transparentIdentifierType, Expression targetExpression) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindJoinQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindJoinQueryCosmosTest.cs index adbfd52d806..56817affdfa 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindJoinQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindJoinQueryCosmosTest.cs @@ -504,6 +504,18 @@ public override Task Left_join_with_tautology_predicate_doesnt_convert_to_cross_ return base.Left_join_with_tautology_predicate_doesnt_convert_to_cross_join(async); } + [ConditionalTheory(Skip = "Issue#17246")] + public override Task SelectMany_with_client_eval(bool async) + { + return base.SelectMany_with_client_eval(async); + } + + [ConditionalTheory(Skip = "Issue#17246")] + public override Task SelectMany_with_client_eval_with_constructor(bool async) + { + return base.SelectMany_with_client_eval_with_constructor(async); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs index a8af3262142..c670d8bc152 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs @@ -1,7 +1,9 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; using Xunit.Abstractions; namespace Microsoft.EntityFrameworkCore.Query @@ -17,5 +19,17 @@ public NorthwindJoinQueryInMemoryTest( { //TestLoggerFactory.TestOutputHelper = testOutputHelper; } + + [ConditionalTheory(Skip = "Issue#21200")] + public override Task SelectMany_with_client_eval(bool async) + { + return base.SelectMany_with_client_eval(async); + } + + [ConditionalTheory(Skip = "Issue#21200")] + public override Task SelectMany_with_client_eval_with_constructor(bool async) + { + return base.SelectMany_with_client_eval_with_constructor(async); + } } } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs index ad066890ee9..560881026ad 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs @@ -716,7 +716,7 @@ public virtual Task Inner_join_with_tautology_predicate_converts_to_cross_join(b return AssertQuery( async, ss => from c in ss.Set().OrderBy(c => c.CustomerID).Take(10) - join o in ss.Set().OrderBy(o => o.OrderID).Take(10) on 1 equals 1 + join o in ss.Set().OrderBy(o => o.OrderID).Take(10) on 1 equals 1 select new { c.CustomerID, o.OrderID }); } @@ -731,5 +731,100 @@ join o in ss.Set().OrderBy(o => o.OrderID).Take(10) on c.CustomerID != nu from o in grouping.DefaultIfEmpty() select new { c.CustomerID, o.OrderID }); } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SelectMany_with_client_eval(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .SelectMany(c => c.Orders.Select(o => new { OrderProperty = ClientMethod(o), CustomerProperty = c.ContactName })), + elementSorter: e => e.OrderProperty, + entryCount: 830); + } + + private static int ClientMethod(Order order) => order.OrderID; + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SelectMany_with_client_eval_with_constructor(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Where(c => c.CustomerID.StartsWith("A")) + .OrderBy(c => c.CustomerID) + .Select(c => new CustomerViewModel( + c.CustomerID, c.City, + c.Orders.SelectMany(o => o.OrderDetails + .Where(od => od.OrderID < 11000) + .Select(od => new OrderDetailViewModel(od.OrderID, od.ProductID))) + .ToArray())), + assertOrder: true); + } + + private class CustomerViewModel + { + private readonly string _customerID; + private readonly string _city; + private readonly OrderDetailViewModel[] _views; + + public CustomerViewModel(string customerID, string city, OrderDetailViewModel[] views) + { + _customerID = customerID; + _city = city; + _views = views; + } + + public override bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + return ReferenceEquals(this, obj) + || obj.GetType() == GetType() + && Equals((CustomerViewModel)obj); + } + + private bool Equals(CustomerViewModel customerViewModel) + => _customerID == customerViewModel._customerID + && _city == customerViewModel._city + && _views.SequenceEqual(customerViewModel._views); + + public override int GetHashCode() => HashCode.Combine(_customerID, _city); + } + + private class OrderDetailViewModel + { + private readonly int _orderID; + private readonly int _productID; + + public OrderDetailViewModel(int orderID, int productID) + { + _orderID = orderID; + _productID = productID; + } + + public override bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + return ReferenceEquals(this, obj) + || obj.GetType() == GetType() + && Equals((OrderDetailViewModel)obj); + } + + private bool Equals(OrderDetailViewModel orderDetailViewModel) + => _orderID == orderDetailViewModel._orderID + && _productID == orderDetailViewModel._productID; + + public override int GetHashCode() => HashCode.Combine(_orderID, _productID); + } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs index a45aa1c9857..52ebf90fbff 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs @@ -513,6 +513,40 @@ ORDER BY [o].[OrderID] ORDER BY [t].[CustomerID]"); } + public override async Task SelectMany_with_client_eval(bool async) + { + await base.SelectMany_with_client_eval(async); + + AssertSql( + @"SELECT [t].[OrderID], [t].[CustomerID], [t].[EmployeeID], [t].[OrderDate], [t].[ContactName] +FROM [Customers] AS [c] +CROSS APPLY ( + SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [c].[ContactName] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] +) AS [t]"); + } + + public override async Task SelectMany_with_client_eval_with_constructor(bool async) + { + await base.SelectMany_with_client_eval_with_constructor(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[City], [t0].[OrderID], [t0].[ProductID], [t0].[OrderID0], [t0].[OrderID1], [t0].[ProductID0] +FROM [Customers] AS [c] +LEFT JOIN ( + SELECT [t].[OrderID], [t].[ProductID], [o].[OrderID] AS [OrderID0], [t].[OrderID] AS [OrderID1], [t].[ProductID] AS [ProductID0], [o].[CustomerID] + FROM [Orders] AS [o] + INNER JOIN ( + SELECT [o0].[OrderID], [o0].[ProductID] + FROM [Order Details] AS [o0] + WHERE [o0].[OrderID] < 11000 + ) AS [t] ON [o].[OrderID] = [t].[OrderID] +) AS [t0] ON [c].[CustomerID] = [t0].[CustomerID] +WHERE [c].[CustomerID] LIKE N'A%' +ORDER BY [c].[CustomerID], [t0].[OrderID0], [t0].[OrderID1], [t0].[ProductID0]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindJoinQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindJoinQuerySqliteTest.cs index a2b3a955a03..21f57411a21 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindJoinQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindJoinQuerySqliteTest.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit.Abstractions; @@ -14,5 +15,8 @@ public NorthwindJoinQuerySqliteTest(NorthwindQuerySqliteFixture Task.CompletedTask; } }