diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs index 6d0a69c0d10..cfe7205c340 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs @@ -60,6 +60,7 @@ public virtual Expression Translate(SelectExpression selectExpression, Expressio _projectionMembers.Push(new ProjectionMember()); var result = Visit(expression); + if (result == QueryCompilationContext.NotTranslatedExpression) { _clientEval = true; @@ -329,7 +330,7 @@ UnaryExpression unaryExpression Expression.Convert(Expression.Convert(entityProjection, typeof(object)), typeof(ValueBuffer)), nullable: true); - case ObjectArrayProjectionExpression objectArrayProjectionExpression: + case ObjectArrayAccessExpression objectArrayProjectionExpression: { var innerShaperExpression = new StructuralTypeShaperExpression( navigation.TargetEntityType, @@ -487,6 +488,9 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp var innerEntityProjection = shaperExpression.ValueBufferExpression switch { + EntityProjectionExpression entityProjection + => entityProjection, + ProjectionBindingExpression innerProjectionBindingExpression => (EntityProjectionExpression)_selectExpression.Projection[innerProjectionBindingExpression.Index!.Value].Expression, @@ -518,26 +522,27 @@ UnaryExpression unaryExpression switch (navigationProjection) { - case EntityProjectionExpression entityProjection: - return new StructuralTypeShaperExpression( - navigation.TargetEntityType, - Expression.Convert(Expression.Convert(entityProjection, typeof(object)), typeof(ValueBuffer)), - nullable: true); + case StructuralTypeShaperExpression shaper when navigation.IsCollection: + var objectArrayAccessExpression = shaper.ValueBufferExpression as ObjectArrayAccessExpression; + Check.DebugAssert(objectArrayAccessExpression is not null, "Expected ObjectArrayAccessExpression"); - case ObjectArrayProjectionExpression objectArrayProjectionExpression: - { var innerShaperExpression = new StructuralTypeShaperExpression( navigation.TargetEntityType, Expression.Convert( - Expression.Convert(objectArrayProjectionExpression.InnerProjection, typeof(object)), typeof(ValueBuffer)), + Expression.Convert(objectArrayAccessExpression.InnerProjection, typeof(object)), typeof(ValueBuffer)), nullable: true); return new CollectionShaperExpression( - objectArrayProjectionExpression, + objectArrayAccessExpression, innerShaperExpression, navigation, innerShaperExpression.StructuralType.ClrType); - } + + case StructuralTypeShaperExpression shaper: + return new StructuralTypeShaperExpression( + shaper.StructuralType, + Expression.Convert(Expression.Convert(shaper.ValueBufferExpression, typeof(object)), typeof(ValueBuffer)), + shaper.IsNullable); default: throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCallExpression.Print())); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs index ec4b83b0552..b4fe51bb07c 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs @@ -49,7 +49,7 @@ public virtual CosmosSqlQuery GetSqlQuery( /// protected override Expression VisitEntityProjection(EntityProjectionExpression entityProjectionExpression) { - Visit(entityProjectionExpression.AccessExpression); + Visit(entityProjectionExpression.Object); return entityProjectionExpression; } @@ -80,18 +80,52 @@ protected override Expression VisitExists(ExistsExpression existsExpression) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override Expression VisitArray(ArrayExpression arrayExpression) + protected override Expression VisitObjectArray(ObjectArrayExpression objectArrayExpression) + { + GenerateArray(objectArrayExpression.Subquery); + return objectArrayExpression; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitScalarArray(ScalarArrayExpression scalarArrayExpression) + { + GenerateArray(scalarArrayExpression.Subquery); + return scalarArrayExpression; + } + + private void GenerateArray(SelectExpression subquery) { _sqlBuilder.AppendLine("ARRAY("); using (_sqlBuilder.Indent()) { - Visit(arrayExpression.Subquery); + Visit(subquery); } _sqlBuilder.Append(")"); + } - return arrayExpression; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitObjectArrayAccess(ObjectArrayAccessExpression objectArrayAccessExpression) + { + Visit(objectArrayAccessExpression.Object); + + _sqlBuilder + .Append("[\"") + .Append(objectArrayAccessExpression.PropertyName) + .Append("\"]"); + + return objectArrayAccessExpression; } /// @@ -100,11 +134,14 @@ protected override Expression VisitArray(ArrayExpression arrayExpression) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override Expression VisitObjectArrayProjection(ObjectArrayProjectionExpression objectArrayProjectionExpression) + protected override Expression VisitObjectArrayIndex(ObjectArrayIndexExpression objectArrayIndexExpression) { - _sqlBuilder.Append(objectArrayProjectionExpression.ToString()); + Visit(objectArrayIndexExpression.Array); + _sqlBuilder.Append("["); + Visit(objectArrayIndexExpression.Index); + _sqlBuilder.Append("]"); - return objectArrayProjectionExpression; + return objectArrayIndexExpression; } /// @@ -113,11 +150,21 @@ protected override Expression VisitObjectArrayProjection(ObjectArrayProjectionEx /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override Expression VisitKeyAccess(KeyAccessExpression keyAccessExpression) + protected override Expression VisitScalarAccess(ScalarAccessExpression scalarAccessExpression) { - _sqlBuilder.Append(keyAccessExpression.ToString()); + Visit(scalarAccessExpression.Object); - return keyAccessExpression; + // TODO: Remove check once __jObject is translated to the access root in a better fashion. + // See issue #17670 and related issue #14121. + if (scalarAccessExpression.PropertyName.Length > 0) + { + _sqlBuilder + .Append("[\"") + .Append(scalarAccessExpression.PropertyName) + .Append("\"]"); + } + + return scalarAccessExpression; } /// @@ -128,7 +175,12 @@ protected override Expression VisitKeyAccess(KeyAccessExpression keyAccessExpres /// protected override Expression VisitObjectAccess(ObjectAccessExpression objectAccessExpression) { - _sqlBuilder.Append(objectAccessExpression.ToString()); + Visit(objectAccessExpression.Object); + + _sqlBuilder + .Append("[\"") + .Append(objectAccessExpression.PropertyName) + .Append("\"]"); return objectAccessExpression; } @@ -474,6 +526,30 @@ protected override Expression VisitSqlBinary(SqlBinaryExpression sqlBinaryExpres return sqlBinaryExpression; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitObjectBinary(ObjectBinaryExpression objectBinaryExpression) + { + var op = objectBinaryExpression.OperatorType switch + { + ExpressionType.Coalesce => " ?? ", + + _ => throw new UnreachableException($"Unsupported unary OperatorType: {objectBinaryExpression.OperatorType}") + }; + + _sqlBuilder.Append('('); + Visit(objectBinaryExpression.Left); + _sqlBuilder.Append(op); + Visit(objectBinaryExpression.Right); + _sqlBuilder.Append(')'); + + return objectBinaryExpression; + } + /// /// 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 @@ -755,6 +831,18 @@ protected virtual void GenerateIn(InExpression inExpression, bool negated) _sqlBuilder.Append(')'); } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitObjectFunction(ObjectFunctionExpression objectFunctionExpression) + { + GenerateFunction(objectFunctionExpression.Name, objectFunctionExpression.Arguments); + return objectFunctionExpression; + } + /// /// 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 @@ -763,12 +851,16 @@ protected virtual void GenerateIn(InExpression inExpression, bool negated) /// protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExpression) { - _sqlBuilder.Append(sqlFunctionExpression.Name); + GenerateFunction(sqlFunctionExpression.Name, sqlFunctionExpression.Arguments); + return sqlFunctionExpression; + } + + private void GenerateFunction(string name, IReadOnlyList arguments) + { + _sqlBuilder.Append(name); _sqlBuilder.Append('('); - GenerateList(sqlFunctionExpression.Arguments, e => Visit(e)); + GenerateList(arguments, e => Visit(e)); _sqlBuilder.Append(')'); - - return sqlFunctionExpression; } private sealed class ParameterNameGenerator diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs index ce672f0098d..8d56fcb8de1 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs @@ -22,7 +22,7 @@ public static class CosmosQueryUtils public static bool TryConvertToArray( ShapedQueryExpression source, ITypeMappingSource typeMappingSource, - [NotNullWhen(true)] out SqlExpression? array, + [NotNullWhen(true)] out Expression? array, bool ignoreOrderings = false) => TryConvertToArray(source, typeMappingSource, out array, out _, ignoreOrderings); @@ -35,8 +35,8 @@ public static bool TryConvertToArray( public static bool TryConvertToArray( ShapedQueryExpression source, ITypeMappingSource typeMappingSource, - [NotNullWhen(true)] out SqlExpression? array, - [NotNullWhen(true)] out SqlExpression? projection, + [NotNullWhen(true)] out Expression? array, + [NotNullWhen(true)] out Expression? projection, bool ignoreOrderings = false) { if (TryExtractBareArray(source, out array, out var projectedScalar, ignoreOrderings)) @@ -53,12 +53,21 @@ public static bool TryConvertToArray( // TODO: Should the type be an array, or enumerable/queryable? var arrayClrType = projection.Type.MakeArrayType(); - // TODO: Temporary hack - need to perform proper derivation of the array type mapping from the element (e.g. for - // value conversion). - var arrayTypeMapping = typeMappingSource.FindMapping(arrayClrType); - array = new ArrayExpression(subquery, arrayClrType, arrayTypeMapping); - return true; + switch (projection) + { + case StructuralTypeShaperExpression: + array = new ObjectArrayExpression(subquery, arrayClrType); + return true; + + // TODO: Temporary hack - need to perform proper derivation of the array type mapping from the element (e.g. for + // value conversion). #34026. + case SqlExpression sqlExpression: + var arrayTypeMapping = typeMappingSource.FindMapping(arrayClrType); + + array = new ScalarArrayExpression(subquery, arrayClrType, arrayTypeMapping); + return true; + } } array = null; @@ -74,7 +83,7 @@ public static bool TryConvertToArray( /// public static bool TryExtractBareArray( ShapedQueryExpression source, - [NotNullWhen(true)] out SqlExpression? array, + [NotNullWhen(true)] out Expression? array, bool ignoreOrderings = false) => TryExtractBareArray(source, out array, out _, ignoreOrderings); @@ -86,8 +95,25 @@ public static bool TryExtractBareArray( /// public static bool TryExtractBareArray( ShapedQueryExpression source, - [NotNullWhen(true)] out SqlExpression? array, - [NotNullWhen(true)] out SqlExpression? projectedScalarReference, + [NotNullWhen(true)] out Expression? array, + [NotNullWhen(true)] out Expression? projection, + bool ignoreOrderings = false) + => TryExtractBareArray(source, out array, out projection, out _, out var boundMember, ignoreOrderings) + && boundMember is null; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static bool TryExtractBareArray( + ShapedQueryExpression source, + [NotNullWhen(true)] out Expression? array, + [NotNullWhen(true)] out Expression? projection, + out StructuralTypeShaperExpression? projectedStructuralTypeShaper, + // On this, see comment in CosmosQueryableMethodTranslatingEV.VisitMethod() + out Expression? boundMember, bool ignoreOrderings = false) { if (source.QueryExpression is not SelectExpression @@ -98,36 +124,73 @@ public static bool TryExtractBareArray( Offset: null } select || (!ignoreOrderings && select.Orderings.Count > 0) - || !TryGetProjection(source, out var projection) - || projection is not ScalarReferenceExpression scalarReferenceProjection) + || !TryGetProjection(source, out projection)) { - array = null; - projectedScalarReference = null; - return false; + goto ExitFailed; } - switch (source.QueryExpression) + // On this, see comment in CosmosQueryableMethodTranslatingEV.VisitMethod() + switch (projection) { - // For properties: SELECT i FROM i IN c.SomeArray - // So just match any SelectExpression with IN. - case SelectExpression { - Sources: [{ WithIn: true, ContainerExpression: SqlExpression a } arraySource], - } when scalarReferenceProjection.Name == arraySource.Alias: + case ScalarAccessExpression { Object: ObjectReferenceExpression objectRef } scalarAccess: + projection = objectRef; + boundMember = scalarAccess; + break; + + case ObjectAccessExpression { Object: ObjectReferenceExpression objectRef } objectAccess: + projection = objectRef; + boundMember = objectAccess; + break; + + default: + boundMember = null; + break; + } + + if (projection is StructuralTypeShaperExpression shaper) + { + projectedStructuralTypeShaper = shaper; + projection = shaper.ValueBufferExpression; + if (projection is ProjectionBindingExpression { ProjectionMember: ProjectionMember projectionMember } + && select.GetMappedProjection(projectionMember) is EntityProjectionExpression entityProjection) { - array = a; - projectedScalarReference = scalarReferenceProjection; - return true; + projection = entityProjection.Object; } + } + else + { + projectedStructuralTypeShaper = null; + } - // For inline and parameter arrays the case is unfortunately more difficult; Cosmos doesn't allow SELECT i FROM i IN [1,2,3] - // or SELECT i FROM i IN @p. - // So we instead generate SELECT i FROM i IN (SELECT VALUE [1,2,3]), which needs to be match here. - case SelectExpression + var projectedReferenceName = projection switch + { + ScalarReferenceExpression { Name: var name } => name, + ObjectReferenceExpression { Name: var name } => name, + + _ => null + }; + + if (projectedReferenceName is null) + { + goto ExitFailed; + } + + switch (select) + { + // 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 + // (e.g. SELECT VALUE i FROM i IN c.SomeArray). + // 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]), and that needs to be + // match here. + case { Sources: [ { WithIn: true, + Alias: var sourceAlias, ContainerExpression: SelectExpression { Sources: [], @@ -137,22 +200,32 @@ public static bool TryExtractBareArray( Orderings: [], IsDistinct: false, UsesSingleValueProjection: true, - Projection: [{Expression: SqlExpression a}] + Projection: [{ Expression: var a }] }, - } arraySource + } ] - } when scalarReferenceProjection.Name == arraySource.Alias: + } when projectedReferenceName == sourceAlias: { array = a; - projectedScalarReference = scalarReferenceProjection; return true; } - default: - array = null; - projectedScalarReference = null; - return false; + // 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 }] } + when projectedReferenceName == sourceAlias: + { + array = a; + return true; + } } + + ExitFailed: + array = null; + projection = null; + projectedStructuralTypeShaper = null; + boundMember = null; + return false; } /// @@ -163,7 +236,7 @@ public static bool TryExtractBareArray( /// public static bool TryGetProjection( ShapedQueryExpression shapedQueryExpression, - [NotNullWhen(true)] out SqlExpression? projectedScalarReference) + [NotNullWhen(true)] out Expression? projection) { var shaperExpression = shapedQueryExpression.ShaperExpression; // No need to check ConvertChecked since this is convert node which we may have added during projection @@ -174,15 +247,23 @@ public static bool TryGetProjection( shaperExpression = unaryExpression.Operand; } - if (shapedQueryExpression.QueryExpression is SelectExpression selectExpression - && shaperExpression is ProjectionBindingExpression { ProjectionMember: ProjectionMember projectionMember } - && selectExpression.GetMappedProjection(projectionMember) is SqlExpression projection) + switch (shaperExpression) { - projectedScalarReference = projection; - return true; + case ProjectionBindingExpression { ProjectionMember: ProjectionMember projectionMember } + when shapedQueryExpression.QueryExpression is SelectExpression selectExpression: + { + projection = selectExpression.GetMappedProjection(projectionMember); + return true; + } + + case StructuralTypeShaperExpression shaper: + { + projection = shaper; + return true; + } } - projectedScalarReference = null; + projection = null; return false; } } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index 5f017ec2e84..42f6403f082 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -233,6 +233,62 @@ static bool ExtractPartitionKeyFromPredicate( } } + /// + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) + { + var method = methodCallExpression.Method; + if (method.DeclaringType == typeof(Queryable)) + { + switch (methodCallExpression.Method.Name) + { + // The following is a bad hack to account for https://github.com/dotnet/efcore/issues/32957#issuecomment-2165864086. + // Basically for the query form Where(b => b.Posts.GetElementAt(0).Id == 1), nav expansion moves the property access + // forward, generating Where(b => b.Posts.Select(p => p.Id).GetElementAt(0)); unfortunately that means that GetElementAt() + // over a bare array in Cosmos doesn't get translated to a simple indexer as it should (b["Posts"][0].Id), since the + // reordering messes things up. + case nameof(Queryable.ElementAt) or nameof(Queryable.ElementAtOrDefault) + when methodCallExpression.Arguments[0] is MethodCallExpression + { + Method: { Name: "Select", IsGenericMethod: true } + } innerMethodCall + && method.GetGenericMethodDefinition() is var genericDefinition + && (genericDefinition == QueryableMethods.ElementAt || genericDefinition == QueryableMethods.ElementAtOrDefault) + && innerMethodCall.Method.GetGenericMethodDefinition() == QueryableMethods.Select: + { + var returnDefault = method.Name == nameof(Queryable.ElementAtOrDefault); + if (Visit(innerMethodCall) is ShapedQueryExpression translatedSelect + && CosmosQueryUtils.TryExtractBareArray(translatedSelect, out _, out _, out _, out var boundMember) + && boundMember is IAccessExpression { PropertyName: string boundPropertyName } + && Visit(innerMethodCall.Arguments[0]) is ShapedQueryExpression innerSource + && TranslateElementAtOrDefault( + innerSource, methodCallExpression.Arguments[1], returnDefault) is ShapedQueryExpression elementAtTranslation) + { +#pragma warning disable EF1001 // Internal EF Core API usage. + var translation = _sqlTranslator.Translate( + Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions.CreateEFPropertyExpression( + elementAtTranslation.ShaperExpression, + elementAtTranslation.ShaperExpression.Type, + boundMember.Type, + boundPropertyName, + makeNullable: true)); +#pragma warning restore EF1001 // Internal EF Core API usage. + + if (translation is not null) + { + var finalShapedQuery = CreateShapedQueryExpression(new SelectExpression(translation), boundMember.Type); + return finalShapedQuery.UpdateResultCardinality( + returnDefault ? ResultCardinality.SingleOrDefault : ResultCardinality.Single); + } + } + + break; + } + } + } + + return base.VisitMethodCall(methodCallExpression); + } + /// protected override Expression VisitExtension(Expression extensionExpression) { @@ -317,6 +373,22 @@ private ShapedQueryExpression CreateShapedQueryExpression(IEntityType entityType false)); } + private ShapedQueryExpression CreateShapedQueryExpression(SelectExpression select, Type elementClrType) + { + var shaperExpression = (Expression)new ProjectionBindingExpression( + select, new ProjectionMember(), elementClrType.MakeNullable()); + if (shaperExpression.Type != elementClrType) + { + Check.DebugAssert( + elementClrType.MakeNullable() == shaperExpression.Type, + "expression.Type must be nullable of targetType"); + + shaperExpression = Expression.Convert(shaperExpression, elementClrType); + } + + return new ShapedQueryExpression(select, shaperExpression); + } + /// /// 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 @@ -345,6 +417,18 @@ private ShapedQueryExpression CreateShapedQueryExpression(IEntityType entityType source = translatedSource; } + // Simplify x.Array.Any() => ARRAY_LENGTH(x.Array) > 0 instead of (EXISTS(SELECT 1 FROM i IN x.Array)) + if (CosmosQueryUtils.TryExtractBareArray(source, out var array, ignoreOrderings: true)) + { + var simplifiedTranslation = _sqlExpressionFactory.GreaterThan( + _sqlExpressionFactory.Function( + "ARRAY_LENGTH", new[] { array }, typeof(int), _typeMappingSource.FindMapping(typeof(int))), + _sqlExpressionFactory.Constant(0)); + var select = new SelectExpression(simplifiedTranslation); + + return source.Update(select, new ProjectionBindingExpression(select, new ProjectionMember(), typeof(int))); + } + var subquery = (SelectExpression)source.QueryExpression; subquery.ClearProjection(); subquery.ApplyProjection(); @@ -419,6 +503,7 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou { // Simplify x.Array.Contains[1] => ARRAY_CONTAINS(x.Array, 1) insert of IN+subquery if (CosmosQueryUtils.TryExtractBareArray(source, out var array, ignoreOrderings: true) + && array is SqlExpression scalarArray // TODO: Contains over arrays of structural types, #34027 && TranslateExpression(item) is SqlExpression translatedItem) { if (array is ArrayConstantExpression arrayConstant) @@ -427,8 +512,8 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou return source.Update(new SelectExpression(inExpression), source.ShaperExpression); } - (translatedItem, array) = _sqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(translatedItem, array); - var simplifiedTranslation = _sqlExpressionFactory.Function("ARRAY_CONTAINS", new[] { array, translatedItem }, typeof(bool)); + (translatedItem, scalarArray) = _sqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(translatedItem, scalarArray); + var simplifiedTranslation = _sqlExpressionFactory.Function("ARRAY_CONTAINS", [scalarArray, translatedItem], typeof(bool)); return source.UpdateQueryExpression(new SelectExpression(simplifiedTranslation)); } @@ -448,45 +533,7 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override ShapedQueryExpression? TranslateCount(ShapedQueryExpression source, LambdaExpression? predicate) - { - // Simplify x.Array.Count() => ARRAY_LENGTH(x.Array) instead of (SELECT COUNT(1) FROM i IN x.Array)) - if (predicate is null - && CosmosQueryUtils.TryExtractBareArray(source, out var array, ignoreOrderings: true)) - { - var simplifiedTranslation = _sqlExpressionFactory.Function("ARRAY_LENGTH", new[] { array }, typeof(int)); - return source.UpdateQueryExpression(new SelectExpression(simplifiedTranslation)); - } - - var selectExpression = (SelectExpression)source.QueryExpression; - if (selectExpression.IsDistinct - || selectExpression.Limit != null - || selectExpression.Offset != null) - { - return null; - } - - if (predicate != null) - { - if (TranslateWhere(source, predicate) is not ShapedQueryExpression translatedSource) - { - return null; - } - - source = translatedSource; - } - - var translation = _sqlExpressionFactory.ApplyDefaultTypeMapping( - _sqlExpressionFactory.Function("COUNT", new[] { _sqlExpressionFactory.Constant(1) }, typeof(int))); - - var projectionMapping = new Dictionary { { new ProjectionMember(), translation } }; - - selectExpression.ClearOrdering(); - selectExpression.ReplaceProjectionMapping(projectionMapping); - return source.UpdateShaperExpression( - Expression.Convert( - new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(int?)), - typeof(int))); - } + => TranslateCountLongCount(source, predicate, typeof(int)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -532,28 +579,65 @@ protected override ShapedQueryExpression TranslateDistinct(ShapedQueryExpression // subquery+OFFSET (which isn't supported by Cosmos). // Even if the source is a full query (not a bare array), convert it to an array via the Cosmos ARRAY() operator; we do this // only in subqueries, because Cosmos supports OFFSET/LIMIT at the top-level but not in subqueries. - var array = CosmosQueryUtils.TryExtractBareArray(source, out var a, out var projectedScalarReference) + var array = CosmosQueryUtils.TryExtractBareArray( + source, out var a, out var projection, out var projectedStructuralTypeShaper, out _) ? a - : _subquery && CosmosQueryUtils.TryConvertToArray(source, _typeMappingSource, out a, out projectedScalarReference) + : _subquery && CosmosQueryUtils.TryConvertToArray(source, _typeMappingSource, out a, out projection) ? a : null; // Simplify x.Array[1] => x.Array[1] (using the Cosmos array subscript operator) instead of a subquery with LIMIT/OFFSET - if (array is SqlExpression scalarArray) // TODO: ElementAt over arrays of structural types + switch (array) { - SqlExpression translation = _sqlExpressionFactory.ArrayIndex( - array, translatedIndex, projectedScalarReference!.Type, projectedScalarReference.TypeMapping); - - if (returnDefault) + // ElementAtOrDefault over an array of scalars + case SqlExpression scalarArray when projection is SqlExpression element: { - translation = _sqlExpressionFactory.CoalesceUndefined( - translation, TranslateExpression(translation.Type.GetDefaultValueConstant())!); + SqlExpression translation = _sqlExpressionFactory.ArrayIndex( + scalarArray, translatedIndex, element.Type, element.TypeMapping); + + // ElementAt may access indexes beyond the end of the array; Cosmos returns undefined for those cases. + // If ElementAtOrDefault is used, add the Cosmos undefined-coalescing operator (??) to return a default value instead. + if (returnDefault) + { + translation = _sqlExpressionFactory.CoalesceUndefined( + translation, TranslateExpression(translation.Type.GetDefaultValueConstant())!); + } + + var translatedSelect = new SelectExpression(translation); + return source.Update( + translatedSelect, + new ProjectionBindingExpression(translatedSelect, new ProjectionMember(), element.Type)); } - return source.UpdateQueryExpression(new SelectExpression(translation)); + // ElementAtOrDefault over an array of structural types + case not null when projectedStructuralTypeShaper is not null: + { + Expression translation = new ObjectArrayIndexExpression(array, translatedIndex, projectedStructuralTypeShaper.Type); + + // ElementAt may access indexes beyond the end of the array; Cosmos returns undefined for those cases. + // If ElementAtOrDefault is used, add the Cosmos undefined-coalescing operator (??) to return a default value instead. + if (returnDefault) + { + // TODO: The following uses SqlConstantExpression as a hack to produce a null for the structural type (#33999) + translation = new ObjectBinaryExpression( + ExpressionType.Coalesce, + translation, + new SqlConstantExpression(Expression.Constant(null, typeof(object)), _typeMappingSource.FindMapping(typeof(int))), + translation.Type); + } + + var translatedSelect = + new SelectExpression(new EntityProjectionExpression(translation, (IEntityType)projectedStructuralTypeShaper.StructuralType)); + return source.Update( + translatedSelect, + new StructuralTypeShaperExpression( + projectedStructuralTypeShaper.StructuralType, + new ProjectionBindingExpression(translatedSelect, new ProjectionMember(), typeof(ValueBuffer)), + nullable: true)); + } } - // Translate using OFFSET/LIMIT, except in subqueries where it isn't supported + // Simplification to indexing failed, translate using OFFSET/LIMIT, except in subqueries where it isn't supported. if (_subquery) { AddTranslationErrorDetails(CosmosStrings.LimitOffsetNotSupportedInSubqueries); @@ -732,36 +816,7 @@ protected override ShapedQueryExpression TranslateDistinct(ShapedQueryExpression /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override ShapedQueryExpression? TranslateLongCount(ShapedQueryExpression source, LambdaExpression? predicate) - { - var selectExpression = (SelectExpression)source.QueryExpression; - if (selectExpression.IsDistinct - || selectExpression.Limit != null - || selectExpression.Offset != null) - { - return null; - } - - if (predicate != null) - { - if (TranslateWhere(source, predicate) is not ShapedQueryExpression translatedSource) - { - return null; - } - - source = translatedSource; - } - - var translation = _sqlExpressionFactory.ApplyDefaultTypeMapping( - _sqlExpressionFactory.Function("COUNT", new[] { _sqlExpressionFactory.Constant(1) }, typeof(long))); - var projectionMapping = new Dictionary { { new ProjectionMember(), translation } }; - - selectExpression.ClearOrdering(); - selectExpression.ReplaceProjectionMapping(projectionMapping); - return source.UpdateShaperExpression( - Expression.Convert( - new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(long?)), - typeof(long))); - } + => TranslateCountLongCount(source, predicate, typeof(long)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -1022,23 +1077,47 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s // subquery+OFFSET (which isn't supported by Cosmos). // Even if the source is a full query (not a bare array), convert it to an array via the Cosmos ARRAY() operator; we do this // only in subqueries, because Cosmos supports OFFSET/LIMIT at the top-level but not in subqueries. - var array = CosmosQueryUtils.TryExtractBareArray(source, out var a, out var projectedScalarReference) + var array = CosmosQueryUtils.TryExtractBareArray( + source, out var a, out var projection, out var projectedStructuralTypeShaper, out _) ? a - : _subquery && CosmosQueryUtils.TryConvertToArray(source, _typeMappingSource, out a, out projectedScalarReference) + : _subquery && CosmosQueryUtils.TryConvertToArray(source, _typeMappingSource, out a, out projection) ? a : null; - if (array is SqlExpression scalarArray) // TODO: Take over arrays of structural types + switch (array) { - var slice = _sqlExpressionFactory.Function( - "ARRAY_SLICE", [scalarArray, translatedCount], scalarArray.Type, scalarArray.TypeMapping); - - // TODO: Proper alias management (#33894). Ideally reach into the source of the original SelectExpression and use that alias. - select = SelectExpression.CreateForPrimitiveCollection( - new SourceExpression(slice, "i", withIn: true), - projectedScalarReference!.Type, - projectedScalarReference.TypeMapping!); - return source.UpdateQueryExpression(select); + // ElementAtOrDefault over an array of scalars + case SqlExpression scalarArray when projection is SqlExpression element: + { + var slice = _sqlExpressionFactory.Function( + "ARRAY_SLICE", [scalarArray, translatedCount], scalarArray.Type, scalarArray.TypeMapping); + + // TODO: Proper alias management (#33894). Ideally reach into the source of the original SelectExpression and use that alias. + var translatedSelect = SelectExpression.CreateForCollection( + slice, + "i", + new ScalarReferenceExpression("i", element.Type, element.TypeMapping)); + return source.UpdateQueryExpression(translatedSelect); + } + + // ElementAtOrDefault over an array os structural types + case not null when projectedStructuralTypeShaper is not null: + { + // TODO: Proper alias management (#33894). + var slice = new ObjectFunctionExpression("ARRAY_SLICE", [array, translatedCount], projectedStructuralTypeShaper.Type); + var translatedSelect = SelectExpression.CreateForCollection( + slice, + "i", + new EntityProjectionExpression( + new ObjectReferenceExpression((IEntityType)projectedStructuralTypeShaper.StructuralType, "i"), + (IEntityType)projectedStructuralTypeShaper.StructuralType)); + return source.Update( + translatedSelect, + new StructuralTypeShaperExpression( + projectedStructuralTypeShaper.StructuralType, + new ProjectionBindingExpression(translatedSelect, new ProjectionMember(), typeof(ValueBuffer)), + nullable: true)); + } } // Translate using OFFSET/LIMIT, except in subqueries where it isn't supported @@ -1119,27 +1198,56 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s // subquery+LIMIT (which isn't supported by Cosmos). // Even if the source is a full query (not a bare array), convert it to an array via the Cosmos ARRAY() operator; we do this // only in subqueries, because Cosmos supports OFFSET/LIMIT at the top-level but not in subqueries. - var array = CosmosQueryUtils.TryExtractBareArray(source, out var a, out var projectedScalarReference) + var array = CosmosQueryUtils.TryExtractBareArray( + source, out var a, out var projection, out var projectedStructuralTypeShaper, out _) ? a - : _subquery && CosmosQueryUtils.TryConvertToArray(source, _typeMappingSource, out a, out projectedScalarReference) + : _subquery && CosmosQueryUtils.TryConvertToArray(source, _typeMappingSource, out a, out projection) ? a : null; - if (array is SqlExpression scalarArray) // TODO: Take over arrays of structural types + switch (array) { - // Take() is composed over Skip(), combine the two together to a single ARRAY_SLICE() - var slice = array is SqlFunctionExpression { Name: "ARRAY_SLICE", Arguments: [var nestedArray, var skipCount] } previousSlice - ? previousSlice.Update([nestedArray, skipCount, translatedCount]) - : _sqlExpressionFactory.Function( - "ARRAY_SLICE", [scalarArray, TranslateExpression(Expression.Constant(0))!, translatedCount], scalarArray.Type, - scalarArray.TypeMapping); - - // TODO: Proper alias management (#33894). Ideally reach into the source of the original SelectExpression and use that alias. - select = SelectExpression.CreateForPrimitiveCollection( - new SourceExpression(slice, "i", withIn: true), - projectedScalarReference!.Type, - projectedScalarReference.TypeMapping!); - return source.UpdateQueryExpression(select); + // ElementAtOrDefault over an array of scalars + case SqlExpression scalarArray when projection is SqlExpression element: + { + // Take() is composed over Skip(), combine the two together to a single ARRAY_SLICE() + var slice = array is SqlFunctionExpression { Name: "ARRAY_SLICE", Arguments: [var nestedArray, var skipCount] } previousSlice + ? previousSlice.Update([nestedArray, skipCount, translatedCount]) + : _sqlExpressionFactory.Function( + "ARRAY_SLICE", [scalarArray, TranslateExpression(Expression.Constant(0))!, translatedCount], scalarArray.Type, + scalarArray.TypeMapping); + + // TODO: Proper alias management (#33894). Ideally reach into the source of the original SelectExpression and use that alias. + select = SelectExpression.CreateForCollection( + slice, + "i", + new ScalarReferenceExpression("i", element.Type, element.TypeMapping)); + return source.UpdateQueryExpression(select); + } + + // ElementAtOrDefault over an array os structural types + case not null when projectedStructuralTypeShaper is not null: + { + // TODO: Proper alias management (#33894). + // Take() is composed over Skip(), combine the two together to a single ARRAY_SLICE() + var slice = array is ObjectFunctionExpression { Name: "ARRAY_SLICE", Arguments: [var nestedArray, var skipCount] } previousSlice + ? previousSlice.Update([nestedArray, skipCount, translatedCount]) + : new ObjectFunctionExpression( + "ARRAY_SLICE", [array, TranslateExpression(Expression.Constant(0))!, translatedCount], projectedStructuralTypeShaper.Type); + + var translatedSelect = SelectExpression.CreateForCollection( + slice, + "i", + new EntityProjectionExpression( + new ObjectReferenceExpression((IEntityType)projectedStructuralTypeShaper.StructuralType, "i"), + (IEntityType)projectedStructuralTypeShaper.StructuralType)); + return source.Update( + translatedSelect, + new StructuralTypeShaperExpression( + projectedStructuralTypeShaper.StructuralType, + new ProjectionBindingExpression(translatedSelect, new ProjectionMember(), typeof(ValueBuffer)), + nullable: true)); + } } // Translate using OFFSET/LIMIT, except in subqueries where it isn't supported @@ -1365,21 +1473,50 @@ when methodCallExpression.TryGetIndexerArguments(_queryCompilationContext.Model, /// protected override ShapedQueryExpression? TranslateMemberAccess(Expression source, MemberIdentity member) { - // TODO: the below immediately wraps the JSON array property in a subquery (SELECT VALUE i FROM i IN c.Array). - // TODO: This isn't strictly necessary, as c.Array can be referenced directly; however, that would mean producing a - // TODO: ShapedQueryExpression that doesn't wrap a SelectExpression, but rather a KeyAccessExpression directly; this isn't currently - // TODO: supported. - // Attempt to translate access into a primitive collection property - if (_sqlTranslator.TryBindMember(_sqlTranslator.Visit(source), member, out var translatedExpression, out var property) - && property is IProperty { IsPrimitiveCollection: true } - && translatedExpression is SqlExpression sqlExpression - && WrapPrimitiveCollectionAsShapedQuery( - sqlExpression, - sqlExpression.Type.GetSequenceType(), - sqlExpression.TypeMapping!.ElementTypeMapping!) is { } primitiveCollectionTranslation) - { - return primitiveCollectionTranslation; + if (_sqlTranslator.TryBindMember( + _sqlTranslator.Visit(source), + member, + out var translatedExpression, + out var property, + wrapResultExpressionInReferenceExpression: false)) + { + // TODO: TryBindMember returns EntityReferenceExpression, which is internal to SqlTranslatingEV. + // Maybe have it return the StructuralTypeShaperExpression instead, and only when binding from within SqlTranslatingEV, + // wrap with ERE? + // Check: how is this currently working in relational? + switch (translatedExpression) + { + case StructuralTypeShaperExpression shaper when property is INavigation { IsCollection: true }: + { + // TODO: Alias management #33894 + var targetEntityType = (IEntityType)shaper.StructuralType; + var sourceAlias = "t"; + var projection = new EntityProjectionExpression( + new ObjectReferenceExpression(targetEntityType, sourceAlias), targetEntityType); + var select = SelectExpression.CreateForCollection( + shaper.ValueBufferExpression, + sourceAlias, + projection); + return CreateShapedQueryExpression(targetEntityType, select); + } + + // TODO: Collection of complex type (#31253) + + // Note that non-collection navigations/complex types are handled in CosmosSqlTranslatingExpressionVisitor + // (no collection -> no queryable operators) + + case SqlExpression sqlExpression when property is IProperty { IsPrimitiveCollection: true }: + { + var elementClrType = sqlExpression.Type.GetSequenceType(); + // TODO: Do proper alias management: #33894 + var select = SelectExpression.CreateForCollection( + sqlExpression, + "i", + new ScalarReferenceExpression("i", elementClrType, sqlExpression.TypeMapping!.ElementTypeMapping!)); + return CreateShapedQueryExpression(select, elementClrType); + } + } } return null; @@ -1409,25 +1546,19 @@ when methodCallExpression.TryGetIndexerArguments(_queryCompilationContext.Model, translatedItems[i] = translatedItem; } - // TODO: Do we need full-on type mapping inference like in relational? - // TODO: The following currently just gets the type mapping from the CLR type, which ignores e.g. value converters on - // TODO: properties compared against + // TODO: Temporary hack - need to perform proper derivation of the array type mapping from the element (e.g. for + // value conversion). #34026. var elementClrType = inlineQueryRootExpression.ElementType; var elementTypeMapping = _typeMappingSource.FindMapping(elementClrType)!; var arrayTypeMapping = _typeMappingSource.FindMapping(elementClrType.MakeArrayType()); // TODO: IEnumerable? var inlineArray = new ArrayConstantExpression(elementClrType, translatedItems, arrayTypeMapping); - // Unfortunately, Cosmos doesn't support selecting directly from an inline array: SELECT i FROM i IN [1,2,3] (syntax error) - // We must wrap the inline array in a subquery: SELECT VALUE i FROM (SELECT VALUE [1,2,3]) - var innerSelect = new SelectExpression( - [new ProjectionExpression(inlineArray, null!)], - sources: [], - orderings: []) - { - UsesSingleValueProjection = true - }; - - return WrapPrimitiveCollectionAsShapedQuery(innerSelect, elementClrType, elementTypeMapping); + // TODO: Do proper alias management: #33894 + var select = SelectExpression.CreateForCollection( + inlineArray, + "i", + new ScalarReferenceExpression("i", elementClrType, elementTypeMapping)); + return CreateShapedQueryExpression(select, elementClrType); } /// @@ -1445,52 +1576,68 @@ [new ProjectionExpression(inlineArray, null!)], return null; } - // TODO: Do we need full-on type mapping inference like in relational? - // TODO: The following currently just gets the type mapping from the CLR type, which ignores e.g. value converters on - // TODO: properties compared against + // TODO: Temporary hack - need to perform proper derivation of the array type mapping from the element (e.g. for + // value conversion). #34026. var elementClrType = parameterQueryRootExpression.ElementType; var arrayTypeMapping = _typeMappingSource.FindMapping(elementClrType.MakeArrayType()); // TODO: IEnumerable? var elementTypeMapping = _typeMappingSource.FindMapping(elementClrType)!; var sqlParameterExpression = new SqlParameterExpression(parameterQueryRootExpression.ParameterExpression, arrayTypeMapping); - // Unfortunately, Cosmos doesn't support selecting directly from an inline array: SELECT i FROM i IN [1,2,3] (syntax error) - // We must wrap the inline array in a subquery: SELECT VALUE i FROM (SELECT VALUE [1,2,3]) - var innerSelect = new SelectExpression( - [new ProjectionExpression(sqlParameterExpression, null!)], - sources: [], - orderings: []) - { - UsesSingleValueProjection = true - }; - - return WrapPrimitiveCollectionAsShapedQuery(innerSelect, elementClrType, elementTypeMapping); + // TODO: Do proper alias management: #33894 + var select = SelectExpression.CreateForCollection( + sqlParameterExpression, + "i", + new ScalarReferenceExpression("i", elementClrType, elementTypeMapping)); + return CreateShapedQueryExpression(select, elementClrType); } - private ShapedQueryExpression WrapPrimitiveCollectionAsShapedQuery( - Expression array, - Type elementClrType, - CoreTypeMapping elementTypeMapping) + #endregion Queryable collection support + + private ShapedQueryExpression? TranslateCountLongCount(ShapedQueryExpression source, LambdaExpression? predicate, Type returnType) { - // TODO: Do proper alias management: #33894 - var select = SelectExpression.CreateForPrimitiveCollection( - new SourceExpression(array, "i", withIn: true), - elementClrType, - elementTypeMapping); - var shaperExpression = (Expression)new ProjectionBindingExpression( - select, new ProjectionMember(), elementClrType.MakeNullable()); - if (shaperExpression.Type != elementClrType) + // Simplify x.Array.Count() => ARRAY_LENGTH(x.Array) instead of (SELECT COUNT(1) FROM i IN x.Array)) + if (predicate is null + && CosmosQueryUtils.TryExtractBareArray(source, out var array, ignoreOrderings: true)) { - Check.DebugAssert( - elementClrType.MakeNullable() == shaperExpression.Type, - "expression.Type must be nullable of targetType"); + var simplifiedTranslation = _sqlExpressionFactory.Function( + "ARRAY_LENGTH", new[] { array }, typeof(int), _typeMappingSource.FindMapping(typeof(int))); + var select = new SelectExpression(simplifiedTranslation); - shaperExpression = Expression.Convert(shaperExpression, elementClrType); + return source.Update(select, new ProjectionBindingExpression(select, new ProjectionMember(), typeof(int))); } - return new ShapedQueryExpression(select, shaperExpression); - } + var selectExpression = (SelectExpression)source.QueryExpression; - #endregion Queryable collection support + // TODO: Subquery pushdown, #33968 + if (selectExpression.IsDistinct + || selectExpression.Limit != null + || selectExpression.Offset != null) + { + return null; + } + + if (predicate != null) + { + if (TranslateWhere(source, predicate) is not ShapedQueryExpression translatedSource) + { + return null; + } + + source = translatedSource; + } + + var translation = _sqlExpressionFactory.ApplyDefaultTypeMapping( + _sqlExpressionFactory.Function("COUNT", new[] { _sqlExpressionFactory.Constant(1) }, typeof(int))); + + var projectionMapping = new Dictionary { { new ProjectionMember(), translation } }; + + selectExpression.ClearOrdering(); + selectExpression.ReplaceProjectionMapping(projectionMapping); + return source.UpdateShaperExpression( + Expression.Convert( + new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), returnType.MakeNullable()), + returnType)); + } private ShapedQueryExpression? TranslateSetOperation( ShapedQueryExpression source1, @@ -1500,18 +1647,33 @@ private ShapedQueryExpression WrapPrimitiveCollectionAsShapedQuery( { if (CosmosQueryUtils.TryConvertToArray(source1, _typeMappingSource, out var array1, out var projection1, ignoreOrderings) && CosmosQueryUtils.TryConvertToArray(source2, _typeMappingSource, out var array2, out var projection2, ignoreOrderings) - && projection1.Type == projection2.Type - && (projection1.TypeMapping ?? projection2.TypeMapping) is CoreTypeMapping typeMapping) + && projection1.Type == projection2.Type) { - var translation = _sqlExpressionFactory.Function(functionName, [array1, array2], projection1.Type, typeMapping); - var select = SelectExpression.CreateForPrimitiveCollection( - new SourceExpression(translation, "i", withIn: true), - projection1.Type, - typeMapping); - return source1.UpdateQueryExpression(select); + // Set operation over arrays of scalars + if (projection1 is SqlExpression sqlProjection1 + && projection2 is SqlExpression sqlProjection2 + && (sqlProjection1.TypeMapping ?? sqlProjection2.TypeMapping) is CoreTypeMapping typeMapping) + { + // TODO: Proper alias management (#33894). + var translation = _sqlExpressionFactory.Function(functionName, [array1, array2], projection1.Type, typeMapping); + var select = SelectExpression.CreateForCollection( + translation, "i", new ScalarReferenceExpression("i", projection1.Type, typeMapping)); + return source1.UpdateQueryExpression(select); + } + + // Set operation over arrays of structural types + if (source1.ShaperExpression is StructuralTypeShaperExpression { StructuralType: var structuralType1 } + && source2.ShaperExpression is StructuralTypeShaperExpression { StructuralType: var structuralType2 } + && structuralType1 == structuralType2) + { + // TODO: Proper alias management (#33894). + var translation = new ObjectFunctionExpression(functionName, [array1, array2], projection1.Type); + var select = SelectExpression.CreateForCollection( + translation, "i", new ObjectReferenceExpression((IEntityType)structuralType1, "i")); + return CreateShapedQueryExpression(select, structuralType1.ClrType); + } } - // TODO: can also handle subqueries via ARRAY() return null; } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs index fb9ab2e96f5..9595903e1f8 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs @@ -86,33 +86,38 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) } Expression innerAccessExpression; - if (projectionExpression is ObjectArrayProjectionExpression objectArrayProjectionExpression) + switch (projectionExpression) { - innerAccessExpression = objectArrayProjectionExpression.AccessExpression; - _projectionBindings[objectArrayProjectionExpression] = parameterExpression; - storeName ??= objectArrayProjectionExpression.Name; - } - else - { - var entityProjectionExpression = (EntityProjectionExpression)projectionExpression; - var accessExpression = entityProjectionExpression.AccessExpression; - _projectionBindings[accessExpression] = parameterExpression; - storeName ??= entityProjectionExpression.Name; - - switch (accessExpression) - { - case ObjectAccessExpression innerObjectAccessExpression: - innerAccessExpression = innerObjectAccessExpression.AccessExpression; - _ownerMappings[accessExpression] = - (innerObjectAccessExpression.Navigation.DeclaringEntityType, innerAccessExpression); - break; - case ObjectReferenceExpression: - innerAccessExpression = jObjectParameter; - break; - default: - throw new InvalidOperationException( - CoreStrings.TranslationFailed(binaryExpression.Print())); - } + case ObjectArrayAccessExpression objectArrayProjectionExpression: + innerAccessExpression = objectArrayProjectionExpression.Object; + _projectionBindings[objectArrayProjectionExpression] = parameterExpression; + storeName ??= objectArrayProjectionExpression.PropertyName; + break; + + case EntityProjectionExpression entityProjectionExpression: + var accessExpression = entityProjectionExpression.Object; + _projectionBindings[accessExpression] = parameterExpression; + storeName ??= entityProjectionExpression.PropertyName; + + switch (accessExpression) + { + case ObjectAccessExpression innerObjectAccessExpression: + innerAccessExpression = innerObjectAccessExpression.Object; + _ownerMappings[accessExpression] = + (innerObjectAccessExpression.Navigation.DeclaringEntityType, innerAccessExpression); + break; + case ObjectReferenceExpression: + innerAccessExpression = jObjectParameter; + break; + default: + throw new InvalidOperationException( + CoreStrings.TranslationFailed(binaryExpression.Print())); + } + + break; + + default: + throw new UnreachableException(); } var valueExpression = CreateGetValueExpression(innerAccessExpression, storeName, parameterExpression.Type); @@ -136,7 +141,7 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) entityProjectionExpression = (EntityProjectionExpression)projection; } - _materializationContextBindings[parameterExpression] = entityProjectionExpression.AccessExpression; + _materializationContextBindings[parameterExpression] = entityProjectionExpression.Object; var updatedExpression = New( newExpression.Constructor, @@ -224,28 +229,28 @@ protected override Expression VisitExtension(Expression extensionExpression) case CollectionShaperExpression collectionShaperExpression: { - ObjectArrayProjectionExpression objectArrayProjection; + ObjectArrayAccessExpression objectArrayAccess; switch (collectionShaperExpression.Projection) { case ProjectionBindingExpression projectionBindingExpression: var projection = GetProjection(projectionBindingExpression); - objectArrayProjection = (ObjectArrayProjectionExpression)projection.Expression; + objectArrayAccess = (ObjectArrayAccessExpression)projection.Expression; break; - case ObjectArrayProjectionExpression objectArrayProjectionExpression: - objectArrayProjection = objectArrayProjectionExpression; + case ObjectArrayAccessExpression objectArrayProjectionExpression: + objectArrayAccess = objectArrayProjectionExpression; break; default: throw new InvalidOperationException(CoreStrings.TranslationFailed(extensionExpression.Print())); } - var jArray = _projectionBindings[objectArrayProjection]; + var jArray = _projectionBindings[objectArrayAccess]; var jObjectParameter = Parameter(typeof(JObject), jArray.Name + "Object"); var ordinalParameter = Parameter(typeof(int), jArray.Name + "Ordinal"); - var accessExpression = objectArrayProjection.InnerProjection.AccessExpression; + var accessExpression = objectArrayAccess.InnerProjection.Object; _projectionBindings[accessExpression] = jObjectParameter; _ownerMappings[accessExpression] = - (objectArrayProjection.Navigation.DeclaringEntityType, objectArrayProjection.AccessExpression); + (objectArrayAccess.Navigation.DeclaringEntityType, objectArrayAccess.Object); _ordinalParameterBindings[accessExpression] = Add( ordinalParameter, Constant(1, typeof(int))); @@ -596,19 +601,15 @@ private Expression CreateGetValueExpression( if (entityType == null || !entityType.IsDocumentRoot()) { - if (ownership != null - && !ownership.IsUnique) + if (ownership is { IsUnique: false } && property.IsOrdinalKeyProperty()) { - if (property.IsOrdinalKeyProperty()) + var ordinalExpression = _ordinalParameterBindings[jObjectExpression]; + if (ordinalExpression.Type != type) { - var ordinalExpression = _ordinalParameterBindings[jObjectExpression]; - if (ordinalExpression.Type != type) - { - ordinalExpression = Convert(ordinalExpression, type); - } - - return ordinalExpression; + ordinalExpression = Convert(ordinalExpression, type); } + + return ordinalExpression; } var principalProperty = property.FindFirstPrincipal(); @@ -629,7 +630,7 @@ private Expression CreateGetValueExpression( } else if (jObjectExpression is ObjectAccessExpression objectAccessExpression) { - ownerJObjectExpression = objectAccessExpression.AccessExpression; + ownerJObjectExpression = objectAccessExpression.Object; } if (ownerJObjectExpression != null) @@ -643,8 +644,7 @@ private Expression CreateGetValueExpression( } // Workaround for old databases that didn't store the key property - if (ownership != null - && !ownership.IsUnique + if (ownership is { IsUnique: false } && !entityType.IsDocumentRoot() && property.ClrType == typeof(int) && !property.IsForeignKey() @@ -686,23 +686,22 @@ private Expression CreateGetValueExpression( { Check.DebugAssert(type.IsNullableType(), "Must read nullable type from JObject."); - var innerExpression = jObjectExpression; - if (_projectionBindings.TryGetValue(jObjectExpression, out var innerVariable)) - { - innerExpression = innerVariable; - } - else if (jObjectExpression is ObjectReferenceExpression objectReferenceExpression) + var innerExpression = jObjectExpression switch { - innerExpression = CreateGetValueExpression( - jObjectParameter, objectReferenceExpression.Name, typeof(JObject)); - } - else if (jObjectExpression is ObjectAccessExpression objectAccessExpression) - { - var innerAccessExpression = objectAccessExpression.AccessExpression; + _ when _projectionBindings.TryGetValue(jObjectExpression, out var innerVariable) + => innerVariable, - innerExpression = CreateGetValueExpression( - innerAccessExpression, ((IAccessExpression)innerAccessExpression).Name, typeof(JObject)); - } + ObjectReferenceExpression objectReference + => CreateGetValueExpression(jObjectParameter, objectReference.Name, typeof(JObject)), + + ObjectAccessExpression objectAccessExpression + => CreateGetValueExpression( + objectAccessExpression.Object, + ((IAccessExpression)objectAccessExpression.Object).PropertyName, + typeof(JObject)), + + _ => jObjectExpression + }; var jTokenExpression = CreateReadJTokenExpression(innerExpression, storeName); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index c582467bc70..4e27560871f 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -169,40 +169,40 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) var visitedLeft = Visit(left); var visitedRight = Visit(right); - if (binaryExpression.NodeType is ExpressionType.Equal or ExpressionType.NotEqual - // Visited expression could be null, We need to pass MemberInitExpression - && TryRewriteEntityEquality( - binaryExpression.NodeType, - visitedLeft == QueryCompilationContext.NotTranslatedExpression ? left : visitedLeft, - visitedRight == QueryCompilationContext.NotTranslatedExpression ? right : visitedRight, - equalsMethod: false, - out var result)) - { - return result; - } - - if (binaryExpression.Method == ConcatMethodInfo) + switch (binaryExpression) { - return QueryCompilationContext.NotTranslatedExpression; - } + // Visited expression could be null, We need to pass MemberInitExpression + case { NodeType: ExpressionType.Equal or ExpressionType.NotEqual } + when TryRewriteEntityEquality( + binaryExpression.NodeType, + visitedLeft == QueryCompilationContext.NotTranslatedExpression ? left : visitedLeft, + visitedRight == QueryCompilationContext.NotTranslatedExpression ? right : visitedRight, + equalsMethod: false, + out var result): + return result; - var uncheckedNodeTypeVariant = binaryExpression.NodeType switch - { - ExpressionType.AddChecked => ExpressionType.Add, - ExpressionType.SubtractChecked => ExpressionType.Subtract, - ExpressionType.MultiplyChecked => ExpressionType.Multiply, - _ => binaryExpression.NodeType - }; + case { Method: var method } when method == ConcatMethodInfo: + return QueryCompilationContext.NotTranslatedExpression; - return TranslationFailed(binaryExpression.Left, visitedLeft, out var sqlLeft) - || TranslationFailed(binaryExpression.Right, visitedRight, out var sqlRight) - ? QueryCompilationContext.NotTranslatedExpression - : sqlExpressionFactory.MakeBinary( - uncheckedNodeTypeVariant, - sqlLeft!, - sqlRight!, - typeMapping: null) - ?? QueryCompilationContext.NotTranslatedExpression; + default: + var uncheckedNodeTypeVariant = binaryExpression.NodeType switch + { + ExpressionType.AddChecked => ExpressionType.Add, + ExpressionType.SubtractChecked => ExpressionType.Subtract, + ExpressionType.MultiplyChecked => ExpressionType.Multiply, + _ => binaryExpression.NodeType + }; + + return TranslationFailed(binaryExpression.Left, visitedLeft, out var sqlLeft) + || TranslationFailed(binaryExpression.Right, visitedRight, out var sqlRight) + ? QueryCompilationContext.NotTranslatedExpression + : sqlExpressionFactory.MakeBinary( + uncheckedNodeTypeVariant, + sqlLeft!, + sqlRight!, + typeMapping: null) + ?? QueryCompilationContext.NotTranslatedExpression; + } Expression ProcessGetType(EntityReferenceExpression entityReferenceExpression, Type comparisonType, bool match) { @@ -339,23 +339,25 @@ protected override Expression VisitExtension(Expression extensionExpression) case SqlExpression: return extensionExpression; - case StructuralTypeShaperExpression entityShaperExpression: - var result = Visit(entityShaperExpression.ValueBufferExpression); - - if (result is UnaryExpression - { - NodeType: ExpressionType.Convert, - Operand.NodeType: ExpressionType.Convert - } outerUnary - && outerUnary.Type == typeof(ValueBuffer) - && outerUnary.Operand.Type == typeof(object)) - { - result = ((UnaryExpression)outerUnary.Operand).Operand; - } - - return result is EntityProjectionExpression entityProjectionExpression - ? new EntityReferenceExpression(entityProjectionExpression) - : QueryCompilationContext.NotTranslatedExpression; + case StructuralTypeShaperExpression shaper: + return new EntityReferenceExpression(shaper); + + // var result = Visit(entityShaperExpression.ValueBufferExpression); + // + // if (result is UnaryExpression + // { + // NodeType: ExpressionType.Convert, + // Operand.NodeType: ExpressionType.Convert + // } outerUnary + // && outerUnary.Type == typeof(ValueBuffer) + // && outerUnary.Operand.Type == typeof(object)) + // { + // result = ((UnaryExpression)outerUnary.Operand).Operand; + // } + // + // return result is EntityProjectionExpression entityProjectionExpression + // ? new EntityReferenceExpression(entityProjectionExpression) + // : QueryCompilationContext.NotTranslatedExpression; case ProjectionBindingExpression projectionBindingExpression: return projectionBindingExpression.ProjectionMember != null @@ -382,8 +384,7 @@ protected override Expression VisitExtension(Expression extensionExpression) && (convertedType == null || convertedType.IsAssignableFrom(ese.Type))) { - // TODO: Subquery projecting out an entity/structural type - return QueryCompilationContext.NotTranslatedExpression; + return new EntityReferenceExpression(shapedQuery.UpdateShaperExpression(innerExpression)); } if (innerExpression is ProjectionBindingExpression pbe @@ -422,22 +423,17 @@ protected override Expression VisitExtension(Expression extensionExpression) return sqlExpression; } - // TODO + // TODO TODO // subquery.ReplaceProjection(new List { sqlExpression }); subquery.ApplyProjection(); - SqlExpression scalarSubqueryExpression = new ScalarSubqueryExpression(subquery); + // Add VALUE to the subquery's projection (SELECT VALUE x ...), to make it project that value rather than a JSON object + // wrapping that value. + subquery = subquery.WithSingleValueProjection(); - if (shapedQuery.ResultCardinality is ResultCardinality.SingleOrDefault - && !shaperExpression.Type.IsNullableType()) - { - throw new NotImplementedException("Subquery with SingleOrDefault"); - // scalarSubqueryExpression = sqlExpressionFactory.Coalesce( - // scalarSubqueryExpression, - // (SqlExpression)Visit(shaperExpression.Type.GetDefaultValueConstant())); - } + Check.DebugAssert(shapedQuery.ResultCardinality == ResultCardinality.Single, "SingleOrDefault not supported in subqueries"); - return scalarSubqueryExpression; + return new ScalarSubqueryExpression(subquery); } // This case is for a subquery embedded in a lambda, returning an array, e.g. Where(b => b.Ints == new[] { 1, 2, 3 }). @@ -880,50 +876,68 @@ public virtual bool TryBindMember( Expression? source, MemberIdentity member, [NotNullWhen(true)] out Expression? expression, - [NotNullWhen(true)] out IPropertyBase? property) + [NotNullWhen(true)] out IPropertyBase? property, + bool wrapResultExpressionInReferenceExpression = true) { - if (source is not EntityReferenceExpression entityReferenceExpression) + if (source is not EntityReferenceExpression typeReference) { expression = null; property = null; return false; } - var result = member switch + switch (typeReference) { - { MemberInfo: MemberInfo memberInfo } - => entityReferenceExpression.ParameterEntity.BindMember( - memberInfo, entityReferenceExpression.Type, clientEval: false, out property), + case { Parameter: StructuralTypeShaperExpression shaper }: + var valueBufferExpression = Visit(shaper.ValueBufferExpression); + var entityProjection = (EntityProjectionExpression)valueBufferExpression; - { Name: string name } - => entityReferenceExpression.ParameterEntity.BindMember( - name, entityReferenceExpression.Type, clientEval: false, out property), + expression = member switch + { + { MemberInfo: MemberInfo memberInfo } + => entityProjection.BindMember( + memberInfo, typeReference.Type, clientEval: false, out property), - _ => throw new UnreachableException() - }; + { Name: string name } + => entityProjection.BindMember( + name, typeReference.Type, clientEval: false, out property), + + _ => throw new UnreachableException() + }; - switch (result) + break; + + case { Subquery: ShapedQueryExpression }: + throw new NotImplementedException("Bind property on structural type coming out of scalar subquery"); + + default: + throw new UnreachableException(); + } + + if (expression is null) { - case EntityProjectionExpression entityProjectionExpression: - expression = new EntityReferenceExpression(entityProjectionExpression); - Check.DebugAssert(property is not null, "Property cannot be null if binding result was non-null"); - return true; - case ObjectArrayProjectionExpression objectArrayProjectionExpression: - expression = new EntityReferenceExpression(objectArrayProjectionExpression.InnerProjection); - Check.DebugAssert(property is not null, "Property cannot be null if binding result was non-null"); + AddTranslationErrorDetails( + CoreStrings.QueryUnableToTranslateMember( + member.Name, + typeReference.EntityType.DisplayName())); + return false; + } + + Check.DebugAssert(property is not null, "Property cannot be null if binding result was non-null"); + + switch (expression) + { + case StructuralTypeShaperExpression shaper when wrapResultExpressionInReferenceExpression: + expression = new EntityReferenceExpression(shaper); return true; - case null: - AddTranslationErrorDetails( - CoreStrings.QueryUnableToTranslateMember( - member.Name, - entityReferenceExpression.EntityType.DisplayName())); - expression = null; - return false; + // case ObjectArrayAccessExpression objectArrayProjectionExpression: + // expression = objectArrayProjectionExpression; + // return true; default: - expression = result; - Check.DebugAssert(property is not null, "Property cannot be null if binding result was non-null"); return true; } + + // return true; } private static Expression TryRemoveImplicitConvert(Expression expression) @@ -1216,37 +1230,92 @@ private static bool TranslationFailed(Expression? original, Expression? translat return false; } + [DebuggerDisplay("{DebuggerDisplay(),nq}")] private sealed class EntityReferenceExpression : Expression { - public EntityReferenceExpression(EntityProjectionExpression parameter) + public EntityReferenceExpression(StructuralTypeShaperExpression parameter) + { + Parameter = parameter; + EntityType = (IEntityType)parameter.StructuralType; + } + + public EntityReferenceExpression(ShapedQueryExpression subquery) { - ParameterEntity = parameter; - EntityType = parameter.EntityType; - Type = EntityType.ClrType; + Subquery = subquery; + EntityType = (IEntityType)((StructuralTypeShaperExpression)subquery.ShaperExpression).StructuralType; } - private EntityReferenceExpression(EntityProjectionExpression parameter, Type type) + private EntityReferenceExpression(EntityReferenceExpression typeReference, ITypeBase structuralType) { - ParameterEntity = parameter; - EntityType = parameter.EntityType; - Type = type; + Parameter = typeReference.Parameter; + Subquery = typeReference.Subquery; + EntityType = (IEntityType)structuralType; } - public EntityProjectionExpression ParameterEntity { get; } + public new StructuralTypeShaperExpression? Parameter { get; } + public ShapedQueryExpression? Subquery { get; } public IEntityType EntityType { get; } - public override Type Type { get; } + public override Type Type + => EntityType.ClrType; public override ExpressionType NodeType => ExpressionType.Extension; public Expression Convert(Type type) - => type == typeof(object) // Ignore object conversion - || type.IsAssignableFrom(Type) // Ignore conversion to base/interface - ? this - : new EntityReferenceExpression(ParameterEntity, type); + { + if (type == typeof(object) // Ignore object conversion + || type.IsAssignableFrom(Type)) // Ignore casting to base type/interface + { + return this; + } + + return EntityType is IEntityType entityType + && entityType.GetDerivedTypes().FirstOrDefault(et => et.ClrType == type) is IEntityType derivedEntityType + ? new EntityReferenceExpression(this, derivedEntityType) + : QueryCompilationContext.NotTranslatedExpression; + } + + private string DebuggerDisplay() + => this switch + { + { Parameter: not null } => Parameter.DebuggerDisplay(), + { Subquery: not null } => ExpressionPrinter.Print(Subquery!), + _ => throw new UnreachableException() + }; } + // private sealed class EntityReferenceExpression : Expression + // { + // public EntityReferenceExpression(EntityProjectionExpression parameter) + // { + // ParameterEntity = parameter; + // EntityType = parameter.EntityType; + // Type = EntityType.ClrType; + // } + // + // private EntityReferenceExpression(EntityProjectionExpression parameter, Type type) + // { + // ParameterEntity = parameter; + // EntityType = parameter.EntityType; + // Type = type; + // } + // + // public EntityProjectionExpression ParameterEntity { get; } + // public IEntityType EntityType { get; } + // + // public override Type Type { get; } + // + // public override ExpressionType NodeType + // => ExpressionType.Extension; + // + // public Expression Convert(Type type) + // => type == typeof(object) // Ignore object conversion + // || type.IsAssignableFrom(Type) // Ignore conversion to base/interface + // ? this + // : new EntityReferenceExpression(ParameterEntity, type); + // } + private sealed class SqlTypeMappingVerifyingExpressionVisitor : ExpressionVisitor { protected override Expression VisitExtension(Expression extensionExpression) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs index 9cbdde344fb..2b516cdd4ce 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs @@ -84,7 +84,7 @@ private Expression VisitSqlConditional(SqlConditionalExpression sqlConditionalEx private SqlExpression? TryCompensateForBoolWithValueConverter(SqlExpression? sqlExpression) => sqlExpression switch { - KeyAccessExpression keyAccessExpression + ScalarAccessExpression keyAccessExpression when keyAccessExpression.TypeMapping!.ClrType == typeof(bool) && keyAccessExpression.TypeMapping!.Converter != null => sqlExpressionFactory.Equal(sqlExpression, sqlExpressionFactory.Constant(true, sqlExpression.TypeMapping)), diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs index d2b371d4316..fab66144c86 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs @@ -15,7 +15,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; public class EntityProjectionExpression : Expression, IPrintableExpression, IAccessExpression { private readonly Dictionary _propertyExpressionsMap = new(); - private readonly Dictionary _navigationExpressionsMap = new(); + private readonly Dictionary _navigationExpressionsMap = new(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -23,11 +23,11 @@ public class EntityProjectionExpression : Expression, IPrintableExpression, IAcc /// 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 EntityProjectionExpression(IEntityType entityType, Expression accessExpression) + public EntityProjectionExpression(Expression @object, IEntityType entityType) { + Object = @object; EntityType = entityType; - AccessExpression = accessExpression; - Name = (accessExpression as IAccessExpression)?.Name; + PropertyName = (@object as IAccessExpression)?.PropertyName; } /// @@ -54,7 +54,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 AccessExpression { get; } + public virtual Expression Object { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -70,7 +70,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? Name { get; } + public virtual string? PropertyName { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -79,7 +79,7 @@ public override Type Type /// 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(AccessExpression)); + => Update(visitor.Visit(Object)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -87,10 +87,10 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// 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 Update(Expression accessExpression) - => accessExpression != AccessExpression - ? new EntityProjectionExpression(EntityType, accessExpression) - : this; + public virtual Expression Update(Expression @object) + => ReferenceEquals(@object, Object) + ? this + : new EntityProjectionExpression(@object, EntityType); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -109,7 +109,8 @@ public virtual Expression BindProperty(IProperty property, bool clientEval) if (!_propertyExpressionsMap.TryGetValue(property, out var expression)) { - expression = new KeyAccessExpression(property, AccessExpression); + expression = new ScalarAccessExpression( + Object, property.GetJsonPropertyName(), property.ClrType, property.GetTypeMapping()); _propertyExpressionsMap[property] = expression; } @@ -118,7 +119,7 @@ public virtual Expression BindProperty(IProperty property, bool clientEval) // would not otherwise be found to be non-translatable. See issues #17670 and #14121. // TODO: We shouldn't be returning null from here && property.Name != StoreKeyConvention.JObjectPropertyName - && expression.Name?.Length is null or 0) + && expression.PropertyName?.Length is null or 0) { // Non-persisted property can't be translated return null!; @@ -144,24 +145,29 @@ public virtual Expression BindNavigation(INavigation navigation, bool clientEval if (!_navigationExpressionsMap.TryGetValue(navigation, out var expression)) { + // TODO: Unify ObjectAccessExpression and ObjectArrayAccessExpression expression = navigation.IsCollection - ? new ObjectArrayProjectionExpression(navigation, AccessExpression) - : new EntityProjectionExpression( + ? new StructuralTypeShaperExpression( navigation.TargetEntityType, - new ObjectAccessExpression(navigation, AccessExpression)); + new ObjectArrayAccessExpression(Object, navigation), + nullable: true) + : new StructuralTypeShaperExpression( + navigation.TargetEntityType, + new EntityProjectionExpression(new ObjectAccessExpression(Object, navigation), navigation.TargetEntityType), + nullable: !navigation.ForeignKey.IsRequiredDependent); _navigationExpressionsMap[navigation] = expression; } - if (!clientEval - && expression.Name?.Length is null or 0) - { - // Non-persisted navigation can't be translated - // TODO: We shouldn't be returning null from here - return null!; - } + // if (!clientEval + // && expression.PropertyName?.Length is null or 0) + // { + // // Non-persisted navigation can't be translated + // // TODO: We shouldn't be returning null from here + // return null!; + // } - return (Expression)expression; + return expression; } /// @@ -237,7 +243,7 @@ public virtual EntityProjectionExpression UpdateEntityType(IEntityType derivedTy derivedType.DisplayName(), EntityType.DisplayName())); } - return new EntityProjectionExpression(derivedType, AccessExpression); + return new EntityProjectionExpression(Object, derivedType); } /// @@ -247,7 +253,7 @@ public virtual EntityProjectionExpression UpdateEntityType(IEntityType derivedTy /// doing so can result in application failures when updating to a new Entity Framework Core release. /// void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) - => expressionPrinter.Visit(AccessExpression); + => expressionPrinter.Visit(Object); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -263,7 +269,7 @@ public override bool Equals(object? obj) private bool Equals(EntityProjectionExpression entityProjectionExpression) => Equals(EntityType, entityProjectionExpression.EntityType) - && AccessExpression.Equals(entityProjectionExpression.AccessExpression); + && Object.Equals(entityProjectionExpression.Object); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -272,5 +278,14 @@ private bool Equals(EntityProjectionExpression entityProjectionExpression) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override int GetHashCode() - => HashCode.Combine(EntityType, AccessExpression); + => HashCode.Combine(EntityType, Object); + + /// + /// 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 override string ToString() + => $"EntityProjectionExpression: {EntityType.ShortName()}"; } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/IAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/IAccessExpression.cs index 2c1f9b8f384..ab442871509 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/IAccessExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/IAccessExpression.cs @@ -18,5 +18,5 @@ public interface IAccessExpression /// 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. /// - string? Name { get; } + string? PropertyName { get; } } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs index 43a9be4c6df..007394d7119 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs @@ -7,11 +7,14 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// +/// Represents a property access on a CosmosJSON object, which returns a JSON object (structural type) (e.g. c.Address). +/// +/// /// 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 class ObjectAccessExpression : Expression, IPrintableExpression, IAccessExpression { /// @@ -20,15 +23,15 @@ public class ObjectAccessExpression : Expression, IPrintableExpression, IAccessE /// 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 ObjectAccessExpression(INavigation navigation, Expression accessExpression) + public ObjectAccessExpression(Expression @object, INavigation navigation) { - Name = navigation.TargetEntityType.GetContainingPropertyName() + PropertyName = navigation.TargetEntityType.GetContainingPropertyName() ?? throw new InvalidOperationException( CosmosStrings.NavigationPropertyIsNotAnEmbeddedEntity( navigation.DeclaringEntityType.DisplayName(), navigation.Name)); Navigation = navigation; - AccessExpression = accessExpression; + Object = @object; } /// @@ -55,7 +58,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 Name { get; } + public virtual Expression Object { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -63,7 +66,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 INavigation Navigation { get; } + public virtual string PropertyName { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -71,7 +74,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 AccessExpression { get; } + public virtual INavigation Navigation { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -80,7 +83,7 @@ public override Type Type /// 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(AccessExpression)); + => Update(visitor.Visit(Object)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -89,8 +92,8 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual ObjectAccessExpression Update(Expression outerExpression) - => outerExpression != AccessExpression - ? new ObjectAccessExpression(Navigation, outerExpression) + => outerExpression != Object + ? new ObjectAccessExpression(outerExpression, Navigation) : this; /// @@ -109,7 +112,7 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override string ToString() - => $"{AccessExpression}[\"{Name}\"]"; + => $"{Object}[\"{PropertyName}\"]"; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -125,7 +128,7 @@ public override bool Equals(object? obj) private bool Equals(ObjectAccessExpression objectAccessExpression) => Navigation == objectAccessExpression.Navigation - && AccessExpression.Equals(objectAccessExpression.AccessExpression); + && Object.Equals(objectAccessExpression.Object); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -134,5 +137,5 @@ private bool Equals(ObjectAccessExpression objectAccessExpression) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override int GetHashCode() - => HashCode.Combine(Navigation, AccessExpression); + => HashCode.Combine(Navigation, Object); } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayProjectionExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs similarity index 86% rename from src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayProjectionExpression.cs rename to src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs index fa7ef7cb4ca..4eb61d119f3 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayProjectionExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs @@ -7,12 +7,16 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// +/// Represents a property access on a CosmosJSON object, which returns an array of JSON objects (structural types) (e.g. +/// c.Addresses). +/// +/// /// 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 class ObjectArrayProjectionExpression : Expression, IPrintableExpression, IAccessExpression +/// +public class ObjectArrayAccessExpression : Expression, IPrintableExpression, IAccessExpression { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -20,25 +24,23 @@ public class ObjectArrayProjectionExpression : Expression, IPrintableExpression, /// 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 ObjectArrayProjectionExpression( + public ObjectArrayAccessExpression( + Expression @object, INavigation navigation, - Expression accessExpression, EntityProjectionExpression? innerProjection = null) { var targetType = navigation.TargetEntityType; Type = typeof(IEnumerable<>).MakeGenericType(targetType.ClrType); - Name = targetType.GetContainingPropertyName() + PropertyName = targetType.GetContainingPropertyName() ?? throw new InvalidOperationException( CosmosStrings.NavigationPropertyIsNotAnEmbeddedEntity( navigation.DeclaringEntityType.DisplayName(), navigation.Name)); Navigation = navigation; - AccessExpression = accessExpression; + Object = @object; InnerProjection = innerProjection - ?? new EntityProjectionExpression( - targetType, - new ObjectReferenceExpression(targetType, "")); + ?? new EntityProjectionExpression(new ObjectReferenceExpression(targetType, ""), targetType); } /// @@ -64,7 +66,7 @@ public sealed override ExpressionType NodeType /// 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 Name { get; } + public virtual Expression Object { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -72,7 +74,7 @@ public sealed override ExpressionType NodeType /// 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 INavigation Navigation { get; } + public virtual string PropertyName { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -80,7 +82,7 @@ public sealed override ExpressionType NodeType /// 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 AccessExpression { get; } + public virtual INavigation Navigation { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -98,7 +100,7 @@ public sealed override ExpressionType NodeType /// protected override Expression VisitChildren(ExpressionVisitor visitor) { - var accessExpression = visitor.Visit(AccessExpression); + var accessExpression = visitor.Visit(Object); var innerProjection = visitor.Visit(InnerProjection); return Update(accessExpression, (EntityProjectionExpression)innerProjection); @@ -110,11 +112,11 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// 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 ObjectArrayProjectionExpression Update( + public virtual ObjectArrayAccessExpression Update( Expression accessExpression, EntityProjectionExpression innerProjection) - => accessExpression != AccessExpression || innerProjection != InnerProjection - ? new ObjectArrayProjectionExpression(Navigation, accessExpression, innerProjection) + => accessExpression != Object || innerProjection != InnerProjection + ? new ObjectArrayAccessExpression(accessExpression, Navigation, innerProjection) : this; /// @@ -133,7 +135,7 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override string ToString() - => $"{AccessExpression}[\"{Name}\"]"; + => $"{Object}[\"{PropertyName}\"]"; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -144,12 +146,12 @@ public override string ToString() public override bool Equals(object? obj) => obj != null && (ReferenceEquals(this, obj) - || obj is ObjectArrayProjectionExpression arrayProjectionExpression + || obj is ObjectArrayAccessExpression arrayProjectionExpression && Equals(arrayProjectionExpression)); - private bool Equals(ObjectArrayProjectionExpression objectArrayProjectionExpression) - => AccessExpression.Equals(objectArrayProjectionExpression.AccessExpression) - && InnerProjection.Equals(objectArrayProjectionExpression.InnerProjection); + private bool Equals(ObjectArrayAccessExpression objectArrayAccessExpression) + => Object.Equals(objectArrayAccessExpression.Object) + && InnerProjection.Equals(objectArrayAccessExpression.InnerProjection); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -158,5 +160,5 @@ private bool Equals(ObjectArrayProjectionExpression objectArrayProjectionExpress /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override int GetHashCode() - => HashCode.Combine(AccessExpression, InnerProjection); + => HashCode.Combine(Object, InnerProjection); } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayExpression.cs new file mode 100644 index 00000000000..56101fa3537 --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayExpression.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; + +/// +/// Represents a Cosmos ARRAY() expression, which projects the result of a query as an array (e.g. +/// ARRAY (SELECT VALUE t.name FROM t in p.tags)). +/// +/// +/// CosmosDB array 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. +/// +[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] +public class ObjectArrayExpression(SelectExpression subquery, Type arrayClrType) + : Expression, 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 override ExpressionType NodeType + => ExpressionType.Extension; + + /// + /// 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 override Type Type + => arrayClrType; + + /// + /// 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 SelectExpression Subquery { get; } = subquery; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override ObjectArrayExpression VisitChildren(ExpressionVisitor visitor) + => visitor.Visit(Subquery) is var newQuery + && ReferenceEquals(newQuery, Subquery) + ? this + : new ObjectArrayExpression((SelectExpression)newQuery, Type); + + /// + /// 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 void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append("ARRAY ("); + expressionPrinter.Visit(Subquery); + expressionPrinter.Append(")"); + } + + /// + public override bool Equals(object? obj) + => obj is ObjectArrayExpression other && Equals(other); + + private bool Equals(ObjectArrayExpression? other) + => ReferenceEquals(this, other) || (other is not null && Subquery.Equals(other.Subquery)); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Subquery); +} diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/KeyAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayIndexExpression.cs similarity index 76% rename from src/EFCore.Cosmos/Query/Internal/Expressions/KeyAccessExpression.cs rename to src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayIndexExpression.cs index 3a15ab61823..3bda0c53100 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/KeyAccessExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayIndexExpression.cs @@ -1,17 +1,21 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// +/// Represents an indexing into a Cosmos array, e.g. c.Ints[3]. +/// +/// /// 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 class KeyAccessExpression(IProperty property, Expression accessExpression) - : SqlExpression(property.ClrType, property.GetTypeMapping()), IAccessExpression +/// +[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] +public class ObjectArrayIndexExpression(Expression array, Expression index, Type elementType) + : Expression, IPrintableExpression { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -19,7 +23,8 @@ public class KeyAccessExpression(IProperty property, Expression accessExpression /// 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 Name { get; } = property.GetJsonPropertyName(); + public override ExpressionType NodeType + => ExpressionType.Extension; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -27,7 +32,7 @@ public class KeyAccessExpression(IProperty property, Expression accessExpression /// 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 new virtual IProperty Property { get; } = property; + public override Type Type { get; } = elementType; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -35,7 +40,7 @@ public class KeyAccessExpression(IProperty property, Expression accessExpression /// 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 AccessExpression { get; } = accessExpression; + public virtual Expression Array { get; } = array; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -43,8 +48,7 @@ public class KeyAccessExpression(IProperty property, Expression accessExpression /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - => Update(visitor.Visit(AccessExpression)); + public virtual Expression Index { get; } = index; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -52,10 +56,8 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// 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 KeyAccessExpression Update(Expression outerExpression) - => outerExpression != AccessExpression - ? new KeyAccessExpression(Property, outerExpression) - : this; + protected override Expression VisitChildren(ExpressionVisitor visitor) + => Update(visitor.Visit(Array), visitor.Visit(Index)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -63,8 +65,10 @@ public virtual KeyAccessExpression Update(Expression outerExpression) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override void Print(ExpressionPrinter expressionPrinter) - => expressionPrinter.Append(ToString()); + public virtual ObjectArrayIndexExpression Update(Expression array, Expression index) + => array == Array && index == Index + ? this + : new ObjectArrayIndexExpression(array, index, Type); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -72,12 +76,13 @@ protected override void Print(ExpressionPrinter expressionPrinter) /// 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 override string ToString() - => Name?.Length > 0 - ? $"{AccessExpression}[\"{Name}\"]" - // TODO: Remove once __jObject is translated to the access root in a better fashion. - // See issue #17670 and related issue #14121. - : $"{AccessExpression}"; + void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Visit(Array); + expressionPrinter.Append("["); + expressionPrinter.Visit(Index); + expressionPrinter.Append("]"); + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -88,13 +93,11 @@ public override string ToString() public override bool Equals(object? obj) => obj != null && (ReferenceEquals(this, obj) - || obj is KeyAccessExpression keyAccessExpression - && Equals(keyAccessExpression)); + || obj is ObjectArrayIndexExpression other + && Equals(other)); - private bool Equals(KeyAccessExpression keyAccessExpression) - => base.Equals(keyAccessExpression) - && Name == keyAccessExpression.Name - && AccessExpression.Equals(keyAccessExpression.AccessExpression); + private bool Equals(ObjectArrayIndexExpression other) + => Array.Equals(other.Array) && Index.Equals(other.Index); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -103,5 +106,5 @@ private bool Equals(KeyAccessExpression keyAccessExpression) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Name, AccessExpression); + => HashCode.Combine(Array, Index); } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectBinaryExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectBinaryExpression.cs new file mode 100644 index 00000000000..433fc59c4dc --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectBinaryExpression.cs @@ -0,0 +1,176 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Cosmos.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; + +/// +/// 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. +/// +[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] +public class ObjectBinaryExpression : Expression, 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 ObjectBinaryExpression(ExpressionType operatorType, Expression left, Expression right, Type type) + { + if (!IsValidOperator(operatorType)) + { + throw new InvalidOperationException( + CosmosStrings.UnsupportedOperatorForSqlExpression( + operatorType, typeof(SqlBinaryExpression).ShortDisplayName())); + } + + Type = type; + OperatorType = operatorType; + Left = left; + Right = right; + } + + /// + /// 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 override ExpressionType NodeType + => ExpressionType.Extension; + + /// + /// 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 override Type Type { get; } + + /// + /// 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 ExpressionType OperatorType { get; } + + /// + /// 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 Left { get; } + + /// + /// 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 Right { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var left = (Expression)visitor.Visit(Left); + var right = (Expression)visitor.Visit(Right); + + return Update(left, right); + } + + /// + /// 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 ObjectBinaryExpression Update(Expression left, Expression right) + => left != Left || right != Right + ? new ObjectBinaryExpression(OperatorType, left, right, Type) + : this; + + internal static bool IsValidOperator(ExpressionType operatorType) + => operatorType is ExpressionType.Coalesce; + + /// + /// 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 void Print(ExpressionPrinter expressionPrinter) + { + var requiresBrackets = RequiresBrackets(Left); + + if (requiresBrackets) + { + expressionPrinter.Append("("); + } + + expressionPrinter.Visit(Left); + + if (requiresBrackets) + { + expressionPrinter.Append(")"); + } + + expressionPrinter.Append(expressionPrinter.GenerateBinaryOperator(OperatorType)); + + requiresBrackets = RequiresBrackets(Right); + + if (requiresBrackets) + { + expressionPrinter.Append("("); + } + + expressionPrinter.Visit(Right); + + if (requiresBrackets) + { + expressionPrinter.Append(")"); + } + + static bool RequiresBrackets(Expression expression) + => expression is ObjectBinaryExpression; + } + + /// + /// 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 override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is ObjectBinaryExpression other + && Equals(other)); + + private bool Equals(ObjectBinaryExpression other) + => Type == other.Type + && OperatorType == other.OperatorType + && Left.Equals(other.Left) + && Right.Equals(other.Right); + + /// + /// 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 override int GetHashCode() + => HashCode.Combine(Type, OperatorType, Left, Right); +} diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectFunctionExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectFunctionExpression.cs new file mode 100644 index 00000000000..13b66a39449 --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectFunctionExpression.cs @@ -0,0 +1,129 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; + +/// +/// 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 class ObjectFunctionExpression(string name, IEnumerable arguments, Type type) + : Expression, 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 override ExpressionType NodeType + => ExpressionType.Extension; + + /// + /// 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 override Type Type { get; } = type; + + /// + /// 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 string Name { get; } = name; + + /// + /// 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 IReadOnlyList Arguments { get; } = arguments.ToList(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var changed = false; + var arguments = new Expression[Arguments.Count]; + for (var i = 0; i < arguments.Length; i++) + { + arguments[i] = visitor.Visit(Arguments[i]); + changed |= arguments[i] != Arguments[i]; + } + + return changed + ? new ObjectFunctionExpression(Name, arguments, Type) + : this; + } + + /// + /// 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 ObjectFunctionExpression Update(IReadOnlyList arguments) + => arguments.SequenceEqual(Arguments) + ? this + : new ObjectFunctionExpression(Name, arguments, Type); + + /// + /// 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 void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append(Name); + expressionPrinter.Append("("); + expressionPrinter.VisitCollection(Arguments); + expressionPrinter.Append(")"); + } + + /// + /// 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 override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is ObjectFunctionExpression objectFunctionExpression + && Equals(objectFunctionExpression)); + + private bool Equals(ObjectFunctionExpression objectFunctionExpression) + => Name == objectFunctionExpression.Name + && Arguments.SequenceEqual(objectFunctionExpression.Arguments); + + /// + /// 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 override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(base.GetHashCode()); + hash.Add(Name); + for (var i = 0; i < Arguments.Count; i++) + { + hash.Add(Arguments[i]); + } + + return hash.ToHashCode(); + } +} diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs index 747255d7a16..c5eb8d958d8 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs @@ -60,7 +60,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. /// - string IAccessExpression.Name + string IAccessExpression.PropertyName => Name; /// diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ProjectionExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ProjectionExpression.cs index b222c657650..99724e9465a 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ProjectionExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ProjectionExpression.cs @@ -36,7 +36,7 @@ public class ProjectionExpression(Expression expression, string alias) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual string? Name - => (Expression as IAccessExpression)?.Name; + => (Expression as IAccessExpression)?.PropertyName; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarAccessExpression.cs new file mode 100644 index 00000000000..28fefbce731 --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarAccessExpression.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; + +/// +/// Represents a property access on a CosmosJSON object, which returns a scalar (e.g. c.Name). +/// +/// +/// 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 class ScalarAccessExpression(Expression @object, string propertyName, Type clrType, CoreTypeMapping? typeMapping) + : SqlExpression(clrType, typeMapping), IAccessExpression +{ + /// + /// 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 Object { get; } = @object; + + /// + /// 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 string PropertyName { get; } = propertyName; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => Update(visitor.Visit(Object)); + + /// + /// 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 ScalarAccessExpression Update(Expression @object) + => ReferenceEquals(@object, Object) + ? this + : new ScalarAccessExpression(@object, PropertyName, Type, TypeMapping); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Visit(Object); + expressionPrinter + .Append("[\"") + .Append(PropertyName) + .Append("\"]"); + } + + /// + /// 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 override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is ScalarAccessExpression keyAccessExpression + && Equals(keyAccessExpression)); + + private bool Equals(ScalarAccessExpression scalarAccessExpression) + => base.Equals(scalarAccessExpression) + && PropertyName == scalarAccessExpression.PropertyName + && Object.Equals(scalarAccessExpression.Object); + + /// + /// 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 override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), PropertyName, Object); +} diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ArrayExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarArrayExpression.cs similarity index 88% rename from src/EFCore.Cosmos/Query/Internal/Expressions/ArrayExpression.cs rename to src/EFCore.Cosmos/Query/Internal/Expressions/ScalarArrayExpression.cs index fc93e4305bc..3e483420717 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ArrayExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarArrayExpression.cs @@ -18,7 +18,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] -public class ArrayExpression(SelectExpression subquery, Type arrayClrType, CoreTypeMapping? arrayTypeMapping = null) +public class ScalarArrayExpression(SelectExpression subquery, Type arrayClrType, CoreTypeMapping? arrayTypeMapping = null) : SqlExpression(arrayClrType, arrayTypeMapping) { /// @@ -35,11 +35,11 @@ public class ArrayExpression(SelectExpression subquery, Type arrayClrType, CoreT /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override ArrayExpression VisitChildren(ExpressionVisitor visitor) + protected override ScalarArrayExpression VisitChildren(ExpressionVisitor visitor) => visitor.Visit(Subquery) is var newQuery && ReferenceEquals(newQuery, Subquery) ? this - : new ArrayExpression((SelectExpression)newQuery, Type, TypeMapping); + : new ScalarArrayExpression((SelectExpression)newQuery, Type, TypeMapping); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -56,9 +56,9 @@ protected override void Print(ExpressionPrinter expressionPrinter) /// public override bool Equals(object? obj) - => obj is ArrayExpression other && Equals(other); + => obj is ScalarArrayExpression other && Equals(other); - private bool Equals(ArrayExpression? other) + private bool Equals(ScalarArrayExpression? other) => ReferenceEquals(this, other) || (base.Equals(other) && Subquery.Equals(other.Subquery)); /// diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarReferenceExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarReferenceExpression.cs index f4c60141a3b..7879c8a3cda 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarReferenceExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarReferenceExpression.cs @@ -5,7 +5,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// -/// Represents a reference to a JSON value in the Cosmos SQL query, e.g. the first i in SELECT i FROM i IN x.y. +/// Represents a reference to a JSON scalar value in the Cosmos SQL query, e.g. the first i in SELECT i FROM i IN x.y. /// When referencing a non-scalar, is used instead. /// /// @@ -31,7 +31,7 @@ public class ScalarReferenceExpression(string name, Type clrType, CoreTypeMappin /// 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. /// - string IAccessExpression.Name + string IAccessExpression.PropertyName => Name; /// diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarSubqueryExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarSubqueryExpression.cs index 01ccf47b853..2a1f8638671 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarSubqueryExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarSubqueryExpression.cs @@ -30,7 +30,7 @@ public ScalarSubqueryExpression(SelectExpression subquery) subquery, subquery.Projection[0].Expression is SqlExpression sqlExpression ? sqlExpression.TypeMapping - : throw new UnreachableException("Can't construct scalar subquery over SelectExpresison with non-SqlExpression projection")) + : throw new UnreachableException("Can't construct scalar subquery over SelectExpression with non-SqlExpression projection")) { Subquery = subquery; } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs index fdf006147c0..b2a829ea311 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs @@ -48,7 +48,7 @@ public SelectExpression(IEntityType entityType) // TODO: Redo aliasing _sources = [new SourceExpression(new ObjectReferenceExpression(entityType, "root"), RootAlias)]; _projectionMapping[new ProjectionMember()] - = new EntityProjectionExpression(entityType, new ObjectReferenceExpression(entityType, RootAlias)); + = new EntityProjectionExpression(new ObjectReferenceExpression(entityType, RootAlias), entityType); } /// @@ -61,8 +61,7 @@ public SelectExpression(IEntityType entityType, string sql, Expression argument) { var fromSql = new FromSqlExpression(entityType.ClrType, sql, argument); _sources = [new SourceExpression(fromSql, RootAlias)]; - _projectionMapping[new ProjectionMember()] = new EntityProjectionExpression( - entityType, new ObjectReferenceExpression(entityType, RootAlias)); + _projectionMapping[new ProjectionMember()] = new EntityProjectionExpression(new ObjectReferenceExpression(entityType, RootAlias), entityType); } /// @@ -87,7 +86,7 @@ public 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 virtual ReadItemInfo? ReadItemInfo { get; } + public virtual ReadItemInfo? ReadItemInfo { get; init; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -95,9 +94,21 @@ public 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 SelectExpression(SqlExpression projection) + public SelectExpression(Expression projection) => _projectionMapping[new ProjectionMember()] = projection; + /// + /// 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 SelectExpression(SourceExpression source, Expression projection) + { + _sources.Add(source); + _projectionMapping[new ProjectionMember()] = projection; + } + private SelectExpression() { } @@ -108,19 +119,41 @@ 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 CreateForPrimitiveCollection( - SourceExpression source, - Type elementClrType, - CoreTypeMapping elementTypeMapping) - => new() + public static SelectExpression CreateForCollection(Expression containerExpression, 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 + // (e.g. SELECT VALUE i FROM i IN c.SomeArray). + // 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; + } + + var source = new SourceExpression(containerExpression, sourceAlias, withIn: true); + + return new SelectExpression { _sources = { source }, - _projectionMapping = - { - [new ProjectionMember()] = new ScalarReferenceExpression(source.Alias, elementClrType, elementTypeMapping) - }, + _projectionMapping = { [new ProjectionMember()] = projection }, UsesSingleValueProjection = true }; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -314,8 +347,8 @@ public virtual int AddToProjection(EntityProjectionExpression entityProjection) /// 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 int AddToProjection(ObjectArrayProjectionExpression objectArrayProjection) - => AddToProjection(objectArrayProjection, null); + public virtual int AddToProjection(ObjectArrayAccessExpression objectArrayAccess) + => AddToProjection(objectArrayAccess, null); private int AddToProjection(Expression expression, string? alias) { @@ -326,7 +359,7 @@ private int AddToProjection(Expression expression, string? alias) } var baseAlias = alias - ?? (expression as IAccessExpression)?.Name + ?? (expression as IAccessExpression)?.PropertyName ?? "c"; var currentAlias = baseAlias; @@ -605,7 +638,34 @@ public virtual SelectExpression Update( Predicate = predicate, Offset = offset, Limit = limit, - IsDistinct = IsDistinct + IsDistinct = IsDistinct, + UsesSingleValueProjection = UsesSingleValueProjection, + ReadItemInfo = ReadItemInfo + }; + } + + /// + /// 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 SelectExpression WithSingleValueProjection() + { + var projectionMapping = new Dictionary(); + foreach (var (projectionMember, expression) in _projectionMapping) + { + projectionMapping[projectionMember] = expression; + } + + return new SelectExpression(Projection.ToList(), Sources.ToList(), Orderings.ToList()) + { + _projectionMapping = projectionMapping, + Predicate = Predicate, + Offset = Offset, + Limit = Limit, + IsDistinct = IsDistinct, + UsesSingleValueProjection = true }; } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SourceExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SourceExpression.cs index 5ed5b1c33fa..f51e9e5f471 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SourceExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SourceExpression.cs @@ -66,7 +66,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. /// - string IAccessExpression.Name + string IAccessExpression.PropertyName => Alias; /// diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlExpression.cs index 39bc4345c06..602854dd08d 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlExpression.cs @@ -13,8 +13,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] -public abstract class SqlExpression(Type type, CoreTypeMapping? typeMapping) - : Expression, IPrintableExpression +public abstract class SqlExpression(Type type, CoreTypeMapping? typeMapping) : Expression, IPrintableExpression { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -74,9 +73,8 @@ public override bool Equals(object? obj) || obj is SqlExpression sqlExpression && Equals(sqlExpression)); - private bool Equals(SqlExpression sqlExpression) - => Type == sqlExpression.Type - && TypeMapping?.Equals(sqlExpression.TypeMapping) == true; + private bool Equals(SqlExpression other) + => Type == other.Type && TypeMapping?.Equals(other.TypeMapping) == true; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlFunctionExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlFunctionExpression.cs index d703d614937..9b1513285dd 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlFunctionExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlFunctionExpression.cs @@ -10,20 +10,32 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// 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 class SqlFunctionExpression( +public class SqlFunctionExpression : SqlExpression +{ + /// + /// 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 SqlFunctionExpression( string name, - IEnumerable arguments, + IEnumerable arguments, Type type, CoreTypeMapping? typeMapping) - : SqlExpression(type, typeMapping) -{ + : base(type, typeMapping) + { + Name = name; + Arguments = arguments.ToList(); + } + /// /// 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 string Name { get; } = name; + public virtual string Name { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -31,7 +43,7 @@ public class SqlFunctionExpression( /// 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 IReadOnlyList Arguments { get; } = arguments.ToList(); + public virtual IReadOnlyList Arguments { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -42,19 +54,15 @@ public class SqlFunctionExpression( protected override Expression VisitChildren(ExpressionVisitor visitor) { var changed = false; - var arguments = new SqlExpression[Arguments.Count]; + var arguments = new Expression[Arguments.Count]; for (var i = 0; i < arguments.Length; i++) { - arguments[i] = (SqlExpression)visitor.Visit(Arguments[i]); + arguments[i] = visitor.Visit(Arguments[i]); changed |= arguments[i] != Arguments[i]; } return changed - ? new SqlFunctionExpression( - Name, - arguments, - Type, - TypeMapping) + ? new SqlFunctionExpression(Name, arguments, Type, TypeMapping) : this; } @@ -73,10 +81,10 @@ public virtual SqlFunctionExpression ApplyTypeMapping(CoreTypeMapping? typeMappi /// 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 SqlFunctionExpression Update(IReadOnlyList arguments) - => !arguments.SequenceEqual(Arguments) - ? new SqlFunctionExpression(Name, arguments, Type, TypeMapping) - : this; + public virtual SqlFunctionExpression Update(IReadOnlyList arguments) + => arguments.SequenceEqual(Arguments) + ? this + : new SqlFunctionExpression(Name, arguments, Type, TypeMapping); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs index ea23e416025..abd4c69d034 100644 --- a/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs +++ b/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs @@ -266,7 +266,7 @@ SqlUnaryExpression Convert( /// SqlFunctionExpression Function( string functionName, - IEnumerable arguments, + IEnumerable arguments, Type returnType, CoreTypeMapping? typeMapping = null); diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs index 4a0f78f0d60..9c5e3be185f 100644 --- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs +++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs @@ -163,7 +163,7 @@ private SqlExpression ApplyTypeMappingOnSqlBinary( case ExpressionType.ArrayIndex: { // TODO: This infers based on the CLR type; need to properly infer based on the element type mapping - // TODO: being applied here (e.g. WHERE @p[1] = c.PropertyWithValueConverter) + // TODO: being applied here (e.g. WHERE @p[1] = c.PropertyWithValueConverter). #34026 var arrayTypeMapping = left.TypeMapping ?? (typeMapping is null ? null : typeMappingSource.FindMapping(typeMapping.ClrType.MakeArrayType())); return new SqlBinaryExpression( @@ -568,15 +568,15 @@ public virtual SqlUnaryExpression Negate(SqlExpression operand) /// public virtual SqlFunctionExpression Function( string functionName, - IEnumerable arguments, + IEnumerable arguments, Type returnType, CoreTypeMapping? typeMapping = null) { - var typeMappedArguments = new List(); + var typeMappedArguments = new List(); foreach (var argument in arguments) { - typeMappedArguments.Add(ApplyDefaultTypeMapping(argument)); + typeMappedArguments.Add(argument is SqlExpression sqlArgument ? ApplyDefaultTypeMapping(sqlArgument) : argument); } return new SqlFunctionExpression( diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs index 98c96d8860d..d95ec7eb170 100644 --- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs @@ -25,13 +25,14 @@ ShapedQueryExpression shapedQueryExpression SelectExpression selectExpression => VisitSelect(selectExpression), ProjectionExpression projectionExpression => VisitProjection(projectionExpression), EntityProjectionExpression entityProjectionExpression => VisitEntityProjection(entityProjectionExpression), - ObjectArrayProjectionExpression arrayProjectionExpression => VisitObjectArrayProjection(arrayProjectionExpression), + ObjectArrayAccessExpression arrayProjectionExpression => VisitObjectArrayAccess(arrayProjectionExpression), FromSqlExpression fromSqlExpression => VisitFromSql(fromSqlExpression), ObjectReferenceExpression objectReferenceExpression => VisitObjectReference(objectReferenceExpression), - KeyAccessExpression keyAccessExpression => VisitKeyAccess(keyAccessExpression), + ScalarAccessExpression keyAccessExpression => VisitScalarAccess(keyAccessExpression), ObjectAccessExpression objectAccessExpression => VisitObjectAccess(objectAccessExpression), ScalarSubqueryExpression scalarSubqueryExpression => VisitScalarSubquery(scalarSubqueryExpression), SqlBinaryExpression sqlBinaryExpression => VisitSqlBinary(sqlBinaryExpression), + ObjectBinaryExpression objectBinaryExpression => VisitObjectBinary(objectBinaryExpression), SqlConstantExpression sqlConstantExpression => VisitSqlConstant(sqlConstantExpression), SqlUnaryExpression sqlUnaryExpression => VisitSqlUnary(sqlUnaryExpression), SqlConditionalExpression sqlConditionalExpression => VisitSqlConditional(sqlConditionalExpression), @@ -39,11 +40,14 @@ ShapedQueryExpression shapedQueryExpression InExpression inExpression => VisitIn(inExpression), ArrayConstantExpression inlineArrayExpression => VisitArrayConstant(inlineArrayExpression), SourceExpression sourceExpression => VisitSource(sourceExpression), + ObjectFunctionExpression objectFunctionExpression => VisitObjectFunction(objectFunctionExpression), SqlFunctionExpression sqlFunctionExpression => VisitSqlFunction(sqlFunctionExpression), OrderingExpression orderingExpression => VisitOrdering(orderingExpression), ScalarReferenceExpression valueReferenceExpression => VisitValueReference(valueReferenceExpression), ExistsExpression existsExpression => VisitExists(existsExpression), - ArrayExpression arrayExpression => VisitArray(arrayExpression), + ObjectArrayExpression arrayExpression => VisitObjectArray(arrayExpression), + ScalarArrayExpression arrayExpression => VisitScalarArray(arrayExpression), + ObjectArrayIndexExpression objectArrayIndexExpression => VisitObjectArrayIndex(objectArrayIndexExpression), _ => base.VisitExtension(extensionExpression) }; @@ -62,7 +66,23 @@ ShapedQueryExpression shapedQueryExpression /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected abstract Expression VisitArray(ArrayExpression arrayExpression); + protected abstract Expression VisitObjectArray(ObjectArrayExpression objectArrayExpression); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected abstract Expression VisitScalarArray(ScalarArrayExpression scalarArrayExpression); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected abstract Expression VisitObjectArrayIndex(ObjectArrayIndexExpression objectArrayIndexExpression); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -80,6 +100,14 @@ ShapedQueryExpression shapedQueryExpression /// protected abstract Expression VisitOrdering(OrderingExpression orderingExpression); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected abstract Expression VisitObjectFunction(ObjectFunctionExpression objectFunctionExpression); + /// /// 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 @@ -158,7 +186,15 @@ ShapedQueryExpression shapedQueryExpression /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected abstract Expression VisitKeyAccess(KeyAccessExpression keyAccessExpression); + protected abstract Expression VisitObjectBinary(ObjectBinaryExpression objectBinaryExpression); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected abstract Expression VisitScalarAccess(ScalarAccessExpression scalarAccessExpression); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -198,7 +234,7 @@ ShapedQueryExpression shapedQueryExpression /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected abstract Expression VisitObjectArrayProjection(ObjectArrayProjectionExpression objectArrayProjectionExpression); + protected abstract Expression VisitObjectArrayAccess(ObjectArrayAccessExpression objectArrayAccessExpression); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Query/StructuralTypeProjectionExpression.cs b/src/EFCore.Relational/Query/StructuralTypeProjectionExpression.cs index 764b5b3a3be..1561af692fe 100644 --- a/src/EFCore.Relational/Query/StructuralTypeProjectionExpression.cs +++ b/src/EFCore.Relational/Query/StructuralTypeProjectionExpression.cs @@ -36,8 +36,8 @@ public StructuralTypeProjectionExpression( : this( type, propertyExpressionMap, - [], - null, + ownedNavigationMap: [], + complexPropertyCache: null, tableMap, nullable, discriminatorExpression) @@ -61,7 +61,7 @@ public StructuralTypeProjectionExpression( : this( type, propertyExpressionMap, - [], + ownedNavigationMap: [], complexPropertyCache, tableMap, nullable, @@ -423,10 +423,7 @@ public virtual void AddNavigationBinding(INavigation navigation, StructuralTypeS : null; } - /// - /// Creates a representation of the Expression. - /// - /// A representation of the Expression. + /// public override string ToString() - => $"EntityProjectionExpression: {StructuralType.ShortName()}"; + => $"StructuralTypeProjectionExpression: {StructuralType.ShortName()}"; } diff --git a/src/EFCore/Infrastructure/ExpressionExtensions.cs b/src/EFCore/Infrastructure/ExpressionExtensions.cs index 761885c1779..2397e7f3685 100644 --- a/src/EFCore/Infrastructure/ExpressionExtensions.cs +++ b/src/EFCore/Infrastructure/ExpressionExtensions.cs @@ -367,7 +367,14 @@ public static Expression CreateEFPropertyExpression( bool makeNullable = true) // No shadow entities in runtime => CreateEFPropertyExpression(target, property.DeclaringType.ClrType, property.ClrType, property.Name, makeNullable); - private static Expression CreateEFPropertyExpression( + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public static Expression CreateEFPropertyExpression( Expression target, Type propertyDeclaringType, Type propertyType, diff --git a/src/EFCore/Query/ExpressionPrinter.cs b/src/EFCore/Query/ExpressionPrinter.cs index b64c26217e2..87e6ddf0a29 100644 --- a/src/EFCore/Query/ExpressionPrinter.cs +++ b/src/EFCore/Query/ExpressionPrinter.cs @@ -87,7 +87,7 @@ public virtual ExpressionPrinter AppendLine() /// /// The string to append. /// This printer so additional calls can be chained. - public virtual ExpressionVisitor AppendLine(string value) + public virtual ExpressionPrinter AppendLine(string value) { _stringBuilder.AppendLine(value); return this; diff --git a/src/EFCore/Query/IncludeExpression.cs b/src/EFCore/Query/IncludeExpression.cs index c16cd724931..13432c9b8bf 100644 --- a/src/EFCore/Query/IncludeExpression.cs +++ b/src/EFCore/Query/IncludeExpression.cs @@ -16,6 +16,7 @@ namespace Microsoft.EntityFrameworkCore.Query; /// See Implementation of database providers and extensions /// and How EF Core queries work for more information and examples. /// +[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] public class IncludeExpression : Expression, IPrintableExpression { /// @@ -108,15 +109,17 @@ public virtual IncludeExpression Update(Expression entityExpression, Expression /// void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) { - expressionPrinter.AppendLine("IncludeExpression("); + expressionPrinter.AppendLine("Include("); using (expressionPrinter.Indent()) { - expressionPrinter.AppendLine("EntityExpression:"); + expressionPrinter.Append("Entity: "); expressionPrinter.Visit(EntityExpression); expressionPrinter.AppendLine(", "); - expressionPrinter.AppendLine("NavigationExpression:"); + expressionPrinter + .Append("Navigation: ") + .Append(Navigation.Name) + .Append(", "); expressionPrinter.Visit(NavigationExpression); - expressionPrinter.AppendLine($", {Navigation.Name})"); } } } diff --git a/src/EFCore/Query/MaterializeCollectionNavigationExpression.cs b/src/EFCore/Query/MaterializeCollectionNavigationExpression.cs index 4cd23a8ffdb..ac740d35f2f 100644 --- a/src/EFCore/Query/MaterializeCollectionNavigationExpression.cs +++ b/src/EFCore/Query/MaterializeCollectionNavigationExpression.cs @@ -16,6 +16,7 @@ namespace Microsoft.EntityFrameworkCore.Query; /// See Implementation of database providers and extensions /// and How EF Core queries work for more information and examples. /// +[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] public class MaterializeCollectionNavigationExpression : Expression, IPrintableExpression { /// @@ -69,7 +70,7 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) using (expressionPrinter.Indent()) { expressionPrinter.AppendLine($"Navigation: {Navigation.DeclaringEntityType.DisplayName()}.{Navigation.Name},"); - expressionPrinter.Append("subquery: "); + expressionPrinter.Append("Subquery: "); expressionPrinter.Visit(Subquery); expressionPrinter.Append(")"); } diff --git a/src/EFCore/Query/QueryCompilationContext.cs b/src/EFCore/Query/QueryCompilationContext.cs index 258ec38a927..2e05c759414 100644 --- a/src/EFCore/Query/QueryCompilationContext.cs +++ b/src/EFCore/Query/QueryCompilationContext.cs @@ -289,13 +289,17 @@ private Expression InsertRuntimeParameters(Expression query) private static readonly MethodInfo QueryContextAddParameterMethodInfo = typeof(QueryContext).GetTypeInfo().GetDeclaredMethod(nameof(QueryContext.AddParameter))!; - private sealed class NotTranslatedExpressionType : Expression + [DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] + private sealed class NotTranslatedExpressionType : Expression, IPrintableExpression { public override Type Type => typeof(object); public override ExpressionType NodeType => ExpressionType.Extension; + + void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) + => expressionPrinter.Append("!!! NotTranslated !!!"); } private sealed class RuntimeParameterConstantLifter(ILiftableConstantFactory liftableConstantFactory) : ExpressionVisitor diff --git a/src/EFCore/Query/ShapedQueryExpression.cs b/src/EFCore/Query/ShapedQueryExpression.cs index 528017a93a7..33585e6dfe4 100644 --- a/src/EFCore/Query/ShapedQueryExpression.cs +++ b/src/EFCore/Query/ShapedQueryExpression.cs @@ -91,11 +91,11 @@ public virtual ShapedQueryExpression Update(Expression queryExpression, Expressi /// The property of the result. /// This expression if shaper expression did not change, or an expression with the updated shaper expression. public virtual ShapedQueryExpression UpdateQueryExpression(Expression queryExpression) - => !ReferenceEquals(queryExpression, QueryExpression) - ? new ShapedQueryExpression( + => ReferenceEquals(queryExpression, QueryExpression) + ? this + : new ShapedQueryExpression( queryExpression, - ReplacingExpressionVisitor.Replace(QueryExpression, queryExpression, ShaperExpression), ResultCardinality) - : this; + ReplacingExpressionVisitor.Replace(QueryExpression, queryExpression, ShaperExpression), ResultCardinality); /// /// Creates a new expression that is like this one, but using the supplied shaper expression. If shaper expression is the same, it will diff --git a/src/EFCore/Query/StructuralTypeShaperExpression.cs b/src/EFCore/Query/StructuralTypeShaperExpression.cs index c063baf356c..07c634650eb 100644 --- a/src/EFCore/Query/StructuralTypeShaperExpression.cs +++ b/src/EFCore/Query/StructuralTypeShaperExpression.cs @@ -273,19 +273,21 @@ public sealed override ExpressionType NodeType /// void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) { - expressionPrinter.AppendLine(nameof(StructuralTypeShaperExpression) + ": "); + expressionPrinter.AppendLine(nameof(StructuralTypeShaperExpression) + "("); using (expressionPrinter.Indent()) { - expressionPrinter.AppendLine(StructuralType.Name); - expressionPrinter.AppendLine(nameof(ValueBufferExpression) + ": "); - using (expressionPrinter.Indent()) - { - expressionPrinter.Visit(ValueBufferExpression); - expressionPrinter.AppendLine(); - } - - expressionPrinter.Append(nameof(IsNullable) + ": "); - expressionPrinter.AppendLine(IsNullable.ToString()); + expressionPrinter + .Append(nameof(StructuralType) + ": ") + .AppendLine(StructuralType.Name); + + expressionPrinter.Append(nameof(ValueBufferExpression) + ": "); + expressionPrinter.Visit(ValueBufferExpression); + expressionPrinter.AppendLine(); + + expressionPrinter + .Append(nameof(IsNullable) + ": ") + .Append(IsNullable.ToString()) + .Append(")"); } } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs index f3d9e807198..4f035765ff5 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Azure.Cosmos; +using Microsoft.EntityFrameworkCore.Cosmos.Internal; + namespace Microsoft.EntityFrameworkCore.Query; #nullable disable @@ -11,18 +14,18 @@ public OwnedQueryCosmosTest(OwnedQueryCosmosFixture fixture, ITestOutputHelper t : base(fixture) { ClearLog(); - //TestLoggerFactory.TestOutputHelper = testOutputHelper; + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - [ConditionalTheory(Skip = "Issue#17246")] + // TODO: Fake LeftJoin, #33969 public override Task Query_loads_reference_nav_automatically_in_projection(bool async) - => base.Query_loads_reference_nav_automatically_in_projection(async); + => AssertTranslationFailed(() => base.Query_loads_reference_nav_automatically_in_projection(async)); - [ConditionalTheory(Skip = "SelectMany #17246")] + // TODO: SelectMany, #17246 public override Task Query_with_owned_entity_equality_operator(bool async) - => base.Query_with_owned_entity_equality_operator(async); + => AssertTranslationFailed(() => base.Query_with_owned_entity_equality_operator(async)); - [ConditionalTheory(Skip = "Count #16146")] + [ConditionalTheory] public override Task Navigation_rewrite_on_owned_collection(bool async) => CosmosTestHelpers.Instance.NoSyncTest( async, async a => @@ -33,29 +36,46 @@ public override Task Navigation_rewrite_on_owned_collection(bool async) """ SELECT c FROM root c -WHERE ((c[""Discriminator""] = ""LeafB"") OR ((c[""Discriminator""] = ""LeafA"") OR ((c[""Discriminator""] = ""Branch"") OR (c[""Discriminator""] = ""OwnedPerson"")))) +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(c["Orders"]) > 0)) +ORDER BY c["Id"] """); }); - [ConditionalTheory(Skip = "Issue#16926")] - public override Task Navigation_rewrite_on_owned_collection_with_composition(bool async) - => CosmosTestHelpers.Instance.NoSyncTest( - async, async a => - { - await base.Navigation_rewrite_on_owned_collection_with_composition(a); + [ConditionalTheory] + public override async Task Navigation_rewrite_on_owned_collection_with_composition(bool async) + { + // Always throws for sync. + if (async) + { + var exception = await Assert.ThrowsAsync(() => base.Navigation_rewrite_on_owned_collection_with_composition(async)); - AssertSql(" "); - }); + Assert.Contains("'ORDER BY' is not supported in subqueries.", exception.Message); - [ConditionalTheory(Skip = "Issue#16926")] - public override Task Navigation_rewrite_on_owned_collection_with_composition_complex(bool async) - => CosmosTestHelpers.Instance.NoSyncTest( - async, async a => - { - await base.Navigation_rewrite_on_owned_collection_with_composition_complex(a); + AssertSql( + """ +SELECT (ARRAY( + SELECT VALUE (t["Id"] != 42) + FROM t IN c["Orders"] + ORDER BY t["Id"])[0] ?? false) AS c +FROM root c +WHERE c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") +ORDER BY c["Id"] +"""); + } + } - AssertSql(" "); - }); + public override async Task Navigation_rewrite_on_owned_collection_with_composition_complex(bool async) + { + // Always throws for sync. + if (async) + { + // TODO: #33995 + await Assert.ThrowsAsync( + () => base.Navigation_rewrite_on_owned_collection_with_composition_complex(async)); + + AssertSql(); + } + } public override Task Navigation_rewrite_on_owned_reference_projecting_entity(bool async) => CosmosTestHelpers.Instance.NoSyncTest( @@ -141,71 +161,87 @@ FROM root c """); }); - [ConditionalTheory(Skip = "LeftJoin #17314")] + // TODO: Fake LeftJoin, #33969 public override Task Filter_owned_entity_chained_with_regular_entity_followed_by_projecting_owned_collection(bool async) - => base.Filter_owned_entity_chained_with_regular_entity_followed_by_projecting_owned_collection(async); + => AssertTranslationFailed( + () => base.Filter_owned_entity_chained_with_regular_entity_followed_by_projecting_owned_collection(async)); + + public override async Task Set_throws_for_owned_type(bool async) + { + await base.Set_throws_for_owned_type(async); + + AssertSql(); + } - [ConditionalTheory(Skip = "LeftJoin #17314")] + // TODO: Fake LeftJoin, #33969 public override Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity(bool async) - => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity(async); + => AssertTranslationFailed( + () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity(async)); - [ConditionalTheory(Skip = "LeftJoin #17314")] + // TODO: Fake LeftJoin, #33969 public override Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_filter(bool async) - => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_filter(async); + => AssertTranslationFailed( + () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_filter(async)); - [ConditionalTheory(Skip = "LeftJoin #17314")] + // TODO: Fake LeftJoin, #33969 public override Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference(bool async) - => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference(async); + => AssertTranslationFailed( + () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference(async)); - [ConditionalTheory(Skip = "LeftJoin #17314")] + // TODO: Fake LeftJoin, #33969 public override Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference_and_scalar(bool async) - => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference_and_scalar(async); + => AssertTranslationFailed( + () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference_and_scalar(async)); - [ConditionalTheory(Skip = "LeftJoin #17314")] + // TODO: Fake LeftJoin, #33969 public override Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_collection(bool async) - => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_collection(async); + => AssertTranslationFailed( + () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_collection(async)); - [ConditionalTheory(Skip = "LeftJoin #17314")] + // TODO: Fake LeftJoin, #33969 public override Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_collection_count(bool async) - => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_collection_count(async); + => AssertTranslationFailed( + () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_collection(async)); - [ConditionalTheory(Skip = "LeftJoin #17314")] + // TODO: Fake LeftJoin, #33969 public override Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_property(bool async) - => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_property(async); + => AssertTranslationFailed( + () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_property(async)); - [ConditionalTheory(Skip = "LeftJoin #17314")] - public override Task - Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference_in_predicate_and_projection(bool async) - => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference_in_predicate_and_projection( - async); + // TODO: Fake LeftJoin, #33969 + public override Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference_in_predicate_and_projection(bool async) + => AssertTranslationFailed( + () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference_in_predicate_and_projection(async)); - [ConditionalTheory(Skip = "LeftJoin #17314")] + // TODO: Fake LeftJoin, #33969 public override Task Project_multiple_owned_navigations(bool async) - => base.Project_multiple_owned_navigations(async); + => AssertTranslationFailed( + () => base.Project_multiple_owned_navigations(async)); - [ConditionalTheory(Skip = "LeftJoin #17314")] + // TODO: Fake LeftJoin, #33969 public override Task Project_multiple_owned_navigations_with_expansion_on_owned_collections(bool async) - => base.Project_multiple_owned_navigations_with_expansion_on_owned_collections(async); + => AssertTranslationFailed( + () => base.Project_multiple_owned_navigations_with_expansion_on_owned_collections(async)); - [ConditionalTheory(Skip = "SelectMany #17246")] + // TODO: SelectMany, #17246 public override Task SelectMany_on_owned_collection(bool async) - => base.SelectMany_on_owned_collection(async); + => AssertTranslationFailed(() => base.SelectMany_on_owned_collection(async)); - [ConditionalTheory(Skip = "SelectMany #17246")] + // TODO: SelectMany, #17246 public override Task SelectMany_on_owned_reference_followed_by_regular_entity_and_collection(bool async) - => base.SelectMany_on_owned_reference_followed_by_regular_entity_and_collection(async); + => AssertTranslationFailed(() => base.SelectMany_on_owned_reference_followed_by_regular_entity_and_collection(async)); - [ConditionalTheory(Skip = "SelectMany #17246")] + // TODO: SelectMany, #17246 public override Task SelectMany_on_owned_reference_with_entity_in_between_ending_in_owned_collection(bool async) - => base.SelectMany_on_owned_reference_with_entity_in_between_ending_in_owned_collection(async); + => AssertTranslationFailed(() => base.SelectMany_on_owned_reference_with_entity_in_between_ending_in_owned_collection(async)); - [ConditionalTheory(Skip = "SelectMany #17246")] + // TODO: SelectMany, #17246 public override Task Query_with_owned_entity_equality_method(bool async) - => base.Query_with_owned_entity_equality_method(async); + => AssertTranslationFailed(() => base.Query_with_owned_entity_equality_method(async)); - [ConditionalTheory(Skip = "SelectMany #17246")] + // TODO: SelectMany, #17246 public override Task Query_with_owned_entity_equality_object_method(bool async) - => base.Query_with_owned_entity_equality_object_method(async); + => AssertTranslationFailed(() => base.Query_with_owned_entity_equality_object_method(async)); public override Task Query_with_OfType_eagerly_loads_correct_owned_navigations(bool async) => CosmosTestHelpers.Instance.NoSyncTest( @@ -221,45 +257,62 @@ FROM root c """); }); - [ConditionalTheory(Skip = "Distinct ordering #16156")] + // TODO: Subquery pushdown, #33968 public override Task Query_when_subquery(bool async) - => base.Query_when_subquery(async); + => AssertTranslationFailed(() => base.Query_when_subquery(async)); - [ConditionalTheory(Skip = "Count #16146")] public override Task No_ignored_include_warning_when_implicit_load(bool async) - => base.No_ignored_include_warning_when_implicit_load(async); + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.No_ignored_include_warning_when_implicit_load(a); - [ConditionalTheory(Skip = "Skip withouth Take #18923")] - public override Task Client_method_skip_loads_owned_navigations(bool async) - => base.Client_method_skip_loads_owned_navigations(async); + AssertSql( + """ +SELECT COUNT(1) AS c +FROM root c +WHERE c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") +"""); + }); - [ConditionalTheory(Skip = "Skip withouth Take #18923")] - public override Task Client_method_skip_loads_owned_navigations_variation_2(bool async) - => base.Client_method_skip_loads_owned_navigations_variation_2(async); + public override async Task Client_method_skip_loads_owned_navigations(bool async) + { + var exception = await Assert.ThrowsAsync(() => base.Client_method_skip_loads_owned_navigations(async)); - [ConditionalTheory(Skip = "Composition over embedded collection #16926")] + Assert.Equal(CosmosStrings.OffsetRequiresLimit, exception.Message); + } + + public override async Task Client_method_skip_loads_owned_navigations_variation_2(bool async) + { + var exception = + await Assert.ThrowsAsync(() => base.Client_method_skip_loads_owned_navigations_variation_2(async)); + + Assert.Equal(CosmosStrings.OffsetRequiresLimit, exception.Message); + } + + // TODO: SelectMany, #17246 public override Task Where_owned_collection_navigation_ToList_Count(bool async) - => base.Where_owned_collection_navigation_ToList_Count(async); + => AssertTranslationFailed(() => base.Where_owned_collection_navigation_ToList_Count(async)); - [ConditionalTheory(Skip = "Composition over embedded collection #16926")] + // TODO: SelectMany, #17246 public override Task Where_collection_navigation_ToArray_Count(bool async) - => base.Where_collection_navigation_ToArray_Count(async); + => AssertTranslationFailed(() => base.Where_collection_navigation_ToArray_Count(async)); - [ConditionalTheory(Skip = "Composition over embedded collection #16926")] + // TODO: SelectMany, #17246 public override Task Where_collection_navigation_AsEnumerable_Count(bool async) - => base.Where_collection_navigation_AsEnumerable_Count(async); + => AssertTranslationFailed(() => base.Where_collection_navigation_AsEnumerable_Count(async)); - [ConditionalTheory(Skip = "Composition over embedded collection #16926")] + // TODO: SelectMany, #17246 public override Task Where_collection_navigation_ToList_Count_member(bool async) - => base.Where_collection_navigation_ToList_Count_member(async); + => AssertTranslationFailed(() => base.Where_collection_navigation_ToList_Count_member(async)); - [ConditionalTheory(Skip = "Composition over embedded collection #16926")] + // TODO: SelectMany, #17246 public override Task Where_collection_navigation_ToArray_Length_member(bool async) - => base.Where_collection_navigation_ToArray_Length_member(async); + => AssertTranslationFailed(() => base.Where_collection_navigation_ToArray_Length_member(async)); - [ConditionalTheory(Skip = "Issue #16146")] + // TODO: GroupBy, #17313 public override Task GroupBy_with_multiple_aggregates_on_owned_navigation_properties(bool async) - => base.GroupBy_with_multiple_aggregates_on_owned_navigation_properties(async); + => AssertTranslationFailed(() => base.GroupBy_with_multiple_aggregates_on_owned_navigation_properties(async)); public override Task Can_query_on_indexer_properties(bool async) => CosmosTestHelpers.Instance.NoSyncTest( @@ -345,77 +398,109 @@ FROM root c """); }); - [ConditionalTheory(Skip = "OrderBy requires composite index #17246")] public override async Task Can_OrderBy_indexer_properties(bool async) { - await base.Can_OrderBy_indexer_properties(async); + // Always throws for sync. + if (async) + { + var exception = await Assert.ThrowsAsync(() => base.Can_OrderBy_indexer_properties(async)); + + Assert.Contains( + "The order by query does not have a corresponding composite index that it can be served from.", + exception.Message); - AssertSql(" "); + AssertSql( + """ +SELECT c +FROM root c +WHERE c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") +ORDER BY c["Name"], c["Id"] +"""); + } } - [ConditionalTheory(Skip = "OrderBy requires composite index #17246")] public override async Task Can_OrderBy_indexer_properties_converted(bool async) { - await base.Can_OrderBy_indexer_properties_converted(async); + // Always throws for sync. + if (async) + { + var exception = await Assert.ThrowsAsync(() => base.Can_OrderBy_indexer_properties_converted(async)); + + Assert.Contains( + "The order by query does not have a corresponding composite index that it can be served from.", + exception.Message); - AssertSql(" "); + AssertSql( + """ +SELECT c["Name"] +FROM root c +WHERE c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") +ORDER BY c["Name"], c["Id"] +"""); + } } - [ConditionalTheory(Skip = "OrderBy requires composite index #17246")] public override async Task Can_OrderBy_owned_indexer_properties(bool async) { - await base.Can_OrderBy_owned_indexer_properties(async); - - AssertSql(" "); - } + // Always throws for sync. + if (async) + { + var exception = await Assert.ThrowsAsync(() => base.Can_OrderBy_owned_indexer_properties(async)); - [ConditionalTheory(Skip = "OrderBy requires composite index #17246")] - public override async Task Can_OrderBy_owened_indexer_properties_converted(bool async) - { - await base.Can_OrderBy_owened_indexer_properties_converted(async); + Assert.Contains( + "The order by query does not have a corresponding composite index that it can be served from.", + exception.Message); - AssertSql(" "); + AssertSql( + """ +SELECT c["Name"] +FROM root c +WHERE c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") +ORDER BY c["PersonAddress"]["ZipCode"], c["Id"] +"""); + } } - [ConditionalTheory(Skip = "GroupBy #17246")] - public override async Task Can_group_by_indexer_property(bool isAsync) + public override async Task Can_OrderBy_owned_indexer_properties_converted(bool async) { - await base.Can_group_by_indexer_property(isAsync); - - AssertSql(" "); - } + // Always throws for sync. + if (async) + { + var exception = await Assert.ThrowsAsync(() => base.Can_OrderBy_owned_indexer_properties_converted(async)); - [ConditionalTheory(Skip = "GroupBy #17246")] - public override async Task Can_group_by_converted_indexer_property(bool isAsync) - { - await base.Can_group_by_converted_indexer_property(isAsync); + Assert.Contains( + "The order by query does not have a corresponding composite index that it can be served from.", + exception.Message); - AssertSql(" "); + AssertSql( + """ +SELECT c["Name"] +FROM root c +WHERE c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") +ORDER BY c["PersonAddress"]["ZipCode"], c["Id"] +"""); + } } - [ConditionalTheory(Skip = "GroupBy #17246")] - public override async Task Can_group_by_owned_indexer_property(bool isAsync) - { - await base.Can_group_by_owned_indexer_property(isAsync); - - AssertSql(" "); - } + // TODO: GroupBy, #17313 + public override Task Can_group_by_indexer_property(bool async) + => AssertTranslationFailed(() => base.Can_group_by_indexer_property(async)); - [ConditionalTheory(Skip = "GroupBy #17246")] - public override async Task Can_group_by_converted_owned_indexer_property(bool isAsync) - { - await base.Can_group_by_converted_owned_indexer_property(isAsync); + // TODO: GroupBy, #17313 + public override Task Can_group_by_converted_indexer_property(bool async) + => AssertTranslationFailed(() => base.Can_group_by_converted_indexer_property(async)); - AssertSql(" "); - } + // TODO: GroupBy, #17313 + public override Task Can_group_by_owned_indexer_property(bool async) + => AssertTranslationFailed(() => base.Can_group_by_owned_indexer_property(async)); - [ConditionalTheory(Skip = "Join #17246")] - public override async Task Can_join_on_indexer_property_on_query(bool async) - { - await base.Can_join_on_indexer_property_on_query(async); + // TODO: GroupBy, #17313 + public override Task Can_group_by_converted_owned_indexer_property(bool async) + => AssertTranslationFailed(() => base.Can_group_by_converted_owned_indexer_property(async)); - AssertSql(" "); - } + // Uncorrelated JOINS aren't supported by Cosmos + public override Task Can_join_on_indexer_property_on_query(bool async) + => AssertTranslationFailed(() => base.Can_group_by_converted_owned_indexer_property(async)); public override Task Projecting_indexer_property_ignores_include(bool async) => CosmosTestHelpers.Instance.NoSyncTest( @@ -445,76 +530,93 @@ FROM root c """); }); - [ConditionalTheory(Skip = "Subquery #17246")] - public override async Task Indexer_property_is_pushdown_into_subquery(bool isAsync) - { - await base.Indexer_property_is_pushdown_into_subquery(isAsync); + public override Task Indexer_property_is_pushdown_into_subquery(bool async) + => AssertTranslationFailedWithDetails( + () => base.Indexer_property_is_pushdown_into_subquery(async), + CosmosStrings.NonCorrelatedSubqueriesNotSupported); - AssertSql(" "); - } + public override Task Can_query_indexer_property_on_owned_collection(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Can_query_indexer_property_on_owned_collection(a); + + AssertSql( + """ +SELECT c["Name"] +FROM root c +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (( + SELECT VALUE COUNT(1) + FROM t IN c["Orders"] + WHERE (DateTimePart("yyyy", t["OrderDate"]) = 2018)) = 1)) +"""); + }); - [ConditionalTheory(Skip = "Composition over owned collection #17246")] - public override async Task Can_query_indexer_property_on_owned_collection(bool isAsync) + public override async Task NoTracking_Include_with_cycles_throws(bool async) { - await base.Can_query_indexer_property_on_owned_collection(isAsync); + await base.NoTracking_Include_with_cycles_throws(async); - AssertSql(" "); + AssertSql(); } - [ConditionalTheory(Skip = "No SelectMany, No Ability to Include navigation back to owner #17246")] + // TODO: SelectMany, #17246 public override Task NoTracking_Include_with_cycles_does_not_throw_when_performing_identity_resolution( bool async, bool useAsTracking) - => base.NoTracking_Include_with_cycles_does_not_throw_when_performing_identity_resolution(async, useAsTracking); + => AssertTranslationFailed( + () => base.NoTracking_Include_with_cycles_does_not_throw_when_performing_identity_resolution(async, useAsTracking)); - [ConditionalTheory(Skip = "No Composite index to process custom ordering #17246")] - public override async Task Ordering_by_identifying_projection(bool async) + public override async Task Trying_to_access_non_existent_indexer_property_throws_meaningful_exception(bool async) { - await base.Ordering_by_identifying_projection(async); + await base.Trying_to_access_non_existent_indexer_property_throws_meaningful_exception(async); - AssertSql(" "); + AssertSql(); } - [ConditionalTheory(Skip = "Composition over owned collection #17246")] - public override async Task Query_on_collection_entry_works_for_owned_collection(bool isAsync) + public override async Task Ordering_by_identifying_projection(bool async) { - await base.Query_on_collection_entry_works_for_owned_collection(isAsync); - - AssertSql(" "); - } + // Always throws for sync. + if (async) + { + var exception = await Assert.ThrowsAsync(() => base.Ordering_by_identifying_projection(async)); - [ConditionalTheory(Skip = "issue #17246")] - public override async Task Projecting_collection_correlated_with_keyless_entity_after_navigation_works_using_parent_identifiers( - bool isAsync) - { - await base.Projecting_collection_correlated_with_keyless_entity_after_navigation_works_using_parent_identifiers(isAsync); + Assert.Contains( + "The order by query does not have a corresponding composite index that it can be served from.", + exception.Message); - AssertSql(" "); + AssertSql( + """ +SELECT c +FROM root c +WHERE c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") +ORDER BY c["PersonAddress"]["PlaceType"], c["Id"] +"""); + } } - [ConditionalTheory(Skip = "LeftJoin #17314")] - public override async Task Left_join_on_entity_with_owned_navigations(bool async) - { - await base.Left_join_on_entity_with_owned_navigations(async); - - AssertSql(" "); - } + // 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)); - [ConditionalTheory(Skip = "LeftJoin #17314")] - public override async Task Left_join_on_entity_with_owned_navigations_complex(bool async) - { - await base.Left_join_on_entity_with_owned_navigations_complex(async); + // Non-correlated queries not supported by Cosmos + public override Task Projecting_collection_correlated_with_keyless_entity_after_navigation_works_using_parent_identifiers( + bool async) + => AssertTranslationFailed( + () => base.Projecting_collection_correlated_with_keyless_entity_after_navigation_works_using_parent_identifiers(async)); - AssertSql(" "); - } + // Non-correlated queries not supported by Cosmos + public override Task Left_join_on_entity_with_owned_navigations(bool async) + => AssertTranslationFailed( + () => base.Left_join_on_entity_with_owned_navigations(async)); - [ConditionalTheory(Skip = "GroupBy #17314")] - public override async Task GroupBy_aggregate_on_owned_navigation_in_aggregate_selector(bool async) - { - await base.GroupBy_aggregate_on_owned_navigation_in_aggregate_selector(async); + // Non-correlated queries not supported by Cosmos + public override Task Left_join_on_entity_with_owned_navigations_complex(bool async) + => AssertTranslationFailed( + () => base.Left_join_on_entity_with_owned_navigations_complex(async)); - AssertSql(); - } + // TODO: GroupBy, #17313 + public override Task GroupBy_aggregate_on_owned_navigation_in_aggregate_selector(bool async) + => AssertTranslationFailed(() => base.GroupBy_aggregate_on_owned_navigation_in_aggregate_selector(async)); public override Task Filter_on_indexer_using_closure(bool async) => CosmosTestHelpers.Instance.NoSyncTest( @@ -544,6 +646,7 @@ FROM root c """); }); + // Non-correlated queries not supported by Cosmos public override Task Preserve_includes_when_applying_skip_take_after_anonymous_type_select(bool async) => AssertTranslationFailed(() => base.Preserve_includes_when_applying_skip_take_after_anonymous_type_select(async)); @@ -727,6 +830,206 @@ OFFSET 0 LIMIT @__p_0 """); }); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override Task Count_over_owned_collection(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Count_over_owned_collection(a); + + AssertSql( + """ +SELECT c +FROM root c +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(c["Orders"]) = 2)) +"""); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override Task Any_without_predicate_over_owned_collection(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Any_without_predicate_over_owned_collection(a); + + AssertSql( + """ +SELECT c +FROM root c +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(c["Orders"]) > 0)) +"""); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override Task Any_with_predicate_over_owned_collection(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Any_with_predicate_over_owned_collection(a); + + AssertSql( + """ +SELECT c +FROM root c +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND EXISTS ( + SELECT 1 + FROM t IN c["Orders"] + WHERE (t["Id"] = -30))) +"""); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override Task Contains_over_owned_collection(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Contains_over_owned_collection(a); + + AssertSql( + """ +SELECT c +FROM root c +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND EXISTS ( + SELECT 1 + FROM t IN c["Orders"] + WHERE (t["Id"] = -30))) +"""); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override Task ElementAt_over_owned_collection(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.ElementAt_over_owned_collection(a); + + AssertSql( + """ +SELECT c +FROM root c +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (c["Orders"][1]["Id"] = -11)) +"""); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override Task ElementAtOrDefault_over_owned_collection(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.ElementAtOrDefault_over_owned_collection(a); + + AssertSql( + """ +SELECT c +FROM root c +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND ((c["Orders"][10] ?? null)["Id"] = -11)) +"""); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override async Task OrderBy_ElementAt_over_owned_collection(bool async) + { + // Always throws for sync. + if (async) + { + var exception = await Assert.ThrowsAsync(() => base.OrderBy_ElementAt_over_owned_collection(async)); + + Assert.Contains("'ORDER BY' is not supported in subqueries.", exception.Message); + + AssertSql( + """ +SELECT c +FROM root c +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY( + SELECT VALUE t["Id"] + FROM t IN c["Orders"] + ORDER BY t["Id"])[1] = -10)) +"""); + } + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override Task Skip_Take_over_owned_collection(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Skip_Take_over_owned_collection(a); + + AssertSql( + """ +SELECT c +FROM root c +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(ARRAY_SLICE(c["Orders"], 1, 1)) = 1)) +"""); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override Task FirstOrDefault_over_owned_collection(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.FirstOrDefault_over_owned_collection(a); + + AssertSql( + """ +SELECT c +FROM root c +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (DateTimePart("yyyy", (ARRAY( + SELECT VALUE t["OrderDate"] + FROM t IN c["Orders"] + WHERE (t["Id"] > -20))[0] ?? "0001-01-01T00:00:00")) = 2018)) +"""); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override async Task Distinct_over_owned_collection(bool async) + { + // Always throws for sync. + if (async) + { + // TODO: Subquery pushdown, #33968 + await AssertTranslationFailed(() => base.Distinct_over_owned_collection(async)); + + AssertSql(); + } + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public override Task Union_over_owned_collection(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Union_over_owned_collection(a); + + AssertSql( + """ +SELECT c +FROM root c +WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(SetUnion(ARRAY( + SELECT VALUE t + FROM t IN c["Orders"] + WHERE (t["Id"] = -10)), ARRAY( + SELECT VALUE t + FROM t IN c["Orders"] + WHERE (t["Id"] = -11)))) = 2)) +"""); + }); + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index f18b2e36b0c..1e9a610f5f6 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -955,9 +955,7 @@ public override Task Non_nullable_reference_column_collection_index_equals_nulla """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (EXISTS ( - SELECT 1 - FROM i IN c["Strings"]) AND (c["Strings"][1] = c["NullableString"]))) +WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((ARRAY_LENGTH(c["Strings"]) > 0) AND (c["Strings"][1] = c["NullableString"]))) """); }); @@ -1289,15 +1287,13 @@ public override Task Column_collection_Any(bool async) """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND EXISTS ( - SELECT 1 - FROM i IN c["Ints"])) +WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY_LENGTH(c["Ints"]) > 0)) """); }); public override async Task Column_collection_Distinct(bool async) { - // TODO: Count after Distinct requires subquery pushdown + // TODO: Subquery pushdown, #33968 await AssertTranslationFailed(() => base.Column_collection_Distinct(async)); AssertSql(); diff --git a/test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryTest.cs index a3d2439403b..c73804cfe64 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryTest.cs @@ -6,6 +6,21 @@ namespace Microsoft.EntityFrameworkCore.Query; public class OwnedQueryInMemoryTest(OwnedQueryInMemoryTest.OwnedQueryInMemoryFixture fixture) : OwnedQueryTestBase(fixture) { + public override Task Contains_over_owned_collection(bool async) + => Assert.ThrowsAsync(() => base.Contains_over_owned_collection(async)); + + public override Task ElementAt_over_owned_collection(bool async) + => AssertTranslationFailed(() => base.ElementAt_over_owned_collection(async)); + + public override Task ElementAtOrDefault_over_owned_collection(bool async) + => AssertTranslationFailed(() => base.ElementAt_over_owned_collection(async)); + + public override Task FirstOrDefault_over_owned_collection(bool async) + => Assert.ThrowsAsync(() => base.FirstOrDefault_over_owned_collection(async)); + + public override Task OrderBy_ElementAt_over_owned_collection(bool async) + => AssertTranslationFailed(() => base.OrderBy_ElementAt_over_owned_collection(async)); + public class OwnedQueryInMemoryFixture : OwnedQueryFixtureBase { protected override ITestStoreFactory TestStoreFactory diff --git a/test/EFCore.Relational.Specification.Tests/Query/OwnedQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/OwnedQueryRelationalTestBase.cs index ae280ebc4b8..ce8c287aeba 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/OwnedQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/OwnedQueryRelationalTestBase.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; + namespace Microsoft.EntityFrameworkCore.Query; #nullable disable @@ -13,6 +15,26 @@ protected OwnedQueryRelationalTestBase(TFixture fixture) { } + public override Task Contains_over_owned_collection(bool async) + => Assert.ThrowsAsync(() => base.Contains_over_owned_collection(async)); + + // The query uses a row limiting operator ('Skip'/'Take') without an 'OrderBy' operator. + public override Task ElementAt_over_owned_collection(bool async) + => Assert.ThrowsAsync(() => base.ElementAt_over_owned_collection(async)); + + // The query uses a row limiting operator ('Skip'/'Take') without an 'OrderBy' operator. + public override Task ElementAtOrDefault_over_owned_collection(bool async) + => Assert.ThrowsAsync(() => base.ElementAtOrDefault_over_owned_collection(async)); + + // The query uses a row limiting operator ('Skip'/'Take') without an 'OrderBy' operator. + public override Task Skip_Take_over_owned_collection(bool async) + => Assert.ThrowsAsync(() => base.Skip_Take_over_owned_collection(async)); + + // This test is non-deterministic on relational, since FirstOrDefault is used without an ordering. + // Since this is FirstOrDefault with a filter, we don't issue our usual "missing ordering" warning (see #33997). + public override Task FirstOrDefault_over_owned_collection(bool async) + => Task.CompletedTask; + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Query_for_base_type_loads_all_owned_navs_split(bool async) diff --git a/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs index fa171178571..7708a68f809 100644 --- a/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs @@ -596,7 +596,7 @@ public virtual Task Can_OrderBy_owned_indexer_properties(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Can_OrderBy_owened_indexer_properties_converted(bool async) + public virtual Task Can_OrderBy_owned_indexer_properties_converted(bool async) => AssertQuery( async, ss => ss.Set().OrderBy(c => (int)c.PersonAddress["ZipCode"]).ThenBy(c => c.Id).Select(c => (string)c["Name"]), @@ -762,7 +762,7 @@ public virtual async Task Query_on_collection_entry_works_for_owned_collection(b { using var context = CreateContext(); - var ownedPerson = context.Set().AsTracking().Single(e => e.Id == 1); + var ownedPerson = await context.Set().AsTracking().SingleAsync(e => e.Id == 1); var collectionQuery = context.Entry(ownedPerson).Collection(e => e.Orders).Query().AsNoTracking(); var actualOrders = async @@ -900,6 +900,91 @@ public virtual Task GroupBy_aggregate_on_owned_navigation_in_aggregate_selector( AssertEqual(e.Sum, a.Sum); }); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Count_over_owned_collection(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(p => p.Orders.Count == 2)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Any_without_predicate_over_owned_collection(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(p => p.Orders.Any())); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Any_with_predicate_over_owned_collection(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(p => p.Orders.Any(i => i.Id == -30))); + + // TODO: proper owned entity containment, #34027 + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Contains_over_owned_collection(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(p => p.Orders.Contains(new Order { Id = -30 })), + ss => ss.Set().Where(p => p.Orders.Any(o => o.Id == -30))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task ElementAt_over_owned_collection(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(p => p.Orders.ElementAt(1).Id == -11), + ss => ss.Set().Where(p => p.Orders.Count >= 2 && p.Orders.ElementAt(1).Id == -11)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task ElementAtOrDefault_over_owned_collection(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(p => p.Orders.ElementAtOrDefault(10).Id == -11), + ss => ss.Set().Where(p => p.Orders.Count >= 11 && p.Orders.ElementAtOrDefault(1).Id == -11), + assertEmpty: true); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task OrderBy_ElementAt_over_owned_collection(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(p => p.Orders.OrderBy(o => o.Id).ElementAt(1).Id == -10), + ss => ss.Set().Where(p => p.Orders.Count >= 2 && p.Orders.OrderBy(o => o.Id).ElementAt(1).Id == -10)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Skip_Take_over_owned_collection(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(p => p.Orders.Skip(1).Take(1).Count() == 1)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task FirstOrDefault_over_owned_collection(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(p => ((DateTime)p.Orders.FirstOrDefault(o => o.Id > -20)["OrderDate"]).Year == 2018), + ss => ss.Set().Where(p => p.Orders.FirstOrDefault(o => o.Id > -20) != null && ((DateTime)p.Orders.FirstOrDefault(o => o.Id > -20)["OrderDate"]).Year == 2018)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Distinct_over_owned_collection(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(p => p.Orders.Distinct().Count() == 2)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_owned_collection(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Where(p => p.Orders.Where(o => o.Id == -10).Union(p.Orders.Where(o => o.Id == -11)).Count() == 2)); + protected virtual DbContext CreateContext() => Fixture.CreateContext(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs index da8eeda5899..e3f6baa47cb 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs @@ -902,9 +902,9 @@ FROM [OwnedPerson] AS [o] """); } - public override async Task Can_OrderBy_owened_indexer_properties_converted(bool async) + public override async Task Can_OrderBy_owned_indexer_properties_converted(bool async) { - await base.Can_OrderBy_owened_indexer_properties_converted(async); + await base.Can_OrderBy_owned_indexer_properties_converted(async); AssertSql( """ @@ -1477,6 +1477,179 @@ GROUP BY [o].[Id] """); } + public override async Task Count_over_owned_collection(bool async) + { + await base.Count_over_owned_collection(async); + + AssertSql( + """ +SELECT [o].[Id], [o].[Discriminator], [o].[Name], [s].[ClientId], [s].[Id], [s].[OrderDate], [s].[OrderClientId], [s].[OrderId], [s].[Id0], [s].[Detail], [o].[PersonAddress_AddressLine], [o].[PersonAddress_PlaceType], [o].[PersonAddress_ZipCode], [o].[PersonAddress_Country_Name], [o].[PersonAddress_Country_PlanetId], [o].[BranchAddress_BranchName], [o].[BranchAddress_PlaceType], [o].[BranchAddress_Country_Name], [o].[BranchAddress_Country_PlanetId], [o].[LeafBAddress_LeafBType], [o].[LeafBAddress_PlaceType], [o].[LeafBAddress_Country_Name], [o].[LeafBAddress_Country_PlanetId], [o].[LeafAAddress_LeafType], [o].[LeafAAddress_PlaceType], [o].[LeafAAddress_Country_Name], [o].[LeafAAddress_Country_PlanetId] +FROM [OwnedPerson] AS [o] +LEFT JOIN ( + SELECT [o1].[ClientId], [o1].[Id], [o1].[OrderDate], [o2].[OrderClientId], [o2].[OrderId], [o2].[Id] AS [Id0], [o2].[Detail] + FROM [Order] AS [o1] + LEFT JOIN [OrderDetail] AS [o2] ON [o1].[ClientId] = [o2].[OrderClientId] AND [o1].[Id] = [o2].[OrderId] +) AS [s] ON [o].[Id] = [s].[ClientId] +WHERE ( + SELECT COUNT(*) + FROM [Order] AS [o0] + WHERE [o].[Id] = [o0].[ClientId]) = 2 +ORDER BY [o].[Id], [s].[ClientId], [s].[Id], [s].[OrderClientId], [s].[OrderId] +"""); + } + + public override async Task Any_without_predicate_over_owned_collection(bool async) + { + await base.Any_without_predicate_over_owned_collection(async); + + AssertSql( + """ +SELECT [o].[Id], [o].[Discriminator], [o].[Name], [s].[ClientId], [s].[Id], [s].[OrderDate], [s].[OrderClientId], [s].[OrderId], [s].[Id0], [s].[Detail], [o].[PersonAddress_AddressLine], [o].[PersonAddress_PlaceType], [o].[PersonAddress_ZipCode], [o].[PersonAddress_Country_Name], [o].[PersonAddress_Country_PlanetId], [o].[BranchAddress_BranchName], [o].[BranchAddress_PlaceType], [o].[BranchAddress_Country_Name], [o].[BranchAddress_Country_PlanetId], [o].[LeafBAddress_LeafBType], [o].[LeafBAddress_PlaceType], [o].[LeafBAddress_Country_Name], [o].[LeafBAddress_Country_PlanetId], [o].[LeafAAddress_LeafType], [o].[LeafAAddress_PlaceType], [o].[LeafAAddress_Country_Name], [o].[LeafAAddress_Country_PlanetId] +FROM [OwnedPerson] AS [o] +LEFT JOIN ( + SELECT [o1].[ClientId], [o1].[Id], [o1].[OrderDate], [o2].[OrderClientId], [o2].[OrderId], [o2].[Id] AS [Id0], [o2].[Detail] + FROM [Order] AS [o1] + LEFT JOIN [OrderDetail] AS [o2] ON [o1].[ClientId] = [o2].[OrderClientId] AND [o1].[Id] = [o2].[OrderId] +) AS [s] ON [o].[Id] = [s].[ClientId] +WHERE EXISTS ( + SELECT 1 + FROM [Order] AS [o0] + WHERE [o].[Id] = [o0].[ClientId]) +ORDER BY [o].[Id], [s].[ClientId], [s].[Id], [s].[OrderClientId], [s].[OrderId] +"""); + } + + public override async Task Any_with_predicate_over_owned_collection(bool async) + { + await base.Any_with_predicate_over_owned_collection(async); + + AssertSql( + """ +SELECT [o].[Id], [o].[Discriminator], [o].[Name], [s].[ClientId], [s].[Id], [s].[OrderDate], [s].[OrderClientId], [s].[OrderId], [s].[Id0], [s].[Detail], [o].[PersonAddress_AddressLine], [o].[PersonAddress_PlaceType], [o].[PersonAddress_ZipCode], [o].[PersonAddress_Country_Name], [o].[PersonAddress_Country_PlanetId], [o].[BranchAddress_BranchName], [o].[BranchAddress_PlaceType], [o].[BranchAddress_Country_Name], [o].[BranchAddress_Country_PlanetId], [o].[LeafBAddress_LeafBType], [o].[LeafBAddress_PlaceType], [o].[LeafBAddress_Country_Name], [o].[LeafBAddress_Country_PlanetId], [o].[LeafAAddress_LeafType], [o].[LeafAAddress_PlaceType], [o].[LeafAAddress_Country_Name], [o].[LeafAAddress_Country_PlanetId] +FROM [OwnedPerson] AS [o] +LEFT JOIN ( + SELECT [o1].[ClientId], [o1].[Id], [o1].[OrderDate], [o2].[OrderClientId], [o2].[OrderId], [o2].[Id] AS [Id0], [o2].[Detail] + FROM [Order] AS [o1] + LEFT JOIN [OrderDetail] AS [o2] ON [o1].[ClientId] = [o2].[OrderClientId] AND [o1].[Id] = [o2].[OrderId] +) AS [s] ON [o].[Id] = [s].[ClientId] +WHERE EXISTS ( + SELECT 1 + FROM [Order] AS [o0] + WHERE [o].[Id] = [o0].[ClientId] AND [o0].[Id] = -30) +ORDER BY [o].[Id], [s].[ClientId], [s].[Id], [s].[OrderClientId], [s].[OrderId] +"""); + } + + public override async Task Contains_over_owned_collection(bool async) + { + await base.Contains_over_owned_collection(async); + + AssertSql(); + } + + public override async Task ElementAt_over_owned_collection(bool async) + { + await base.ElementAt_over_owned_collection(async); + + AssertSql(); + } + + public override async Task ElementAtOrDefault_over_owned_collection(bool async) + { + await base.ElementAtOrDefault_over_owned_collection(async); + + AssertSql(); + } + + public override async Task OrderBy_ElementAt_over_owned_collection(bool async) + { + await base.OrderBy_ElementAt_over_owned_collection(async); + + AssertSql( + """ +SELECT [o].[Id], [o].[Discriminator], [o].[Name], [s].[ClientId], [s].[Id], [s].[OrderDate], [s].[OrderClientId], [s].[OrderId], [s].[Id0], [s].[Detail], [o].[PersonAddress_AddressLine], [o].[PersonAddress_PlaceType], [o].[PersonAddress_ZipCode], [o].[PersonAddress_Country_Name], [o].[PersonAddress_Country_PlanetId], [o].[BranchAddress_BranchName], [o].[BranchAddress_PlaceType], [o].[BranchAddress_Country_Name], [o].[BranchAddress_Country_PlanetId], [o].[LeafBAddress_LeafBType], [o].[LeafBAddress_PlaceType], [o].[LeafBAddress_Country_Name], [o].[LeafBAddress_Country_PlanetId], [o].[LeafAAddress_LeafType], [o].[LeafAAddress_PlaceType], [o].[LeafAAddress_Country_Name], [o].[LeafAAddress_Country_PlanetId] +FROM [OwnedPerson] AS [o] +LEFT JOIN ( + SELECT [o1].[ClientId], [o1].[Id], [o1].[OrderDate], [o2].[OrderClientId], [o2].[OrderId], [o2].[Id] AS [Id0], [o2].[Detail] + FROM [Order] AS [o1] + LEFT JOIN [OrderDetail] AS [o2] ON [o1].[ClientId] = [o2].[OrderClientId] AND [o1].[Id] = [o2].[OrderId] +) AS [s] ON [o].[Id] = [s].[ClientId] +WHERE ( + SELECT [o0].[Id] + FROM [Order] AS [o0] + WHERE [o].[Id] = [o0].[ClientId] + ORDER BY [o0].[Id] + OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY) = -10 +ORDER BY [o].[Id], [s].[ClientId], [s].[Id], [s].[OrderClientId], [s].[OrderId] +"""); + } + + public override async Task Skip_Take_over_owned_collection(bool async) + { + await base.Skip_Take_over_owned_collection(async); + + AssertSql(); + } + + public override async Task FirstOrDefault_over_owned_collection(bool async) + { + await base.FirstOrDefault_over_owned_collection(async); + + AssertSql(); + } + + public override async Task Distinct_over_owned_collection(bool async) + { + await base.Distinct_over_owned_collection(async); + + AssertSql( + """ +SELECT [o].[Id], [o].[Discriminator], [o].[Name], [s].[ClientId], [s].[Id], [s].[OrderDate], [s].[OrderClientId], [s].[OrderId], [s].[Id0], [s].[Detail], [o].[PersonAddress_AddressLine], [o].[PersonAddress_PlaceType], [o].[PersonAddress_ZipCode], [o].[PersonAddress_Country_Name], [o].[PersonAddress_Country_PlanetId], [o].[BranchAddress_BranchName], [o].[BranchAddress_PlaceType], [o].[BranchAddress_Country_Name], [o].[BranchAddress_Country_PlanetId], [o].[LeafBAddress_LeafBType], [o].[LeafBAddress_PlaceType], [o].[LeafBAddress_Country_Name], [o].[LeafBAddress_Country_PlanetId], [o].[LeafAAddress_LeafType], [o].[LeafAAddress_PlaceType], [o].[LeafAAddress_Country_Name], [o].[LeafAAddress_Country_PlanetId] +FROM [OwnedPerson] AS [o] +LEFT JOIN ( + SELECT [o2].[ClientId], [o2].[Id], [o2].[OrderDate], [o3].[OrderClientId], [o3].[OrderId], [o3].[Id] AS [Id0], [o3].[Detail] + FROM [Order] AS [o2] + LEFT JOIN [OrderDetail] AS [o3] ON [o2].[ClientId] = [o3].[OrderClientId] AND [o2].[Id] = [o3].[OrderId] +) AS [s] ON [o].[Id] = [s].[ClientId] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT DISTINCT [o0].[ClientId], [o0].[Id], [o0].[OrderDate] + FROM [Order] AS [o0] + WHERE [o].[Id] = [o0].[ClientId] + ) AS [o1]) = 2 +ORDER BY [o].[Id], [s].[ClientId], [s].[Id], [s].[OrderClientId], [s].[OrderId] +"""); + } + + public override async Task Union_over_owned_collection(bool async) + { + await base.Union_over_owned_collection(async); + + AssertSql( + """ +SELECT [o].[Id], [o].[Discriminator], [o].[Name], [s].[ClientId], [s].[Id], [s].[OrderDate], [s].[OrderClientId], [s].[OrderId], [s].[Id0], [s].[Detail], [o].[PersonAddress_AddressLine], [o].[PersonAddress_PlaceType], [o].[PersonAddress_ZipCode], [o].[PersonAddress_Country_Name], [o].[PersonAddress_Country_PlanetId], [o].[BranchAddress_BranchName], [o].[BranchAddress_PlaceType], [o].[BranchAddress_Country_Name], [o].[BranchAddress_Country_PlanetId], [o].[LeafBAddress_LeafBType], [o].[LeafBAddress_PlaceType], [o].[LeafBAddress_Country_Name], [o].[LeafBAddress_Country_PlanetId], [o].[LeafAAddress_LeafType], [o].[LeafAAddress_PlaceType], [o].[LeafAAddress_Country_Name], [o].[LeafAAddress_Country_PlanetId] +FROM [OwnedPerson] AS [o] +LEFT JOIN ( + SELECT [o2].[ClientId], [o2].[Id], [o2].[OrderDate], [o3].[OrderClientId], [o3].[OrderId], [o3].[Id] AS [Id0], [o3].[Detail] + FROM [Order] AS [o2] + LEFT JOIN [OrderDetail] AS [o3] ON [o2].[ClientId] = [o3].[OrderClientId] AND [o2].[Id] = [o3].[OrderId] +) AS [s] ON [o].[Id] = [s].[ClientId] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT [o0].[ClientId], [o0].[Id], [o0].[OrderDate] + FROM [Order] AS [o0] + WHERE [o].[Id] = [o0].[ClientId] AND [o0].[Id] = -10 + UNION + SELECT [o1].[ClientId], [o1].[Id], [o1].[OrderDate] + FROM [Order] AS [o1] + WHERE [o].[Id] = [o1].[ClientId] AND [o1].[Id] = -11 + ) AS [u]) = 2 +ORDER BY [o].[Id], [s].[ClientId], [s].[Id], [s].[OrderClientId], [s].[OrderId] +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalOwnedQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalOwnedQuerySqlServerTest.cs index 6d44b5c618f..f1b85d890ab 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalOwnedQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalOwnedQuerySqlServerTest.cs @@ -916,9 +916,9 @@ SELECT [o].[Name] """); } - public override async Task Can_OrderBy_owened_indexer_properties_converted(bool async) + public override async Task Can_OrderBy_owned_indexer_properties_converted(bool async) { - await base.Can_OrderBy_owened_indexer_properties_converted(async); + await base.Can_OrderBy_owned_indexer_properties_converted(async); AssertSql( """