Skip to content

Commit b4c7a0e

Browse files
committed
Fix indexing on nested complex JSON collections (dotnet#37017)
Fixes dotnet#37016 (cherry picked from commit d5d1933)
1 parent 12b8d44 commit b4c7a0e

File tree

8 files changed

+165
-20
lines changed

8 files changed

+165
-20
lines changed

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ namespace Microsoft.EntityFrameworkCore.Query;
1111
/// <inheritdoc />
1212
public partial class RelationalQueryableMethodTranslatingExpressionVisitor : QueryableMethodTranslatingExpressionVisitor
1313
{
14+
private static readonly bool UseOldBehavior37016 =
15+
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37016", out var enabled) && enabled;
16+
1417
private const string SqlQuerySingleColumnAlias = "Value";
1518

1619
private readonly RelationalSqlTranslatingExpressionVisitor _sqlTranslator;
@@ -1945,12 +1948,19 @@ static TableExpressionBase FindRootTableExpressionForColumn(SelectExpression sel
19451948
}
19461949

19471950
source = source.UnwrapTypeConversion(out var convertedType);
1948-
if (source is not StructuralTypeShaperExpression shaper)
1951+
1952+
var type = source switch
1953+
{
1954+
StructuralTypeShaperExpression shaper => shaper.StructuralType,
1955+
JsonQueryExpression jsonQuery when !UseOldBehavior37016 => jsonQuery.StructuralType,
1956+
_ => null
1957+
};
1958+
1959+
if (type is null)
19491960
{
19501961
return null;
19511962
}
19521963

1953-
var type = shaper.StructuralType;
19541964
if (convertedType != null)
19551965
{
19561966
Check.DebugAssert(
@@ -1969,22 +1979,65 @@ static TableExpressionBase FindRootTableExpressionForColumn(SelectExpression sel
19691979
var property = type.FindProperty(memberName);
19701980
if (property?.IsPrimitiveCollection is true)
19711981
{
1972-
return source.CreateEFPropertyExpression(property);
1982+
return source!.CreateEFPropertyExpression(property);
19731983
}
19741984

19751985
// See comments on indexing-related hacks in VisitMethodCall above
1976-
if (_bindComplexProperties
1977-
&& type.FindComplexProperty(memberName) is { IsCollection: true } complexProperty)
1986+
if (UseOldBehavior37016)
1987+
{
1988+
if (_bindComplexProperties && type.FindComplexProperty(memberName) is { IsCollection: true } complexProperty)
1989+
{
1990+
Check.DebugAssert(complexProperty.ComplexType.IsMappedToJson());
1991+
1992+
if (queryableTranslator._sqlTranslator.TryBindMember(
1993+
queryableTranslator._sqlTranslator.Visit(source), MemberIdentity.Create(memberName),
1994+
out var translatedExpression, out _)
1995+
&& translatedExpression is CollectionResultExpression { QueryExpression: JsonQueryExpression jsonQuery })
1996+
{
1997+
return jsonQuery;
1998+
}
1999+
}
2000+
}
2001+
else if (_bindComplexProperties && type.FindComplexProperty(memberName) is IComplexProperty complexProperty)
19782002
{
1979-
Check.DebugAssert(complexProperty.ComplexType.IsMappedToJson());
2003+
Expression? translatedExpression;
19802004

1981-
if (queryableTranslator._sqlTranslator.TryBindMember(
1982-
queryableTranslator._sqlTranslator.Visit(source), MemberIdentity.Create(memberName),
1983-
out var translatedExpression, out _)
1984-
&& translatedExpression is CollectionResultExpression { QueryExpression: JsonQueryExpression jsonQuery })
2005+
if (source is JsonQueryExpression jsonSource)
19852006
{
1986-
return jsonQuery;
2007+
translatedExpression = jsonSource.BindStructuralProperty(complexProperty);
19872008
}
2009+
else if (!queryableTranslator._sqlTranslator.TryBindMember(
2010+
queryableTranslator._sqlTranslator.Visit(source), MemberIdentity.Create(memberName),
2011+
out translatedExpression, out _))
2012+
{
2013+
return null;
2014+
}
2015+
2016+
// Hack: when returning a StructuralTypeShaperExpression, _sqlTranslator returns it wrapped by a
2017+
// StructuralTypeReferenceExpression, which is supposed to be a private wrapper only within the SQL translator.
2018+
// Call TranslateProjection to unwrap it (need to look into getting rid StructuralTypeReferenceExpression altogether).
2019+
if (translatedExpression is not JsonQueryExpression and not CollectionResultExpression)
2020+
{
2021+
if (queryableTranslator._sqlTranslator.TranslateProjection(translatedExpression) is { } unwrappedTarget)
2022+
{
2023+
translatedExpression = unwrappedTarget;
2024+
}
2025+
else
2026+
{
2027+
return null;
2028+
}
2029+
}
2030+
2031+
return complexProperty switch
2032+
{
2033+
{ IsCollection: false } when translatedExpression is StructuralTypeShaperExpression { ValueBufferExpression: JsonQueryExpression jsonQuery }
2034+
=> jsonQuery,
2035+
{ IsCollection: true } when translatedExpression is CollectionResultExpression { QueryExpression: JsonQueryExpression jsonQuery }
2036+
=> jsonQuery,
2037+
{ IsCollection: true } when translatedExpression is JsonQueryExpression jsonQuery
2038+
=> jsonQuery,
2039+
_ => null
2040+
};
19882041
}
19892042

19902043
return null;

src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
2020
/// </summary>
2121
public class SqlServerQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor
2222
{
23+
private static readonly bool UseOldBehavior37016 =
24+
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37016", out var enabled) && enabled;
25+
2326
private readonly SqlServerQueryCompilationContext _queryCompilationContext;
2427
private readonly IRelationalTypeMappingSource _typeMappingSource;
2528
private readonly ISqlExpressionFactory _sqlExpressionFactory;
@@ -377,7 +380,7 @@ IComplexType complexType
377380
} selectExpression
378381
when TranslateExpression(index) is { } translatedIndex
379382
&& _sqlServerSingletonOptions.SupportsJsonFunctions
380-
&& TryTranslate(selectExpression, valuesParameter, translatedIndex, out var result):
383+
&& TryTranslate(selectExpression, valuesParameter, path: null, translatedIndex, out var result):
381384
return result;
382385

383386
// Index on JSON array
@@ -406,7 +409,7 @@ when TranslateExpression(index) is { } translatedIndex
406409
} selectExpression
407410
when orderingTableAlias == openJsonExpression.Alias
408411
&& TranslateExpression(index) is { } translatedIndex
409-
&& TryTranslate(selectExpression, jsonArrayColumn, translatedIndex, out var result):
412+
&& TryTranslate(selectExpression, jsonArrayColumn, openJsonExpression.Path, translatedIndex, out var result):
410413
return result;
411414
}
412415
}
@@ -415,7 +418,8 @@ when TranslateExpression(index) is { } translatedIndex
415418

416419
bool TryTranslate(
417420
SelectExpression selectExpression,
418-
SqlExpression jsonArrayColumn,
421+
SqlExpression jsonColumn,
422+
IReadOnlyList<PathSegment>? path,
419423
SqlExpression translatedIndex,
420424
[NotNullWhen(true)] out ShapedQueryExpression? result)
421425
{
@@ -441,16 +445,22 @@ bool TryTranslate(
441445
return false;
442446
}
443447

444-
// If the inner expression happens to itself be a JsonScalarExpression, simply append the two paths to avoid creating
448+
// If the inner expression happens to itself be a JsonScalarExpression, simply append the paths to avoid creating
445449
// JSON_VALUE within JSON_VALUE.
446-
var (json, path) = jsonArrayColumn is JsonScalarExpression innerJsonScalarExpression
447-
? (innerJsonScalarExpression.Json,
448-
innerJsonScalarExpression.Path.Append(new(translatedIndex)).ToArray())
449-
: (jsonArrayColumn, [new(translatedIndex)]);
450+
var (json, newPath) = jsonColumn is JsonScalarExpression innerJsonScalarExpression
451+
? (innerJsonScalarExpression.Json, new List<PathSegment>(innerJsonScalarExpression.Path))
452+
: (jsonColumn, []);
453+
454+
if (!UseOldBehavior37016 && path is not null)
455+
{
456+
newPath.AddRange(path);
457+
}
458+
459+
newPath.Add(new(translatedIndex));
450460

451461
var translation = new JsonScalarExpression(
452462
json,
453-
path,
463+
newPath,
454464
projection.Type,
455465
projection.TypeMapping,
456466
projectionColumn.IsNullable);

test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionCosmosTest.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,18 @@ FROM root c
126126
""");
127127
}
128128

129+
public override async Task Index_on_nested_collection()
130+
{
131+
await base.Index_on_nested_collection();
132+
133+
AssertSql(
134+
"""
135+
SELECT VALUE c
136+
FROM root c
137+
WHERE (c["RequiredAssociate"]["NestedCollection"][0]["Int"] = 8)
138+
""");
139+
}
140+
129141
#endregion Index
130142

131143
#region GroupBy

test/EFCore.Specification.Tests/Query/Associations/AssociationsCollectionTestBase.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ public virtual Task Index_column()
7575
ss => ss.Set<RootEntity>().Where(e => e.AssociateCollection[e.Id - 1].Int == 8),
7676
ss => ss.Set<RootEntity>().Where(e => e.AssociateCollection.Count > e.Id - 1 && e.AssociateCollection[e.Id - 1].Int == 8)));
7777

78+
[ConditionalFact]
79+
public virtual Task Index_on_nested_collection()
80+
=> AssertOrderedCollectionQuery(() => AssertQuery(
81+
ss => ss.Set<RootEntity>().Where(e => e.RequiredAssociate.NestedCollection[0].Int == 8),
82+
ss => ss.Set<RootEntity>().Where(
83+
e => e.RequiredAssociate.NestedCollection.Count > 0
84+
&& e.RequiredAssociate.NestedCollection[0].Int == 8)));
85+
7886
[ConditionalFact]
7987
public virtual Task Index_out_of_bounds()
8088
=> AssertOrderedCollectionQuery(() => AssertQuery(

test/EFCore.SqlServer.FunctionalTests/Query/Associations/ComplexJson/ComplexJsonCollectionSqlServerTest.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,30 @@ WHERE CAST(JSON_VALUE([r].[AssociateCollection], '$[' + CAST([r].[Id] - 1 AS nva
244244
}
245245
}
246246

247+
public override async Task Index_on_nested_collection()
248+
{
249+
await base.Index_on_nested_collection();
250+
251+
if (Fixture.UsingJsonType)
252+
{
253+
AssertSql(
254+
"""
255+
SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate]
256+
FROM [RootEntity] AS [r]
257+
WHERE JSON_VALUE([r].[RequiredAssociate], '$.NestedCollection[0].Int' RETURNING int) = 8
258+
""");
259+
}
260+
else
261+
{
262+
AssertSql(
263+
"""
264+
SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate]
265+
FROM [RootEntity] AS [r]
266+
WHERE CAST(JSON_VALUE([r].[RequiredAssociate], '$.NestedCollection[0].Int') AS int) = 8
267+
""");
268+
}
269+
}
270+
247271
public override async Task Index_out_of_bounds()
248272
{
249273
await base.Index_out_of_bounds();

test/EFCore.SqlServer.FunctionalTests/Query/Associations/Navigations/NavigationsCollectionSqlServerTest.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,13 @@ public override async Task Index_column()
199199
AssertSql();
200200
}
201201

202+
public override async Task Index_on_nested_collection()
203+
{
204+
await base.Index_on_nested_collection();
205+
206+
AssertSql();
207+
}
208+
202209
public override async Task Index_out_of_bounds()
203210
{
204211
await base.Index_out_of_bounds();

test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedJson/OwnedJsonCollectionSqlServerTest.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,30 @@ WHERE CAST(JSON_VALUE([r].[AssociateCollection], '$[' + CAST([r].[Id] - 1 AS nva
317317
}
318318
}
319319

320+
public override async Task Index_on_nested_collection()
321+
{
322+
await base.Index_on_nested_collection();
323+
324+
if (Fixture.UsingJsonType)
325+
{
326+
AssertSql(
327+
"""
328+
SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate]
329+
FROM [RootEntity] AS [r]
330+
WHERE JSON_VALUE([r].[RequiredAssociate], '$.NestedCollection[0].Int' RETURNING int) = 8
331+
""");
332+
}
333+
else
334+
{
335+
AssertSql(
336+
"""
337+
SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate]
338+
FROM [RootEntity] AS [r]
339+
WHERE CAST(JSON_VALUE([r].[RequiredAssociate], '$.NestedCollection[0].Int') AS int) = 8
340+
""");
341+
}
342+
}
343+
320344
public override async Task Index_out_of_bounds()
321345
{
322346
await base.Index_out_of_bounds();

test/EFCore.SqlServer.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsCollectionSqlServerTest.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,13 @@ public override async Task Index_column()
202202
AssertSql();
203203
}
204204

205+
public override async Task Index_on_nested_collection()
206+
{
207+
await base.Index_on_nested_collection();
208+
209+
AssertSql();
210+
}
211+
205212
public override async Task Index_out_of_bounds()
206213
{
207214
await base.Index_out_of_bounds();

0 commit comments

Comments
 (0)