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);