diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index b7ef50f00fa..8593eb8f9f2 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1407,6 +1407,12 @@ public static string ParameterNotObjectArray(object? parameter) public static string PendingAmbientTransaction => GetString("PendingAmbientTransaction"); + /// + /// Query is projecting entity mapped to JSON that accesses a JSON collection element. Those queries require 'AsNoTracking' option, even when the parent entity is projected. + /// + public static string ProjectingJsonCollectionElementRequiresNoTracking + => GetString("ProjectingJsonCollectionElementRequiresNoTracking"); + /// /// Unable to translate set operations when both sides don't assign values to the same properties in the nominal type. Please make sure that the same properties are included on both sides, and consider assigning default values if a property doesn't require a specific value. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index d10d59b928c..6da739e3889 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -950,6 +950,9 @@ This connection was used with an ambient transaction. The original ambient transaction needs to be completed before this connection can be used outside of it. + + Query is projecting entity mapped to JSON that accesses a JSON collection element. Those queries require 'AsNoTracking' option, even when the parent entity is projected. + Unable to translate set operations when both sides don't assign values to the same properties in the nominal type. Please make sure that the same properties are included on both sides, and consider assigning default values if a property doesn't require a specific value. diff --git a/src/EFCore.Relational/Query/Internal/CollectionIndexerToElementAtConvertingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/CollectionIndexerToElementAtConvertingExpressionVisitor.cs new file mode 100644 index 00000000000..bd1c542f62f --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/CollectionIndexerToElementAtConvertingExpressionVisitor.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq.Expressions; + +namespace Microsoft.EntityFrameworkCore.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. +/// +internal class CollectionIndexerToElementAtConvertingExpressionVisitor : ExpressionVisitor +{ + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) + { + if (methodCallExpression.Method.Name == "get_Item" + && !methodCallExpression.Method.IsStatic + && methodCallExpression.Method.DeclaringType != null + && methodCallExpression.Method.DeclaringType.IsGenericType + && methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typeof(List<>) + && ShouldConvert(methodCallExpression.Type)) + { + var source = Visit(methodCallExpression.Object!); + var index = Visit(methodCallExpression.Arguments[0]); + var sourceTypeArgument = source.Type.GetSequenceType(); + + return Expression.Call( + QueryableMethods.ElementAt.MakeGenericMethod(sourceTypeArgument), + Expression.Call( + QueryableMethods.AsQueryable.MakeGenericMethod(sourceTypeArgument), + source), + index); + } + + return base.VisitMethodCall(methodCallExpression); + } + + protected override Expression VisitBinary(BinaryExpression binaryExpression) + { + if (binaryExpression.NodeType == ExpressionType.ArrayIndex + && binaryExpression.Left.Type != typeof(byte[]) + && ShouldConvert(binaryExpression.Type)) + { + var source = Visit(binaryExpression.Left); + var index = Visit(binaryExpression.Right); + var sourceTypeArgument = source.Type.GetSequenceType(); + + return Expression.Call( + QueryableMethods.ElementAt.MakeGenericMethod(sourceTypeArgument), + Expression.Call( + QueryableMethods.AsQueryable.MakeGenericMethod(sourceTypeArgument), + source), + index); + } + + return base.VisitBinary(binaryExpression); + } + + private static bool ShouldConvert(Type type) + => type.IsPrimitive && !type.IsEnum; +} diff --git a/src/EFCore.Relational/Query/JsonQueryExpression.cs b/src/EFCore.Relational/Query/JsonQueryExpression.cs index d646b593216..3e5b941920f 100644 --- a/src/EFCore.Relational/Query/JsonQueryExpression.cs +++ b/src/EFCore.Relational/Query/JsonQueryExpression.cs @@ -161,6 +161,30 @@ public virtual JsonQueryExpression BindNavigation(INavigation navigation) IsNullable || !navigation.ForeignKey.IsRequiredDependent); } + /// + /// Binds a collection element access with this JSON query expression to get the SQL representation. + /// + /// The collection index to bind. + public virtual JsonQueryExpression BindCollectionElement(SqlExpression collectionIndexExpression) + { + Debug.Assert( + Path.Last().CollectionIndexExpression == null, + "Already accessing collection element."); + + var newPath = Path.ToList(); + newPath.Add(new PathSegment(collectionIndexExpression)); + + return new JsonQueryExpression( + EntityType, + JsonColumn, + _keyPropertyMap, + newPath, + EntityType.ClrType, + collection: false, + // TODO: is this the right nullable? + nullable: true); + } + /// /// Makes this JSON query expression nullable. /// diff --git a/src/EFCore.Relational/Query/PathSegment.cs b/src/EFCore.Relational/Query/PathSegment.cs index e251ea2fc01..3dd9483cf2a 100644 --- a/src/EFCore.Relational/Query/PathSegment.cs +++ b/src/EFCore.Relational/Query/PathSegment.cs @@ -17,22 +17,40 @@ namespace Microsoft.EntityFrameworkCore.Query; public class PathSegment { /// - /// Creates a new instance of the class. + /// Creates a new instance of the class representing JSON property access. /// /// A key which is being accessed in the JSON. public PathSegment(string key) { Key = key; + CollectionIndexExpression = null; + } + + /// + /// Creates a new instance of the class representing JSON collection element access. + /// + /// A collection index which is being accessed in the JSON. + public PathSegment(SqlExpression collectionIndexExpression) + { + CollectionIndexExpression = collectionIndexExpression; + Key = null; } /// /// The key which is being accessed in the JSON. /// - public virtual string Key { get; } + public virtual string? Key { get; } + + /// + /// The index of the collection which is being accessed in the JSON. + /// + public virtual SqlExpression? CollectionIndexExpression { get; } /// public override string ToString() - => (Key == "$" ? "" : ".") + Key; + => (Key == "$" ? "" : ".") + + (Key == null ? "" : Key) + + (CollectionIndexExpression == null ? "" : $"[{CollectionIndexExpression}]"); /// public override bool Equals(object? obj) @@ -42,9 +60,11 @@ public override bool Equals(object? obj) && Equals(pathSegment)); private bool Equals(PathSegment pathSegment) - => Key == pathSegment.Key; + => Key == pathSegment.Key + && ((CollectionIndexExpression == null && pathSegment.CollectionIndexExpression == null) + || (CollectionIndexExpression != null && CollectionIndexExpression.Equals(pathSegment.CollectionIndexExpression))); /// public override int GetHashCode() - => HashCode.Combine(Key); + => HashCode.Combine(Key, CollectionIndexExpression); } diff --git a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs index 42e5416464b..1a5cb9783fc 100644 --- a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs @@ -37,6 +37,7 @@ public override Expression NormalizeQueryableMethod(Expression expression) expression = new RelationalQueryMetadataExtractingExpressionVisitor(_relationalQueryCompilationContext).Visit(expression); expression = base.NormalizeQueryableMethod(expression); expression = new TableValuedFunctionToQueryRootConvertingExpressionVisitor(QueryCompilationContext.Model).Visit(expression); + expression = new CollectionIndexerToElementAtConvertingExpressionVisitor().Visit(expression); return expression; } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 7c0e5934361..e97b2066132 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -1640,6 +1640,64 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp return TryExpand(source, MemberIdentity.Create(navigationName)) ?? methodCallExpression.Update(null!, new[] { source, methodCallExpression.Arguments[1] }); } + else if (methodCallExpression.Method.IsGenericMethod + && (methodCallExpression.Method.GetGenericMethodDefinition() == QueryableMethods.ElementAt + || methodCallExpression.Method.GetGenericMethodDefinition() == QueryableMethods.ElementAtOrDefault)) + { + source = methodCallExpression.Arguments[0]; + var selectMethodCallExpression = default(MethodCallExpression); + + if (source is MethodCallExpression sourceMethod + && sourceMethod.Method.IsGenericMethod + && sourceMethod.Method.GetGenericMethodDefinition() == QueryableMethods.Select) + { + selectMethodCallExpression = sourceMethod; + source = sourceMethod.Arguments[0]; + } + + if (source is MethodCallExpression asQueryableMethodCallExpression + && asQueryableMethodCallExpression.Method.IsGenericMethod + && asQueryableMethodCallExpression.Method.GetGenericMethodDefinition() == QueryableMethods.AsQueryable) + { + source = asQueryableMethodCallExpression.Arguments[0]; + } + + source = Visit(source); + + if (source is JsonQueryExpression jsonQueryExpression) + { + var collectionIndexExpression = _sqlTranslator.Translate(methodCallExpression.Arguments[1]!); + + if (collectionIndexExpression == null) + { + return methodCallExpression.Update(null!, new[] { source, methodCallExpression.Arguments[1] }); + } + + collectionIndexExpression = _sqlExpressionFactory.ApplyDefaultTypeMapping(collectionIndexExpression); + var newJsonQuery = jsonQueryExpression.BindCollectionElement(collectionIndexExpression!); + + var entityShaper = new RelationalEntityShaperExpression( + jsonQueryExpression.EntityType, + newJsonQuery, + nullable: true); + + // look into select (if there was any) + // strip the includes + // and if there was anything (e.g. MaterializeCollectionNavigationExpression) wrap the entity shaper around it and return that + if (selectMethodCallExpression != null) + { + var selectorLambda = selectMethodCallExpression.Arguments[1].UnwrapLambdaFromQuote(); + var replaced = new ReplacingExpressionVisitor(new[] { selectorLambda.Parameters[0] }, new[] { entityShaper }) + .Visit(selectorLambda.Body); + + var result = Visit(replaced); + + return result; + } + + return entityShaper; + } + } return base.VisitMethodCall(methodCallExpression); } diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs index 92736474a3c..3bab5a75895 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs @@ -416,9 +416,14 @@ protected override Expression VisitExtension(Expression extensionExpression) { if (!_variableShaperMapping.TryGetValue(entityShaperExpression.ValueBufferExpression, out var accessor)) { - if (GetProjectionIndex(projectionBindingExpression) is ValueTuple, string[]> + if (GetProjectionIndex(projectionBindingExpression) is ValueTuple, string[], int> jsonProjectionIndex) { + if (_isTracking && jsonProjectionIndex.Item4 > 0) + { + throw new InvalidOperationException(RelationalStrings.ProjectingJsonCollectionElementRequiresNoTracking); + } + // json entity at the root var (jsonElementParameter, keyValuesParameter) = JsonShapingPreProcess( jsonProjectionIndex, @@ -510,8 +515,13 @@ protected override Expression VisitExtension(Expression extensionExpression) case CollectionResultExpression collectionResultExpression when collectionResultExpression.Navigation is INavigation navigation && GetProjectionIndex(collectionResultExpression.ProjectionBindingExpression) - is ValueTuple, string[]> jsonProjectionIndex: + is ValueTuple, string[], int> jsonProjectionIndex: { + if (_isTracking && jsonProjectionIndex.Item4 > 0) + { + throw new InvalidOperationException(RelationalStrings.ProjectingJsonCollectionElementRequiresNoTracking); + } + // json entity collection at the root var (jsonElementParameter, keyValuesParameter) = JsonShapingPreProcess( jsonProjectionIndex, @@ -781,7 +791,7 @@ when collectionResultExpression.Navigation is INavigation navigation // json include case if (projectionBindingExpression != null - && GetProjectionIndex(projectionBindingExpression) is ValueTuple, string[]> + && GetProjectionIndex(projectionBindingExpression) is ValueTuple, string[], int> jsonProjectionIndex) { var (jsonElementParameter, keyValuesParameter) = JsonShapingPreProcess( @@ -1236,16 +1246,17 @@ private Expression CreateJsonShapers( } private (ParameterExpression, ParameterExpression) JsonShapingPreProcess( - ValueTuple, string[]> projectionIndex, + ValueTuple, string[], int> projectionIndex, IEntityType entityType, bool isCollection) { var jsonColumnProjectionIndex = projectionIndex.Item1; var keyInfo = projectionIndex.Item2; var additionalPath = projectionIndex.Item3; + var specifiedCollectionIndexesCount = projectionIndex.Item4; var keyValuesParameter = Expression.Parameter(typeof(object[])); - var keyValues = new Expression[keyInfo.Count]; + var keyValues = new Expression[keyInfo.Count + specifiedCollectionIndexesCount]; for (var i = 0; i < keyInfo.Count; i++) { @@ -1262,6 +1273,12 @@ private Expression CreateJsonShapers( typeof(object)); } + for (var i = 0; i < specifiedCollectionIndexesCount; i++) + { + keyValues[keyInfo.Count + i] = Expression.Convert( + Expression.Constant(0), typeof(object)); + } + var keyValuesInitialize = Expression.NewArrayInit(typeof(object), keyValues); var keyValuesAssignment = Expression.Assign(keyValuesParameter, keyValuesInitialize); diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index bf688dac9ec..aff209278a5 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -1529,15 +1529,15 @@ Expression CopyProjectionToOuter(SelectExpression innerSelectExpression, Express remappedConstant = Constant(newDictionary); } - else if (constantValue is ValueTuple>, string[]> tuple) + else if (constantValue is ValueTuple, string[], int> tuple) { - var newList = new List>(); + var newList = new List<(IProperty, int)>(); foreach (var item in tuple.Item2) { newList.Add((item.Item1, projectionIndexMap[item.Item2])); } - remappedConstant = Constant((projectionIndexMap[tuple.Item1], newList, tuple.Item3)); + remappedConstant = Constant((projectionIndexMap[tuple.Item1], newList, tuple.Item3, tuple.Item4)); } else { @@ -1590,7 +1590,8 @@ static Dictionary BuildJsonProjection { var ordered = projections .OrderBy(x => $"{x.JsonColumn.TableAlias}.{x.JsonColumn.Name}") - .ThenBy(x => x.Path.Count); + .ThenBy(x => x.Path.Count) + .ThenBy(x => x.Path.Last().CollectionIndexExpression != null); var needed = new List(); foreach (var orderedElement in ordered) @@ -1640,10 +1641,13 @@ ConstantExpression AddJsonProjection(JsonQueryExpression jsonQueryExpression, Js { var additionalPath = new string[0]; - // this will be more tricky once we support more complicated json path options + // TODO: change this when implementing #29513 + // deduplication doesn't happen currently if the additional path contains indexes + Debug.Assert(jsonQueryExpression.Path.Skip(jsonScalarToAdd.Path.Count).All(x => x.Key != null)); + additionalPath = jsonQueryExpression.Path .Skip(jsonScalarToAdd.Path.Count) - .Select(x => x.Key) + .Select(x => x.Key!) .ToArray(); var jsonColumnIndex = AddToProjection(jsonScalarToAdd); @@ -1656,7 +1660,9 @@ ConstantExpression AddJsonProjection(JsonQueryExpression jsonQueryExpression, Js keyInfo.Add((keyProperty, AddToProjection(keyColumn))); } - return Constant((jsonColumnIndex, keyInfo, additionalPath)); + var specifiedCollectionIndexesCount = jsonScalarToAdd.Path.Count(x => x.CollectionIndexExpression != null); + + return Constant((jsonColumnIndex, keyInfo, additionalPath, specifiedCollectionIndexesCount)); } static IReadOnlyList GetMappedKeyProperties(IKey key) @@ -1697,7 +1703,15 @@ static bool JsonEntityContainedIn(JsonScalarExpression sourceExpression, JsonQue return false; } - return sourcePath.SequenceEqual(targetPath.Take(sourcePath.Count)); + if (!sourcePath.SequenceEqual(targetPath.Take(sourcePath.Count))) + { + return false; + } + + // we can only perform deduplication if there additional path doesn't contain any collection indexes + // collection indexes can only be part of the source path + // see issue #29513 + return targetPath.Skip(sourcePath.Count).All(x => x.CollectionIndexExpression == null); } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs index bfac93e291b..82315f79de6 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs @@ -321,7 +321,36 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp Visit(jsonScalarExpression.JsonColumn); - Sql.Append($",'{string.Join("", jsonScalarExpression.Path.Select(e => e.ToString()))}')"); + Sql.Append(",'"); + foreach (var pathSegment in jsonScalarExpression.Path) + { + if (pathSegment.Key != null) + { + Sql.Append((pathSegment.Key == "$" ? "" : ".") + pathSegment.Key); + } + + if (pathSegment.CollectionIndexExpression != null) + { + Sql.Append("["); + + if (pathSegment.CollectionIndexExpression is SqlConstantExpression) + { + Visit(pathSegment.CollectionIndexExpression); + } + else + { + Sql.Append("' + CAST("); + Visit(pathSegment.CollectionIndexExpression); + Sql.Append(" AS "); + Sql.Append(_typeMappingSource.GetMapping(typeof(string)).StoreType); + Sql.Append(") + '"); + } + + Sql.Append("]"); + } + } + + Sql.Append("')"); if (jsonScalarExpression.TypeMapping is not SqlServerJsonTypeMapping and not StringTypeMapping) { diff --git a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs index dc1ff2e3076..f629ffcbcf2 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs @@ -112,7 +112,8 @@ public virtual Task Basic_json_projection_enum_inside_json_entity(bool async) ss => ss.Set().Select( x => new { - x.Id, x.OwnedReferenceRoot.OwnedReferenceBranch.Enum, + x.Id, + x.OwnedReferenceRoot.OwnedReferenceBranch.Enum, }), elementSorter: e => e.Id, elementAsserter: (e, a) => @@ -632,15 +633,326 @@ public virtual Task Json_entity_backtracking(bool async) public virtual Task Json_collection_element_access_in_projection_basic(bool async) => AssertQuery( async, - ss => ss.Set().Select(x => x.OwnedCollectionRoot[0]).AsNoTracking()); + ss => ss.Set().Select(x => x.OwnedCollectionRoot[1]).AsNoTracking()); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_projection_using_ElementAt(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedCollectionRoot.AsQueryable().ElementAt(1)).AsNoTracking()); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_projection_using_ElementAtOrDefault(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedCollectionRoot.AsQueryable().ElementAtOrDefault(1)).AsNoTracking()); - [ConditionalTheory(Skip = "issue #28648")] + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_projection_project_collection(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedCollectionRoot[1].OwnedCollectionBranch).AsNoTracking(), + elementAsserter: (e, a) => AssertCollection(e, a, ordered: true)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_projection_using_ElementAt_project_collection(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Select(x => x.OwnedCollectionRoot.AsQueryable().ElementAt(1).OwnedCollectionBranch) + .AsNoTracking(), + elementAsserter: (e, a) => AssertCollection(e, a, ordered: true)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_projection_using_ElementAtOrDefault_project_collection(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Select(x => x.OwnedCollectionRoot.AsQueryable().ElementAtOrDefault(1).OwnedCollectionBranch) + .AsNoTracking(), + elementAsserter: (e, a) => AssertCollection(e, a, ordered: true)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_projection_using_parameter(bool async) + { + var prm = 0; + + return AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedCollectionRoot[prm]).AsNoTracking()); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_projection_using_column(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedCollectionRoot[x.Id]).AsNoTracking()); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_projection_nested(bool async) + { + var prm = 1; + + return AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedCollectionRoot[0].OwnedCollectionBranch[prm]).AsNoTracking()); + } + + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Json_collection_element_access_in_predicate(bool async) + public virtual Task Json_collection_element_access_in_projection_nested_project_scalar(bool async) + { + var prm = 1; + + return AssertQueryScalar( + async, + ss => ss.Set().Select(x => x.OwnedCollectionRoot[0].OwnedCollectionBranch[prm].Date)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_projection_nested_project_reference(bool async) + { + var prm = 1; + + return AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedCollectionRoot[0].OwnedCollectionBranch[prm].OwnedReferenceLeaf).AsNoTracking()); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_projection_nested_project_collection(bool async) + { + var prm = 1; + + return AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select(x => x.OwnedCollectionRoot[0].OwnedCollectionBranch[prm].OwnedCollectionLeaf) + .AsNoTracking(), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a, ordered: true)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_projection_nested_project_collection_anonymous_projection(bool async) + { + var prm = 1; + + return AssertQuery( + async, + ss => ss.Set() + .Select(x => new { x.Id, x.OwnedCollectionRoot[0].OwnedCollectionBranch[prm].OwnedCollectionLeaf }) + .AsNoTracking(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + AssertCollection(e.OwnedCollectionLeaf, a.OwnedCollectionLeaf, ordered: true); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_predicate_using_constant(bool async) => AssertQueryScalar( async, ss => ss.Set().Where(x => x.OwnedCollectionRoot[0].Name != "Foo").Select(x => x.Id)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_predicate_using_variable(bool async) + { + var prm = 1; + + return AssertQueryScalar( + async, + ss => ss.Set().Where(x => x.OwnedCollectionRoot[prm].Name != "Foo").Select(x => x.Id)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_predicate_using_column(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(x => x.OwnedCollectionRoot[x.Id].Name == "e1_c2").Select(x => new { x.Id, x }), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + AssertEqual(e.x, a.x); + }, + entryCount: 40); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_predicate_using_complex_expression1(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(x => x.OwnedCollectionRoot[x.Id == 1 ? 0 : 1].Name == "e1_c1").Select(x => new { x.Id, x }), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + AssertEqual(e.x, a.x); + }, + entryCount: 40); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_predicate_using_complex_expression2(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(x => x.OwnedCollectionRoot[ss.Set().Max(x => x.Id)].Name == "e1_c2").Select(x => new { x.Id, x }), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + AssertEqual(e.x, a.x); + }, + entryCount: 40); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_predicate_using_ElementAt(bool async) + => AssertQueryScalar( + async, + ss => ss.Set().Where(x => x.OwnedCollectionRoot.AsQueryable().ElementAt(1).Name != "Foo").Select(x => x.Id)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_collection_element_access_in_predicate_nested_mix(bool async) + { + var prm = 0; + + return AssertQuery( + async, + ss => ss.Set().Where(x => x.OwnedCollectionRoot[1].OwnedCollectionBranch[prm].OwnedCollectionLeaf[x.Id - 1].SomethingSomething == "e1_c2_c1_c1"), + entryCount: 40); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_deduplication_with_collection_indexer_in_original(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => new + { + x.Id, + Duplicate1 = x.OwnedCollectionRoot[0].OwnedReferenceBranch, + Original = x.OwnedCollectionRoot[0], + Duplicate2 = x.OwnedCollectionRoot[0].OwnedReferenceBranch.OwnedCollectionLeaf + }).AsNoTracking(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Original, a.Original); + AssertEqual(e.Duplicate1, a.Duplicate1); + AssertCollection(e.Duplicate2, a.Duplicate2, ordered: true); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_deduplication_with_collection_indexer_in_target(bool async) + { + var prm = 1; + + // issue #29513 + return AssertQuery( + async, + ss => ss.Set().Select(x => new + { + x.Id, + Duplicate1 = x.OwnedReferenceRoot.OwnedCollectionBranch[1], + Original = x.OwnedReferenceRoot, + Duplicate2 = x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf[prm] + }).AsNoTracking(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Original, a.Original); + AssertEqual(e.Duplicate1, a.Duplicate1); + AssertEqual(e.Duplicate2, a.Duplicate2); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_deduplication_with_collection_in_original_and_collection_indexer_in_target(bool async) + // issue #29513 + => AssertQuery( + async, + ss => ss.Set().Select(x => new + { + x.Id, + Original = x.OwnedReferenceRoot.OwnedCollectionBranch, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[1], + }).AsNoTracking(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertCollection(e.Original, a.Original, ordered: true); + AssertEqual(e.Duplicate, a.Duplicate); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_collection_element_access_in_projection_requires_NoTracking_even_if_owner_is_present(bool async) + { + var exception = await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set().Select(x => new + { + x, + CollectionElement = x.OwnedCollectionRoot[1] + }), + elementSorter: e => e.x.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.x, a.x); + AssertEqual(e.CollectionElement, a.CollectionElement); + })); + + Assert.Equal(RelationalStrings.ProjectingJsonCollectionElementRequiresNoTracking, exception.Message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_collection_element_access_in_projection_requires_NoTracking_even_if_owner_is_present2(bool async) + { + var message = (await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set().Select(x => new + { + x, + CollectionElement = x.OwnedCollectionRoot[1].OwnedReferenceBranch + }), + elementSorter: e => e.x.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.x, a.x); + AssertEqual(e.CollectionElement, a.CollectionElement); + }))).Message; + + Assert.Equal(RelationalStrings.ProjectingJsonCollectionElementRequiresNoTracking, message); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Json_scalar_required_null_semantics(bool async) @@ -667,6 +979,15 @@ public virtual Task Group_by_on_json_scalar(bool async) ss => ss.Set() .GroupBy(x => x.OwnedReferenceRoot.Name).Select(x => new { x.Key, Count = x.Count() })); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Group_by_on_json_scalar_using_collection_indexer(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(x => x.OwnedCollectionRoot[0].Name).Select(x => new { x.Key, Count = x.Count() })); + + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Group_by_First_on_json_scalar(bool async) diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index adb2a61365c..2cf7861456f 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -8224,6 +8224,20 @@ public virtual Task Where_subquery_with_ElementAt_using_column_as_index(bool asy ss => ss.Set().Where(s => s.Members.OrderBy(m => m.Nickname).ElementAt(s.Id).Nickname == "Cole Train"), ss => ss.Set().Where(s => s.Members.OrderBy(m => m.Nickname).ElementAtOrDefault(s.Id).Nickname == "Cole Train")); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Using_indexer_on_byte_array_and_string_in_projection(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => new { x.Id, ByteArray = x.Banner[0], String = x.Name[1] }), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + Assert.Equal(e.ByteArray, a.ByteArray); + Assert.Equal(e.String, a.String); + }); + protected GearsOfWarContext CreateContext() => Fixture.CreateContext(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index b5d77fa52af..97fc8406506 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -10032,6 +10032,17 @@ ORDER BY [g].[Nickname] """); } + public override async Task Using_indexer_on_byte_array_and_string_in_projection(bool async) + { + await base.Using_indexer_on_byte_array_and_string_in_projection(async); + + AssertSql( +""" +SELECT [s].[Id], CAST(SUBSTRING([s].[Banner], 0 + 1, 1) AS tinyint), [s].[Name] +FROM [Squads] AS [s] +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs index 2470fcd8b92..218137b1e51 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs @@ -584,20 +584,298 @@ public override async Task Json_collection_element_access_in_projection_basic(bo { await base.Json_collection_element_access_in_projection_basic(async); - // array element access in projection is currently done on the client - issue 28648 AssertSql( """ -SELECT [j].[OwnedCollectionRoot], [j].[Id] +SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[1]'), [j].[Id] FROM [JsonEntitiesBasic] AS [j] """); } - public override async Task Json_collection_element_access_in_predicate(bool async) + public override async Task Json_collection_element_access_in_projection_using_ElementAt(bool async) { - await base.Json_collection_element_access_in_predicate(async); + await base.Json_collection_element_access_in_projection_using_ElementAt(async); AssertSql( - @""); +""" +SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[1]'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_collection_element_access_in_projection_using_ElementAtOrDefault(bool async) + { + await base.Json_collection_element_access_in_projection_using_ElementAtOrDefault(async); + + AssertSql( +""" +SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[1]'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_collection_element_access_in_projection_project_collection(bool async) + { + await base.Json_collection_element_access_in_projection_project_collection(async); + + AssertSql( +""" +SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[1].OwnedCollectionBranch'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_collection_element_access_in_projection_using_ElementAt_project_collection(bool async) + { + await base.Json_collection_element_access_in_projection_using_ElementAt_project_collection(async); + + AssertSql( +""" +SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[1].OwnedCollectionBranch'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_collection_element_access_in_projection_using_ElementAtOrDefault_project_collection(bool async) + { + await base.Json_collection_element_access_in_projection_using_ElementAtOrDefault_project_collection(async); + + AssertSql( +""" +SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[1].OwnedCollectionBranch'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_collection_element_access_in_projection_using_parameter(bool async) + { + await base.Json_collection_element_access_in_projection_using_parameter(async); + + AssertSql( +""" +@__prm_0='0' + +SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[' + CAST(@__prm_0 AS nvarchar(max)) + ']'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_collection_element_access_in_projection_using_column(bool async) + { + await base.Json_collection_element_access_in_projection_using_column(async); + + AssertSql( +""" +SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[' + CAST([j].[Id] AS nvarchar(max)) + ']'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_collection_element_access_in_projection_nested(bool async) + { + await base.Json_collection_element_access_in_projection_nested(async); + + AssertSql( +""" +@__prm_0='1' + +SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[0].OwnedCollectionBranch[' + CAST(@__prm_0 AS nvarchar(max)) + ']'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_collection_element_access_in_projection_nested_project_scalar(bool async) + { + await base.Json_collection_element_access_in_projection_nested_project_scalar(async); + + AssertSql( +""" +@__prm_0='1' + +SELECT CAST(JSON_VALUE([j].[OwnedCollectionRoot],'$[0].OwnedCollectionBranch[' + CAST(@__prm_0 AS nvarchar(max)) + '].Date') AS datetime2) +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_collection_element_access_in_projection_nested_project_reference(bool async) + { + await base.Json_collection_element_access_in_projection_nested_project_reference(async); + + AssertSql( +""" +@__prm_0='1' + +SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[0].OwnedCollectionBranch[' + CAST(@__prm_0 AS nvarchar(max)) + '].OwnedReferenceLeaf'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_collection_element_access_in_projection_nested_project_collection(bool async) + { + await base.Json_collection_element_access_in_projection_nested_project_collection(async); + + AssertSql( +""" +@__prm_0='1' + +SELECT JSON_QUERY([j].[OwnedCollectionRoot],'$[0].OwnedCollectionBranch[' + CAST(@__prm_0 AS nvarchar(max)) + '].OwnedCollectionLeaf'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +ORDER BY [j].[Id] +"""); + } + + public override async Task Json_collection_element_access_in_projection_nested_project_collection_anonymous_projection(bool async) + { + await base.Json_collection_element_access_in_projection_nested_project_collection_anonymous_projection(async); + + AssertSql( +""" +@__prm_0='1' + +SELECT [j].[Id], JSON_QUERY([j].[OwnedCollectionRoot],'$[0].OwnedCollectionBranch[' + CAST(@__prm_0 AS nvarchar(max)) + '].OwnedCollectionLeaf') +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_collection_element_access_in_predicate_using_constant(bool async) + { + await base.Json_collection_element_access_in_predicate_using_constant(async); + + AssertSql( +""" +SELECT [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +WHERE JSON_VALUE([j].[OwnedCollectionRoot],'$[0].Name') <> N'Foo' OR (JSON_VALUE([j].[OwnedCollectionRoot],'$[0].Name') IS NULL) +"""); + } + + public override async Task Json_collection_element_access_in_predicate_using_variable(bool async) + { + await base.Json_collection_element_access_in_predicate_using_variable(async); + + AssertSql( +""" +@__prm_0='1' + +SELECT [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +WHERE JSON_VALUE([j].[OwnedCollectionRoot],'$[' + CAST(@__prm_0 AS nvarchar(max)) + '].Name') <> N'Foo' OR (JSON_VALUE([j].[OwnedCollectionRoot],'$[' + CAST(@__prm_0 AS nvarchar(max)) + '].Name') IS NULL) +"""); + } + + public override async Task Json_collection_element_access_in_predicate_using_column(bool async) + { + await base.Json_collection_element_access_in_predicate_using_column(async); + + AssertSql( +""" +SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE JSON_VALUE([j].[OwnedCollectionRoot],'$[' + CAST([j].[Id] AS nvarchar(max)) + '].Name') = N'e1_c2' +"""); + } + + public override async Task Json_collection_element_access_in_predicate_using_complex_expression1(bool async) + { + await base.Json_collection_element_access_in_predicate_using_complex_expression1(async); + + AssertSql( +""" +SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE JSON_VALUE([j].[OwnedCollectionRoot],'$[' + CAST(CASE + WHEN [j].[Id] = 1 THEN 0 + ELSE 1 +END AS nvarchar(max)) + '].Name') = N'e1_c1' +"""); + } + + public override async Task Json_collection_element_access_in_predicate_using_complex_expression2(bool async) + { + await base.Json_collection_element_access_in_predicate_using_complex_expression2(async); + + AssertSql( +""" +SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE JSON_VALUE([j].[OwnedCollectionRoot],'$[' + CAST(( + SELECT MAX([j].[Id]) + FROM [JsonEntitiesBasic] AS [j]) AS nvarchar(max)) + '].Name') = N'e1_c2' +"""); + } + + public override async Task Json_collection_element_access_in_predicate_using_ElementAt(bool async) + { + await base.Json_collection_element_access_in_predicate_using_ElementAt(async); + + AssertSql( +""" +SELECT [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +WHERE JSON_VALUE([j].[OwnedCollectionRoot],'$[1].Name') <> N'Foo' OR (JSON_VALUE([j].[OwnedCollectionRoot],'$[1].Name') IS NULL) +"""); + } + + public override async Task Json_collection_element_access_in_predicate_nested_mix(bool async) + { + await base.Json_collection_element_access_in_predicate_nested_mix(async); + + AssertSql( +""" +@__prm_0='0' + +SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE JSON_VALUE([j].[OwnedCollectionRoot],'$[1].OwnedCollectionBranch[' + CAST(@__prm_0 AS nvarchar(max)) + '].OwnedCollectionLeaf[' + CAST([j].[Id] - 1 AS nvarchar(max)) + '].SomethingSomething') = N'e1_c2_c1_c1' +"""); + } + + public override async Task Json_projection_deduplication_with_collection_indexer_in_original(bool async) + { + await base.Json_projection_deduplication_with_collection_indexer_in_original(async); + + AssertSql( +""" +SELECT [j].[Id], JSON_QUERY([j].[OwnedCollectionRoot],'$[0]') +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_projection_deduplication_with_collection_indexer_in_target(bool async) + { + await base.Json_projection_deduplication_with_collection_indexer_in_target(async); + + AssertSql( +""" +@__prm_0='1' + +SELECT [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot],'$.OwnedCollectionBranch[1]'), [j].[OwnedReferenceRoot], JSON_QUERY([j].[OwnedReferenceRoot],'$.OwnedReferenceBranch.OwnedCollectionLeaf[' + CAST(@__prm_0 AS nvarchar(max)) + ']') +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_projection_deduplication_with_collection_in_original_and_collection_indexer_in_target(bool async) + { + await base.Json_projection_deduplication_with_collection_in_original_and_collection_indexer_in_target(async); + + AssertSql( +""" +SELECT [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot],'$.OwnedCollectionBranch'), JSON_QUERY([j].[OwnedReferenceRoot],'$.OwnedCollectionBranch[1]') +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_collection_element_access_in_projection_requires_NoTracking_even_if_owner_is_present(bool async) + { + await base.Json_collection_element_access_in_projection_requires_NoTracking_even_if_owner_is_present(async); + + AssertSql(); + } + + public override async Task Json_collection_element_access_in_projection_requires_NoTracking_even_if_owner_is_present2(bool async) + { + await base.Json_collection_element_access_in_projection_requires_NoTracking_even_if_owner_is_present2(async); + + AssertSql(); } public override async Task Json_scalar_required_null_semantics(bool async) @@ -639,6 +917,21 @@ GROUP BY [t].[Key] """); } + public override async Task Group_by_on_json_scalar_using_collection_indexer(bool async) + { + await base.Group_by_on_json_scalar_using_collection_indexer(async); + + AssertSql( +""" +SELECT [t].[Key], COUNT(*) AS [Count] +FROM ( + SELECT JSON_VALUE([j].[OwnedCollectionRoot],'$[0].Name') AS [Key] + FROM [JsonEntitiesBasic] AS [j] +) AS [t] +GROUP BY [t].[Key] +"""); + } + public override async Task Group_by_First_on_json_scalar(bool async) { await base.Group_by_First_on_json_scalar(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs index 648539cedde..c3281b75bf2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs @@ -13126,6 +13126,17 @@ ORDER BY [t].[Nickname] """); } + public override async Task Using_indexer_on_byte_array_and_string_in_projection(bool async) + { + await base.Using_indexer_on_byte_array_and_string_in_projection(async); + + AssertSql( +""" +SELECT [s].[Id], CAST(SUBSTRING([s].[Banner], 0 + 1, 1) AS tinyint), [s].[Name] +FROM [Squads] AS [s] +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index 94fc8da2bf6..71d9c0fccde 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -11288,6 +11288,17 @@ ORDER BY [g].[Nickname] """); } + public override async Task Using_indexer_on_byte_array_and_string_in_projection(bool async) + { + await base.Using_indexer_on_byte_array_and_string_in_projection(async); + + AssertSql( +""" +SELECT [s].[Id], CAST(SUBSTRING([s].[Banner], 0 + 1, 1) AS tinyint), [s].[Name] +FROM [Squads] AS [s] +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index e26fc12d9d5..2081b1e7282 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -9862,6 +9862,17 @@ ORDER BY [g].[Nickname] """); } + public override async Task Using_indexer_on_byte_array_and_string_in_projection(bool async) + { + await base.Using_indexer_on_byte_array_and_string_in_projection(async); + + AssertSql( +""" +SELECT [s].[Id], CAST(SUBSTRING([s].[Banner], 0 + 1, 1) AS tinyint), [s].[Name] +FROM [Squads] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [s] +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index 4dbe4b83639..22881f07e98 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -9433,6 +9433,17 @@ ORDER BY "g"."Nickname" """); } + public override async Task Using_indexer_on_byte_array_and_string_in_projection(bool async) + { + await base.Using_indexer_on_byte_array_and_string_in_projection(async); + + AssertSql( +""" +SELECT "s"."Id", "s"."Banner", "s"."Name" +FROM "Squads" AS "s" +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); }