Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1921,7 +1921,11 @@ protected virtual bool TryMakeNonNullable(
{
if (processedValues is null)
{
var elementClrType = values.GetType().GetSequenceType();
// We found the first null value - we need to start copying values to a new list which will be used for the rewritten parameter.
// The type of the new list must match that of the original enumerable parameter, as there may be value converters involved which
// rely on the precise element type (see #37605). We therefore get the type of the element from the original list if it implements
// IEnumerable<T>, or default to object.
var elementClrType = enumerable.GetType().TryGetElementType(typeof(IEnumerable<>)) ?? typeof(object);
processedValues = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementClrType), values.Count)!;
for (var j = 0; j < i; j++)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ public class SqlServerSqlNullabilityProcessor(
private int _openJsonAliasCounter;
private int _totalParameterCount;


/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down Expand Up @@ -300,6 +299,7 @@ protected override SqlExpression VisitIn(InExpression inExpression, bool allowOp
{
Check.DebugAssert(valuesParameter.TypeMapping is not null);
Check.DebugAssert(valuesParameter.TypeMapping.ElementTypeMapping is not null);

var elementTypeMapping = (RelationalTypeMapping)valuesParameter.TypeMapping.ElementTypeMapping;

if (TryHandleOverLimitParameters(
Expand All @@ -325,7 +325,7 @@ protected override SqlExpression VisitIn(InExpression inExpression, bool allowOp
new ColumnExpression(
columnName,
openJson.Alias,
valuesParameter.Type.GetSequenceType(),
valuesParameter.Type.GetSequenceType().UnwrapNullableType(),
elementTypeMapping,
containsNulls!.Value),
columnName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,20 @@ WHERE NOT(ARRAY_CONTAINS(@nullableInts, c["NullableInt"]))
""");
}

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

AssertSql(
"""
@nullableInts='[null,999]'

SELECT VALUE c
FROM root c
WHERE ARRAY_CONTAINS(@nullableInts, c["NullableInt"])
""");
}

public override async Task Parameter_collection_of_strings_Contains_string()
{
await base.Parameter_collection_of_strings_Contains_string();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,16 @@ public virtual async Task Parameter_collection_of_nullable_ints_Contains_nullabl
await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => !nullableInts.Contains(c.NullableInt)));
}

[ConditionalFact] // #37605
public virtual async Task Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter()
{
var nullableInts = new int?[] { null, 999 };

await AssertQuery(
ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => EF.Parameter(nullableInts).Contains(c.NullableInt)),
ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => nullableInts.Contains(c.NullableInt)));
}

[ConditionalFact]
public virtual async Task Parameter_collection_of_structs_Contains_struct()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,14 @@ WHERE [p].[NullableInt] IS NOT NULL AND [p].[NullableInt] <> @nullableInts1
""");
}

public override async Task Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter()
{
// EF.Parameter() on primitive collection (OPENJSON on SQL Server) not supported on old versions of SQL Server.
await Assert.ThrowsAsync<InvalidOperationException>(base.Parameter_collection_of_nullable_ints_Contains_nullable_int_with_EF_Parameter);

AssertSql();
}

public override async Task Parameter_collection_of_strings_Contains_string()
{
await base.Parameter_collection_of_strings_Contains_string();
Expand Down Expand Up @@ -1806,6 +1814,19 @@ SELECT COUNT(*)
""");
}

[ConditionalFact] // #37605
public virtual async Task Parameter_collection_with_null_value_Contains_null_2201_values()
{
using var context = Fixture.CreateContext();

var values = Enumerable.Range(1, 2200).Select(i => (int?)i).ToList();
values.Add(null);

await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>().Where(e => values.Contains(e.NullableInt)));

// No SQL assertion as the SQL is huge
}

[ConditionalFact]
public virtual void Check_all_tests_overridden()
=> TestHelpers.AssertAllMethodsOverridden(GetType());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,23 @@ WHERE [p].[NullableInt] IS NOT NULL AND [p].[NullableInt] <> @nullableInts1
""");
}

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

AssertSql(
"""
@nullableInts_without_nulls='[999]' (Size = 4000)

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]
FROM [PrimitiveCollectionsEntity] AS [p]
WHERE [p].[NullableInt] IN (
SELECT [n].[value]
FROM OPENJSON(@nullableInts_without_nulls) AS [n]
) OR [p].[NullableInt] IS NULL
""");
}

public override async Task Parameter_collection_of_strings_Contains_string()
{
await base.Parameter_collection_of_strings_Contains_string();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,23 @@ WHERE [p].[NullableInt] IS NOT NULL AND [p].[NullableInt] <> @nullableInts1
""");
}

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

AssertSql(
"""
@nullableInts_without_nulls='[999]' (Size = 5)

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]
FROM [PrimitiveCollectionsEntity] AS [p]
WHERE [p].[NullableInt] IN (
SELECT [n].[value]
FROM OPENJSON(@nullableInts_without_nulls) AS [n]
) OR [p].[NullableInt] IS NULL
""");
}

public override async Task Parameter_collection_of_structs_Contains_struct()
{
await base.Parameter_collection_of_structs_Contains_struct();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,23 @@ WHERE [p].[NullableInt] IS NOT NULL AND [p].[NullableInt] <> @nullableInts1
""");
}

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

AssertSql(
"""
@nullableInts_without_nulls='[999]' (Size = 4000)

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]
FROM [PrimitiveCollectionsEntity] AS [p]
WHERE [p].[NullableInt] IN (
SELECT [n].[value]
FROM OPENJSON(@nullableInts_without_nulls) AS [n]
) OR [p].[NullableInt] IS NULL
""");
}

public override async Task Parameter_collection_of_strings_Contains_string()
{
await base.Parameter_collection_of_strings_Contains_string();
Expand Down Expand Up @@ -2568,6 +2585,19 @@ SELECT COUNT(*)
""");
}

[ConditionalFact] // #37605
public virtual async Task Parameter_collection_with_null_value_Contains_null_2201_values()
{
using var context = Fixture.CreateContext();

var values = Enumerable.Range(1, 2200).Select(i => (int?)i).ToList();
values.Add(null);

await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>().Where(e => values.Contains(e.NullableInt)));

// No SQL assertion as the SQL is huge
}

[ConditionalFact]
public virtual async Task Parameter_collection_of_ints_Contains_int_2071_values()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,23 @@ public override async Task Parameter_collection_of_nullable_ints_Contains_nullab
""");
}

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

AssertSql(
"""
@nullableInts_without_nulls='[999]' (Size = 5)

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"
FROM "PrimitiveCollectionsEntity" AS "p"
WHERE "p"."NullableInt" IN (
SELECT "n"."value"
FROM json_each(@nullableInts_without_nulls) AS "n"
) OR "p"."NullableInt" IS NULL
""");
}

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