Skip to content

Commit 604c7dd

Browse files
committed
Fix uncorrelated parameterized collections in compiled queries
Fixes #37370
1 parent 94b2ee9 commit 604c7dd

File tree

9 files changed

+170
-28
lines changed

9 files changed

+170
-28
lines changed

src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,14 +175,30 @@ protected virtual ValuesExpression ApplyTypeMappingsOnValuesExpression(ValuesExp
175175

176176
// VALUES over a values parameter (i.e. a parameter representing the entire collection, that will be constantized into the SQL
177177
// later). Apply the inferred type mapping on the parameter.
178-
case { ValuesParameter: { TypeMapping: null } valuesParameter }
179-
when inferredTypeMappings[1] is { } elementTypeMapping:
178+
case { ValuesParameter: { TypeMapping: null } valuesParameter }:
180179
{
181-
if (RelationalDependencies.TypeMappingSource.FindMapping(
182-
valuesParameter.Type, QueryCompilationContext.Model, elementTypeMapping) is not
183-
{ ElementTypeMapping: not null } collectionParameterTypeMapping)
180+
RelationalTypeMapping? collectionParameterTypeMapping;
181+
182+
if (inferredTypeMappings[1] is { } elementTypeMapping)
183+
{
184+
// In the usual case, some operation performed against the elements of the collection (e.g. comparison to a column) provides us with
185+
// an element type mapping; infer the collection's type mapping from that.
186+
collectionParameterTypeMapping = RelationalDependencies.TypeMappingSource.FindMapping(
187+
valuesParameter.Type, QueryCompilationContext.Model, elementTypeMapping);
188+
189+
if (collectionParameterTypeMapping is not { ElementTypeMapping: not null })
190+
{
191+
throw new UnreachableException("A RelationalTypeMapping collection type mapping could not be found");
192+
}
193+
}
194+
else
184195
{
185-
throw new UnreachableException("A RelationalTypeMapping collection type mapping could not be found");
196+
// We have no inferred type mapping for the element type. This means that there's was nothing in the query done
197+
// against the elements of the collection (e.g. comparison to a column), which tells us what type mapping it is.
198+
// In normal circumstances, such an expression would get client-evaluated in the funceltizer (no reference to a
199+
// column/database-side object), but with compiled queries the collection parameter gets preserved as-is.
200+
// The only thing we can do is apply the default type mapping.
201+
collectionParameterTypeMapping = RelationalDependencies.TypeMappingSource.FindMapping(valuesParameter.Type, QueryCompilationContext.Model);
186202
}
187203

188204
return valuesExpression.Update(valuesParameter.ApplyTypeMapping(collectionParameterTypeMapping));

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

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ protected override Expression VisitExtension(Expression expression)
4343
=> expression switch
4444
{
4545
SqlServerOpenJsonExpression openJsonExpression
46-
when TryGetInferredTypeMapping(openJsonExpression.Alias, "value", out var typeMapping)
47-
=> ApplyTypeMappingsOnOpenJsonExpression(openJsonExpression, [typeMapping]),
46+
=> ApplyTypeMappingsOnOpenJsonExpression(openJsonExpression),
4847

4948
_ => base.VisitExtension(expression)
5049
};
@@ -55,13 +54,8 @@ when TryGetInferredTypeMapping(openJsonExpression.Alias, "value", out var typeMa
5554
/// any release. You should only use it directly in your code with extreme caution and knowing that
5655
/// doing so can result in application failures when updating to a new Entity Framework Core release.
5756
/// </summary>
58-
protected virtual SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpression(
59-
SqlServerOpenJsonExpression openJsonExpression,
60-
IReadOnlyList<RelationalTypeMapping> typeMappings)
57+
protected virtual SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpression(SqlServerOpenJsonExpression openJsonExpression)
6158
{
62-
Check.DebugAssert(typeMappings.Count == 1);
63-
var elementTypeMapping = typeMappings[0];
64-
6559
// Constant queryables are translated to VALUES, no need for JSON.
6660
// Column queryables have their type mapping from the model, so we don't ever need to apply an inferred mapping on them.
6761
if (openJsonExpression.JsonExpression is not SqlParameterExpression { TypeMapping: null } parameterExpression)
@@ -77,22 +71,45 @@ protected virtual SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpress
7771
Check.DebugAssert(
7872
openJsonExpression.ColumnInfos is null, "OpenJsonExpression has no ColumnInfos when applying an inferred type mapping");
7973

80-
// We need to apply the inferred type mapping in two places: the collection type mapping on the parameter expanded by OPENJSON,
81-
// and on the WITH clause determining the conversion out on the SQL Server side
82-
83-
// First, find the collection type mapping and apply it to the parameter
84-
if (_typeMappingSource.FindMapping(parameterExpression.Type, _model, elementTypeMapping) is not SqlServerStringTypeMapping
85-
{
86-
ElementTypeMapping: not null
87-
}
88-
parameterTypeMapping)
74+
// In the usual case, some operation performed against the elements of the collection (e.g. comparison to a column) provides us with
75+
// an element type mapping; infer the collection's type mapping from that.
76+
// NOTE: This assumes that the OPENJSON always returns only a single column, which is currently true but won't always be.
77+
if (TryGetInferredTypeMapping(openJsonExpression.Alias, "value", out var elementTypeMapping))
8978
{
90-
throw new UnreachableException("A SqlServerStringTypeMapping collection type mapping could not be found");
79+
// We need to apply the inferred type mapping in two places: the collection type mapping on the parameter expanded by OPENJSON,
80+
// and on the WITH clause determining the conversion out on the SQL Server side
81+
82+
// First, find the collection type mapping and apply it to the parameter
83+
var parameterTypeMapping = _typeMappingSource.FindMapping(parameterExpression.Type, _model, elementTypeMapping);
84+
85+
if (parameterTypeMapping is not SqlServerStringTypeMapping { ElementTypeMapping: not null })
86+
{
87+
throw new UnreachableException("A SqlServerStringTypeMapping collection type mapping could not be found");
88+
}
89+
90+
return openJsonExpression.Update(
91+
parameterExpression.ApplyTypeMapping(parameterTypeMapping),
92+
path: null,
93+
[new SqlServerOpenJsonExpression.ColumnInfo("value", elementTypeMapping, [])]);
9194
}
95+
else
96+
{
97+
// We have no inferred type mapping for the element type. This means that there's was nothing in the query done
98+
// against the elements of the collection (e.g. comparison to a column), which tells us what type mapping it is.
99+
// In normal circumstances, such an expression would get client-evaluated in the funceltizer (no reference to a
100+
// column/database-side object), but with compiled queries the collection parameter gets preserved as-is.
101+
// The only thing we can do is apply the default type mapping.
102+
var parameterTypeMapping = RelationalDependencies.TypeMappingSource.FindMapping(parameterExpression.Type, QueryCompilationContext.Model);
92103

93-
return openJsonExpression.Update(
94-
parameterExpression.ApplyTypeMapping(parameterTypeMapping),
95-
path: null,
96-
[new SqlServerOpenJsonExpression.ColumnInfo("value", elementTypeMapping, [])]);
104+
if (parameterTypeMapping is not { ElementTypeMapping: RelationalTypeMapping elementTypeMapping2 })
105+
{
106+
throw new UnreachableException("Default type mapping has no element type mapping");
107+
}
108+
109+
return openJsonExpression.Update(
110+
parameterExpression.ApplyTypeMapping(parameterTypeMapping),
111+
path: null,
112+
[new SqlServerOpenJsonExpression.ColumnInfo("value", elementTypeMapping2, [])]);
113+
}
97114
}
98115
}

test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1738,6 +1738,20 @@ public override async Task Parameter_collection_in_subquery_Union_another_parame
17381738
AssertSql();
17391739
}
17401740

1741+
public override async Task Compiled_query_with_uncorrelated_parameter_collection_expression()
1742+
{
1743+
await base.Compiled_query_with_uncorrelated_parameter_collection_expression();
1744+
1745+
AssertSql(
1746+
"""
1747+
@ids='[]'
1748+
1749+
SELECT VALUE c
1750+
FROM root c
1751+
WHERE (ARRAY_LENGTH(@ids) > 0)
1752+
""");
1753+
}
1754+
17411755
public override async Task Column_collection_in_subquery_Union_parameter_collection()
17421756
{
17431757
await base.Column_collection_in_subquery_Union_parameter_collection();

test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1384,6 +1384,16 @@ public virtual async Task Parameter_collection_in_subquery_Union_another_paramet
13841384
_ = compiledQuery(context, ints1, ints2).ToList();
13851385
}
13861386

1387+
[ConditionalFact] // #37370
1388+
public virtual async Task Compiled_query_with_uncorrelated_parameter_collection_expression()
1389+
{
1390+
var func = EF.CompileAsyncQuery(
1391+
(PrimitiveCollectionsContext context, int[] ids) => context.Set<PrimitiveCollectionsEntity>().Where(e => ids.Any()));
1392+
1393+
await using var context = Fixture.CreateContext();
1394+
_ = await func(context, []).ToListAsync();
1395+
}
1396+
13871397
[ConditionalFact]
13881398
public virtual Task Column_collection_in_subquery_Union_parameter_collection()
13891399
{

test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,6 +1401,23 @@ public override async Task Parameter_collection_in_subquery_Union_another_parame
14011401
AssertSql();
14021402
}
14031403

1404+
public override async Task Compiled_query_with_uncorrelated_parameter_collection_expression()
1405+
{
1406+
await base.Compiled_query_with_uncorrelated_parameter_collection_expression();
1407+
1408+
AssertSql(
1409+
"""
1410+
SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId]
1411+
FROM [PrimitiveCollectionsEntity] AS [p]
1412+
WHERE EXISTS (
1413+
SELECT 1
1414+
FROM (
1415+
SELECT NULL AS [Value]
1416+
WHERE 0 = 1
1417+
) AS [i])
1418+
""");
1419+
}
1420+
14041421
public override async Task Project_collection_of_ints_simple()
14051422
{
14061423
await base.Project_collection_of_ints_simple();

test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2021,6 +2021,23 @@ public override async Task Parameter_collection_in_subquery_Union_another_parame
20212021
AssertSql();
20222022
}
20232023

2024+
public override async Task Compiled_query_with_uncorrelated_parameter_collection_expression()
2025+
{
2026+
await base.Compiled_query_with_uncorrelated_parameter_collection_expression();
2027+
2028+
AssertSql(
2029+
"""
2030+
SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId]
2031+
FROM [PrimitiveCollectionsEntity] AS [p]
2032+
WHERE EXISTS (
2033+
SELECT 1
2034+
FROM (
2035+
SELECT NULL AS [Value]
2036+
WHERE 0 = 1
2037+
) AS [i])
2038+
""");
2039+
}
2040+
20242041
public override async Task Column_collection_in_subquery_Union_parameter_collection()
20252042
{
20262043
await base.Column_collection_in_subquery_Union_parameter_collection();

test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2168,6 +2168,23 @@ public override async Task Parameter_collection_in_subquery_Union_another_parame
21682168
AssertSql();
21692169
}
21702170

2171+
public override async Task Compiled_query_with_uncorrelated_parameter_collection_expression()
2172+
{
2173+
await base.Compiled_query_with_uncorrelated_parameter_collection_expression();
2174+
2175+
AssertSql(
2176+
"""
2177+
SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId]
2178+
FROM [PrimitiveCollectionsEntity] AS [p]
2179+
WHERE EXISTS (
2180+
SELECT 1
2181+
FROM (
2182+
SELECT NULL AS [Value]
2183+
WHERE 0 = 1
2184+
) AS [i])
2185+
""");
2186+
}
2187+
21712188
public override async Task Column_collection_in_subquery_Union_parameter_collection()
21722189
{
21732190
await base.Column_collection_in_subquery_Union_parameter_collection();

test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2044,6 +2044,23 @@ public override async Task Parameter_collection_in_subquery_Union_another_parame
20442044
AssertSql();
20452045
}
20462046

2047+
public override async Task Compiled_query_with_uncorrelated_parameter_collection_expression()
2048+
{
2049+
await base.Compiled_query_with_uncorrelated_parameter_collection_expression();
2050+
2051+
AssertSql(
2052+
"""
2053+
SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId]
2054+
FROM [PrimitiveCollectionsEntity] AS [p]
2055+
WHERE EXISTS (
2056+
SELECT 1
2057+
FROM (
2058+
SELECT NULL AS [Value]
2059+
WHERE 0 = 1
2060+
) AS [i])
2061+
""");
2062+
}
2063+
20472064
public override async Task Column_collection_in_subquery_Union_parameter_collection()
20482065
{
20492066
await base.Column_collection_in_subquery_Union_parameter_collection();

test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1862,6 +1862,23 @@ public override async Task Parameter_collection_in_subquery_Union_another_parame
18621862
AssertSql();
18631863
}
18641864

1865+
public override async Task Compiled_query_with_uncorrelated_parameter_collection_expression()
1866+
{
1867+
await base.Compiled_query_with_uncorrelated_parameter_collection_expression();
1868+
1869+
AssertSql(
1870+
"""
1871+
SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."NullableWrappedId", "p"."NullableWrappedIdWithNullableComparer", "p"."String", "p"."Strings", "p"."WrappedId"
1872+
FROM "PrimitiveCollectionsEntity" AS "p"
1873+
WHERE EXISTS (
1874+
SELECT 1
1875+
FROM (
1876+
SELECT NULL AS "Value"
1877+
WHERE 0
1878+
) AS "i")
1879+
""");
1880+
}
1881+
18651882
public override async Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query()
18661883
{
18671884
await base.Parameter_collection_in_subquery_Union_column_collection_as_compiled_query();

0 commit comments

Comments
 (0)