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..de52cff2d77 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. @@ -335,14 +343,6 @@ public static string PartitionKeyBadValueType(object? propertyType, object? enti GetString("PartitionKeyBadValueType", nameof(propertyType), nameof(entityType), nameof(property), nameof(valueType)), 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. - /// - public static string PartitionKeyMismatch(object? partitionKey1, object? partitionKey2) - => string.Format( - GetString("PartitionKeyMismatch", nameof(partitionKey1), nameof(partitionKey2)), - partitionKey1, partitionKey2); - /// /// 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 +444,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..e4ad7e2a6ab 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. @@ -282,9 +285,6 @@ 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. - 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 +327,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..130fbe68d6d --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/CosmosReadItemAndPartitionKeysExtractor.cs @@ -0,0 +1,281 @@ +// 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). + _isPredicateCompatibleWithReadItem = true; + var jsonIdProperties = _entityType.GetJsonIdDefinition()?.Properties ?? []; + if (jsonIdProperties.Count == 0) + { + // No JSON ID definition - no ReadItem + _isPredicateCompatibleWithReadItem = false; + } + + _jsonIdPropertyValues = jsonIdProperties.ToDictionary(p => p, _ => (Expression?)null); + + var partitionKeyProperties = _entityType.GetPartitionKeyProperties(); + _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. + // Note that if the user called WithPartitionKey(), we'll have already populated the partition key property values from there, and + // we skip lifting the predicate comparisons. + if (allPartitionKeyPropertiesSpecified + && queryCompilationContext.PartitionKeyPropertyValues.Count == 0) + { + foreach (var partitionKeyProperty in partitionKeyProperties) + { + queryCompilationContext.PartitionKeyPropertyValues.Add(_partitionKeyPropertyValues[partitionKeyProperty]!); + } + + 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 + // Note that queryCompilationContext.PartitionKeyPropertyValues may have been populated with WithPartitionKey(), which has + // a params object[] argument that gets parameterized as a single array. So the number of property values may not match the + // number of partition key properties. + && (partitionKeyProperties.Count == 0 || queryCompilationContext.PartitionKeyPropertyValues.Count > 0) + // 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()) + && 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.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs index 54dc03462c9..9270ae3687b 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs @@ -3,7 +3,9 @@ #nullable disable +using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; +using Microsoft.EntityFrameworkCore.Internal; using Newtonsoft.Json.Linq; using static System.Linq.Expressions.Expression; @@ -25,9 +27,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 +35,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 +81,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 +91,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 +109,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,10 +124,79 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery Constant(selectExpression), shaperConstant, contextTypeConstant, - containerConstant, - Constant(_partitionKeyValueFromExtension, typeof(PartitionKey)), + rootEntityTypeConstant, + Constant(cosmosQueryCompilationContext.PartitionKeyPropertyValues), standAloneStateManagerConstant, threadSafetyConstant) }; } + + 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 < partitionKeyProperties.Count; i++) + { + 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 + || i != 1) + { + 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/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..34618bfa658 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; } @@ -417,6 +424,42 @@ public virtual SqlExpression LessThanOrEqual(SqlExpression left, SqlExpression r public virtual SqlExpression AndAlso(SqlExpression left, SqlExpression right) => MakeBinary(ExpressionType.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 /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -426,6 +469,43 @@ public virtual SqlExpression AndAlso(SqlExpression left, SqlExpression right) public virtual SqlExpression OrElse(SqlExpression left, SqlExpression right) => MakeBinary(ExpressionType.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 /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index d580a748e1f..993803f8c54 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -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)!; - 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..c6d0b31e748 100644 --- a/test/EFCore.Cosmos.FunctionalTests/HierarchicalPartitionKeyTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/HierarchicalPartitionKeyTest.cs @@ -3,6 +3,7 @@ namespace Microsoft.EntityFrameworkCore; +// TODO: Consider removing these in favor of ReadItemPartitionKeyQueryTest public class HierarchicalPartitionKeyTest : IClassFixture { private const string DatabaseName = nameof(HierarchicalPartitionKeyTest); @@ -79,6 +80,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/PartitionKeyTest.cs b/test/EFCore.Cosmos.FunctionalTests/PartitionKeyTest.cs index 2e027542742..5af5a4b0dde 100644 --- a/test/EFCore.Cosmos.FunctionalTests/PartitionKeyTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/PartitionKeyTest.cs @@ -5,6 +5,7 @@ namespace Microsoft.EntityFrameworkCore; #nullable disable +// TODO: Consider removing these in favor of ReadItemPartitionKeyQueryTest public class PartitionKeyTest : IClassFixture { private const string DatabaseName = nameof(PartitionKeyTest); 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..6ddb72653f8 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryTest.cs @@ -0,0 +1,757 @@ +// 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 Both_WithPartitionKey_and_predicate_comparisons_with_different_values() + { + await AssertQuery( + async: true, + ss => ss.Set().WithPartitionKey("PK1").Where(e => e.PartitionKey == "PK2"), + ss => ss.Set().Where(e => e.PartitionKey == "PK1").Where(e => e.PartitionKey == "PK2"), + assertEmpty: true); + + AssertSql( + """ +SELECT c +FROM root c +WHERE (c["PartitionKey"] = "PK2") +"""); + } + + [ConditionalFact] + public async Task Both_WithPartitionKey_and_predicate_comparisons_with_same_values() + { + await AssertQuery( + async: true, + ss => ss.Set() + .WithPartitionKey("PK1") + .Where(e => e.PartitionKey == "PK1")); + + AssertSql( + """ +SELECT c +FROM root c +WHERE (c["PartitionKey"] = "PK1") +"""); + } + + [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 ReadItem_with_WithPartitionKey() + { + await AssertQuery( + async: true, + ss => ss.Set().WithPartitionKey("PK1").Where(e => e.Id == 1), + ss => ss.Set().Where(e => e.PartitionKey == "PK1").Where(e => e.Id == 1)); + + 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(); }