Skip to content

Commit 4b15cbf

Browse files
committed
Fix indexing on nested complex JSON collections
Fixes #37016
1 parent 9f488d7 commit 4b15cbf

File tree

8 files changed

+144
-20
lines changed

8 files changed

+144
-20
lines changed

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1945,12 +1945,19 @@ static TableExpressionBase FindRootTableExpressionForColumn(SelectExpression sel
19451945
}
19461946

19471947
source = source.UnwrapTypeConversion(out var convertedType);
1948-
if (source is not StructuralTypeShaperExpression shaper)
1948+
1949+
var type = source switch
1950+
{
1951+
StructuralTypeShaperExpression shaper => shaper.StructuralType,
1952+
JsonQueryExpression jsonQuery => jsonQuery.StructuralType,
1953+
_ => null
1954+
};
1955+
1956+
if (type is null)
19491957
{
19501958
return null;
19511959
}
19521960

1953-
var type = shaper.StructuralType;
19541961
if (convertedType != null)
19551962
{
19561963
Check.DebugAssert(
@@ -1969,22 +1976,50 @@ static TableExpressionBase FindRootTableExpressionForColumn(SelectExpression sel
19691976
var property = type.FindProperty(memberName);
19701977
if (property?.IsPrimitiveCollection is true)
19711978
{
1972-
return source.CreateEFPropertyExpression(property);
1979+
return source!.CreateEFPropertyExpression(property);
19731980
}
19741981

19751982
// See comments on indexing-related hacks in VisitMethodCall above
1976-
if (_bindComplexProperties
1977-
&& type.FindComplexProperty(memberName) is { IsCollection: true } complexProperty)
1983+
if (_bindComplexProperties && type.FindComplexProperty(memberName) is IComplexProperty complexProperty)
19781984
{
1979-
Check.DebugAssert(complexProperty.ComplexType.IsMappedToJson());
1985+
Expression? translatedExpression;
1986+
1987+
if (source is JsonQueryExpression jsonSource)
1988+
{
1989+
translatedExpression = jsonSource.BindStructuralProperty(complexProperty);
1990+
}
1991+
else if (!queryableTranslator._sqlTranslator.TryBindMember(
1992+
queryableTranslator._sqlTranslator.Visit(source), MemberIdentity.Create(memberName),
1993+
out translatedExpression, out _))
1994+
{
1995+
return null;
1996+
}
19801997

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 })
1998+
// Hack: when returning a StructuralTypeShaperExpression, _sqlTranslator returns it wrapped by a
1999+
// StructuralTypeReferenceExpression, which is supposed to be a private wrapper only within the SQL translator.
2000+
// Call TranslateProjection to unwrap it (need to look into getting rid StructuralTypeReferenceExpression altogether).
2001+
if (translatedExpression is not JsonQueryExpression and not CollectionResultExpression)
19852002
{
1986-
return jsonQuery;
2003+
if (queryableTranslator._sqlTranslator.TranslateProjection(translatedExpression) is { } unwrappedTarget)
2004+
{
2005+
translatedExpression = unwrappedTarget;
2006+
}
2007+
else
2008+
{
2009+
return null;
2010+
}
19872011
}
2012+
2013+
return complexProperty switch
2014+
{
2015+
{ IsCollection: false } when translatedExpression is StructuralTypeShaperExpression { ValueBufferExpression: JsonQueryExpression jsonQuery }
2016+
=> jsonQuery,
2017+
{ IsCollection: true } when translatedExpression is CollectionResultExpression { QueryExpression: JsonQueryExpression jsonQuery }
2018+
=> jsonQuery,
2019+
{ IsCollection: true } when translatedExpression is JsonQueryExpression jsonQuery
2020+
=> jsonQuery,
2021+
_ => null
2022+
};
19882023
}
19892024

19902025
return null;

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ IComplexType complexType
377377
} selectExpression
378378
when TranslateExpression(index) is { } translatedIndex
379379
&& _sqlServerSingletonOptions.SupportsJsonFunctions
380-
&& TryTranslate(selectExpression, valuesParameter, translatedIndex, out var result):
380+
&& TryTranslate(selectExpression, valuesParameter, path: null, translatedIndex, out var result):
381381
return result;
382382

383383
// Index on JSON array
@@ -406,7 +406,7 @@ when TranslateExpression(index) is { } translatedIndex
406406
} selectExpression
407407
when orderingTableAlias == openJsonExpression.Alias
408408
&& TranslateExpression(index) is { } translatedIndex
409-
&& TryTranslate(selectExpression, jsonArrayColumn, translatedIndex, out var result):
409+
&& TryTranslate(selectExpression, jsonArrayColumn, openJsonExpression.Path, translatedIndex, out var result):
410410
return result;
411411
}
412412
}
@@ -415,7 +415,8 @@ when TranslateExpression(index) is { } translatedIndex
415415

416416
bool TryTranslate(
417417
SelectExpression selectExpression,
418-
SqlExpression jsonArrayColumn,
418+
SqlExpression jsonColumn,
419+
IReadOnlyList<PathSegment>? path,
419420
SqlExpression translatedIndex,
420421
[NotNullWhen(true)] out ShapedQueryExpression? result)
421422
{
@@ -441,16 +442,22 @@ bool TryTranslate(
441442
return false;
442443
}
443444

444-
// If the inner expression happens to itself be a JsonScalarExpression, simply append the two paths to avoid creating
445+
// If the inner expression happens to itself be a JsonScalarExpression, simply append the paths to avoid creating
445446
// 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)]);
447+
var (json, newPath) = jsonColumn is JsonScalarExpression innerJsonScalarExpression
448+
? (innerJsonScalarExpression.Json, new List<PathSegment>(innerJsonScalarExpression.Path))
449+
: (jsonColumn, []);
450+
451+
if (path is not null)
452+
{
453+
newPath.AddRange(path);
454+
}
455+
456+
newPath.Add(new(translatedIndex));
450457

451458
var translation = new JsonScalarExpression(
452459
json,
453-
path,
460+
newPath,
454461
projection.Type,
455462
projection.TypeMapping,
456463
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)