From 499e04fb4477e901fe73c8c97b677ca76fd893da Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 4 Jul 2024 13:55:19 +0200 Subject: [PATCH] Redo ReadItem and partition key management Closes #20693 Closes #20350 Fixes #34085 Fixes #33960 --- .../Extensions/CosmosQueryableExtensions.cs | 8 +- .../Internal/PartitionKeyBuilderExtensions.cs | 53 +- .../Properties/CosmosStrings.Designer.cs | 38 +- .../Properties/CosmosStrings.resx | 17 +- .../Internal/CosmosQueryCompilationContext.cs | 6 +- ...ueryMetadataExtractingExpressionVisitor.cs | 61 - .../Query/Internal/CosmosQuerySqlGenerator.cs | 2 +- .../CosmosQueryTranslationPostprocessor.cs | 5 +- .../CosmosQueryTranslationPreprocessor.cs | 14 - ...yableMethodTranslatingExpressionVisitor.cs | 327 +---- ...CosmosReadItemAndPartitionKeysExtractor.cs | 280 ++++ ...ressionVisitor.PagingQueryingEnumerable.cs | 32 +- ...ingExpressionVisitor.QueryingEnumerable.cs | 33 +- ...ssionVisitor.ReadItemQueryingEnumerable.cs | 86 +- ...pedQueryCompilingExpressionVisitor.Util.cs | 89 ++ ...osShapedQueryCompilingExpressionVisitor.cs | 22 +- .../CosmosSqlTranslatingExpressionVisitor.cs | 23 +- ...eConverterCompensatingExpressionVisitor.cs | 2 +- .../Internal/Expressions/ReadItemInfo.cs | 29 +- .../Internal/Expressions/SelectExpression.cs | 64 +- .../Expressions/SqlParameterExpression.cs | 2 +- .../Query/Internal/ISqlExpressionFactory.cs | 7 +- .../Query/Internal/SqlExpressionFactory.cs | 90 +- .../Query/SqlExpressionFactory.cs | 30 +- src/EFCore/Query/QueryCompilationContext.cs | 2 +- ...yableMethodTranslatingExpressionVisitor.cs | 8 +- .../EmbeddedDocumentsTest.cs | 7 + .../FindCosmosTest.cs | 82 +- .../HierarchicalPartitionKeyTest.cs | 1 + .../Query/InheritanceQueryCosmosTest.cs | 10 +- ...thwindAggregateOperatorsQueryCosmosTest.cs | 48 +- .../NorthwindFunctionsQueryCosmosTest.cs | 9 +- .../NorthwindMiscellaneousQueryCosmosTest.cs | 155 +- .../Query/NorthwindSelectQueryCosmosTest.cs | 7 +- .../Query/NorthwindWhereQueryCosmosTest.cs | 155 +- .../Query/OwnedQueryCosmosTest.cs | 220 ++- .../PrimitiveCollectionsQueryCosmosTest.cs | 7 +- .../Query/ReadItemPartitionKeyQueryTest.cs | 724 ++++++++++ .../ReadItemTest.cs | 1277 ----------------- .../ReloadTest.cs | 27 +- .../Query/NorthwindWhereQueryTestBase.cs | 7 +- .../PrimitiveCollectionsQueryTestBase.cs | 10 +- 42 files changed, 1757 insertions(+), 2319 deletions(-) delete mode 100644 src/EFCore.Cosmos/Query/Internal/CosmosQueryMetadataExtractingExpressionVisitor.cs create mode 100644 src/EFCore.Cosmos/Query/Internal/CosmosReadItemAndPartitionKeysExtractor.cs create mode 100644 src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.Util.cs create mode 100644 test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryTest.cs delete mode 100644 test/EFCore.Cosmos.FunctionalTests/ReadItemTest.cs diff --git a/src/EFCore.Cosmos/Extensions/CosmosQueryableExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosQueryableExtensions.cs index 7def403562a..68417190ef9 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosQueryableExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosQueryableExtensions.cs @@ -35,9 +35,7 @@ internal static readonly MethodInfo WithPartitionKeyMethodInfo /// The source query. /// The partition key value. /// A new query with the set partition key. - public static IQueryable WithPartitionKey( - this IQueryable source, - [NotParameterized] string partitionKey) + public static IQueryable WithPartitionKey(this IQueryable source, string partitionKey) where TEntity : class => WithPartitionKey(source, partitionKey, []); @@ -56,8 +54,8 @@ public static IQueryable WithPartitionKey( /// A new query with the set partition key. public static IQueryable WithPartitionKey( this IQueryable source, - [NotParameterized] object partitionKeyValue, - [NotParameterized] params object[] additionalPartitionKeyValues) + object partitionKeyValue, + params object[] additionalPartitionKeyValues) where TEntity : class { Check.NotNull(partitionKeyValue, nameof(partitionKeyValue)); diff --git a/src/EFCore.Cosmos/Extensions/Internal/PartitionKeyBuilderExtensions.cs b/src/EFCore.Cosmos/Extensions/Internal/PartitionKeyBuilderExtensions.cs index 38370530003..a7775eb0247 100644 --- a/src/EFCore.Cosmos/Extensions/Internal/PartitionKeyBuilderExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/Internal/PartitionKeyBuilderExtensions.cs @@ -35,36 +35,37 @@ public static PartitionKeyBuilder Add(this PartitionKeyBuilder builder, object? else { var expectedType = (converter?.ProviderClrType ?? property?.ClrType)?.UnwrapNullableType(); - if (value is string stringValue) + switch (value) { - if (expectedType != null && expectedType != typeof(string)) - { - CheckType(typeof(string)); - } + case string stringValue: + if (expectedType != null && expectedType != typeof(string)) + { + CheckType(typeof(string)); + } - builder.Add(stringValue); - } - else if (value is bool boolValue) - { - if (expectedType != null && expectedType != typeof(bool)) - { - CheckType(typeof(bool)); - } + builder.Add(stringValue); + break; - builder.Add(boolValue); - } - else if (value.GetType().IsNumeric()) - { - if (expectedType != null && !expectedType.IsNumeric()) - { - CheckType(value.GetType()); - } + case bool boolValue: + if (expectedType != null && expectedType != typeof(bool)) + { + CheckType(typeof(bool)); + } - builder.Add(Convert.ToDouble(value)); - } - else - { - throw new InvalidOperationException(CosmosStrings.PartitionKeyBadValue(value.GetType())); + builder.Add(boolValue); + break; + + case var _ when value.GetType().IsNumeric(): + if (expectedType != null && !expectedType.IsNumeric()) + { + CheckType(value.GetType()); + } + + builder.Add(Convert.ToDouble(value)); + break; + + default: + throw new InvalidOperationException(CosmosStrings.PartitionKeyBadValue(value.GetType())); } void CheckType(Type actualType) diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs index 4402ce74916..b009715b442 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs @@ -107,6 +107,14 @@ public static string IdNonStringStoreType(object? idProperty, object? entityType GetString("IdNonStringStoreType", nameof(idProperty), nameof(entityType), nameof(propertyType)), idProperty, entityType, propertyType); + /// + /// {actual} partition key values were provided, but the entity type '{entityType}' has {expected} partition key values defined. + /// + public static string IncorrectPartitionKeyNumber(object? entityType, object? actual, object? expected) + => string.Format( + GetString("IncorrectPartitionKeyNumber", nameof(entityType), nameof(actual), nameof(expected)), + entityType, actual, expected); + /// /// The entity type '{entityType}' has an index defined over properties '{properties}'. The Azure Cosmos DB provider for EF Core currently does not support index definitions. /// @@ -166,12 +174,12 @@ public static string MissingOrderingInSelectExpression => GetString("MissingOrderingInSelectExpression"); /// - /// Cosmos container '{container1}' is referenced by the query, but '{container2}' is already being referenced. A query can only reference a single Cosmos container. + /// Root entity type '{entityType1}' is referenced by the query, but '{entityType2}' is already being referenced. A query can only reference a single root entity type. /// - public static string MultipleContainersReferencedInQuery(object? container1, object? container2) + public static string MultipleRootEntityTypesReferencedInQuery(object? entityType1, object? entityType2) => string.Format( - GetString("MultipleContainersReferencedInQuery", nameof(container1), nameof(container2)), - container1, container2); + GetString("MultipleRootEntityTypesReferencedInQuery", nameof(entityType1), nameof(entityType2)), + entityType1, entityType2); /// /// Navigation '{entityType}.{navigationName}' doesn't point to an embedded entity. @@ -336,12 +344,10 @@ public static string PartitionKeyBadValueType(object? propertyType, object? enti propertyType, entityType, property, valueType); /// - /// The partition key specified in the 'WithPartitionKey' call '{partitionKey1}' and the partition key specified in the 'Where' predicate '{partitionKey2}' must be identical to return any results. Remove one of them. + /// The partition key specified in the 'WithPartitionKey' call and the partition key specified in the 'Where' predicate must be identical to return any results. Remove one of them. /// - public static string PartitionKeyMismatch(object? partitionKey1, object? partitionKey2) - => string.Format( - GetString("PartitionKeyMismatch", nameof(partitionKey1), nameof(partitionKey2)), - partitionKey1, partitionKey2); + public static string PartitionKeyMismatch + => GetString("PartitionKeyMismatch"); /// /// Unable to execute a 'ReadItem' query since the partition key value is missing. Consider using the 'WithPartitionKey' method on the query to specify partition key to use. @@ -444,11 +450,23 @@ public static string VisitChildrenMustBeOverridden => GetString("VisitChildrenMustBeOverridden"); /// - /// 'WithPartitionKeyMethodInfo' can only be called on a entity query root. See https://aka.ms/efdocs-cosmos-partition-keys for more information. + /// 'WithPartitionKey' can only be called once in a query. See https://aka.ms/efdocs-cosmos-partition-keys for more information. + /// + public static string WithPartitionKeyAlreadyCalled + => GetString("WithPartitionKeyAlreadyCalled"); + + /// + /// 'WithPartitionKey' can only be called on a entity query root. See https://aka.ms/efdocs-cosmos-partition-keys for more information. /// public static string WithPartitionKeyBadNode => GetString("WithPartitionKeyBadNode"); + /// + /// 'WithPartitionKey' only accepts simple constant or parameter arguments. See https://aka.ms/efdocs-cosmos-partition-keys for more information. + /// + public static string WithPartitionKeyNotConstantOrParameter + => GetString("WithPartitionKeyNotConstantOrParameter"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name)!; diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.resx b/src/EFCore.Cosmos/Properties/CosmosStrings.resx index 34887367904..f2e6bec1130 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.resx +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.resx @@ -153,6 +153,9 @@ The type of the '{idProperty}' property on '{entityType}' is '{propertyType}'. All 'id' properties must be strings or have a string value converter. + + {actual} partition key values were provided, but the entity type '{entityType}' has {expected} partition key values defined. + The entity type '{entityType}' has an index defined over properties '{properties}'. The Azure Cosmos DB provider for EF Core currently does not support index definitions. @@ -213,8 +216,8 @@ 'Reverse' could not be translated to the server because there is no ordering on the server side. - - Cosmos container '{container1}' is referenced by the query, but '{container2}' is already being referenced. A query can only reference a single Cosmos container. + + Root entity type '{entityType1}' is referenced by the query, but '{entityType2}' is already being referenced. A query can only reference a single root entity type. Navigation '{entityType}.{navigationName}' doesn't point to an embedded entity. @@ -283,7 +286,7 @@ The partition key value supplied for '{propertyType}' property '{entityType}.{property}' is of type '{valueType}'. Partition key values must be of a type assignable to the property. - The partition key specified in the 'WithPartitionKey' call '{partitionKey1}' and the partition key specified in the 'Where' predicate '{partitionKey2}' must be identical to return any results. Remove one of them. + The partition key specified in the 'WithPartitionKey' call and the partition key specified in the 'Where' predicate must be identical to return any results. Remove one of them. Unable to execute a 'ReadItem' query since the partition key value is missing. Consider using the 'WithPartitionKey' method on the query to specify partition key to use. @@ -327,7 +330,13 @@ 'VisitChildren' must be overridden in the class deriving from 'SqlExpression'. + + 'WithPartitionKey' can only be called once in a query. See https://aka.ms/efdocs-cosmos-partition-keys for more information. + - 'WithPartitionKeyMethodInfo' can only be called on a entity query root. See https://aka.ms/efdocs-cosmos-partition-keys for more information. + 'WithPartitionKey' can only be called on a entity query root. See https://aka.ms/efdocs-cosmos-partition-keys for more information. + + + 'WithPartitionKey' only accepts simple constant or parameter arguments. See https://aka.ms/efdocs-cosmos-partition-keys for more information. \ No newline at end of file diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryCompilationContext.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryCompilationContext.cs index bee3bdb6b5b..2673f87de42 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryCompilationContext.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryCompilationContext.cs @@ -13,7 +13,7 @@ public class CosmosQueryCompilationContext(QueryCompilationContextDependencies d : QueryCompilationContext(dependencies, async) { /// - /// The name of the Cosmos container against which this query will be executed. + /// The root entity type being queried. /// /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -21,7 +21,7 @@ public class CosmosQueryCompilationContext(QueryCompilationContextDependencies d /// any release. You should only use it directly in 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? CosmosContainer { get; internal set; } + public virtual IEntityType? RootEntityType { get; internal set; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -29,7 +29,7 @@ public class CosmosQueryCompilationContext(QueryCompilationContextDependencies d /// any release. You should only use it directly in 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 PartitionKey? PartitionKeyValueFromExtension { get; internal set; } + public virtual List PartitionKeyPropertyValues { get; internal set; } = new(); /// /// A manager for aliases, capable of generate uniquified source aliases. diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryMetadataExtractingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryMetadataExtractingExpressionVisitor.cs deleted file mode 100644 index 4e0dd73b3a9..00000000000 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryMetadataExtractingExpressionVisitor.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.EntityFrameworkCore.Cosmos.Internal; -using Microsoft.EntityFrameworkCore.Internal; - -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 CosmosQueryMetadataExtractingExpressionVisitor(CosmosQueryCompilationContext cosmosQueryCompilationContext) - : ExpressionVisitor -{ - /// - /// 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 VisitMethodCall(MethodCallExpression methodCallExpression) - { - if (methodCallExpression.Method.IsGenericMethod - && methodCallExpression.Method.GetGenericMethodDefinition() == CosmosQueryableExtensions.WithPartitionKeyMethodInfo) - { - var innerQueryable = Visit(methodCallExpression.Arguments[0]); - - var firstValue = methodCallExpression.Arguments[1].GetConstantValue(); - if (firstValue == null) - { - cosmosQueryCompilationContext.PartitionKeyValueFromExtension = PartitionKey.None; - } - else - { - if (innerQueryable is EntityQueryRootExpression rootExpression) - { - var partitionKeyProperties = rootExpression.EntityType.GetPartitionKeyProperties(); - var allValues = new[] { firstValue }.Concat(methodCallExpression.Arguments[2].GetConstantValue()).ToList(); - var builder = new PartitionKeyBuilder(); - for (var i = 0; i < allValues.Count; i++) - { - builder.Add(allValues[i], partitionKeyProperties[i]); - } - - cosmosQueryCompilationContext.PartitionKeyValueFromExtension = builder.Build(); - } - else - { - throw new InvalidOperationException(CosmosStrings.WithPartitionKeyBadNode); - } - } - - return innerQueryable; - } - - return base.VisitMethodCall(methodCallExpression); - } -} diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs index d55224c3f8e..110caa01760 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs @@ -294,7 +294,7 @@ protected override Expression VisitSelect(SelectExpression selectExpression) // 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 is [{ Alias: string 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 { diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPostprocessor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPostprocessor.cs index afd87a9d63b..bc82944976e 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPostprocessor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPostprocessor.cs @@ -27,13 +27,14 @@ public override Expression Process(Expression query) if (query is ShapedQueryExpression { QueryExpression: SelectExpression selectExpression }) { - // Cosmos does not have nested select expression so this should be safe. selectExpression.ApplyProjection(); } var afterValueConverterCompensation = new CosmosValueConverterCompensatingExpressionVisitor(sqlExpressionFactory).Visit(query); var afterAliases = queryCompilationContext.AliasManager.PostprocessAliases(afterValueConverterCompensation); + var afterExtraction = new CosmosReadItemAndPartitionKeysExtractor().ExtractPartitionKeysAndId( + queryCompilationContext, sqlExpressionFactory, afterAliases); - return afterAliases; + return afterExtraction; } } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPreprocessor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPreprocessor.cs index b991a06f3a9..cc8cadf8b0d 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPreprocessor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPreprocessor.cs @@ -14,20 +14,6 @@ public class CosmosQueryTranslationPreprocessor( CosmosQueryCompilationContext cosmosQueryCompilationContext) : QueryTranslationPreprocessor(dependencies, cosmosQueryCompilationContext) { - /// - /// 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 Expression NormalizeQueryableMethod(Expression query) - { - query = new CosmosQueryMetadataExtractingExpressionVisitor(cosmosQueryCompilationContext).Visit(query); - query = base.NormalizeQueryableMethod(query); - - return query; - } - /// protected override Expression ProcessQueryRoots(Expression expression) => new CosmosQueryRootProcessor(Dependencies, QueryCompilationContext).Visit(expression); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index d128a9182f5..56fcda77c5c 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -27,7 +27,6 @@ public class CosmosQueryableMethodTranslatingExpressionVisitor : QueryableMethod private readonly CosmosProjectionBindingExpressionVisitor _projectionBindingExpressionVisitor; private readonly CosmosAliasManager _aliasManager; private bool _subquery; - private ReadItemInfo? _readItemInfo; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -154,160 +153,48 @@ public override Expression Translate(Expression expression) return base.Translate(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. - /// - [return: NotNullIfNotNull(nameof(expression))] - public override Expression? Visit(Expression? expression) + /// + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) { - if (expression is MethodCallExpression - { - Method: { Name: nameof(Queryable.FirstOrDefault), IsGenericMethod: true }, - Arguments: [MethodCallExpression innerMethodCall] - }) + var method = methodCallExpression.Method; + + if (methodCallExpression.Method.DeclaringType == typeof(CosmosQueryableExtensions) + && methodCallExpression.Method.Name == nameof(CosmosQueryableExtensions.WithPartitionKey)) { - var clrType = innerMethodCall.Type.TryGetSequenceType() ?? typeof(object); - if (innerMethodCall is - { - Method: { Name: nameof(Queryable.Select), IsGenericMethod: true }, - Arguments: - [ - MethodCallExpression innerInnerMethodCall, - UnaryExpression { NodeType: ExpressionType.Quote } unaryExpression - ] - }) + if (_queryCompilationContext.PartitionKeyPropertyValues.Count > 0) { - // Strip out Include and Convert expressions until we get to the parameter, or not. - var processing = unaryExpression.Operand; - while (true) - { - switch (processing) - { - case UnaryExpression { NodeType: ExpressionType.Quote or ExpressionType.Convert } q: - processing = q.Operand; - continue; - case LambdaExpression l: - processing = l.Body; - continue; - case IncludeExpression i: - processing = i.EntityExpression; - continue; - } - break; - } - - // If we are left with the ParameterExpression, then it's safe to use ReadItem. - if (processing is ParameterExpression) - { - innerMethodCall = innerInnerMethodCall; - } + throw new InvalidOperationException(CosmosStrings.WithPartitionKeyAlreadyCalled); } - if (innerMethodCall is - { - Method: { Name: nameof(Queryable.Where), IsGenericMethod: true }, - Arguments: - [ - EntityQueryRootExpression { EntityType: var entityType }, - UnaryExpression { Operand: LambdaExpression lambdaExpression, NodeType: ExpressionType.Quote } - ] - }) + if (methodCallExpression.Arguments[0] is not EntityQueryRootExpression) { - var queryProperties = new List(); - var parameterNames = new List(); - - if (ExtractPartitionKeyFromPredicate(entityType, lambdaExpression.Body, queryProperties, parameterNames)) - { - var entityTypePrimaryKeyProperties = entityType.FindPrimaryKey()!.Properties; - var partitionKeyProperties = entityType.GetPartitionKeyProperties(); - - if (entityTypePrimaryKeyProperties.SequenceEqual(queryProperties) - && (!partitionKeyProperties.Any() - || partitionKeyProperties.All(p => entityTypePrimaryKeyProperties.Contains(p))) - && entityType.GetJsonIdDefinition() != null) - { - var propertyParameterList = queryProperties.Zip( - parameterNames, - (property, parameter) => (property, parameter)) - .ToDictionary(tuple => tuple.property, tuple => tuple.parameter); - - // TODO: Reimplement ReadItem properly: #34157 - _readItemInfo = new ReadItemInfo(entityType, propertyParameterList, clrType); - } - } + throw new InvalidOperationException(CosmosStrings.WithPartitionKeyBadNode); } - } - return base.Visit(expression); + var innerQueryable = Visit(methodCallExpression.Arguments[0]); - static bool ExtractPartitionKeyFromPredicate( - IEntityType entityType, - Expression joinCondition, - ICollection properties, - ICollection parameterNames) - { - switch (joinCondition) + var firstValue = _sqlTranslator.Translate(methodCallExpression.Arguments[1], applyDefaultTypeMapping: false); + if (firstValue is not SqlConstantExpression and not SqlParameterExpression) { - case BinaryExpression joinBinaryExpression: - switch (joinBinaryExpression) - { - case { NodeType: ExpressionType.AndAlso }: - return ExtractPartitionKeyFromPredicate(entityType, joinBinaryExpression.Left, properties, parameterNames) - && ExtractPartitionKeyFromPredicate(entityType, joinBinaryExpression.Right, properties, parameterNames); - - case - { - NodeType: ExpressionType.Equal, - Left: MethodCallExpression equalMethodCallExpression, - Right: ParameterExpression { Name: string parameterName } - } when equalMethodCallExpression.TryGetEFPropertyArguments(out _, out var propertyName): - var property = entityType.FindProperty(propertyName); - if (property == null) - { - return false; - } - - properties.Add(property); - parameterNames.Add(parameterName); - return true; - } + throw new InvalidOperationException(CosmosStrings.WithPartitionKeyNotConstantOrParameter); + } - break; + _queryCompilationContext.PartitionKeyPropertyValues.Add(firstValue); - case MethodCallExpression - { - Method.Name: "Equals", - Object: null, - Arguments: - [ - MethodCallExpression equalsMethodCallExpression, - ParameterExpression { Name: string parameterName } - ] - } when equalsMethodCallExpression.TryGetEFPropertyArguments(out _, out var propertyName): + if (methodCallExpression.Arguments.Count == 3) + { + var remainingValuesArray = _sqlTranslator.Translate(methodCallExpression.Arguments[2], applyDefaultTypeMapping: false); + if (remainingValuesArray is not SqlParameterExpression) { - var property = entityType.FindProperty(propertyName); - if (property == null) - { - return false; - } - - properties.Add(property); - parameterNames.Add(parameterName); - return true; + throw new InvalidOperationException(CosmosStrings.WithPartitionKeyNotConstantOrParameter); } + + _queryCompilationContext.PartitionKeyPropertyValues.Add(remainingValuesArray); } - return false; + return innerQueryable; } - } - /// - protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) - { - var method = methodCallExpression.Method; if (method.DeclaringType == typeof(Queryable) && method.IsGenericMethod) { switch (methodCallExpression.Method.Name) @@ -376,7 +263,7 @@ protected override Expression VisitExtension(Expression extensionExpression) var selectExpression = new SelectExpression( new SourceExpression(fromSql, alias), new EntityProjectionExpression(new ObjectReferenceExpression(entityType, alias), entityType)); - return CreateShapedQueryExpression(entityType, selectExpression); + return CreateShapedQueryExpression(entityType, selectExpression) ?? QueryCompilationContext.NotTranslatedExpression; default: return base.VisitExtension(extensionExpression); @@ -416,15 +303,14 @@ 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. /// - protected override ShapedQueryExpression CreateShapedQueryExpression(IEntityType entityType) + protected override ShapedQueryExpression? CreateShapedQueryExpression(IEntityType entityType) { Check.DebugAssert(!entityType.IsOwned(), "Can't create ShapedQueryExpression for owned entity type"); var alias = _aliasManager.GenerateSourceAlias("c"); var selectExpression = new SelectExpression( new SourceExpression(new ObjectReferenceExpression(entityType, "root"), alias), - new EntityProjectionExpression(new ObjectReferenceExpression(entityType, alias), entityType), - _readItemInfo); + new EntityProjectionExpression(new ObjectReferenceExpression(entityType, alias), entityType)); // Add discriminator predicate var concreteEntityTypes = entityType.GetConcreteDerivedTypesInclusive().ToList(); @@ -460,20 +346,19 @@ protected override ShapedQueryExpression CreateShapedQueryExpression(IEntityType return CreateShapedQueryExpression(entityType, selectExpression); } - private ShapedQueryExpression CreateShapedQueryExpression(IEntityType entityType, SelectExpression queryExpression) + private ShapedQueryExpression? CreateShapedQueryExpression(IEntityType entityType, SelectExpression queryExpression) { if (!entityType.IsOwned()) { - var cosmosContainer = entityType.GetContainer(); - var existingContainer = _queryCompilationContext.CosmosContainer; - Check.DebugAssert(cosmosContainer is not null, "Non-owned entity type without a Cosmos container"); - - if (existingContainer is not null && existingContainer != cosmosContainer) + var existingEntityType = _queryCompilationContext.RootEntityType; + if (existingEntityType is not null && existingEntityType != entityType) { - throw new InvalidOperationException(CosmosStrings.MultipleContainersReferencedInQuery(cosmosContainer, existingContainer)); + AddTranslationErrorDetails( + CosmosStrings.MultipleRootEntityTypesReferencedInQuery(entityType.DisplayName(), existingEntityType.DisplayName())); + return null; } - _queryCompilationContext.CosmosContainer = cosmosContainer; + _queryCompilationContext.RootEntityType = entityType; } return new ShapedQueryExpression( @@ -1502,149 +1387,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? TranslateWhere(ShapedQueryExpression source, LambdaExpression predicate) - { - var select = (SelectExpression)source.QueryExpression; - - if (source.ShaperExpression is StructuralTypeShaperExpression { StructuralType: IEntityType entityType } - && entityType.GetPartitionKeyPropertyNames().FirstOrDefault() != null) - { - List<(Expression Expression, IProperty Property)?> partitionKeyValues = new(); - if (TryExtractPartitionKey(predicate.Body, entityType, out var newPredicate, partitionKeyValues)) - { - foreach (var propertyName in entityType.GetPartitionKeyPropertyNames()) - { - var partitionKeyValue = partitionKeyValues.FirstOrDefault(p => p!.Value.Property.Name == propertyName); - if (partitionKeyValue == null) - { - newPredicate = null; - break; - } - - ((SelectExpression)source.QueryExpression).AddPartitionKey( - partitionKeyValue.Value.Property, partitionKeyValue.Value.Expression); - } - - if (newPredicate == null) - { - return source; - } - - predicate = Expression.Lambda(newPredicate, predicate.Parameters); - } - } - - return TryApplyPredicate(source, predicate) ? source : null; - - bool TryExtractPartitionKey( - Expression expression, - IEntityType entityType, - out Expression? updatedPredicate, - List<(Expression, IProperty)?> partitionKeyValues) - { - updatedPredicate = null; - if (expression is BinaryExpression binaryExpression) - { - if (TryGetPartitionKeyValue(binaryExpression, entityType, out var valueExpression, out var property)) - { - partitionKeyValues.Add((valueExpression!, property!)); - return true; - } - - if (binaryExpression.NodeType == ExpressionType.AndAlso) - { - var foundInRight = TryExtractPartitionKey(binaryExpression.Left, entityType, out var leftPredicate, partitionKeyValues); - - var foundInLeft = TryExtractPartitionKey( - binaryExpression.Right, - entityType, - out var rightPredicate, - partitionKeyValues); - - if (foundInLeft && foundInRight) - { - return true; - } - - if (foundInLeft || foundInRight) - { - updatedPredicate = leftPredicate != null - ? rightPredicate != null - ? binaryExpression.Update(leftPredicate, binaryExpression.Conversion, rightPredicate) - : leftPredicate - : rightPredicate; - - return true; - } - } - } - else if (expression.NodeType == ExpressionType.MemberAccess - && expression.Type == typeof(bool)) - { - if (IsPartitionKeyPropertyAccess(expression, entityType, out var property)) - { - partitionKeyValues.Add((Expression.Constant(true), property!)); - return true; - } - } - else if (expression.NodeType == ExpressionType.Not) - { - if (IsPartitionKeyPropertyAccess(((UnaryExpression)expression).Operand, entityType, out var property)) - { - partitionKeyValues.Add((Expression.Constant(false), property!)); - return true; - } - } - - updatedPredicate = expression; - return false; - } - - bool TryGetPartitionKeyValue( - BinaryExpression binaryExpression, - IEntityType entityType, - out Expression? expression, - out IProperty? property) - { - if (binaryExpression.NodeType == ExpressionType.Equal) - { - expression = IsPartitionKeyPropertyAccess(binaryExpression.Left, entityType, out property) - ? binaryExpression.Right - : IsPartitionKeyPropertyAccess(binaryExpression.Right, entityType, out property) - ? binaryExpression.Left - : null; - - if (expression is ConstantExpression - || (expression is ParameterExpression valueParameterExpression - && valueParameterExpression.Name? - .StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal) - == true)) - { - return true; - } - } - - expression = null; - property = null; - return false; - } - - bool IsPartitionKeyPropertyAccess(Expression expression, IEntityType entityType, out IProperty? property) - { - property = expression switch - { - MemberExpression memberExpression - => entityType.FindProperty(memberExpression.Member.GetSimpleMemberName()), - MethodCallExpression methodCallExpression when methodCallExpression.TryGetEFPropertyArguments(out _, out var propertyName) - => entityType.FindProperty(propertyName), - MethodCallExpression methodCallExpression - when methodCallExpression.TryGetIndexerArguments(_queryCompilationContext.Model, out _, out var propertyName) - => entityType.FindProperty(propertyName), - _ => null - }; - - return property != null && entityType.GetPartitionKeyPropertyNames().Contains(property.Name); - } - } + => TryApplyPredicate(source, predicate) ? source : null; #region Queryable collection support diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosReadItemAndPartitionKeysExtractor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosReadItemAndPartitionKeysExtractor.cs new file mode 100644 index 00000000000..46938e3bbd5 --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/CosmosReadItemAndPartitionKeysExtractor.cs @@ -0,0 +1,280 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; + +/// +/// Identifies Cosmos queries that can be transformed to optimized ReadItem form and performs the transformation. +/// +/// +/// 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 CosmosReadItemAndPartitionKeysExtractor : ExpressionVisitor +{ + private ISqlExpressionFactory _sqlExpressionFactory = null!; + private IEntityType _entityType = null!; + private string _rootAlias = null!; + private bool _isPredicateCompatibleWithReadItem; + private string? _discriminatorJsonPropertyName; + private Dictionary _jsonIdPropertyValues = null!; + private Dictionary _partitionKeyPropertyValues = 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 Expression ExtractPartitionKeysAndId( + CosmosQueryCompilationContext queryCompilationContext, + ISqlExpressionFactory sqlExpressionFactory, + Expression expression) + { + _entityType = queryCompilationContext.RootEntityType + ?? throw new UnreachableException("No root entity type was set during query processing."); + _sqlExpressionFactory = sqlExpressionFactory; + + if (expression is not ShapedQueryExpression + { + QueryExpression: SelectExpression + { + Sources: [{ Expression: ObjectReferenceExpression } rootSource, ..], + Predicate: SqlExpression predicate + } select + } shapedQuery) + { + return expression; + } + + _rootAlias = rootSource.Alias; + + // We're going to be looking for equality comparisons on the JSON id definition properties and the partition key properties of the + // entity type; build a dictionary where the properties are the keys, and where the values are expressions that will get populated + // from the tree (either constants or parameters). + // We also want to ignore the discriminator property if it's compared to our entity type's discriminator value (see below). + var partitionKeyProperties = _entityType.GetPartitionKeyProperties(); + (_isPredicateCompatibleWithReadItem, var jsonIdProperties, _jsonIdPropertyValues) = + _entityType.GetJsonIdDefinition() is IJsonIdDefinition jsonIdDefinition + ? (true, jsonIdDefinition.Properties, jsonIdDefinition.Properties.ToDictionary(p => p, _ => (Expression?)null)) + : (false, [], new()); + + _partitionKeyPropertyValues = partitionKeyProperties.ToDictionary(p => p, _ => (Expression?)null); + + var discriminatorProperty = _entityType.FindDiscriminatorProperty(); + _discriminatorJsonPropertyName = discriminatorProperty?.GetJsonPropertyName(); + + // Visit the predicate. + // This will populate _jsonIdPropertyValues and _partitionKeyPropertyValues with comparisons found in the predicate, and return + // a rewritten predicate where the partition key comparisons have been removed. + var predicateWithoutPartitionKeyComparisons = (SqlExpression)Visit(predicate); + + // If the discriminator is part of the JSON id definition, a comparison may be missing from the predicate, since we don't add one + // if it's not needed (e.g. only one entity type mapped to the container). For that case, add the entity type's discriminator value. + if (discriminatorProperty is not null + && _jsonIdPropertyValues.TryGetValue(discriminatorProperty, out var discriminatorValue) + && discriminatorValue is null) + { + _jsonIdPropertyValues[discriminatorProperty] = _sqlExpressionFactory.Constant( + _entityType.GetDiscriminatorValue(), discriminatorProperty.ClrType); + } + + var allIdPropertiesSpecified = + _jsonIdPropertyValues.Values.All(p => p is not null) && _jsonIdPropertyValues.Count > 0; + var allPartitionKeyPropertiesSpecified = _partitionKeyPropertyValues.Values.All(p => p is not null); + + // First, take care of the partition key properties; if the visitation above returned a different predicate, that means that some + // partition key comparisons were extracted (and therefore found). Lift these up to the query compilation context and rewrite + // the SelectExpression with the new, reduced predicate. + if (allPartitionKeyPropertiesSpecified) + { + // If the user called WithPartitionKey(), check that it's identical to what we extracted from the predicate. + if (queryCompilationContext.PartitionKeyPropertyValues.Count == 0) + { + foreach (var partitionKeyProperty in partitionKeyProperties) + { + queryCompilationContext.PartitionKeyPropertyValues.Add(_partitionKeyPropertyValues[partitionKeyProperty]!); + } + } + else if (!queryCompilationContext.PartitionKeyPropertyValues.SequenceEqual( + partitionKeyProperties.Select(p => _partitionKeyPropertyValues[p]!))) + { + throw new InvalidOperationException(CosmosStrings.PartitionKeyMismatch); + } + + select = select.Update( + select.Sources.ToList(), + predicateWithoutPartitionKeyComparisons is SqlConstantExpression { Value: true } + ? null + : predicateWithoutPartitionKeyComparisons, + select.Projection.ToList(), + select.Orderings.ToList(), + select.Offset, + select.Limit); + + shapedQuery = shapedQuery.UpdateQueryExpression(select); + } + + // Now, attempt to also transform the query to ReadItem form if possible. + if (_isPredicateCompatibleWithReadItem + && allIdPropertiesSpecified + && allPartitionKeyPropertiesSpecified + // If the entity type being queried has derived types and the discriminator is part of the JSON id, we can't reliably use + // ReadItem, since we don't know in advance which derived type the document represents. + && (!jsonIdProperties.Contains(discriminatorProperty) + || (!_entityType.GetDerivedTypes().Any() && _entityType.GetIsDiscriminatorMappingComplete())) + && select is + { + Offset: null or SqlConstantExpression { Value: 0 }, + Limit: null or SqlConstantExpression { Value: > 0 } + } + // We only transform to ReadItem if the entire document (i.e. root entity type) is being projected out. + // Using ReadItem even when a projection is present is tracked by #34163. + && Unwrap(shapedQuery.ShaperExpression) is StructuralTypeShaperExpression { StructuralType: var projectedStructuralType } + && projectedStructuralType == _entityType) + { + return shapedQuery.UpdateQueryExpression(select.WithReadItemInfo(new ReadItemInfo(_jsonIdPropertyValues!))); + } + + return shapedQuery; + + Expression Unwrap(Expression shaper) + { + if (shaper is UnaryExpression { NodeType: ExpressionType.Convert } convert + && convert.Type == typeof(object)) + { + shaper = convert.Operand; + } + + while (shaper is IncludeExpression { EntityExpression: var nested }) + { + shaper = nested; + } + + return shaper; + } + } + + /// + /// 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 VisitExtension(Expression node) + { + switch (node) + { + case SqlBinaryExpression { OperatorType: ExpressionType.Equal, Left: var left, Right: var right } binary: + { + // TODO: Handle property accesses into complex types/owned entity types, #25548 + var (scalarAccess, propertyValue) = + left is ScalarAccessExpression leftScalarAccess + && right is SqlParameterExpression or SqlConstantExpression + ? (leftScalarAccess, right) + : right is ScalarAccessExpression rightScalarAccess + && left is SqlParameterExpression or SqlConstantExpression + ? (rightScalarAccess, left) + : (null, null); + + if (scalarAccess?.Object is ObjectReferenceExpression { Name: var referencedSourceAlias } + && referencedSourceAlias == _rootAlias) + { + return ProcessPropertyComparison(scalarAccess.PropertyName, propertyValue!, binary); + } + + _isPredicateCompatibleWithReadItem = false; + return binary; + } + + // Bool property access (e.g. Where(b => b.BoolPartitionKey)) + case ScalarAccessExpression { PropertyName: var propertyName } scalarAccess: + return ProcessPropertyComparison(propertyName, _sqlExpressionFactory.Constant(true), scalarAccess); + + // Negated bool property access (e.g. Where(b => !b.BoolPartitionKey)) + case SqlUnaryExpression + { + OperatorType: ExpressionType.Not, + Operand: ScalarAccessExpression { PropertyName: var propertyName } + } unary: + return ProcessPropertyComparison(propertyName, _sqlExpressionFactory.Constant(false), unary); + + case SqlBinaryExpression { OperatorType: ExpressionType.AndAlso } binary: + return _sqlExpressionFactory.MakeBinary( + ExpressionType.AndAlso, + (SqlExpression)Visit(binary.Left), + (SqlExpression)Visit(binary.Right), + binary.TypeMapping, + binary)!; + + default: + // Anything else in the predicate, e.g. an OR, immediately disqualifies it from being a ReadItem query, and means we + // can't extract partition key properties. + _isPredicateCompatibleWithReadItem = false; + return node; + } + + SqlExpression ProcessPropertyComparison(string propertyName, SqlExpression propertyValue, SqlExpression originalExpression) + { + // We assume that the comparison is incompatible with ReadItem until proven otherwise, i.e. the comparison is for a JSON ID + // property, a partition key property, or certain cases involving the discriminator property. + var isCompatibleComparisonForReadItem = false; + + foreach (var property in _jsonIdPropertyValues.Keys) + { + if (propertyName == property.GetJsonPropertyName()) + { + if (_jsonIdPropertyValues.TryGetValue(property, out var previousValue) + && (previousValue is null || previousValue.Equals(propertyValue))) + { + _jsonIdPropertyValues[property] = propertyValue; + isCompatibleComparisonForReadItem = true; + } + break; + } + } + + foreach (var property in _partitionKeyPropertyValues.Keys) + { + // We found a comparison for a partition key property. + // Extract its value expression and elide the comparison from the predicate - it'll be lifted out to the Cosmos SDK + // call. Note that this is always considered a compatible comparison for ReadItem. + if (propertyName == property.GetJsonPropertyName() + && _partitionKeyPropertyValues.TryGetValue(property, out var previousValue) + && (previousValue is null || previousValue.Equals(propertyValue))) + { + _partitionKeyPropertyValues[property] = propertyValue; + return _sqlExpressionFactory.Constant(true); + } + } + + // The query contains a comparison on the discriminator property. + // If the discriminator is part of the JSON ID property, it'll be handled below like any other JSON ID property. + // However, if it isn't, we may need to ignore the comparison, and allow transforming to ReadItem. For example, when + // multiple entity types are mapped to the same container, EF adds a discriminator comparison; but we want to use ReadItem + // for these (common) cases - so we ignore the comparison for the purpose of ReadItem transformation, and validate the + // discriminator coming back from Cosmos in the shaper, to ensure throwing for an incorrect type. + if (isCompatibleComparisonForReadItem + && propertyName == _discriminatorJsonPropertyName + && propertyValue is SqlConstantExpression { Value: object specifiedDiscriminatorValue } + && _entityType.FindDiscriminatorProperty() is IProperty discriminatorProperty + && _entityType.GetDiscriminatorValue() is object entityDiscriminatorValue + && discriminatorProperty.GetProviderValueComparer().Equals(specifiedDiscriminatorValue, entityDiscriminatorValue)) + { + isCompatibleComparisonForReadItem = true; + } + + if (!isCompatibleComparisonForReadItem) + { + _isPredicateCompatibleWithReadItem = false; + } + + return originalExpression; + } + } +} diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.PagingQueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.PagingQueryingEnumerable.cs index 1e5c16966f5..5add7ca24cd 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.PagingQueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.PagingQueryingEnumerable.cs @@ -28,7 +28,7 @@ private sealed class PagingQueryingEnumerable : IAsyncEnumerable _queryLogger; private readonly IDiagnosticsLogger _commandLogger; private readonly bool _standAloneStateManager; @@ -44,8 +44,8 @@ public PagingQueryingEnumerable( SelectExpression selectExpression, Func shaper, Type contextType, - string cosmosContainer, - PartitionKey partitionKeyValueFromExtension, + IEntityType rootEntityType, + List partitionKeyPropertyValues, bool standAloneStateManager, bool threadSafetyChecksEnabled, string maxItemCountParameterName, @@ -66,16 +66,10 @@ public PagingQueryingEnumerable( _continuationTokenParameterName = continuationTokenParameterName; _responseContinuationTokenLimitInKbParameterName = responseContinuationTokenLimitInKbParameterName; - var partitionKey = selectExpression.GetPartitionKeyValue(cosmosQueryContext.ParameterValues); - if (partitionKey != PartitionKey.None - && partitionKeyValueFromExtension != PartitionKey.None - && !partitionKeyValueFromExtension.Equals(partitionKey)) - { - throw new InvalidOperationException(CosmosStrings.PartitionKeyMismatch(partitionKeyValueFromExtension, partitionKey)); - } - - _cosmosPartitionKeyValue = partitionKey != PartitionKey.None ? partitionKey : partitionKeyValueFromExtension; - _cosmosContainer = cosmosContainer; + _cosmosContainer = rootEntityType.GetContainer() + ?? throw new UnreachableException("Root entity type without a Cosmos container."); + _cosmosPartitionKey = GeneratePartitionKey( + rootEntityType, partitionKeyPropertyValues, _cosmosQueryContext.ParameterValues); } public IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = default) @@ -96,7 +90,7 @@ private sealed class AsyncEnumerator : IAsyncEnumerator> private readonly Func _shaper; private readonly Type _contextType; private readonly string _cosmosContainer; - private readonly PartitionKey _cosmosPartitionKeyValue; + private readonly PartitionKey _cosmosPartitionKey; private readonly IDiagnosticsLogger _queryLogger; private readonly IDiagnosticsLogger _commandLogger; private readonly bool _standAloneStateManager; @@ -114,7 +108,7 @@ public AsyncEnumerator(PagingQueryingEnumerable queryingEnumerable, Cancellat _shaper = queryingEnumerable._shaper; _contextType = queryingEnumerable._contextType; _cosmosContainer = queryingEnumerable._cosmosContainer; - _cosmosPartitionKeyValue = queryingEnumerable._cosmosPartitionKeyValue; + _cosmosPartitionKey = queryingEnumerable._cosmosPartitionKey; _queryLogger = queryingEnumerable._queryLogger; _commandLogger = queryingEnumerable._commandLogger; _standAloneStateManager = queryingEnumerable._standAloneStateManager; @@ -158,13 +152,13 @@ public async ValueTask MoveNextAsync() ResponseContinuationTokenLimitInKb = responseContinuationTokenLimitInKb }; - if (_cosmosPartitionKeyValue != PartitionKey.None) + if (_cosmosPartitionKey != PartitionKey.None) { - queryRequestOptions.PartitionKey = _cosmosPartitionKeyValue; + queryRequestOptions.PartitionKey = _cosmosPartitionKey; } var cosmosClient = _cosmosQueryContext.CosmosClient; - _commandLogger.ExecutingSqlQuery(_cosmosContainer, _cosmosPartitionKeyValue, sqlQuery); + _commandLogger.ExecutingSqlQuery(_cosmosContainer, _cosmosPartitionKey, sqlQuery); _cosmosQueryContext.InitializeStateManager(_standAloneStateManager); var results = new List(maxItemCount); @@ -182,7 +176,7 @@ public async ValueTask MoveNextAsync() responseMessage.Headers.RequestCharge, responseMessage.Headers.ActivityId, _cosmosContainer, - _cosmosPartitionKeyValue, + _cosmosPartitionKey, sqlQuery); responseMessage.EnsureSuccessStatusCode(); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs index 705ef00ea79..24e7d4fba29 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs @@ -5,7 +5,6 @@ using System.Collections; using System.Text; -using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Newtonsoft.Json.Linq; @@ -28,7 +27,7 @@ private sealed class QueryingEnumerable : IEnumerable, IAsyncEnumerable private readonly IQuerySqlGeneratorFactory _querySqlGeneratorFactory; private readonly Type _contextType; private readonly string _cosmosContainer; - private readonly PartitionKey _cosmosPartitionKeyValue; + private readonly PartitionKey _cosmosPartitionKey; private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; private readonly bool _threadSafetyChecksEnabled; @@ -40,8 +39,8 @@ public QueryingEnumerable( SelectExpression selectExpression, Func shaper, Type contextType, - string cosmosContainer, - PartitionKey partitionKeyValueFromExtension, + IEntityType rootEntityType, + List partitionKeyPropertyValues, bool standAloneStateManager, bool threadSafetyChecksEnabled) { @@ -55,16 +54,10 @@ public QueryingEnumerable( _standAloneStateManager = standAloneStateManager; _threadSafetyChecksEnabled = threadSafetyChecksEnabled; - var partitionKey = selectExpression.GetPartitionKeyValue(cosmosQueryContext.ParameterValues); - if (partitionKey != PartitionKey.None - && partitionKeyValueFromExtension != PartitionKey.None - && !partitionKeyValueFromExtension.Equals(partitionKey)) - { - throw new InvalidOperationException(CosmosStrings.PartitionKeyMismatch(partitionKeyValueFromExtension, partitionKey)); - } - - _cosmosPartitionKeyValue = partitionKey != PartitionKey.None ? partitionKey : partitionKeyValueFromExtension; - _cosmosContainer = cosmosContainer; + _cosmosContainer = rootEntityType.GetContainer() + ?? throw new UnreachableException("Root entity type without a Cosmos container."); + _cosmosPartitionKey = GeneratePartitionKey( + rootEntityType, partitionKeyPropertyValues, _cosmosQueryContext.ParameterValues); } public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) @@ -113,7 +106,7 @@ private sealed class Enumerator : IEnumerator private readonly Func _shaper; private readonly Type _contextType; private readonly string _cosmosContainer; - private readonly PartitionKey _cosmosPartitionKeyValue; + private readonly PartitionKey _cosmosPartitionKey; private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; private readonly IConcurrencyDetector _concurrencyDetector; @@ -128,7 +121,7 @@ public Enumerator(QueryingEnumerable queryingEnumerable) _shaper = queryingEnumerable._shaper; _contextType = queryingEnumerable._contextType; _cosmosContainer = queryingEnumerable._cosmosContainer; - _cosmosPartitionKeyValue = queryingEnumerable._cosmosPartitionKeyValue; + _cosmosPartitionKey = queryingEnumerable._cosmosPartitionKey; _queryLogger = queryingEnumerable._queryLogger; _standAloneStateManager = queryingEnumerable._standAloneStateManager; _exceptionDetector = _cosmosQueryContext.ExceptionDetector; @@ -156,7 +149,7 @@ public bool MoveNext() EntityFrameworkMetricsData.ReportQueryExecuting(); _enumerator = _cosmosQueryContext.CosmosClient - .ExecuteSqlQuery(_cosmosContainer, _cosmosPartitionKeyValue, sqlQuery) + .ExecuteSqlQuery(_cosmosContainer, _cosmosPartitionKey, sqlQuery) .GetEnumerator(); _cosmosQueryContext.InitializeStateManager(_standAloneStateManager); } @@ -202,7 +195,7 @@ private sealed class AsyncEnumerator : IAsyncEnumerator private readonly Func _shaper; private readonly Type _contextType; private readonly string _cosmosContainer; - private readonly PartitionKey _cosmosPartitionKeyValue; + private readonly PartitionKey _cosmosPartitionKey; private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; private readonly CancellationToken _cancellationToken; @@ -218,7 +211,7 @@ public AsyncEnumerator(QueryingEnumerable queryingEnumerable, CancellationTok _shaper = queryingEnumerable._shaper; _contextType = queryingEnumerable._contextType; _cosmosContainer = queryingEnumerable._cosmosContainer; - _cosmosPartitionKeyValue = queryingEnumerable._cosmosPartitionKeyValue; + _cosmosPartitionKey = queryingEnumerable._cosmosPartitionKey; _queryLogger = queryingEnumerable._queryLogger; _standAloneStateManager = queryingEnumerable._standAloneStateManager; _exceptionDetector = _cosmosQueryContext.ExceptionDetector; @@ -244,7 +237,7 @@ public async ValueTask MoveNextAsync() EntityFrameworkMetricsData.ReportQueryExecuting(); _enumerator = _cosmosQueryContext.CosmosClient - .ExecuteSqlQueryAsync(_cosmosContainer, _cosmosPartitionKeyValue, sqlQuery) + .ExecuteSqlQueryAsync(_cosmosContainer, _cosmosPartitionKey, sqlQuery) .GetAsyncEnumerator(_cancellationToken); _cosmosQueryContext.InitializeStateManager(_standAloneStateManager); } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs index de9dff32bdf..2aa09cd6614 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs @@ -6,7 +6,6 @@ using System.Collections; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; -using Microsoft.EntityFrameworkCore.Internal; using Newtonsoft.Json.Linq; namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; @@ -22,8 +21,10 @@ public partial class CosmosShapedQueryCompilingExpressionVisitor private sealed class ReadItemQueryingEnumerable : IEnumerable, IAsyncEnumerable, IQueryingEnumerable { private readonly CosmosQueryContext _cosmosQueryContext; + private readonly IEntityType _rootEntityType; private readonly string _cosmosContainer; private readonly ReadItemInfo _readItemInfo; + private readonly PartitionKey _cosmosPartitionKey; private readonly Func _shaper; private readonly Type _contextType; private readonly IDiagnosticsLogger _queryLogger; @@ -32,7 +33,8 @@ private sealed class ReadItemQueryingEnumerable : IEnumerable, IAsyncEnume public ReadItemQueryingEnumerable( CosmosQueryContext cosmosQueryContext, - string cosmosContainer, + IEntityType rootEntityType, + List partitionKeyPropertyValues, ReadItemInfo readItemInfo, Func shaper, Type contextType, @@ -40,13 +42,18 @@ public ReadItemQueryingEnumerable( bool threadSafetyChecksEnabled) { _cosmosQueryContext = cosmosQueryContext; - _cosmosContainer = cosmosContainer; + _rootEntityType = rootEntityType; _readItemInfo = readItemInfo; _shaper = shaper; _contextType = contextType; _queryLogger = _cosmosQueryContext.QueryLogger; _standAloneStateManager = standAloneStateManager; _threadSafetyChecksEnabled = threadSafetyChecksEnabled; + + _cosmosContainer = rootEntityType.GetContainer() + ?? throw new UnreachableException("Root entity type without a Cosmos container."); + _cosmosPartitionKey = GeneratePartitionKey( + rootEntityType, partitionKeyPropertyValues, _cosmosQueryContext.ParameterValues); } public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) @@ -61,60 +68,24 @@ IEnumerator IEnumerable.GetEnumerator() public string ToQueryString() { TryGetResourceId(out var resourceId); - TryGetPartitionKey(out var partitionKey); - return CosmosStrings.NoReadItemQueryString(resourceId, partitionKey); - } - - private bool TryGetPartitionKey(out PartitionKey partitionKeyValue) - { - var properties = _readItemInfo.EntityType.GetPartitionKeyProperties(); - if (!properties.Any()) - { - partitionKeyValue = PartitionKey.None; - return true; - } - - var builder = new PartitionKeyBuilder(); - foreach (var property in properties) - { - if (TryGetParameterValue(property, out var value)) - { - if (value == null) - { - partitionKeyValue = PartitionKey.Null; - return false; - } - builder.Add(value, property); - } - } - - partitionKeyValue = builder.Build(); - - return true; + return CosmosStrings.NoReadItemQueryString(resourceId, _cosmosPartitionKey); } private bool TryGetResourceId(out string resourceId) { - var entityType = _readItemInfo.EntityType; - var jsonIdDefinition = entityType.GetJsonIdDefinition(); + var jsonIdDefinition = _rootEntityType.GetJsonIdDefinition(); Check.DebugAssert(jsonIdDefinition != null, "Should not be using this enumerable if not using ReadItem, which needs an id definition."); var values = new List(jsonIdDefinition.Properties.Count); foreach (var property in jsonIdDefinition.Properties) { - if (!TryGetParameterValue(property, out var value)) + var value = _readItemInfo.PropertyValues[property] switch { - var discriminatorProperty = entityType.FindDiscriminatorProperty(); - if (discriminatorProperty == property) - { - value = entityType.GetDiscriminatorValue(); - } - else - { - Check.DebugFail("Parameters should cover all properties or we should not be using ReadItem."); - } - } + SqlParameterExpression { Name: var parameterName } => _cosmosQueryContext.ParameterValues[parameterName], + SqlConstantExpression { Value: var constantValue } => constantValue, + _ => throw new UnreachableException() + }; values.Add(value); } @@ -128,17 +99,11 @@ private bool TryGetResourceId(out string resourceId) return true; } - private bool TryGetParameterValue(IProperty property, out object value) - { - value = null; - return _readItemInfo.PropertyParameters.TryGetValue(property, out var parameterName) - && _cosmosQueryContext.ParameterValues.TryGetValue(parameterName, out value); - } - private sealed class Enumerator : IEnumerator, IAsyncEnumerator { private readonly CosmosQueryContext _cosmosQueryContext; private readonly string _cosmosContainer; + private readonly PartitionKey _cosmosPartitionKey; private readonly Func _shaper; private readonly Type _contextType; private readonly IDiagnosticsLogger _queryLogger; @@ -155,6 +120,7 @@ public Enumerator(ReadItemQueryingEnumerable readItemEnumerable, Cancellation { _cosmosQueryContext = readItemEnumerable._cosmosQueryContext; _cosmosContainer = readItemEnumerable._cosmosContainer; + _cosmosPartitionKey = readItemEnumerable._cosmosPartitionKey; _shaper = readItemEnumerable._shaper; _contextType = readItemEnumerable._contextType; _queryLogger = readItemEnumerable._queryLogger; @@ -189,16 +155,11 @@ public bool MoveNext() throw new InvalidOperationException(CosmosStrings.ResourceIdMissing); } - if (!_readItemEnumerable.TryGetPartitionKey(out var partitionKeyValue)) - { - throw new InvalidOperationException(CosmosStrings.PartitionKeyMissing); - } - EntityFrameworkMetricsData.ReportQueryExecuting(); _item = _cosmosQueryContext.CosmosClient.ExecuteReadItem( _cosmosContainer, - partitionKeyValue, + _cosmosPartitionKey, resourceId); return ShapeResult(); @@ -234,16 +195,11 @@ public async ValueTask MoveNextAsync() throw new InvalidOperationException(CosmosStrings.ResourceIdMissing); } - if (!_readItemEnumerable.TryGetPartitionKey(out var partitionKeyValue)) - { - throw new InvalidOperationException(CosmosStrings.PartitionKeyMissing); - } - EntityFrameworkMetricsData.ReportQueryExecuting(); _item = await _cosmosQueryContext.CosmosClient.ExecuteReadItemAsync( _cosmosContainer, - partitionKeyValue, + _cosmosPartitionKey, resourceId, _cancellationToken) .ConfigureAwait(false); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.Util.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.Util.cs new file mode 100644 index 00000000000..03cab5e72bb --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.Util.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Internal; + +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 partial class CosmosShapedQueryCompilingExpressionVisitor +{ + private static PartitionKey GeneratePartitionKey( + IEntityType rootEntityType, + List partitionKeyPropertyValues, + IReadOnlyDictionary parameterValues) + { + if (partitionKeyPropertyValues.Count == 0) + { + return PartitionKey.None; + } + + var builder = new PartitionKeyBuilder(); + + var partitionKeyProperties = rootEntityType.GetPartitionKeyProperties(); + + int i; + for (i = 0; i < partitionKeyPropertyValues.Count; i++) + { + if (i >= partitionKeyProperties.Count) + { + break; + } + + var property = partitionKeyProperties[i]; + + switch (partitionKeyPropertyValues[i]) + { + case SqlConstantExpression constant: + builder.Add(constant.Value, property); + continue; + + // If WithPartitionKey() was used, its second argument is a params object[] array, which gets parameterized as a single + // parameter. Extract the object[] and iterate over the values within here. + case SqlParameterExpression parameter when parameter.Type == typeof(object[]): + { + if (!parameterValues.TryGetValue(parameter.Name, out var value) + || value is not object[] remainingValuesArray) + { + throw new UnreachableException("Couldn't find partition key parameter value"); + } + + for (var j = 0; j < remainingValuesArray.Length; j++, i++) + { + builder.Add(remainingValuesArray[j], partitionKeyProperties[i]); + } + + goto End; + } + + case SqlParameterExpression parameter: + { + builder.Add( + parameterValues.TryGetValue(parameter.Name, out var value) + ? value + : throw new UnreachableException("Couldn't find partition key parameter value"), + property); + continue; + } + + default: + throw new UnreachableException(); + } + } + + End: + if (i != partitionKeyProperties.Count) + { + throw new InvalidOperationException( + CosmosStrings.IncorrectPartitionKeyNumber(rootEntityType.DisplayName(), i, partitionKeyProperties.Count)); + } + + return builder.Build(); + } +} diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs index 54dc03462c9..8f768739a3c 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs @@ -25,9 +25,6 @@ public partial class CosmosShapedQueryCompilingExpressionVisitor( private readonly Type _contextType = cosmosQueryCompilationContext.ContextType; private readonly bool _threadSafetyChecksEnabled = dependencies.CoreSingletonOptions.AreThreadSafetyChecksEnabled; - private readonly PartitionKey _partitionKeyValueFromExtension = cosmosQueryCompilationContext.PartitionKeyValueFromExtension - ?? PartitionKey.None; - /// /// 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 @@ -36,9 +33,9 @@ public partial class CosmosShapedQueryCompilingExpressionVisitor( /// protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQueryExpression) { - if (cosmosQueryCompilationContext.CosmosContainer is null) + if (cosmosQueryCompilationContext.RootEntityType is not IEntityType rootEntityType) { - throw new UnreachableException("No Cosmos container was set during query processing."); + throw new UnreachableException("No root entity type was set during query processing."); } var jObjectParameter = Parameter(typeof(JObject), "jObject"); @@ -82,7 +79,7 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery var cosmosQueryContextConstant = Convert(QueryCompilationContext.QueryContextParameter, typeof(CosmosQueryContext)); var shaperConstant = Constant(shaperLambda.Compile()); var contextTypeConstant = Constant(_contextType); - var containerConstant = Constant(cosmosQueryCompilationContext.CosmosContainer); + var rootEntityTypeConstant = Constant(rootEntityType); var threadSafetyConstant = Constant(_threadSafetyChecksEnabled); var standAloneStateManagerConstant = Constant( QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution); @@ -92,9 +89,10 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery return selectExpression switch { { ReadItemInfo: ReadItemInfo readItemInfo } => New( - typeof(ReadItemQueryingEnumerable<>).MakeGenericType(readItemInfo.Type).GetConstructors()[0], + typeof(ReadItemQueryingEnumerable<>).MakeGenericType(shaperLambda.ReturnType).GetConstructors()[0], cosmosQueryContextConstant, - containerConstant, + rootEntityTypeConstant, + Constant(cosmosQueryCompilationContext.PartitionKeyPropertyValues), Constant(readItemInfo), shaperConstant, contextTypeConstant, @@ -109,8 +107,8 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery Constant(selectExpression), shaperConstant, contextTypeConstant, - containerConstant, - Constant(_partitionKeyValueFromExtension, typeof(PartitionKey)), + rootEntityTypeConstant, + Constant(cosmosQueryCompilationContext.PartitionKeyPropertyValues), standAloneStateManagerConstant, threadSafetyConstant, Constant(maxItemCount.Name), @@ -124,8 +122,8 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery Constant(selectExpression), shaperConstant, contextTypeConstant, - containerConstant, - Constant(_partitionKeyValueFromExtension, typeof(PartitionKey)), + rootEntityTypeConstant, + Constant(cosmosQueryCompilationContext.PartitionKeyPropertyValues), standAloneStateManagerConstant, threadSafetyConstant) }; diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index d3303c9d3cf..4882efcd955 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -77,28 +77,31 @@ protected virtual void AddTranslationErrorDetails(string details) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual SqlExpression? Translate(Expression expression) + public virtual SqlExpression? Translate(Expression expression, bool applyDefaultTypeMapping = true) { TranslationErrorDetails = null; - return TranslateInternal(expression); + return TranslateInternal(expression, applyDefaultTypeMapping); } - private SqlExpression? TranslateInternal(Expression expression) + private SqlExpression? TranslateInternal(Expression expression, bool applyDefaultTypeMapping = true) { var result = Visit(expression); if (result is SqlExpression translation) { - translation = sqlExpressionFactory.ApplyDefaultTypeMapping(translation); - - if (translation.TypeMapping == null) + if (applyDefaultTypeMapping) { - // The return type is not-mappable hence return null - return null; - } + translation = sqlExpressionFactory.ApplyDefaultTypeMapping(translation); + + if (translation.TypeMapping == null) + { + // The return type is not-mappable hence return null + return null; + } - _sqlVerifyingExpressionVisitor.Visit(translation); + _sqlVerifyingExpressionVisitor.Visit(translation); + } return translation; } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs index 2b516cdd4ce..5ed43e4dacb 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs @@ -67,7 +67,7 @@ private Expression VisitSelect(SelectExpression selectExpression) var offset = (SqlExpression?)Visit(selectExpression.Offset); return changed - ? selectExpression.Update(projections, sources, predicate, orderings, limit, offset) + ? selectExpression.Update(sources, predicate, projections, orderings, offset, limit) : selectExpression; } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ReadItemInfo.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ReadItemInfo.cs index f6e8178fdd4..9a93540830f 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ReadItemInfo.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ReadItemInfo.cs @@ -18,7 +18,7 @@ public class ReadItemInfo /// any release. You should only use it directly in 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 Type Type { get; } + public virtual IDictionary PropertyValues { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -26,29 +26,6 @@ public class ReadItemInfo /// any release. You should only use it directly in 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 IEntityType EntityType { get; } - - /// - /// 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 IDictionary PropertyParameters { get; } - - /// - /// 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 ReadItemInfo( - IEntityType entityType, - IDictionary propertyParameters, - Type type) - { - Type = type; - EntityType = entityType; - PropertyParameters = propertyParameters; - } + public ReadItemInfo(IDictionary propertyValues) + => PropertyValues = propertyValues; } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs index 3eea7d26e9e..fc4a2cd3c19 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs @@ -39,13 +39,21 @@ public sealed class SelectExpression : Expression, IPrintableExpression /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public SelectExpression( - List projections, List sources, - List orderings) + SqlExpression? predicate, + List projections, + bool distinct, + List orderings, + SqlExpression? offset, + SqlExpression? limit) { - _projection = projections; _sources = sources; + Predicate = predicate; + _projection = projections; + IsDistinct = distinct; _orderings = orderings; + Offset = offset; + Limit = limit; } /// @@ -63,11 +71,10 @@ public SelectExpression(Expression projection) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public SelectExpression(SourceExpression source, Expression projection, ReadItemInfo? readItemInfo = null) + public SelectExpression(SourceExpression source, Expression projection) { _sources.Add(source); _projectionMapping[new ProjectionMember()] = projection; - ReadItemInfo = readItemInfo; } /// @@ -87,9 +94,13 @@ public static SelectExpression CreateForCollection(Expression sourceExpression, if (!SourceExpression.IsCompatible(sourceExpression)) { sourceExpression = new SelectExpression( - [new ProjectionExpression(sourceExpression, null!)], sources: [], - orderings: []) + predicate: null, + [new ProjectionExpression(sourceExpression, null!)], + distinct: false, + orderings: [], + offset: null, + limit: null) { UsesSingleValueProjection = true }; @@ -225,6 +236,7 @@ ParameterExpression parameterExpression when parameterValues.TryGetValue(paramet => value, _ => null }; + builder.Add(rawKeyValue, tuple.Property); } @@ -589,13 +601,9 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) if (changed) { - var newSelectExpression = new SelectExpression(projections, sources, orderings) + var newSelectExpression = new SelectExpression(sources, predicate, projections, IsDistinct, orderings, offset, limit) { _projectionMapping = projectionMapping, - Predicate = predicate, - Offset = offset, - Limit = limit, - IsDistinct = IsDistinct, UsesSingleValueProjection = UsesSingleValueProjection }; @@ -612,12 +620,12 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public SelectExpression Update( - List projections, List sources, SqlExpression? predicate, + List projections, List orderings, - SqlExpression? limit, - SqlExpression? offset) + SqlExpression? offset, + SqlExpression? limit) { var projectionMapping = new Dictionary(); foreach (var (projectionMember, expression) in _projectionMapping) @@ -625,18 +633,28 @@ public SelectExpression Update( projectionMapping[projectionMember] = expression; } - return new SelectExpression(projections, sources, orderings) + return new SelectExpression(sources, predicate, projections, IsDistinct, orderings, offset, limit) { _projectionMapping = projectionMapping, - Predicate = predicate, - Offset = offset, - Limit = limit, - IsDistinct = IsDistinct, UsesSingleValueProjection = UsesSingleValueProjection, ReadItemInfo = ReadItemInfo }; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SelectExpression WithReadItemInfo(ReadItemInfo readItemInfo) + => new(Sources.ToList(), Predicate, Projection.ToList(), IsDistinct, Orderings.ToList(), Offset, Limit) + { + _projectionMapping = _projectionMapping, + UsesSingleValueProjection = UsesSingleValueProjection, + ReadItemInfo = readItemInfo + }; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -651,13 +669,9 @@ public SelectExpression WithSingleValueProjection() projectionMapping[projectionMember] = expression; } - return new SelectExpression(Projection.ToList(), Sources.ToList(), Orderings.ToList()) + return new SelectExpression(Sources.ToList(), Predicate, Projection.ToList(), IsDistinct, Orderings.ToList(), Offset, Limit) { _projectionMapping = projectionMapping, - Predicate = Predicate, - Offset = Offset, - Limit = Limit, - IsDistinct = IsDistinct, UsesSingleValueProjection = true }; } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlParameterExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlParameterExpression.cs index c10c4725f8c..f8f7dfa640a 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SqlParameterExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlParameterExpression.cs @@ -61,7 +61,7 @@ public override bool Equals(object? obj) && Equals(sqlParameterExpression)); private bool Equals(SqlParameterExpression sqlParameterExpression) - => base.Equals(sqlParameterExpression) && Name != sqlParameterExpression.Name; + => base.Equals(sqlParameterExpression) && Name == sqlParameterExpression.Name; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs index b1abb161f11..f427a381f8a 100644 --- a/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs +++ b/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs @@ -45,7 +45,12 @@ public interface ISqlExpressionFactory /// any release. You should only use it directly in 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? MakeBinary(ExpressionType operatorType, SqlExpression left, SqlExpression right, CoreTypeMapping? typeMapping); + SqlExpression? MakeBinary( + ExpressionType operatorType, + SqlExpression left, + SqlExpression right, + CoreTypeMapping? typeMapping, + SqlExpression? existingExpr = null); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs index ba466916174..b4c2d0feea0 100644 --- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs +++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs @@ -319,8 +319,17 @@ var t when t.TryGetSequenceType() != typeof(object) => t, ExpressionType operatorType, SqlExpression left, SqlExpression right, - CoreTypeMapping? typeMapping) + CoreTypeMapping? typeMapping, + SqlExpression? existingExpr = null) { + switch (operatorType) + { + case ExpressionType.AndAlso: + return ApplyTypeMapping(AndAlso(left, right, existingExpr), typeMapping); + case ExpressionType.OrElse: + return ApplyTypeMapping(OrElse(left, right, existingExpr), typeMapping); + } + if (!SqlBinaryExpression.IsValidOperator(operatorType)) { return null; @@ -335,8 +344,6 @@ var t when t.TryGetSequenceType() != typeof(object) => t, case ExpressionType.LessThan: case ExpressionType.LessThanOrEqual: case ExpressionType.NotEqual: - case ExpressionType.AndAlso: - case ExpressionType.OrElse: returnType = typeof(bool); break; } @@ -415,7 +422,43 @@ public virtual SqlExpression LessThanOrEqual(SqlExpression left, SqlExpression r /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual SqlExpression AndAlso(SqlExpression left, SqlExpression right) - => MakeBinary(ExpressionType.AndAlso, left, right, null)!; + => AndAlso(left, right, null); + + private SqlExpression AndAlso(SqlExpression left, SqlExpression right, SqlExpression? existingExpr) + { + // false && x -> false + // x && true -> x + // x && x -> x + if (left is SqlConstantExpression { Value: false } + || right is SqlConstantExpression { Value: true } + || left.Equals(right)) + { + return left; + } + // true && x -> x + // x && false -> false + if (left is SqlConstantExpression { Value: true } || right is SqlConstantExpression { Value: false }) + { + return right; + } + // x is null && x is not null -> false + // x is not null && x is null -> false + if (left is SqlUnaryExpression { OperatorType: ExpressionType.Equal or ExpressionType.NotEqual } leftUnary + && right is SqlUnaryExpression { OperatorType: ExpressionType.Equal or ExpressionType.NotEqual } rightUnary + && leftUnary.Operand.Equals(rightUnary.Operand)) + { + // the case in which left and right are the same expression is handled above + return Constant(false); + } + if (existingExpr is SqlBinaryExpression { OperatorType: ExpressionType.AndAlso } binaryExpr + && left == binaryExpr.Left + && right == binaryExpr.Right) + { + return existingExpr; + } + + return new SqlBinaryExpression(ExpressionType.AndAlso, left, right, typeof(bool), null); + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -424,7 +467,44 @@ public virtual SqlExpression AndAlso(SqlExpression left, SqlExpression right) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual SqlExpression OrElse(SqlExpression left, SqlExpression right) - => MakeBinary(ExpressionType.OrElse, left, right, null)!; + => OrElse(left, right, null); + + private SqlExpression OrElse(SqlExpression left, SqlExpression right, SqlExpression? existingExpr) + { + // true || x -> true + // x || false -> x + // x || x -> x + if (left is SqlConstantExpression { Value: true } + || right is SqlConstantExpression { Value: false } + || left.Equals(right)) + { + return left; + } + // false || x -> x + // x || true -> true + if (left is SqlConstantExpression { Value: false } + || right is SqlConstantExpression { Value: true }) + { + return right; + } + // x is null || x is not null -> true + // x is not null || x is null -> true + if (left is SqlUnaryExpression { OperatorType: ExpressionType.Equal or ExpressionType.NotEqual } leftUnary + && right is SqlUnaryExpression { OperatorType: ExpressionType.Equal or ExpressionType.NotEqual } rightUnary + && leftUnary.Operand.Equals(rightUnary.Operand)) + { + // the case in which left and right are the same expression is handled above + return Constant(true); + } + if (existingExpr is SqlBinaryExpression { OperatorType: ExpressionType.OrElse } binaryExpr + && left == binaryExpr.Left + && right == binaryExpr.Right) + { + return existingExpr; + } + + return new SqlBinaryExpression(ExpressionType.OrElse, left, right, typeof(bool), null); + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index d580a748e1f..a4d4e904429 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -454,7 +454,7 @@ public virtual SqlExpression LessThanOrEqual(SqlExpression left, SqlExpression r /// public virtual SqlExpression AndAlso(SqlExpression left, SqlExpression right) - => MakeBinary(ExpressionType.AndAlso, left, right, null)!; + => AndAlso(left, right, null); private SqlExpression AndAlso(SqlExpression left, SqlExpression right, SqlExpression? existingExpr) { @@ -469,36 +469,34 @@ private SqlExpression AndAlso(SqlExpression left, SqlExpression right, SqlExpres } // true && x -> x // x && false -> false - else if (left is SqlConstantExpression { Value: true } || right is SqlConstantExpression { Value: false }) + if (left is SqlConstantExpression { Value: true } || right is SqlConstantExpression { Value: false }) { return right; } // x is null && x is not null -> false // x is not null && x is null -> false - else if (left is SqlUnaryExpression { OperatorType: ExpressionType.Equal or ExpressionType.NotEqual } leftUnary + if (left is SqlUnaryExpression { OperatorType: ExpressionType.Equal or ExpressionType.NotEqual } leftUnary && right is SqlUnaryExpression { OperatorType: ExpressionType.Equal or ExpressionType.NotEqual } rightUnary && leftUnary.Operand.Equals(rightUnary.Operand)) { // the case in which left and right are the same expression is handled above return Constant(false); } - else if (existingExpr is SqlBinaryExpression { OperatorType: ExpressionType.AndAlso } binaryExpr + if (existingExpr is SqlBinaryExpression { OperatorType: ExpressionType.AndAlso } binaryExpr && left == binaryExpr.Left && right == binaryExpr.Right) { return existingExpr; } - else - { - return new SqlBinaryExpression(ExpressionType.AndAlso, left, right, typeof(bool), null); - } + + return new SqlBinaryExpression(ExpressionType.AndAlso, left, right, typeof(bool), null); } /// public virtual SqlExpression OrElse(SqlExpression left, SqlExpression right) - => MakeBinary(ExpressionType.OrElse, left, right, null)!; + => OrElse(left, right, null); - private SqlExpression OrElse(SqlExpression left, SqlExpression right, SqlExpression? existingExpr = null) + private SqlExpression OrElse(SqlExpression left, SqlExpression right, SqlExpression? existingExpr) { // true || x -> true // x || false -> x @@ -511,30 +509,28 @@ private SqlExpression OrElse(SqlExpression left, SqlExpression right, SqlExpress } // false || x -> x // x || true -> true - else if (left is SqlConstantExpression { Value: false } + if (left is SqlConstantExpression { Value: false } || right is SqlConstantExpression { Value: true }) { return right; } // x is null || x is not null -> true // x is not null || x is null -> true - else if (left is SqlUnaryExpression { OperatorType: ExpressionType.Equal or ExpressionType.NotEqual } leftUnary + if (left is SqlUnaryExpression { OperatorType: ExpressionType.Equal or ExpressionType.NotEqual } leftUnary && right is SqlUnaryExpression { OperatorType: ExpressionType.Equal or ExpressionType.NotEqual } rightUnary && leftUnary.Operand.Equals(rightUnary.Operand)) { // the case in which left and right are the same expression is handled above return Constant(true); } - else if (existingExpr is SqlBinaryExpression { OperatorType: ExpressionType.OrElse } binaryExpr + if (existingExpr is SqlBinaryExpression { OperatorType: ExpressionType.OrElse } binaryExpr && left == binaryExpr.Left && right == binaryExpr.Right) { return existingExpr; } - else - { - return new SqlBinaryExpression(ExpressionType.OrElse, left, right, typeof(bool), null); - } + + return new SqlBinaryExpression(ExpressionType.OrElse, left, right, typeof(bool), null); } /// diff --git a/src/EFCore/Query/QueryCompilationContext.cs b/src/EFCore/Query/QueryCompilationContext.cs index 286dc70c201..c30e04ff448 100644 --- a/src/EFCore/Query/QueryCompilationContext.cs +++ b/src/EFCore/Query/QueryCompilationContext.cs @@ -301,7 +301,7 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) private sealed class RuntimeParameterConstantLifter(ILiftableConstantFactory liftableConstantFactory) : ExpressionVisitor { - private readonly static MethodInfo ComplexPropertyListElementAddMethod = typeof(List).GetMethod(nameof(List.Add))!; + private static readonly MethodInfo ComplexPropertyListElementAddMethod = typeof(List).GetMethod(nameof(List.Add))!; protected override Expression VisitConstant(ConstantExpression constantExpression) { diff --git a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs index f5322a31a1f..7f48d4d6559 100644 --- a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs @@ -119,7 +119,11 @@ protected override Expression VisitExtension(Expression extensionExpression) // SQL Server TemporalQueryRootExpression. if (queryRootExpression.GetType() == typeof(EntityQueryRootExpression)) { - return CreateShapedQueryExpression(((EntityQueryRootExpression)extensionExpression).EntityType); + var shapedQuery = CreateShapedQueryExpression(((EntityQueryRootExpression)extensionExpression).EntityType); + if (shapedQuery is not null) + { + return shapedQuery; + } } _untranslatedExpression = queryRootExpression; @@ -586,7 +590,7 @@ protected virtual Expression MarkShaperNullable(Expression shaperExpression) /// /// The entity type. /// A shaped query expression for the given entity type. - protected abstract ShapedQueryExpression CreateShapedQueryExpression(IEntityType entityType); + protected abstract ShapedQueryExpression? CreateShapedQueryExpression(IEntityType entityType); /// /// Translates method over the given source. diff --git a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs index 110aadaa4d7..03669349a81 100644 --- a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs @@ -531,6 +531,13 @@ public virtual async Task Can_query_and_modify_nested_embedded_types() { var missile = await context.Set().FirstAsync(v => v.Name == "AIM-9M Sidewinder"); + AssertSql( + """ +SELECT c +FROM root c +WHERE (c["Discriminator"] IN ("Vehicle", "PoweredVehicle") AND (c["Name"] = "AIM-9M Sidewinder")) +OFFSET 0 LIMIT 1 +"""); Assert.Equal("Heat-seeking", missile.Operator.Details.Type); missile.Operator.Details.Type = "IR"; diff --git a/test/EFCore.Cosmos.FunctionalTests/FindCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/FindCosmosTest.cs index bf0e875145a..bc3d0a2f63e 100644 --- a/test/EFCore.Cosmos.FunctionalTests/FindCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/FindCosmosTest.cs @@ -209,20 +209,14 @@ public override async Task Find_int_key_from_store_async(CancellationType cancel { await base.Find_int_key_from_store_async(cancellationType); - AssertSql( - """ -ReadItem(None, IntKey|77) -"""); + AssertSql("ReadItem(None, IntKey|77)"); } public override async Task Returns_null_for_int_key_not_in_store_async(CancellationType cancellationType) { await base.Returns_null_for_int_key_not_in_store_async(cancellationType); - AssertSql( - """ -ReadItem(None, IntKey|99) -"""); + AssertSql("ReadItem(None, IntKey|99)"); } public override async Task Find_nullable_int_key_tracked_async(CancellationType cancellationType) @@ -236,20 +230,14 @@ public override async Task Find_nullable_int_key_from_store_async(CancellationTy { await base.Find_nullable_int_key_from_store_async(cancellationType); - AssertSql( - """ -ReadItem(None, NullableIntKey|77) -"""); + AssertSql("ReadItem(None, NullableIntKey|77)"); } public override async Task Returns_null_for_nullable_int_key_not_in_store_async(CancellationType cancellationType) { await base.Returns_null_for_nullable_int_key_not_in_store_async(cancellationType); - AssertSql( - """ -ReadItem(None, NullableIntKey|99) -"""); + AssertSql("ReadItem(None, NullableIntKey|99)"); } public override async Task Find_string_key_tracked_async(CancellationType cancellationType) @@ -263,20 +251,14 @@ public override async Task Find_string_key_from_store_async(CancellationType can { await base.Find_string_key_from_store_async(cancellationType); - AssertSql( - """ -ReadItem(None, StringKey|Cat) -"""); + AssertSql("ReadItem(None, StringKey|Cat)"); } public override async Task Returns_null_for_string_key_not_in_store_async(CancellationType cancellationType) { await base.Returns_null_for_string_key_not_in_store_async(cancellationType); - AssertSql( - """ -ReadItem(None, StringKey|Fox) -"""); + AssertSql("ReadItem(None, StringKey|Fox)"); } public override async Task Find_composite_key_tracked_async(CancellationType cancellationType) @@ -290,20 +272,14 @@ public override async Task Find_composite_key_from_store_async(CancellationType { await base.Find_composite_key_from_store_async(cancellationType); - AssertSql( - """ -ReadItem(None, CompositeKey|77|Dog) -"""); + AssertSql("ReadItem(None, CompositeKey|77|Dog)"); } public override async Task Returns_null_for_composite_key_not_in_store_async(CancellationType cancellationType) { await base.Returns_null_for_composite_key_not_in_store_async(cancellationType); - AssertSql( - """ -ReadItem(None, CompositeKey|77|Fox) -"""); + AssertSql("ReadItem(None, CompositeKey|77|Fox)"); } public override async Task Find_base_type_tracked_async(CancellationType cancellationType) @@ -319,7 +295,12 @@ public override async Task Find_base_type_from_store_async(CancellationType canc AssertSql( """ -ReadItem(None, BaseType|77) +@__p_0='77' + +SELECT c +FROM root c +WHERE (c["Discriminator"] IN ("BaseType", "DerivedType") AND (c["Id"] = @__p_0)) +OFFSET 0 LIMIT 1 """); } @@ -327,9 +308,13 @@ public override async Task Returns_null_for_base_type_not_in_store_async(Cancell { await base.Returns_null_for_base_type_not_in_store_async(cancellationType); - AssertSql( - """ -ReadItem(None, BaseType|99) + AssertSql(""" +@__p_0='99' + +SELECT c +FROM root c +WHERE (c["Discriminator"] IN ("BaseType", "DerivedType") AND (c["Id"] = @__p_0)) +OFFSET 0 LIMIT 1 """); } @@ -344,30 +329,21 @@ public override async Task Find_derived_type_from_store_async(CancellationType c { await base.Find_derived_type_from_store_async(cancellationType); - AssertSql( - """ -ReadItem(None, DerivedType|78) -"""); + AssertSql("ReadItem(None, DerivedType|78)"); } public override async Task Returns_null_for_derived_type_not_in_store_async(CancellationType cancellationType) { await base.Returns_null_for_derived_type_not_in_store_async(cancellationType); - AssertSql( - """ -ReadItem(None, DerivedType|99) -"""); + AssertSql("ReadItem(None, DerivedType|99)"); } public override async Task Find_base_type_using_derived_set_from_store_async(CancellationType cancellationType) { await base.Find_base_type_using_derived_set_from_store_async(cancellationType); - AssertSql( - """ -ReadItem(None, DerivedType|77) -"""); + AssertSql("ReadItem(None, DerivedType|77)"); } public override async Task Find_derived_type_using_base_set_tracked_async(CancellationType cancellationType) @@ -388,20 +364,14 @@ public override async Task Find_shadow_key_from_store_async(CancellationType can { await base.Find_shadow_key_from_store_async(cancellationType); - AssertSql( - """ -ReadItem(None, ShadowKey|77) -"""); + AssertSql("ReadItem(None, ShadowKey|77)"); } public override async Task Returns_null_for_shadow_key_not_in_store_async(CancellationType cancellationType) { await base.Returns_null_for_shadow_key_not_in_store_async(cancellationType); - AssertSql( - """ -ReadItem(None, ShadowKey|99) -"""); + AssertSql("ReadItem(None, ShadowKey|99)"); } public override async Task Returns_null_for_null_key_values_array_async(CancellationType cancellationType) diff --git a/test/EFCore.Cosmos.FunctionalTests/HierarchicalPartitionKeyTest.cs b/test/EFCore.Cosmos.FunctionalTests/HierarchicalPartitionKeyTest.cs index b068d7f40dc..cc5ae4650c0 100644 --- a/test/EFCore.Cosmos.FunctionalTests/HierarchicalPartitionKeyTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/HierarchicalPartitionKeyTest.cs @@ -79,6 +79,7 @@ public async Task Can_query_with_implicit_partition_key_filter() """ SELECT c FROM root c +WHERE ((c["Id"] = 42) OR (c["Name"] = "John Snow")) OFFSET 0 LIMIT 2 """; diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/InheritanceQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/InheritanceQueryCosmosTest.cs index 559016ca746..6b468bf6e68 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/InheritanceQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/InheritanceQueryCosmosTest.cs @@ -155,7 +155,7 @@ public override Task Can_use_of_type_bird(bool async) """ SELECT c FROM root c -WHERE (c["Discriminator"] IN ("Eagle", "Kiwi") AND c["Discriminator"] IN ("Eagle", "Kiwi")) +WHERE c["Discriminator"] IN ("Eagle", "Kiwi") ORDER BY c["Species"] """); }); @@ -185,7 +185,7 @@ public override Task Can_use_of_type_bird_with_projection(bool async) """ SELECT c["EagleId"] FROM root c -WHERE (c["Discriminator"] IN ("Eagle", "Kiwi") AND c["Discriminator"] IN ("Eagle", "Kiwi")) +WHERE c["Discriminator"] IN ("Eagle", "Kiwi") """); }); @@ -199,7 +199,7 @@ public override Task Can_use_of_type_bird_first(bool async) """ SELECT c FROM root c -WHERE (c["Discriminator"] IN ("Eagle", "Kiwi") AND c["Discriminator"] IN ("Eagle", "Kiwi")) +WHERE c["Discriminator"] IN ("Eagle", "Kiwi") ORDER BY c["Species"] OFFSET 0 LIMIT 1 """); @@ -585,7 +585,7 @@ public override Task GetType_in_hierarchy_in_abstract_base_type(bool async) """ SELECT c FROM root c -WHERE (c["Discriminator"] IN ("Eagle", "Kiwi") AND false) +WHERE false """); }); @@ -599,7 +599,7 @@ public override Task GetType_in_hierarchy_in_intermediate_type(bool async) """ SELECT c FROM root c -WHERE (c["Discriminator"] IN ("Eagle", "Kiwi") AND false) +WHERE false """); }); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs index 7b12caaaca7..8d6afb928ae 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs @@ -232,13 +232,7 @@ public override Task Where_Single(bool async) { await base.Where_Single(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI")) -OFFSET 0 LIMIT 2 -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); public override Task FirstOrDefault(bool async) @@ -293,13 +287,7 @@ public override Task SingleOrDefault_Predicate(bool async) { await base.SingleOrDefault_Predicate(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI")) -OFFSET 0 LIMIT 2 -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); public override async Task SingleOrDefault_Throws(bool async) @@ -341,13 +329,7 @@ public override Task Where_SingleOrDefault(bool async) { await base.Where_SingleOrDefault(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI")) -OFFSET 0 LIMIT 2 -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); public override async Task Select_All(bool async) @@ -1245,13 +1227,7 @@ public override Task Single_Predicate(bool async) { await base.Single_Predicate(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI")) -OFFSET 0 LIMIT 2 -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); public override async Task FirstOrDefault_inside_subquery_gets_server_evaluated(bool async) @@ -1300,13 +1276,7 @@ public override Task Last_when_no_order_by(bool async) { await base.Last_when_no_order_by(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI")) -OFFSET 0 LIMIT 1 -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); public override Task LastOrDefault_when_no_order_by(bool async) @@ -1315,13 +1285,7 @@ public override Task LastOrDefault_when_no_order_by(bool async) { await base.LastOrDefault_when_no_order_by(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI")) -OFFSET 0 LIMIT 1 -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); public override Task Last_Predicate(bool async) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs index 8b94e8ddd97..0492b5947c3 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs @@ -1621,12 +1621,7 @@ public override Task Static_string_equals_in_predicate(bool async) { await base.Static_string_equals_in_predicate(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ANATR")) -"""); + AssertSql("ReadItem(None, Customer|ANATR)"); }); public override Task Static_equals_nullable_datetime_compared_to_non_nullable(bool async) @@ -1655,7 +1650,7 @@ public override Task Static_equals_int_compared_to_long(bool async) """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Order") AND false) +WHERE false """); }); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index 4fa47fe12e4..bb174e94fb5 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -87,15 +87,7 @@ public override Task Local_dictionary(bool async) { await base.Local_dictionary(a); - AssertSql( - """ -@__p_0='ALFKI' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = @__p_0)) -OFFSET 0 LIMIT 2 -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); public override Task Entity_equality_self(bool async) @@ -134,15 +126,7 @@ public override Task Entity_equality_local_composite_key(bool async) { await base.Entity_equality_local_composite_key(a); - AssertSql( - """ -@__entity_equality_local_0_OrderID='10248' -@__entity_equality_local_0_ProductID='11' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "OrderDetail") AND ((c["OrderID"] = @__entity_equality_local_0_OrderID) AND (c["ProductID"] = @__entity_equality_local_0_ProductID))) -"""); + AssertSql("ReadItem(None, OrderDetail|10248|11)"); }); public override async Task Join_with_entity_equality_local_on_both_sources(bool async) @@ -173,12 +157,7 @@ public override Task Entity_equality_local_inline_composite_key(bool async) { await base.Entity_equality_local_inline_composite_key(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "OrderDetail") AND ((c["OrderID"] = 10248) AND (c["ProductID"] = 11))) -"""); + AssertSql("ReadItem(None, OrderDetail|10248|11)"); }); public override Task Entity_equality_null(bool async) @@ -1138,104 +1117,117 @@ public override async Task Where_Join_Not_Exists(bool async) public override async Task Join_OrderBy_Count(bool async) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Join_OrderBy_Count(async)); + await AssertTranslationFailedWithDetails( + () => base.Join_OrderBy_Count(async), + CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Order), nameof(Customer))); AssertSql(); } public override async Task Multiple_joins_Where_Order_Any(bool async) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Multiple_joins_Where_Order_Any(async)); + await AssertTranslationFailedWithDetails( + () => base.Multiple_joins_Where_Order_Any(async), + CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Order), nameof(Customer))); AssertSql(); } public override async Task Where_join_select(bool async) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Where_join_select(async)); + await AssertTranslationFailedWithDetails( + () => base.Where_join_select(async), + CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Order), nameof(Customer))); AssertSql(); } public override async Task Where_orderby_join_select(bool async) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Where_orderby_join_select(async)); + await AssertTranslationFailedWithDetails( + () => base.Where_orderby_join_select(async), + CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Order), nameof(Customer))); AssertSql(); } public override async Task Where_join_orderby_join_select(bool async) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Where_join_orderby_join_select(async)); + await AssertTranslationFailedWithDetails( + () => base.Where_join_orderby_join_select(async), + CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Order), nameof(Customer))); AssertSql(); } public override async Task Where_select_many(bool async) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Where_select_many(async)); + await AssertTranslationFailedWithDetails( + () => base.Where_select_many(async), + CosmosStrings.NonCorrelatedSubqueriesNotSupported); AssertSql(); } public override async Task Where_orderby_select_many(bool async) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.Where_orderby_select_many(async)); + await AssertTranslationFailedWithDetails( + () => base.Where_orderby_select_many(async), + CosmosStrings.NonCorrelatedSubqueriesNotSupported); AssertSql(); } public override async Task SelectMany_cartesian_product_with_ordering(bool async) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.SelectMany_cartesian_product_with_ordering(async)); + await AssertTranslationFailedWithDetails( + () => base.SelectMany_cartesian_product_with_ordering(async), + CosmosStrings.NonCorrelatedSubqueriesNotSupported); AssertSql(); } public override async Task SelectMany_Joined_DefaultIfEmpty(bool async) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.SelectMany_Joined_DefaultIfEmpty(async)); + await AssertTranslationFailedWithDetails( + () => base.SelectMany_Joined_DefaultIfEmpty(async), + CosmosStrings.NonCorrelatedSubqueriesNotSupported); AssertSql(); } public override async Task SelectMany_Joined_DefaultIfEmpty2(bool async) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.SelectMany_Joined_DefaultIfEmpty2(async)); + await AssertTranslationFailedWithDetails( + () => base.SelectMany_Joined_DefaultIfEmpty2(async), + CosmosStrings.NonCorrelatedSubqueriesNotSupported); AssertSql(); } public override async Task SelectMany_Joined_DefaultIfEmpty3(bool async) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.SelectMany_Joined_DefaultIfEmpty3(async)); + await AssertTranslationFailedWithDetails( + () => base.SelectMany_Joined_DefaultIfEmpty3(async), + CosmosStrings.NonCorrelatedSubqueriesNotSupported); AssertSql(); } public override async Task SelectMany_Joined(bool async) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.SelectMany_Joined(async)); + await AssertTranslationFailedWithDetails( + () => base.SelectMany_Joined(async), + CosmosStrings.NonCorrelatedSubqueriesNotSupported); AssertSql(); } public override async Task SelectMany_Joined_Take(bool async) { - // Cosmos client evaluation. Issue #17246. - await AssertTranslationFailed(() => base.SelectMany_Joined_Take(async)); + await AssertTranslationFailedWithDetails( + () => base.SelectMany_Joined_Take(async), + CosmosStrings.NonCorrelatedSubqueriesNotSupported); AssertSql(); } @@ -2443,7 +2435,7 @@ public override Task Parameter_extraction_short_circuits_1(bool async) SELECT c FROM root c -WHERE ((c["Discriminator"] = "Order") AND ((c["OrderID"] < 10400) AND (false OR (((c["OrderDate"] != null) AND (DateTimePart("mm", c["OrderDate"]) = @__dateFilter_Value_Month_0)) AND (DateTimePart("yyyy", c["OrderDate"]) = @__dateFilter_Value_Year_1))))) +WHERE ((c["Discriminator"] = "Order") AND ((c["OrderID"] < 10400) AND (((c["OrderDate"] != null) AND (DateTimePart("mm", c["OrderDate"]) = @__dateFilter_Value_Month_0)) AND (DateTimePart("yyyy", c["OrderDate"]) = @__dateFilter_Value_Year_1)))) """, // """ @@ -2473,7 +2465,7 @@ FROM root c """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Order") AND false) +WHERE false """); }); @@ -3177,12 +3169,7 @@ public override Task Int16_parameter_can_be_used_for_int_column(bool async) { await base.Int16_parameter_can_be_used_for_int_column(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] = 10300)) -"""); + AssertSql("ReadItem(None, Order|10300)"); }); public override async Task Subquery_is_null_translated_correctly(bool async) @@ -4194,14 +4181,7 @@ public override Task Entity_equality_with_null_coalesce_client_side(bool async) { await base.Entity_equality_with_null_coalesce_client_side(a); - AssertSql( - """ -@__entity_equality_a_0_CustomerID='ALFKI' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = @__entity_equality_a_0_CustomerID)) -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); public override Task Entity_equality_contains_with_list_of_null(bool async) @@ -4557,12 +4537,7 @@ public override Task Where_Property_when_non_shadow(bool async) { await base.Where_Property_when_non_shadow(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] = 10248)) -"""); + AssertSql("ReadItem(None, Order|10248)"); }); public override Task OrderBy_Select(bool async) @@ -4713,12 +4688,7 @@ public override Task Null_parameter_name_works(bool async) { await base.Null_parameter_name_works(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = null)) -"""); + AssertSql("ReadItem(None, Customer|null)"); }); public override Task Where_Property_shadow_closure(bool async) @@ -5299,14 +5269,7 @@ public override Task Static_member_access_gets_parameterized_within_larger_evalu { await base.Static_member_access_gets_parameterized_within_larger_evaluatable(a); - AssertSql( - """ -@__p_0='ALFKI' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = @__p_0)) -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); [ConditionalFact] @@ -5398,6 +5361,26 @@ ORDER BY c["CustomerID"] """); } + [ConditionalFact] + public virtual async Task ToPageAsync_does_not_use_ReadItem() + { + await using var context = CreateContext(); + + var onlyPage = await context.Set() + .Where(c => c.CustomerID == "ALFKI") + .ToPageAsync(pageSize: 10, continuationToken: null); + + Assert.Equal("ALFKI", onlyPage.Values[0].CustomerID); + Assert.Null(onlyPage.ContinuationToken); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI")) +"""); + } + [ConditionalFact] public virtual async Task ToPageAsync_in_subquery_throws() => await AssertTranslationFailedWithDetails( diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs index db461fc5360..e1a28039882 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs @@ -1638,10 +1638,9 @@ public override async Task Reverse_in_join_inner(bool async) public override async Task Reverse_in_join_inner_with_skip(bool async) { - Assert.Equal( - CosmosStrings.ReverseAfterSkipTakeNotSupported, - (await Assert.ThrowsAsync( - () => base.Reverse_in_join_inner_with_skip(async))).Message); + await AssertTranslationFailedWithDetails( + () => base.Reverse_in_join_inner_with_skip(async), + CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Order), nameof(Customer))); AssertSql(); } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs index d48b83e5b54..94ae70d42f3 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs @@ -1000,12 +1000,7 @@ public override Task Where_equals_method_int(bool async) { await base.Where_equals_method_int(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Employee") AND (c["EmployeeID"] = 1)) -"""); + AssertSql("ReadItem(None, Employee|1)"); }); public override Task Where_equals_using_object_overload_on_mismatched_types(bool async) @@ -1018,7 +1013,7 @@ public override Task Where_equals_using_object_overload_on_mismatched_types(bool """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Employee") AND false) +WHERE false """); }); @@ -1028,14 +1023,7 @@ public override Task Where_equals_using_int_overload_on_mismatched_types(bool as { await base.Where_equals_using_int_overload_on_mismatched_types(a); - AssertSql( - """ -@__p_0='1' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Employee") AND (c["EmployeeID"] = @__p_0)) -"""); + AssertSql("ReadItem(None, Employee|1)"); }); public override Task Where_equals_on_mismatched_types_nullable_int_long(bool async) @@ -1048,13 +1036,13 @@ public override Task Where_equals_on_mismatched_types_nullable_int_long(bool asy """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Employee") AND false) +WHERE false """, // """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Employee") AND false) +WHERE false """); }); @@ -1068,13 +1056,13 @@ public override Task Where_equals_on_mismatched_types_nullable_long_nullable_int """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Employee") AND false) +WHERE false """, // """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Employee") AND false) +WHERE false """); }); @@ -1478,7 +1466,7 @@ public override Task Where_constant_is_null(bool async) """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Customer") AND false) +WHERE false """); }); @@ -1506,7 +1494,7 @@ public override Task Where_null_is_not_null(bool async) """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Customer") AND false) +WHERE false """); }); @@ -1907,7 +1895,7 @@ public override Task Where_false(bool async) """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Customer") AND false) +WHERE false """); }); @@ -1921,20 +1909,12 @@ public override Task Where_bool_closure(bool async) """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Customer") AND false) +WHERE false """, // - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI")) -""", + "ReadItem(None, Customer|ALFKI)", // - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI")) -""", + "ReadItem(None, Customer|ALFKI)", // """ SELECT c @@ -1963,12 +1943,7 @@ public override Task Where_expression_invoke_1(bool async) { await base.Where_expression_invoke_1(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI")) -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); public override async Task Where_expression_invoke_2(bool async) @@ -1985,12 +1960,7 @@ public override Task Where_expression_invoke_3(bool async) { await base.Where_expression_invoke_3(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI")) -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); public override async Task Where_concat_string_int_comparison1(bool async) @@ -2144,7 +2114,7 @@ public override Task Where_ternary_boolean_condition_with_false_as_result_false( """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Product") AND false) +WHERE false """); }); @@ -2295,14 +2265,7 @@ public override Task Where_array_index(bool async) { await base.Where_array_index(a); - AssertSql( - """ -@__p_0='ALFKI' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = @__p_0)) -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); public override async Task Where_multiple_contains_in_subquery_with_or(bool async) @@ -2603,13 +2566,8 @@ public override Task Filter_with_EF_Property_using_closure_for_property_name(boo { await base.Filter_with_EF_Property_using_closure_for_property_name(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI")) -"""); - }); + AssertSql("ReadItem(None, Customer|ALFKI)"); + }); public override Task Filter_with_EF_Property_using_function_for_property_name(bool async) => Fixture.NoSyncTest( @@ -2617,12 +2575,7 @@ public override Task Filter_with_EF_Property_using_function_for_property_name(bo { await base.Filter_with_EF_Property_using_function_for_property_name(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI")) -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); public override async Task FirstOrDefault_over_scalar_projection_compared_to_null(bool async) @@ -2823,7 +2776,7 @@ public override Task GetType_on_non_hierarchy2(bool async) """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Customer") AND false) +WHERE false """); }); @@ -2837,7 +2790,7 @@ public override Task GetType_on_non_hierarchy3(bool async) """ SELECT c FROM root c -WHERE ((c["Discriminator"] = "Customer") AND false) +WHERE false """); }); @@ -2918,21 +2871,9 @@ public override Task Enclosing_class_settable_member_generates_parameter(bool as await base.Enclosing_class_settable_member_generates_parameter(a); AssertSql( - """ -@__SettableProperty_0='10274' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] = @__SettableProperty_0)) -""", + "ReadItem(None, Order|10274)", // - """ -@__SettableProperty_0='10275' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] = @__SettableProperty_0)) -"""); + "ReadItem(None, Order|10275)"); }); public override Task Enclosing_class_readonly_member_generates_parameter(bool async) @@ -2941,14 +2882,7 @@ public override Task Enclosing_class_readonly_member_generates_parameter(bool as { await base.Enclosing_class_readonly_member_generates_parameter(a); - AssertSql( - """ -@__ReadOnlyProperty_0='10275' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] = @__ReadOnlyProperty_0)) -"""); + AssertSql("ReadItem(None, Order|10275)"); }); public override Task Enclosing_class_const_member_does_not_generate_parameter(bool async) @@ -2957,12 +2891,7 @@ public override Task Enclosing_class_const_member_does_not_generate_parameter(bo { await base.Enclosing_class_const_member_does_not_generate_parameter(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Order") AND (c["OrderID"] = 10274)) -"""); + AssertSql("ReadItem(None, Order|10274)"); }); public override Task Generic_Ilist_contains_translates_to_server(bool async) @@ -3304,12 +3233,7 @@ public override Task EF_Constant(bool async) { await base.EF_Constant(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI")) -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); public override Task EF_Constant_with_subtree(bool async) @@ -3318,12 +3242,7 @@ public override Task EF_Constant_with_subtree(bool async) { await base.EF_Constant_with_subtree(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI")) -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); public override Task EF_Constant_does_not_parameterized_as_part_of_bigger_subtree(bool async) @@ -3353,14 +3272,7 @@ public override Task EF_Parameter(bool async) { await base.EF_Parameter(a); - AssertSql( - """ -@__p_0='ALFKI' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = @__p_0)) -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); public override Task EF_Parameter_with_subtree(bool async) @@ -3369,14 +3281,7 @@ public override Task EF_Parameter_with_subtree(bool async) { await base.EF_Parameter_with_subtree(a); - AssertSql( - """ -@__p_0='ALFKI' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = @__p_0)) -"""); + AssertSql("ReadItem(None, Customer|ALFKI)"); }); public override Task EF_Parameter_does_not_parameterized_as_part_of_bigger_subtree(bool async) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs index f5537bf5faa..0f9bd9e7ca3 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs @@ -18,15 +18,24 @@ public OwnedQueryCosmosTest(OwnedQueryCosmosFixture fixture, ITestOutputHelper t Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - // Fink.Barton is a non-owned navigation, cross-document join - public override Task Query_loads_reference_nav_automatically_in_projection(bool async) - => AssertTranslationFailedWithDetails( - () => base.Query_loads_reference_nav_automatically_in_projection(async), - CosmosStrings.CrossDocumentJoinNotSupported); + public override async Task Query_loads_reference_nav_automatically_in_projection(bool async) + { + // Fink.Barton is a non-owned navigation, cross-document join + await AssertTranslationFailedWithDetails( + () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference(async), + CosmosStrings.MultipleRootEntityTypesReferencedInQuery("Planet", "OwnedPerson")); - // Non-correlated queries not supported by Cosmos - public override Task Query_with_owned_entity_equality_operator(bool async) - => AssertTranslationFailed(() => base.Query_with_owned_entity_equality_operator(async)); + AssertSql(); + } + + public override async Task Query_with_owned_entity_equality_operator(bool async) + { + await AssertTranslationFailedWithDetails( + () => base.Query_with_owned_entity_equality_operator(async), + CosmosStrings.NonCorrelatedSubqueriesNotSupported); + + AssertSql(); + } [ConditionalTheory] public override Task Navigation_rewrite_on_owned_collection(bool async) @@ -164,11 +173,15 @@ FROM root c """); }); - // Address.Planet is a non-owned navigation, cross-document join - public override Task Filter_owned_entity_chained_with_regular_entity_followed_by_projecting_owned_collection(bool async) - => AssertTranslationFailedWithDetails( + public override async Task Filter_owned_entity_chained_with_regular_entity_followed_by_projecting_owned_collection(bool async) + { + // Address.Planet is a non-owned navigation, cross-document join + await AssertTranslationFailedWithDetails( () => base.Filter_owned_entity_chained_with_regular_entity_followed_by_projecting_owned_collection(async), - CosmosStrings.CrossDocumentJoinNotSupported); + CosmosStrings.MultipleRootEntityTypesReferencedInQuery("Planet", "OwnedPerson")); + + AssertSql(); + } public override async Task Set_throws_for_owned_type(bool async) { @@ -177,65 +190,106 @@ public override async Task Set_throws_for_owned_type(bool async) AssertSql(); } - // Address.Planet is a non-owned navigation, cross-document join - public override Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity(bool async) - => AssertTranslationFailedWithDetails( + public override async Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity(bool async) + { + // Address.Planet is a non-owned navigation, cross-document join + await AssertTranslationFailedWithDetails( () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity(async), - CosmosStrings.CrossDocumentJoinNotSupported); + CosmosStrings.MultipleRootEntityTypesReferencedInQuery("Planet", "OwnedPerson")); - // Address.Planet is a non-owned navigation, cross-document join - public override Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_filter(bool async) - => AssertTranslationFailedWithDetails( - () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity(async), - CosmosStrings.CrossDocumentJoinNotSupported); + AssertSql(); + } - // Address.Planet is a non-owned navigation, cross-document join - public override Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference(bool async) - => AssertTranslationFailedWithDetails( + public override async Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_filter(bool async) + { + // Address.Planet is a non-owned navigation, cross-document join + await AssertTranslationFailedWithDetails( + () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_filter(async), + CosmosStrings.MultipleRootEntityTypesReferencedInQuery("Planet", "OwnedPerson")); + + AssertSql(); + } + + public override async Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference(bool async) + { + // Address.Planet is a non-owned navigation, cross-document join + await AssertTranslationFailedWithDetails( () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference(async), - CosmosStrings.CrossDocumentJoinNotSupported); + CosmosStrings.MultipleRootEntityTypesReferencedInQuery("Planet", "OwnedPerson")); - // Address.Planet is a non-owned navigation, cross-document join - public override Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference_and_scalar(bool async) - => AssertTranslationFailedWithDetails( + AssertSql(); + } + + public override async Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference_and_scalar(bool async) + { + // Address.Planet is a non-owned navigation, cross-document join + await AssertTranslationFailedWithDetails( () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference_and_scalar(async), - CosmosStrings.CrossDocumentJoinNotSupported); + CosmosStrings.MultipleRootEntityTypesReferencedInQuery("Planet", "OwnedPerson")); - // Address.Planet is a non-owned navigation, cross-document join - public override Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_collection(bool async) - => AssertTranslationFailedWithDetails( + AssertSql(); + } + + public override async Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_collection(bool async) + { + // Address.Planet is a non-owned navigation, cross-document join + await AssertTranslationFailedWithDetails( () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_collection(async), - CosmosStrings.CrossDocumentJoinNotSupported); + CosmosStrings.MultipleRootEntityTypesReferencedInQuery("Planet", "OwnedPerson")); - // Address.Planet is a non-owned navigation, cross-document join - public override Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_collection_count(bool async) - => AssertTranslationFailedWithDetails( + AssertSql(); + } + + public override async Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_collection_count(bool async) + { + // Address.Planet is a non-owned navigation, cross-document join + await AssertTranslationFailedWithDetails( () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_collection_count(async), - CosmosStrings.CrossDocumentJoinNotSupported); + CosmosStrings.MultipleRootEntityTypesReferencedInQuery("Planet", "OwnedPerson")); - // Address.Planet is a non-owned navigation, cross-document join - public override Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_property(bool async) - => AssertTranslationFailedWithDetails( + AssertSql(); + } + + public override async Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_property(bool async) + { + // Address.Planet is a non-owned navigation, cross-document join + await AssertTranslationFailedWithDetails( () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_property(async), - CosmosStrings.CrossDocumentJoinNotSupported); + CosmosStrings.MultipleRootEntityTypesReferencedInQuery("Planet", "OwnedPerson")); - // Address.Planet is a non-owned navigation, cross-document join - public override Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference_in_predicate_and_projection(bool async) - => AssertTranslationFailedWithDetails( + AssertSql(); + } + + public override async Task Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference_in_predicate_and_projection( + bool async) + { + // Address.Planet is a non-owned navigation, cross-document join + await AssertTranslationFailedWithDetails( () => base.Navigation_rewrite_on_owned_reference_followed_by_regular_entity_and_another_reference_in_predicate_and_projection(async), - CosmosStrings.CrossDocumentJoinNotSupported); + CosmosStrings.MultipleRootEntityTypesReferencedInQuery("Planet", "OwnedPerson")); - // Address.Planet is a non-owned navigation, cross-document join - public override Task Project_multiple_owned_navigations(bool async) - => AssertTranslationFailedWithDetails( + AssertSql(); + } + + public override async Task Project_multiple_owned_navigations(bool async) + { + // Address.Planet is a non-owned navigation, cross-document join + await AssertTranslationFailedWithDetails( () => base.Project_multiple_owned_navigations(async), - CosmosStrings.CrossDocumentJoinNotSupported); + CosmosStrings.MultipleRootEntityTypesReferencedInQuery("Planet", "OwnedPerson")); - // Address.Planet is a non-owned navigation, cross-document join - public override Task Project_multiple_owned_navigations_with_expansion_on_owned_collections(bool async) - => AssertTranslationFailedWithDetails( + AssertSql(); + } + + public override async Task Project_multiple_owned_navigations_with_expansion_on_owned_collections(bool async) + { + // Address.Planet is a non-owned navigation, cross-document join + await AssertTranslationFailedWithDetails( () => base.Project_multiple_owned_navigations_with_expansion_on_owned_collections(async), - CosmosStrings.CrossDocumentJoinNotSupported); + CosmosStrings.MultipleRootEntityTypesReferencedInQuery("Planet", "OwnedPerson")); + + AssertSql(); + } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -275,17 +329,26 @@ JOIN o IN c["Orders"] """); }); - // Address.Planet is a non-owned navigation, cross-document join - public override Task SelectMany_on_owned_reference_followed_by_regular_entity_and_collection(bool async) - => AssertTranslationFailedWithDetails( + public override async Task SelectMany_on_owned_reference_followed_by_regular_entity_and_collection(bool async) + { + // Address.Planet is a non-owned navigation, cross-document join + await AssertTranslationFailedWithDetails( () => base.SelectMany_on_owned_reference_followed_by_regular_entity_and_collection(async), - CosmosStrings.CrossDocumentJoinNotSupported); + CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Planet), nameof(OwnedPerson))); + + AssertSql(); + } // Address.Planet is a non-owned navigation, cross-document join - public override Task SelectMany_on_owned_reference_with_entity_in_between_ending_in_owned_collection(bool async) - => AssertTranslationFailedWithDetails( + public override async Task SelectMany_on_owned_reference_with_entity_in_between_ending_in_owned_collection(bool async) + { + // Address.Planet is a non-owned navigation, cross-document join + await AssertTranslationFailedWithDetails( () => base.SelectMany_on_owned_reference_with_entity_in_between_ending_in_owned_collection(async), - CosmosStrings.CrossDocumentJoinNotSupported); + CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Planet), nameof(OwnedPerson))); + + AssertSql(); + } // Non-correlated queries not supported by Cosmos public override Task Query_with_owned_entity_equality_method(bool async) @@ -813,21 +876,34 @@ JOIN o IN c["Orders"] """); }); - // Non-correlated queries not supported by Cosmos - public override Task Projecting_collection_correlated_with_keyless_entity_after_navigation_works_using_parent_identifiers( + public override async Task Projecting_collection_correlated_with_keyless_entity_after_navigation_works_using_parent_identifiers( bool async) - => AssertTranslationFailed( - () => base.Projecting_collection_correlated_with_keyless_entity_after_navigation_works_using_parent_identifiers(async)); + { + await AssertTranslationFailedWithDetails( + () => base.Projecting_collection_correlated_with_keyless_entity_after_navigation_works_using_parent_identifiers(async), + CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Barton), nameof(Fink))); - // Non-correlated queries not supported by Cosmos - public override Task Left_join_on_entity_with_owned_navigations(bool async) - => AssertTranslationFailed( - () => base.Left_join_on_entity_with_owned_navigations(async)); + AssertSql(); - // Non-correlated queries not supported by Cosmos - public override Task Left_join_on_entity_with_owned_navigations_complex(bool async) - => AssertTranslationFailed( - () => base.Left_join_on_entity_with_owned_navigations_complex(async)); + } + + public override async Task Left_join_on_entity_with_owned_navigations(bool async) + { + await AssertTranslationFailedWithDetails( + () => base.Left_join_on_entity_with_owned_navigations(async), + CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(OwnedPerson), nameof(Planet))); + + AssertSql(); + } + + public override async Task Left_join_on_entity_with_owned_navigations_complex(bool async) + { + await AssertTranslationFailedWithDetails( + () => base.Left_join_on_entity_with_owned_navigations_complex(async), + CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(OwnedPerson), nameof(Planet))); + + AssertSql(); + } // TODO: GroupBy, #17313 public override Task GroupBy_aggregate_on_owned_navigation_in_aggregate_selector(bool async) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index de0e74fb1e9..eb9bfb4ca36 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -149,12 +149,7 @@ public override Task Inline_collection_Contains_with_one_value(bool async) { await base.Inline_collection_Contains_with_one_value(a); - AssertSql( - """ -SELECT c -FROM root c -WHERE (c["Id"] = 2) -"""); + AssertSql("ReadItem(None, PrimitiveCollectionsEntity|2)"); }); public override Task Inline_collection_Contains_with_two_values(bool async) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryTest.cs new file mode 100644 index 00000000000..35d518bf39c --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryTest.cs @@ -0,0 +1,724 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Cosmos.Internal; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class ReadItemPartitionKeyQueryTest : QueryTestBase +{ + public ReadItemPartitionKeyQueryTest(ReadItemPartitionKeyQueryFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalFact] + public async Task Predicate_with_hierarchical_partition_key() + { + await AssertQuery( + async: true, + ss => ss.Set() + .Where(e => e.PartitionKey1 == "PK1" && e.PartitionKey2 == 1 && e.PartitionKey3)); + + AssertSql( + """ +SELECT c +FROM root c +"""); + } + + [ConditionalFact] + public async Task Predicate_with_single_partition_key() + { + await AssertQuery( + async: true, + ss => ss.Set().Where(e => e.PartitionKey == "PK1")); + + AssertSql( + """ +SELECT c +FROM root c +"""); + } + + [ConditionalFact] + public async Task Predicate_with_partial_values_in_hierarchical_partition_key() + { + await AssertQuery( + async: true, + ss => ss.Set() + .Where(e => e.PartitionKey1 == "PK1" && e.PartitionKey2 == 1)); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["PartitionKey1"] = "PK1") AND (c["PartitionKey2"] = 1)) +"""); + } + + [ConditionalFact] // #33960 + public async Task Predicate_with_hierarchical_partition_key_and_additional_things_in_predicate() + { + await AssertQuery( + async: true, + ss => ss.Set() + .Where(e => e.Payload.Contains("3") && e.PartitionKey1 == "PK1" && e.PartitionKey2 == 1 && e.PartitionKey3)); + + AssertSql( + """ +SELECT c +FROM root c +WHERE CONTAINS(c["Payload"], "3") +"""); + } + + [ConditionalFact] + public async Task WithPartitionKey_with_hierarchical_partition_key() + { + var partitionKey2 = 1; + + await AssertQuery( + async: true, + ss => ss.Set().WithPartitionKey("PK1", 1, true), + ss => ss.Set() + .Where(e => e.PartitionKey1 == "PK1" && e.PartitionKey2 == partitionKey2 && e.PartitionKey3)); + + AssertSql( + """ +SELECT c +FROM root c +"""); + } + + [ConditionalFact] + public async Task WithPartitionKey_with_single_partition_key() + { + await AssertQuery( + async: true, + ss => ss.Set().WithPartitionKey("PK1"), + ss => ss.Set().Where(e => e.PartitionKey == "PK1")); + + AssertSql( + """ +SELECT c +FROM root c +"""); + } + + [ConditionalFact] + public async Task WithPartitionKey_with_missing_value_in_hierarchical_partition_key() + { + var message = await Assert.ThrowsAsync( + () => AssertQuery( + async: true, + ss => ss.Set().WithPartitionKey("PK1", 1), + ss => ss.Set() + .Where(e => e.PartitionKey1 == "PK1" && e.PartitionKey2 == 1 && e.PartitionKey3))); + + Assert.Equal(CosmosStrings.IncorrectPartitionKeyNumber(nameof(HierarchicalPartitionKeyEntity), 2, 3), message.Message); + } + + [ConditionalFact] + public async Task Different_partition_keys_specified_in_WithPartitionKey_and_in_predicate() + { + var exception = await Assert.ThrowsAsync(() => AssertQuery( + async: true, + ss => ss.Set() + .WithPartitionKey("Pk1") + .Where(e => e.PartitionKey == "PK2"))); + + Assert.Equal(CosmosStrings.PartitionKeyMismatch, exception.Message); + } + + [ConditionalFact] + public async Task ReadItem_with_hierarchical_partition_key() + { + var partitionKey2 = 1; + + await AssertQuery( + async: true, + ss => ss.Set() + .Where(e => e.Id == 1 && e.PartitionKey1 == "PK1" && e.PartitionKey2 == partitionKey2 && e.PartitionKey3)); + + AssertSql("""ReadItem(["PK1",1.0,true], HierarchicalPartitionKeyEntity|1)"""); + } + + [ConditionalFact] + public async Task ReadItem_with_single_partition_key_constant() + { + await AssertQuery( + async: true, + ss => ss.Set().Where(e => e.Id == 1 && e.PartitionKey == "PK1")); + + AssertSql("""ReadItem(["PK1"], SinglePartitionKeyEntity|1)"""); + } + + [ConditionalFact] + public async Task ReadItem_with_single_partition_key_parameter() + { + var partitionKey = "PK1"; + + await AssertQuery( + async: true, + ss => ss.Set().Where(e => e.Id == 1 && e.PartitionKey == partitionKey)); + + AssertSql("""ReadItem(["PK1"], SinglePartitionKeyEntity|1)"""); + } + + [ConditionalFact] + public async Task ReadItem_with_SingleAsync() + { + var partitionKey = "PK1"; + + await AssertSingle( + async: true, + ss => ss.Set().Where(e => e.Id == 1 && e.PartitionKey == partitionKey)); + + AssertSql("""ReadItem(["PK1"], SinglePartitionKeyEntity|1)"""); + } + + [ConditionalFact] + public async Task ReadItem_with_inverse_comparison() + { + await AssertQuery( + async: true, + ss => ss.Set().Where(e => 1 == e.Id && "PK1" == e.PartitionKey)); + + AssertSql("""ReadItem(["PK1"], SinglePartitionKeyEntity|1)"""); + } + + [ConditionalFact] + public async Task ReadItem_with_EF_Property() + { + await AssertQuery( + async: true, + ss => ss.Set().Where( + e => EF.Property(e, nameof(SinglePartitionKeyEntity.Id)) == 1 + && EF.Property(e, nameof(SinglePartitionKeyEntity.PartitionKey)) == "PK1")); + + AssertSql("""ReadItem(["PK1"], SinglePartitionKeyEntity|1)"""); + } + + [ConditionalFact] + public async Task Multiple_incompatible_predicate_comparisons_cause_no_ReadItem() + { + var partitionKey = "PK1"; + + await AssertQuery( + async: true, + ss => ss.Set().Where(e => e.Id == 1 && e.Id == 2 && e.PartitionKey == partitionKey), + assertEmpty: true); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Id"] = 1) AND (c["Id"] = 2)) +"""); + } + + [ConditionalFact] + public async Task ReadItem_with_no_partition_key() + { + await AssertQuery( + async: true, + ss => ss.Set().Where(e => e.Id == 1)); + + AssertSql("ReadItem(None, NoPartitionKeyEntity|1)"); + } + + [ConditionalFact] + public async Task ReadItem_is_not_used_without_partition_key() + { + await AssertQuery( + async: true, + ss => ss.Set().Where(e => e.Id == 1)); + + AssertSql( + """ +SELECT c +FROM root c +WHERE (c["Id"] = 1) +"""); + } + + [ConditionalFact] + public async Task ReadItem_with_non_existent_id() + { + await AssertQuery( + async: true, + ss => ss.Set().Where(e => e.Id == 999 && e.PartitionKey == "PK1"), + assertEmpty: true); + + AssertSql("""ReadItem(["PK1"], SinglePartitionKeyEntity|999)"""); + } + + [ConditionalFact] + public async Task ReadItem_with_AsNoTracking() + { + await AssertQuery( + async: true, + ss => ss.Set().AsNoTracking().Where(e => e.Id == 1 && e.PartitionKey == "PK1")); + + AssertSql("""ReadItem(["PK1"], SinglePartitionKeyEntity|1)"""); + } + + [ConditionalFact] + public async Task ReadItem_with_AsNoTrackingWithIdentityResolution() + { + await AssertQuery( + async: true, + ss => ss.Set().AsNoTrackingWithIdentityResolution().Where(e => e.Id == 1 && e.PartitionKey == "PK1")); + + AssertSql("""ReadItem(["PK1"], SinglePartitionKeyEntity|1)"""); + } + + [ConditionalFact] + public async Task ReadItem_with_shared_container() + { + await AssertQuery( + async: true, + ss => ss.Set().Where(e => e.Id == 1 && e.PartitionKey == "PK1")); + + AssertSql("""ReadItem(["PK1"], SharedContainerEntity1|1)"""); + } + + [ConditionalFact] + public async Task ReadItem_for_base_type_with_shared_container() + { + await AssertQuery( + async: true, + ss => ss.Set().Where(e => e.Id == 4 && e.PartitionKey == "PK2")); + + AssertSql( + """ +SELECT c +FROM root c +WHERE (c["Discriminator"] IN ("SharedContainerEntity2", "SharedContainerEntity2Child") AND (c["Id"] = 4)) +"""); + } + + [ConditionalFact] + public async Task ReadItem_for_child_type_with_shared_container() + { + await AssertQuery( + async: true, + ss => ss.Set().Where(e => e.Id == 5 && e.PartitionKey == "PK2")); + + AssertSql("""ReadItem(["PK2"], SharedContainerEntity2Child|5)"""); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class PartitionKeyContext(DbContextOptions options) : PoolableDbContext(options); + + public class ReadItemPartitionKeyQueryFixture : SharedStoreFixtureBase, IQueryFixtureBase + { + private PartitionKeyData? _expectedData; + + protected override string StoreName + => "PartitionKeyQueryTest"; + + protected override ITestStoreFactory TestStoreFactory + => CosmosTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + modelBuilder.Entity() + .ToContainer(nameof(HierarchicalPartitionKeyEntity)) + .HasPartitionKey(h => new { h.PartitionKey1, h.PartitionKey2, h.PartitionKey3 }); + modelBuilder.Entity() + .ToContainer(nameof(SinglePartitionKeyEntity)) + .HasPartitionKey(h => h.PartitionKey); + modelBuilder.Entity() + .ToContainer(nameof(NoPartitionKeyEntity)); + modelBuilder.Entity() + .ToContainer("SharedContainer") + .HasPartitionKey(e => e.PartitionKey); + modelBuilder.Entity() + .ToContainer("SharedContainer") + .HasPartitionKey(e => e.PartitionKey); + modelBuilder.Entity() + .HasPartitionKey(e => e.PartitionKey); + } + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder.ConfigureWarnings( + w => w.Ignore(CosmosEventId.NoPartitionKeyDefined))); + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + public Func GetContextCreator() + => () => CreateContext(); + + protected override Task SeedAsync(PartitionKeyContext context) + { + context.AddRange(new PartitionKeyData().HierarchicalPartitionKeyEntities); + context.AddRange(new PartitionKeyData().SinglePartitionKeyEntities); + context.AddRange(new PartitionKeyData().NoPartitionKeyEntities); + context.AddRange(new PartitionKeyData().SharedContainerEntities1); + context.AddRange(new PartitionKeyData().SharedContainerEntities2); + context.AddRange(new PartitionKeyData().SharedContainerEntities2Children); + return context.SaveChangesAsync(); + } + + public ISetSource GetExpectedData() + => _expectedData ??= new PartitionKeyData(); + + public IReadOnlyDictionary EntitySorters { get; } = new Dictionary> + { + { typeof(HierarchicalPartitionKeyEntity), e => ((HierarchicalPartitionKeyEntity?)e)?.Id }, + { typeof(SinglePartitionKeyEntity), e => ((SinglePartitionKeyEntity?)e)?.Id }, + { typeof(NoPartitionKeyEntity), e => ((NoPartitionKeyEntity?)e)?.Id }, + { typeof(SharedContainerEntity1), e => ((SharedContainerEntity1?)e)?.Id }, + { typeof(SharedContainerEntity2), e => ((SharedContainerEntity2?)e)?.Id } + }.ToDictionary(e => e.Key, e => (object)e.Value); + + public IReadOnlyDictionary EntityAsserters { get; } = new Dictionary> + { + { + typeof(HierarchicalPartitionKeyEntity), (e, a) => + { + Assert.Equal(e == null, a == null); + + if (a != null) + { + var ee = (HierarchicalPartitionKeyEntity)e!; + var aa = (HierarchicalPartitionKeyEntity)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.PartitionKey1, aa.PartitionKey1); + Assert.Equal(ee.PartitionKey2, aa.PartitionKey2); + Assert.Equal(ee.PartitionKey3, aa.PartitionKey3); + Assert.Equal(ee.Payload, aa.Payload); + } + } + }, + { + typeof(SinglePartitionKeyEntity), (e, a) => + { + Assert.Equal(e == null, a == null); + + if (a != null) + { + var ee = (SinglePartitionKeyEntity)e!; + var aa = (SinglePartitionKeyEntity)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.PartitionKey, aa.PartitionKey); + Assert.Equal(ee.Payload, aa.Payload); + } + } + }, + { + typeof(NoPartitionKeyEntity), (e, a) => + { + Assert.Equal(e == null, a == null); + + if (a != null) + { + var ee = (NoPartitionKeyEntity)e!; + var aa = (NoPartitionKeyEntity)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.Payload, aa.Payload); + } + } + }, + { + typeof(SharedContainerEntity1), (e, a) => + { + Assert.Equal(e == null, a == null); + + if (a != null) + { + var ee = (SharedContainerEntity1)e!; + var aa = (SharedContainerEntity1)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.PartitionKey, aa.PartitionKey); + Assert.Equal(ee.Payload1, aa.Payload1); + } + } + }, + { + typeof(SharedContainerEntity2), (e, a) => + { + Assert.Equal(e == null, a == null); + + if (a != null) + { + var ee = (SharedContainerEntity2)e!; + var aa = (SharedContainerEntity2)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.PartitionKey, aa.PartitionKey); + Assert.Equal(ee.Payload2, aa.Payload2); + } + } + }, + { + typeof(SharedContainerEntity2Child), (e, a) => + { + Assert.Equal(e == null, a == null); + + if (a != null) + { + var ee = (SharedContainerEntity2Child)e!; + var aa = (SharedContainerEntity2Child)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.PartitionKey, aa.PartitionKey); + Assert.Equal(ee.Payload2, aa.Payload2); + Assert.Equal(ee.ChildPayload, aa.ChildPayload); + } + } + } + }.ToDictionary(e => e.Key, e => (object)e.Value); + } + + public class HierarchicalPartitionKeyEntity + { + public int Id { get; set; } + + public required string PartitionKey1 { get; set; } + public int PartitionKey2 { get; set; } + public bool PartitionKey3 { get; set; } + + public required string Payload { get; set; } + } + + public class SinglePartitionKeyEntity + { + public int Id { get; set; } + + public required string PartitionKey { get; set; } + + public required string Payload { get; set; } + } + + public class NoPartitionKeyEntity + { + public int Id { get; set; } + + public required string Payload { get; set; } + } + + public class SharedContainerEntity1 + { + public int Id { get; set; } + public required string PartitionKey { get; set; } + public required string Payload1 { get; set; } + } + + public class SharedContainerEntity2 + { + public int Id { get; set; } + public required string PartitionKey { get; set; } + public required string Payload2 { get; set; } + } + + public class SharedContainerEntity2Child : SharedContainerEntity2 + { + public required string ChildPayload { get; set; } + } + + public class PartitionKeyData : ISetSource + { + public IReadOnlyList HierarchicalPartitionKeyEntities { get; } + public IReadOnlyList SinglePartitionKeyEntities { get; } + public IReadOnlyList NoPartitionKeyEntities { get; } + public IReadOnlyList SharedContainerEntities1 { get; } + public IReadOnlyList SharedContainerEntities2 { get; } + public IReadOnlyList SharedContainerEntities2Children { get; } + + public PartitionKeyData(PartitionKeyContext? context = null) + { + HierarchicalPartitionKeyEntities = CreateHierarchicalPartitionKeyEntities(); + SinglePartitionKeyEntities = CreateSinglePartitionKeyEntities(); + NoPartitionKeyEntities = CreateNoPartitionKeyEntities(); + SharedContainerEntities1 = CreateSharedContainerEntities1(); + SharedContainerEntities2 = CreateSharedContainerEntities2(); + SharedContainerEntities2Children = CreateSharedContainerEntities2Children(); + } + + public IQueryable Set() + where TEntity : class + { + if (typeof(TEntity) == typeof(HierarchicalPartitionKeyEntity)) + { + return (IQueryable)HierarchicalPartitionKeyEntities.AsQueryable(); + } + + if (typeof(TEntity) == typeof(SinglePartitionKeyEntity)) + { + return (IQueryable)SinglePartitionKeyEntities.AsQueryable(); + } + + if (typeof(TEntity) == typeof(NoPartitionKeyEntity)) + { + return (IQueryable)NoPartitionKeyEntities.AsQueryable(); + } + + if (typeof(TEntity) == typeof(SharedContainerEntity1)) + { + return (IQueryable)SharedContainerEntities1.AsQueryable(); + } + + if (typeof(TEntity) == typeof(SharedContainerEntity2)) + { + return (IQueryable)SharedContainerEntities2.AsQueryable(); + } + + if (typeof(TEntity) == typeof(SharedContainerEntity2Child)) + { + return (IQueryable)SharedContainerEntities2Children.AsQueryable(); + } + + throw new InvalidOperationException("Invalid entity type: " + typeof(TEntity)); + } + + private static IReadOnlyList CreateHierarchicalPartitionKeyEntities() + => new List + { + new() + { + Id = 1, + PartitionKey1 = "PK1", + PartitionKey2 = 1, + PartitionKey3 = true, + Payload = "Payload1" + }, + new() + { + Id = 1, + PartitionKey1 = "PK2", + PartitionKey2 = 2, + PartitionKey3 = false, + Payload = "Payload2" + }, + new() + { + Id = 2, + PartitionKey1 = "PK1", + PartitionKey2 = 1, + PartitionKey3 = true, + Payload = "Payload3" + }, + new() + { + Id = 2, + PartitionKey1 = "PK2", + PartitionKey2 = 2, + PartitionKey3 = false, + Payload = "Payload4" + } + }; + + private static IReadOnlyList CreateSinglePartitionKeyEntities() + => new List + { + new() + { + Id = 1, + PartitionKey = "PK1", + Payload = "Payload1" + }, + new() + { + Id = 1, + PartitionKey = "PK2", + Payload = "Payload2" + }, + new() + { + Id = 2, + PartitionKey = "PK1", + Payload = "Payload3" + }, + new() + { + Id = 2, + PartitionKey = "PK2", + Payload = "Payload4" + } + }; + + private static IReadOnlyList CreateNoPartitionKeyEntities() + => new List + { + new() { Id = 1, Payload = "Payload1" }, + new() { Id = 2, Payload = "Payload2" } + }; + + private static IReadOnlyList CreateSharedContainerEntities1() + => new List + { + new() + { + Id = 1, + PartitionKey = "PK1", + Payload1 = "Payload1" + }, + new() + { + Id = 1, + PartitionKey = "PK2", + Payload1 = "Payload2" + }, + new() + { + Id = 2, + PartitionKey = "PK1", + Payload1 = "Payload3" + }, + new() + { + Id = 2, + PartitionKey = "PK2", + Payload1 = "Payload4" + } + }; + + private static IReadOnlyList CreateSharedContainerEntities2() + => new List + { + new() + { + Id = 4, + PartitionKey = "PK1", + Payload2 = "Payload4" + }, + new() + { + Id = 4, + PartitionKey = "PK2", + Payload2 = "Payload5" + } + }; + + private static IReadOnlyList CreateSharedContainerEntities2Children() + => new List + { + new() + { + Id = 5, + PartitionKey = "PK1", + Payload2 = "Payload6", + ChildPayload = "Child1" + }, + new() + { + Id = 5, + PartitionKey = "PK2", + Payload2 = "Payload7", + ChildPayload = "Child2" + } + }; + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/ReadItemTest.cs b/test/EFCore.Cosmos.FunctionalTests/ReadItemTest.cs deleted file mode 100644 index d4f4e682f38..00000000000 --- a/test/EFCore.Cosmos.FunctionalTests/ReadItemTest.cs +++ /dev/null @@ -1,1277 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel.DataAnnotations.Schema; - -namespace Microsoft.EntityFrameworkCore; - -public class ReadItemTest : IClassFixture -{ - public ReadItemTest(ReadItemFixture fixture) - { - Fixture = fixture; - fixture.TestSqlLoggerFactory.Clear(); - } - - protected ReadItemFixture Fixture { get; } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task FirstOrDefault_int_key_constant_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).FirstOrDefaultAsync(e => e.Id == 77); - - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = 77)) -OFFSET 0 LIMIT 1 -"""); - - ValidateIntKeyValues(entity!); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task FirstOrDefault_int_key_variable_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - - var val = 77; - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).FirstOrDefaultAsync(e => e.Id == val); - - AssertSql( - """ -@__val_0='77' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = @__val_0)) -OFFSET 0 LIMIT 1 -"""); - - ValidateIntKeyValues(entity!); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task FirstOrDefault_int_key_constant_value_first_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).FirstOrDefaultAsync(e => 77 == e.Id); - - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (77 = c["Id"])) -OFFSET 0 LIMIT 1 -"""); - - ValidateIntKeyValues(entity!); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task FirstOrDefault_int_key_variable_value_first_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - - var val = 77; - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).FirstOrDefaultAsync(e => val == e.Id); - - AssertSql( - """ -@__val_0='77' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (@__val_0 = c["Id"])) -OFFSET 0 LIMIT 1 -"""); - - ValidateIntKeyValues(entity!); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task First_int_key_constant_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).FirstAsync(e => e.Id == 77); - - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = 77)) -OFFSET 0 LIMIT 1 -"""); - - AssertSql( - ); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task First_int_key_variable_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - - var val = 77; - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).FirstAsync(e => e.Id == val); - - AssertSql( - """ -@__val_0='77' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = @__val_0)) -OFFSET 0 LIMIT 1 -"""); - - ValidateIntKeyValues(entity); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task First_int_key_constant_value_first_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).FirstAsync(e => 77 == e.Id); - - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (77 = c["Id"])) -OFFSET 0 LIMIT 1 -"""); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task First_int_key_variable_value_first_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - - var val = 77; - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).FirstAsync(e => val == e.Id); - - AssertSql( - """ -@__val_0='77' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (@__val_0 = c["Id"])) -OFFSET 0 LIMIT 1 -"""); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task SingleOrDefault_int_key_constant_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).SingleOrDefaultAsync(e => e.Id == 77); - - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = 77)) -OFFSET 0 LIMIT 2 -"""); - - AssertSql( - ); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task SingleOrDefault_int_key_variable_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - - var val = 77; - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).SingleOrDefaultAsync(e => e.Id == val); - - AssertSql( - """ -@__val_0='77' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = @__val_0)) -OFFSET 0 LIMIT 2 -"""); - - ValidateIntKeyValues(entity!); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task SingleOrDefault_int_key_constant_value_first_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).SingleOrDefaultAsync(e => 77 == e.Id); - - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (77 = c["Id"])) -OFFSET 0 LIMIT 2 -"""); - - ValidateIntKeyValues(entity!); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task SingleOrDefault_int_key_variable_value_first_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - - var val = 77; - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).SingleOrDefaultAsync(e => val == e.Id); - - AssertSql( - """ -@__val_0='77' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (@__val_0 = c["Id"])) -OFFSET 0 LIMIT 2 -"""); - - ValidateIntKeyValues(entity!); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task Single_int_key_constant_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).SingleAsync(e => e.Id == 77); - - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = 77)) -OFFSET 0 LIMIT 2 -"""); - - ValidateIntKeyValues(entity); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task Single_int_key_variable_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - - var val = 77; - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).SingleAsync(e => e.Id == val); - - AssertSql( - """ -@__val_0='77' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = @__val_0)) -OFFSET 0 LIMIT 2 -"""); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task Single_int_key_constant_value_first_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).SingleAsync(e => 77 == e.Id); - - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (77 = c["Id"])) -OFFSET 0 LIMIT 2 -"""); - - ValidateIntKeyValues(entity); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task Single_int_key_variable_value_first_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - - var val = 77; - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).SingleAsync(e => val == e.Id); - - AssertSql( - """ -@__val_0='77' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (@__val_0 = c["Id"])) -OFFSET 0 LIMIT 2 -"""); - - ValidateIntKeyValues(entity); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task FirstOrDefault_int_key_constant_with_EF_Property_is_translated_to_ReadItem( - QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) - .FirstOrDefaultAsync(e => EF.Property(e, nameof(IntKey.Id)) == 77); - - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = 77)) -OFFSET 0 LIMIT 1 -"""); - - ValidateIntKeyValues(entity!); - } - - [ConditionalTheory] - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task FirstOrDefault_int_key_variable_with_EF_Property_is_translated_to_ReadItem( - QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - - var val = 77; - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) - .FirstOrDefaultAsync(e => EF.Property(e, nameof(IntKey.Id)) == val); - - AssertSql( - """ -ReadItem(None, IntKey|77) -"""); - - ValidateIntKeyValues(entity!); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task FirstOrDefault_int_key_constant_value_first_with_EF_Property_is_translated_to_ReadItem( - QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) - .FirstOrDefaultAsync(e => 77 == EF.Property(e, nameof(IntKey.Id))); - - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (77 = c["Id"])) -OFFSET 0 LIMIT 1 -"""); - - ValidateIntKeyValues(entity!); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task FirstOrDefault_int_key_variable_value_first_with_EF_Property_is_translated_to_ReadItem( - QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - - var val = 77; - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) - .FirstOrDefaultAsync(e => val == EF.Property(e, nameof(IntKey.Id))); - - AssertSql( - """ -@__val_0='77' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (@__val_0 = c["Id"])) -OFFSET 0 LIMIT 1 -"""); - - ValidateIntKeyValues(entity!); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task First_int_key_constant_with_EF_Property_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) - .FirstAsync(e => EF.Property(e, nameof(IntKey.Id)) == 77); - - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = 77)) -OFFSET 0 LIMIT 1 -"""); - - AssertSql( - ); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task First_int_key_variable_with_EF_Property_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - - var val = 77; - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) - .FirstAsync(e => EF.Property(e, nameof(IntKey.Id)) == val); - - AssertSql( - """ -@__val_0='77' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = @__val_0)) -OFFSET 0 LIMIT 1 -"""); - - ValidateIntKeyValues(entity); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task First_int_key_constant_value_first_with_EF_Property_is_translated_to_ReadItem( - QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) - .FirstAsync(e => 77 == EF.Property(e, nameof(IntKey.Id))); - - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (77 = c["Id"])) -OFFSET 0 LIMIT 1 -"""); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task First_int_key_variable_value_first_with_EF_Property_is_translated_to_ReadItem( - QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - - var val = 77; - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) - .FirstAsync(e => val == EF.Property(e, nameof(IntKey.Id))); - - AssertSql( - """ -@__val_0='77' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (@__val_0 = c["Id"])) -OFFSET 0 LIMIT 1 -"""); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task SingleOrDefault_int_key_constant_with_EF_Property_is_translated_to_ReadItem( - QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) - .SingleOrDefaultAsync(e => EF.Property(e, nameof(IntKey.Id)) == 77); - - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = 77)) -OFFSET 0 LIMIT 2 -"""); - - AssertSql( - ); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task SingleOrDefault_int_key_variable_with_EF_Property_is_translated_to_ReadItem( - QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - - var val = 77; - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) - .SingleOrDefaultAsync(e => EF.Property(e, nameof(IntKey.Id)) == val); - - AssertSql( - """ -@__val_0='77' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = @__val_0)) -OFFSET 0 LIMIT 2 -"""); - - ValidateIntKeyValues(entity!); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task SingleOrDefault_int_key_constant_value_first_with_EF_Property_is_translated_to_ReadItem( - QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) - .SingleOrDefaultAsync(e => 77 == EF.Property(e, nameof(IntKey.Id))); - - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (77 = c["Id"])) -OFFSET 0 LIMIT 2 -"""); - - ValidateIntKeyValues(entity!); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task SingleOrDefault_int_key_variable_value_first_with_EF_Property_is_translated_to_ReadItem( - QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - - var val = 77; - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) - .SingleOrDefaultAsync(e => val == EF.Property(e, nameof(IntKey.Id))); - - AssertSql( - """ -@__val_0='77' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (@__val_0 = c["Id"])) -OFFSET 0 LIMIT 2 -"""); - - ValidateIntKeyValues(entity!); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task Single_int_key_constant_with_EF_Property_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) - .SingleAsync(e => EF.Property(e, nameof(IntKey.Id)) == 77); - - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = 77)) -OFFSET 0 LIMIT 2 -"""); - - ValidateIntKeyValues(entity); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task Single_int_key_variable_with_EF_Property_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - - var val = 77; - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) - .SingleAsync(e => EF.Property(e, nameof(IntKey.Id)) == val); - - AssertSql( - """ -@__val_0='77' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = @__val_0)) -OFFSET 0 LIMIT 2 -"""); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task Single_int_key_constant_value_first_with_EF_Property_is_translated_to_ReadItem( - QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) - .SingleAsync(e => 77 == EF.Property(e, nameof(IntKey.Id))); - - AssertSql( - """ -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (77 = c["Id"])) -OFFSET 0 LIMIT 2 -"""); - - ValidateIntKeyValues(entity); - } - - [ConditionalTheory] // Issue #20693 - [InlineData(QueryTrackingBehavior.TrackAll)] - [InlineData(QueryTrackingBehavior.NoTracking)] - [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] - public virtual async Task Single_int_key_variable_value_first_with_EF_Property_is_translated_to_ReadItem( - QueryTrackingBehavior trackingBehavior) - { - using var context = CreateContext(); - - var val = 77; - var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) - .SingleAsync(e => val == EF.Property(e, nameof(IntKey.Id))); - - AssertSql( - """ -@__val_0='77' - -SELECT c -FROM root c -WHERE ((c["Discriminator"] = "IntKey") AND (@__val_0 = c["Id"])) -OFFSET 0 LIMIT 2 -"""); - - ValidateIntKeyValues(entity); - } - - private static void ValidateIntKeyValues(IntKey entity) - { - Assert.Equal("Smokey", entity.Foo); - Assert.Equal(7, entity.OwnedReference.Prop); - Assert.Equal(2, entity.OwnedCollection.Count); - Assert.Contains(71, entity.OwnedCollection.Select(e => e.Prop)); - Assert.Contains(72, entity.OwnedCollection.Select(e => e.Prop)); - Assert.Equal("7", entity.OwnedReference.NestedOwned.Prop); - Assert.Equal(2, entity.OwnedReference.NestedOwnedCollection.Count); - Assert.Contains("71", entity.OwnedReference.NestedOwnedCollection.Select(e => e.Prop)); - Assert.Contains("72", entity.OwnedReference.NestedOwnedCollection.Select(e => e.Prop)); - } - - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Returns_null_for_int_key_not_in_store_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Null(await Finder.FindAsync(cancellationType, context, [99])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Find_nullable_int_key_tracked_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // var entity = context.Attach( - // new NullableIntKey { Id = 88 }).Entity; - // - // Assert.Same(entity, await Finder.FindAsync(cancellationType, context, [88])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Find_nullable_int_key_from_store_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Equal("Smokey", (await Finder.FindAsync(cancellationType, context, [77])).Foo); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Returns_null_for_nullable_int_key_not_in_store_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Null(await Finder.FindAsync(cancellationType, context, [99])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Find_string_key_tracked_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // var entity = context.Attach( - // new StringKey { Id = "Rabbit" }).Entity; - // - // Assert.Same(entity, await Finder.FindAsync(cancellationType, context, ["Rabbit"])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Find_string_key_from_store_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Equal("Alice", (await Finder.FindAsync(cancellationType, context, ["Cat"])).Foo); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Returns_null_for_string_key_not_in_store_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Null(await Finder.FindAsync(cancellationType, context, ["Fox"])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Find_composite_key_tracked_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // var entity = context.Attach( - // new CompositeKey { Id1 = 88, Id2 = "Rabbit" }).Entity; - // - // Assert.Same(entity, await Finder.FindAsync(cancellationType, context, [88, "Rabbit"])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Find_composite_key_from_store_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Equal("Olive", (await Finder.FindAsync(cancellationType, context, [77, "Dog"])).Foo); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Returns_null_for_composite_key_not_in_store_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Null(await Finder.FindAsync(cancellationType, context, [77, "Fox"])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Find_base_type_tracked_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // var entity = context.Attach( - // new BaseType { Id = 88 }).Entity; - // - // Assert.Same(entity, await Finder.FindAsync(cancellationType, context, [88])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Find_base_type_from_store_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Equal("Baxter", (await Finder.FindAsync(cancellationType, context, [77])).Foo); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Returns_null_for_base_type_not_in_store_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Null(await Finder.FindAsync(cancellationType, context, [99])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Find_derived_type_tracked_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // var entity = context.Attach( - // new DerivedType { Id = 88 }).Entity; - // - // Assert.Same(entity, await Finder.FindAsync(cancellationType, context, [88])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Find_derived_type_from_store_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // var derivedType = await Finder.FindAsync(cancellationType, context, [78]); - // Assert.Equal("Strawberry", derivedType.Foo); - // Assert.Equal("Cheesecake", derivedType.Boo); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Returns_null_for_derived_type_not_in_store_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Null(await Finder.FindAsync(cancellationType, context, [99])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Find_base_type_using_derived_set_tracked_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // context.Attach( - // new BaseType { Id = 88 }); - // - // Assert.Null(await Finder.FindAsync(cancellationType, context, [88])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Find_base_type_using_derived_set_from_store_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Null(await Finder.FindAsync(cancellationType, context, [77])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Find_derived_type_using_base_set_tracked_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // var entity = context.Attach( - // new DerivedType { Id = 88 }).Entity; - // - // Assert.Same(entity, await Finder.FindAsync(cancellationType, context, [88])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Find_derived_using_base_set_type_from_store_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // var derivedType = await Finder.FindAsync(cancellationType, context, [78]); - // Assert.Equal("Strawberry", derivedType.Foo); - // Assert.Equal("Cheesecake", ((DerivedType)derivedType).Boo); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Find_shadow_key_tracked_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // var entry = context.Entry(new ShadowKey()); - // entry.Property("Id").CurrentValue = 88; - // entry.State = EntityState.Unchanged; - // - // Assert.Same(entry.Entity, await Finder.FindAsync(cancellationType, context, [88])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Find_shadow_key_from_store_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Equal("Clippy", (await Finder.FindAsync(cancellationType, context, [77])).Foo); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Returns_null_for_shadow_key_not_in_store_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Null(await Finder.FindAsync(cancellationType, context, [99])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Returns_null_for_null_key_values_array_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Null(await Finder.FindAsync(cancellationType, context, null)); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Returns_null_for_null_key_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Null(await Finder.FindAsync(cancellationType, context, [null])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Returns_null_for_null_in_composite_key_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Null(await Finder.FindAsync(cancellationType, context, [77, null])); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Throws_for_multiple_values_passed_for_simple_key_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Equal( - // CoreStrings.FindNotCompositeKey("IntKey", cancellationType == CancellationType.Wrong ? 3 : 2), - // (await Assert.ThrowsAsync( - // () => Finder.FindAsync(cancellationType, context, [77, 88]).AsTask())).Message); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Throws_for_wrong_number_of_values_for_composite_key_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Equal( - // cancellationType == CancellationType.Wrong - // ? CoreStrings.FindValueTypeMismatch(1, "CompositeKey", "CancellationToken", "string") - // : CoreStrings.FindValueCountMismatch("CompositeKey", 2, 1), - // (await Assert.ThrowsAsync( - // () => Finder.FindAsync(cancellationType, context, [77]).AsTask())).Message); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Throws_for_bad_type_for_simple_key_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Equal( - // CoreStrings.FindValueTypeMismatch(0, "IntKey", "string", "int"), - // (await Assert.ThrowsAsync( - // () => Finder.FindAsync(cancellationType, context, ["77"]).AsTask())).Message); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Throws_for_bad_type_for_composite_key_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Equal( - // CoreStrings.FindValueTypeMismatch(1, "CompositeKey", "int", "string"), - // (await Assert.ThrowsAsync( - // () => Finder.FindAsync(cancellationType, context, [77, 78]).AsTask())).Message); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Throws_for_bad_entity_type_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // Assert.Equal( - // CoreStrings.InvalidSetType(nameof(Random)), - // (await Assert.ThrowsAsync( - // () => Finder.FindAsync(cancellationType, context, [77]).AsTask())).Message); - // } - // - // [ConditionalTheory] - // [InlineData((int)CancellationType.Right)] - // [InlineData((int)CancellationType.Wrong)] - // [InlineData((int)CancellationType.None)] - // public virtual async Task Throws_for_bad_entity_type_with_different_namespace_async(CancellationType cancellationType) - // { - // using var context = CreateContext(); - // - // Assert.Equal( - // CoreStrings.InvalidSetSameTypeWithDifferentNamespace( - // typeof(DifferentNamespace.ShadowKey).DisplayName(), typeof(ShadowKey).DisplayName()), - // (await Assert.ThrowsAsync( - // () => Finder.FindAsync(cancellationType, context, [77]).AsTask())) - // .Message); - // } - - public enum CancellationType - { - Right, - Wrong, - None - } - - protected class BaseType - { - [DatabaseGenerated(DatabaseGeneratedOption.None)] - public int Id { get; set; } - - public string? Foo { get; set; } - } - - protected class DerivedType : BaseType - { - public string? Boo { get; set; } - } - - protected class IntKey - { - [DatabaseGenerated(DatabaseGeneratedOption.None)] - public int Id { get; set; } - - public string? Foo { get; set; } - - public Owned1 OwnedReference { get; set; } = null!; - public List OwnedCollection { get; set; } = null!; - } - - protected class NullableIntKey - { - [DatabaseGenerated(DatabaseGeneratedOption.None)] - public int? Id { get; set; } - - public string? Foo { get; set; } - } - - protected class StringKey - { - public string Id { get; set; } = null!; - - public string? Foo { get; set; } - } - - protected class CompositeKey - { - public int Id1 { get; set; } - public string Id2 { get; set; } = null!; - public string? Foo { get; set; } - } - - protected class ShadowKey - { - public string? Foo { get; set; } - } - - [Owned] - protected class Owned1 - { - public int Prop { get; set; } - public Owned2 NestedOwned { get; set; } = null!; - public List NestedOwnedCollection { get; set; } = null!; - } - - [Owned] - protected class Owned2 - { - public string Prop { get; set; } = null!; - } - - protected DbContext CreateContext() - => Fixture.CreateContext(); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - private static IQueryable ApplyTrackingBehavior(IQueryable query, QueryTrackingBehavior trackingBehavior) - { - query = trackingBehavior switch - { - QueryTrackingBehavior.TrackAll => query, - QueryTrackingBehavior.NoTracking => query.AsNoTracking(), - QueryTrackingBehavior.NoTrackingWithIdentityResolution => query.AsNoTrackingWithIdentityResolution(), - _ => throw new ArgumentOutOfRangeException(nameof(trackingBehavior), trackingBehavior, null) - }; - return query; - } - - public class ReadItemFixture : SharedStoreFixtureBase - { - protected override string StoreName - => "ReadItemTest"; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - modelBuilder.Entity(); - modelBuilder.Entity(); - modelBuilder.Entity(); - modelBuilder.Entity().HasKey( - e => new { e.Id1, e.Id2 }); - modelBuilder.Entity(); - modelBuilder.Entity(); - modelBuilder.Entity().Property(typeof(int), "Id").ValueGeneratedNever(); - } - - protected override Task SeedAsync(PoolableDbContext context) - { - context.AddRange( - new IntKey - { - Id = 77, - Foo = "Smokey", - OwnedReference = new() - { - Prop = 7, - NestedOwned = new() { Prop = "7" }, - NestedOwnedCollection = new() { new() { Prop = "71" }, new() { Prop = "72" } } - }, - OwnedCollection = new() { new() { Prop = 71 }, new() { Prop = 72 } } - }, - new NullableIntKey { Id = 77, Foo = "Smokey" }, - new StringKey { Id = "Cat", Foo = "Alice" }, - new CompositeKey - { - Id1 = 77, - Id2 = "Dog", - Foo = "Olive" - }, - new BaseType { Id = 77, Foo = "Baxter" }, - new DerivedType - { - Id = 78, - Foo = "Strawberry", - Boo = "Cheesecake" - }); - - var entry = context.Entry( - new ShadowKey { Foo = "Clippy" }); - entry.Property("Id").CurrentValue = 77; - entry.State = EntityState.Added; - - return context.SaveChangesAsync(); - } - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - => base.AddOptions(builder).ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined)); - - protected override ITestStoreFactory TestStoreFactory - => CosmosTestStoreFactory.Instance; - } -} diff --git a/test/EFCore.Cosmos.FunctionalTests/ReloadTest.cs b/test/EFCore.Cosmos.FunctionalTests/ReloadTest.cs index 5965397444f..9efd9d08404 100644 --- a/test/EFCore.Cosmos.FunctionalTests/ReloadTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/ReloadTest.cs @@ -5,8 +5,6 @@ namespace Microsoft.EntityFrameworkCore; -#nullable disable - public class ReloadTest : IClassFixture { public static IEnumerable IsAsyncData = [[false], [true]]; @@ -30,8 +28,7 @@ public async Task Entity_reference_can_be_reloaded() { using var context = CreateContext(); - var entry = await context.AddAsync(new Item { Id = 1337 }); - + var entry = await context.AddAsync(new Item { Id = 1337, PartitionKey = "Foo" }); await context.SaveChangesAsync(); var itemJson = entry.Property("__jObject").CurrentValue; @@ -39,6 +36,23 @@ public async Task Entity_reference_can_be_reloaded() await entry.ReloadAsync(); + AssertSql( + """ +@__p_0='1337' + +SELECT VALUE +{ + "Id" : c["Id"], + "PartitionKey" : c["PartitionKey"], + "Discriminator" : c["Discriminator"], + "id0" : c["id"], + "" : c +} +FROM root c +WHERE (c["Id"] = @__p_0) +OFFSET 0 LIMIT 1 +"""); + itemJson = entry.Property("__jObject").CurrentValue; Assert.Null(itemJson["unmapped"]); } @@ -64,7 +78,7 @@ public TestSqlLoggerFactory TestSqlLoggerFactory public class ReloadTestContext(DbContextOptions dbContextOptions) : DbContext(dbContextOptions) { protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => b.HasPartitionKey(e => e.Id)); + => modelBuilder.Entity(b => b.HasPartitionKey(e => e.PartitionKey)); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { @@ -74,11 +88,12 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) optionsBuilder.ConfigureWarnings(w => w.Log(CoreEventId.FirstWithoutOrderByAndFilterWarning)); } - public DbSet Items { get; set; } + public DbSet Items { get; set; } = null!; } public class Item { public int Id { get; set; } + public required string PartitionKey { get; set; } } } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs index a50dd017052..f8865c6b9ef 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs @@ -2065,9 +2065,7 @@ public virtual Task Constant_array_Contains_AndAlso_another_Contains_gets_combin [MemberData(nameof(IsAsyncData))] public virtual Task Multiple_AndAlso_on_same_column_converted_to_in_using_parameters(bool async) { - var prm1 = "ALFKI"; - var prm2 = "ANATR"; - var prm3 = "ANTON"; + var (prm1, prm2, prm3) = ("ALFKI", "ANATR", "ANTON"); return AssertQuery( async, @@ -2078,8 +2076,7 @@ public virtual Task Multiple_AndAlso_on_same_column_converted_to_in_using_parame [MemberData(nameof(IsAsyncData))] public virtual Task Array_of_parameters_Contains_OrElse_comparison_with_constant_gets_combined_to_one_in(bool async) { - var prm1 = "ALFKI"; - var prm2 = "ANATR"; + var (prm1, prm2) = ("ALFKI", "ANATR"); return AssertQuery( async, diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index d5ca1613162..6a4d5cb42bd 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -1209,7 +1209,7 @@ public virtual Task Nested_contains_with_arrays_and_no_inferred_type_mapping(boo public abstract class PrimitiveCollectionsQueryFixtureBase : SharedStoreFixtureBase, IQueryFixtureBase { - private PrimitiveArrayData? _expectedData; + private PrimitiveCollectionsData? _expectedData; protected override string StoreName => "PrimitiveCollectionsTest"; @@ -1222,12 +1222,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con protected override Task SeedAsync(PrimitiveCollectionsContext context) { - context.AddRange(new PrimitiveArrayData().PrimitiveArrayEntities); + context.AddRange(new PrimitiveCollectionsData().PrimitiveArrayEntities); return context.SaveChangesAsync(); } public virtual ISetSource GetExpectedData() - => _expectedData ??= new PrimitiveArrayData(); + => _expectedData ??= new PrimitiveCollectionsData(); public IReadOnlyDictionary EntitySorters { get; } = new Dictionary> { @@ -1283,11 +1283,11 @@ public class PrimitiveCollectionsEntity public enum MyEnum { Value1, Value2, Value3, Value4 } - public class PrimitiveArrayData : ISetSource + public class PrimitiveCollectionsData : ISetSource { public IReadOnlyList PrimitiveArrayEntities { get; } - public PrimitiveArrayData(PrimitiveCollectionsContext? context = null) + public PrimitiveCollectionsData(PrimitiveCollectionsContext? context = null) { PrimitiveArrayEntities = CreatePrimitiveArrayEntities(); }