Skip to content
Closed
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
5 changes: 3 additions & 2 deletions src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1858,11 +1858,12 @@ protected virtual bool TryMakeNonNullable(
{
// We're looking at a parameter beyond its simple nullability, so we can't use the SQL cache for this query.
var parameters = ParametersDecorator.GetAndDisableCaching();
if (parameters[collectionParameter.Name] is not IList values)
if (parameters[collectionParameter.Name] is not IEnumerable enumerable)
{
throw new UnreachableException($"Parameter '{collectionParameter.Name}' is not an IList.");
throw new UnreachableException($"Parameter '{collectionParameter.Name}' is not an IEnumerable.");
}

var values = enumerable.Cast<object?>().ToList();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks odd - you're doing ToList on the enumerable (into values), only to then iterate over values and populate another list from it (processedValues); any reason to not just do foreach on the IEnumerable (instead of the for just below), and populate processedValues directly from that, skipping the additional values copy in the middle?

(the one problem with this is the we'd move the generic list creation with MakeGenericType out and do it always, as opposed to only when there's a NULL today. But MakeGenericType is a single constant cost, whereas doing another ToList is O(N)).

BTW doing a ToList() like this would indeed make sense if it prevented double-enumeration of the enumerable (since enumerating an enumerable can be arbitrarily heavy, depending on what it actually does). However, the change as proposed here doesn't seem to prevent double-enumeration anyway: if no NULL is encountered, we just exit below (processedValues is null), and the enumerable will be enumerated again at some point later.

Finally, ideally enumerables would be enumerable/materialized very early on - in the funcletizer - so that can later just assume we have a List everywhere. That would prevent us from having to worry about this in every single place in the query pipeline. I think that would be my preferred fix.

Note: I could see your ToList solution making sense for the patch, to really keep the change super-minimal. But for 11 let's discuss the best way forward.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly I never understood the lazy creating of processedValues...

IList? processedValues = null;

for (var i = 0; i < values.Count; i++)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,20 @@ WHERE ARRAY_CONTAINS(@p, c["Id"])
""");
}

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

AssertSql(
"""
@Select='["10","a","aa"]'

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

public override async Task Inline_collection_Count_with_column_predicate_with_EF_Parameter()
{
await base.Inline_collection_Count_with_column_predicate_with_EF_Parameter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,16 @@ public virtual Task Inline_collection_Contains_with_EF_Parameter()
ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => EF.Parameter(new[] { 2, 999, 1000 }).Contains(c.Id)),
ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => new[] { 2, 999, 1000 }.Contains(c.Id)));

[ConditionalFact]
public virtual Task Inline_collection_Contains_with_IEnumerable_EF_Parameter()
{
List<string?> data = ["10", "a", "aa",];

return AssertQuery(
ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => EF.Parameter(data.Select(x => x)).Contains(c.NullableString)),
ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => data.Select(x => x).Contains(c.NullableString)));
}

[ConditionalFact]
public virtual Task Inline_collection_Count_with_column_predicate_with_EF_Parameter()
=> AssertQuery(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,9 @@ SELECT COUNT(*)
public override Task Inline_collection_Contains_with_EF_Parameter()
=> AssertCompatibilityLevelTooLow(() => base.Inline_collection_Contains_with_EF_Parameter());

public override Task Inline_collection_Contains_with_IEnumerable_EF_Parameter()
=> AssertCompatibilityLevelTooLow(() => base.Inline_collection_Contains_with_IEnumerable_EF_Parameter());

public override Task Inline_collection_Count_with_column_predicate_with_EF_Parameter()
=> AssertCompatibilityLevelTooLow(() => base.Inline_collection_Count_with_column_predicate_with_EF_Parameter());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,23 @@ FROM OPENJSON(@p) WITH ([value] int '$') AS [p0]
""");
}

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

AssertSql(
"""
@Select='["10","a","aa"]' (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].[NullableString] IN (
SELECT [s].[value]
FROM OPENJSON(@Select) WITH ([value] nvarchar(max) '$') AS [s]
)
""");
}

public override async Task Inline_collection_Count_with_column_predicate_with_EF_Parameter()
{
await base.Inline_collection_Count_with_column_predicate_with_EF_Parameter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,23 @@ FROM OPENJSON(@p) WITH ([value] int '$') AS [p0]
""");
}

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

AssertSql(
"""
@Select='["10","a","aa"]' (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].[NullableString] IN (
SELECT [s].[value]
FROM OPENJSON(@Select) WITH ([value] nvarchar(max) '$') AS [s]
)
""");
}

public override async Task Inline_collection_Count_with_column_predicate_with_EF_Parameter()
{
await base.Inline_collection_Count_with_column_predicate_with_EF_Parameter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,23 @@ FROM OPENJSON(@p) WITH ([value] int '$') AS [p0]
""");
}

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

AssertSql(
"""
@Select='["10","a","aa"]' (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].[NullableString] IN (
SELECT [s].[value]
FROM OPENJSON(@Select) WITH ([value] nvarchar(max) '$') AS [s]
)
""");
}

public override async Task Inline_collection_Count_with_column_predicate_with_EF_Parameter()
{
await base.Inline_collection_Count_with_column_predicate_with_EF_Parameter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,23 @@ FROM json_each(@p) AS "p0"
""");
}

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

AssertSql(
"""
@Select='["10","a","aa"]' (Size = 15)

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"."NullableString" IN (
SELECT "s"."value"
FROM json_each(@Select) AS "s"
)
""");
}

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