From c0c0e08735166d319bb4e5aeb0c3740f15a02b51 Mon Sep 17 00:00:00 2001 From: Maurycy Markowski Date: Thu, 3 Nov 2022 17:03:51 -0700 Subject: [PATCH] Fix to #28648 - Json/Query: translate element access of a json array Converting indexer into AsQueryable().ElementAt(x) so that nav expansion can understand it and inject MaterializeCollectionNavigationExpression in the right places. Then in translation we recognize the pattern and convert it back to element access on a JsonQueryExpression. In order to shape the results correctly, we need to add all the array indexes (that are not constants) to the projection, so that we can populate the ordinal keys correctly (and also to do de-duplication). Fixes #28648 --- ...rToElementAtConvertingExpressionVisitor.cs | 39 +++ .../Query/JsonQueryExpression.cs | 26 ++ src/EFCore.Relational/Query/PathSegment.cs | 24 +- .../RelationalQueryTranslationPreprocessor.cs | 1 + ...yableMethodTranslatingExpressionVisitor.cs | 59 ++++ ...sitor.ShaperProcessingExpressionVisitor.cs | 17 +- .../Query/SqlExpressions/SelectExpression.cs | 21 +- .../Internal/SqlServerQuerySqlGenerator.cs | 27 +- .../NavigationExpandingExpressionVisitor.cs | 2 - ...yableMethodNormalizingExpressionVisitor.cs | 31 +- .../Query/JsonQueryTestBase.cs | 304 +++++++++++++++++- .../Query/JsonQuerySqlServerTest.cs | 293 ++++++++++++++++- 12 files changed, 815 insertions(+), 29 deletions(-) create mode 100644 src/EFCore.Relational/Query/Internal/CollectionIndexerToElementAtConvertingExpressionVisitor.cs diff --git a/src/EFCore.Relational/Query/Internal/CollectionIndexerToElementAtConvertingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/CollectionIndexerToElementAtConvertingExpressionVisitor.cs new file mode 100644 index 00000000000..422e7c924a8 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/CollectionIndexerToElementAtConvertingExpressionVisitor.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.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 != typeof(string) + && methodCallExpression.Method.DeclaringType != typeof(byte[]) + && ((methodCallExpression.Method.DeclaringType.IsGenericType + && methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typeof(List<>)) + || methodCallExpression.Method.DeclaringType.IsArray)) + { + 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); + } +} diff --git a/src/EFCore.Relational/Query/JsonQueryExpression.cs b/src/EFCore.Relational/Query/JsonQueryExpression.cs index d646b593216..11326cf2159 100644 --- a/src/EFCore.Relational/Query/JsonQueryExpression.cs +++ b/src/EFCore.Relational/Query/JsonQueryExpression.cs @@ -161,6 +161,32 @@ 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) + { + var newPath = Path.Take(Path.Count - 1).ToList(); + var lastPathSegment = Path.Last(); + if (lastPathSegment.CollectionIndexExpression != null) + { + throw new InvalidOperationException("Already accessing collection element."); + } + + newPath.Add(new PathSegment(lastPathSegment.Key, 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..5c24de40b11 100644 --- a/src/EFCore.Relational/Query/PathSegment.cs +++ b/src/EFCore.Relational/Query/PathSegment.cs @@ -25,14 +25,30 @@ public PathSegment(string key) Key = key; } + /// + /// Creates a new instance of the class. + /// + /// A key which is being accessed in the JSON. + /// A collection index which is being accessed in the JSON. + public PathSegment(string key, SqlExpression collectionIndexExpression) + : this(key) + { + CollectionIndexExpression = collectionIndexExpression; + } + /// /// The key which is being accessed in the JSON. /// 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 + (CollectionIndexExpression == null ? "" : $"[{CollectionIndexExpression}]"); /// public override bool Equals(object? obj) @@ -42,9 +58,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..cb86c4d24d8 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -1640,6 +1640,65 @@ 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..602136289fb 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs @@ -416,7 +416,7 @@ 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) { // json entity at the root @@ -510,7 +510,7 @@ 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: { // json entity collection at the root var (jsonElementParameter, keyValuesParameter) = JsonShapingPreProcess( @@ -781,7 +781,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 +1236,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 +1263,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..a833a60c2e2 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 { @@ -1591,6 +1591,7 @@ static Dictionary BuildJsonProjection var ordered = projections .OrderBy(x => $"{x.JsonColumn.TableAlias}.{x.JsonColumn.Name}") .ThenBy(x => x.Path.Count); + //.ThenBy(x => x.Path.Last().CollectionIndexExpression == null); var needed = new List(); foreach (var orderedElement in ordered) @@ -1656,7 +1657,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 +1700,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 39a3ec2731a..a1cb60d1257 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs @@ -318,7 +318,32 @@ 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) + { + 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.Type != typeof(JsonElement)) { diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index 22d87b508ee..58f959c28c6 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -619,8 +619,6 @@ when QueryableMethods.IsSumWithSelector(method): // GroupJoin overloads // Zip // SequenceEqual overloads - // ElementAt - // ElementAtOrDefault // SkipWhile // TakeWhile // DefaultIfEmpty with argument diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index 659fd7cdb2d..f22b8a6bd49 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -125,13 +125,36 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp visitedExpression = base.VisitMethodCall(methodCallExpression); } - if (visitedExpression is MethodCallExpression visitedMethodCall - && visitedMethodCall.Method.DeclaringType == typeof(Queryable) - && visitedMethodCall.Method.IsGenericMethod) + if (visitedExpression is MethodCallExpression visitedMethodCall) { - return TryFlattenGroupJoinSelectMany(visitedMethodCall); + if (visitedMethodCall.Method.DeclaringType == typeof(Queryable) + && visitedMethodCall.Method.IsGenericMethod) + { + return TryFlattenGroupJoinSelectMany(visitedMethodCall); + } + + if (visitedMethodCall.Method is MethodInfo methodInfo + && methodInfo.Name == "get_Item" + && !methodInfo.IsStatic + && ((methodInfo.DeclaringType!.IsGenericType && methodInfo.DeclaringType.GetGenericTypeDefinition() == typeof(List<>)) + || methodInfo.DeclaringType.IsArray)) + { + return Expression.Call( + QueryableMethods.ElementAt.MakeGenericMethod(visitedMethodCall.Type), + Expression.Call( + QueryableMethods.AsQueryable.MakeGenericMethod(visitedMethodCall.Type), + visitedMethodCall.Object!), + visitedMethodCall.Arguments[0]); + } } + //if (visitedExpression is MethodCallExpression visitedMethodCall + // && visitedMethodCall.Method.DeclaringType == typeof(Queryable) + // && visitedMethodCall.Method.IsGenericMethod) + //{ + // return TryFlattenGroupJoinSelectMany(visitedMethodCall); + //} + return visitedExpression; } diff --git a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs index 01076a3e50e..34d2e40a295 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,301 @@ 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] + [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(Skip = "issue #28648")] + [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_predicate(bool async) + 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_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 Task Json_collection_element_access_in_projection_requires_NoTracking_even_if_owner_is_present(bool async) + { + return 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); + }); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Json_scalar_required_null_semantics(bool async) @@ -667,6 +954,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.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs index 2af33cb7814..b368a2047b6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs @@ -584,20 +584,288 @@ 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( +""" +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 CAST(JSON_VALUE([j].[OwnedCollectionRoot],'$[0].Name') AS nvarchar(max)) <> N'Foo' OR (CAST(JSON_VALUE([j].[OwnedCollectionRoot],'$[0].Name') AS nvarchar(max)) 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 CAST(JSON_VALUE([j].[OwnedCollectionRoot],'$[' + CAST(@__prm_0 AS nvarchar(max)) + '].Name') AS nvarchar(max)) <> N'Foo' OR (CAST(JSON_VALUE([j].[OwnedCollectionRoot],'$[' + CAST(@__prm_0 AS nvarchar(max)) + '].Name') AS nvarchar(max)) 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 CAST(JSON_VALUE([j].[OwnedCollectionRoot],'$[' + CAST([j].[Id] AS nvarchar(max)) + '].Name') AS nvarchar(max)) = 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 CAST(JSON_VALUE([j].[OwnedCollectionRoot],'$[' + CAST(CASE + WHEN [j].[Id] = 1 THEN 0 + ELSE 1 +END AS nvarchar(max)) + '].Name') AS nvarchar(max)) = 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 CAST(JSON_VALUE([j].[OwnedCollectionRoot],'$[' + CAST(( + SELECT MAX([j].[Id]) + FROM [JsonEntitiesBasic] AS [j]) AS nvarchar(max)) + '].Name') AS nvarchar(max)) = 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 CAST(JSON_VALUE([j].[OwnedCollectionRoot],'$[1].Name') AS nvarchar(max)) <> N'Foo' OR (CAST(JSON_VALUE([j].[OwnedCollectionRoot],'$[1].Name') AS nvarchar(max)) 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 CAST(JSON_VALUE([j].[OwnedCollectionRoot],'$[1].OwnedCollectionBranch[' + CAST(@__prm_0 AS nvarchar(max)) + '].OwnedCollectionLeaf[' + CAST([j].[Id] - 1 AS nvarchar(max)) + '].SomethingSomething') AS nvarchar(max)) = 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_scalar_required_null_semantics(bool async) @@ -639,6 +907,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 CAST(JSON_VALUE([j].[OwnedCollectionRoot],'$[0].Name') AS nvarchar(max)) 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);