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..9928df66c65 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(")");
+ }
+
+ ///
+ /// 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 arrayExpression;
+ 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);
+
+ // 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 keyAccessExpression;
+ 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;
}
@@ -755,6 +807,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 +827,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..a221fc8c600 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).
+ 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 c42ae691446..825475606d6 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs
@@ -232,6 +232,59 @@ 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
+ && innerMethodCall.Method.GetGenericMethodDefinition() == QueryableMethods.Select:
+ {
+ var returnDefault = method.Name.EndsWith("OrDefault", StringComparison.Ordinal);
+ 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)
{
@@ -316,6 +369,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
@@ -344,6 +413,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();
@@ -418,6 +499,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
&& TranslateExpression(item) is SqlExpression translatedItem)
{
if (array is ArrayConstantExpression arrayConstant)
@@ -426,8 +508,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));
}
@@ -447,45 +529,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
@@ -531,28 +575,57 @@ 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);
+
+ 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 os structural types
+ case not null when projectedStructuralTypeShaper is not null:
+ {
+ var translation = new ObjectArrayIndexExpression(array, translatedIndex, projectedStructuralTypeShaper.Type);
+
+ if (returnDefault)
+ {
+ // TODO
+ throw new InvalidOperationException("ElementAtOrDefault over array of entity types is not supported.");
+ }
+
+ 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);
@@ -731,36 +804,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
@@ -1021,23 +1065,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);
+ // 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);
+ }
- // 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 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
@@ -1118,27 +1186,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);
+ // 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);
+ }
- // 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 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
@@ -1364,21 +1461,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;
@@ -1416,17 +1542,12 @@ when methodCallExpression.TryGetIndexerArguments(_queryCompilationContext.Model,
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);
}
///
@@ -1452,44 +1573,61 @@ [new ProjectionExpression(inlineArray, null!)],
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,
@@ -1499,18 +1637,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/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..f2e14843c2d 100644
--- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs
+++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs
@@ -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..0f9ce4b8a13 100644
--- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs
+++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs
@@ -25,10 +25,10 @@ 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),
@@ -39,11 +39,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 +65,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 +99,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 +185,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 VisitKeyAccess(KeyAccessExpression keyAccessExpression);
+ protected abstract Expression VisitScalarAccess(ScalarAccessExpression scalarAccessExpression);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -198,7 +225,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..0fc3a77638b 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);
+
+ AssertSql(
+ """
+SELECT COUNT(1) AS c
+FROM root c
+WHERE c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA")
+""");
+ });
+
+ 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 = "Skip withouth Take #18923")]
- public override Task Client_method_skip_loads_owned_navigations(bool async)
- => base.Client_method_skip_loads_owned_navigations(async);
+ 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));
- [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);
+ Assert.Equal(CosmosStrings.OffsetRequiresLimit, exception.Message);
+ }
- [ConditionalTheory(Skip = "Composition over embedded collection #16926")]
+ // 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));
- AssertSql(" ");
+ Assert.Contains(
+ "The order by query does not have a corresponding composite index that it can be served from.",
+ exception.Message);
+
+ 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));
- AssertSql(" ");
+ Assert.Contains(
+ "The order by query does not have a corresponding composite index that it can be served from.",
+ exception.Message);
+
+ 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);
+ // Always throws for sync.
+ if (async)
+ {
+ var exception = await Assert.ThrowsAsync(() => base.Ordering_by_identifying_projection(async));
- AssertSql(" ");
- }
+ Assert.Contains(
+ "The order by query does not have a corresponding composite index that it can be served from.",
+ exception.Message);
- [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);
-
- 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);
+ // 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));
- AssertSql(" ");
- }
+ // 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));
- [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 Left_join_on_entity_with_owned_navigations(bool async)
+ => AssertTranslationFailed(
+ () => base.Left_join_on_entity_with_owned_navigations(async));
- AssertSql(" ");
- }
+ // 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));
- [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);
-
- 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,190 @@ 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 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 ab6c7346da7..19fcafaa10a 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..c4247a29bdf 100644
--- a/test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryTest.cs
+++ b/test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryTest.cs
@@ -6,6 +6,18 @@ 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 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..0b543f1f93f 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,22 @@ 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 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..523bdba8db5 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,81 @@ 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)));
+
+ [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 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..5d706e5d1a0 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,172 @@ 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 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(
"""