From 61a763069ca6e54f557ca1191890ecff0bc9b76c Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 16 Jun 2024 12:05:28 +0200 Subject: [PATCH] Cosmos: Implement SelectMany Also introduces substantial infrastructure for general joins Closes #17312 --- .../Properties/CosmosStrings.Designer.cs | 6 + .../Properties/CosmosStrings.resx | 3 + .../Query/Internal/CosmosQuerySqlGenerator.cs | 32 ++- .../Query/Internal/CosmosQueryUtils.cs | 4 +- ...yableMethodTranslatingExpressionVisitor.cs | 96 +++++++-- .../Internal/Expressions/SelectExpression.cs | 141 +++++++++++-- .../Internal/Expressions/SourceExpression.cs | 64 ++++-- .../InMemoryQueryExpression.Helper.cs | 27 +-- ...yableMethodTranslatingExpressionVisitor.cs | 9 +- .../SqlExpressions/SelectExpression.Helper.cs | 27 +-- .../Query/SqlExpressions/SelectExpression.cs | 155 +++++++-------- src/EFCore/Query/QueryCompilationContext.cs | 17 +- .../Query/TransparentIdentifierFactory.cs | 25 ++- .../Query/OwnedQueryCosmosTest.cs | 188 ++++++++++++++++-- .../PrimitiveCollectionsQueryCosmosTest.cs | 46 ++++- .../PrimitiveCollectionsQueryTestBase.cs | 14 ++ ...imitiveCollectionsQueryOldSqlServerTest.cs | 6 + .../PrimitiveCollectionsQuerySqlServerTest.cs | 28 +++ .../PrimitiveCollectionsQuerySqliteTest.cs | 12 ++ 19 files changed, 667 insertions(+), 233 deletions(-) diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs index 527c56ca5d4..67c08640c24 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs @@ -37,6 +37,12 @@ public static string AnalyticalTTLMismatch(object? ttl1, object? entityType1, ob public static string CanConnectNotSupported => GetString("CanConnectNotSupported"); + /// + /// Complex projections in subqueries are currently unsupported. + /// + public static string ComplexProjectionInSubqueryNotSupported + => GetString("ComplexProjectionInSubqueryNotSupported"); + /// /// None of connection string, CredentialToken, account key or account endpoint were specified. Specify a set of connection details. /// diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.resx b/src/EFCore.Cosmos/Properties/CosmosStrings.resx index d15bd153984..10552b282e4 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.resx +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.resx @@ -123,6 +123,9 @@ The Cosmos database does not support 'CanConnect' or 'CanConnectAsync'. + + Complex projections in subqueries are currently unsupported. + None of connection string, CredentialToken, account key or account endpoint were specified. Specify a set of connection details. diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs index b4fe51bb07c..92a985d7b23 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs @@ -283,16 +283,11 @@ protected override Expression VisitSelect(SelectExpression selectExpression) { // If the SELECT projects a single value out, we just project that with the Cosmos VALUE keyword (without VALUE, // Cosmos projects a JSON object containing the value). - if (selectExpression.UsesSingleValueProjection) + // TODO: Ideally, just always use VALUE for all single-projection SELECTs - but this like requires shaper changes. + if (selectExpression.UsesSingleValueProjection && projection is [var singleProjection]) { _sqlBuilder.Append("VALUE "); - if (projection is not [var singleProjection]) - { - throw new UnreachableException( - $"Encountered SelectExpression with UsesValueProject=true and Projection.Count={projection.Count}."); - } - Visit(singleProjection.Expression); } // Otherwise, we'll project a JSON object; Cosmos has two syntaxes for doing so: @@ -319,16 +314,19 @@ protected override Expression VisitSelect(SelectExpression selectExpression) _sqlBuilder.Append('1'); } - if (selectExpression.Sources.Count > 0) + var sources = selectExpression.Sources; + if (sources.Count > 0) { - if (selectExpression.Sources.Count > 1) - { - throw new NotImplementedException("JOINs not yet supported"); - } - _sqlBuilder.AppendLine().Append("FROM "); - Visit(selectExpression.Sources[0]); + Visit(sources[0]); + + for (var i = 1; i < sources.Count; i++) + { + _sqlBuilder.AppendLine().Append("JOIN "); + + Visit(sources[i]); + } } if (selectExpression.Predicate != null) @@ -752,11 +750,11 @@ protected sealed override Expression VisitSource(SourceExpression sourceExpressi .Append(" IN "); - VisitContainerExpression(sourceExpression.ContainerExpression); + VisitContainerExpression(sourceExpression.Expression); } else { - VisitContainerExpression(sourceExpression.ContainerExpression); + VisitContainerExpression(sourceExpression.Expression); if (sourceExpression.Alias is not null) { @@ -795,7 +793,7 @@ void VisitContainerExpression(Expression containerExpression) } } - Visit(sourceExpression.ContainerExpression); + Visit(sourceExpression.Expression); if (subquery) { diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs index 8d56fcb8de1..da9deb08c5b 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs @@ -191,7 +191,7 @@ public static bool TryExtractBareArray( { WithIn: true, Alias: var sourceAlias, - ContainerExpression: SelectExpression + Expression: SelectExpression { Sources: [], Predicate: null, @@ -212,7 +212,7 @@ public static bool TryExtractBareArray( // For properties: SELECT i FROM i IN c.SomeArray // So just match any SelectExpression with IN. - case { Sources: [{ WithIn: true, ContainerExpression: var a, Alias: var sourceAlias }] } + case { Sources: [{ WithIn: true, Expression: var a, Alias: var sourceAlias }] } when projectedReferenceName == sourceAlias: { array = a; diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index 42f6403f082..1aba18a409f 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -22,7 +22,7 @@ public class CosmosQueryableMethodTranslatingExpressionVisitor : QueryableMethod private readonly IMethodCallTranslatorProvider _methodCallTranslatorProvider; private readonly CosmosSqlTranslatingExpressionVisitor _sqlTranslator; private readonly CosmosProjectionBindingExpressionVisitor _projectionBindingExpressionVisitor; - private readonly bool _subquery; + private bool _subquery; private ReadItemInfo? _readItemExpression; /// @@ -237,7 +237,7 @@ static bool ExtractPartitionKeyFromPredicate( protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) { var method = methodCallExpression.Method; - if (method.DeclaringType == typeof(Queryable)) + if (method.DeclaringType == typeof(Queryable) && method.IsGenericMethod) { switch (methodCallExpression.Method.Name) { @@ -370,7 +370,7 @@ private ShapedQueryExpression CreateShapedQueryExpression(IEntityType entityType new StructuralTypeShaperExpression( entityType, new ProjectionBindingExpression(queryExpression, new ProjectionMember(), typeof(ValueBuffer)), - false)); + nullable: false)); } private ShapedQueryExpression CreateShapedQueryExpression(SelectExpression select, Type elementClrType) @@ -994,7 +994,7 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s return null!; } - var newSelectorBody = ReplacingExpressionVisitor.Replace(selector.Parameters.Single(), source.ShaperExpression, selector.Body); + var newSelectorBody = RemapLambdaBody(source, selector); return source.UpdateShaperExpression(_projectionBindingExpressionVisitor.Translate(selectExpression, newSelectorBody)); } @@ -1009,16 +1009,53 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s ShapedQueryExpression source, LambdaExpression collectionSelector, LambdaExpression resultSelector) - => null; + { + var collectionSelectorBody = RemapLambdaBody(source, collectionSelector); - /// - /// 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. - /// + // The collection selector gets translated in subquery context; specifically, if an uncorrelated SelectMany() is attempted + // (from b in context.Blogs from p in context.Posts...), we want to detect that and fail translation as an uncorrelated query + // (see VisitExtension visitation for EntityQueryRootExpression) + var previousSubquery = _subquery; + _subquery = true; + try + { + if (Visit(collectionSelectorBody) is ShapedQueryExpression inner) + { + var select = (SelectExpression)source.QueryExpression; + var shaper = select.AddJoin(inner, source.ShaperExpression); + + return TranslateTwoParameterSelector(source.UpdateShaperExpression(shaper), resultSelector); + } + + return null; + } + finally + { + _subquery = previousSubquery; + } + } + + /// protected override ShapedQueryExpression? TranslateSelectMany(ShapedQueryExpression source, LambdaExpression selector) - => null; + { + // TODO: Note that we currently never actually seem to get SelectMany without a result selector, because nav expansion rewrites + // that to a more complex variant with a result selector (see https://github.com/dotnet/efcore/issues/32957#issuecomment-2170950767) + // blogs.SelectMany(c => c.Ints) becomes: + // blogs + // .SelectMany(p => Property(p, "Ints").AsQueryable(), (p, c) => new TransparentIdentifier`2(Outer = p, Inner = c)) + // .Select(ti => ti.Inner) + + // TODO: In Cosmos, we currently always add a predicate for the discriminator (unless HasNoDiscriminator is explicitly specified), + // so the source is almost never a bare array. + // If we stop doing that (see #34005, #20268), and we remove the result selector problem (see just above), we should check if the + // source is a bare array, and simply return the ShapedQueryExpression returned from visiting the collection selector. This would + // remove the extra unneeded JOIN we'd currently generate. + var innerParameter = Expression.Parameter(selector.ReturnType.GetSequenceType(), "i"); + var resultSelector = Expression.Lambda( + innerParameter, Expression.Parameter(source.Type.GetSequenceType()), innerParameter); + + return TranslateSelectMany(source, selector, resultSelector); + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -1691,14 +1728,11 @@ when methodCallExpression.TryGetIndexerArguments(_queryCompilationContext.Model, private SqlExpression? TranslateLambdaExpression( ShapedQueryExpression shapedQueryExpression, LambdaExpression lambdaExpression) - { - var lambdaBody = RemapLambdaBody(shapedQueryExpression.ShaperExpression, lambdaExpression); - - return TranslateExpression(lambdaBody); - } + => TranslateExpression(RemapLambdaBody(shapedQueryExpression, lambdaExpression)); - private static Expression RemapLambdaBody(Expression shaperBody, LambdaExpression lambdaExpression) - => ReplacingExpressionVisitor.Replace(lambdaExpression.Parameters.Single(), shaperBody, lambdaExpression.Body); + private Expression RemapLambdaBody(ShapedQueryExpression shapedQueryExpression, LambdaExpression lambdaExpression) + => ReplacingExpressionVisitor.Replace( + lambdaExpression.Parameters.Single(), shapedQueryExpression.ShaperExpression, lambdaExpression.Body); private static ShapedQueryExpression AggregateResultShaper( ShapedQueryExpression source, @@ -1743,4 +1777,28 @@ private static ShapedQueryExpression AggregateResultShaper( return source.UpdateShaperExpression(shaper); } + + 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)!); } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs index b2a829ea311..cf593cb0531 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs @@ -119,7 +119,7 @@ private SelectExpression() /// 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. /// - public static SelectExpression CreateForCollection(Expression containerExpression, string sourceAlias, Expression projection) + public static SelectExpression CreateForCollection(Expression sourceExpression, string sourceAlias, Expression projection) { // SelectExpressions representing bare arrays are of the form SELECT VALUE i FROM i IN x. // Unfortunately, Cosmos doesn't support x being anything but a root container or a property access @@ -127,25 +127,18 @@ public static SelectExpression CreateForCollection(Expression containerExpressio // For example, x cannot be a function invocation (SELECT VALUE i FROM i IN SetUnion(...)) or an array constant // (SELECT VALUE i FROM i IN [1,2,3]). // So we wrap any non-property in a subquery as follows: SELECT i FROM i IN (SELECT VALUE [1,2,3]) - switch (containerExpression) - { - case ObjectReferenceExpression: - case ScalarReferenceExpression: - case ObjectArrayAccessExpression: - case ScalarAccessExpression: - break; - default: - containerExpression = new SelectExpression( - [new ProjectionExpression(containerExpression, null!)], - sources: [], - orderings: []) - { - UsesSingleValueProjection = true - }; - break; + if (!SourceExpression.IsCompatible(sourceExpression)) + { + sourceExpression = new SelectExpression( + [new ProjectionExpression(sourceExpression, null!)], + sources: [], + orderings: []) + { + UsesSingleValueProjection = true + }; } - var source = new SourceExpression(containerExpression, sourceAlias, withIn: true); + var source = new SourceExpression(sourceExpression, sourceAlias, withIn: true); return new SelectExpression { @@ -517,6 +510,86 @@ public virtual void ReverseOrderings() } } + /// + /// 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. + /// + public virtual Expression AddJoin(ShapedQueryExpression inner, Expression outerShaper) + { + var (innerSelect, innerShaper) = ((SelectExpression)inner.QueryExpression, inner.ShaperExpression); + + // TODO: Proper alias management (#33894). + // Create a new source (JOIN) for the server side of the query; if the inner query represents a bare array, unwrap it and + // add the JOIN directly + var joinSource = CosmosQueryUtils.TryExtractBareArray(inner, out var bareArray) && SourceExpression.IsCompatible(bareArray) + ? new SourceExpression(bareArray, "a", withIn: true) + : new SourceExpression(innerSelect, "a"); + + // Make the necessary modifications to the shaper side, projecting out a TransparentIdentifier (outer/inner) + var transparentIdentifierType = TransparentIdentifierFactory.Create(outerShaper.Type, innerShaper.Type); + var outerMemberInfo = transparentIdentifierType.GetTypeInfo().GetDeclaredField("Outer")!; + var innerMemberInfo = transparentIdentifierType.GetTypeInfo().GetDeclaredField("Inner")!; + + var projectionMapping = new Dictionary(); + var mapping = new Dictionary(); + + foreach (var (projectionMember, expression) in _projectionMapping) + { + var remappedProjectionMember = projectionMember.Prepend(outerMemberInfo); + mapping[projectionMember] = remappedProjectionMember; + projectionMapping[remappedProjectionMember] = expression; + } + + outerShaper = new ProjectionMemberRemappingExpressionVisitor(this, mapping).Visit(outerShaper); + mapping.Clear(); + + foreach (var (projectionMember, expression) in innerSelect._projectionMapping) + { + var remappedProjectionMember = projectionMember.Prepend(innerMemberInfo); + mapping[projectionMember] = remappedProjectionMember; + + Expression projectionToAdd; + if (projectionMember.Last is null) + { + projectionToAdd = expression switch + { + SqlExpression e => new ScalarReferenceExpression(joinSource.Alias, e.Type, e.TypeMapping), + EntityProjectionExpression e => e.Update(new ObjectReferenceExpression(e.EntityType, joinSource.Alias)), + + _ => throw new UnreachableException( + $"Unexpected expression type in projection when adding join: {expression.GetType().Name}") + }; + } + else + { + // TODO: #34004 + // The subquery is projecting out a JSON object; for the projection mapping of the outer query, we need to generate + // property accesses over that object: Scalar/ObjectAccessExpressions over the ObjectReferenceExpression that references + // the JOIN source. + // However, the JSON object being projected out of the subquery doesn't correspond to any entity type, and there's currently + // no way for us to represent a reference to that - ObjectReferenceExpression requires an IEntityType. Changing that + // requires shaper-side changes (see comment in ObjectReferenceExpression); if we can remove that requirement, we can + // possibly also merge ScalarReferenceExpression and ObjectReferenceExpression to a single SourceReferenceExpression. + throw new InvalidOperationException(CosmosStrings.ComplexProjectionInSubqueryNotSupported); + } + + projectionMapping[remappedProjectionMember] = projectionToAdd; + } + + innerSelect.ApplyProjection(); + _sources.Add(joinSource); + + innerShaper = new ProjectionMemberRemappingExpressionVisitor(this, mapping).Visit(innerShaper); + _projectionMapping = projectionMapping; + innerSelect._projectionMapping.Clear(); + + return New( + transparentIdentifierType.GetTypeInfo().DeclaredConstructors.Single(), + new[] { outerShaper, innerShaper }, outerMemberInfo, innerMemberInfo); + } + /// /// 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 @@ -732,11 +805,16 @@ private void PrintSql(ExpressionPrinter expressionPrinter, bool withTags = true) expressionPrinter.Append("1"); } - if (Sources.Any()) + if (Sources.Count > 0) { expressionPrinter.AppendLine().Append("FROM "); + expressionPrinter.Visit(Sources[0]); - expressionPrinter.VisitCollection(Sources, p => p.AppendLine()); + for (var i = 1; i < Sources.Count; i++) + { + expressionPrinter.AppendLine().Append("JOIN "); + expressionPrinter.Visit(Sources[i]); + } } if (Predicate != null) @@ -787,4 +865,27 @@ public virtual string DebugView => this.Print(); #endregion Print + + private sealed class ProjectionMemberRemappingExpressionVisitor( + SelectExpression queryExpression, + Dictionary projectionMemberMappings) + : ExpressionVisitor + { + protected override Expression VisitExtension(Expression expression) + { + if (expression is ProjectionBindingExpression projectionBindingExpression) + { + Check.DebugAssert( + projectionBindingExpression.ProjectionMember is not null, + "ProjectionBindingExpression must have projection member."); + + return new ProjectionBindingExpression( + queryExpression, + projectionMemberMappings[projectionBindingExpression.ProjectionMember], + projectionBindingExpression.Type); + } + + return base.VisitExtension(expression); + } + } } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SourceExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SourceExpression.cs index f51e9e5f471..8c0a8a716af 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SourceExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SourceExpression.cs @@ -12,9 +12,26 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// /// FROM clause (NoSQL query) [DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] -public class SourceExpression(Expression containerExpression, string alias, bool withIn = false) - : Expression, IAccessExpression, IPrintableExpression +public class SourceExpression : Expression, IAccessExpression, IPrintableExpression { + /// + /// 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. + /// + public SourceExpression(Expression expression, string alias, bool withIn = false) + { + if (!IsCompatible(expression)) + { + throw new ArgumentException($"Expression type '{expression.GetType().Name}' cannot appear directly in a source expression"); + } + + Expression = expression; + Alias = alias; + WithIn = withIn; + } + /// /// 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 @@ -31,7 +48,7 @@ public sealed override ExpressionType NodeType /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override Type Type - => ContainerExpression.Type; + => Expression.Type; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -39,7 +56,7 @@ public override Type Type /// 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. /// - public virtual Expression ContainerExpression { get; } = containerExpression; + public virtual Expression Expression { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -47,7 +64,7 @@ public override Type Type /// 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. /// - public virtual string Alias { get; } = alias; + public virtual string Alias { get; } /// /// Specifies that the source uses IN, and will be generated as FROM x IN c.Tags @@ -58,7 +75,7 @@ public override Type Type /// 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. /// - public virtual bool WithIn { get; } = withIn; + public virtual bool WithIn { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -76,7 +93,7 @@ string IAccessExpression.PropertyName /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override Expression VisitChildren(ExpressionVisitor visitor) - => Update(visitor.Visit(ContainerExpression)); + => Update(visitor.Visit(Expression)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -85,10 +102,33 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual SourceExpression Update(Expression containerExpression) - => ReferenceEquals(containerExpression, ContainerExpression) + => ReferenceEquals(containerExpression, Expression) ? this : new SourceExpression(containerExpression, Alias); + /// + /// Returns whether the given expression type can appear directly within a source expression. + /// + /// + /// 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. + /// + public static bool IsCompatible(Expression expression) + => expression switch + { + ObjectReferenceExpression => true, + ScalarReferenceExpression => true, + ObjectArrayAccessExpression => true, + ScalarAccessExpression => true, + + SelectExpression => true, + FromSqlExpression => true, + + _ => false + }; + /// /// 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 @@ -109,11 +149,11 @@ public void Print(ExpressionPrinter expressionPrinter) expressionPrinter .Append(Alias) .Append(" IN "); - expressionPrinter.Visit(ContainerExpression); + expressionPrinter.Visit(Expression); } else { - expressionPrinter.Visit(ContainerExpression); + expressionPrinter.Visit(Expression); expressionPrinter .Append(" AS ") .Append(Alias); @@ -134,7 +174,7 @@ private bool Equals(SourceExpression? other) || (other is not null && Alias == other.Alias && WithIn == other.WithIn - && ContainerExpression.Equals(other.ContainerExpression)); + && Expression.Equals(other.Expression)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -145,7 +185,7 @@ private bool Equals(SourceExpression? other) public override int GetHashCode() { var hashCode = new HashCode(); - hashCode.Add(ContainerExpression); + hashCode.Add(Expression); hashCode.Add(Alias); hashCode.Add(WithIn); return hashCode.ToHashCode(); diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.Helper.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.Helper.cs index 912b08fccbe..bef8d85ee42 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.Helper.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.Helper.cs @@ -61,35 +61,26 @@ void IDisposable.Dispose() } } - private sealed class ProjectionMemberRemappingExpressionVisitor : ExpressionVisitor + private sealed class ProjectionMemberRemappingExpressionVisitor( + Expression queryExpression, + Dictionary projectionMemberMappings) + : ExpressionVisitor { - private readonly Expression _queryExpression; - private readonly Dictionary _projectionMemberMappings; - - public ProjectionMemberRemappingExpressionVisitor( - Expression queryExpression, - Dictionary projectionMemberMappings) - { - _queryExpression = queryExpression; - _projectionMemberMappings = projectionMemberMappings; - } - - [return: NotNullIfNotNull(nameof(expression))] - public override Expression? Visit(Expression? expression) + protected override Expression VisitExtension(Expression expression) { if (expression is ProjectionBindingExpression projectionBindingExpression) { Check.DebugAssert( - projectionBindingExpression.ProjectionMember != null, + projectionBindingExpression.ProjectionMember is not null, "ProjectionBindingExpression must have projection member."); return new ProjectionBindingExpression( - _queryExpression, - _projectionMemberMappings[projectionBindingExpression.ProjectionMember], + queryExpression, + projectionMemberMappings[projectionBindingExpression.ProjectionMember], projectionBindingExpression.Type); } - return base.Visit(expression); + return base.VisitExtension(expression); } } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index a52124103fd..20115136354 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -1094,6 +1094,7 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s LambdaExpression collectionSelector, LambdaExpression resultSelector) { + var select = (SelectExpression)source.QueryExpression; var (newCollectionSelector, correlated, defaultIfEmpty) = new CorrelationFindingExpressionVisitor().IsCorrelated(collectionSelector); if (correlated) @@ -1101,10 +1102,9 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s var collectionSelectorBody = RemapLambdaBody(source, newCollectionSelector); if (Visit(collectionSelectorBody) is ShapedQueryExpression inner) { - var innerSelectExpression = (SelectExpression)source.QueryExpression; var shaper = defaultIfEmpty - ? innerSelectExpression.AddOuterApply(inner, source.ShaperExpression) - : innerSelectExpression.AddCrossApply(inner, source.ShaperExpression); + ? select.AddOuterApply(inner, source.ShaperExpression) + : select.AddCrossApply(inner, source.ShaperExpression); return TranslateTwoParameterSelector(source.UpdateShaperExpression(shaper), resultSelector); } @@ -1124,8 +1124,7 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s inner = translatedInner; } - var innerSelectExpression = (SelectExpression)source.QueryExpression; - var shaper = innerSelectExpression.AddCrossJoin(inner, source.ShaperExpression); + var shaper = select.AddCrossJoin(inner, source.ShaperExpression); return TranslateTwoParameterSelector(source.UpdateShaperExpression(shaper), resultSelector); } diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs index 16c8390cef6..40e1c668e36 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs @@ -55,35 +55,26 @@ public bool ContainsOuterReference(SelectExpression selectExpression) } } - private sealed class ProjectionMemberRemappingExpressionVisitor : ExpressionVisitor + private sealed class ProjectionMemberRemappingExpressionVisitor( + SelectExpression queryExpression, + Dictionary projectionMemberMappings) + : ExpressionVisitor { - private readonly SelectExpression _queryExpression; - private readonly Dictionary _projectionMemberMappings; - - public ProjectionMemberRemappingExpressionVisitor( - SelectExpression queryExpression, - Dictionary projectionMemberMappings) - { - _queryExpression = queryExpression; - _projectionMemberMappings = projectionMemberMappings; - } - - [return: NotNullIfNotNull(nameof(expression))] - public override Expression? Visit(Expression? expression) + protected override Expression VisitExtension(Expression expression) { if (expression is ProjectionBindingExpression projectionBindingExpression) { Check.DebugAssert( - projectionBindingExpression.ProjectionMember != null, + projectionBindingExpression.ProjectionMember is not null, "ProjectionBindingExpression must have projection member."); return new ProjectionBindingExpression( - _queryExpression, - _projectionMemberMappings[projectionBindingExpression.ProjectionMember], + queryExpression, + projectionMemberMappings[projectionBindingExpression.ProjectionMember], projectionBindingExpression.Type); } - return base.Visit(expression); + return base.VisitExtension(expression); } } diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index fbbf01b9b5a..3754f1030fe 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -2728,18 +2728,18 @@ private enum JoinType private Expression AddJoin( JoinType joinType, - SelectExpression innerSelectExpression, + SelectExpression innerSelect, Expression outerShaper, Expression innerShaper, SqlExpression? joinPredicate = null) { - AddJoin(joinType, ref innerSelectExpression, out _, joinPredicate); + AddJoin(joinType, ref innerSelect, out _, joinPredicate); var transparentIdentifierType = TransparentIdentifierFactory.Create(outerShaper.Type, innerShaper.Type); var outerMemberInfo = transparentIdentifierType.GetTypeInfo().GetDeclaredField("Outer")!; var innerMemberInfo = transparentIdentifierType.GetTypeInfo().GetDeclaredField("Inner")!; var outerClientEval = _clientProjections.Count > 0; - var innerClientEval = innerSelectExpression._clientProjections.Count > 0; + var innerClientEval = innerSelect._clientProjections.Count > 0; var innerNullable = joinType is JoinType.LeftJoin or JoinType.OuterApply; if (outerClientEval) @@ -2748,25 +2748,25 @@ private Expression AddJoin( if (innerClientEval) { // Add inner to projection and update indexes - var indexMap = new int[innerSelectExpression._clientProjections.Count]; - for (var i = 0; i < innerSelectExpression._clientProjections.Count; i++) + var indexMap = new int[innerSelect._clientProjections.Count]; + for (var i = 0; i < innerSelect._clientProjections.Count; i++) { - var projectionToAdd = innerSelectExpression._clientProjections[i]; + var projectionToAdd = innerSelect._clientProjections[i]; projectionToAdd = MakeNullable(projectionToAdd, innerNullable); _clientProjections.Add(projectionToAdd); - _aliasForClientProjections.Add(innerSelectExpression._aliasForClientProjections[i]); + _aliasForClientProjections.Add(innerSelect._aliasForClientProjections[i]); indexMap[i] = _clientProjections.Count - 1; } - innerSelectExpression._clientProjections.Clear(); - innerSelectExpression._aliasForClientProjections.Clear(); + innerSelect._clientProjections.Clear(); + innerSelect._aliasForClientProjections.Clear(); - innerShaper = new ProjectionIndexRemappingExpressionVisitor(innerSelectExpression, this, indexMap).Visit(innerShaper); + innerShaper = new ProjectionIndexRemappingExpressionVisitor(innerSelect, this, indexMap).Visit(innerShaper); } else { // Apply inner projection mapping and convert projection member binding to indexes - var mapping = ConvertProjectionMappingToClientProjections(innerSelectExpression._projectionMapping, innerNullable); + var mapping = ConvertProjectionMappingToClientProjections(innerSelect._projectionMapping, innerNullable); innerShaper = new ProjectionMemberToIndexConvertingExpressionVisitor(this, mapping).Visit(innerShaper); } } @@ -2779,20 +2779,20 @@ private Expression AddJoin( var mapping = ConvertProjectionMappingToClientProjections(_projectionMapping); outerShaper = new ProjectionMemberToIndexConvertingExpressionVisitor(this, mapping).Visit(outerShaper); - var indexMap = new int[innerSelectExpression._clientProjections.Count]; - for (var i = 0; i < innerSelectExpression._clientProjections.Count; i++) + var indexMap = new int[innerSelect._clientProjections.Count]; + for (var i = 0; i < innerSelect._clientProjections.Count; i++) { - var projectionToAdd = innerSelectExpression._clientProjections[i]; + var projectionToAdd = innerSelect._clientProjections[i]; projectionToAdd = MakeNullable(projectionToAdd, innerNullable); _clientProjections.Add(projectionToAdd); - _aliasForClientProjections.Add(innerSelectExpression._aliasForClientProjections[i]); + _aliasForClientProjections.Add(innerSelect._aliasForClientProjections[i]); indexMap[i] = _clientProjections.Count - 1; } - innerSelectExpression._clientProjections.Clear(); - innerSelectExpression._aliasForClientProjections.Clear(); + innerSelect._clientProjections.Clear(); + innerSelect._aliasForClientProjections.Clear(); - innerShaper = new ProjectionIndexRemappingExpressionVisitor(innerSelectExpression, this, indexMap).Visit(innerShaper); + innerShaper = new ProjectionIndexRemappingExpressionVisitor(innerSelect, this, indexMap).Visit(innerShaper); } else { @@ -2809,19 +2809,18 @@ private Expression AddJoin( outerShaper = new ProjectionMemberRemappingExpressionVisitor(this, mapping).Visit(outerShaper); mapping.Clear(); - foreach (var projection in innerSelectExpression._projectionMapping) + foreach (var (projectionMember, expression) in innerSelect._projectionMapping) { - var projectionMember = projection.Key; - var remappedProjectionMember = projection.Key.Prepend(innerMemberInfo); + var remappedProjectionMember = projectionMember.Prepend(innerMemberInfo); mapping[projectionMember] = remappedProjectionMember; - var projectionToAdd = projection.Value; + var projectionToAdd = expression; projectionToAdd = MakeNullable(projectionToAdd, innerNullable); projectionMapping[remappedProjectionMember] = projectionToAdd; } innerShaper = new ProjectionMemberRemappingExpressionVisitor(this, mapping).Visit(innerShaper); _projectionMapping = projectionMapping; - innerSelectExpression._projectionMapping.Clear(); + innerSelect._projectionMapping.Clear(); } } @@ -2837,7 +2836,7 @@ private Expression AddJoin( private void AddJoin( JoinType joinType, - ref SelectExpression innerSelectExpression, + ref SelectExpression innerSelect, out bool innerPushdownOccurred, SqlExpression? joinPredicate = null) { @@ -2845,43 +2844,43 @@ private void AddJoin( // Try to convert Apply to normal join if (joinType is JoinType.CrossApply or JoinType.OuterApply) { - var limit = innerSelectExpression.Limit; - var offset = innerSelectExpression.Offset; - if (!innerSelectExpression.IsDistinct + var limit = innerSelect.Limit; + var offset = innerSelect.Offset; + if (!innerSelect.IsDistinct || (limit == null && offset == null)) { - innerSelectExpression.Limit = null; - innerSelectExpression.Offset = null; + innerSelect.Limit = null; + innerSelect.Offset = null; - var originalInnerSelectPredicate = innerSelectExpression.GroupBy.Count > 0 - ? innerSelectExpression.Having - : innerSelectExpression.Predicate; + var originalInnerSelectPredicate = innerSelect.GroupBy.Count > 0 + ? innerSelect.Having + : innerSelect.Predicate; - joinPredicate = TryExtractJoinKey(this, innerSelectExpression, allowNonEquality: limit == null && offset == null); + joinPredicate = TryExtractJoinKey(this, innerSelect, allowNonEquality: limit == null && offset == null); if (joinPredicate != null) { var containsOuterReference = new SelectExpressionCorrelationFindingExpressionVisitor(this) - .ContainsOuterReference(innerSelectExpression); + .ContainsOuterReference(innerSelect); if (!containsOuterReference) { if (limit != null || offset != null) { var partitions = new List(); - GetPartitions(innerSelectExpression, joinPredicate, partitions); - var orderings = innerSelectExpression.Orderings.Count > 0 - ? innerSelectExpression.Orderings - : innerSelectExpression._identifier.Count > 0 - ? innerSelectExpression._identifier.Select(e => new OrderingExpression(e.Column, true)) + GetPartitions(innerSelect, joinPredicate, partitions); + var orderings = innerSelect.Orderings.Count > 0 + ? innerSelect.Orderings + : innerSelect._identifier.Count > 0 + ? innerSelect._identifier.Select(e => new OrderingExpression(e.Column, true)) : new[] { new OrderingExpression(new SqlFragmentExpression("(SELECT 1)"), true) }; var rowNumberExpression = new RowNumberExpression( partitions, orderings.ToList(), (limit ?? offset)!.TypeMapping); - innerSelectExpression.ClearOrdering(); + innerSelect.ClearOrdering(); - joinPredicate = innerSelectExpression.PushdownIntoSubqueryInternal().Remap(joinPredicate); + joinPredicate = innerSelect.PushdownIntoSubqueryInternal().Remap(joinPredicate); - var outerColumn = ((SelectExpression)innerSelectExpression.Tables[0]).GenerateOuterColumn( - innerSelectExpression.Tables[0].Alias!, rowNumberExpression, "row"); + var outerColumn = ((SelectExpression)innerSelect.Tables[0]).GenerateOuterColumn( + innerSelect.Tables[0].Alias!, rowNumberExpression, "row"); SqlExpression? offsetPredicate = null; SqlExpression? limitPredicate = null; if (offset != null) @@ -2913,12 +2912,12 @@ private void AddJoin( joinPredicate.TypeMapping) : offsetPredicate : limitPredicate; - innerSelectExpression.ApplyPredicate(predicate!); + innerSelect.ApplyPredicate(predicate!); } AddJoin( joinType == JoinType.CrossApply ? JoinType.InnerJoin : JoinType.LeftJoin, - ref innerSelectExpression, + ref innerSelect, out innerPushdownOccurred, joinPredicate); @@ -2927,13 +2926,13 @@ private void AddJoin( if (originalInnerSelectPredicate != null) { - if (innerSelectExpression.GroupBy.Count > 0) + if (innerSelect.GroupBy.Count > 0) { - innerSelectExpression.Having = originalInnerSelectPredicate; + innerSelect.Having = originalInnerSelectPredicate; } else { - innerSelectExpression.Predicate = originalInnerSelectPredicate; + innerSelect.Predicate = originalInnerSelectPredicate; } } @@ -2943,12 +2942,12 @@ private void AddJoin( // Order matters Apply Offset before Limit if (offset != null) { - innerSelectExpression.ApplyOffset(offset); + innerSelect.ApplyOffset(offset); } if (limit != null) { - innerSelectExpression.ApplyLimit(limit); + innerSelect.ApplyLimit(limit); } } } @@ -2959,31 +2958,31 @@ private void AddJoin( || GroupBy.Count > 0) { var sqlRemappingVisitor = PushdownIntoSubqueryInternal(); - innerSelectExpression = sqlRemappingVisitor.Remap(innerSelectExpression); + innerSelect = sqlRemappingVisitor.Remap(innerSelect); joinPredicate = sqlRemappingVisitor.Remap(joinPredicate); } - if (innerSelectExpression.Limit != null - || innerSelectExpression.Offset != null - || innerSelectExpression.IsDistinct - || innerSelectExpression.Predicate != null - || innerSelectExpression.Tables.Count > 1 - || innerSelectExpression.GroupBy.Count > 0) + if (innerSelect.Limit != null + || innerSelect.Offset != null + || innerSelect.IsDistinct + || innerSelect.Predicate != null + || innerSelect.Tables.Count > 1 + || innerSelect.GroupBy.Count > 0) { - joinPredicate = innerSelectExpression.PushdownIntoSubqueryInternal().Remap(joinPredicate); + joinPredicate = innerSelect.PushdownIntoSubqueryInternal().Remap(joinPredicate); innerPushdownOccurred = true; } if (_identifier.Count > 0 - && innerSelectExpression._identifier.Count > 0) + && innerSelect._identifier.Count > 0) { if (joinType is JoinType.LeftJoin or JoinType.OuterApply) { - _identifier.AddRange(innerSelectExpression._identifier.Select(e => (e.Column.MakeNullable(), e.Comparer))); + _identifier.AddRange(innerSelect._identifier.Select(e => (e.Column.MakeNullable(), e.Comparer))); } else { - _identifier.AddRange(innerSelectExpression._identifier); + _identifier.AddRange(innerSelect._identifier); } } else @@ -2991,10 +2990,10 @@ private void AddJoin( // if the subquery that is joined to can't be uniquely identified // then the entire join should also not be marked as non-identifiable _identifier.Clear(); - innerSelectExpression._identifier.Clear(); + innerSelect._identifier.Clear(); } - var innerTable = innerSelectExpression.Tables.Single(); + var innerTable = innerSelect.Tables.Single(); var joinTable = joinType switch { JoinType.InnerJoin => new InnerJoinExpression(innerTable, joinPredicate!), @@ -3007,26 +3006,26 @@ private void AddJoin( _tables.Add(joinTable); - static void GetPartitions(SelectExpression selectExpression, SqlExpression sqlExpression, List partitions) + static void GetPartitions(SelectExpression select, SqlExpression sqlExpression, List partitions) { - if (sqlExpression is SqlBinaryExpression sqlBinaryExpression) + switch (sqlExpression) { - if (sqlBinaryExpression.OperatorType == ExpressionType.Equal) + case SqlBinaryExpression { OperatorType: ExpressionType.Equal } binary: { - if (sqlBinaryExpression.Left is ColumnExpression columnExpression - && selectExpression.ContainsReferencedTable(columnExpression)) - { - partitions.Add(sqlBinaryExpression.Left); - } - else - { - partitions.Add(sqlBinaryExpression.Right); - } + partitions.Add( + binary.Left is ColumnExpression column && select.ContainsReferencedTable(column) + ? binary.Left + : binary.Right); + + break; } - else if (sqlBinaryExpression.OperatorType == ExpressionType.AndAlso) + + case SqlBinaryExpression { OperatorType: ExpressionType.AndAlso } binary: { - GetPartitions(selectExpression, sqlBinaryExpression.Left, partitions); - GetPartitions(selectExpression, sqlBinaryExpression.Right, partitions); + GetPartitions(select, binary.Left, partitions); + GetPartitions(select, binary.Right, partitions); + + break; } } } diff --git a/src/EFCore/Query/QueryCompilationContext.cs b/src/EFCore/Query/QueryCompilationContext.cs index 2e05c759414..286dc70c201 100644 --- a/src/EFCore/Query/QueryCompilationContext.cs +++ b/src/EFCore/Query/QueryCompilationContext.cs @@ -225,23 +225,20 @@ public virtual Func CreateQueryExecutor(Expressi public virtual Expression> CreateQueryExecutorExpression(Expression query) { var queryAndEventData = Logger.QueryCompilationStarting(Dependencies.Context, _expressionPrinter, query); - query = queryAndEventData.Query; + var interceptedQuery = queryAndEventData.Query; - query = _queryTranslationPreprocessorFactory.Create(this).Process(query); - // Convert EntityQueryable to ShapedQueryExpression - query = _queryableMethodTranslatingExpressionVisitorFactory.Create(this).Translate(query); - query = _queryTranslationPostprocessorFactory.Create(this).Process(query); + var preprocessedQuery = _queryTranslationPreprocessorFactory.Create(this).Process(interceptedQuery); + var translatedQuery = _queryableMethodTranslatingExpressionVisitorFactory.Create(this).Translate(preprocessedQuery); + var postprocessedQuery = _queryTranslationPostprocessorFactory.Create(this).Process(translatedQuery); - // Inject actual entity materializer - // Inject tracking - query = _shapedQueryCompilingExpressionVisitorFactory.Create(this).Visit(query); + var compiledQuery = _shapedQueryCompilingExpressionVisitorFactory.Create(this).Visit(postprocessedQuery); // If any additional parameters were added during the compilation phase (e.g. entity equality ID expression), // wrap the query with code adding those parameters to the query context - query = InsertRuntimeParameters(query); + var compiledQueryWithRuntimeParameters = InsertRuntimeParameters(compiledQuery); return Expression.Lambda>( - query, + compiledQueryWithRuntimeParameters, QueryContextParameter); } diff --git a/src/EFCore/Query/TransparentIdentifierFactory.cs b/src/EFCore/Query/TransparentIdentifierFactory.cs index 6bbdf329929..15c0417906d 100644 --- a/src/EFCore/Query/TransparentIdentifierFactory.cs +++ b/src/EFCore/Query/TransparentIdentifierFactory.cs @@ -34,19 +34,24 @@ public static class TransparentIdentifierFactory public static Type Create(Type outerType, Type innerType) => typeof(TransparentIdentifier<,>).MakeGenericType(outerType, innerType); - private readonly struct TransparentIdentifier + private readonly struct TransparentIdentifier(TOuter outer, TInner inner) { + /// + /// 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. + /// [UsedImplicitly] - public TransparentIdentifier(TOuter outer, TInner inner) - { - Outer = outer; - Inner = inner; - } + public readonly TOuter Outer = outer; + /// + /// 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. + /// [UsedImplicitly] - public readonly TOuter Outer; - - [UsedImplicitly] - public readonly TInner Inner; + public readonly TInner Inner = inner; } } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs index 4f035765ff5..d65433ac39b 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs @@ -21,7 +21,7 @@ public OwnedQueryCosmosTest(OwnedQueryCosmosFixture fixture, ITestOutputHelper t public override Task Query_loads_reference_nav_automatically_in_projection(bool async) => AssertTranslationFailed(() => base.Query_loads_reference_nav_automatically_in_projection(async)); - // TODO: SelectMany, #17246 + // Non-correlated queries not supported by Cosmos public override Task Query_with_owned_entity_equality_operator(bool async) => AssertTranslationFailed(() => base.Query_with_owned_entity_equality_operator(async)); @@ -223,23 +223,36 @@ public override Task Project_multiple_owned_navigations_with_expansion_on_owned_ => AssertTranslationFailed( () => base.Project_multiple_owned_navigations_with_expansion_on_owned_collections(async)); - // TODO: SelectMany, #17246 + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] public override Task SelectMany_on_owned_collection(bool async) - => AssertTranslationFailed(() => base.SelectMany_on_owned_collection(async)); + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.SelectMany_on_owned_collection(a); - // TODO: SelectMany, #17246 + AssertSql( + """ +SELECT a +FROM root c +JOIN a IN c["Orders"] +WHERE c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") +"""); + }); + + // TODO: Fake LeftJoin, #33969 public override Task SelectMany_on_owned_reference_followed_by_regular_entity_and_collection(bool async) => AssertTranslationFailed(() => base.SelectMany_on_owned_reference_followed_by_regular_entity_and_collection(async)); - // TODO: SelectMany, #17246 + // TODO: Fake LeftJoin, #33969 public override Task SelectMany_on_owned_reference_with_entity_in_between_ending_in_owned_collection(bool async) => AssertTranslationFailed(() => base.SelectMany_on_owned_reference_with_entity_in_between_ending_in_owned_collection(async)); - // TODO: SelectMany, #17246 + // Non-correlated queries not supported by Cosmos public override Task Query_with_owned_entity_equality_method(bool async) => AssertTranslationFailed(() => base.Query_with_owned_entity_equality_method(async)); - // TODO: SelectMany, #17246 + // Non-correlated queries not supported by Cosmos public override Task Query_with_owned_entity_equality_object_method(bool async) => AssertTranslationFailed(() => base.Query_with_owned_entity_equality_object_method(async)); @@ -290,25 +303,140 @@ public override async Task Client_method_skip_loads_owned_navigations_variation_ Assert.Equal(CosmosStrings.OffsetRequiresLimit, exception.Message); } - // TODO: SelectMany, #17246 public override Task Where_owned_collection_navigation_ToList_Count(bool async) - => AssertTranslationFailed(() => base.Where_owned_collection_navigation_ToList_Count(async)); + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + // TODO: #34011 + // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets persisted + // as null instead of []. + await AssertQuery( + a, + ss => ss.Set() + .OrderBy(p => p.Id) + .SelectMany(p => p.Orders) + .Select(p => p.Details.ToList()) + .Where(e => e.Count() == 1), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a)); + + AssertSql( + """ +SELECT a +FROM root c +JOIN a IN c["Orders"] +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(a["Details"]) = 1)) +ORDER BY c["Id"] +"""); + }); - // TODO: SelectMany, #17246 public override Task Where_collection_navigation_ToArray_Count(bool async) - => AssertTranslationFailed(() => base.Where_collection_navigation_ToArray_Count(async)); + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + // TODO: #34011 + // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets persisted + // as null instead of []. + await AssertQuery( + a, + ss => ss.Set() + .OrderBy(p => p.Id) + .SelectMany(p => p.Orders) + .Select(p => p.Details.AsEnumerable().ToArray()) + .Where(e => e.Count() == 1), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a)); + + AssertSql( + """ +SELECT a +FROM root c +JOIN a IN c["Orders"] +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(a["Details"]) = 1)) +ORDER BY c["Id"] +"""); + }); - // TODO: SelectMany, #17246 public override Task Where_collection_navigation_AsEnumerable_Count(bool async) - => AssertTranslationFailed(() => base.Where_collection_navigation_AsEnumerable_Count(async)); + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + // TODO: #34011 + // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets persisted + // as null instead of []. + await AssertQuery( + a, + ss => ss.Set() + .OrderBy(p => p.Id) + .SelectMany(p => p.Orders) + .Select(p => p.Details.AsEnumerable()) + .Where(e => e.Count() == 1), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a)); + + AssertSql( + """ +SELECT a +FROM root c +JOIN a IN c["Orders"] +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(a["Details"]) = 1)) +ORDER BY c["Id"] +"""); + }); - // TODO: SelectMany, #17246 public override Task Where_collection_navigation_ToList_Count_member(bool async) - => AssertTranslationFailed(() => base.Where_collection_navigation_ToList_Count_member(async)); + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + // TODO: #34011 + // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets persisted + // as null instead of []. + await AssertQuery( + a, + ss => ss.Set() + .OrderBy(p => p.Id) + .SelectMany(p => p.Orders) + .Select(p => p.Details.ToList()) + .Where(e => e.Count == 1), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a)); + + AssertSql( + """ +SELECT a +FROM root c +JOIN a IN c["Orders"] +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(a["Details"]) = 1)) +ORDER BY c["Id"] +"""); + }); - // TODO: SelectMany, #17246 public override Task Where_collection_navigation_ToArray_Length_member(bool async) - => AssertTranslationFailed(() => base.Where_collection_navigation_ToArray_Length_member(async)); + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + // TODO: #34011 + // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets persisted + // as null instead of []. + await AssertQuery( + a, + ss => ss.Set() + .OrderBy(p => p.Id) + .SelectMany(p => p.Orders) + .Select(p => p.Details.AsEnumerable().ToArray()) + .Where(e => e.Length == 1), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a)); + + AssertSql( + """ +SELECT a +FROM root c +JOIN a IN c["Orders"] +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(a["Details"]) = 1)) +ORDER BY c["Id"] +"""); + }); // TODO: GroupBy, #17313 public override Task GroupBy_with_multiple_aggregates_on_owned_navigation_properties(bool async) @@ -559,7 +687,7 @@ public override async Task NoTracking_Include_with_cycles_throws(bool async) AssertSql(); } - // TODO: SelectMany, #17246 + // TODO: Fake LeftJoin, #33969 public override Task NoTracking_Include_with_cycles_does_not_throw_when_performing_identity_resolution( bool async, bool useAsTracking) @@ -594,9 +722,29 @@ FROM root c } } - // TODO: SelectMany, #17246 public override Task Query_on_collection_entry_works_for_owned_collection(bool async) - => AssertTranslationFailed(() => base.Query_on_collection_entry_works_for_owned_collection(async)); + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Query_on_collection_entry_works_for_owned_collection(a); + + AssertSql( + """ +SELECT c +FROM root c +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (c["Id"] = 1)) +OFFSET 0 LIMIT 2 +""", + // + """ +@__p_0='1' + +SELECT a +FROM root c +JOIN a IN c["Orders"] +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (a["ClientId"] = @__p_0)) +"""); + }); // Non-correlated queries not supported by Cosmos public override Task Projecting_collection_correlated_with_keyless_entity_after_navigation_works_using_parent_identifiers( diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index 1e9a610f5f6..9e2864cb8a7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -1299,12 +1299,50 @@ public override async Task Column_collection_Distinct(bool async) AssertSql(); } - public override async Task Column_collection_SelectMany(bool async) + public override Task Column_collection_SelectMany(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Column_collection_SelectMany(a); + + AssertSql( + """ +SELECT a +FROM root c +JOIN a IN c["Ints"] +WHERE (c["Discriminator"] = "PrimitiveCollectionsEntity") +"""); + }); + + public override Task Column_collection_SelectMany_with_filter(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Column_collection_SelectMany_with_filter(a); + + AssertSql( + """ +SELECT a +FROM root c +JOIN ( + SELECT VALUE i + FROM i IN c["Ints"] + WHERE (i > 1)) a +WHERE (c["Discriminator"] = "PrimitiveCollectionsEntity") +"""); + }); + + public override async Task Column_collection_SelectMany_with_Select_to_anonymous_type(bool async) { - // TODO: SelectMany - await AssertTranslationFailed(() => base.Column_collection_SelectMany(async)); + // Always throws for sync. + if (async) + { + // TODO: #34004 + var exception = await Assert.ThrowsAsync( + () => base.Column_collection_SelectMany_with_Select_to_anonymous_type(async)); - AssertSql(); + Assert.Equal(CosmosStrings.ComplexProjectionInSubqueryNotSupported, exception.Message); + } } public override Task Column_collection_projection_from_top_level(bool async) diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index e7bf0d05611..45490365abd 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -738,6 +738,20 @@ public virtual Task Column_collection_SelectMany(bool async) async, ss => ss.Set().SelectMany(c => c.Ints)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_SelectMany_with_filter(bool async) + => AssertQuery( + async, + ss => ss.Set().SelectMany(c => c.Ints.Where(i => i > 1))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Column_collection_SelectMany_with_Select_to_anonymous_type(bool async) + => AssertQuery( + async, + ss => ss.Set().SelectMany(c => c.Ints.Select(i => new { Original = i, Incremented = i + 1 }))); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Column_collection_projection_from_top_level(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs index 7ec617c8858..655aef626db 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -751,6 +751,12 @@ public override Task Column_collection_Distinct(bool async) public override Task Column_collection_SelectMany(bool async) => AssertTranslationFailed(() => base.Column_collection_SelectMany(async)); + public override Task Column_collection_SelectMany_with_filter(bool async) + => AssertTranslationFailed(() => base.Column_collection_SelectMany_with_filter(async)); + + public override Task Column_collection_SelectMany_with_Select_to_anonymous_type(bool async) + => AssertTranslationFailed(() => base.Column_collection_SelectMany_with_Select_to_anonymous_type(async)); + public override async Task Column_collection_projection_from_top_level(bool async) { await base.Column_collection_projection_from_top_level(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index 7e352ef43b8..37725332129 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -1273,6 +1273,34 @@ CROSS APPLY OPENJSON([p].[Ints]) WITH ([value] int '$') AS [i] """); } + public override async Task Column_collection_SelectMany_with_filter(bool async) + { + await base.Column_collection_SelectMany_with_filter(async); + + AssertSql( + """ +SELECT [i0].[value] +FROM [PrimitiveCollectionsEntity] AS [p] +CROSS APPLY ( + SELECT [i].[value] + FROM OPENJSON([p].[Ints]) WITH ([value] int '$') AS [i] + WHERE [i].[value] > 1 +) AS [i0] +"""); + } + + public override async Task Column_collection_SelectMany_with_Select_to_anonymous_type(bool async) + { + await base.Column_collection_SelectMany_with_Select_to_anonymous_type(async); + + AssertSql( + """ +SELECT [i].[value] AS [Original], [i].[value] + 1 AS [Incremented] +FROM [PrimitiveCollectionsEntity] AS [p] +CROSS APPLY OPENJSON([p].[Ints]) WITH ([value] int '$') AS [i] +"""); + } + public override async Task Column_collection_projection_from_top_level(bool async) { await base.Column_collection_projection_from_top_level(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index 90be08ddd74..a0641afdf81 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -1253,6 +1253,18 @@ public override async Task Column_collection_SelectMany(bool async) (await Assert.ThrowsAsync( () => base.Column_collection_SelectMany(async))).Message); + public override async Task Column_collection_SelectMany_with_filter(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Column_collection_SelectMany_with_filter(async))).Message); + + public override async Task Column_collection_SelectMany_with_Select_to_anonymous_type(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Column_collection_SelectMany_with_Select_to_anonymous_type(async))).Message); + public override async Task Column_collection_projection_from_top_level(bool async) { await base.Column_collection_projection_from_top_level(async);