diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs
index ad7425bf61c..f3e5c259847 100644
--- a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs
+++ b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs
@@ -37,14 +37,6 @@ public static string AnalyticalTTLMismatch(object? ttl1, object? entityType1, ob
public static string CanConnectNotSupported
=> GetString("CanConnectNotSupported");
- ///
- /// The query contained a new array expression containing non-constant elements, which could not be translated: '{newArrayExpression}'.
- ///
- public static string CannotTranslateNonConstantNewArrayExpression(object? newArrayExpression)
- => string.Format(
- GetString("CannotTranslateNonConstantNewArrayExpression", nameof(newArrayExpression)),
- newArrayExpression);
-
///
/// None of connection string, CredentialToken, account key or account endpoint were specified. Specify a set of connection details.
///
@@ -89,6 +81,12 @@ public static string ETagNonStringStoreType(object? property, object? entityType
GetString("ETagNonStringStoreType", nameof(property), nameof(entityType), nameof(propertyType)),
property, entityType, propertyType);
+ ///
+ /// The 'Except()' LINQ operator isn't supported by Cosmos.
+ ///
+ public static string ExceptNotSupported
+ => GetString("ExceptNotSupported");
+
///
/// The type of the '{idProperty}' property on '{entityType}' is '{propertyType}'. All 'id' properties must be strings or have a string value converter.
///
@@ -173,6 +171,12 @@ public static string NoIdProperty(object? entityType)
GetString("NoIdProperty", nameof(entityType)),
entityType);
+ ///
+ /// Cosmos subqueries must be correlated, referencing values from the outer query.
+ ///
+ public static string NonCorrelatedSubqueriesNotSupported
+ => GetString("NonCorrelatedSubqueriesNotSupported");
+
///
/// Including navigation '{navigation}' is not supported as the navigation is not embedded in same resource.
///
diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.resx b/src/EFCore.Cosmos/Properties/CosmosStrings.resx
index 3243c41e5c1..b432bfe31a1 100644
--- a/src/EFCore.Cosmos/Properties/CosmosStrings.resx
+++ b/src/EFCore.Cosmos/Properties/CosmosStrings.resx
@@ -123,9 +123,6 @@
The Cosmos database does not support 'CanConnect' or 'CanConnectAsync'.
-
- The query contained a new array expression containing non-constant elements, which could not be translated: '{newArrayExpression}'.
-
None of connection string, CredentialToken, account key or account endpoint were specified. Specify a set of connection details.
@@ -144,6 +141,9 @@
The type of the etag property '{property}' on '{entityType}' is '{propertyType}'. All etag properties must be strings or have a string value converter.
+
+ The 'Except()' LINQ operator isn't supported by Cosmos.
+
The type of the '{idProperty}' property on '{entityType}' is '{propertyType}'. All 'id' properties must be strings or have a string value converter.
@@ -213,6 +213,9 @@
The entity type '{entityType}' does not have a property mapped to the 'id' property in the database. Add a property mapped to 'id'.
+
+ Cosmos subqueries must be correlated, referencing values from the outer query.
+
Including navigation '{navigation}' is not supported as the navigation is not embedded in same resource.
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryRootProcessor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryRootProcessor.cs
new file mode 100644
index 00000000000..f7d4f62bd99
--- /dev/null
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryRootProcessor.cs
@@ -0,0 +1,56 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.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 CosmosQueryRootProcessor : QueryRootProcessor
+{
+ private readonly IModel _model;
+
+ ///
+ /// 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 CosmosQueryRootProcessor(QueryTranslationPreprocessorDependencies dependencies, QueryCompilationContext queryCompilationContext)
+ : base(dependencies, queryCompilationContext)
+ {
+ _model = queryCompilationContext.Model;
+ }
+
+ ///
+ /// 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 bool ShouldConvertToInlineQueryRoot(Expression expression)
+ => true;
+
+ ///
+ /// 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 bool ShouldConvertToParameterQueryRoot(ParameterExpression parameterExpression)
+ => true;
+
+ ///
+ protected override Expression VisitExtension(Expression node)
+ => node switch
+ {
+ // We skip FromSqlQueryRootExpression, since that contains the arguments as an object array parameter, and don't want to convert
+ // that to a query root
+ FromSqlQueryRootExpression e => e,
+
+ _ => base.VisitExtension(node)
+ };
+}
diff --git a/src/EFCore.Cosmos/Query/Internal/QuerySqlGenerator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs
similarity index 68%
rename from src/EFCore.Cosmos/Query/Internal/QuerySqlGenerator.cs
rename to src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs
index 60376aa8361..6054109b01f 100644
--- a/src/EFCore.Cosmos/Query/Internal/QuerySqlGenerator.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs
@@ -14,12 +14,11 @@ 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 QuerySqlGenerator(ITypeMappingSource typeMappingSource) : SqlExpressionVisitor
+public class CosmosQuerySqlGenerator(ITypeMappingSource typeMappingSource) : SqlExpressionVisitor
{
private readonly IndentedStringBuilder _sqlBuilder = new();
private IReadOnlyDictionary _parameterValues = null!;
private List _sqlParameters = null!;
- private bool _useValueProjection;
private ParameterNameGenerator _parameterNameGenerator = null!;
private readonly IDictionary _operatorMap = new Dictionary
@@ -89,6 +88,46 @@ protected override Expression VisitEntityProjection(EntityProjectionExpression e
return entityProjectionExpression;
}
+ ///
+ /// 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 VisitExists(ExistsExpression existsExpression)
+ {
+ _sqlBuilder.AppendLine("EXISTS (");
+
+ using (_sqlBuilder.Indent())
+ {
+ Visit(existsExpression.Subquery);
+ }
+
+ _sqlBuilder.Append(")");
+
+ return existsExpression;
+ }
+
+ ///
+ /// 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 VisitArray(ArrayExpression arrayExpression)
+ {
+ _sqlBuilder.AppendLine("ARRAY (");
+
+ using (_sqlBuilder.Indent())
+ {
+ Visit(arrayExpression.Subquery);
+ }
+
+ _sqlBuilder.Append(")");
+
+ return arrayExpression;
+ }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -128,6 +167,25 @@ protected override Expression VisitObjectAccess(ObjectAccessExpression objectAcc
return objectAccessExpression;
}
+ ///
+ /// 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 VisitScalarSubquery(ScalarSubqueryExpression scalarSubqueryExpression)
+ {
+ _sqlBuilder.AppendLine("(");
+ using (_sqlBuilder.Indent())
+ {
+ Visit(scalarSubqueryExpression.Subquery);
+ }
+
+ _sqlBuilder.Append(")");
+
+ return scalarSubqueryExpression;
+ }
+
///
/// 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
@@ -135,15 +193,24 @@ protected override Expression VisitObjectAccess(ObjectAccessExpression objectAcc
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
protected override Expression VisitProjection(ProjectionExpression projectionExpression)
+ => VisitProjection(projectionExpression, objectProjectionStyle: false);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// 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 virtual Expression VisitProjection(ProjectionExpression projectionExpression, bool objectProjectionStyle)
{
- if (_useValueProjection)
+ if (objectProjectionStyle)
{
_sqlBuilder.Append('"').Append(projectionExpression.Alias).Append("\" : ");
}
Visit(projectionExpression.Expression);
- if (!_useValueProjection
+ if (!objectProjectionStyle
&& !string.IsNullOrEmpty(projectionExpression.Alias)
&& projectionExpression.Alias != projectionExpression.Name)
{
@@ -159,11 +226,24 @@ protected override Expression VisitProjection(ProjectionExpression projectionExp
/// 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 VisitRootReference(RootReferenceExpression rootReferenceExpression)
+ protected override Expression VisitObjectReference(ObjectReferenceExpression objectReferenceExpression)
{
- _sqlBuilder.Append(rootReferenceExpression.ToString());
+ _sqlBuilder.Append(objectReferenceExpression.Name);
- return rootReferenceExpression;
+ return objectReferenceExpression;
+ }
+
+ ///
+ /// 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 VisitValueReference(ScalarReferenceExpression scalarReferenceExpression)
+ {
+ _sqlBuilder.Append(scalarReferenceExpression.Name);
+
+ return scalarReferenceExpression;
}
///
@@ -181,20 +261,39 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
_sqlBuilder.Append("DISTINCT ");
}
- if (selectExpression.Projection.Count > 0)
+ if (selectExpression.Projection is { Count: > 0 } projection)
{
- if (selectExpression.Projection.Any(p => !string.IsNullOrEmpty(p.Alias) && p.Alias != p.Name)
- && !selectExpression.Projection.Any(p => p.Expression is SqlFunctionExpression)) // Aggregates are not allowed
+ // If the SELECT projects a single value out, we just project that with the Cosmos VALUE keyword (without VALUE,
+ // Cosmos projects a JSON object containing the value).
+ if (selectExpression.UsesSingleValueProjection)
+ {
+ _sqlBuilder.Append("VALUE ");
+
+ if (projection is not [var singleProjection])
+ {
+ throw new UnreachableException(
+ $"Encountered SelectExpression with UsesValueProject=true and Projection.Count={projection.Count}.");
+ }
+
+ Visit(singleProjection.Expression);
+ }
+ // Otherwise, we'll project a JSON object; Cosmos has two syntaxes for doing so:
+ // 1. Project out a JSON object as a value (SELECT VALUE { 'a': a, 'b': b }), or
+ // 2. Project a set of properties with optional AS+aliases (SELECT 'a' AS a, 'b' AS b)
+ // Both methods produce the exact same results; we usually prefer the 1st, but in some cases we use the 2nd.
+ else if ((projection.Count > 1
+ // Cosmos does not support "AS Value" projections, specifically for the alias "Value"
+ || projection is [{ Alias: var alias }] && alias.Equals("value", StringComparison.OrdinalIgnoreCase))
+ && projection.Any(p => !string.IsNullOrEmpty(p.Alias) && p.Alias != p.Name)
+ && !projection.Any(p => p.Expression is SqlFunctionExpression)) // Aggregates are not allowed
{
- _useValueProjection = true;
- _sqlBuilder.Append("VALUE {");
- GenerateList(selectExpression.Projection, e => Visit(e));
- _sqlBuilder.Append('}');
- _useValueProjection = false;
+ _sqlBuilder.AppendLine("VALUE").AppendLine("{").IncrementIndent();
+ GenerateList(projection, e => VisitProjection(e, objectProjectionStyle: true), joinAction: sql => sql.AppendLine(","));
+ _sqlBuilder.AppendLine().DecrementIndent().Append("}");
}
else
{
- GenerateList(selectExpression.Projection, e => Visit(e));
+ GenerateList(projection, e => Visit(e));
}
}
else
@@ -202,11 +301,17 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
_sqlBuilder.Append('1');
}
- _sqlBuilder.AppendLine();
+ if (selectExpression.Sources.Count > 0)
+ {
+ if (selectExpression.Sources.Count > 1)
+ {
+ throw new NotImplementedException("JOINs not yet supported");
+ }
- _sqlBuilder.Append(selectExpression.FromExpression is FromSqlExpression ? "FROM " : "FROM root ");
+ _sqlBuilder.AppendLine().Append("FROM ");
- Visit(selectExpression.FromExpression);
+ Visit(selectExpression.Sources[0]);
+ }
if (selectExpression.Predicate != null)
{
@@ -311,9 +416,7 @@ fromSqlExpression.Arguments is ConstantExpression constantExpression
_sqlBuilder.AppendLines(sql);
}
- _sqlBuilder
- .Append(") ")
- .Append(fromSqlExpression.Alias);
+ _sqlBuilder.Append(")");
return fromSqlExpression;
}
@@ -344,6 +447,16 @@ protected override Expression VisitOrdering(OrderingExpression orderingExpressio
///
protected override Expression VisitSqlBinary(SqlBinaryExpression sqlBinaryExpression)
{
+ if (sqlBinaryExpression.OperatorType is ExpressionType.ArrayIndex)
+ {
+ Visit(sqlBinaryExpression.Left);
+ _sqlBuilder.Append('[');
+ Visit(sqlBinaryExpression.Right);
+ _sqlBuilder.Append(']');
+
+ return sqlBinaryExpression;
+ }
+
var op = _operatorMap[sqlBinaryExpression.OperatorType];
_sqlBuilder.Append('(');
Visit(sqlBinaryExpression.Left);
@@ -510,6 +623,112 @@ protected sealed override Expression VisitIn(InExpression inExpression)
return inExpression;
}
+ ///
+ /// 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 VisitArrayConstant(ArrayConstantExpression arrayConstantExpression)
+ {
+ _sqlBuilder.Append("[");
+
+ var items = arrayConstantExpression.Items;
+ for (var i = 0; i < items.Count; i++)
+ {
+ if (i > 0)
+ {
+ _sqlBuilder.Append(", ");
+ }
+
+ Visit(items[i]);
+ }
+
+ _sqlBuilder.Append("]");
+
+ return arrayConstantExpression;
+ }
+
+ ///
+ /// 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 sealed override Expression VisitSource(SourceExpression sourceExpression)
+ {
+ // https://learn.microsoft.com/azure/cosmos-db/nosql/query/from
+ if (sourceExpression.WithIn)
+ {
+ if (sourceExpression.Alias is null)
+ {
+ throw new UnreachableException("Alias cannot be null when WithIn is true");
+ }
+
+ _sqlBuilder
+ .Append(sourceExpression.Alias)
+ .Append(" IN ");
+
+
+ VisitContainerExpression(sourceExpression.ContainerExpression);
+ }
+ else
+ {
+ VisitContainerExpression(sourceExpression.ContainerExpression);
+
+ if (sourceExpression.Alias is not null)
+ {
+ _sqlBuilder
+ .Append(" ")
+ .Append(sourceExpression.Alias);
+ }
+ }
+
+ return sourceExpression;
+
+ void VisitContainerExpression(Expression containerExpression)
+ {
+ var subquery = containerExpression is SelectExpression;
+ var simpleValueProjectionSubquery = containerExpression is SelectExpression
+ {
+ Sources: [],
+ Predicate: null,
+ Offset: null,
+ Limit: null,
+ Orderings: [],
+ IsDistinct: false,
+ UsesSingleValueProjection: true,
+ Projection.Count: 1
+ };
+
+ if (subquery)
+ {
+ if (simpleValueProjectionSubquery)
+ {
+ _sqlBuilder.Append("(");
+ }
+ else
+ {
+ _sqlBuilder.AppendLine("(").IncrementIndent();
+ }
+ }
+
+ Visit(sourceExpression.ContainerExpression);
+
+ if (subquery)
+ {
+ if (simpleValueProjectionSubquery)
+ {
+ _sqlBuilder.Append(")");
+ }
+ else
+ {
+ _sqlBuilder.DecrementIndent().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
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPreprocessor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPreprocessor.cs
index d058fadab95..b991a06f3a9 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPreprocessor.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPreprocessor.cs
@@ -27,4 +27,8 @@ public override Expression NormalizeQueryableMethod(Expression query)
return query;
}
+
+ ///
+ protected override Expression ProcessQueryRoots(Expression expression)
+ => new CosmosQueryRootProcessor(Dependencies, QueryCompilationContext).Visit(expression);
}
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs
new file mode 100644
index 00000000000..79c6d8d572c
--- /dev/null
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs
@@ -0,0 +1,188 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+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 static class CosmosQueryUtils
+{
+ ///
+ /// 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 TryConvertToArray(
+ ShapedQueryExpression source,
+ ITypeMappingSource typeMappingSource,
+ [NotNullWhen(true)] out SqlExpression? array,
+ bool ignoreOrderings = false)
+ => TryConvertToArray(source, typeMappingSource, out array, out _, ignoreOrderings);
+
+ ///
+ /// 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 TryConvertToArray(
+ ShapedQueryExpression source,
+ ITypeMappingSource typeMappingSource,
+ [NotNullWhen(true)] out SqlExpression? array,
+ [NotNullWhen(true)] out SqlExpression? projection,
+ bool ignoreOrderings = false)
+ {
+ if (TryExtractBareArray(source, out array, out var projectedScalar, ignoreOrderings))
+ {
+ projection = projectedScalar;
+ return true;
+ }
+
+ // Otherwise, wrap the subquery with an ARRAY() operator, converting the subquery to an array first.
+ if (source.QueryExpression is SelectExpression subquery
+ && TryGetProjection(source, out projection))
+ {
+ subquery.ApplyProjection();
+
+ // 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;
+ }
+
+ array = null;
+ projection = null;
+ return false;
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// 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 SqlExpression? array,
+ bool ignoreOrderings = false)
+ => TryExtractBareArray(source, out array, out _, ignoreOrderings);
+
+ ///
+ /// 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 SqlExpression? array,
+ [NotNullWhen(true)] out ScalarReferenceExpression? projectedScalarReference,
+ bool ignoreOrderings = false)
+ {
+ if (source.QueryExpression is not SelectExpression
+ {
+ Predicate: null,
+ IsDistinct: false,
+ Limit: null,
+ Offset: null
+ } select
+ || (!ignoreOrderings && select.Orderings.Count > 0)
+ || !TryGetProjection(source, out var projection)
+ || projection is not ScalarReferenceExpression scalarReferenceProjection)
+ {
+ array = null;
+ projectedScalarReference = null;
+ return false;
+ }
+
+ switch (source.QueryExpression)
+ {
+ // 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:
+ {
+ array = a;
+ projectedScalarReference = scalarReferenceProjection;
+ return true;
+ }
+
+ // 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
+ {
+ Sources:
+ [
+ {
+ WithIn: true,
+ ContainerExpression: SelectExpression
+ {
+ Sources: [],
+ Predicate: null,
+ Offset: null,
+ Limit: null,
+ Orderings: [],
+ IsDistinct: false,
+ UsesSingleValueProjection: true,
+ Projection: [{Expression: SqlExpression a}]
+ },
+ } arraySource
+ ]
+ } when scalarReferenceProjection.Name == arraySource.Alias:
+ {
+ array = a;
+ projectedScalarReference = scalarReferenceProjection;
+ return true;
+ }
+
+ default:
+ array = null;
+ projectedScalarReference = null;
+ return false;
+ }
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// 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 TryGetProjection(
+ ShapedQueryExpression shapedQueryExpression,
+ [NotNullWhen(true)] out SqlExpression? projectedScalarReference)
+ {
+ var shaperExpression = shapedQueryExpression.ShaperExpression;
+ // No need to check ConvertChecked since this is convert node which we may have added during projection
+ if (shaperExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression
+ && unaryExpression.Operand.Type.IsNullableType()
+ && unaryExpression.Operand.Type.UnwrapNullableType() == unaryExpression.Type)
+ {
+ shaperExpression = unaryExpression.Operand;
+ }
+
+ if (shapedQueryExpression.QueryExpression is SelectExpression selectExpression
+ && shaperExpression is ProjectionBindingExpression { ProjectionMember: ProjectionMember projectionMember }
+ && selectExpression.GetMappedProjection(projectionMember) is SqlExpression projection)
+ {
+ projectedScalarReference = projection;
+ return true;
+ }
+
+ projectedScalarReference = null;
+ return false;
+ }
+}
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs
index 6a122009775..5b68e1c5798 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs
@@ -16,10 +16,12 @@ public class CosmosQueryableMethodTranslatingExpressionVisitor : QueryableMethod
{
private readonly QueryCompilationContext _queryCompilationContext;
private readonly ISqlExpressionFactory _sqlExpressionFactory;
+ private readonly ITypeMappingSource _typeMappingSource;
private readonly IMemberTranslatorProvider _memberTranslatorProvider;
private readonly IMethodCallTranslatorProvider _methodCallTranslatorProvider;
private readonly CosmosSqlTranslatingExpressionVisitor _sqlTranslator;
private readonly CosmosProjectionBindingExpressionVisitor _projectionBindingExpressionVisitor;
+ private readonly bool _subquery;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -31,21 +33,26 @@ public CosmosQueryableMethodTranslatingExpressionVisitor(
QueryableMethodTranslatingExpressionVisitorDependencies dependencies,
QueryCompilationContext queryCompilationContext,
ISqlExpressionFactory sqlExpressionFactory,
+ ITypeMappingSource typeMappingSource,
IMemberTranslatorProvider memberTranslatorProvider,
IMethodCallTranslatorProvider methodCallTranslatorProvider)
: base(dependencies, queryCompilationContext, subquery: false)
{
_queryCompilationContext = queryCompilationContext;
_sqlExpressionFactory = sqlExpressionFactory;
+ _typeMappingSource = typeMappingSource;
_memberTranslatorProvider = memberTranslatorProvider;
_methodCallTranslatorProvider = methodCallTranslatorProvider;
_sqlTranslator = new CosmosSqlTranslatingExpressionVisitor(
queryCompilationContext,
_sqlExpressionFactory,
+ _typeMappingSource,
_memberTranslatorProvider,
- _methodCallTranslatorProvider);
+ _methodCallTranslatorProvider,
+ this);
_projectionBindingExpressionVisitor =
new CosmosProjectionBindingExpressionVisitor(_queryCompilationContext.Model, _sqlTranslator);
+ _subquery = false;
}
///
@@ -60,15 +67,19 @@ protected CosmosQueryableMethodTranslatingExpressionVisitor(
{
_queryCompilationContext = parentVisitor._queryCompilationContext;
_sqlExpressionFactory = parentVisitor._sqlExpressionFactory;
+ _typeMappingSource = parentVisitor._typeMappingSource;
_memberTranslatorProvider = parentVisitor._memberTranslatorProvider;
_methodCallTranslatorProvider = parentVisitor._methodCallTranslatorProvider;
_sqlTranslator = new CosmosSqlTranslatingExpressionVisitor(
QueryCompilationContext,
_sqlExpressionFactory,
+ _typeMappingSource,
_memberTranslatorProvider,
- _methodCallTranslatorProvider);
+ _methodCallTranslatorProvider,
+ parentVisitor);
_projectionBindingExpressionVisitor =
new CosmosProjectionBindingExpressionVisitor(_queryCompilationContext.Model, _sqlTranslator);
+ _subquery = true;
}
///
@@ -193,16 +204,23 @@ static bool ExtractPartitionKeyFromPredicate(
///
protected override Expression VisitExtension(Expression extensionExpression)
- => extensionExpression switch
+ {
+ switch (extensionExpression)
{
- FromSqlQueryRootExpression fromSqlQueryRootExpression
- => CreateShapedQueryExpression(
+ case EntityQueryRootExpression when _subquery:
+ AddTranslationErrorDetails(CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+ return QueryCompilationContext.NotTranslatedExpression;
+
+ case FromSqlQueryRootExpression fromSqlQueryRootExpression:
+ return CreateShapedQueryExpression(
fromSqlQueryRootExpression.EntityType,
_sqlExpressionFactory.Select(
- fromSqlQueryRootExpression.EntityType, fromSqlQueryRootExpression.Sql, fromSqlQueryRootExpression.Argument)),
+ fromSqlQueryRootExpression.EntityType, fromSqlQueryRootExpression.Sql, fromSqlQueryRootExpression.Argument));
- _ => base.VisitExtension(extensionExpression)
- };
+ default:
+ return base.VisitExtension(extensionExpression);
+ }
+ }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -219,8 +237,17 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
/// 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 ShapedQueryExpression TranslateSubquery(Expression expression)
- => throw new InvalidOperationException(CoreStrings.TranslationFailed(expression.Print()));
+ public override ShapedQueryExpression? TranslateSubquery(Expression expression)
+ {
+ var subqueryVisitor = CreateSubqueryVisitor();
+ var translation = subqueryVisitor.Translate(expression) as ShapedQueryExpression;
+ if (translation == null && subqueryVisitor.TranslationErrorDetails != null)
+ {
+ AddTranslationErrorDetails(subqueryVisitor.TranslationErrorDetails);
+ }
+
+ return translation;
+ }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -255,7 +282,34 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
protected override ShapedQueryExpression? TranslateAny(ShapedQueryExpression source, LambdaExpression? predicate)
- => null;
+ {
+ if (predicate != null)
+ {
+ var translatedSource = TranslateWhere(source, predicate);
+ if (translatedSource == null)
+ {
+ return null;
+ }
+
+ source = translatedSource;
+ }
+
+ var subquery = (SelectExpression)source.QueryExpression;
+ subquery.ClearProjection();
+ subquery.ApplyProjection();
+ if (subquery.Limit == null
+ && subquery.Offset == null)
+ {
+ subquery.ClearOrdering();
+ }
+
+ var translation = _sqlExpressionFactory.Exists(subquery);
+ var selectExpression = new SelectExpression(subquery.Container, translation);
+
+ return source.Update(
+ selectExpression,
+ Expression.Convert(new ProjectionBindingExpression(selectExpression, new ProjectionMember(), typeof(bool?)), typeof(bool)));
+ }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -302,7 +356,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? TranslateConcat(ShapedQueryExpression source1, ShapedQueryExpression source2)
- => null;
+ => TranslateSetOperation(source1, source2, "ARRAY_CONCAT");
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -311,7 +365,26 @@ 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? TranslateContains(ShapedQueryExpression source, Expression item)
- => null;
+ {
+ // Simplify x.Array.Contains[1] => ARRAY_CONTAINS(x.Array, 1) insert of IN+subquery
+ if (CosmosQueryUtils.TryExtractBareArray(source, out var array, ignoreOrderings: true)
+ && TranslateExpression(item) is SqlExpression translatedItem
+ && source.QueryExpression is SelectExpression { Container: var container })
+ {
+ if (array is ArrayConstantExpression arrayConstant)
+ {
+ var inExpression = _sqlExpressionFactory.In(translatedItem, arrayConstant.Items);
+ return source.Update(new SelectExpression(container, inExpression), source.ShaperExpression);
+ }
+
+ (translatedItem, array) = _sqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(translatedItem, array);
+ var simplifiedTranslation = _sqlExpressionFactory.Function("ARRAY_CONTAINS", new[] { array, translatedItem }, typeof(bool));
+ return source.UpdateQueryExpression(new SelectExpression(container, simplifiedTranslation));
+ }
+
+ // TODO: Translation to IN, with scalars and with subquery
+ return null;
+ }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -321,6 +394,15 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou
///
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)
+ && source.QueryExpression is SelectExpression { Container: var container })
+ {
+ var simplifiedTranslation = _sqlExpressionFactory.Function("ARRAY_LENGTH", new[] { array }, typeof(int));
+ return source.UpdateQueryExpression(new SelectExpression(container, simplifiedTranslation));
+ }
+
var selectExpression = (SelectExpression)source.QueryExpression;
if (selectExpression.IsDistinct
|| selectExpression.Limit != null
@@ -384,7 +466,22 @@ protected override ShapedQueryExpression TranslateDistinct(ShapedQueryExpression
ShapedQueryExpression source,
Expression index,
bool returnDefault)
- => null;
+ {
+ // Simplify x.Array[1] => x.Array[1] (using the Cosmos array subscript operator) instead of a subquery with LIMIT/OFFSET
+ if (!returnDefault
+ && CosmosQueryUtils.TryExtractBareArray(source, out var array, out var projectedScalarReference)
+ && TranslateExpression(index) is { } translatedIndex
+ && source.QueryExpression is SelectExpression { Container: var container })
+ {
+ var arrayIndex = _sqlExpressionFactory.ArrayIndex(
+ array, translatedIndex, projectedScalarReference.Type, projectedScalarReference.TypeMapping);
+ return source.UpdateQueryExpression(new SelectExpression(container, arrayIndex));
+ }
+
+ // Note that Cosmos doesn't support OFFSET/LIMIT in subqueries, so this translation is for top-level entity querying only.
+ // TODO: Translate with OFFSET/LIMIT
+ return null;
+ }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -393,7 +490,10 @@ protected override ShapedQueryExpression TranslateDistinct(ShapedQueryExpression
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
protected override ShapedQueryExpression? TranslateExcept(ShapedQueryExpression source1, ShapedQueryExpression source2)
- => null;
+ {
+ AddTranslationErrorDetails(CosmosStrings.ExceptNotSupported);
+ return null;
+ }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -464,7 +564,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? TranslateIntersect(ShapedQueryExpression source1, ShapedQueryExpression source2)
- => null;
+ => TranslateSetOperation(source1, source2, "SetIntersect", ignoreOrderings: true);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -808,7 +908,11 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
if (translation != null)
{
- if (selectExpression.Orderings.Count == 0)
+ // Ordering of documents is not guaranteed in Cosmos, so we warn for Skip without OrderBy.
+ // However, when querying on JSON arrays within documents, the order of elements is guaranteed, and Skip without OrderBy is
+ // fine. Since subqueries must be correlated (i.e. reference an array in the outer query), we use that to decide whether to
+ // warn or not.
+ if (selectExpression.Orderings.Count == 0 && !_subquery)
{
_queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning();
}
@@ -872,7 +976,11 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
if (translation != null)
{
- if (selectExpression.Orderings.Count == 0)
+ // Ordering of documents is not guaranteed in Cosmos, so we warn for Take without OrderBy.
+ // However, when querying on JSON arrays within documents, the order of elements is guaranteed, and Take without OrderBy is
+ // fine. Since subqueries must be correlated (i.e. reference an array in the outer query), we use that to decide whether to
+ // warn or not.
+ if (selectExpression.Orderings.Count == 0 && !_subquery)
{
_queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning();
}
@@ -919,7 +1027,7 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
protected override ShapedQueryExpression? TranslateUnion(ShapedQueryExpression source1, ShapedQueryExpression source2)
- => null;
+ => TranslateSetOperation(source1, source2, "SetUnion", ignoreOrderings: true);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -1077,6 +1185,168 @@ when methodCallExpression.TryGetIndexerArguments(_queryCompilationContext.Model,
}
}
+ #region Queryable collection support
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override ShapedQueryExpression? 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;
+ }
+
+ return null;
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override ShapedQueryExpression? TranslateInlineQueryRoot(InlineQueryRootExpression inlineQueryRootExpression)
+ {
+ // The below produces an InlineArrayExpression ([1,2,3]), wrapped by a SelectExpression (SELECT VALUE [1,2,3]).
+ // This is because a bare inline array can only appear in the projection. For example, the following is wrong:
+ // SELECT i FROM i IN [1,2,3] (syntax error)
+ var values = inlineQueryRootExpression.Values;
+ var translatedItems = new SqlExpression[values.Count];
+
+ for (var i = 0; i < values.Count; i++)
+ {
+ if (TranslateExpression(values[i]) is not SqlExpression translatedItem)
+ {
+ return null;
+ }
+
+ translatedItems[i] = translatedItem;
+ }
+
+ // TODO: Do we need full-on type mapping inference like in relational?
+ // TODO: The following currently just gets the type mapping from the CLR type, which ignores e.g. value converters on
+ // TODO: properties compared against
+ var elementClrType = inlineQueryRootExpression.ElementType;
+ var elementTypeMapping = _typeMappingSource.FindMapping(elementClrType)!;
+ var arrayTypeMapping = _typeMappingSource.FindMapping(elementClrType.MakeArrayType()); // TODO: IEnumerable?
+ var inlineArray = new ArrayConstantExpression(elementClrType, translatedItems, arrayTypeMapping);
+
+ // Unfortunately, Cosmos doesn't support selecting directly from an inline array: SELECT i FROM i IN [1,2,3] (syntax error)
+ // We must wrap the inline array in a subquery: SELECT VALUE i FROM (SELECT VALUE [1,2,3])
+ var innerSelect = new SelectExpression(
+ [new ProjectionExpression(inlineArray, null!)],
+ sources: [],
+ orderings: [],
+ container: null!)
+ {
+ UsesSingleValueProjection = true
+ };
+
+ return WrapPrimitiveCollectionAsShapedQuery(innerSelect, elementClrType, elementTypeMapping);
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override ShapedQueryExpression? TranslateParameterQueryRoot(ParameterQueryRootExpression parameterQueryRootExpression)
+ {
+ if (parameterQueryRootExpression.ParameterExpression.Name?.StartsWith(
+ QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal)
+ != true)
+ {
+ return null;
+ }
+
+ // TODO: Do we need full-on type mapping inference like in relational?
+ // TODO: The following currently just gets the type mapping from the CLR type, which ignores e.g. value converters on
+ // TODO: properties compared against
+ var elementClrType = parameterQueryRootExpression.ElementType;
+ var arrayTypeMapping = _typeMappingSource.FindMapping(elementClrType.MakeArrayType()); // TODO: IEnumerable?
+ var elementTypeMapping = _typeMappingSource.FindMapping(elementClrType)!;
+ var sqlParameterExpression = new SqlParameterExpression(parameterQueryRootExpression.ParameterExpression, arrayTypeMapping);
+
+ // Unfortunately, Cosmos doesn't support selecting directly from an inline array: SELECT i FROM i IN [1,2,3] (syntax error)
+ // We must wrap the inline array in a subquery: SELECT VALUE i FROM (SELECT VALUE [1,2,3])
+ var innerSelect = new SelectExpression(
+ [new ProjectionExpression(sqlParameterExpression, null!)],
+ sources: [],
+ orderings: [],
+ container: null!)
+ {
+ UsesSingleValueProjection = true
+ };
+
+ return WrapPrimitiveCollectionAsShapedQuery(innerSelect, elementClrType, elementTypeMapping);
+ }
+
+ private ShapedQueryExpression WrapPrimitiveCollectionAsShapedQuery(
+ Expression containerExpression,
+ Type elementClrType,
+ CoreTypeMapping elementTypeMapping)
+ {
+ // TODO: Do proper alias management: #33894
+ var selectExpression = SelectExpression.CreateForPrimitiveCollection(
+ new SourceExpression(containerExpression, "i", withIn: true),
+ elementClrType,
+ elementTypeMapping);
+ var shaperExpression = (Expression)new ProjectionBindingExpression(
+ selectExpression, 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(selectExpression, shaperExpression);
+ }
+
+ #endregion Queryable collection support
+
+ private ShapedQueryExpression? TranslateSetOperation(
+ ShapedQueryExpression source1,
+ ShapedQueryExpression source2,
+ string functionName,
+ bool ignoreOrderings = false)
+ {
+ 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)
+ {
+ 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);
+ }
+
+ // TODO: can also handle subqueries via ARRAY()
+ return null;
+ }
+
private SqlExpression? TranslateExpression(Expression expression)
{
var translation = _sqlTranslator.Translate(expression);
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitorFactory.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitorFactory.cs
index c790e3fe1a5..f43f8fbe98b 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitorFactory.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitorFactory.cs
@@ -12,6 +12,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
public class CosmosQueryableMethodTranslatingExpressionVisitorFactory(
QueryableMethodTranslatingExpressionVisitorDependencies dependencies,
ISqlExpressionFactory sqlExpressionFactory,
+ ITypeMappingSource typeMappingSource,
IMemberTranslatorProvider memberTranslatorProvider,
IMethodCallTranslatorProvider methodCallTranslatorProvider)
: IQueryableMethodTranslatingExpressionVisitorFactory
@@ -32,6 +33,7 @@ public virtual QueryableMethodTranslatingExpressionVisitor Create(QueryCompilati
Dependencies,
queryCompilationContext,
sqlExpressionFactory,
+ typeMappingSource,
memberTranslatorProvider,
methodCallTranslatorProvider);
}
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs
index 04c084aa427..fb9ab2e96f5 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs
@@ -106,7 +106,7 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression)
_ownerMappings[accessExpression] =
(innerObjectAccessExpression.Navigation.DeclaringEntityType, innerAccessExpression);
break;
- case RootReferenceExpression:
+ case ObjectReferenceExpression:
innerAccessExpression = jObjectParameter;
break;
default:
@@ -623,9 +623,9 @@ private Expression CreateGetValueExpression(
ownerJObjectExpression = ownerInfo.JObjectExpression;
}
- else if (jObjectExpression is RootReferenceExpression rootReferenceExpression)
+ else if (jObjectExpression is ObjectReferenceExpression objectReferenceExpression)
{
- ownerJObjectExpression = rootReferenceExpression;
+ ownerJObjectExpression = objectReferenceExpression;
}
else if (jObjectExpression is ObjectAccessExpression objectAccessExpression)
{
@@ -691,10 +691,10 @@ private Expression CreateGetValueExpression(
{
innerExpression = innerVariable;
}
- else if (jObjectExpression is RootReferenceExpression rootReferenceExpression)
+ else if (jObjectExpression is ObjectReferenceExpression objectReferenceExpression)
{
innerExpression = CreateGetValueExpression(
- jObjectParameter, rootReferenceExpression.Alias, typeof(JObject));
+ jObjectParameter, objectReferenceExpression.Name, typeof(JObject));
}
else if (jObjectExpression is ObjectAccessExpression objectAccessExpression)
{
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs
index f81bc71e330..c582467bc70 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs
@@ -16,8 +16,10 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
public class CosmosSqlTranslatingExpressionVisitor(
QueryCompilationContext queryCompilationContext,
ISqlExpressionFactory sqlExpressionFactory,
+ ITypeMappingSource typeMappingSource,
IMemberTranslatorProvider memberTranslatorProvider,
- IMethodCallTranslatorProvider methodCallTranslatorProvider)
+ IMethodCallTranslatorProvider methodCallTranslatorProvider,
+ QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor)
: ExpressionVisitor
{
private const string RuntimeParameterPrefix = QueryCompilationContext.QueryParameterPrefix + "entity_equality_";
@@ -234,7 +236,8 @@ Expression ProcessGetType(EntityReferenceExpression entityReferenceExpression, T
if (TryBindMember(
entityReferenceExpression,
MemberIdentity.Create(entityType.GetDiscriminatorPropertyName()),
- out var discriminatorMember)
+ out var discriminatorMember,
+ out _)
&& discriminatorMember is SqlExpression discriminatorColumn)
{
return match
@@ -360,6 +363,90 @@ protected override Expression VisitExtension(Expression extensionExpression)
.GetMappedProjection(projectionBindingExpression.ProjectionMember)
: QueryCompilationContext.NotTranslatedExpression;
+ // This case is for a subquery embedded in a lambda, returning a scalar, e.g. Where(b => b.Posts.Count() > 0).
+ // For most cases, generate a scalar subquery (WHERE (SELECT COUNT(*) FROM Posts) > 0).
+ case ShapedQueryExpression { ResultCardinality: not ResultCardinality.Enumerable } shapedQuery:
+ {
+ var shaperExpression = shapedQuery.ShaperExpression;
+ ProjectionBindingExpression? mappedProjectionBindingExpression = null;
+
+ var innerExpression = shaperExpression;
+ Type? convertedType = null;
+ if (shaperExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression)
+ {
+ convertedType = unaryExpression.Type;
+ innerExpression = unaryExpression.Operand;
+ }
+
+ if (innerExpression is StructuralTypeShaperExpression ese
+ && (convertedType == null
+ || convertedType.IsAssignableFrom(ese.Type)))
+ {
+ // TODO: Subquery projecting out an entity/structural type
+ return QueryCompilationContext.NotTranslatedExpression;
+ }
+
+ if (innerExpression is ProjectionBindingExpression pbe
+ && (convertedType == null
+ || convertedType.MakeNullable() == innerExpression.Type))
+ {
+ mappedProjectionBindingExpression = pbe;
+ }
+
+ if (mappedProjectionBindingExpression == null
+ && shaperExpression is BlockExpression
+ {
+ Expressions: [BinaryExpression { NodeType: ExpressionType.Assign, Right: ProjectionBindingExpression pbe2 }, _]
+ })
+ {
+ mappedProjectionBindingExpression = pbe2;
+ }
+
+ if (mappedProjectionBindingExpression == null)
+ {
+ return QueryCompilationContext.NotTranslatedExpression;
+ }
+
+ var subquery = (SelectExpression)shapedQuery.QueryExpression;
+
+ var projection = mappedProjectionBindingExpression.ProjectionMember is ProjectionMember projectionMember
+ ? subquery.GetMappedProjection(projectionMember)
+ : throw new NotImplementedException("Subquery with index projection binding");
+ if (projection is not SqlExpression sqlExpression)
+ {
+ return QueryCompilationContext.NotTranslatedExpression;
+ }
+
+ if (subquery.Sources.Count == 0)
+ {
+ return sqlExpression;
+ }
+
+ // TODO
+ // subquery.ReplaceProjection(new List { sqlExpression });
+ subquery.ApplyProjection();
+
+ SqlExpression scalarSubqueryExpression = new ScalarSubqueryExpression(subquery);
+
+ if (shapedQuery.ResultCardinality is ResultCardinality.SingleOrDefault
+ && !shaperExpression.Type.IsNullableType())
+ {
+ throw new NotImplementedException("Subquery with SingleOrDefault");
+ // scalarSubqueryExpression = sqlExpressionFactory.Coalesce(
+ // scalarSubqueryExpression,
+ // (SqlExpression)Visit(shaperExpression.Type.GetDefaultValueConstant()));
+ }
+
+ return scalarSubqueryExpression;
+ }
+
+ // This case is for a subquery embedded in a lambda, returning an array, e.g. Where(b => b.Ints == new[] { 1, 2, 3 }).
+ // If the subquery represents a bare array (without any operators composed on top), simply extract and return that.
+ // Otherwise, wrap the subquery with an ARRAY() operator, converting the subquery to an array first.
+ case ShapedQueryExpression { ResultCardinality: ResultCardinality.Enumerable } shapedQuery
+ when CosmosQueryUtils.TryConvertToArray(shapedQuery, typeMappingSource, out var array):
+ return array;
+
default:
return QueryCompilationContext.NotTranslatedExpression;
}
@@ -402,7 +489,7 @@ protected override Expression VisitMember(MemberExpression memberExpression)
{
var innerExpression = Visit(memberExpression.Expression);
- return TryBindMember(innerExpression, MemberIdentity.Create(memberExpression.Member), out var expression)
+ return TryBindMember(innerExpression, MemberIdentity.Create(memberExpression.Member), out var expression, out _)
? expression
: (TranslationFailed(memberExpression.Expression, innerExpression, out var sqlInnerExpression)
? QueryCompilationContext.NotTranslatedExpression
@@ -433,7 +520,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
if (methodCallExpression.TryGetEFPropertyArguments(out var source, out var propertyName)
|| methodCallExpression.TryGetIndexerArguments(_model, out source, out propertyName))
{
- return TryBindMember(Visit(source), MemberIdentity.Create(propertyName), out var result)
+ return TryBindMember(Visit(source), MemberIdentity.Create(propertyName), out var result, out _)
? result
: QueryCompilationContext.NotTranslatedExpression;
}
@@ -518,6 +605,10 @@ when method.GetGenericMethodDefinition().Equals(EnumerableMethods.Contains):
case { Arguments: [var argument] } when method.IsContainsMethod():
return TranslateContains(argument, methodCallExpression.Object!);
+ // For queryable methods, either we translate the whole aggregate or we go to subquery mode
+ case { Method.IsStatic: true, Arguments.Count: > 0 } when method.DeclaringType == typeof(Queryable):
+ return TranslateAsSubquery(methodCallExpression);
+
default:
{
if (TranslationFailed(methodCallExpression.Object, Visit(methodCallExpression.Object), out sqlObject))
@@ -531,7 +622,7 @@ when method.GetGenericMethodDefinition().Equals(EnumerableMethods.Contains):
var argument = methodCallExpression.Arguments[i];
if (TranslationFailed(argument, Visit(argument), out var sqlArgument))
{
- return QueryCompilationContext.NotTranslatedExpression;
+ return TranslateAsSubquery(methodCallExpression);
}
arguments[i] = sqlArgument!;
@@ -541,28 +632,42 @@ when method.GetGenericMethodDefinition().Equals(EnumerableMethods.Contains):
}
}
- var translation = methodCallTranslatorProvider.Translate(
+ Expression? translation = methodCallTranslatorProvider.Translate(
_model, sqlObject, methodCallExpression.Method, arguments, queryCompilationContext.Logger);
+ if (translation is not null)
+ {
+ return translation;
+ }
- if (translation is null)
+ translation = TranslateAsSubquery(methodCallExpression);
+ if (translation != QueryCompilationContext.NotTranslatedExpression)
{
- if (methodCallExpression.Method == StringEqualsWithStringComparison
- || methodCallExpression.Method == StringEqualsWithStringComparisonStatic)
- {
- AddTranslationErrorDetails(CoreStrings.QueryUnableToTranslateStringEqualsWithStringComparison);
- }
- else
- {
- AddTranslationErrorDetails(
- CoreStrings.QueryUnableToTranslateMethod(
- methodCallExpression.Method.DeclaringType?.DisplayName(),
- methodCallExpression.Method.Name));
- }
+ return translation;
+ }
- return QueryCompilationContext.NotTranslatedExpression;
+ if (methodCallExpression.Method == StringEqualsWithStringComparison
+ || methodCallExpression.Method == StringEqualsWithStringComparisonStatic)
+ {
+ AddTranslationErrorDetails(CoreStrings.QueryUnableToTranslateStringEqualsWithStringComparison);
+ }
+ else
+ {
+ AddTranslationErrorDetails(
+ CoreStrings.QueryUnableToTranslateMethod(
+ methodCallExpression.Method.DeclaringType?.DisplayName(),
+ methodCallExpression.Method.Name));
}
- return translation;
+ return QueryCompilationContext.NotTranslatedExpression;
+
+ Expression TranslateAsSubquery(Expression expression)
+ {
+ var subqueryTranslation = queryableMethodTranslatingExpressionVisitor.TranslateSubquery(expression);
+
+ return subqueryTranslation == null
+ ? QueryCompilationContext.NotTranslatedExpression
+ : Visit(subqueryTranslation);
+ }
Expression TranslateContains(Expression untranslatedItem, Expression untranslatedCollection)
{
@@ -652,13 +757,24 @@ protected override Expression VisitNew(NewExpression newExpression)
///
protected override Expression VisitNewArray(NewArrayExpression newArrayExpression)
{
- if (TryEvaluateToConstant(newArrayExpression, out var sqlConstantExpression))
+ var expressions = newArrayExpression.Expressions;
+ var translatedItems = new SqlExpression[expressions.Count];
+
+ for (var i = 0; i < expressions.Count; i++)
{
- return sqlConstantExpression;
+ if (Translate(expressions[i]) is not SqlExpression translatedItem)
+ {
+ return QueryCompilationContext.NotTranslatedExpression;
+ }
+
+ translatedItems[i] = translatedItem;
}
- AddTranslationErrorDetails(CosmosStrings.CannotTranslateNonConstantNewArrayExpression(newArrayExpression.Print()));
- return QueryCompilationContext.NotTranslatedExpression;
+ var arrayTypeMapping = typeMappingSource.FindMapping(newArrayExpression.Type);
+ var elementClrType = newArrayExpression.Type.GetElementType()!;
+ var inlineArray = new ArrayConstantExpression(elementClrType, translatedItems, arrayTypeMapping);
+
+ return inlineArray;
}
///
@@ -734,7 +850,8 @@ protected override Expression VisitTypeBinary(TypeBinaryExpression typeBinaryExp
&& TryBindMember(
entityReferenceExpression,
MemberIdentity.Create(entityType.GetDiscriminatorPropertyName()),
- out var discriminatorMember)
+ out var discriminatorMember,
+ out _)
&& discriminatorMember is SqlExpression discriminatorColumn)
{
var concreteEntityTypes = derivedType.GetConcreteDerivedTypesInclusive().ToList();
@@ -752,11 +869,23 @@ protected override Expression VisitTypeBinary(TypeBinaryExpression typeBinaryExp
return QueryCompilationContext.NotTranslatedExpression;
}
- private bool TryBindMember(Expression? source, MemberIdentity member, [NotNullWhen(true)] out Expression? 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.
+ ///
+ [EntityFrameworkInternal]
+ public virtual bool TryBindMember(
+ Expression? source,
+ MemberIdentity member,
+ [NotNullWhen(true)] out Expression? expression,
+ [NotNullWhen(true)] out IPropertyBase? property)
{
if (source is not EntityReferenceExpression entityReferenceExpression)
{
expression = null;
+ property = null;
return false;
}
@@ -764,11 +893,11 @@ private bool TryBindMember(Expression? source, MemberIdentity member, [NotNullWh
{
{ MemberInfo: MemberInfo memberInfo }
=> entityReferenceExpression.ParameterEntity.BindMember(
- memberInfo, entityReferenceExpression.Type, clientEval: false, out _),
+ memberInfo, entityReferenceExpression.Type, clientEval: false, out property),
{ Name: string name }
=> entityReferenceExpression.ParameterEntity.BindMember(
- name, entityReferenceExpression.Type, clientEval: false, out _),
+ name, entityReferenceExpression.Type, clientEval: false, out property),
_ => throw new UnreachableException()
};
@@ -777,9 +906,11 @@ private bool TryBindMember(Expression? source, MemberIdentity member, [NotNullWh
{
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");
return true;
case null:
AddTranslationErrorDetails(
@@ -790,6 +921,7 @@ private bool TryBindMember(Expression? source, MemberIdentity member, [NotNullWh
return false;
default:
expression = result;
+ Check.DebugAssert(property is not null, "Property cannot be null if binding result was non-null");
return true;
}
}
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs
index 1b839f044ed..e58f20a8f82 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs
@@ -45,8 +45,13 @@ private Expression VisitSelect(SelectExpression selectExpression)
changed |= updatedProjection != item;
}
- var fromExpression = (RootReferenceExpression)Visit(selectExpression.FromExpression);
- changed |= fromExpression != selectExpression.FromExpression;
+ var sources = new List();
+ foreach (var item in selectExpression.Sources)
+ {
+ var updatedSource = (SourceExpression)Visit(item);
+ sources.Add(updatedSource);
+ changed |= updatedSource != item;
+ }
var predicate = TryCompensateForBoolWithValueConverter((SqlExpression?)Visit(selectExpression.Predicate));
changed |= predicate != selectExpression.Predicate;
@@ -63,7 +68,7 @@ private Expression VisitSelect(SelectExpression selectExpression)
var offset = (SqlExpression?)Visit(selectExpression.Offset);
return changed
- ? selectExpression.Update(projections, fromExpression, predicate, orderings, limit, offset)
+ ? selectExpression.Update(projections, sources, predicate, orderings, limit, offset)
: selectExpression;
}
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ArrayConstantExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ArrayConstantExpression.cs
new file mode 100644
index 00000000000..05c0700da2a
--- /dev/null
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ArrayConstantExpression.cs
@@ -0,0 +1,84 @@
+// 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 inline array in a Cosmos SQL query, e.g. [1, 2, c.Id].
+///
+/// CosmosDB constant expressions
+///
+/// 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 ArrayConstantExpression(Type elementClrType, IReadOnlyList items, CoreTypeMapping? typeMapping = null)
+ : SqlExpression(typeof(IEnumerable<>).MakeGenericType(elementClrType), 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.
+ ///
+ public virtual IReadOnlyList Items { get; } = items;
+
+ ///
+ /// 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 ArrayConstantExpression VisitChildren(ExpressionVisitor visitor)
+ => visitor.VisitAndConvert(Items) is var newItems
+ && ReferenceEquals(newItems, Items)
+ ? this
+ : new ArrayConstantExpression(Type, newItems);
+
+ ///
+ /// 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.Append("[");
+
+ var count = Items.Count;
+ for (var i = 0; i < count; i++)
+ {
+ expressionPrinter.Visit(Items[i]);
+
+ if (i < count - 1)
+ {
+ expressionPrinter.Append(", ");
+ }
+ }
+
+ expressionPrinter.Append("]");
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ => obj is ArrayConstantExpression other && Equals(other);
+
+ private bool Equals(ArrayConstantExpression? other)
+ => ReferenceEquals(this, other) || (base.Equals(other) && Items.SequenceEqual(other.Items));
+
+ ///
+ public override int GetHashCode()
+ {
+ var hashCode = new HashCode();
+
+ foreach (var item in Items)
+ {
+ hashCode.Add(item);
+ }
+
+ return hashCode.ToHashCode();
+ }
+}
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ArrayExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ArrayExpression.cs
new file mode 100644
index 00000000000..fc93e4305bc
--- /dev/null
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ArrayExpression.cs
@@ -0,0 +1,67 @@
+// 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 ArrayExpression(SelectExpression subquery, Type arrayClrType, CoreTypeMapping? arrayTypeMapping = null)
+ : SqlExpression(arrayClrType, arrayTypeMapping)
+{
+ ///
+ /// 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 ArrayExpression VisitChildren(ExpressionVisitor visitor)
+ => visitor.Visit(Subquery) is var newQuery
+ && ReferenceEquals(newQuery, Subquery)
+ ? this
+ : new ArrayExpression((SelectExpression)newQuery, 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.Append("ARRAY (");
+ expressionPrinter.Visit(Subquery);
+ expressionPrinter.Append(")");
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ => obj is ArrayExpression other && Equals(other);
+
+ private bool Equals(ArrayExpression? other)
+ => ReferenceEquals(this, other) || (base.Equals(other) && Subquery.Equals(other.Subquery));
+
+ ///
+ public override int GetHashCode()
+ => HashCode.Combine(base.GetHashCode(), Subquery);
+}
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ExistsExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ExistsExpression.cs
new file mode 100644
index 00000000000..2548d510b5b
--- /dev/null
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ExistsExpression.cs
@@ -0,0 +1,91 @@
+// 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;
+
+///
+///
+/// An expression that represents projecting a SQL EXISTS expression.
+///
+///
+/// This type is typically used by database providers (and other extensions). It is generally
+/// not used in application code.
+///
+///
+/// CosmosDB subqueries
+///
+/// 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 ExistsExpression : 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 ExistsExpression(SelectExpression subquery, CoreTypeMapping? typeMapping)
+ : base(typeof(bool), typeMapping)
+ {
+ Subquery = subquery;
+ }
+
+ ///
+ /// The subquery for which to check for element existence.
+ ///
+ ///
+ /// 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; }
+
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ => Update((SelectExpression)visitor.Visit(Subquery));
+
+ ///
+ /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
+ /// return this expression.
+ ///
+ /// The property of the result.
+ /// This expression if no children changed, or an expression with the updated children.
+ ///
+ /// 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 ExistsExpression Update(SelectExpression subquery)
+ => subquery == Subquery
+ ? this
+ : new ExistsExpression(subquery, TypeMapping);
+
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.Append("EXISTS (");
+ using (expressionPrinter.Indent())
+ {
+ expressionPrinter.Visit(Subquery);
+ }
+
+ expressionPrinter.Append(")");
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ => obj is ExistsExpression other && Equals(other);
+
+ private bool Equals(ExistsExpression? other)
+ => ReferenceEquals(this, other) || (base.Equals(other) && Subquery.Equals(other.Subquery));
+
+ ///
+ public override int GetHashCode()
+ => HashCode.Combine(base.GetHashCode(), Subquery);
+}
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/FromSqlExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/FromSqlExpression.cs
index 23bca1dc126..728e7a1c736 100644
--- a/src/EFCore.Cosmos/Query/Internal/Expressions/FromSqlExpression.cs
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/FromSqlExpression.cs
@@ -10,12 +10,17 @@ 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 FromSqlExpression(IEntityType entityType, string alias, string sql, Expression arguments)
- : RootReferenceExpression(entityType, alias), IPrintableExpression
+public class FromSqlExpression(Type clrType, string sql, Expression arguments)
+ : Expression, IPrintableExpression
{
- ///
- public override string Alias
- => base.Alias!;
+ ///
+ /// 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 sealed override ExpressionType NodeType
+ => ExpressionType.Extension;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -41,7 +46,7 @@ public override string Alias
///
public virtual FromSqlExpression Update(Expression arguments)
=> arguments != Arguments
- ? new FromSqlExpression(EntityType, Alias, Sql, arguments)
+ ? new FromSqlExpression(Type, Sql, arguments)
: this;
///
@@ -49,8 +54,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
=> this;
///
- public override Type Type
- => typeof(object);
+ public override Type Type { get; } = clrType;
///
void IPrintableExpression.Print(ExpressionPrinter expressionPrinter)
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayProjectionExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayProjectionExpression.cs
index df414272507..fa7ef7cb4ca 100644
--- a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayProjectionExpression.cs
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayProjectionExpression.cs
@@ -38,7 +38,7 @@ public ObjectArrayProjectionExpression(
InnerProjection = innerProjection
?? new EntityProjectionExpression(
targetType,
- new RootReferenceExpression(targetType, ""));
+ new ObjectReferenceExpression(targetType, ""));
}
///
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/RootReferenceExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs
similarity index 81%
rename from src/EFCore.Cosmos/Query/Internal/Expressions/RootReferenceExpression.cs
rename to src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs
index d78ea5d908a..747255d7a16 100644
--- a/src/EFCore.Cosmos/Query/Internal/Expressions/RootReferenceExpression.cs
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs
@@ -5,12 +5,16 @@
namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
///
+/// Represents a reference to a JSON object in the Cosmos SQL query, e.g. the first c in SELECT c FROM Customers c.
+/// When referencing a scalar, - which is a - is used instead.
+///
+///
/// 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 RootReferenceExpression(IEntityType entityType, string alias) : Expression, IAccessExpression
+///
+public class ObjectReferenceExpression(IEntityType entityType, string name) : Expression, IAccessExpression
{
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -36,6 +40,10 @@ 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.
///
+ // TODO: The entity type is currently necessary to distinguish between different entity types when generating the shaper
+ // TODO: (CosmosProjectionBindingRemovingExpressionVisitorBase._projectionBindings has IAccessExpressions as keys, and so entity types
+ // TODO: need to participate in the equality etc.). Long-term, this should be a server-side SQL expression that knows nothing about
+ // TODO: the shaper side.
public virtual IEntityType EntityType { get; } = entityType;
///
@@ -44,7 +52,7 @@ public override Type Type
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- public virtual string Alias { get; } = alias;
+ public virtual string Name { get; } = name;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -53,7 +61,7 @@ public override Type Type
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
string IAccessExpression.Name
- => Alias;
+ => Name;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -71,7 +79,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public override string ToString()
- => Alias;
+ => Name;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -82,12 +90,12 @@ public override string ToString()
public override bool Equals(object? obj)
=> obj != null
&& (ReferenceEquals(this, obj)
- || obj is RootReferenceExpression rootReferenceExpression
- && Equals(rootReferenceExpression));
+ || obj is ObjectReferenceExpression objectReferenceExpression
+ && Equals(objectReferenceExpression));
- private bool Equals(RootReferenceExpression rootReferenceExpression)
- => Alias == rootReferenceExpression.Alias
- && EntityType.Equals(rootReferenceExpression.EntityType);
+ private bool Equals(ObjectReferenceExpression objectReferenceExpression)
+ => Name == objectReferenceExpression.Name
+ && EntityType.Equals(objectReferenceExpression.EntityType);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -96,5 +104,5 @@ private bool Equals(RootReferenceExpression rootReferenceExpression)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public override int GetHashCode()
- => HashCode.Combine(Alias, EntityType);
+ => Name.GetHashCode();
}
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ReadItemExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ReadItemExpression.cs
index a6b8cf6e3eb..7b77dd70e89 100644
--- a/src/EFCore.Cosmos/Query/Internal/Expressions/ReadItemExpression.cs
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ReadItemExpression.cs
@@ -80,7 +80,7 @@ public ReadItemExpression(
ProjectionExpression = new ProjectionExpression(
new EntityProjectionExpression(
entityType,
- new RootReferenceExpression(entityType, RootAlias)),
+ new ObjectReferenceExpression(entityType, RootAlias)),
RootAlias);
EntityType = entityType;
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarReferenceExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarReferenceExpression.cs
new file mode 100644
index 00000000000..f4c60141a3b
--- /dev/null
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarReferenceExpression.cs
@@ -0,0 +1,79 @@
+// 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 reference to a JSON 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.
+///
+///
+/// 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 ScalarReferenceExpression(string name, Type clrType, CoreTypeMapping? typeMapping = null)
+ : 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 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.
+ ///
+ string IAccessExpression.Name
+ => 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.
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ => 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 override string ToString()
+ => 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.
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ => expressionPrinter.Append(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 override bool Equals(object? obj)
+ => obj is ScalarReferenceExpression other && Equals(other);
+
+ private bool Equals(ScalarReferenceExpression other)
+ => ReferenceEquals(this, other) || (base.Equals(other) && Name == other.Name);
+
+ ///
+ public override int GetHashCode()
+ => HashCode.Combine(base.GetHashCode(), Name);
+}
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarSubqueryExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarSubqueryExpression.cs
new file mode 100644
index 00000000000..01ccf47b853
--- /dev/null
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarSubqueryExpression.cs
@@ -0,0 +1,106 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// ReSharper disable once CheckNamespace
+namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
+
+///
+///
+/// An expression that represents projecting a scalar SQL value from a subquery.
+///
+///
+/// This type is typically used by database providers (and other extensions). It is generally
+/// not used in application code.
+///
+///
+///
+/// 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 ScalarSubqueryExpression : SqlExpression
+{
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// A subquery projecting single row with a single scalar projection.
+ public ScalarSubqueryExpression(SelectExpression subquery)
+ : this(
+ subquery,
+ subquery.Projection[0].Expression is SqlExpression sqlExpression
+ ? sqlExpression.TypeMapping
+ : throw new UnreachableException("Can't construct scalar subquery over SelectExpresison with non-SqlExpression projection"))
+ {
+ Subquery = subquery;
+ }
+
+ private ScalarSubqueryExpression(SelectExpression subquery, CoreTypeMapping? typeMapping)
+ : base(subquery.Projection[0].Type, typeMapping)
+ {
+ Subquery = subquery;
+ }
+
+ ///
+ /// The subquery projecting single row with single scalar 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 virtual SelectExpression Subquery { get; }
+
+ ///
+ /// Applies supplied type mapping to this expression.
+ ///
+ /// A relational type mapping to apply.
+ /// A new expression which has supplied type mapping.
+ public virtual SqlExpression ApplyTypeMapping(CoreTypeMapping? typeMapping)
+ => new ScalarSubqueryExpression(Subquery, typeMapping);
+
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ => Update((SelectExpression)visitor.Visit(Subquery));
+
+ ///
+ /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
+ /// return this expression.
+ ///
+ /// The property of the result.
+ /// This expression if no children changed, or an expression with the updated children.
+ ///
+ /// 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 ScalarSubqueryExpression Update(SelectExpression subquery)
+ => subquery == Subquery
+ ? this
+ : new ScalarSubqueryExpression(subquery);
+
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.Append("(");
+ using (expressionPrinter.Indent())
+ {
+ expressionPrinter.Visit(Subquery);
+ }
+
+ expressionPrinter.Append(")");
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ => obj is ScalarSubqueryExpression other && Equals(other);
+
+ private bool Equals(ScalarSubqueryExpression? other)
+ => ReferenceEquals(this, other) || (base.Equals(other) && Subquery.Equals(other.Subquery));
+
+ ///
+ public override int GetHashCode()
+ => HashCode.Combine(base.GetHashCode(), Subquery);
+}
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs
index 98a14b4c54d..1ab23bb3917 100644
--- a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs
@@ -13,11 +13,13 @@ 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 SelectExpression : Expression
+[DebuggerDisplay("{PrintShortSql(), nq}")]
+public class SelectExpression : Expression, IPrintableExpression
{
private const string RootAlias = "c";
private IDictionary _projectionMapping = new Dictionary();
+ private readonly List _sources = [];
private readonly List _projection = [];
private readonly List _orderings = [];
@@ -33,8 +35,11 @@ public SelectExpression(IEntityType entityType)
{
// TODO: All queries should reference a non-null container ID, but GetContainer returns null for owned entities.
Container = entityType.GetContainer()!;
- FromExpression = new RootReferenceExpression(entityType, RootAlias);
- _projectionMapping[new ProjectionMember()] = new EntityProjectionExpression(entityType, FromExpression);
+
+ // TODO: Redo aliasing
+ _sources = [new SourceExpression(new ObjectReferenceExpression(entityType, "root"), RootAlias)];
+ _projectionMapping[new ProjectionMember()]
+ = new EntityProjectionExpression(entityType, new ObjectReferenceExpression(entityType, RootAlias));
}
///
@@ -47,23 +52,72 @@ public SelectExpression(IEntityType entityType, string sql, Expression argument)
{
// TODO: All queries should reference a non-null container ID, but GetContainer returns null for owned entities.
Container = entityType.GetContainer()!;
- FromExpression = new FromSqlExpression(entityType, RootAlias, sql, argument);
+ var fromSql = new FromSqlExpression(entityType.ClrType, sql, argument);
+ _sources = [new SourceExpression(fromSql, RootAlias)];
_projectionMapping[new ProjectionMember()] = new EntityProjectionExpression(
- entityType, new RootReferenceExpression(entityType, RootAlias));
+ entityType, new ObjectReferenceExpression(entityType, RootAlias));
}
- private SelectExpression(
+ ///
+ /// 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(
List projections,
- RootReferenceExpression fromExpression,
+ List sources,
List orderings,
string container)
{
_projection = projections;
- FromExpression = fromExpression;
+ _sources = sources;
_orderings = orderings;
Container = container;
}
+ ///
+ /// 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(string container, SqlExpression projection)
+ : this(container)
+ => _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(string? container)
+ {
+ // TODO: Move container out of SelectExpression to QueryCompilationContext
+ Container = container!;
+ }
+
+ ///
+ /// 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 SelectExpression CreateForPrimitiveCollection(
+ SourceExpression source,
+ Type elementClrType,
+ CoreTypeMapping elementTypeMapping)
+ => new(container: null)
+ {
+ _sources = { source },
+ _projectionMapping =
+ {
+ [new ProjectionMember()] = new ScalarReferenceExpression(source.Alias, elementClrType, elementTypeMapping)
+ },
+ UsesSingleValueProjection = true
+ };
+
///
/// 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
@@ -81,13 +135,26 @@ private SelectExpression(
public virtual IReadOnlyList Projection
=> _projection;
+ ///
+ /// If set, indicates that the has a Cosmos VALUE projection, which does not get wrapped in a
+ /// JSON object. If , must contain a single item.
+ ///
+ ///
+ /// 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 bool UsesSingleValueProjection { get; init; }
+
///
/// 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 RootReferenceExpression FromExpression { get; }
+ public virtual IReadOnlyList Sources
+ => _sources;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -139,6 +206,15 @@ public virtual IReadOnlyList Orderings
public virtual Expression GetMappedProjection(ProjectionMember projectionMember)
=> _projectionMapping[projectionMember];
+ ///
+ /// 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 void ClearProjection()
+ => _projectionMapping.Clear();
+
///
/// 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
@@ -465,8 +541,13 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
}
}
- var fromExpression = (RootReferenceExpression)visitor.Visit(FromExpression);
- changed |= fromExpression != FromExpression;
+ var sources = new List();
+ foreach (var source in _sources)
+ {
+ var visitedSource = (SourceExpression)visitor.Visit(source);
+ changed |= visitedSource != source;
+ sources.Add(visitedSource);
+ }
var predicate = (SqlExpression?)visitor.Visit(Predicate);
changed |= predicate != Predicate;
@@ -487,7 +568,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
if (changed)
{
- var newSelectExpression = new SelectExpression(projections, fromExpression, orderings, Container)
+ var newSelectExpression = new SelectExpression(projections, sources, orderings, Container)
{
_projectionMapping = projectionMapping,
Predicate = predicate,
@@ -510,7 +591,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
///
public virtual SelectExpression Update(
List projections,
- RootReferenceExpression fromExpression,
+ List sources,
SqlExpression? predicate,
List orderings,
SqlExpression? limit,
@@ -522,7 +603,7 @@ public virtual SelectExpression Update(
projectionMapping[projectionMember] = expression;
}
- return new SelectExpression(projections, fromExpression, orderings, Container)
+ return new SelectExpression(projections, sources, orderings, Container)
{
_projectionMapping = projectionMapping,
Predicate = predicate,
@@ -531,4 +612,123 @@ public virtual SelectExpression Update(
IsDistinct = IsDistinct
};
}
+
+ #region Print
+
+ ///
+ /// 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)
+ {
+ PrintProjections(expressionPrinter);
+ expressionPrinter.AppendLine();
+ PrintSql(expressionPrinter);
+ }
+
+ private void PrintProjections(ExpressionPrinter expressionPrinter)
+ {
+ if (_projectionMapping.Count > 0)
+ {
+ expressionPrinter.AppendLine("Projection Mapping:");
+ using (expressionPrinter.Indent())
+ {
+ foreach (var (projectionMember, expression) in _projectionMapping)
+ {
+ expressionPrinter.AppendLine();
+ expressionPrinter.Append(projectionMember.ToString()).Append(" -> ");
+ expressionPrinter.Visit(expression);
+ }
+ }
+ }
+ }
+
+ private void PrintSql(ExpressionPrinter expressionPrinter, bool withTags = true)
+ {
+ if (withTags)
+ {
+ // foreach (var tag in Tags)
+ // {
+ // expressionPrinter.Append($"-- {tag}");
+ // }
+ }
+
+ expressionPrinter.Append("SELECT ");
+
+ if (IsDistinct)
+ {
+ expressionPrinter.Append("DISTINCT ");
+ }
+
+ if (Projection.Any())
+ {
+ if (UsesSingleValueProjection)
+ {
+ expressionPrinter.Append("VALUE ");
+ }
+
+ expressionPrinter.VisitCollection(Projection);
+ }
+ else
+ {
+ expressionPrinter.Append("1");
+ }
+
+ if (Sources.Any())
+ {
+ expressionPrinter.AppendLine().Append("FROM ");
+
+ expressionPrinter.VisitCollection(Sources, p => p.AppendLine());
+ }
+
+ if (Predicate != null)
+ {
+ expressionPrinter.AppendLine().Append("WHERE ");
+ expressionPrinter.Visit(Predicate);
+ }
+
+ if (Orderings.Any())
+ {
+ expressionPrinter.AppendLine().Append("ORDER BY ");
+ expressionPrinter.VisitCollection(Orderings);
+ }
+
+ if (Offset != null)
+ {
+ expressionPrinter.AppendLine().Append("OFFSET ");
+ expressionPrinter.Visit(Offset);
+ expressionPrinter.Append(" ROWS");
+
+ if (Limit != null)
+ {
+ expressionPrinter.Append(" FETCH NEXT ");
+ expressionPrinter.Visit(Limit);
+ expressionPrinter.Append(" ROWS ONLY");
+ }
+ }
+ }
+
+ private string PrintShortSql()
+ {
+ var expressionPrinter = new ExpressionPrinter();
+ PrintSql(expressionPrinter, withTags: false);
+ return expressionPrinter.ToString();
+ }
+
+ ///
+ ///
+ /// Expand this property in the debugger for a human-readable representation of this .
+ ///
+ ///
+ /// Warning: Do not rely on the format of the debug strings.
+ /// They are designed for debugging only and may change arbitrarily between releases.
+ ///
+ ///
+ [EntityFrameworkInternal]
+ public virtual string DebugView
+ => this.Print();
+
+ #endregion Print
}
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SourceExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SourceExpression.cs
new file mode 100644
index 00000000000..5ed5b1c33fa
--- /dev/null
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SourceExpression.cs
@@ -0,0 +1,153 @@
+// 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.
+///
+/// FROM clause (NoSQL query)
+[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")]
+public class SourceExpression(Expression containerExpression, string alias, bool withIn = false)
+ : Expression, IAccessExpression, IPrintableExpression
+{
+ ///
+ /// 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 sealed 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
+ => ContainerExpression.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 Expression ContainerExpression { get; } = containerExpression;
+
+ ///
+ /// 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 Alias { get; } = alias;
+
+ ///
+ /// Specifies that the source uses IN, and will be generated as FROM x IN c.Tags
+ ///
+ ///
+ /// 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 bool WithIn { get; } = withIn;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// 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
+ => Alias;
+
+ ///
+ /// 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(ContainerExpression));
+
+ ///
+ /// 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 SourceExpression Update(Expression containerExpression)
+ => ReferenceEquals(containerExpression, ContainerExpression)
+ ? this
+ : new SourceExpression(containerExpression, Alias);
+
+ ///
+ /// 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()
+ => Alias;
+
+ ///
+ /// Creates a printable string representation of the given expression using .
+ ///
+ /// The expression printer to use.
+ public void Print(ExpressionPrinter expressionPrinter)
+ {
+ if (WithIn)
+ {
+ expressionPrinter
+ .Append(Alias)
+ .Append(" IN ");
+ expressionPrinter.Visit(ContainerExpression);
+ }
+ else
+ {
+ expressionPrinter.Visit(ContainerExpression);
+ expressionPrinter
+ .Append(" AS ")
+ .Append(Alias);
+ }
+ }
+
+ ///
+ /// 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 is SourceExpression other && Equals(other);
+
+ private bool Equals(SourceExpression? other)
+ => ReferenceEquals(this, other)
+ || (other is not null
+ && Alias == other.Alias
+ && WithIn == other.WithIn
+ && ContainerExpression.Equals(other.ContainerExpression));
+
+ ///
+ /// 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 hashCode = new HashCode();
+ hashCode.Add(ContainerExpression);
+ hashCode.Add(Alias);
+ hashCode.Add(WithIn);
+ return hashCode.ToHashCode();
+ }
+}
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlBinaryExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlBinaryExpression.cs
index c1fe7c3fe4f..15c110fec28 100644
--- a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlBinaryExpression.cs
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlBinaryExpression.cs
@@ -33,7 +33,8 @@ public class SqlBinaryExpression : SqlExpression
ExpressionType.NotEqual,
ExpressionType.ExclusiveOr,
ExpressionType.RightShift,
- ExpressionType.LeftShift
+ ExpressionType.LeftShift,
+ ExpressionType.ArrayIndex
};
internal static bool IsValidOperator(ExpressionType operatorType)
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlExpression.cs
index 0f30c1839f2..39bc4345c06 100644
--- a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlExpression.cs
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlExpression.cs
@@ -12,6 +12,7 @@ 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.
///
+[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")]
public abstract class SqlExpression(Type type, CoreTypeMapping? typeMapping)
: Expression, IPrintableExpression
{
diff --git a/src/EFCore.Cosmos/Query/Internal/IQuerySqlGeneratorFactory.cs b/src/EFCore.Cosmos/Query/Internal/IQuerySqlGeneratorFactory.cs
index c087e65a466..0d7d1599389 100644
--- a/src/EFCore.Cosmos/Query/Internal/IQuerySqlGeneratorFactory.cs
+++ b/src/EFCore.Cosmos/Query/Internal/IQuerySqlGeneratorFactory.cs
@@ -17,5 +17,5 @@ public interface IQuerySqlGeneratorFactory
/// 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.
///
- QuerySqlGenerator Create();
+ CosmosQuerySqlGenerator Create();
}
diff --git a/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs
index c56fa117635..c277d45246c 100644
--- a/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs
+++ b/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs
@@ -31,6 +31,14 @@ public interface ISqlExpressionFactory
[return: NotNullIfNotNull(nameof(sqlExpression))]
SqlExpression? ApplyDefaultTypeMapping(SqlExpression? 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.
+ ///
+ (SqlExpression, SqlExpression) ApplyTypeMappingsOnItemAndArray(SqlExpression item, SqlExpression array);
+
///
/// 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
@@ -59,6 +67,13 @@ public interface ISqlExpressionFactory
///
SqlBinaryExpression NotEqual(SqlExpression left, SqlExpression right);
+ ///
+ /// Creates a new which represents an EXISTS operation in a SQL tree.
+ ///
+ /// A subquery to check existence of.
+ /// An expression representing an EXISTS operation in a SQL tree.
+ ExistsExpression Exists(SelectExpression 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
@@ -200,6 +215,14 @@ SqlBinaryExpression Or(
///
SqlBinaryExpression IsNotNull(SqlExpression operand);
+ ///
+ /// 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.
+ ///
+ SqlBinaryExpression ArrayIndex(SqlExpression left, SqlExpression right, Type type, CoreTypeMapping? typeMapping = null);
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore.Cosmos/Query/Internal/QuerySqlGeneratorFactory.cs b/src/EFCore.Cosmos/Query/Internal/QuerySqlGeneratorFactory.cs
index d6861f531e7..0f2bfc78e63 100644
--- a/src/EFCore.Cosmos/Query/Internal/QuerySqlGeneratorFactory.cs
+++ b/src/EFCore.Cosmos/Query/Internal/QuerySqlGeneratorFactory.cs
@@ -17,6 +17,6 @@ public class QuerySqlGeneratorFactory(ITypeMappingSource typeMappingSource) : IQ
/// 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 QuerySqlGenerator Create()
+ public virtual CosmosQuerySqlGenerator Create()
=> new(typeMappingSource);
}
diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs
index 4f902e2e562..7ba033dddb3 100644
--- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs
+++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs
@@ -42,6 +42,7 @@ public class SqlExpressionFactory(ITypeMappingSource typeMappingSource, IModel m
{
null or { TypeMapping: not null } => sqlExpression,
+ ScalarSubqueryExpression e => e.ApplyTypeMapping(typeMapping),
SqlConditionalExpression sqlConditionalExpression => ApplyTypeMappingOnSqlConditional(sqlConditionalExpression, typeMapping),
SqlBinaryExpression sqlBinaryExpression => ApplyTypeMappingOnSqlBinary(sqlBinaryExpression, typeMapping),
SqlUnaryExpression sqlUnaryExpression => ApplyTypeMappingOnSqlUnary(sqlUnaryExpression, typeMapping),
@@ -158,6 +159,18 @@ private SqlExpression ApplyTypeMappingOnSqlBinary(
}
break;
+ case ExpressionType.ArrayIndex:
+ // TODO: This infers based on the CLR type; need to properly infer based on the element type mapping
+ // TODO: being applied here (e.g. WHERE @p[1] = c.PropertyWithValueConverter)
+ var arrayTypeMapping = left.TypeMapping
+ ?? (typeMapping is null ? null : typeMappingSource.FindMapping(typeMapping.ClrType.MakeArrayType()));
+ return new SqlBinaryExpression(
+ ExpressionType.ArrayIndex,
+ ApplyTypeMapping(left, arrayTypeMapping),
+ ApplyDefaultTypeMapping(right),
+ sqlBinaryExpression.Type,
+ typeMapping ?? sqlBinaryExpression.TypeMapping);
+
default:
throw new InvalidOperationException(
CosmosStrings.UnsupportedOperatorForSqlExpression(
@@ -238,6 +251,60 @@ private InExpression ApplyTypeMappingOnIn(InExpression inExpression)
: inExpression.ApplyTypeMapping(_boolTypeMapping);
}
+ ///
+ /// 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 (SqlExpression, SqlExpression) ApplyTypeMappingsOnItemAndArray(SqlExpression itemExpression, SqlExpression arrayExpression)
+ {
+ // Attempt type inference either from the operand to the array or the other way around
+ var arrayMapping = arrayExpression.TypeMapping;
+
+ var itemMapping =
+ itemExpression.TypeMapping
+ // Unwrap convert-to-object nodes - these get added for object[].Contains(x)
+ ?? (itemExpression is SqlUnaryExpression { OperatorType: ExpressionType.Convert } unary && unary.Type == typeof(object)
+ ? unary.Operand.TypeMapping
+ : null)
+ // If we couldn't find a type mapping on the item, try inferring it from the array
+ ?? arrayMapping?.ElementTypeMapping
+ ?? typeMappingSource.FindMapping(itemExpression.Type, model);
+
+ if (itemMapping is null)
+ {
+ throw new InvalidOperationException("Couldn't find element type mapping when applying item/array mappings");
+ }
+
+ // If the array's type mapping isn't provided (parameter/constant), attempt to infer it from the item.
+ if (arrayMapping is null)
+ {
+ // Get a type mapping for the array from the item.
+ // If the array CLR type is anything but an object[], just use that CLR type.
+ // For object[], where the type mapping wouldn't be fine, construct an array/List CLR type based on the
+ // items' CLR type.
+ var arrayClrType = arrayExpression.Type switch
+ {
+ var t when t.TryGetSequenceType() != typeof(object) => t,
+ { IsArray: true } => itemExpression.Type.MakeArrayType(),
+ { IsConstructedGenericType: true, GenericTypeArguments.Length: 1 } t
+ => t.GetGenericTypeDefinition().MakeGenericType(itemExpression.Type),
+ _ => throw new InvalidOperationException(
+ $"Can't construct generic primitive collection type for array type '{arrayExpression.Type}'")
+ };
+
+ arrayMapping = typeMappingSource.FindMapping(arrayClrType, model, itemMapping.ElementTypeMapping);
+
+ if (arrayMapping is null)
+ {
+ throw new InvalidOperationException("Couldn't find array type mapping when applying item/array mappings");
+ }
+ }
+
+ return (ApplyTypeMapping(itemExpression, itemMapping), ApplyTypeMapping(arrayExpression, arrayMapping));
+ }
+
///
/// 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
@@ -292,6 +359,15 @@ public virtual SqlBinaryExpression Equal(SqlExpression left, SqlExpression right
public virtual SqlBinaryExpression NotEqual(SqlExpression left, SqlExpression right)
=> MakeBinary(ExpressionType.NotEqual, left, right, 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 virtual ExistsExpression Exists(SelectExpression subquery)
+ => new(subquery, _boolTypeMapping);
+
///
/// 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
@@ -436,6 +512,15 @@ public virtual SqlBinaryExpression IsNull(SqlExpression operand)
public virtual SqlBinaryExpression IsNotNull(SqlExpression operand)
=> NotEqual(operand, Constant(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 virtual SqlBinaryExpression ArrayIndex(SqlExpression left, SqlExpression right, Type type, CoreTypeMapping? typeMapping = null)
+ => new(ExpressionType.ArrayIndex, left, right, 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
diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs
index e44b07cbf80..e867950b8f9 100644
--- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs
+++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs
@@ -28,21 +28,43 @@ ShapedQueryExpression shapedQueryExpression
EntityProjectionExpression entityProjectionExpression => VisitEntityProjection(entityProjectionExpression),
ObjectArrayProjectionExpression arrayProjectionExpression => VisitObjectArrayProjection(arrayProjectionExpression),
FromSqlExpression fromSqlExpression => VisitFromSql(fromSqlExpression),
- RootReferenceExpression rootReferenceExpression => VisitRootReference(rootReferenceExpression),
+ ObjectReferenceExpression objectReferenceExpression => VisitObjectReference(objectReferenceExpression),
KeyAccessExpression keyAccessExpression => VisitKeyAccess(keyAccessExpression),
ObjectAccessExpression objectAccessExpression => VisitObjectAccess(objectAccessExpression),
+ ScalarSubqueryExpression scalarSubqueryExpression => VisitScalarSubquery(scalarSubqueryExpression),
SqlBinaryExpression sqlBinaryExpression => VisitSqlBinary(sqlBinaryExpression),
SqlConstantExpression sqlConstantExpression => VisitSqlConstant(sqlConstantExpression),
SqlUnaryExpression sqlUnaryExpression => VisitSqlUnary(sqlUnaryExpression),
SqlConditionalExpression sqlConditionalExpression => VisitSqlConditional(sqlConditionalExpression),
SqlParameterExpression sqlParameterExpression => VisitSqlParameter(sqlParameterExpression),
InExpression inExpression => VisitIn(inExpression),
+ ArrayConstantExpression inlineArrayExpression => VisitArrayConstant(inlineArrayExpression),
+ SourceExpression sourceExpression => VisitSource(sourceExpression),
SqlFunctionExpression sqlFunctionExpression => VisitSqlFunction(sqlFunctionExpression),
OrderingExpression orderingExpression => VisitOrdering(orderingExpression),
+ ScalarReferenceExpression valueReferenceExpression => VisitValueReference(valueReferenceExpression),
+ ExistsExpression existsExpression => VisitExists(existsExpression),
+ ArrayExpression arrayExpression => VisitArray(arrayExpression),
_ => base.VisitExtension(extensionExpression)
};
+ ///
+ /// 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 VisitExists(ExistsExpression existsExpression);
+
+ ///
+ /// 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 VisitArray(ArrayExpression arrayExpression);
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -75,6 +97,22 @@ ShapedQueryExpression shapedQueryExpression
///
protected abstract Expression VisitIn(InExpression inExpression);
+ ///
+ /// 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 VisitArrayConstant(ArrayConstantExpression arrayConstantExpression);
+
+ ///
+ /// 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 VisitSource(SourceExpression sourceExpression);
+
///
/// 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
@@ -137,7 +175,15 @@ ShapedQueryExpression shapedQueryExpression
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- protected abstract Expression VisitRootReference(RootReferenceExpression rootReferenceExpression);
+ protected abstract Expression VisitScalarSubquery(ScalarSubqueryExpression scalarSubqueryExpression);
+
+ ///
+ /// 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 VisitObjectReference(ObjectReferenceExpression objectReferenceExpression);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -170,4 +216,12 @@ ShapedQueryExpression shapedQueryExpression
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
protected abstract Expression VisitSelect(SelectExpression selectExpression);
+
+ ///
+ /// 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 VisitValueReference(ScalarReferenceExpression scalarReferenceExpression);
}
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMapping.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMapping.cs
index d2e9b00b9fd..a7b1c835f3d 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMapping.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMapping.cs
@@ -31,6 +31,7 @@ public CosmosTypeMapping(
Type clrType,
ValueComparer? comparer = null,
ValueComparer? keyComparer = null,
+ CoreTypeMapping? elementMapping = null,
JsonValueReaderWriter? jsonValueReaderWriter = null)
: base(
new CoreTypeMappingParameters(
@@ -38,6 +39,7 @@ public CosmosTypeMapping(
converter: null,
comparer,
keyComparer,
+ elementMapping: elementMapping,
jsonValueReaderWriter: jsonValueReaderWriter))
{
}
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
index b24d1a1fcab..48a9227bcc3 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
@@ -71,8 +71,26 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies)
private CoreTypeMapping? FindCollectionMapping(in TypeMappingInfo mappingInfo)
{
var clrType = mappingInfo.ClrType!;
+ var elementMapping = mappingInfo.ElementTypeMapping;
- if (mappingInfo.ElementTypeMapping != null)
+ // Special case for byte[], to allow it to be treated as a scalar (i.e. base64 encoding) rather than as a collection
+ if (clrType == typeof(byte[]) && elementMapping is null)
+ {
+ return null;
+ }
+
+ // First attempt to resolve this as a primitive collection (e.g. List). This does not handle Dictionary.
+ if (TryFindJsonCollectionMapping(
+ mappingInfo, clrType, providerClrType: null, ref elementMapping, out var elementComparer,
+ out var collectionReaderWriter)
+ && elementMapping is not null)
+ {
+ return new CosmosTypeMapping(
+ clrType, elementComparer, elementMapping: elementMapping, jsonValueReaderWriter: collectionReaderWriter);
+ }
+
+ // Next, attempt to resolve this as a dictionary (e.g. Dictionary).
+ if (elementMapping is not null)
{
return null;
}
@@ -100,7 +118,7 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies)
elementType = genericArguments[1];
var elementMappingInfo = new TypeMappingInfo(elementType);
- var elementMapping = FindPrimitiveMapping(elementMappingInfo)
+ elementMapping = FindPrimitiveMapping(elementMappingInfo)
?? FindCollectionMapping(elementMappingInfo);
return elementMapping == null
? null
diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
index a08841312fc..13132440687 100644
--- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
@@ -646,12 +646,9 @@ protected override Expression VisitExtension(Expression extensionExpression)
((SelectExpression)projectionBindingExpression.QueryExpression)
.GetProjection(projectionBindingExpression));
- case ShapedQueryExpression shapedQueryExpression:
- if (shapedQueryExpression.ResultCardinality == ResultCardinality.Enumerable)
- {
- return QueryCompilationContext.NotTranslatedExpression;
- }
-
+ // This case is for a subquery embedded in a lambda, returning a scalar, e.g. Where(b => b.Posts.Count() > 0).
+ // For most cases, generate a scalar subquery (WHERE (SELECT COUNT(*) FROM Posts) > 0).
+ case ShapedQueryExpression { ResultCardinality: not ResultCardinality.Enumerable } shapedQueryExpression:
var shaperExpression = shapedQueryExpression.ShaperExpression;
ProjectionBindingExpression? mappedProjectionBindingExpression = null;
@@ -1022,7 +1019,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
}
}
- var translation = enumerableExpression != null
+ Expression? translation = enumerableExpression != null
? TranslateAggregateMethod(enumerableExpression, method, scalarArguments)
: Dependencies.MethodCallTranslatorProvider.Translate(
_model, sqlObject, method, scalarArguments, _queryCompilationContext.Logger);
@@ -1032,26 +1029,26 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
return translation;
}
- var subqueryTranslation = TranslateAsSubquery(methodCallExpression);
- if (subqueryTranslation == QueryCompilationContext.NotTranslatedExpression)
+ translation = TranslateAsSubquery(methodCallExpression);
+ if (translation != QueryCompilationContext.NotTranslatedExpression)
{
- if (method == StringEqualsWithStringComparison
- || method == StringEqualsWithStringComparisonStatic)
- {
- AddTranslationErrorDetails(CoreStrings.QueryUnableToTranslateStringEqualsWithStringComparison);
- }
- else
- {
- AddTranslationErrorDetails(
- CoreStrings.QueryUnableToTranslateMethod(
- method.DeclaringType?.DisplayName(),
- method.Name));
- }
+ return translation;
+ }
- return QueryCompilationContext.NotTranslatedExpression;
+ if (method == StringEqualsWithStringComparison
+ || method == StringEqualsWithStringComparisonStatic)
+ {
+ AddTranslationErrorDetails(CoreStrings.QueryUnableToTranslateStringEqualsWithStringComparison);
+ }
+ else
+ {
+ AddTranslationErrorDetails(
+ CoreStrings.QueryUnableToTranslateMethod(
+ method.DeclaringType?.DisplayName(),
+ method.Name));
}
- return subqueryTranslation;
+ return QueryCompilationContext.NotTranslatedExpression;
Expression TranslateAsSubquery(Expression expression)
{
diff --git a/src/EFCore.Relational/Query/SqlExpressions/ExistsExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ExistsExpression.cs
index d42b54c186a..bf8e423b180 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/ExistsExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/ExistsExpression.cs
@@ -32,7 +32,7 @@ public ExistsExpression(
}
///
- /// The subquery to check existence of.
+ /// The subquery for which to check for element existence.
///
public virtual SelectExpression Subquery { get; }
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
index b9d2337677f..fbbf01b9b5a 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
@@ -3432,7 +3432,7 @@ private SqlRemappingVisitor PushdownIntoSubqueryInternal(bool liftOrderings = tr
_sqlAliasManager.GenerateTableAlias(_tables is [{ Alias: string singleTableAlias }] ? singleTableAlias : "subquery");
var subquery = new SelectExpression(
- subqueryAlias, _tables.ToList(), _groupBy.ToList(), [], _orderings.ToList(), Annotations, _sqlAliasManager)
+ subqueryAlias, _tables.ToList(), _groupBy.ToList(), projections: [], _orderings.ToList(), Annotations, _sqlAliasManager)
{
IsDistinct = IsDistinct,
Predicate = Predicate,
@@ -4284,6 +4284,8 @@ public override Expression Quote()
RelationalExpressionQuotingUtilities.QuoteTags(Tags),
RelationalExpressionQuotingUtilities.QuoteAnnotations(Annotations));
+ #region Print
+
///
protected override void Print(ExpressionPrinter expressionPrinter)
{
@@ -4438,6 +4440,8 @@ private string PrintShortSql()
public string DebugView
=> this.Print();
+ #endregion Print
+
///
public override bool Equals(object? obj)
=> obj != null
diff --git a/src/EFCore/Query/ExpressionPrinter.cs b/src/EFCore/Query/ExpressionPrinter.cs
index d13448a2a90..b64c26217e2 100644
--- a/src/EFCore/Query/ExpressionPrinter.cs
+++ b/src/EFCore/Query/ExpressionPrinter.cs
@@ -53,7 +53,9 @@ public class ExpressionPrinter : ExpressionVisitor
{ ExpressionType.Modulo, " % " },
{ ExpressionType.And, " & " },
{ ExpressionType.Or, " | " },
- { ExpressionType.ExclusiveOr, " ^ " }
+ { ExpressionType.ExclusiveOr, " ^ " },
+ { ExpressionType.LeftShift, " << " },
+ { ExpressionType.RightShift, " >> " }
};
///
diff --git a/src/EFCore/Storage/Json/JsonValueReaderWriterSource.cs b/src/EFCore/Storage/Json/JsonValueReaderWriterSource.cs
index 046ec7540aa..4e7710b8059 100644
--- a/src/EFCore/Storage/Json/JsonValueReaderWriterSource.cs
+++ b/src/EFCore/Storage/Json/JsonValueReaderWriterSource.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 System.Collections.Concurrent;
+
namespace Microsoft.EntityFrameworkCore.Storage.Json;
///
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/InheritanceQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/InheritanceQueryCosmosTest.cs
index 85da1020934..559016ca746 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/InheritanceQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/InheritanceQueryCosmosTest.cs
@@ -94,7 +94,10 @@ public override Task Can_use_is_kiwi_with_cast(bool async)
AssertSql(
"""
-SELECT VALUE {"Value" : ((c["Discriminator"] = "Kiwi") ? c["FoundOn"] : 0)}
+SELECT VALUE
+{
+ "Value" : ((c["Discriminator"] = "Kiwi") ? c["FoundOn"] : 0)
+}
FROM root c
WHERE c["Discriminator"] IN ("Eagle", "Kiwi")
""");
@@ -136,7 +139,7 @@ public override Task Can_use_is_kiwi_in_projection(bool async)
AssertSql(
"""
-SELECT VALUE {"c" : (c["Discriminator"] = "Kiwi")}
+SELECT (c["Discriminator"] = "Kiwi") AS c
FROM root c
WHERE c["Discriminator"] IN ("Eagle", "Kiwi")
""");
@@ -422,7 +425,7 @@ public override Task Discriminator_with_cast_in_shadow_property(bool async)
AssertSql(
"""
-SELECT VALUE {"Predator" : c["Name"]}
+SELECT c["Name"] AS Predator
FROM root c
WHERE (c["Discriminator"] IN ("Eagle", "Kiwi") AND ("Kiwi" = c["Discriminator"]))
""");
@@ -517,7 +520,7 @@ public override Task Byte_enum_value_constant_used_in_projection(bool async)
AssertSql(
"""
-SELECT VALUE {"c" : (c["IsFlightless"] ? 0 : 1)}
+SELECT (c["IsFlightless"] ? 0 : 1) AS c
FROM root c
WHERE (c["Discriminator"] = "Kiwi")
""");
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs
index 03975383358..eca2f8a19fd 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs
@@ -40,25 +40,16 @@ FROM root c
""");
});
- public override Task Contains_over_keyless_entity_throws(bool async)
- => Fixture.NoSyncTest(
- async, async a =>
- {
- // The `First()` query is always executed synchronously. The outer query does not translate.
- if (!a)
- {
- // Aggregates. Issue #16146.
- await base.Contains_over_keyless_entity_throws(a);
+ public override async Task Contains_over_keyless_entity_throws(bool async)
+ {
+ // The subquery inside the Contains gets executed separately during shaper generation - and synchronously (even in
+ // the async variant of the test), but Cosmos doesn't support sync I/O. So both sync and async variants fail because of unsupported
+ // sync I/O.
+ await CosmosTestHelpers.Instance.NoSyncTest(
+ async: false, a => base.Contains_over_keyless_entity_throws(a));
- AssertSql(
- """
-SELECT c
-FROM root c
-WHERE (c["Discriminator"] = "Customer")
-OFFSET 0 LIMIT 1
-""");
- }
- });
+ AssertSql();
+ }
public override Task Contains_with_local_non_primitive_list_closure_mix(bool async)
=> Fixture.NoSyncTest(
@@ -68,9 +59,11 @@ public override Task Contains_with_local_non_primitive_list_closure_mix(bool asy
AssertSql(
"""
+@__Select_0='["ABCDE","ALFKI"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE", "ALFKI"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__Select_0, c["CustomerID"]))
""");
});
@@ -82,15 +75,19 @@ public override Task Contains_with_local_non_primitive_list_inline_closure_mix(b
AssertSql(
"""
+@__Select_0='["ABCDE","ALFKI"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE", "ALFKI"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__Select_0, c["CustomerID"]))
""",
//
"""
+@__Select_0='["ABCDE","ANATR"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE", "ANATR"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__Select_0, c["CustomerID"]))
""");
});
@@ -1421,15 +1418,19 @@ public override Task Contains_with_local_array_closure(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE", "ALFKI"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
""",
//
"""
+@__ids_0='["ABCDE"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
""");
});
@@ -1449,15 +1450,19 @@ public override Task Contains_with_local_uint_array_closure(bool async)
AssertSql(
"""
+@__ids_0='[0,1]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Employee") AND c["EmployeeID"] IN (0, 1))
+WHERE ((c["Discriminator"] = "Employee") AND ARRAY_CONTAINS(@__ids_0, c["EmployeeID"]))
""",
//
"""
+@__ids_0='[0]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Employee") AND c["EmployeeID"] IN (0))
+WHERE ((c["Discriminator"] = "Employee") AND ARRAY_CONTAINS(@__ids_0, c["EmployeeID"]))
""");
});
@@ -1469,15 +1474,19 @@ public override Task Contains_with_local_nullable_uint_array_closure(bool async)
AssertSql(
"""
+@__ids_0='[0,1]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Employee") AND c["EmployeeID"] IN (0, 1))
+WHERE ((c["Discriminator"] = "Employee") AND ARRAY_CONTAINS(@__ids_0, c["EmployeeID"]))
""",
//
"""
+@__ids_0='[0]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Employee") AND c["EmployeeID"] IN (0))
+WHERE ((c["Discriminator"] = "Employee") AND ARRAY_CONTAINS(@__ids_0, c["EmployeeID"]))
""");
});
@@ -1503,9 +1512,11 @@ public override Task Contains_with_local_list_closure(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE", "ALFKI"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
""");
});
@@ -1517,9 +1528,11 @@ public override Task Contains_with_local_object_list_closure(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE", "ALFKI"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
""");
});
@@ -1531,9 +1544,11 @@ public override Task Contains_with_local_list_closure_all_null(bool async)
AssertSql(
"""
+@__ids_0='[null,null]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = null))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
""");
});
@@ -1559,15 +1574,19 @@ public override Task Contains_with_local_list_inline_closure_mix(bool async)
AssertSql(
"""
+@__p_0='["ABCDE","ALFKI"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE", "ALFKI"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__p_0, c["CustomerID"]))
""",
//
"""
+@__p_0='["ABCDE","ANATR"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE", "ANATR"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__p_0, c["CustomerID"]))
""");
});
@@ -1578,15 +1597,19 @@ public override Task Contains_with_local_enumerable_closure(bool async)
await base.Contains_with_local_enumerable_closure(a);
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE", "ALFKI"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
""",
//
"""
+@__ids_0='["ABCDE"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
""");
});
@@ -1598,11 +1621,12 @@ public override Task Contains_with_local_object_enumerable_closure(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE", "ALFKI"))
-"""
- );
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
+""");
});
public override Task Contains_with_local_enumerable_closure_all_null(bool async)
@@ -1613,11 +1637,12 @@ public override Task Contains_with_local_enumerable_closure_all_null(bool async)
AssertSql(
"""
+@__ids_0='[]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (true = false))
-"""
- );
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
+""");
});
public override async Task Contains_with_local_enumerable_inline(bool async)
@@ -1648,17 +1673,20 @@ public override Task Contains_with_local_ordered_enumerable_closure(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE", "ALFKI"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
""",
-//
+ //
"""
+@__ids_0='["ABCDE"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE"))
-"""
- );
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
+""");
});
public override Task Contains_with_local_object_ordered_enumerable_closure(bool async)
@@ -1669,11 +1697,12 @@ public override Task Contains_with_local_object_ordered_enumerable_closure(bool
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE", "ALFKI"))
-"""
- );
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
+""");
});
public override Task Contains_with_local_ordered_enumerable_closure_all_null(bool async)
@@ -1684,11 +1713,12 @@ public override Task Contains_with_local_ordered_enumerable_closure_all_null(boo
AssertSql(
"""
+@__ids_0='[null,null]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = null))
-"""
- );
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
+""");
});
public override Task Contains_with_local_ordered_enumerable_inline(bool async)
@@ -1714,17 +1744,20 @@ public override Task Contains_with_local_ordered_enumerable_inline_closure_mix(b
AssertSql(
"""
+@__Order_0='["ABCDE","ALFKI"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE", "ALFKI"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__Order_0, c["CustomerID"]))
""",
-//
+ //
"""
+@__Order_0='["ABCDE","ANATR"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE", "ANATR"))
-"""
- );
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__Order_0, c["CustomerID"]))
+""");
});
public override Task Contains_with_local_read_only_collection_closure(bool async)
@@ -1822,9 +1855,11 @@ public override Task Contains_with_local_collection_false(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] NOT IN ("ABCDE", "ALFKI"))
+WHERE ((c["Discriminator"] = "Customer") AND NOT(ARRAY_CONTAINS(@__ids_0, c["CustomerID"])))
""");
});
@@ -1836,9 +1871,11 @@ public override Task Contains_with_local_collection_complex_predicate_and(bool a
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (((c["CustomerID"] = "ALFKI") OR (c["CustomerID"] = "ABCDE")) AND c["CustomerID"] IN ("ABCDE", "ALFKI")))
+WHERE ((c["Discriminator"] = "Customer") AND (((c["CustomerID"] = "ALFKI") OR (c["CustomerID"] = "ABCDE")) AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"])))
""");
});
@@ -1850,9 +1887,11 @@ public override Task Contains_with_local_collection_complex_predicate_or(bool as
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] IN ("ABCDE", "ALFKI") OR ((c["CustomerID"] = "ALFKI") OR (c["CustomerID"] = "ABCDE"))))
+WHERE ((c["Discriminator"] = "Customer") AND (ARRAY_CONTAINS(@__ids_0, c["CustomerID"]) OR ((c["CustomerID"] = "ALFKI") OR (c["CustomerID"] = "ABCDE"))))
""");
});
@@ -1864,9 +1903,11 @@ public override Task Contains_with_local_collection_complex_predicate_not_matchi
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (((c["CustomerID"] = "ALFKI") OR (c["CustomerID"] = "ABCDE")) OR c["CustomerID"] NOT IN ("ABCDE", "ALFKI")))
+WHERE ((c["Discriminator"] = "Customer") AND (((c["CustomerID"] = "ALFKI") OR (c["CustomerID"] = "ABCDE")) OR NOT(ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))))
""");
});
@@ -1878,9 +1919,11 @@ public override Task Contains_with_local_collection_complex_predicate_not_matchi
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] IN ("ABCDE", "ALFKI") AND ((c["CustomerID"] != "ALFKI") AND (c["CustomerID"] != "ABCDE"))))
+WHERE ((c["Discriminator"] = "Customer") AND (ARRAY_CONTAINS(@__ids_0, c["CustomerID"]) AND ((c["CustomerID"] != "ALFKI") AND (c["CustomerID"] != "ABCDE"))))
""");
});
@@ -1892,9 +1935,11 @@ public override Task Contains_with_local_collection_sql_injection(bool async)
AssertSql(
"""
+@__ids_0='["ALFKI","ABC')); GO; DROP TABLE Orders; GO; --"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] IN ("ALFKI", "ABC')); GO; DROP TABLE Orders; GO; --") OR ((c["CustomerID"] = "ALFKI") OR (c["CustomerID"] = "ABCDE"))))
+WHERE ((c["Discriminator"] = "Customer") AND (ARRAY_CONTAINS(@__ids_0, c["CustomerID"]) OR ((c["CustomerID"] = "ALFKI") OR (c["CustomerID"] = "ABCDE"))))
""");
});
@@ -1906,9 +1951,11 @@ public override Task Contains_with_local_collection_empty_closure(bool async)
AssertSql(
"""
+@__ids_0='[]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (true = false))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
""");
});
@@ -2208,9 +2255,11 @@ public override Task Where_subquery_any_equals_operator(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI","ANATR"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE", "ALFKI", "ANATR"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
""");
});
@@ -2236,9 +2285,11 @@ public override Task Where_subquery_any_equals_static(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI","ANATR"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] IN ("ABCDE", "ALFKI", "ANATR"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
""");
});
@@ -2250,15 +2301,19 @@ public override Task Where_subquery_where_any(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI","ANATR"]'
+
SELECT c
FROM root c
-WHERE (((c["Discriminator"] = "Customer") AND (c["City"] = "México D.F.")) AND c["CustomerID"] IN ("ABCDE", "ALFKI", "ANATR"))
+WHERE (((c["Discriminator"] = "Customer") AND (c["City"] = "México D.F.")) AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
""",
//
"""
+@__ids_0='["ABCDE","ALFKI","ANATR"]'
+
SELECT c
FROM root c
-WHERE (((c["Discriminator"] = "Customer") AND (c["City"] = "México D.F.")) AND c["CustomerID"] IN ("ABCDE", "ALFKI", "ANATR"))
+WHERE (((c["Discriminator"] = "Customer") AND (c["City"] = "México D.F.")) AND ARRAY_CONTAINS(@__ids_0, c["CustomerID"]))
""");
});
@@ -2270,9 +2325,11 @@ public override Task Where_subquery_all_not_equals_operator(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI","ANATR"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] NOT IN ("ABCDE", "ALFKI", "ANATR"))
+WHERE ((c["Discriminator"] = "Customer") AND NOT(ARRAY_CONTAINS(@__ids_0, c["CustomerID"])))
""");
});
@@ -2298,9 +2355,11 @@ public override Task Where_subquery_all_not_equals_static(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI","ANATR"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["CustomerID"] NOT IN ("ABCDE", "ALFKI", "ANATR"))
+WHERE ((c["Discriminator"] = "Customer") AND NOT(ARRAY_CONTAINS(@__ids_0, c["CustomerID"])))
""");
});
@@ -2312,15 +2371,19 @@ public override Task Where_subquery_where_all(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI","ANATR"]'
+
SELECT c
FROM root c
-WHERE (((c["Discriminator"] = "Customer") AND (c["City"] = "México D.F.")) AND c["CustomerID"] NOT IN ("ABCDE", "ALFKI", "ANATR"))
+WHERE (((c["Discriminator"] = "Customer") AND (c["City"] = "México D.F.")) AND NOT(ARRAY_CONTAINS(@__ids_0, c["CustomerID"])))
""",
//
"""
+@__ids_0='["ABCDE","ALFKI","ANATR"]'
+
SELECT c
FROM root c
-WHERE (((c["Discriminator"] = "Customer") AND (c["City"] = "México D.F.")) AND c["CustomerID"] NOT IN ("ABCDE", "ALFKI", "ANATR"))
+WHERE (((c["Discriminator"] = "Customer") AND (c["City"] = "México D.F.")) AND NOT(ARRAY_CONTAINS(@__ids_0, c["CustomerID"])))
""");
});
@@ -2540,7 +2603,9 @@ public override Task Contains_inside_Average_without_GroupBy(bool async)
AssertSql(
"""
-SELECT AVG((c["City"] IN ("London", "Berlin") ? 1.0 : 0.0)) AS c
+@__cities_0='["London","Berlin"]'
+
+SELECT AVG((ARRAY_CONTAINS(@__cities_0, c["City"]) ? 1.0 : 0.0)) AS c
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
@@ -2554,7 +2619,9 @@ public override Task Contains_inside_Sum_without_GroupBy(bool async)
AssertSql(
"""
-SELECT SUM((c["City"] IN ("London", "Berlin") ? 1 : 0)) AS c
+@__cities_0='["London","Berlin"]'
+
+SELECT SUM((ARRAY_CONTAINS(@__cities_0, c["City"]) ? 1 : 0)) AS c
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
@@ -2568,9 +2635,11 @@ public override Task Contains_inside_Count_without_GroupBy(bool async)
AssertSql(
"""
+@__cities_0='["London","Berlin"]'
+
SELECT COUNT(1) AS c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["City"] IN ("London", "Berlin"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__cities_0, c["City"]))
""");
});
@@ -2582,9 +2651,11 @@ public override Task Contains_inside_LongCount_without_GroupBy(bool async)
AssertSql(
"""
+@__cities_0='["London","Berlin"]'
+
SELECT COUNT(1) AS c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["City"] IN ("London", "Berlin"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__cities_0, c["City"]))
""");
});
@@ -2596,7 +2667,9 @@ public override Task Contains_inside_Max_without_GroupBy(bool async)
AssertSql(
"""
-SELECT MAX((c["City"] IN ("London", "Berlin") ? 1 : 0)) AS c
+@__cities_0='["London","Berlin"]'
+
+SELECT MAX((ARRAY_CONTAINS(@__cities_0, c["City"]) ? 1 : 0)) AS c
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
@@ -2610,7 +2683,9 @@ public override Task Contains_inside_Min_without_GroupBy(bool async)
AssertSql(
"""
-SELECT MIN((c["City"] IN ("London", "Berlin") ? 1 : 0)) AS c
+@__cities_0='["London","Berlin"]'
+
+SELECT MIN((ARRAY_CONTAINS(@__cities_0, c["City"]) ? 1 : 0)) AS c
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs
index 424a74feccb..3bcc0fc6643 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs
@@ -1911,8 +1911,12 @@ public override Task String_Contains_negated_in_projection(bool async)
await base.String_Contains_negated_in_projection(a);
AssertSql(
-"""
-SELECT VALUE {"Id" : c["CustomerID"], "Value" : NOT(CONTAINS(c["CompanyName"], c["ContactName"]))}
+ """
+SELECT VALUE
+{
+ "Id" : c["CustomerID"],
+ "Value" : NOT(CONTAINS(c["CompanyName"], c["ContactName"]))
+}
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs
index 3a5a1b51799..389a10b238b 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs
@@ -555,7 +555,7 @@ public override async Task Skip(bool async)
Assert.Equal(
CosmosStrings.OffsetRequiresLimit,
(await Assert.ThrowsAsync(
- () => base.Skip_Distinct(async))).Message);
+ () => base.Skip(async))).Message);
AssertSql();
}
@@ -835,18 +835,42 @@ await AssertTranslationFailedWithDetails(
public override async Task Any_simple(bool async)
{
- // Cosmos client evaluation. Issue #17246.
- await AssertTranslationFailed(() => base.Any_simple(async));
+ // Always throws for sync.
+ if (async)
+ {
+ // Top-level Any(), see #33854.
+ var exception = await Assert.ThrowsAsync(() => base.Any_simple(async));
- AssertSql();
+ Assert.Contains("Identifier 'root' could not be resolved.", exception.Message);
+
+ AssertSql(
+ """
+SELECT EXISTS (
+ SELECT 1
+ FROM root c
+ WHERE (c["Discriminator"] = "Customer")) AS c
+""");
+ }
}
public override async Task Any_predicate(bool async)
{
- // Cosmos client evaluation. Issue #17246.
- await AssertTranslationFailed(() => base.Any_predicate(async));
+ // Always throws for sync.
+ if (async)
+ {
+ // Top-level Any(), see #33854.
+ var exception = await Assert.ThrowsAsync(() => base.Any_predicate(async));
- AssertSql();
+ Assert.Contains("Identifier 'root' could not be resolved.", exception.Message);
+
+ AssertSql(
+ """
+SELECT EXISTS (
+ SELECT 1
+ FROM root c
+ WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["ContactName"], "A"))) AS c
+""");
+ }
}
public override async Task Any_nested_negated(bool async)
@@ -1288,10 +1312,27 @@ OFFSET @__p_0 LIMIT @__p_1
public override async Task Skip_Take_Any(bool async)
{
- // Cosmos client evaluation. Issue #17246.
- await AssertTranslationFailed(() => base.Skip_Take_Any(async));
+ // Always throws for sync.
+ if (async)
+ {
+ // Top-level Any(), see #33854.
+ var exception = await Assert.ThrowsAsync(() => base.Skip_Take_Any(async));
- AssertSql();
+ Assert.Contains("Identifier 'root' could not be resolved.", exception.Message);
+
+ AssertSql(
+ """
+@__p_0='5'
+@__p_1='10'
+
+SELECT EXISTS (
+ SELECT 1
+ FROM root c
+ WHERE (c["Discriminator"] = "Customer")
+ ORDER BY c["ContactName"]
+ OFFSET @__p_0 LIMIT @__p_1) AS c
+""");
+ }
}
public override async Task Skip_Take_All(bool async)
@@ -1312,18 +1353,51 @@ public override async Task Take_All(bool async)
public override async Task Skip_Take_Any_with_predicate(bool async)
{
- // Cosmos client evaluation. Issue #17246.
- await AssertTranslationFailed(() => base.Skip_Take_Any_with_predicate(async));
+ // Always throws for sync.
+ if (async)
+ {
+ // Top-level parameterless Any(), see #33854.
+ var exception = await Assert.ThrowsAsync(() => base.Skip_Take_Any_with_predicate(async));
- AssertSql();
+ Assert.Contains("Identifier 'root' could not be resolved.", exception.Message);
+
+ AssertSql(
+ """
+@__p_0='5'
+@__p_1='7'
+
+SELECT EXISTS (
+ SELECT 1
+ FROM root c
+ WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["CustomerID"], "C"))
+ ORDER BY c["CustomerID"]
+ OFFSET @__p_0 LIMIT @__p_1) AS c
+""");
+ }
}
public override async Task Take_Any_with_predicate(bool async)
{
- // Cosmos client evaluation. Issue #17246.
- await AssertTranslationFailed(() => base.Take_Any_with_predicate(async));
+ // Always throws for sync.
+ if (async)
+ {
+ // Top-level parameterless Any(), see #33854.
+ var exception = await Assert.ThrowsAsync(() => base.Take_Any_with_predicate(async));
- AssertSql();
+ Assert.Contains("Identifier 'root' could not be resolved.", exception.Message);
+
+ AssertSql(
+ """
+@__p_0='5'
+
+SELECT EXISTS (
+ SELECT 1
+ FROM root c
+ WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["CustomerID"], "B"))
+ ORDER BY c["CustomerID"]
+ OFFSET 0 LIMIT @__p_0) AS c
+""");
+ }
}
public override Task OrderBy(bool async)
@@ -1520,10 +1594,22 @@ FROM root c
public override async Task OrderBy_ThenBy_Any(bool async)
{
- // Cosmos client evaluation. Issue #17246.
- await AssertTranslationFailed(() => base.OrderBy_ThenBy_Any(async));
+ // Always throws for sync.
+ if (async)
+ {
+ // Top-level Any(), see #33854.
+ var exception = await Assert.ThrowsAsync(() => base.OrderBy_ThenBy_Any(async));
- AssertSql();
+ Assert.Contains("Identifier 'root' could not be resolved.", exception.Message);
+
+ AssertSql(
+ """
+SELECT EXISTS (
+ SELECT 1
+ FROM root c
+ WHERE (c["Discriminator"] = "Customer")) AS c
+""");
+ }
}
public override async Task OrderBy_correlated_subquery1(bool async)
@@ -1634,7 +1720,11 @@ public override Task Select_DTO_with_member_init_distinct_translated_to_server(b
AssertSql(
"""
-SELECT DISTINCT VALUE {"Id" : c["CustomerID"], "Count" : c["OrderID"]}
+SELECT DISTINCT VALUE
+{
+ "Id" : c["CustomerID"],
+ "Count" : c["OrderID"]
+}
FROM root c
WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] < 10300))
""");
@@ -1751,7 +1841,12 @@ await Assert.ThrowsAsync(
AssertSql(
"""
-SELECT VALUE {"CustomerID" : c["CustomerID"], "CompanyName" : c["CompanyName"], "Region" : ((c["Region"] != null) ? c["Region"] : "ZZ")}
+SELECT VALUE
+{
+ "CustomerID" : c["CustomerID"],
+ "CompanyName" : c["CompanyName"],
+ "Region" : ((c["Region"] != null) ? c["Region"] : "ZZ")
+}
FROM root c
WHERE (c["Discriminator"] = "Customer")
ORDER BY ((c["Region"] != null) ? c["Region"] : "ZZ"), c["CustomerID"]
@@ -1820,7 +1915,12 @@ public override Task Projection_null_coalesce_operator(bool async)
AssertSql(
"""
-SELECT VALUE {"CustomerID" : c["CustomerID"], "CompanyName" : c["CompanyName"], "Region" : ((c["Region"] != null) ? c["Region"] : "ZZ")}
+SELECT VALUE
+{
+ "CustomerID" : c["CustomerID"],
+ "CompanyName" : c["CompanyName"],
+ "Region" : ((c["Region"] != null) ? c["Region"] : "ZZ")
+}
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
@@ -1863,7 +1963,12 @@ await Assert.ThrowsAsync(
"""
@__p_0='5'
-SELECT VALUE {"CustomerID" : c["CustomerID"], "CompanyName" : c["CompanyName"], "Region" : ((c["Region"] != null) ? c["Region"] : "ZZ")}
+SELECT VALUE
+{
+ "CustomerID" : c["CustomerID"],
+ "CompanyName" : c["CompanyName"],
+ "Region" : ((c["Region"] != null) ? c["Region"] : "ZZ")
+}
FROM root c
WHERE (c["Discriminator"] = "Customer")
ORDER BY ((c["Region"] != null) ? c["Region"] : "ZZ")
@@ -2082,7 +2187,11 @@ public override async Task Select_bitwise_or(bool async)
AssertSql(
"""
-SELECT VALUE {"CustomerID" : c["CustomerID"], "Value" : ((c["CustomerID"] = "ALFKI") | (c["CustomerID"] = "ANATR"))}
+SELECT VALUE
+{
+ "CustomerID" : c["CustomerID"],
+ "Value" : ((c["CustomerID"] = "ALFKI") | (c["CustomerID"] = "ANATR"))
+}
FROM root c
WHERE (c["Discriminator"] = "Customer")
ORDER BY c["CustomerID"]
@@ -2100,7 +2209,11 @@ public override async Task Select_bitwise_or_multiple(bool async)
AssertSql(
"""
-SELECT VALUE {"CustomerID" : c["CustomerID"], "Value" : (((c["CustomerID"] = "ALFKI") | (c["CustomerID"] = "ANATR")) | (c["CustomerID"] = "ANTON"))}
+SELECT VALUE
+{
+ "CustomerID" : c["CustomerID"],
+ "Value" : (((c["CustomerID"] = "ALFKI") | (c["CustomerID"] = "ANATR")) | (c["CustomerID"] = "ANTON"))
+}
FROM root c
WHERE (c["Discriminator"] = "Customer")
ORDER BY c["CustomerID"]
@@ -2118,7 +2231,11 @@ public override async Task Select_bitwise_and(bool async)
AssertSql(
"""
-SELECT VALUE {"CustomerID" : c["CustomerID"], "Value" : ((c["CustomerID"] = "ALFKI") & (c["CustomerID"] = "ANATR"))}
+SELECT VALUE
+{
+ "CustomerID" : c["CustomerID"],
+ "Value" : ((c["CustomerID"] = "ALFKI") & (c["CustomerID"] = "ANATR"))
+}
FROM root c
WHERE (c["Discriminator"] = "Customer")
ORDER BY c["CustomerID"]
@@ -2136,7 +2253,11 @@ public override async Task Select_bitwise_and_or(bool async)
AssertSql(
"""
-SELECT VALUE {"CustomerID" : c["CustomerID"], "Value" : (((c["CustomerID"] = "ALFKI") & (c["CustomerID"] = "ANATR")) | (c["CustomerID"] = "ANTON"))}
+SELECT VALUE
+{
+ "CustomerID" : c["CustomerID"],
+ "Value" : (((c["CustomerID"] = "ALFKI") & (c["CustomerID"] = "ANATR")) | (c["CustomerID"] = "ANTON"))
+}
FROM root c
WHERE (c["Discriminator"] = "Customer")
ORDER BY c["CustomerID"]
@@ -2260,7 +2381,11 @@ public override async Task Select_bitwise_or_with_logical_or(bool async)
AssertSql(
"""
-SELECT VALUE {"CustomerID" : c["CustomerID"], "Value" : (((c["CustomerID"] = "ALFKI") | (c["CustomerID"] = "ANATR")) OR (c["CustomerID"] = "ANTON"))}
+SELECT VALUE
+{
+ "CustomerID" : c["CustomerID"],
+ "Value" : (((c["CustomerID"] = "ALFKI") | (c["CustomerID"] = "ANATR")) OR (c["CustomerID"] = "ANTON"))
+}
FROM root c
WHERE (c["Discriminator"] = "Customer")
ORDER BY c["CustomerID"]
@@ -2278,7 +2403,11 @@ public override async Task Select_bitwise_and_with_logical_and(bool async)
AssertSql(
"""
-SELECT VALUE {"CustomerID" : c["CustomerID"], "Value" : (((c["CustomerID"] = "ALFKI") & (c["CustomerID"] = "ANATR")) AND (c["CustomerID"] = "ANTON"))}
+SELECT VALUE
+{
+ "CustomerID" : c["CustomerID"],
+ "Value" : (((c["CustomerID"] = "ALFKI") & (c["CustomerID"] = "ANATR")) AND (c["CustomerID"] = "ANTON"))
+}
FROM root c
WHERE (c["Discriminator"] = "Customer")
ORDER BY c["CustomerID"]
@@ -2516,7 +2645,7 @@ public override Task Add_minutes_on_constant_value(bool async)
AssertSql(
"""
-SELECT VALUE {"c" : (c["OrderID"] % 25)}
+SELECT (c["OrderID"] % 25) AS c
FROM root c
WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] < 10500))
ORDER BY c["OrderID"]
@@ -2832,7 +2961,7 @@ public override Task Anonymous_complex_distinct_where(bool async)
AssertSql(
"""
-SELECT DISTINCT VALUE {"A" : (c["CustomerID"] || c["City"])}
+SELECT DISTINCT (c["CustomerID"] || c["City"]) AS A
FROM root c
WHERE ((c["Discriminator"] = "Customer") AND ((c["CustomerID"] || c["City"]) = "ALFKIBerlin"))
""");
@@ -2866,7 +2995,7 @@ await Assert.ThrowsAsync(
AssertSql(
"""
-SELECT VALUE {"A" : (c["CustomerID"] || c["City"])}
+SELECT (c["CustomerID"] || c["City"]) AS A
FROM root c
WHERE (c["Discriminator"] = "Customer")
ORDER BY (c["CustomerID"] || c["City"])
@@ -2890,7 +3019,7 @@ public override Task DTO_member_distinct_where(bool async)
AssertSql(
"""
-SELECT DISTINCT VALUE {"Property" : c["CustomerID"]}
+SELECT DISTINCT c["CustomerID"] AS Property
FROM root c
WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI"))
""");
@@ -2922,7 +3051,7 @@ public override Task DTO_complex_distinct_where(bool async)
AssertSql(
"""
-SELECT DISTINCT VALUE {"Property" : (c["CustomerID"] || c["City"])}
+SELECT DISTINCT (c["CustomerID"] || c["City"]) AS Property
FROM root c
WHERE ((c["Discriminator"] = "Customer") AND ((c["CustomerID"] || c["City"]) = "ALFKIBerlin"))
""");
@@ -2957,7 +3086,7 @@ await Assert.ThrowsAsync(
AssertSql(
"""
-SELECT VALUE {"Property" : (c["CustomerID"] || c["City"])}
+SELECT (c["CustomerID"] || c["City"]) AS Property
FROM root c
WHERE (c["Discriminator"] = "Customer")
ORDER BY (c["CustomerID"] || c["City"])
@@ -3390,7 +3519,7 @@ public override Task OrderBy_Dto_projection_skip_take(bool async)
@__p_0='5'
@__p_1='10'
-SELECT VALUE {"Id" : c["CustomerID"]}
+SELECT c["CustomerID"] AS Id
FROM root c
WHERE (c["Discriminator"] = "Customer")
ORDER BY c["CustomerID"]
@@ -3416,10 +3545,12 @@ await Assert.ThrowsAsync(
AssertSql(
"""
+@__list_0='[]'
+
SELECT c
FROM root c
WHERE (c["Discriminator"] = "Customer")
-ORDER BY (true = false)
+ORDER BY ARRAY_CONTAINS(@__list_0, c["CustomerID"])
""");
}
}
@@ -3434,10 +3565,12 @@ await Assert.ThrowsAsync(
AssertSql(
"""
+@__list_0='[]'
+
SELECT c
FROM root c
WHERE (c["Discriminator"] = "Customer")
-ORDER BY NOT((true = false))
+ORDER BY NOT(ARRAY_CONTAINS(@__list_0, c["CustomerID"]))
""");
}
}
@@ -4269,7 +4402,7 @@ public override Task Null_Coalesce_Short_Circuit_with_server_correlated_leftover
AssertSql(
"""
-SELECT VALUE {"Result" : false}
+SELECT false AS Result
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
@@ -4479,7 +4612,11 @@ public override Task Ternary_should_not_evaluate_both_sides(bool async)
AssertSql(
"""
-SELECT VALUE {"CustomerID" : c["CustomerID"], "Data1" : "none"}
+SELECT VALUE
+{
+ "CustomerID" : c["CustomerID"],
+ "Data1" : "none"
+}
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
@@ -4682,7 +4819,7 @@ public override Task Ternary_should_not_evaluate_both_sides_with_parameter(bool
AssertSql(
"""
-SELECT VALUE {"Data1" : true}
+SELECT true AS Data1
FROM root c
WHERE (c["Discriminator"] = "Order")
""");
@@ -4858,14 +4995,33 @@ public override async Task Parameter_extraction_can_throw_exception_from_user_co
public override async Task Where_query_composition5(bool async)
{
- await base.Where_query_composition5(async);
+ var exception = await Assert.ThrowsAsync(
+ () => AssertQuery(
+ async,
+ ss => from c1 in ss.Set()
+ where c1.IsLondon == ss.Set().OrderBy(c => c.CustomerID).First().IsLondon
+ select c1));
+
+ Assert.Contains(CosmosStrings.NonCorrelatedSubqueriesNotSupported, exception.Message);
+ Assert.Contains(CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer)), exception.Message);
AssertSql();
}
public override async Task Where_query_composition6(bool async)
{
- await base.Where_query_composition6(async);
+ var exception = await Assert.ThrowsAsync(
+ () => AssertQuery(
+ async,
+ ss => from c1 in ss.Set()
+ where c1.IsLondon
+ == ss.Set().OrderBy(c => c.CustomerID)
+ .Select(c => new { Foo = c })
+ .First().Foo.IsLondon
+ select c1));
+
+ Assert.Contains(CosmosStrings.NonCorrelatedSubqueriesNotSupported, exception.Message);
+ Assert.Contains(CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer)), exception.Message);
AssertSql();
}
@@ -4999,9 +5155,11 @@ public override Task Contains_over_concatenated_columns_with_different_sizes(boo
AssertSql(
"""
+@__data_0='["ALFKIAlfreds Futterkiste","ANATRAna Trujillo Emparedados y helados"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] || c["CompanyName"]) IN ("ALFKIAlfreds Futterkiste", "ANATRAna Trujillo Emparedados y helados"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__data_0, (c["CustomerID"] || c["CompanyName"])))
""");
});
@@ -5013,9 +5171,11 @@ public override Task Contains_over_concatenated_column_and_constant(bool async)
AssertSql(
"""
+@__data_0='["ALFKISomeConstant","ANATRSomeConstant","ALFKIX"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] || "SomeConstant") IN ("ALFKISomeConstant", "ANATRSomeConstant", "ALFKIX"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__data_0, (c["CustomerID"] || "SomeConstant")))
""");
});
@@ -5035,11 +5195,12 @@ public override Task Contains_over_concatenated_column_and_parameter(bool async)
AssertSql(
"""
+@__data_1='["ALFKISomeVariable","ANATRSomeVariable","ALFKIX"]'
@__someVariable_0='SomeVariable'
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] || @__someVariable_0) IN ("ALFKISomeVariable", "ANATRSomeVariable", "ALFKIX"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__data_1, (c["CustomerID"] || @__someVariable_0)))
""");
});
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs
index fecf0a5be64..034445f1628 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs
@@ -37,7 +37,10 @@ await AssertQuery(
AssertSql(
"""
-SELECT VALUE {"Value" : c["OrderID"]}
+SELECT VALUE
+{
+ "Value" : c["OrderID"]
+}
FROM root c
WHERE (c["Discriminator"] = "Order")
""");
@@ -51,7 +54,11 @@ public override Task Projection_when_arithmetic_expression_precedence(bool async
AssertSql(
"""
-SELECT VALUE {"A" : (c["OrderID"] / (c["OrderID"] / 2)), "B" : ((c["OrderID"] / c["OrderID"]) / 2)}
+SELECT VALUE
+{
+ "A" : (c["OrderID"] / (c["OrderID"] / 2)),
+ "B" : ((c["OrderID"] / c["OrderID"]) / 2)
+}
FROM root c
WHERE (c["Discriminator"] = "Order")
""");
@@ -65,7 +72,16 @@ public override Task Projection_when_arithmetic_expressions(bool async)
AssertSql(
"""
-SELECT VALUE {"OrderID" : c["OrderID"], "Double" : (c["OrderID"] * 2), "Add" : (c["OrderID"] + 23), "Sub" : (100000 - c["OrderID"]), "Divide" : (c["OrderID"] / (c["OrderID"] / 2)), "Literal" : 42, "o" : c}
+SELECT VALUE
+{
+ "OrderID" : c["OrderID"],
+ "Double" : (c["OrderID"] * 2),
+ "Add" : (c["OrderID"] + 23),
+ "Sub" : (100000 - c["OrderID"]),
+ "Divide" : (c["OrderID"] / (c["OrderID"] / 2)),
+ "Literal" : 42,
+ "o" : c
+}
FROM root c
WHERE (c["Discriminator"] = "Order")
""");
@@ -169,7 +185,7 @@ public override Task Project_to_int_array(bool async)
AssertSql(
"""
-SELECT c["EmployeeID"], c["ReportsTo"]
+SELECT [c["EmployeeID"], c["ReportsTo"]] AS c
FROM root c
WHERE ((c["Discriminator"] = "Employee") AND (c["EmployeeID"] = 1))
""");
@@ -195,7 +211,7 @@ await Assert.ThrowsAsync(
"""
@__boolean_0='false'
-SELECT VALUE {"c" : @__boolean_0}
+SELECT @__boolean_0 AS c
FROM root c
WHERE (c["Discriminator"] = "Customer")
ORDER BY @__boolean_0
@@ -267,7 +283,11 @@ public override Task Select_anonymous_bool_constant_true(bool async)
AssertSql(
"""
-SELECT VALUE {"CustomerID" : c["CustomerID"], "ConstantTrue" : true}
+SELECT VALUE
+{
+ "CustomerID" : c["CustomerID"],
+ "ConstantTrue" : true
+}
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
@@ -281,7 +301,11 @@ public override Task Select_anonymous_constant_in_expression(bool async)
AssertSql(
"""
-SELECT VALUE {"CustomerID" : c["CustomerID"], "Expression" : (LENGTH(c["CustomerID"]) + 5)}
+SELECT VALUE
+{
+ "CustomerID" : c["CustomerID"],
+ "Expression" : (LENGTH(c["CustomerID"]) + 5)
+}
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
@@ -295,7 +319,11 @@ public override Task Select_anonymous_conditional_expression(bool async)
AssertSql(
"""
-SELECT VALUE {"ProductID" : c["ProductID"], "IsAvailable" : (c["UnitsInStock"] > 0)}
+SELECT VALUE
+{
+ "ProductID" : c["ProductID"],
+ "IsAvailable" : (c["UnitsInStock"] > 0)
+}
FROM root c
WHERE (c["Discriminator"] = "Product")
""");
@@ -323,7 +351,7 @@ public override Task Select_constant_int(bool async)
AssertSql(
"""
-SELECT VALUE {"c" : 0}
+SELECT 0 AS c
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
@@ -337,7 +365,7 @@ public override Task Select_constant_null_string(bool async)
AssertSql(
"""
-SELECT VALUE {"c" : null}
+SELECT null AS c
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
@@ -353,7 +381,7 @@ public override Task Select_local(bool async)
"""
@__x_0='10'
-SELECT VALUE {"c" : @__x_0}
+SELECT @__x_0 AS c
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
@@ -550,7 +578,7 @@ public override Task Select_non_matching_value_types_from_binary_expression_intr
AssertSql(
"""
-SELECT VALUE {"c" : (c["OrderID"] + c["OrderID"])}
+SELECT (c["OrderID"] + c["OrderID"]) AS c
FROM root c
WHERE ((c["Discriminator"] = "Order") AND (c["CustomerID"] = "ALFKI"))
ORDER BY c["OrderID"]
@@ -581,7 +609,7 @@ public override Task Select_non_matching_value_types_from_unary_expression_intro
AssertSql(
"""
-SELECT VALUE {"c" : -(c["OrderID"])}
+SELECT -(c["OrderID"]) AS c
FROM root c
WHERE ((c["Discriminator"] = "Order") AND (c["CustomerID"] = "ALFKI"))
ORDER BY c["OrderID"]
@@ -667,7 +695,7 @@ public override Task Select_conditional_with_null_comparison_in_test(bool async)
AssertSql(
"""
-SELECT VALUE {"c" : ((c["CustomerID"] = null) ? true : (c["OrderID"] < 100))}
+SELECT ((c["CustomerID"] = null) ? true : (c["OrderID"] < 100)) AS c
FROM root c
WHERE ((c["Discriminator"] = "Order") AND (c["CustomerID"] = "ALFKI"))
""");
@@ -908,7 +936,7 @@ public override Task Select_byte_constant(bool async)
AssertSql(
"""
-SELECT VALUE {"c" : ((c["CustomerID"] = "ALFKI") ? 1 : 2)}
+SELECT ((c["CustomerID"] = "ALFKI") ? 1 : 2) AS c
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
@@ -922,7 +950,7 @@ public override Task Select_short_constant(bool async)
AssertSql(
"""
-SELECT VALUE {"c" : ((c["CustomerID"] = "ALFKI") ? 1 : 2)}
+SELECT ((c["CustomerID"] = "ALFKI") ? 1 : 2) AS c
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
@@ -936,7 +964,7 @@ public override Task Select_bool_constant(bool async)
AssertSql(
"""
-SELECT VALUE {"c" : ((c["CustomerID"] = "ALFKI") ? true : false)}
+SELECT ((c["CustomerID"] = "ALFKI") ? true : false) AS c
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
@@ -964,7 +992,7 @@ public override Task Anonymous_projection_with_repeated_property_being_ordered(b
AssertSql(
"""
-SELECT VALUE {"A" : c["CustomerID"]}
+SELECT c["CustomerID"] AS A
FROM root c
WHERE (c["Discriminator"] = "Customer")
ORDER BY c["CustomerID"]
@@ -1181,7 +1209,11 @@ public override Task Explicit_cast_in_arithmetic_operation_is_preserved(bool asy
AssertSql(
"""
-SELECT VALUE {"OrderID" : c["OrderID"], "c" : (c["OrderID"] + 1000)}
+SELECT VALUE
+{
+ "OrderID" : c["OrderID"],
+ "c" : (c["OrderID"] + 1000)
+}
FROM root c
WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] = 10250))
""");
@@ -1243,7 +1275,7 @@ public override Task Coalesce_over_nullable_uint(bool async)
AssertSql(
"""
-SELECT VALUE {"c" : ((c["EmployeeID"] != null) ? c["EmployeeID"] : 0)}
+SELECT ((c["EmployeeID"] != null) ? c["EmployeeID"] : 0) AS c
FROM root c
WHERE (c["Discriminator"] = "Order")
""");
@@ -1311,7 +1343,7 @@ public override Task Projection_custom_type_in_both_sides_of_ternary(bool async)
AssertSql(
"""
-SELECT VALUE {"c" : (c["City"] = "Seattle")}
+SELECT (c["City"] = "Seattle") AS c
FROM root c
WHERE (c["Discriminator"] = "Customer")
ORDER BY c["CustomerID"]
@@ -1412,7 +1444,7 @@ public override Task Projection_take_predicate_projection(bool async)
"""
@__p_0='10'
-SELECT VALUE {"Aggregate" : ((c["CustomerID"] || " ") || c["City"])}
+SELECT ((c["CustomerID"] || " ") || c["City"]) AS Aggregate
FROM root c
WHERE ((c["Discriminator"] = "Customer") AND STARTSWITH(c["CustomerID"], "A"))
ORDER BY c["CustomerID"]
@@ -1430,7 +1462,7 @@ public override Task Projection_take_projection_doesnt_project_intermittent_colu
"""
@__p_0='10'
-SELECT VALUE {"Aggregate" : ((c["CustomerID"] || " ") || c["City"])}
+SELECT ((c["CustomerID"] || " ") || c["City"]) AS Aggregate
FROM root c
WHERE (c["Discriminator"] = "Customer")
ORDER BY c["CustomerID"]
@@ -1508,7 +1540,12 @@ public override Task Ternary_in_client_eval_assigns_correct_types(bool async)
AssertSql(
"""
-SELECT VALUE {"CustomerID" : c["CustomerID"], "OrderDate" : c["OrderDate"], "c" : (c["OrderID"] - 10000)}
+SELECT VALUE
+{
+ "CustomerID" : c["CustomerID"],
+ "OrderDate" : c["OrderDate"],
+ "c" : (c["OrderID"] - 10000)
+}
FROM root c
WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] < 10300))
ORDER BY c["OrderID"]
@@ -1953,7 +1990,7 @@ public override Task Select_anonymous_literal(bool async)
AssertSql(
"""
-SELECT VALUE {"X" : 10}
+SELECT 10 AS X
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
@@ -1981,7 +2018,7 @@ public override Task Select_over_10_nested_ternary_condition(bool async)
AssertSql(
"""
-SELECT VALUE {"c" : ((c["CustomerID"] = "1") ? "01" : ((c["CustomerID"] = "2") ? "02" : ((c["CustomerID"] = "3") ? "03" : ((c["CustomerID"] = "4") ? "04" : ((c["CustomerID"] = "5") ? "05" : ((c["CustomerID"] = "6") ? "06" : ((c["CustomerID"] = "7") ? "07" : ((c["CustomerID"] = "8") ? "08" : ((c["CustomerID"] = "9") ? "09" : ((c["CustomerID"] = "10") ? "10" : ((c["CustomerID"] = "11") ? "11" : null)))))))))))}
+SELECT ((c["CustomerID"] = "1") ? "01" : ((c["CustomerID"] = "2") ? "02" : ((c["CustomerID"] = "3") ? "03" : ((c["CustomerID"] = "4") ? "04" : ((c["CustomerID"] = "5") ? "05" : ((c["CustomerID"] = "6") ? "06" : ((c["CustomerID"] = "7") ? "07" : ((c["CustomerID"] = "8") ? "08" : ((c["CustomerID"] = "9") ? "09" : ((c["CustomerID"] = "10") ? "10" : ((c["CustomerID"] = "11") ? "11" : null))))))))))) AS c
FROM root c
WHERE (c["Discriminator"] = "Customer")
""");
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs
index f155b73d375..9193055635d 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs
@@ -1,6 +1,7 @@
// 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;
using Microsoft.EntityFrameworkCore.TestModels.Northwind;
using Xunit.Sdk;
@@ -467,7 +468,7 @@ FROM root c
public override async Task Where_as_queryable_expression(bool async)
{
- // Cosmos client evaluation. Issue #17246.
+ // Uncorrelated subquery, not supported by Cosmos
await AssertTranslationFailed(() => base.Where_as_queryable_expression(async));
AssertSql();
@@ -912,7 +913,7 @@ OFFSET 0 LIMIT @__p_0
public override async Task Where_shadow_subquery_FirstOrDefault(bool async)
{
- // Cosmos client evaluation. Issue #17246.
+ // Uncorrelated subquery, not supported by Cosmos
await AssertTranslationFailed(() => base.Where_shadow_subquery_FirstOrDefault(async));
AssertSql();
@@ -927,7 +928,7 @@ public override async Task Where_client(bool async)
public override async Task Where_subquery_correlated(bool async)
{
- // Cosmos client evaluation. Issue #17246.
+ // Uncorrelated subquery, not supported by Cosmos
await AssertTranslationFailed(() => base.Where_subquery_correlated(async));
AssertSql();
@@ -2271,7 +2272,7 @@ public override async Task Where_contains_on_navigation(bool async)
public override async Task Where_subquery_FirstOrDefault_is_null(bool async)
{
- // Cosmos client evaluation. Issue #17246.
+ // Uncorrelated subquery, not supported by Cosmos
await AssertTranslationFailed(() => base.Where_subquery_FirstOrDefault_is_null(async));
AssertSql();
@@ -2339,7 +2340,7 @@ FROM root c
public override async Task Filter_non_nullable_value_after_FirstOrDefault_on_empty_collection(bool async)
{
- // Cosmos client evaluation. Issue #17246.
+ // Uncorrelated subquery, not supported by Cosmos
await AssertTranslationFailed(() => base.Filter_non_nullable_value_after_FirstOrDefault_on_empty_collection(async));
AssertSql();
@@ -2513,9 +2514,11 @@ public override Task Where_list_object_contains_over_value_type(bool async)
AssertSql(
"""
+@__orderIds_0='[10248,10249]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Order") AND c["OrderID"] IN (10248, 10249))
+WHERE ((c["Discriminator"] = "Order") AND ARRAY_CONTAINS(@__orderIds_0, c["OrderID"]))
""");
});
@@ -2527,9 +2530,11 @@ public override Task Where_array_of_object_contains_over_value_type(bool async)
AssertSql(
"""
+@__orderIds_0='[10248,10249]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Order") AND c["OrderID"] IN (10248, 10249))
+WHERE ((c["Discriminator"] = "Order") AND ARRAY_CONTAINS(@__orderIds_0, c["OrderID"]))
""");
});
@@ -2563,7 +2568,7 @@ FROM root c
public override async Task FirstOrDefault_over_scalar_projection_compared_to_null(bool async)
{
- // Cosmos client evaluation. Issue #17246.
+ // Uncorrelated subquery, not supported by Cosmos
await AssertTranslationFailed(() => base.FirstOrDefault_over_scalar_projection_compared_to_null(async));
AssertSql();
@@ -2571,7 +2576,7 @@ public override async Task FirstOrDefault_over_scalar_projection_compared_to_nul
public override async Task FirstOrDefault_over_scalar_projection_compared_to_not_null(bool async)
{
- // Cosmos client evaluation. Issue #17246.
+ // Uncorrelated subquery, not supported by Cosmos
await AssertTranslationFailed(() => base.FirstOrDefault_over_scalar_projection_compared_to_not_null(async));
AssertSql();
@@ -2697,9 +2702,11 @@ public override Task Where_Contains_and_comparison(bool async)
AssertSql(
"""
+@__customerIds_0='["ALFKI","FISSA","WHITC"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] IN ("ALFKI", "FISSA", "WHITC") AND (c["City"] = "Seattle")))
+WHERE ((c["Discriminator"] = "Customer") AND (ARRAY_CONTAINS(@__customerIds_0, c["CustomerID"]) AND (c["City"] = "Seattle")))
""");
});
@@ -2711,9 +2718,11 @@ public override Task Where_Contains_or_comparison(bool async)
AssertSql(
"""
+@__customerIds_0='["ALFKI","FISSA"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] IN ("ALFKI", "FISSA") OR (c["City"] = "Seattle")))
+WHERE ((c["Discriminator"] = "Customer") AND (ARRAY_CONTAINS(@__customerIds_0, c["CustomerID"]) OR (c["City"] = "Seattle")))
""");
});
@@ -2905,9 +2914,11 @@ public override Task Generic_Ilist_contains_translates_to_server(bool async)
AssertSql(
"""
+@__cities_0='["Seattle"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND c["City"] IN ("Seattle"))
+WHERE ((c["Discriminator"] = "Customer") AND ARRAY_CONTAINS(@__cities_0, c["City"]))
""");
});
@@ -3054,9 +3065,11 @@ public override Task Parameter_array_Contains_OrElse_comparison_with_constant(bo
AssertSql(
"""
+@__array_0='["ALFKI","ANATR"]'
+
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] IN ("ALFKI", "ANATR") OR (c["CustomerID"] = "ANTON")))
+WHERE ((c["Discriminator"] = "Customer") AND (ARRAY_CONTAINS(@__array_0, c["CustomerID"]) OR (c["CustomerID"] = "ANTON")))
""");
});
@@ -3069,11 +3082,12 @@ public override Task Parameter_array_Contains_OrElse_comparison_with_parameter_w
AssertSql(
"""
@__prm1_0='ANTON'
+@__array_1='["ALFKI","ANATR"]'
@__prm2_2='ALFKI'
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (((c["CustomerID"] = @__prm1_0) OR c["CustomerID"] IN ("ALFKI", "ANATR")) OR (c["CustomerID"] = @__prm2_2)))
+WHERE ((c["Discriminator"] = "Customer") AND (((c["CustomerID"] = @__prm1_0) OR ARRAY_CONTAINS(@__array_1, c["CustomerID"])) OR (c["CustomerID"] = @__prm2_2)))
""");
});
@@ -3141,7 +3155,11 @@ public override Task Where_simple_shadow_projection_mixed(bool async)
AssertSql(
"""
-SELECT VALUE {"e" : c, "Title" : c["Title"]}
+SELECT VALUE
+{
+ "e" : c,
+ "Title" : c["Title"]
+}
FROM root c
WHERE ((c["Discriminator"] = "Employee") AND (c["Title"] = "Sales Representative"))
""");
@@ -3174,7 +3192,7 @@ public override Task Where_primitive_tracked2(bool async)
"""
@__p_0='9'
-SELECT VALUE {"e" : c}
+SELECT c AS e
FROM root c
WHERE ((c["Discriminator"] = "Employee") AND (c["EmployeeID"] = 5))
OFFSET 0 LIMIT @__p_0
@@ -3379,25 +3397,25 @@ public override Task Interface_casting_though_generic_method(bool async)
"""
@__id_0='10252'
-SELECT VALUE {"Id" : c["OrderID"]}
+SELECT c["OrderID"] AS Id
FROM root c
WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] = @__id_0))
""",
//
"""
-SELECT VALUE {"Id" : c["OrderID"]}
+SELECT c["OrderID"] AS Id
FROM root c
WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] = 10252))
""",
//
"""
-SELECT VALUE {"Id" : c["OrderID"]}
+SELECT c["OrderID"] AS Id
FROM root c
WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] = 10252))
""",
//
"""
-SELECT VALUE {"Id" : c["OrderID"]}
+SELECT c["OrderID"] AS Id
FROM root c
WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] = 10252))
""");
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs
index 2b1155b1400..f3d9e807198 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs
@@ -425,7 +425,7 @@ public override Task Projecting_indexer_property_ignores_include(bool async)
AssertSql(
"""
-SELECT VALUE {"Nation" : c["PersonAddress"]["ZipCode"]}
+SELECT c["PersonAddress"]["ZipCode"] AS Nation
FROM root c
WHERE c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA")
""");
@@ -439,7 +439,7 @@ public override Task Projecting_indexer_property_ignores_include_converted(bool
AssertSql(
"""
-SELECT VALUE {"Nation" : c["PersonAddress"]["ZipCode"]}
+SELECT c["PersonAddress"]["ZipCode"] AS Nation
FROM root c
WHERE c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA")
""");
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs
new file mode 100644
index 00000000000..152837b212d
--- /dev/null
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs
@@ -0,0 +1,1533 @@
+// 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;
+using Xunit.Sdk;
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+public class PrimitiveCollectionsQueryCosmosTest : PrimitiveCollectionsQueryTestBase<
+ PrimitiveCollectionsQueryCosmosTest.PrimitiveCollectionsQueryCosmosFixture>
+{
+ public PrimitiveCollectionsQueryCosmosTest(PrimitiveCollectionsQueryCosmosFixture fixture, ITestOutputHelper testOutputHelper)
+ : base(fixture)
+ {
+ Fixture.TestSqlLoggerFactory.Clear();
+ Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
+ }
+
+ public override Task Inline_collection_of_ints_Contains(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_of_ints_Contains(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND c["Int"] IN (10, 999))
+""");
+ });
+
+ public override Task Inline_collection_of_nullable_ints_Contains(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_of_nullable_ints_Contains(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND c["NullableInt"] IN (10, 999))
+""");
+ });
+
+ public override Task Inline_collection_of_nullable_ints_Contains_null(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_of_nullable_ints_Contains_null(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (c["NullableInt"] IN (999) OR (c["NullableInt"] = null)))
+""");
+ });
+
+ public override Task Inline_collection_Count_with_zero_values(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_Count_with_zero_values(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((
+ SELECT VALUE COUNT(1)
+ FROM i IN (SELECT VALUE [])
+ WHERE (i > c["Id"])) = 1))
+""");
+ });
+
+ public override Task Inline_collection_Count_with_one_value(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_Count_with_one_value(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((
+ SELECT VALUE COUNT(1)
+ FROM i IN (SELECT VALUE [2])
+ WHERE (i > c["Id"])) = 1))
+""");
+ });
+
+ public override Task Inline_collection_Count_with_two_values(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_Count_with_two_values(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((
+ SELECT VALUE COUNT(1)
+ FROM i IN (SELECT VALUE [2, 999])
+ WHERE (i > c["Id"])) = 1))
+""");
+ });
+
+ public override Task Inline_collection_Count_with_three_values(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_Count_with_three_values(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((
+ SELECT VALUE COUNT(1)
+ FROM i IN (SELECT VALUE [2, 999, 1000])
+ WHERE (i > c["Id"])) = 2))
+""");
+ });
+
+ public override Task Inline_collection_Contains_with_zero_values(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_Contains_with_zero_values(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (true = false))
+""");
+ });
+
+ public override Task Inline_collection_Contains_with_one_value(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_Contains_with_one_value(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND c["Id"] IN (2))
+""");
+ });
+
+ public override Task Inline_collection_Contains_with_two_values(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_Contains_with_two_values(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND c["Id"] IN (2, 999))
+""");
+ });
+
+ public override Task Inline_collection_Contains_with_three_values(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_Contains_with_three_values(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND c["Id"] IN (2, 999, 1000))
+""");
+ });
+
+ public override Task Inline_collection_Contains_with_EF_Constant(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_Contains_with_EF_Constant(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND c["Id"] IN (2, 999, 1000))
+""");
+ });
+
+ public override Task Inline_collection_Contains_with_all_parameters(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_Contains_with_all_parameters(a);
+
+ AssertSql(
+ """
+@__i_0='2'
+@__j_1='999'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND c["Id"] IN (@__i_0, @__j_1))
+""");
+ });
+
+ public override Task Inline_collection_Contains_with_constant_and_parameter(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_Contains_with_constant_and_parameter(a);
+
+ AssertSql(
+ """
+@__j_0='999'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND c["Id"] IN (2, @__j_0))
+""");
+ });
+
+ // TODO: Remove incorrect null semantics compensation for Cosmos: #31063
+ public override Task Inline_collection_Contains_with_mixed_value_types(bool async)
+ => Assert.ThrowsAsync(() => base.Inline_collection_Contains_with_mixed_value_types(async));
+
+ // TODO: Remove incorrect null semantics compensation for Cosmos: #31063
+ public override Task Inline_collection_List_Contains_with_mixed_value_types(bool async)
+ => Assert.ThrowsAsync(() => base.Inline_collection_List_Contains_with_mixed_value_types(async));
+
+ public override Task Inline_collection_Contains_as_Any_with_predicate(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_Contains_as_Any_with_predicate(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND c["Id"] IN (2, 999))
+""");
+ });
+
+ public override Task Inline_collection_negated_Contains_as_All(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_negated_Contains_as_All(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND c["Id"] NOT IN (2, 999))
+""");
+ });
+
+ public override Task Inline_collection_Min_with_two_values(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_Min_with_two_values(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((
+ SELECT VALUE MIN(i)
+ FROM i IN (SELECT VALUE [30, c["Int"]])) = 30))
+""");
+ });
+
+ public override Task Inline_collection_List_Min_with_two_values(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_List_Min_with_two_values(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((
+ SELECT VALUE MIN(i)
+ FROM i IN (SELECT VALUE [30, c["Int"]])) = 30))
+""");
+ });
+
+ public override Task Inline_collection_Max_with_two_values(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_Max_with_two_values(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((
+ SELECT VALUE MAX(i)
+ FROM i IN (SELECT VALUE [30, c["Int"]])) = 30))
+""");
+ });
+
+ public override Task Inline_collection_List_Max_with_two_values(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_List_Max_with_two_values(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((
+ SELECT VALUE MAX(i)
+ FROM i IN (SELECT VALUE [30, c["Int"]])) = 30))
+""");
+ });
+
+ public override Task Inline_collection_Min_with_three_values(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_Min_with_three_values(a);
+
+ AssertSql(
+ """
+@__i_0='25'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((
+ SELECT VALUE MIN(i)
+ FROM i IN (SELECT VALUE [30, c["Int"], @__i_0])) = 25))
+""");
+ });
+
+ public override Task Inline_collection_List_Min_with_three_values(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_List_Min_with_three_values(a);
+
+ AssertSql(
+ """
+@__i_0='25'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((
+ SELECT VALUE MIN(i)
+ FROM i IN (SELECT VALUE [30, c["Int"], @__i_0])) = 25))
+""");
+ });
+
+ public override Task Inline_collection_Max_with_three_values(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_Max_with_three_values(a);
+
+ AssertSql(
+ """
+@__i_0='35'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((
+ SELECT VALUE MAX(i)
+ FROM i IN (SELECT VALUE [30, c["Int"], @__i_0])) = 35))
+""");
+ });
+
+ public override Task Inline_collection_List_Max_with_three_values(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Inline_collection_List_Max_with_three_values(a);
+
+ AssertSql(
+ """
+@__i_0='35'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((
+ SELECT VALUE MAX(i)
+ FROM i IN (SELECT VALUE [30, c["Int"], @__i_0])) = 35))
+""");
+ });
+
+ public override Task Parameter_collection_Count(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Parameter_collection_Count(a);
+
+ AssertSql(
+ """
+@__ids_0='[2,999]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((
+ SELECT VALUE COUNT(1)
+ FROM i IN (SELECT VALUE @__ids_0)
+ WHERE (i > c["Id"])) = 1))
+""");
+ });
+
+ public override Task Parameter_collection_of_ints_Contains_int(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Parameter_collection_of_ints_Contains_int(a);
+
+ AssertSql(
+ """
+@__ints_0='[10,999]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(@__ints_0, c["Int"]))
+""",
+ //
+ """
+@__ints_0='[10,999]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND NOT(ARRAY_CONTAINS(@__ints_0, c["Int"])))
+""");
+ });
+
+ public override Task Parameter_collection_of_ints_Contains_nullable_int(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Parameter_collection_of_ints_Contains_nullable_int(a);
+
+ AssertSql(
+ """
+@__ints_0='[10,999]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(@__ints_0, c["NullableInt"]))
+""",
+ //
+ """
+@__ints_0='[10,999]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND NOT(ARRAY_CONTAINS(@__ints_0, c["NullableInt"])))
+""");
+ });
+
+ public override Task Parameter_collection_of_nullable_ints_Contains_int(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Parameter_collection_of_nullable_ints_Contains_int(a);
+
+ AssertSql(
+ """
+@__nullableInts_0='[10,999]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(@__nullableInts_0, c["Int"]))
+""",
+ //
+ """
+@__nullableInts_0='[10,999]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND NOT(ARRAY_CONTAINS(@__nullableInts_0, c["Int"])))
+""");
+ });
+
+ public override Task Parameter_collection_of_nullable_ints_Contains_nullable_int(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Parameter_collection_of_nullable_ints_Contains_nullable_int(a);
+
+ AssertSql(
+ """
+@__nullableInts_0='[null,999]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(@__nullableInts_0, c["NullableInt"]))
+""",
+ //
+ """
+@__nullableInts_0='[null,999]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND NOT(ARRAY_CONTAINS(@__nullableInts_0, c["NullableInt"])))
+""");
+ });
+
+ public override Task Parameter_collection_of_strings_Contains_string(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Parameter_collection_of_strings_Contains_string(a);
+
+ AssertSql(
+ """
+@__strings_0='["10","999"]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(@__strings_0, c["String"]))
+""",
+ //
+ """
+@__strings_0='["10","999"]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND NOT(ARRAY_CONTAINS(@__strings_0, c["String"])))
+""");
+ });
+
+ public override Task Parameter_collection_of_strings_Contains_nullable_string(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Parameter_collection_of_strings_Contains_nullable_string(a);
+
+ AssertSql(
+ """
+@__strings_0='["10","999"]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(@__strings_0, c["NullableString"]))
+""",
+ //
+ """
+@__strings_0='["10","999"]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND NOT(ARRAY_CONTAINS(@__strings_0, c["NullableString"])))
+""");
+ });
+
+ public override Task Parameter_collection_of_nullable_strings_Contains_string(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Parameter_collection_of_nullable_strings_Contains_string(a);
+
+ AssertSql(
+ """
+@__strings_0='["10",null]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(@__strings_0, c["String"]))
+""",
+ //
+ """
+@__strings_0='["10",null]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND NOT(ARRAY_CONTAINS(@__strings_0, c["String"])))
+""");
+ });
+
+ public override Task Parameter_collection_of_nullable_strings_Contains_nullable_string(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Parameter_collection_of_nullable_strings_Contains_nullable_string(a);
+
+ AssertSql(
+ """
+@__strings_0='["999",null]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(@__strings_0, c["NullableString"]))
+""",
+ //
+ """
+@__strings_0='["999",null]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND NOT(ARRAY_CONTAINS(@__strings_0, c["NullableString"])))
+""");
+ });
+
+ public override Task Parameter_collection_of_DateTimes_Contains(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Parameter_collection_of_DateTimes_Contains(a);
+
+ AssertSql(
+ """
+@__dateTimes_0='["2020-01-10T12:30:00Z","9999-01-01T00:00:00Z"]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(@__dateTimes_0, c["DateTime"]))
+""");
+ });
+
+ public override Task Parameter_collection_of_bools_Contains(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Parameter_collection_of_bools_Contains(a);
+
+ AssertSql(
+ """
+@__bools_0='[true]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(@__bools_0, c["Bool"]))
+""");
+ });
+
+ public override Task Parameter_collection_of_enums_Contains(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Parameter_collection_of_enums_Contains(a);
+
+ AssertSql(
+ """
+@__enums_0='[0,3]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(@__enums_0, c["Enum"]))
+""");
+ });
+
+ public override Task Parameter_collection_null_Contains(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Parameter_collection_null_Contains(a);
+
+ AssertSql(
+ """
+@__ints_0=null
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(@__ints_0, c["Int"]))
+""");
+ });
+
+ public override Task Column_collection_of_ints_Contains(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_of_ints_Contains(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(c["Ints"], 10))
+""");
+ });
+
+ public override Task Column_collection_of_nullable_ints_Contains(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_of_nullable_ints_Contains(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(c["NullableInts"], 10))
+""");
+ });
+
+ public override Task Column_collection_of_nullable_ints_Contains_null(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_of_nullable_ints_Contains_null(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(c["NullableInts"], null))
+""");
+ });
+
+ public override Task Column_collection_of_strings_contains_null(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_of_strings_contains_null(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(c["Strings"], null))
+""");
+ });
+
+ public override Task Column_collection_of_nullable_strings_contains_null(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_of_nullable_strings_contains_null(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(c["NullableStrings"], null))
+""");
+ });
+
+ public override Task Column_collection_of_bools_Contains(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_of_bools_Contains(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(c["Bools"], true))
+""");
+ });
+
+ public override Task Column_collection_Count_method(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_Count_method(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY_LENGTH(c["Ints"]) = 2))
+""");
+ });
+
+ public override Task Column_collection_Length(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_Length(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY_LENGTH(c["Ints"]) = 2))
+""");
+ });
+
+ public override Task Column_collection_index_int(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_index_int(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (c["Ints"][1] = 10))
+""");
+ });
+
+ public override Task Column_collection_index_string(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_index_string(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (c["Strings"][1] = "10"))
+""");
+ });
+
+ public override Task Column_collection_index_datetime(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_index_datetime(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (c["DateTimes"][1] = "2020-01-10T12:30:00Z"))
+""");
+ });
+
+ public override Task Column_collection_index_beyond_end(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_index_beyond_end(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (c["Ints"][999] = 10))
+""");
+ });
+
+ public override async Task Nullable_reference_column_collection_index_equals_nullable_column(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ await Assert.ThrowsAsync(() => base.Nullable_reference_column_collection_index_equals_nullable_column(async));
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (c["NullableStrings"][2] = c["NullableString"]))
+""");
+ }
+ }
+
+ public override Task Non_nullable_reference_column_collection_index_equals_nullable_column(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Non_nullable_reference_column_collection_index_equals_nullable_column(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (EXISTS (
+ SELECT 1
+ FROM i IN c["Strings"]) AND (c["Strings"][1] = c["NullableString"])))
+""");
+ });
+
+ public override async Task Inline_collection_index_Column(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ // Member indexer (c.Array[c.SomeMember]) isn't supported by Cosmos, and neither is LIMIT/OFFSET within subqueries.
+ var exception = await Assert.ThrowsAsync(() => base.Inline_collection_index_Column(async));
+
+ Assert.Contains("The specified query includes 'member indexer' which is currently not supported.", exception.Message);
+ }
+ }
+
+ public override async Task Inline_collection_value_index_Column(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ // Member indexer (c.Array[c.SomeMember]) isn't supported by Cosmos, and neither is LIMIT/OFFSET within subqueries.
+ var exception = await Assert.ThrowsAsync(() => base.Inline_collection_value_index_Column(async));
+
+ Assert.Contains("The specified query includes 'member indexer' which is currently not supported.", exception.Message);
+ }
+ }
+
+ public override async Task Inline_collection_List_value_index_Column(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ // Member indexer (c.Array[c.SomeMember]) isn't supported by Cosmos, and neither is LIMIT/OFFSET within subqueries.
+ var exception = await Assert.ThrowsAsync(() => base.Inline_collection_List_value_index_Column(async));
+
+ Assert.Contains("The specified query includes 'member indexer' which is currently not supported.", exception.Message);
+ }
+ }
+
+ public override async Task Parameter_collection_index_Column_equal_Column(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ // Member indexer (c.Array[c.SomeMember]) isn't supported by Cosmos, and neither is LIMIT/OFFSET within subqueries.
+ var exception = await Assert.ThrowsAsync(() => base.Parameter_collection_index_Column_equal_Column(async));
+
+ Assert.Contains("The specified query includes 'member indexer' which is currently not supported.", exception.Message);
+ }
+ }
+
+ public override async Task Parameter_collection_index_Column_equal_constant(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ // Member indexer (c.Array[c.SomeMember]) isn't supported by Cosmos, and neither is LIMIT/OFFSET within subqueries.
+ var exception = await Assert.ThrowsAsync(() => base.Parameter_collection_index_Column_equal_constant(async));
+
+ Assert.Contains("The specified query includes 'member indexer' which is currently not supported.", exception.Message);
+ }
+ }
+
+ public override Task Column_collection_ElementAt(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_ElementAt(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (c["Ints"][1] = 10))
+""");
+ });
+
+ public override async Task Column_collection_Skip(bool async)
+ {
+ // TODO: Count after Distinct requires subquery pushdown
+ await AssertTranslationFailed(() => base.Column_collection_Skip(async));
+
+ AssertSql();
+ }
+
+ public override async Task Column_collection_Take(bool async)
+ {
+ // TODO: IN with subquery
+ await AssertTranslationFailed(() => base.Column_collection_Take(async));
+
+ AssertSql();
+ }
+
+ public override async Task Column_collection_Skip_Take(bool async)
+ {
+ // TODO: Count after Distinct requires subquery pushdown
+ await AssertTranslationFailed(() => base.Column_collection_Skip_Take(async));
+
+ AssertSql();
+ }
+
+ public override async Task Column_collection_OrderByDescending_ElementAt(bool async)
+ {
+ // TODO: ElementAt over composed query (non-simple array)
+ await AssertTranslationFailed(() => base.Column_collection_OrderByDescending_ElementAt(async));
+
+ AssertSql();
+ }
+
+ public override Task Column_collection_Any(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_Any(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND EXISTS (
+ SELECT 1
+ FROM i IN c["Ints"]))
+""");
+ });
+
+ public override async Task Column_collection_Distinct(bool async)
+ {
+ // TODO: Count after Distinct requires subquery pushdown
+ await AssertTranslationFailed(() => base.Column_collection_Distinct(async));
+
+ AssertSql();
+ }
+
+ public override async Task Column_collection_SelectMany(bool async)
+ {
+ // TODO: SelectMany
+ await AssertTranslationFailed(() => base.Column_collection_SelectMany(async));
+
+ AssertSql();
+ }
+
+ public override Task Column_collection_projection_from_top_level(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_projection_from_top_level(a);
+
+ AssertSql(
+ """
+SELECT c["Ints"]
+FROM root c
+WHERE (c["Discriminator"] = "PrimitiveCollectionsEntity")
+ORDER BY c["Id"]
+""");
+ });
+
+ public override async Task Column_collection_Join_parameter_collection(bool async)
+ {
+ // Cosmos join support. Issue #16920.
+ await AssertTranslationFailed(() => base.Column_collection_Join_parameter_collection(async));
+
+ AssertSql();
+ }
+
+ public override async Task Inline_collection_Join_ordered_column_collection(bool async)
+ {
+ // Cosmos join support. Issue #16920.
+ await AssertTranslationFailed(() => base.Column_collection_Join_parameter_collection(async));
+
+ AssertSql();
+ }
+
+ public override Task Parameter_collection_Concat_column_collection(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Parameter_collection_Concat_column_collection(a);
+
+ AssertSql(
+ """
+@__ints_0='[11,111]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY_LENGTH(ARRAY_CONCAT(@__ints_0, c["Ints"])) = 2))
+""");
+ });
+
+ public override Task Column_collection_Union_parameter_collection(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_Union_parameter_collection(a);
+
+ AssertSql(
+ """
+@__ints_0='[11,111]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY_LENGTH(SetUnion(c["Ints"], @__ints_0)) = 2))
+""");
+ });
+
+ public override Task Column_collection_Intersect_inline_collection(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_Intersect_inline_collection(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY_LENGTH(SetIntersect(c["Ints"], [11, 111])) = 2))
+""");
+ });
+
+ public override async Task Inline_collection_Except_column_collection(bool async)
+ {
+ await AssertTranslationFailedWithDetails(
+ () => base.Inline_collection_Except_column_collection(async),
+ CosmosStrings.ExceptNotSupported);
+
+ AssertSql();
+ }
+
+ public override Task Column_collection_Where_Union(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_Where_Union(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY_LENGTH(SetUnion(ARRAY (
+ SELECT VALUE i
+ FROM i IN c["Ints"]
+ WHERE (i > 100)), [50])) = 2))
+""");
+ });
+
+ public override Task Column_collection_equality_parameter_collection(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_equality_parameter_collection(a);
+
+ AssertSql(
+ """
+@__ints_0='[1,10]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (c["Ints"] = @__ints_0))
+""");
+ });
+
+ public override Task Column_collection_Concat_parameter_collection_equality_inline_collection(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_Concat_parameter_collection_equality_inline_collection(a);
+
+ AssertSql(
+ """
+@__ints_0='[1,10]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY_CONCAT(c["Ints"], @__ints_0) = [1,11,111,1,10]))
+""");
+ });
+
+ public override Task Column_collection_equality_inline_collection(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_equality_inline_collection(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (c["Ints"] = [1,10]))
+""");
+ });
+
+ public override Task Column_collection_equality_inline_collection_with_parameters(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_equality_inline_collection_with_parameters(a);
+
+ AssertSql(
+ """
+@__i_0='1'
+@__j_1='10'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (c["Ints"] = [@__i_0, @__j_1]))
+""");
+ });
+
+ public override Task Column_collection_Where_equality_inline_collection(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_Where_equality_inline_collection(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY (
+ SELECT VALUE i
+ FROM i IN c["Ints"]
+ WHERE (i != 11)) = [1,111]))
+""");
+ });
+
+ public override async Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => base.Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(async));
+
+ // Note that even if the query didn't attempt to do offset without limit, Cosmos still doesn't support OFFSET/LIMIT in subqueries,
+ // so this test would fail anyway.
+ Assert.Equal(CosmosStrings.OffsetRequiresLimit, exception.Message);
+
+ AssertSql();
+ }
+ }
+
+ public override Task Parameter_collection_in_subquery_Union_column_collection(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Parameter_collection_in_subquery_Union_column_collection(a);
+
+ AssertSql(
+ """
+@__Skip_0='[111]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY_LENGTH(SetUnion(@__Skip_0, c["Ints"])) = 3))
+""");
+ });
+
+ public override async Task Parameter_collection_in_subquery_Union_column_collection_nested(bool async)
+ {
+ // TODO: Subquery pushdown
+ await AssertTranslationFailed(() => base.Parameter_collection_in_subquery_Union_column_collection_nested(async));
+
+ AssertSql();
+ }
+
+ public override void Parameter_collection_in_subquery_and_Convert_as_compiled_query()
+ {
+ // Array indexer over a parameter array ([1,2,3][0]) isn't supported by Cosmos.
+ // TODO: general OFFSET/LIMIT support
+ AssertTranslationFailed(() => base.Parameter_collection_in_subquery_and_Convert_as_compiled_query());
+
+ AssertSql();
+ }
+
+ public override async Task Parameter_collection_in_subquery_Count_as_compiled_query(bool async)
+ {
+ // TODO: Count after Skip requires subquery pushdown
+ await AssertTranslationFailed(() => base.Parameter_collection_in_subquery_Count_as_compiled_query(async));
+
+ AssertSql();
+ }
+
+ public override async Task Parameter_collection_in_subquery_Union_another_parameter_collection_as_compiled_query(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => base.Parameter_collection_in_subquery_Union_another_parameter_collection_as_compiled_query(async));
+
+ // Note that even if the query didn't attempt to do offset without limit, Cosmos still doesn't support OFFSET/LIMIT in
+ // subqueries, so this test would fail anyway.
+ Assert.Equal(CosmosStrings.OffsetRequiresLimit, exception.Message);
+
+ AssertSql();
+ }
+ }
+
+ public override async Task Column_collection_in_subquery_Union_parameter_collection(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => base.Column_collection_in_subquery_Union_parameter_collection(async));
+
+ // Note that even if the query didn't attempt to do offset without limit, Cosmos still doesn't support OFFSET/LIMIT in subqueries,
+ // so this test would fail anyway.
+ Assert.Equal(CosmosStrings.OffsetRequiresLimit, exception.Message);
+
+ AssertSql();
+ }
+ }
+
+ public override Task Project_collection_of_ints_simple(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Project_collection_of_ints_simple(a);
+
+ AssertSql(
+ """
+SELECT c["Ints"]
+FROM root c
+WHERE (c["Discriminator"] = "PrimitiveCollectionsEntity")
+ORDER BY c["Id"]
+""");
+ });
+
+ public override async Task Project_collection_of_ints_ordered(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ var exception = await Assert.ThrowsAsync(() => base.Project_collection_of_ints_ordered(async));
+
+ Assert.Contains("'ORDER BY' is not supported in subqueries.", exception.Message);
+ }
+ }
+
+ // TODO: Project out primitive collection subquery: #33797
+ public override async Task Project_collection_of_datetimes_filtered(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ await Assert.ThrowsAsync(() => base.Project_collection_of_datetimes_filtered(async));
+ }
+ }
+
+ public override async Task Project_collection_of_nullable_ints_with_paging(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ var exception =
+ await Assert.ThrowsAsync(() => base.Project_collection_of_nullable_ints_with_paging(async: true));
+
+ Assert.Contains("'OFFSET LIMIT' clause is not supported in subqueries.", exception.Message);
+ }
+ }
+
+ public override async Task Project_collection_of_nullable_ints_with_paging2(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => base.Project_collection_of_nullable_ints_with_paging2(async: true));
+
+ // Note that even if the query didn't attempt to do offset without limit, Cosmos still doesn't support OFFSET/LIMIT in subqueries,
+ // so this test would fail anyway.
+ Assert.Equal(CosmosStrings.OffsetRequiresLimit, exception.Message);
+
+ AssertSql();
+ }
+ }
+
+ public override async Task Project_collection_of_nullable_ints_with_paging3(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => base.Project_collection_of_nullable_ints_with_paging3(async));
+
+ // Note that even if the query didn't attempt to do offset without limit, Cosmos still doesn't support OFFSET/LIMIT in subqueries,
+ // so this test would fail anyway.
+ Assert.Equal(CosmosStrings.OffsetRequiresLimit, exception.Message);
+
+ AssertSql();
+ }
+ }
+
+ // TODO: Project out primitive collection subquery: #33797
+ public override async Task Project_collection_of_ints_with_distinct(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ await Assert.ThrowsAsync(() => base.Project_collection_of_ints_with_distinct(async));
+ }
+ }
+
+ public override Task Project_collection_of_nullable_ints_with_distinct(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Project_collection_of_nullable_ints_with_distinct(a);
+
+ AssertSql(
+ """
+SELECT VALUE {"c" : [c["String"], "foo"]}
+FROM root c
+WHERE (c["Discriminator"] = "PrimitiveCollectionsEntity")
+""");
+ });
+
+ // TODO: Project out primitive collection subquery: #33797
+ public override async Task Project_collection_of_ints_with_ToList_and_FirstOrDefault(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ await Assert.ThrowsAsync(() => base.Project_collection_of_ints_with_ToList_and_FirstOrDefault(async));
+ }
+ }
+
+ // TODO: Project out primitive collection subquery: #33797
+ public override async Task Project_empty_collection_of_nullables_and_collection_only_containing_nulls(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ await Assert.ThrowsAsync(
+ () => base.Project_empty_collection_of_nullables_and_collection_only_containing_nulls(async));
+ }
+ }
+
+ public override async Task Project_multiple_collections(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ // TODO: Project out primitive collection subquery: #33797
+ await Assert.ThrowsAsync(() => base.Project_multiple_collections(async));
+ }
+ }
+
+ public override Task Project_primitive_collections_element(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Project_primitive_collections_element(a);
+
+ AssertSql(
+ """
+SELECT VALUE
+{
+ "Indexer" : c["Ints"][0],
+ "EnumerableElementAt" : c["DateTimes"][0],
+ "QueryableElementAt" : c["Strings"][1]
+}
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (c["Id"] < 4))
+ORDER BY c["Id"]
+""");
+ });
+
+ public override Task Project_inline_collection(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Project_inline_collection(a);
+
+ // The following should be SELECT VALUE [c["String"], "foo"], #33779
+ AssertSql(
+ """
+SELECT [c["String"], "foo"] AS c
+FROM root c
+WHERE (c["Discriminator"] = "PrimitiveCollectionsEntity")
+""");
+ });
+
+ public override async Task Project_inline_collection_with_Union(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ // TODO: Project out primitive collection subquery: #33797
+ await Assert.ThrowsAsync(() => base.Project_inline_collection_with_Union(async));
+ }
+ }
+
+ public override async Task Project_inline_collection_with_Concat(bool async)
+ {
+ // Always throws for sync.
+ if (async)
+ {
+ // TODO: Project out primitive collection subquery: #33797
+ await Assert.ThrowsAsync(() => base.Project_inline_collection_with_Concat(async));
+ }
+ }
+
+ public override Task Nested_contains_with_Lists_and_no_inferred_type_mapping(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Nested_contains_with_Lists_and_no_inferred_type_mapping(a);
+
+ AssertSql(
+ """
+@__strings_1='["one","two","three"]'
+@__ints_0='[1,2,3]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(@__strings_1, (ARRAY_CONTAINS(@__ints_0, c["Int"]) ? "one" : "two")))
+""");
+ });
+
+ public override Task Nested_contains_with_arrays_and_no_inferred_type_mapping(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Nested_contains_with_arrays_and_no_inferred_type_mapping(a);
+
+ AssertSql(
+ """
+@__strings_1='["one","two","three"]'
+@__ints_0='[1,2,3]'
+
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(@__strings_1, (ARRAY_CONTAINS(@__ints_0, c["Int"]) ? "one" : "two")))
+""");
+ });
+
+ [ConditionalFact]
+ public virtual void Check_all_tests_overridden()
+ => TestHelpers.AssertAllMethodsOverridden(GetType());
+
+ public class PrimitiveCollectionsQueryCosmosFixture : PrimitiveCollectionsQueryFixtureBase
+ {
+ public TestSqlLoggerFactory TestSqlLoggerFactory
+ => (TestSqlLoggerFactory)ListLoggerFactory;
+
+ protected override ITestStoreFactory TestStoreFactory
+ => CosmosTestStoreFactory.Instance;
+
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder.ConfigureWarnings(
+ w => w.Ignore(CosmosEventId.NoPartitionKeyDefined)));
+ }
+
+ private void AssertSql(params string[] expected)
+ => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
+}
diff --git a/test/EFCore.Relational.Specification.Tests/Query/PrimitiveCollectionsQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/PrimitiveCollectionsQueryRelationalTestBase.cs
index e1cf534ae07..f4315a0b31d 100644
--- a/test/EFCore.Relational.Specification.Tests/Query/PrimitiveCollectionsQueryRelationalTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Query/PrimitiveCollectionsQueryRelationalTestBase.cs
@@ -51,4 +51,9 @@ public override async Task Project_inline_collection_with_Concat(bool async)
Assert.Equal(RelationalStrings.InsufficientInformationToIdentifyElementOfCollectionJoin, message);
}
+
+ // TODO: Requires converting the results of a subquery (relational rowset) to a primitive collection for comparison,
+ // not yet supported (#33792)
+ public override async Task Column_collection_Where_equality_inline_collection(bool async)
+ => await AssertTranslationFailed(() => base.Column_collection_Where_equality_inline_collection(async));
}
diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
index 1e56a23a06e..2017dd60587 100644
--- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
@@ -38,7 +38,8 @@ public virtual Task Inline_collection_Count_with_zero_values(bool async)
=> AssertQuery(
async,
// ReSharper disable once UseArrayEmptyMethod
- ss => ss.Set().Where(c => new int[0].Count(i => i > c.Id) == 1));
+ ss => ss.Set().Where(c => new int[0].Count(i => i > c.Id) == 1),
+ assertEmpty: true);
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
@@ -232,7 +233,7 @@ public virtual async Task Inline_collection_List_Max_with_three_values(bool asyn
await AssertQuery(
async,
- ss => ss.Set().Where(c => new List() { 30, c.Int, i }.Max() == 35));
+ ss => ss.Set().Where(c => new List { 30, c.Int, i }.Max() == 35));
}
[ConditionalTheory]
@@ -502,6 +503,7 @@ public virtual Task Column_collection_index_beyond_end(bool async)
ss => ss.Set().Where(c => false),
assertEmpty: true);
+ // TODO: This test is incorrect, see #33784
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Nullable_reference_column_collection_index_equals_nullable_column(bool async)
@@ -700,13 +702,20 @@ public virtual Task Column_collection_Intersect_inline_collection(bool async)
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Inline_collection_Except_column_collection(bool async)
- // Note that since the VALUES is on the left side of the set operation, it must assign column names, otherwise the column coming
- // out of the set operation has undetermined naming.
+ // Note that in relational, since the VALUES is on the left side of the set operation, it must assign column names, otherwise the
+ // column coming out of the set operation has undetermined naming.
=> AssertQuery(
async,
ss => ss.Set().Where(
c => new[] { 11, 111 }.Except(c.Ints).Count(i => i % 2 == 1) == 2));
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Where_Union(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Where(i => i > 100).Union(new[] { 50 }).Count() == 2));
+
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Column_collection_equality_parameter_collection(bool async)
@@ -751,6 +760,14 @@ await AssertQuery(
ss => ss.Set().Where(c => c.Ints.SequenceEqual(new[] { i, j })));
}
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Where_equality_inline_collection(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Where(i => i != 11) == new[] { 1, 111 }),
+ ss => ss.Set().Where(c => c.Ints.Where(i => i != 11).SequenceEqual(new[] { 1, 111 })));
+
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Parameter_collection_in_subquery_Count_as_compiled_query(bool async)
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
index eec352c4d7e..38367ffccf5 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
@@ -730,6 +730,9 @@ public override Task Column_collection_Intersect_inline_collection(bool async)
public override Task Inline_collection_Except_column_collection(bool async)
=> AssertCompatibilityLevelTooLow(() => base.Inline_collection_Except_column_collection(async));
+ public override Task Column_collection_Where_Union(bool async)
+ => AssertCompatibilityLevelTooLow(() => base.Inline_collection_Except_column_collection(async));
+
public override async Task Column_collection_equality_parameter_collection(bool async)
{
await base.Column_collection_equality_parameter_collection(async);
@@ -770,6 +773,13 @@ public override async Task Column_collection_equality_inline_collection_with_par
AssertSql();
}
+ public override async Task Column_collection_Where_equality_inline_collection(bool async)
+ {
+ await base.Column_collection_Where_equality_inline_collection(async);
+
+ AssertSql();
+ }
+
public override Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(bool async)
=> AssertCompatibilityLevelTooLow(() => base.Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(async));
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
index 82446229a06..0f02b9803ec 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
@@ -837,6 +837,7 @@ WHERE CAST(JSON_VALUE([p].[Ints], '$[999]') AS int) = 10
public override async Task Nullable_reference_column_collection_index_equals_nullable_column(bool async)
{
+ // TODO: This test is incorrect, see #33784
await base.Nullable_reference_column_collection_index_equals_nullable_column(async);
AssertSql(
@@ -1191,6 +1192,27 @@ FROM OPENJSON([p].[Ints]) WITH ([value] int '$') AS [i]
""");
}
+ public override async Task Column_collection_Where_Union(bool async)
+ {
+ await base.Column_collection_Where_Union(async);
+
+ AssertSql(
+ """
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM (
+ SELECT [i].[value]
+ FROM OPENJSON([p].[Ints]) WITH ([value] int '$') AS [i]
+ WHERE [i].[value] > 100
+ UNION
+ SELECT [v].[Value] AS [value]
+ FROM (VALUES (CAST(50 AS int))) AS [v]([Value])
+ ) AS [u]) = 2
+""");
+ }
+
public override async Task Column_collection_equality_parameter_collection(bool async)
{
await base.Column_collection_equality_parameter_collection(async);
@@ -1231,6 +1253,13 @@ public override async Task Column_collection_equality_inline_collection_with_par
AssertSql();
}
+ public override async Task Column_collection_Where_equality_inline_collection(bool async)
+ {
+ await base.Column_collection_Where_equality_inline_collection(async);
+
+ AssertSql();
+ }
+
public override async Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(bool async)
{
await base.Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(async);
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
index b341480199a..a16cf45767c 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
@@ -1167,6 +1167,26 @@ FROM json_each("p"."Ints") AS "i"
""");
}
+ public override async Task Column_collection_Where_Union(bool async)
+ {
+ await base.Column_collection_Where_Union(async);
+
+ AssertSql(
+ """
+SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings"
+FROM "PrimitiveCollectionsEntity" AS "p"
+WHERE (
+ SELECT COUNT(*)
+ FROM (
+ SELECT "i"."value"
+ FROM json_each("p"."Ints") AS "i"
+ WHERE "i"."value" > 100
+ UNION
+ SELECT CAST(50 AS INTEGER) AS "Value"
+ ) AS "u") = 2
+""");
+ }
+
public override async Task Column_collection_equality_parameter_collection(bool async)
{
await base.Column_collection_equality_parameter_collection(async);
@@ -1207,6 +1227,13 @@ public override async Task Column_collection_equality_inline_collection_with_par
AssertSql();
}
+ public override async Task Column_collection_Where_equality_inline_collection(bool async)
+ {
+ await base.Column_collection_Where_equality_inline_collection(async);
+
+ AssertSql();
+ }
+
public override async Task Parameter_collection_in_subquery_Count_as_compiled_query(bool async)
{
await base.Parameter_collection_in_subquery_Count_as_compiled_query(async);