diff --git a/src/EFCore.Cosmos/Extensions/CosmosDbFunctionsExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosDbFunctionsExtensions.cs
new file mode 100644
index 00000000000..94a5b2d50af
--- /dev/null
+++ b/src/EFCore.Cosmos/Extensions/CosmosDbFunctionsExtensions.cs
@@ -0,0 +1,50 @@
+// 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.Extensions;
+
+///
+/// Provides CLR methods that get translated to database functions when used in LINQ to Entities queries.
+/// The methods on this class are accessed via .
+///
+///
+/// See Database functions, and
+/// Accessing Cosmos with EF Core for more information and examples.
+///
+public static class CosmosDbFunctionsExtensions
+{
+ ///
+ /// Returns a boolean indicating if the property has been assigned a value. Corresponds to the Cosmos IS_DEFINED function.
+ ///
+ ///
+ /// See Database functions, and
+ /// Accessing Cosmos with EF Core
+ /// for more information and examples.
+ ///
+ /// The instance.
+ /// The expression to check.
+ /// Cosmos IS_DEFINED_ function
+ public static bool IsDefined(this DbFunctions _, object? expression)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsDefined)));
+
+ ///
+ /// Coalesces a Cosmos undefined value via the ?? operator.
+ ///
+ ///
+ /// See Database functions, and
+ /// Accessing Cosmos with EF Core
+ /// for more information and examples.
+ ///
+ /// The instance.
+ ///
+ /// The expression to coalesce. This expression will be returned unless it is undefined, in which case
+ /// will be returned.
+ ///
+ /// The expression to be returned if is undefined.
+ /// Cosmos coalesce operator
+ public static T CoalesceUndefined(
+ this DbFunctions _,
+ T expression1,
+ T expression2)
+ => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(CoalesceUndefined)));
+}
diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs
index d520b5fc6e3..527c56ca5d4 100644
--- a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs
+++ b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs
@@ -125,6 +125,12 @@ public static string JsonPropertyCollision(object? property1, object? property2,
GetString("JsonPropertyCollision", nameof(property1), nameof(property2), nameof(entityType), nameof(storeName)),
property1, property2, entityType, storeName);
+ ///
+ /// Skip, Take, First/FirstOrDefault and Single/SingleOrDefault aren't supported in subqueries since Cosmos doesn't support LIMIT/OFFSET in subqueries.
+ ///
+ public static string LimitOffsetNotSupportedInSubqueries
+ => GetString("LimitOffsetNotSupportedInSubqueries");
+
///
/// 'Reverse' could not be translated to the server because there is no ordering on the server side.
///
diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.resx b/src/EFCore.Cosmos/Properties/CosmosStrings.resx
index 9aa9ddd4412..d15bd153984 100644
--- a/src/EFCore.Cosmos/Properties/CosmosStrings.resx
+++ b/src/EFCore.Cosmos/Properties/CosmosStrings.resx
@@ -159,6 +159,9 @@
Both properties '{property1}' and '{property2}' on entity type '{entityType}' are mapped to '{storeName}'. Map one of the properties to a different JSON property.
+
+ Skip, Take, First/FirstOrDefault and Single/SingleOrDefault aren't supported in subqueries since Cosmos doesn't support LIMIT/OFFSET in subqueries.
+
Executed CreateItem ({elapsed} ms, {charge} RU) ActivityId='{activityId}', Container='{container}', Id='{id}', Partition='{partitionKey}'
Information CosmosEventId.ExecutedCreateItem string string string string string string?
@@ -243,6 +246,9 @@
Cosmos SQL does not allow Offset without Limit. Consider specifying a 'Take' operation on the query.
+
+ SingleOrDefault and FirstOrDefault cannot be used Cosmos SQL does not allow Offset without Limit. Consider specifying a 'Take' operation on the query.
+
Exactly one of '{param1}' or '{param2}' must be set.
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs b/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs
index 4d3b06c79a9..65ea3e9d188 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs
@@ -32,7 +32,8 @@ public CosmosMethodCallTranslatorProvider(
new CosmosStringMethodTranslator(sqlExpressionFactory),
new CosmosRandomTranslator(sqlExpressionFactory),
new CosmosMathTranslator(sqlExpressionFactory),
- new CosmosRegexTranslator(sqlExpressionFactory)
+ new CosmosRegexTranslator(sqlExpressionFactory),
+ new CosmosTypeCheckingTranslator(sqlExpressionFactory)
//new LikeTranslator(sqlExpressionFactory),
//new EnumHasFlagTranslator(sqlExpressionFactory),
//new GetValueOrDefaultTranslator(sqlExpressionFactory),
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs
index 6054109b01f..ec4b83b0552 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs
@@ -21,40 +21,6 @@ public class CosmosQuerySqlGenerator(ITypeMappingSource typeMappingSource) : Sql
private List _sqlParameters = null!;
private ParameterNameGenerator _parameterNameGenerator = null!;
- private readonly IDictionary _operatorMap = new Dictionary
- {
- // Arithmetic
- { ExpressionType.Add, " + " },
- { ExpressionType.Subtract, " - " },
- { ExpressionType.Multiply, " * " },
- { ExpressionType.Divide, " / " },
- { ExpressionType.Modulo, " % " },
-
- // Bitwise >>> (zero-fill right shift) not available in C#
- { ExpressionType.Or, " | " },
- { ExpressionType.And, " & " },
- { ExpressionType.ExclusiveOr, " ^ " },
- { ExpressionType.LeftShift, " << " },
- { ExpressionType.RightShift, " >> " },
-
- // Logical
- { ExpressionType.AndAlso, " AND " },
- { ExpressionType.OrElse, " OR " },
-
- // Comparison
- { ExpressionType.Equal, " = " },
- { ExpressionType.NotEqual, " != " },
- { ExpressionType.GreaterThan, " > " },
- { ExpressionType.GreaterThanOrEqual, " >= " },
- { ExpressionType.LessThan, " < " },
- { ExpressionType.LessThanOrEqual, " <= " },
-
- // Unary
- { ExpressionType.UnaryPlus, "+" },
- { ExpressionType.Negate, "-" },
- { ExpressionType.Not, "~" }
- };
-
///
/// 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
@@ -116,7 +82,7 @@ protected override Expression VisitExists(ExistsExpression existsExpression)
///
protected override Expression VisitArray(ArrayExpression arrayExpression)
{
- _sqlBuilder.AppendLine("ARRAY (");
+ _sqlBuilder.AppendLine("ARRAY(");
using (_sqlBuilder.Indent())
{
@@ -457,7 +423,40 @@ protected override Expression VisitSqlBinary(SqlBinaryExpression sqlBinaryExpres
return sqlBinaryExpression;
}
- var op = _operatorMap[sqlBinaryExpression.OperatorType];
+ var op = sqlBinaryExpression.OperatorType switch
+ {
+ // Arithmetic
+ ExpressionType.Add => " + ",
+ ExpressionType.Subtract => " - ",
+ ExpressionType.Multiply => " * " ,
+ ExpressionType.Divide => " / " ,
+ ExpressionType.Modulo => " % ",
+
+ // Bitwise >>> (zero-fill right shift) not available in C#
+ ExpressionType.Or => " | ",
+ ExpressionType.And => " & ",
+ ExpressionType.ExclusiveOr => " ^ ",
+ ExpressionType.LeftShift => " << ",
+ ExpressionType.RightShift => " >> ",
+
+ // Logical
+ ExpressionType.AndAlso => " AND ",
+ ExpressionType.OrElse => " OR ",
+
+ // Comparison
+ ExpressionType.Equal => " = ",
+ ExpressionType.NotEqual => " != ",
+ ExpressionType.GreaterThan => " > ",
+ ExpressionType.GreaterThanOrEqual => " >= ",
+ ExpressionType.LessThan => " < ",
+ ExpressionType.LessThanOrEqual => " <= ",
+
+ // Other
+ ExpressionType.Coalesce => " ?? ",
+
+ _ => throw new UnreachableException($"Unsupported unary OperatorType: {sqlBinaryExpression.OperatorType}")
+ };
+
_sqlBuilder.Append('(');
Visit(sqlBinaryExpression.Left);
@@ -483,7 +482,14 @@ protected override Expression VisitSqlBinary(SqlBinaryExpression sqlBinaryExpres
///
protected override Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpression)
{
- var op = _operatorMap[sqlUnaryExpression.OperatorType];
+ var op = sqlUnaryExpression.OperatorType switch
+ {
+ ExpressionType.UnaryPlus => "+",
+ ExpressionType.Negate => "-",
+ ExpressionType.Not => "~",
+
+ _ => throw new UnreachableException($"Unsupported unary OperatorType: {sqlUnaryExpression.OperatorType}")
+ };
if (sqlUnaryExpression.OperatorType == ExpressionType.Not
&& sqlUnaryExpression.Operand.Type == typeof(bool))
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs
index 79c6d8d572c..ce672f0098d 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs
@@ -87,7 +87,7 @@ public static bool TryExtractBareArray(
public static bool TryExtractBareArray(
ShapedQueryExpression source,
[NotNullWhen(true)] out SqlExpression? array,
- [NotNullWhen(true)] out ScalarReferenceExpression? projectedScalarReference,
+ [NotNullWhen(true)] out SqlExpression? projectedScalarReference,
bool ignoreOrderings = false)
{
if (source.QueryExpression is not SelectExpression
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs
index f2df46b18d8..c42ae691446 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs
@@ -520,18 +520,57 @@ protected override ShapedQueryExpression TranslateDistinct(ShapedQueryExpression
Expression index,
bool returnDefault)
{
+ if (TranslateExpression(index) is not SqlExpression translatedIndex)
+ {
+ return null;
+ }
+
+ var select = (SelectExpression)source.QueryExpression;
+
+ // If the source query represents a bare array (e.g. x.Array), simplify x.Array.Skip(2) => ARRAY_SLICE(x.Array, 2) instead of
+ // subquery+OFFSET (which isn't supported by Cosmos).
+ // Even if the source is a full query (not a bare array), convert it to an array via the Cosmos ARRAY() operator; we do this
+ // only in subqueries, because Cosmos supports OFFSET/LIMIT at the top-level but not in subqueries.
+ var array = CosmosQueryUtils.TryExtractBareArray(source, out var a, out var projectedScalarReference)
+ ? a
+ : _subquery && CosmosQueryUtils.TryConvertToArray(source, _typeMappingSource, out a, out projectedScalarReference)
+ ? a
+ : 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)
+ if (array is SqlExpression scalarArray) // TODO: ElementAt over arrays of structural types
{
- var arrayIndex = _sqlExpressionFactory.ArrayIndex(
- array, translatedIndex, projectedScalarReference.Type, projectedScalarReference.TypeMapping);
- return source.UpdateQueryExpression(new SelectExpression(arrayIndex));
+ SqlExpression translation = _sqlExpressionFactory.ArrayIndex(
+ array, translatedIndex, projectedScalarReference!.Type, projectedScalarReference.TypeMapping);
+
+ if (returnDefault)
+ {
+ translation = _sqlExpressionFactory.CoalesceUndefined(
+ translation, TranslateExpression(translation.Type.GetDefaultValueConstant())!);
+ }
+
+ return source.UpdateQueryExpression(new SelectExpression(translation));
}
- // 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
+ // Translate using OFFSET/LIMIT, except in subqueries where it isn't supported
+ if (_subquery)
+ {
+ AddTranslationErrorDetails(CosmosStrings.LimitOffsetNotSupportedInSubqueries);
+ return null;
+ }
+
+ // 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 (select.Orderings.Count == 0 && !_subquery)
+ {
+ _queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning();
+ }
+
+ select.ApplyOffset(translatedIndex);
+ select.ApplyLimit(TranslateExpression(Expression.Constant(1))!);
+
return null;
}
@@ -569,6 +608,14 @@ protected override ShapedQueryExpression TranslateDistinct(ShapedQueryExpression
source = translatedSource;
}
+ // Cosmos does not support LIMIT in subqueries, so call into TranslateElementAtOrDefault which knows how to either extract an
+ // array from the source or wrap it in a Cosmos ARRAY() operator, to turn it into an array. At that point, a regular array index
+ // (x.Array[0]) can be used to get the first element.
+ if (_subquery)
+ {
+ return TranslateElementAtOrDefault(source, Expression.Constant(0), returnDefault);
+ }
+
var selectExpression = (SelectExpression)source.QueryExpression;
if (selectExpression is { Predicate: null, Orderings: [] })
{
@@ -939,6 +986,14 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
source = translatedSource;
}
+ // Cosmos does not support LIMIT in subqueries, so call into TranslateElementAtOrDefault which knows how to either extract an
+ // array from the source or wrap it in a Cosmos ARRAY() operator, to turn it into an array. At that point, a regular array index
+ // (x.Array[0]) can be used to get the first element.
+ if (_subquery)
+ {
+ return TranslateElementAtOrDefault(source, Expression.Constant(0), returnDefault);
+ }
+
var selectExpression = (SelectExpression)source.QueryExpression;
selectExpression.ApplyLimit(TranslateExpression(Expression.Constant(2))!);
@@ -955,26 +1010,55 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
///
protected override ShapedQueryExpression? TranslateSkip(ShapedQueryExpression source, Expression count)
{
- var selectExpression = (SelectExpression)source.QueryExpression;
- var translation = TranslateExpression(count);
+ if (TranslateExpression(count) is not SqlExpression translatedCount)
+ {
+ return null;
+ }
+
+ var select = (SelectExpression)source.QueryExpression;
- if (translation != null)
+ // If the source query represents a bare array (e.g. x.Array), simplify x.Array.Skip(2) => ARRAY_SLICE(x.Array, 2) instead of
+ // subquery+OFFSET (which isn't supported by Cosmos).
+ // Even if the source is a full query (not a bare array), convert it to an array via the Cosmos ARRAY() operator; we do this
+ // only in subqueries, because Cosmos supports OFFSET/LIMIT at the top-level but not in subqueries.
+ var array = CosmosQueryUtils.TryExtractBareArray(source, out var a, out var projectedScalarReference)
+ ? a
+ : _subquery && CosmosQueryUtils.TryConvertToArray(source, _typeMappingSource, out a, out projectedScalarReference)
+ ? a
+ : null;
+
+ if (array is SqlExpression scalarArray) // TODO: Take over arrays of structural types
{
- // 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();
- }
+ var slice = _sqlExpressionFactory.Function(
+ "ARRAY_SLICE", [scalarArray, translatedCount], scalarArray.Type, scalarArray.TypeMapping);
+
+ // TODO: Proper alias management (#33894). Ideally reach into the source of the original SelectExpression and use that alias.
+ select = SelectExpression.CreateForPrimitiveCollection(
+ new SourceExpression(slice, "i", withIn: true),
+ projectedScalarReference!.Type,
+ projectedScalarReference.TypeMapping!);
+ return source.UpdateQueryExpression(select);
+ }
- selectExpression.ApplyOffset(translation);
+ // Translate using OFFSET/LIMIT, except in subqueries where it isn't supported
+ if (_subquery)
+ {
+ AddTranslationErrorDetails(CosmosStrings.LimitOffsetNotSupportedInSubqueries);
+ return null;
+ }
- return source;
+ // 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 (select.Orderings.Count == 0 && !_subquery)
+ {
+ _queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning();
}
- return null;
+ select.ApplyOffset(translatedCount);
+
+ return source;
}
///
@@ -1023,26 +1107,59 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
///
protected override ShapedQueryExpression? TranslateTake(ShapedQueryExpression source, Expression count)
{
- var selectExpression = (SelectExpression)source.QueryExpression;
- var translation = TranslateExpression(count);
+ if (TranslateExpression(count) is not SqlExpression translatedCount)
+ {
+ return null;
+ }
+
+ var select = (SelectExpression)source.QueryExpression;
- if (translation != null)
+ // If the source query represents a bare array (e.g. x.Array), simplify x.Array.Take(2) => ARRAY_SLICE(x.Array, 0, 2) instead of
+ // subquery+LIMIT (which isn't supported by Cosmos).
+ // Even if the source is a full query (not a bare array), convert it to an array via the Cosmos ARRAY() operator; we do this
+ // only in subqueries, because Cosmos supports OFFSET/LIMIT at the top-level but not in subqueries.
+ var array = CosmosQueryUtils.TryExtractBareArray(source, out var a, out var projectedScalarReference)
+ ? a
+ : _subquery && CosmosQueryUtils.TryConvertToArray(source, _typeMappingSource, out a, out projectedScalarReference)
+ ? a
+ : null;
+
+ if (array is SqlExpression scalarArray) // TODO: Take over arrays of structural types
{
- // 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();
- }
+ // Take() is composed over Skip(), combine the two together to a single ARRAY_SLICE()
+ var slice = array is SqlFunctionExpression { Name: "ARRAY_SLICE", Arguments: [var nestedArray, var skipCount] } previousSlice
+ ? previousSlice.Update([nestedArray, skipCount, translatedCount])
+ : _sqlExpressionFactory.Function(
+ "ARRAY_SLICE", [scalarArray, TranslateExpression(Expression.Constant(0))!, translatedCount], scalarArray.Type,
+ scalarArray.TypeMapping);
+
+ // TODO: Proper alias management (#33894). Ideally reach into the source of the original SelectExpression and use that alias.
+ select = SelectExpression.CreateForPrimitiveCollection(
+ new SourceExpression(slice, "i", withIn: true),
+ projectedScalarReference!.Type,
+ projectedScalarReference.TypeMapping!);
+ return source.UpdateQueryExpression(select);
+ }
- selectExpression.ApplyLimit(translation);
+ // Translate using OFFSET/LIMIT, except in subqueries where it isn't supported
+ if (_subquery)
+ {
+ AddTranslationErrorDetails(CosmosStrings.LimitOffsetNotSupportedInSubqueries);
+ return null;
+ }
- return source;
+ // 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 (select.Orderings.Count == 0 && !_subquery)
+ {
+ _queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning();
}
- return null;
+ select.ApplyLimit(translatedCount);
+
+ return source;
}
///
@@ -1349,17 +1466,17 @@ [new ProjectionExpression(sqlParameterExpression, null!)],
}
private ShapedQueryExpression WrapPrimitiveCollectionAsShapedQuery(
- Expression containerExpression,
+ Expression array,
Type elementClrType,
CoreTypeMapping elementTypeMapping)
{
// TODO: Do proper alias management: #33894
- var selectExpression = SelectExpression.CreateForPrimitiveCollection(
- new SourceExpression(containerExpression, "i", withIn: true),
+ var select = SelectExpression.CreateForPrimitiveCollection(
+ new SourceExpression(array, "i", withIn: true),
elementClrType,
elementTypeMapping);
var shaperExpression = (Expression)new ProjectionBindingExpression(
- selectExpression, new ProjectionMember(), elementClrType.MakeNullable());
+ select, new ProjectionMember(), elementClrType.MakeNullable());
if (shaperExpression.Type != elementClrType)
{
Check.DebugAssert(
@@ -1369,7 +1486,7 @@ private ShapedQueryExpression WrapPrimitiveCollectionAsShapedQuery(
shaperExpression = Expression.Convert(shaperExpression, elementClrType);
}
- return new ShapedQueryExpression(selectExpression, shaperExpression);
+ return new ShapedQueryExpression(select, shaperExpression);
}
#endregion Queryable collection support
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlBinaryExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlBinaryExpression.cs
index 15c110fec28..a0e3dc07408 100644
--- a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlBinaryExpression.cs
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlBinaryExpression.cs
@@ -14,32 +14,6 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
///
public class SqlBinaryExpression : SqlExpression
{
- private static readonly ISet AllowedOperators = new HashSet
- {
- ExpressionType.Add,
- ExpressionType.Subtract,
- ExpressionType.Multiply,
- ExpressionType.Divide,
- ExpressionType.Modulo,
- ExpressionType.And,
- ExpressionType.AndAlso,
- ExpressionType.Or,
- ExpressionType.OrElse,
- ExpressionType.LessThan,
- ExpressionType.LessThanOrEqual,
- ExpressionType.GreaterThan,
- ExpressionType.GreaterThanOrEqual,
- ExpressionType.Equal,
- ExpressionType.NotEqual,
- ExpressionType.ExclusiveOr,
- ExpressionType.RightShift,
- ExpressionType.LeftShift,
- ExpressionType.ArrayIndex
- };
-
- internal static bool IsValidOperator(ExpressionType operatorType)
- => AllowedOperators.Contains(operatorType);
-
///
/// 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
@@ -115,6 +89,36 @@ public virtual SqlBinaryExpression Update(SqlExpression left, SqlExpression righ
? new SqlBinaryExpression(OperatorType, left, right, Type, TypeMapping)
: this;
+ internal static bool IsValidOperator(ExpressionType operatorType)
+ {
+ switch (operatorType)
+ {
+ case ExpressionType.Add:
+ case ExpressionType.Subtract:
+ case ExpressionType.Multiply:
+ case ExpressionType.Divide:
+ case ExpressionType.Modulo:
+ case ExpressionType.And:
+ case ExpressionType.AndAlso:
+ case ExpressionType.Or:
+ case ExpressionType.OrElse:
+ case ExpressionType.LessThan:
+ case ExpressionType.LessThanOrEqual:
+ case ExpressionType.GreaterThan:
+ case ExpressionType.GreaterThanOrEqual:
+ case ExpressionType.Equal:
+ case ExpressionType.NotEqual:
+ case ExpressionType.ExclusiveOr:
+ case ExpressionType.RightShift:
+ case ExpressionType.LeftShift:
+ case ExpressionType.ArrayIndex:
+ case ExpressionType.Coalesce:
+ return true;
+ default:
+ 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
@@ -123,6 +127,15 @@ public virtual SqlBinaryExpression Update(SqlExpression left, SqlExpression righ
///
protected override void Print(ExpressionPrinter expressionPrinter)
{
+ if (OperatorType is ExpressionType.ArrayIndex)
+ {
+ expressionPrinter.Visit(Left);
+ expressionPrinter.Append("[");
+ expressionPrinter.Visit(Right);
+ expressionPrinter.Append("]");
+ return;
+ }
+
var requiresBrackets = RequiresBrackets(Left);
if (requiresBrackets)
diff --git a/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs
index 3ef9969b804..ea23e416025 100644
--- a/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs
+++ b/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs
@@ -199,6 +199,14 @@ SqlBinaryExpression Or(
SqlExpression right,
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
+ /// any release. You should only use it directly in 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 CoalesceUndefined(SqlExpression left, SqlExpression right, 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/SqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs
index 6ebb83cd545..4a0f78f0d60 100644
--- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs
+++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs
@@ -131,8 +131,8 @@ private SqlExpression ApplyTypeMappingOnSqlBinary(
: typeMappingSource.FindMapping(right.Type, model));
resultType = typeof(bool);
resultTypeMapping = _boolTypeMapping;
- }
break;
+ }
case ExpressionType.AndAlso:
case ExpressionType.OrElse:
@@ -140,8 +140,8 @@ private SqlExpression ApplyTypeMappingOnSqlBinary(
inferredTypeMapping = _boolTypeMapping;
resultType = typeof(bool);
resultTypeMapping = _boolTypeMapping;
- }
break;
+ }
case ExpressionType.Add:
case ExpressionType.Subtract:
@@ -152,14 +152,16 @@ private SqlExpression ApplyTypeMappingOnSqlBinary(
case ExpressionType.RightShift:
case ExpressionType.And:
case ExpressionType.Or:
+ case ExpressionType.Coalesce:
{
inferredTypeMapping = typeMapping ?? ExpressionExtensions.InferTypeMapping(left, right);
resultType = inferredTypeMapping?.ClrType ?? left.Type;
resultTypeMapping = inferredTypeMapping;
- }
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
@@ -170,6 +172,7 @@ private SqlExpression ApplyTypeMappingOnSqlBinary(
ApplyDefaultTypeMapping(right),
sqlBinaryExpression.Type,
typeMapping ?? sqlBinaryExpression.TypeMapping);
+ }
default:
throw new InvalidOperationException(
@@ -494,6 +497,15 @@ public virtual SqlBinaryExpression Or(SqlExpression left, SqlExpression right, C
? (SqlUnaryExpression)ApplyTypeMapping(new SqlUnaryExpression(operatorType, operand, type, null), 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
+ /// any release. You should only use it directly in 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 SqlExpression CoalesceUndefined(SqlExpression left, SqlExpression right, CoreTypeMapping? typeMapping = null)
+ => MakeBinary(ExpressionType.Coalesce, left, right, 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/Translators/CosmosTypeCheckingTranslator.cs b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosTypeCheckingTranslator.cs
new file mode 100644
index 00000000000..84fabb881fd
--- /dev/null
+++ b/src/EFCore.Cosmos/Query/Internal/Translators/CosmosTypeCheckingTranslator.cs
@@ -0,0 +1,45 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.Cosmos.Extensions;
+
+// ReSharper disable once CheckNamespace
+namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
+
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+public class CosmosTypeCheckingTranslator(ISqlExpressionFactory sqlExpressionFactory) : IMethodCallTranslator
+{
+ ///
+ /// 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? Translate(
+ SqlExpression? instance,
+ MethodInfo method,
+ IReadOnlyList arguments,
+ IDiagnosticsLogger logger)
+ {
+ if (method.DeclaringType != typeof(CosmosDbFunctionsExtensions))
+ {
+ return null;
+ }
+
+ return method.Name switch
+ {
+ nameof(CosmosDbFunctionsExtensions.IsDefined)
+ => sqlExpressionFactory.Function("IS_DEFINED", [arguments[1]], typeof(bool)),
+
+ nameof(CosmosDbFunctionsExtensions.CoalesceUndefined)
+ => sqlExpressionFactory.CoalesceUndefined(arguments[1], arguments[2]),
+
+ _ => null
+ };
+ }
+}
diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
index 91a80321896..5ba1ee7fd78 100644
--- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs
+++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
@@ -17,25 +17,6 @@ namespace Microsoft.EntityFrameworkCore.Query;
///
public class QuerySqlGenerator : SqlExpressionVisitor
{
- private static readonly Dictionary OperatorMap = new()
- {
- { ExpressionType.Equal, " = " },
- { ExpressionType.NotEqual, " <> " },
- { ExpressionType.GreaterThan, " > " },
- { ExpressionType.GreaterThanOrEqual, " >= " },
- { ExpressionType.LessThan, " < " },
- { ExpressionType.LessThanOrEqual, " <= " },
- { ExpressionType.AndAlso, " AND " },
- { ExpressionType.OrElse, " OR " },
- { ExpressionType.Add, " + " },
- { ExpressionType.Subtract, " - " },
- { ExpressionType.Multiply, " * " },
- { ExpressionType.Divide, " / " },
- { ExpressionType.Modulo, " % " },
- { ExpressionType.And, " & " },
- { ExpressionType.Or, " | " }
- };
-
private readonly IRelationalCommandBuilderFactory _relationalCommandBuilderFactory;
private readonly ISqlGenerationHelper _sqlGenerationHelper;
private IRelationalCommandBuilder _relationalCommandBuilder;
@@ -1098,7 +1079,26 @@ protected override Expression VisitAtTimeZone(AtTimeZoneExpression atTimeZoneExp
/// A SQL binary operation.
/// A string representation of the binary operator.
protected virtual string GetOperator(SqlBinaryExpression binaryExpression)
- => OperatorMap[binaryExpression.OperatorType];
+ => binaryExpression.OperatorType switch
+ {
+ ExpressionType.Equal => " = ",
+ ExpressionType.NotEqual => " <> ",
+ ExpressionType.GreaterThan => " > ",
+ ExpressionType.GreaterThanOrEqual => " >= ",
+ ExpressionType.LessThan => " < ",
+ ExpressionType.LessThanOrEqual => " <= ",
+ ExpressionType.AndAlso => " AND ",
+ ExpressionType.OrElse => " OR ",
+ ExpressionType.Add => " + ",
+ ExpressionType.Subtract => " - ",
+ ExpressionType.Multiply => " * ",
+ ExpressionType.Divide => " / ",
+ ExpressionType.Modulo => " % ",
+ ExpressionType.And => " & ",
+ ExpressionType.Or => " | ",
+
+ _ => throw new UnreachableException($"Unsupported unary OperatorType: {binaryExpression.OperatorType}")
+ };
///
/// Generates SQL for the TOP clause of the given SELECT expression.
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SqlBinaryExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SqlBinaryExpression.cs
index d9fc939ff46..0b84052639d 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SqlBinaryExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SqlBinaryExpression.cs
@@ -14,35 +14,8 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
///
public class SqlBinaryExpression : SqlExpression
{
- private static readonly ISet AllowedOperators = new HashSet
- {
- ExpressionType.Add,
- ExpressionType.Subtract,
- ExpressionType.Multiply,
- ExpressionType.Divide,
- ExpressionType.Modulo,
- //ExpressionType.Power,
- ExpressionType.And,
- ExpressionType.AndAlso,
- ExpressionType.Or,
- ExpressionType.OrElse,
- ExpressionType.LessThan,
- ExpressionType.LessThanOrEqual,
- ExpressionType.GreaterThan,
- ExpressionType.GreaterThanOrEqual,
- ExpressionType.Equal,
- ExpressionType.NotEqual
- //ExpressionType.ExclusiveOr,
- //ExpressionType.ArrayIndex,
- //ExpressionType.RightShift,
- //ExpressionType.LeftShift,
- };
-
private static ConstructorInfo? _quotingConstructor;
- internal static bool IsValidOperator(ExpressionType operatorType)
- => AllowedOperators.Contains(operatorType);
-
///
/// Creates a new instance of the class.
///
@@ -107,6 +80,32 @@ public virtual SqlBinaryExpression Update(SqlExpression left, SqlExpression righ
? new SqlBinaryExpression(OperatorType, left, right, Type, TypeMapping)
: this;
+ internal static bool IsValidOperator(ExpressionType operatorType)
+ {
+ switch (operatorType)
+ {
+ case ExpressionType.Add:
+ case ExpressionType.Subtract:
+ case ExpressionType.Multiply:
+ case ExpressionType.Divide:
+ case ExpressionType.Modulo:
+ case ExpressionType.And:
+ case ExpressionType.AndAlso:
+ case ExpressionType.Or:
+ case ExpressionType.OrElse:
+ case ExpressionType.LessThan:
+ case ExpressionType.LessThanOrEqual:
+ case ExpressionType.GreaterThan:
+ case ExpressionType.GreaterThanOrEqual:
+ case ExpressionType.Equal:
+ case ExpressionType.NotEqual:
+ case ExpressionType.Coalesce:
+ return true;
+ default:
+ return false;
+ }
+ }
+
///
public override Expression Quote()
=> New(
diff --git a/src/Shared/SharedTypeExtensions.cs b/src/Shared/SharedTypeExtensions.cs
index 97e8f11f2d7..f1aa698fdeb 100644
--- a/src/Shared/SharedTypeExtensions.cs
+++ b/src/Shared/SharedTypeExtensions.cs
@@ -644,12 +644,5 @@ public static IEnumerable GetNamespaces(this Type type)
}
public static ConstantExpression GetDefaultValueConstant(this Type type)
- => (ConstantExpression)GenerateDefaultValueConstantMethod
- .MakeGenericMethod(type).Invoke(null, [])!;
-
- private static readonly MethodInfo GenerateDefaultValueConstantMethod =
- typeof(SharedTypeExtensions).GetTypeInfo().GetDeclaredMethod(nameof(GenerateDefaultValueConstant))!;
-
- private static ConstantExpression GenerateDefaultValueConstant()
- => Expression.Constant(default(TDefault), typeof(TDefault));
+ => Expression.Constant(type.IsValueType ? RuntimeHelpers.GetUninitializedObject(type) : null, type);
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs
index d3315af2619..9afc3b8d845 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs
@@ -42,6 +42,7 @@ FROM root c
public override async Task Contains_over_keyless_entity_throws(bool async)
{
+ // TODO: #33931
// 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.
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs
index 9aaa7615e82..a61773d30c9 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Azure.Cosmos;
+using Microsoft.EntityFrameworkCore.Cosmos.Extensions;
using Microsoft.EntityFrameworkCore.Cosmos.Internal;
using Xunit.Sdk;
@@ -1009,35 +1010,154 @@ FROM root c
""");
});
- public override async Task Column_collection_Skip(bool async)
- {
- // TODO: Count after Distinct requires subquery pushdown
- await AssertTranslationFailed(() => base.Column_collection_Skip(async));
+ public override Task Column_collection_First(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_First(a);
- AssertSql();
- }
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (c["Ints"][0] = 1))
+""");
+ });
- public override async Task Column_collection_Take(bool async)
- {
- // Always throws for sync.
- if (async)
- {
- var exception = await Assert.ThrowsAsync(() => base.Column_collection_Take(async));
+ public override Task Column_collection_FirstOrDefault(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_FirstOrDefault(a);
- Assert.Contains("'OFFSET LIMIT' clause is not supported in subqueries.", exception.Message);
- }
- }
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((c["Ints"][0] ?? 0) = 1))
+""");
+ });
- public override async Task Column_collection_Skip_Take(bool async)
- {
- // Always throws for sync.
- if (async)
- {
- var exception = await Assert.ThrowsAsync(() => base.Column_collection_Skip_Take(async));
+ public override Task Column_collection_Single(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_Single(a);
- Assert.Contains("'OFFSET LIMIT' clause is not supported in subqueries.", exception.Message);
- }
- }
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (c["Ints"][0] = 1))
+""");
+ });
+
+ public override Task Column_collection_SingleOrDefault(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_SingleOrDefault(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((c["Ints"][0] ?? 0) = 1))
+""");
+ });
+
+ public override Task Column_collection_Skip(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_Skip(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY_LENGTH(ARRAY_SLICE(c["Ints"], 1)) = 2))
+""");
+ });
+
+ public override Task Column_collection_Take(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_Take(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(ARRAY_SLICE(c["Ints"], 0, 2), 11))
+""");
+ });
+
+ public override Task Column_collection_Skip_Take(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_Skip_Take(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ARRAY_CONTAINS(ARRAY_SLICE(c["Ints"], 1, 2), 11))
+""");
+ });
+
+ public override Task Column_collection_Where_Skip(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_Where_Skip(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY_LENGTH(ARRAY_SLICE(ARRAY(
+ SELECT VALUE i
+ FROM i IN c["Ints"]
+ WHERE (i > 1)), 1)) = 3))
+""");
+ });
+
+ public override Task Column_collection_Where_Take(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_Where_Take(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY_LENGTH(ARRAY_SLICE(ARRAY(
+ SELECT VALUE i
+ FROM i IN c["Ints"]
+ WHERE (i > 1)), 0, 2)) = 2))
+""");
+ });
+
+ public override Task Column_collection_Where_Skip_Take(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_Where_Skip_Take(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY_LENGTH(ARRAY_SLICE(ARRAY(
+ SELECT VALUE i
+ FROM i IN c["Ints"]
+ WHERE (i > 1)), 1, 2)) = 1))
+""");
+ });
public override Task Column_collection_Contains_over_subquery(bool async)
=> CosmosTestHelpers.Instance.NoSyncTest(
@@ -1058,12 +1178,42 @@ FROM i IN c["Ints"]
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));
+ // Always throws for sync.
+ if (async)
+ {
+ var exception = await Assert.ThrowsAsync(() => base.Column_collection_OrderByDescending_ElementAt(async));
- AssertSql();
+ Assert.Contains("'ORDER BY' is not supported in subqueries.", exception.Message);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY(
+ SELECT VALUE i
+ FROM i IN c["Ints"]
+ ORDER BY i DESC)[0] = 111))
+""");
+ }
}
+ public override Task Column_collection_Where_ElementAt(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_Where_ElementAt(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY(
+ SELECT VALUE i
+ FROM i IN c["Ints"]
+ WHERE (i > 1))[0] = 11))
+""");
+ });
+
public override Task Column_collection_Any(bool async)
=> CosmosTestHelpers.Instance.NoSyncTest(
async, async a =>
@@ -1192,7 +1342,7 @@ public override Task Column_collection_Where_Union(bool async)
"""
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY_LENGTH(SetUnion(ARRAY (
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY_LENGTH(SetUnion(ARRAY(
SELECT VALUE i
FROM i IN c["Ints"]
WHERE (i > 100)), [50])) = 2))
@@ -1272,7 +1422,7 @@ public override Task Column_collection_Where_equality_inline_collection(bool asy
"""
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY (
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND (ARRAY(
SELECT VALUE i
FROM i IN c["Ints"]
WHERE (i != 11)) = [1,111]))
@@ -1281,18 +1431,14 @@ FROM i IN c["Ints"]
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));
+ // TODO: #33931
+ // The ToList inside the query 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.Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(a));
- // 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();
- }
+ AssertSql();
}
public override Task Parameter_collection_in_subquery_Union_column_collection(bool async)
@@ -1330,42 +1476,38 @@ public override void Parameter_collection_in_subquery_and_Convert_as_compiled_qu
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));
+ // TODO: #33931
+ // The ToList inside the query 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.Parameter_collection_in_subquery_Count_as_compiled_query(a));
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);
+ // TODO: #33931
+ // The ToList inside the query 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.Parameter_collection_in_subquery_Union_another_parameter_collection_as_compiled_query(a));
- AssertSql();
- }
+ 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));
+ // TODO: #33931
+ // The ToList inside the query 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.Column_collection_in_subquery_Union_parameter_collection(a));
- // 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();
- }
+ AssertSql();
}
public override Task Project_collection_of_ints_simple(bool async)
@@ -1406,46 +1548,38 @@ public override async Task Project_collection_of_datetimes_filtered(bool 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));
+ // TODO: #33931
+ // The ToList inside the query 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.Project_collection_of_nullable_ints_with_paging(a));
- Assert.Contains("'OFFSET LIMIT' clause is not supported in subqueries.", exception.Message);
- }
+ AssertSql();
}
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);
+ // TODO: #33931
+ // The ToList inside the query 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.Project_collection_of_nullable_ints_with_paging2(a));
- AssertSql();
- }
+ 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);
+ // TODO: #33931
+ // The ToList inside the query 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.Project_collection_of_nullable_ints_with_paging3(a));
- AssertSql();
- }
+ AssertSql();
}
// TODO: Project out primitive collection subquery: #33797
@@ -1592,6 +1726,48 @@ FROM root c
""");
});
+ #region Cosmos-specific tests
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task IsDefined(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await AssertQuery(
+ a,
+ ss => ss.Set().Where(e => EF.Functions.IsDefined(e.Ints[2])),
+ ss => ss.Set().Where(e => e.Ints.Length >= 3));
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND IS_DEFINED(c["Ints"][2]))
+""");
+ });
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task CoalesceUndefined(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await AssertQuery(
+ a,
+ ss => ss.Set().Where(e => EF.Functions.CoalesceUndefined(e.Ints[2], 999) == 999),
+ ss => ss.Set().Where(e => e.Ints.Length < 3));
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE ((c["Discriminator"] = "PrimitiveCollectionsEntity") AND ((c["Ints"][2] ?? 999) = 999))
+""");
+ });
+
+ #endregion Cosmos-specific tests
+
[ConditionalFact]
public virtual void Check_all_tests_overridden()
=> TestHelpers.AssertAllMethodsOverridden(GetType());
diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
index 148b4378865..3545cf76f35 100644
--- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
@@ -598,6 +598,37 @@ public virtual Task Column_collection_ElementAt(bool async)
ss => ss.Set().Where(c => c.Ints.ElementAt(1) == 10),
ss => ss.Set().Where(c => (c.Ints.Length >= 2 ? c.Ints.ElementAt(1) : -1) == 10));
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_First(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.First() == 1),
+ ss => ss.Set().Where(c => (c.Ints.Length >= 1 ? c.Ints.First() : -1) == 1));
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_FirstOrDefault(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.FirstOrDefault() == 1));
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Single(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Single() == 1),
+ ss => ss.Set().Where(c => (c.Ints.Length >= 1 ? c.Ints.First() : -1) == 1));
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_SingleOrDefault(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.SingleOrDefault() == 1),
+ ss => ss.Set().Where(c => (c.Ints.Length >= 1 ? c.Ints[0] : -1) == 1));
+
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Column_collection_Skip(bool async)
@@ -619,6 +650,27 @@ public virtual Task Column_collection_Skip_Take(bool async)
async,
ss => ss.Set().Where(c => c.Ints.Skip(1).Take(2).Contains(11)));
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Where_Skip(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Where(i => i > 1).Skip(1).Count() == 3));
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Where_Take(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Where(i => i > 1).Take(2).Count() == 2));
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Where_Skip_Take(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Where(i => i > 1).Skip(1).Take(2).Count() == 1));
+
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Column_collection_Contains_over_subquery(bool async)
@@ -636,6 +688,16 @@ public virtual Task Column_collection_OrderByDescending_ElementAt(bool async)
ss => ss.Set()
.Where(c => c.Ints.Length > 0 && c.Ints.OrderByDescending(i => i).ElementAt(0) == 111));
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Where_ElementAt(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set()
+ .Where(c => c.Ints.Where(i => i > 1).ElementAt(0) == 11),
+ ss => ss.Set()
+ .Where(c => c.Ints.Where(i => i > 1).FirstOrDefault(0) == 11));
+
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Column_collection_Any(bool async)
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
index 1c9bdcd9dab..6d584398488 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
@@ -685,6 +685,18 @@ public override Task Parameter_collection_index_Column_equal_constant(bool async
public override Task Column_collection_ElementAt(bool async)
=> AssertCompatibilityLevelTooLow(() => base.Column_collection_ElementAt(async));
+ public override Task Column_collection_First(bool async)
+ => AssertCompatibilityLevelTooLow(() => base.Column_collection_First(async));
+
+ public override Task Column_collection_FirstOrDefault(bool async)
+ => AssertCompatibilityLevelTooLow(() => base.Column_collection_FirstOrDefault(async));
+
+ public override Task Column_collection_Single(bool async)
+ => AssertCompatibilityLevelTooLow(() => base.Column_collection_Single(async));
+
+ public override Task Column_collection_SingleOrDefault(bool async)
+ => AssertCompatibilityLevelTooLow(() => base.Column_collection_SingleOrDefault(async));
+
public override Task Column_collection_Skip(bool async)
=> AssertCompatibilityLevelTooLow(() => base.Column_collection_Skip(async));
@@ -694,12 +706,24 @@ public override Task Column_collection_Take(bool async)
public override Task Column_collection_Skip_Take(bool async)
=> AssertCompatibilityLevelTooLow(() => base.Column_collection_Skip_Take(async));
+ public override Task Column_collection_Where_Skip(bool async)
+ => AssertCompatibilityLevelTooLow(() => base.Column_collection_Where_Skip(async));
+
+ public override Task Column_collection_Where_Take(bool async)
+ => AssertCompatibilityLevelTooLow(() => base.Column_collection_Where_Take(async));
+
+ public override Task Column_collection_Where_Skip_Take(bool async)
+ => AssertCompatibilityLevelTooLow(() => base.Column_collection_Where_Skip_Take(async));
+
public override Task Column_collection_Contains_over_subquery(bool async)
=> AssertCompatibilityLevelTooLow(() => base.Column_collection_Skip_Take(async));
public override Task Column_collection_OrderByDescending_ElementAt(bool async)
=> AssertTranslationFailed(() => base.Column_collection_OrderByDescending_ElementAt(async));
+ public override Task Column_collection_Where_ElementAt(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_Where_ElementAt(async));
+
public override Task Column_collection_Any(bool async)
=> AssertCompatibilityLevelTooLow(() => base.Column_collection_Any(async));
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
index fa5e05a521c..e45fcd6d8a9 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
@@ -982,6 +982,66 @@ WHERE CAST(JSON_VALUE([p].[Ints], '$[1]') AS int) = 10
""");
}
+ public override async Task Column_collection_First(bool async)
+ {
+ await base.Column_collection_First(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 TOP(1) CAST([i].[value] AS int) AS [value]
+ FROM OPENJSON([p].[Ints]) AS [i]
+ ORDER BY CAST([i].[key] AS int)) = 1
+""");
+ }
+
+ public override async Task Column_collection_FirstOrDefault(bool async)
+ {
+ await base.Column_collection_FirstOrDefault(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 COALESCE((
+ SELECT TOP(1) CAST([i].[value] AS int) AS [value]
+ FROM OPENJSON([p].[Ints]) AS [i]
+ ORDER BY CAST([i].[key] AS int)), 0) = 1
+""");
+ }
+
+ public override async Task Column_collection_Single(bool async)
+ {
+ await base.Column_collection_Single(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 TOP(1) CAST([i].[value] AS int) AS [value]
+ FROM OPENJSON([p].[Ints]) AS [i]
+ ORDER BY CAST([i].[key] AS int)) = 1
+""");
+ }
+
+ public override async Task Column_collection_SingleOrDefault(bool async)
+ {
+ await base.Column_collection_SingleOrDefault(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 COALESCE((
+ SELECT TOP(1) CAST([i].[value] AS int) AS [value]
+ FROM OPENJSON([p].[Ints]) AS [i]
+ ORDER BY CAST([i].[key] AS int)), 0) = 1
+""");
+ }
+
public override async Task Column_collection_Skip(bool async)
{
await base.Column_collection_Skip(async);
@@ -1034,6 +1094,65 @@ OFFSET 1 ROWS FETCH NEXT 2 ROWS ONLY
""");
}
+ public override async Task Column_collection_Where_Skip(bool async)
+ {
+ await base.Column_collection_Where_Skip(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 1 AS empty
+ FROM OPENJSON([p].[Ints]) AS [i]
+ WHERE CAST([i].[value] AS int) > 1
+ ORDER BY CAST([i].[key] AS int)
+ OFFSET 1 ROWS
+ ) AS [i0]) = 3
+""");
+ }
+
+ public override async Task Column_collection_Where_Take(bool async)
+ {
+ await base.Column_collection_Where_Take(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 TOP(2) 1 AS empty
+ FROM OPENJSON([p].[Ints]) AS [i]
+ WHERE CAST([i].[value] AS int) > 1
+ ORDER BY CAST([i].[key] AS int)
+ ) AS [i0]) = 2
+""");
+ }
+
+ public override async Task Column_collection_Where_Skip_Take(bool async)
+ {
+ await base.Column_collection_Where_Skip_Take(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 1 AS empty
+ FROM OPENJSON([p].[Ints]) AS [i]
+ WHERE CAST([i].[value] AS int) > 1
+ ORDER BY CAST([i].[key] AS int)
+ OFFSET 1 ROWS FETCH NEXT 2 ROWS ONLY
+ ) AS [i0]) = 1
+""");
+ }
+
public override async Task Column_collection_Contains_over_subquery(bool async)
{
await base.Column_collection_Contains_over_subquery(async);
@@ -1066,6 +1185,23 @@ ORDER BY [i].[value] DESC
""");
}
+ public override async Task Column_collection_Where_ElementAt(bool async)
+ {
+ await base.Column_collection_Where_ElementAt(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 CAST([i].[value] AS int) AS [value]
+ FROM OPENJSON([p].[Ints]) AS [i]
+ WHERE CAST([i].[value] AS int) > 1
+ ORDER BY CAST([i].[key] AS int)
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) = 11
+""");
+ }
+
public override async Task Column_collection_Any(bool async)
{
await base.Column_collection_Any(async);
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
index 1470ea95e73..90d513a75b9 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
@@ -964,6 +964,70 @@ public override async Task Column_collection_ElementAt(bool async)
""");
}
+ public override async Task Column_collection_First(bool async)
+ {
+ await base.Column_collection_First(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 "i"."value"
+ FROM json_each("p"."Ints") AS "i"
+ ORDER BY "i"."key"
+ LIMIT 1) = 1
+""");
+ }
+
+ public override async Task Column_collection_FirstOrDefault(bool async)
+ {
+ await base.Column_collection_FirstOrDefault(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 COALESCE((
+ SELECT "i"."value"
+ FROM json_each("p"."Ints") AS "i"
+ ORDER BY "i"."key"
+ LIMIT 1), 0) = 1
+""");
+ }
+
+ public override async Task Column_collection_Single(bool async)
+ {
+ await base.Column_collection_Single(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 "i"."value"
+ FROM json_each("p"."Ints") AS "i"
+ ORDER BY "i"."key"
+ LIMIT 1) = 1
+""");
+ }
+
+ public override async Task Column_collection_SingleOrDefault(bool async)
+ {
+ await base.Column_collection_SingleOrDefault(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 COALESCE((
+ SELECT "i"."value"
+ FROM json_each("p"."Ints") AS "i"
+ ORDER BY "i"."key"
+ LIMIT 1), 0) = 1
+""");
+ }
+
public override async Task Column_collection_Skip(bool async)
{
await base.Column_collection_Skip(async);
@@ -1017,6 +1081,66 @@ LIMIT 2 OFFSET 1
""");
}
+ public override async Task Column_collection_Where_Skip(bool async)
+ {
+ await base.Column_collection_Where_Skip(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 1
+ FROM json_each("p"."Ints") AS "i"
+ WHERE "i"."value" > 1
+ ORDER BY "i"."key"
+ LIMIT -1 OFFSET 1
+ ) AS "i0") = 3
+""");
+ }
+
+ public override async Task Column_collection_Where_Take(bool async)
+ {
+ await base.Column_collection_Where_Take(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 1
+ FROM json_each("p"."Ints") AS "i"
+ WHERE "i"."value" > 1
+ ORDER BY "i"."key"
+ LIMIT 2
+ ) AS "i0") = 2
+""");
+ }
+
+ public override async Task Column_collection_Where_Skip_Take(bool async)
+ {
+ await base.Column_collection_Where_Skip_Take(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 1
+ FROM json_each("p"."Ints") AS "i"
+ WHERE "i"."value" > 1
+ ORDER BY "i"."key"
+ LIMIT 2 OFFSET 1
+ ) AS "i0") = 1
+""");
+ }
+
public override async Task Column_collection_Contains_over_subquery(bool async)
{
await base.Column_collection_Contains_over_subquery(async);
@@ -1049,6 +1173,23 @@ ORDER BY "i"."value" DESC
""");
}
+ public override async Task Column_collection_Where_ElementAt(bool async)
+ {
+ await base.Column_collection_Where_ElementAt(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 "i"."value"
+ FROM json_each("p"."Ints") AS "i"
+ WHERE "i"."value" > 1
+ ORDER BY "i"."key"
+ LIMIT 1 OFFSET 0) = 11
+""");
+ }
+
public override async Task Column_collection_Any(bool async)
{
await base.Column_collection_Any(async);