Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,7 @@ InExpression ProcessInExpressionValues(
if (translationMode is ParameterTranslationMode.MultipleParameters)
{
var padFactor = CalculateParameterBucketSize(values.Count, elementTypeMapping);
var padding = (padFactor - (values.Count % padFactor)) % padFactor;
var padding = CalculatePadding(values.Count, padFactor);
for (var i = 0; i < padding; i++)
{
// Create parameter for value if we didn't create it yet,
Expand Down Expand Up @@ -1553,6 +1553,13 @@ protected virtual int CalculateParameterBucketSize(int count, RelationalTypeMapp
_ => 200,
};

/// <summary>
/// Foo
/// </summary>
[EntityFrameworkInternal]
protected virtual int CalculatePadding(int count, int padFactor)
=> (padFactor - (count % padFactor)) % padFactor;

// Note that we can check parameter values for null since we cache by the parameter nullability; but we cannot do the same for bool.
private bool IsNull(SqlExpression? expression)
=> expression is SqlConstantExpression { Value: null }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.ComponentModel.Design;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;

Expand Down Expand Up @@ -30,6 +32,7 @@ public class SqlServerSqlNullabilityProcessor : SqlNullabilityProcessor
private readonly ISqlServerSingletonOptions _sqlServerSingletonOptions;

private int _openJsonAliasCounter;
private int _parametersCount;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -52,6 +55,15 @@ public SqlServerSqlNullabilityProcessor(
/// </summary>
public override Expression Process(Expression queryExpression, ParametersCacheDecorator parametersDecorator)
{
var parametersCounter = new ParametersCounter(
parametersDecorator,
CollectionParameterTranslationMode,
#pragma warning disable EF1001
(count, elementTypeMapping) => CalculatePadding(count, CalculateParameterBucketSize(count, elementTypeMapping)));
#pragma warning restore EF1001
parametersCounter.Visit(queryExpression);
_parametersCount = parametersCounter.Count;

var result = base.Process(queryExpression, parametersDecorator);
_openJsonAliasCounter = 0;
return result;
Expand Down Expand Up @@ -303,14 +315,14 @@ private bool TryHandleOverLimitParameters(
out List<SqlExpression>? constantsResult,
out bool? containsNulls)
{
var parameters = ParametersDecorator.GetAndDisableCaching();
var values = ((IEnumerable?)parameters[valuesParameter.Name])?.Cast<object>().ToList() ?? [];

// SQL Server has limit on number of parameters in a query.
// If we're over that limit, we switch to using single parameter
// and processing it through JSON functions.
if (values.Count > MaxParameterCount)
if (_parametersCount > MaxParameterCount)
{
var parameters = ParametersDecorator.GetAndDisableCaching();
var values = ((IEnumerable?)parameters[valuesParameter.Name])?.Cast<object>().ToList() ?? [];

if (_sqlServerSingletonOptions.SupportsJsonFunctions)
{
var openJsonExpression = new SqlServerOpenJsonExpression(
Expand Down Expand Up @@ -368,3 +380,75 @@ valuesExpression is not null
}
#pragma warning restore EF1001
}

/// <summary>
/// Foo
/// </summary>
public class ParametersCounter(
ParametersCacheDecorator parametersDecorator,
ParameterTranslationMode collectionParameterTranslationMode,
Func<int, RelationalTypeMapping, int> bucketizationPadding) : ExpressionVisitor
{
/// <summary>
/// Foo
/// </summary>
public int Count { get; private set; } = 0;

private readonly HashSet<SqlParameterExpression> _visitedParameters = new();

/// <summary>
/// Foo
/// </summary>
protected override Expression VisitExtension(Expression node)
{
switch (node)
{
case ValuesExpression valuesExpression when valuesExpression.ValuesParameter is { } valuesParameter:
ProcessCollectionParameter(valuesParameter, false);
break;

case InExpression inExpression when inExpression.ValuesParameter is { } valuesParameter:
ProcessCollectionParameter(valuesParameter, true);
break;

case SqlParameterExpression sqlParameterExpression:
ProcessParameter(sqlParameterExpression);
break;
}
return base.VisitExtension(node);
}

private void ProcessParameter(SqlParameterExpression sqlParameterExpression)
{
if (!_visitedParameters.Add(sqlParameterExpression))
{
return;
}

Count++;
}

private void ProcessCollectionParameter(SqlParameterExpression sqlParameterExpression, bool bucketization)
{
if (!_visitedParameters.Add(sqlParameterExpression))
{
return;
}

if ((sqlParameterExpression.TranslationMode ?? collectionParameterTranslationMode) is ParameterTranslationMode.MultipleParameters)
{
var parameters = parametersDecorator.GetAndDisableCaching();
var count = ((IEnumerable?)parameters[sqlParameterExpression.Name])?.Cast<object>().Count() ?? 0;
Count += count;
if (bucketization)
{
var elementTypeMapping = (RelationalTypeMapping)sqlParameterExpression.TypeMapping!.ElementTypeMapping!;
Count += bucketizationPadding(count, elementTypeMapping);
}
}
else
{
ProcessParameter(sqlParameterExpression);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,110 @@ public virtual async Task Parameter_collection_of_ints_Contains_int_with_huge_nu
await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => !ints.Contains(c.Int)));
}

[ConditionalFact]
public virtual Task Parameter_collection_Count_with_huge_number_of_values_over_5_operations()
{
if (NumberOfValuesForHugeParameterCollectionTests is null)
{
return Task.CompletedTask;
}

var extra1 = Enumerable.Range(1000, (int)NumberOfValuesForHugeParameterCollectionTests / 5);
var extra2 = Enumerable.Range(1000, (int)NumberOfValuesForHugeParameterCollectionTests / 5);
var extra3 = Enumerable.Range(1000, (int)NumberOfValuesForHugeParameterCollectionTests / 5);
var extra4 = Enumerable.Range(1000, (int)NumberOfValuesForHugeParameterCollectionTests / 5);
var extra5 = Enumerable.Range(1000, (int)NumberOfValuesForHugeParameterCollectionTests / 5);
var ids = new[] { 2, 999 };


return AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>()
.Where(c => ids.Count(i => i > c.Id) > 0)
.Where(c => extra1.Count(i => i > c.Id) > 0)
.Where(c => extra2.Count(i => i > c.Id) > 0)
.Where(c => extra3.Count(i => i > c.Id) > 0)
.Where(c => extra4.Count(i => i > c.Id) > 0)
.Where(c => extra5.Count(i => i > c.Id) > 0));
}

[ConditionalFact]
public virtual Task Parameter_collection_Count_with_huge_number_of_values_over_5_operations_same_parameter()
{
if (NumberOfValuesForHugeParameterCollectionTests is null)
{
return Task.CompletedTask;
}

var extra = Enumerable.Range(1000, (int)NumberOfValuesForHugeParameterCollectionTests / 5);
var ids = new[] { 2, 999 };


return AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>()
.Where(c => ids.Count(i => i > c.Id) > 0)
.Where(c => extra.Count(i => i > c.Id) > 0)
.Where(c => extra.Count(i => i > c.Id) > 0)
.Where(c => extra.Count(i => i > c.Id) > 0)
.Where(c => extra.Count(i => i > c.Id) > 0)
.Where(c => extra.Count(i => i > c.Id) > 0));
}

[ConditionalFact]
public virtual async Task Parameter_collection_of_ints_Contains_int_with_huge_number_of_values_over_5_operations()
{
if (NumberOfValuesForHugeParameterCollectionTests is null)
{
return;
}

var extra1 = Enumerable.Range(10, (int)NumberOfValuesForHugeParameterCollectionTests / 5);
var extra2 = Enumerable.Range(10, (int)NumberOfValuesForHugeParameterCollectionTests / 5);
var extra3 = Enumerable.Range(10, (int)NumberOfValuesForHugeParameterCollectionTests / 5);
var extra4 = Enumerable.Range(10, (int)NumberOfValuesForHugeParameterCollectionTests / 5);
var extra5 = Enumerable.Range(10, (int)NumberOfValuesForHugeParameterCollectionTests / 5);
var ints = new[] { 10, 999 };

await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>()
.Where(c => ints.Contains(c.Int))
.Where(c => extra1.Contains(c.Int))
.Where(c => extra2.Contains(c.Int))
.Where(c => extra3.Contains(c.Int))
.Where(c => extra4.Contains(c.Int))
.Where(c => extra5.Contains(c.Int)));
await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>()
.Where(c => !ints.Contains(c.Int))
.Where(c => !extra1.Contains(c.Int))
.Where(c => !extra2.Contains(c.Int))
.Where(c => !extra3.Contains(c.Int))
.Where(c => !extra4.Contains(c.Int))
.Where(c => !extra5.Contains(c.Int)));
}

[ConditionalFact]
public virtual async Task Parameter_collection_of_ints_Contains_int_with_huge_number_of_values_over_5_operations_same_parameter()
{
if (NumberOfValuesForHugeParameterCollectionTests is null)
{
return;
}

var extra = Enumerable.Range(10, (int)NumberOfValuesForHugeParameterCollectionTests / 5);
var ints = new[] { 10, 999 };

await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>()
.Where(c => ints.Contains(c.Int))
.Where(c => extra.Contains(c.Int))
.Where(c => extra.Contains(c.Int))
.Where(c => extra.Contains(c.Int))
.Where(c => extra.Contains(c.Int))
.Where(c => extra.Contains(c.Int)));
await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>()
.Where(c => !ints.Contains(c.Int))
.Where(c => !extra.Contains(c.Int))
.Where(c => !extra.Contains(c.Int))
.Where(c => !extra.Contains(c.Int))
.Where(c => !extra.Contains(c.Int))
.Where(c => !extra.Contains(c.Int)));
}

[ConditionalFact]
public virtual async Task Static_readonly_collection_List_of_ints_Contains_int()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,29 @@ public override async Task Parameter_collection_of_ints_Contains_int_with_huge_n
Assert.Contains("OPENJSON(@ints) WITH ([value] int '$')", Fixture.TestSqlLoggerFactory.SqlStatements[1], StringComparison.Ordinal);
}

