Skip to content

Commit 126e6a2

Browse files
authored
[release/10.0] Fallback to OPENJSON with many parameters (#37320)
1 parent ebb6756 commit 126e6a2

File tree

9 files changed

+768
-3
lines changed

9 files changed

+768
-3
lines changed

src/EFCore.Relational/Query/SqlNullabilityProcessor.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -926,7 +926,7 @@ InExpression ProcessInExpressionValues(
926926
if (translationMode is ParameterTranslationMode.MultipleParameters)
927927
{
928928
var padFactor = CalculateParameterBucketSize(values.Count, elementTypeMapping);
929-
var padding = (padFactor - (values.Count % padFactor)) % padFactor;
929+
var padding = CalculatePadding(values.Count, padFactor);
930930
for (var i = 0; i < padding; i++)
931931
{
932932
// Create parameter for value if we didn't create it yet,
@@ -1587,6 +1587,15 @@ protected virtual int CalculateParameterBucketSize(int count, RelationalTypeMapp
15871587
_ => 200,
15881588
};
15891589

1590+
/// <summary>
1591+
/// Calculates the number of padding parameters needed to align the total count to the nearest bucket size.
1592+
/// </summary>
1593+
/// <param name="count">Number of value parameters.</param>
1594+
/// <param name="padFactor">Padding factor.</param>
1595+
[EntityFrameworkInternal]
1596+
protected virtual int CalculatePadding(int count, int padFactor)
1597+
=> (padFactor - (count % padFactor)) % padFactor;
1598+
15901599
// Note that we can check parameter values for null since we cache by the parameter nullability; but we cannot do the same for bool.
15911600
private bool IsNull(SqlExpression? expression)
15921601
=> expression is SqlConstantExpression { Value: null }

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

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ public class SqlServerSqlNullabilityProcessor : SqlNullabilityProcessor
2121
private static readonly bool UseOldBehavior37151 =
2222
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37151", out var enabled) && enabled;
2323

24+
private static readonly bool UseOldBehavior37185 =
25+
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37185", out var enabled) && enabled;
26+
2427
/// <summary>
2528
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
2629
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -33,6 +36,7 @@ public class SqlServerSqlNullabilityProcessor : SqlNullabilityProcessor
3336
private readonly ISqlServerSingletonOptions _sqlServerSingletonOptions;
3437

3538
private int _openJsonAliasCounter;
39+
private int _totalParameterCount;
3640

3741
/// <summary>
3842
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -55,6 +59,18 @@ public SqlServerSqlNullabilityProcessor(
5559
/// </summary>
5660
public override Expression Process(Expression queryExpression, ParametersCacheDecorator parametersDecorator)
5761
{
62+
if (!UseOldBehavior37185)
63+
{
64+
var parametersCounter = new ParametersCounter(
65+
parametersDecorator,
66+
CollectionParameterTranslationMode,
67+
#pragma warning disable EF1001
68+
(count, elementTypeMapping) => CalculatePadding(count, CalculateParameterBucketSize(count, elementTypeMapping)));
69+
#pragma warning restore EF1001
70+
parametersCounter.Visit(queryExpression);
71+
_totalParameterCount = parametersCounter.Count;
72+
}
73+
5874
var result = base.Process(queryExpression, parametersDecorator);
5975
_openJsonAliasCounter = 0;
6076
return result;
@@ -313,7 +329,9 @@ private bool TryHandleOverLimitParameters(
313329
// SQL Server has limit on number of parameters in a query.
314330
// If we're over that limit, we switch to using single parameter
315331
// and processing it through JSON functions.
316-
if (values.Count > MaxParameterCount)
332+
if (UseOldBehavior37185
333+
? values.Count > MaxParameterCount
334+
: _totalParameterCount > MaxParameterCount)
317335
{
318336
if (_sqlServerSingletonOptions.SupportsJsonFunctions)
319337
{
@@ -372,3 +390,107 @@ valuesExpression is not null
372390
}
373391
#pragma warning restore EF1001
374392
}
393+
394+
/// <summary>
395+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
396+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
397+
/// any release. You should only use it directly in your code with extreme caution and knowing that
398+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
399+
/// </summary>
400+
public class ParametersCounter(
401+
ParametersCacheDecorator parametersDecorator,
402+
ParameterTranslationMode collectionParameterTranslationMode,
403+
Func<int, RelationalTypeMapping, int> bucketizationPadding) : ExpressionVisitor
404+
{
405+
/// <summary>
406+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
407+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
408+
/// any release. You should only use it directly in your code with extreme caution and knowing that
409+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
410+
/// </summary>
411+
public virtual int Count { get; private set; }
412+
413+
private readonly HashSet<SqlParameterExpression> _visitedSqlParameters =
414+
new(EqualityComparer<SqlParameterExpression>.Create(
415+
(lhs, rhs) =>
416+
ReferenceEquals(lhs, rhs)
417+
|| (lhs is not null && rhs is not null
418+
&& lhs.InvariantName == rhs.InvariantName
419+
&& lhs.TranslationMode == rhs.TranslationMode),
420+
x => HashCode.Combine(x.InvariantName, x.TranslationMode)));
421+
422+
private readonly HashSet<QueryParameterExpression> _visitedQueryParameters =
423+
new(EqualityComparer<QueryParameterExpression>.Create(
424+
(lhs, rhs) =>
425+
ReferenceEquals(lhs, rhs)
426+
|| (lhs is not null && rhs is not null
427+
&& lhs.Name == rhs.Name
428+
&& lhs.TranslationMode == rhs.TranslationMode),
429+
x => HashCode.Combine(x.Name, x.TranslationMode)));
430+
431+
/// <inheritdoc/>
432+
protected override Expression VisitExtension(Expression node)
433+
{
434+
switch (node)
435+
{
436+
case ValuesExpression { ValuesParameter: { } valuesParameter }:
437+
ProcessCollectionParameter(valuesParameter, bucketization: false);
438+
break;
439+
440+
case InExpression { ValuesParameter: { } valuesParameter }:
441+
ProcessCollectionParameter(valuesParameter, bucketization: true);
442+
break;
443+
444+
case FromSqlExpression { Arguments: QueryParameterExpression queryParameter }:
445+
if (_visitedQueryParameters.Add(queryParameter))
446+
{
447+
var parameters = parametersDecorator.GetAndDisableCaching();
448+
Count += ((object?[])parameters[queryParameter.Name]!).Length;
449+
}
450+
break;
451+
452+
case SqlParameterExpression sqlParameterExpression:
453+
if (_visitedSqlParameters.Add(sqlParameterExpression))
454+
{
455+
Count++;
456+
}
457+
break;
458+
}
459+
460+
return base.VisitExtension(node);
461+
}
462+
463+
private void ProcessCollectionParameter(SqlParameterExpression sqlParameterExpression, bool bucketization)
464+
{
465+
if (!_visitedSqlParameters.Add(sqlParameterExpression))
466+
{
467+
return;
468+
}
469+
470+
switch (sqlParameterExpression.TranslationMode ?? collectionParameterTranslationMode)
471+
{
472+
case ParameterTranslationMode.MultipleParameters:
473+
var parameters = parametersDecorator.GetAndDisableCaching();
474+
var count = ((IEnumerable?)parameters[sqlParameterExpression.Name])?.Cast<object?>().Count() ?? 0;
475+
Count += count;
476+
477+
if (bucketization)
478+
{
479+
var elementTypeMapping = (RelationalTypeMapping)sqlParameterExpression.TypeMapping!.ElementTypeMapping!;
480+
Count += bucketizationPadding(count, elementTypeMapping);
481+
}
482+
483+
break;
484+
485+
case ParameterTranslationMode.Parameter:
486+
Count++;
487+
break;
488+
489+
case ParameterTranslationMode.Constant:
490+
break;
491+
492+
default:
493+
throw new UnreachableException();
494+
}
495+
}
496+
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,10 +839,42 @@ public override async Task Parameter_collection_Count_with_column_predicate_with
839839
public override Task Parameter_collection_Count_with_huge_number_of_values()
840840
=> base.Parameter_collection_Count_with_huge_number_of_values();
841841

842+
// nothing to test here
843+
public override Task Parameter_collection_Count_with_huge_number_of_values_over_5_operations()
844+
=> base.Parameter_collection_Count_with_huge_number_of_values_over_5_operations();
845+
846+
// nothing to test here
847+
public override Task Parameter_collection_Count_with_huge_number_of_values_over_5_operations_forced_constants()
848+
=> base.Parameter_collection_Count_with_huge_number_of_values_over_5_operations_forced_constants();
849+
850+
// nothing to test here
851+
public override Task Parameter_collection_Count_with_huge_number_of_values_over_5_operations_same_parameter()
852+
=> base.Parameter_collection_Count_with_huge_number_of_values_over_5_operations_same_parameter();
853+
854+
// nothing to test here
855+
public override Task Parameter_collection_Count_with_huge_number_of_values_over_5_operations_mixed_parameters_constants()
856+
=> base.Parameter_collection_Count_with_huge_number_of_values_over_5_operations_mixed_parameters_constants();
857+
842858
// nothing to test here
843859
public override Task Parameter_collection_of_ints_Contains_int_with_huge_number_of_values()
844860
=> base.Parameter_collection_of_ints_Contains_int_with_huge_number_of_values();
845861

862+
// nothing to test here
863+
public override Task Parameter_collection_of_ints_Contains_int_with_huge_number_of_values_over_5_operations()
864+
=> base.Parameter_collection_of_ints_Contains_int_with_huge_number_of_values_over_5_operations();
865+
866+
// nothing to test here
867+
public override Task Parameter_collection_of_ints_Contains_int_with_huge_number_of_values_over_5_operations_same_parameter()
868+
=> base.Parameter_collection_of_ints_Contains_int_with_huge_number_of_values_over_5_operations_same_parameter();
869+
870+
// nothing to test here
871+
public override Task Parameter_collection_of_ints_Contains_int_with_huge_number_of_values_over_5_operations_forced_constants()
872+
=> base.Parameter_collection_of_ints_Contains_int_with_huge_number_of_values_over_5_operations_forced_constants();
873+
874+
// nothing to test here
875+
public override Task Parameter_collection_of_ints_Contains_int_with_huge_number_of_values_over_5_operations_mixed_parameters_constants()
876+
=> base.Parameter_collection_of_ints_Contains_int_with_huge_number_of_values_over_5_operations_mixed_parameters_constants();
877+
846878
public override async Task Column_collection_of_ints_Contains()
847879
{
848880
await base.Column_collection_of_ints_Contains();

0 commit comments

Comments
 (0)