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