public override async Task Parameter_collection_Count_with_huge_number_of_values_over_5_operations()
{
await base.Parameter_collection_Count_with_huge_number_of_values_over_5_operations();
}

public override async Task Parameter_collection_of_ints_Contains_int_with_huge_number_of_values_over_5_operations()
{
await base.Parameter_collection_of_ints_Contains_int_with_huge_number_of_values_over_5_operations();

Assert.Contains("@ints=", Fixture.TestSqlLoggerFactory.SqlStatements[0], StringComparison.Ordinal);
Assert.Contains("@ints=", Fixture.TestSqlLoggerFactory.SqlStatements[1], StringComparison.Ordinal);
}

public override async Task Parameter_collection_of_ints_Contains_int_with_huge_number_of_values_over_5_operations_same_parameter()
{
await base.Parameter_collection_of_ints_Contains_int_with_huge_number_of_values_over_5_operations_same_parameter();

Assert.Contains("@ints1=", Fixture.TestSqlLoggerFactory.SqlStatements[0], StringComparison.Ordinal);
Assert.Contains("@ints2=", Fixture.TestSqlLoggerFactory.SqlStatements[0], StringComparison.Ordinal);
Assert.Contains("@ints1=", Fixture.TestSqlLoggerFactory.SqlStatements[1], StringComparison.Ordinal);
Assert.Contains("@ints2=", Fixture.TestSqlLoggerFactory.SqlStatements[1], StringComparison.Ordinal);
}

public override async Task Static_readonly_collection_List_of_ints_Contains_int()
{
await base.Static_readonly_collection_List_of_ints_Contains_int();
Expand Down
Loading