Skip to content
Open
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
27 changes: 15 additions & 12 deletions src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,22 +232,25 @@ public override DbParameter CreateParameter(
{
// In queries which compose non-server-correlated LINQ operators over an array parameter (e.g. Where(b => ids.Skip(1)...) we
// get an enumerable parameter value that isn't an array/list - but those aren't supported at the Npgsql ADO level.
// Detect this here and evaluate the enumerable to get a fully materialized List.
// Note that when we have a value converter (e.g. for HashSet), we don't want to convert it to a List, since the value converter
// expects the original type.
// Detect this here and evaluate the enumerable to get a fully materialized TConcreteCollection (array or List).
// Note that when we have a value converter (e.g. for HashSet), we don't want to convert values that already match
// the converter's model type, since the value converter expects that original type.
// However, if the value's collection shape differs from the converter model type (e.g. List<MyId> vs MyId[] after
// type mapping inference for Intersect().Any() → &&), normalize to TConcreteCollection so Sanitize succeeds.
// TODO: Make Npgsql support IList<> instead of only arrays and List<>
if (value is not null && Converter is null && !value.GetType().IsArrayOrGenericList())
if (value is not null
&& ((Converter is null && !value.GetType().IsArrayOrGenericList())
|| (Converter is not null && !Converter.ModelClrType.IsInstanceOfType(value))))
{
switch (value)
var castedElements = value switch
{
case IEnumerable<TElement> elements:
value = elements.ToList();
break;
IEnumerable<TElement> elements => elements,
IEnumerable elements => elements.Cast<TElement>(),
_ => throw new InvalidOperationException(
$"Cannot create a parameter for {GetType().Name} from value of type '{value.GetType().Name}'")
};

case IEnumerable elements:
value = elements.Cast<TElement>().ToList();
break;
}
value = typeof(TConcreteCollection).IsArray ? castedElements.ToArray() : castedElements.ToList();
}

var param = base.CreateParameter(command, name, value, nullable, direction);
Expand Down
62 changes: 62 additions & 0 deletions test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Immutable;
using Microsoft.EntityFrameworkCore.TestModels.Array;
using Npgsql.EntityFrameworkCore.PostgreSQL.Internal;

Expand Down Expand Up @@ -867,6 +868,67 @@ public virtual async Task All_Contains()

#endregion Any/All

#region Intersect

[ConditionalFact]
public virtual async Task Intersect_parameter_list_over_value_converted_array()
{
List<SomeEnum> toFindList = [SomeEnum.One, SomeEnum.Three, SomeEnum.Eight];

await AssertQuery(ss => ss.Set<ArrayEntity>().Where(e => e.ValueConvertedArrayOfEnum.Intersect(toFindList).Any()));

AssertSql(
"""
@toFindList={ 'One'
'Three'
'Eight' } (DbType = Object)

SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15"
FROM "SomeEntities" AS s
WHERE s."ValueConvertedArrayOfEnum" && @toFindList
""");
}

[ConditionalFact]
public virtual async Task Intersect_parameter_immutable_list_over_value_converted_array()
{
ImmutableList<SomeEnum> toFindImmutableList = [SomeEnum.One, SomeEnum.Three, SomeEnum.Eight];

await AssertQuery(ss => ss.Set<ArrayEntity>().Where(e => e.ValueConvertedArrayOfEnum.Intersect(toFindImmutableList).Any()));

AssertSql(
"""
@toFindImmutableList={ 'One'
'Three'
'Eight' } (DbType = Object)

SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15"
FROM "SomeEntities" AS s
WHERE s."ValueConvertedArrayOfEnum" && @toFindImmutableList
""");
}

[ConditionalFact]
public virtual async Task Intersect_parameter_array_over_value_converted_array()
{
SomeEnum[] toFindArray = [SomeEnum.One, SomeEnum.Three, SomeEnum.Eight];

await AssertQuery(ss => ss.Set<ArrayEntity>().Where(e => e.ValueConvertedArrayOfEnum.Intersect(toFindArray).Any()));

AssertSql(
"""
@toFindArray={ 'One'
'Three'
'Eight' } (DbType = Object)

SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15"
FROM "SomeEntities" AS s
WHERE s."ValueConvertedArrayOfEnum" && @toFindArray
""");
}

#endregion

#region Other translations

[ConditionalFact]
Expand Down
62 changes: 62 additions & 0 deletions test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Immutable;
using Microsoft.EntityFrameworkCore.TestModels.Array;

namespace Microsoft.EntityFrameworkCore.Query;
Expand Down Expand Up @@ -873,6 +874,67 @@ public virtual async Task All_Contains()

#endregion Any/All

#region Intersect

[ConditionalFact]
public virtual async Task Intersect_parameter_array_over_value_converted_list()
{
SomeEnum[] toFindArray = [SomeEnum.One, SomeEnum.Three, SomeEnum.Eight];

await AssertQuery(ss => ss.Set<ArrayEntity>().Where(e => e.ValueConvertedListOfEnum.Intersect(toFindArray).Any()));

AssertSql(
"""
@toFindArray={ 'One'
'Three'
'Eight' } (DbType = Object)

SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15"
FROM "SomeEntities" AS s
WHERE s."ValueConvertedListOfEnum" && @toFindArray
""");
}

[ConditionalFact]
public virtual async Task Intersect_parameter_immutable_list_over_value_converted_list()
{
ImmutableList<SomeEnum> toFindImmutableList = [SomeEnum.One, SomeEnum.Three, SomeEnum.Eight];

await AssertQuery(ss => ss.Set<ArrayEntity>().Where(e => e.ValueConvertedListOfEnum.Intersect(toFindImmutableList).Any()));

AssertSql(
"""
@toFindImmutableList={ 'One'
'Three'
'Eight' } (DbType = Object)

SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15"
FROM "SomeEntities" AS s
WHERE s."ValueConvertedListOfEnum" && @toFindImmutableList
""");
}

[ConditionalFact]
public virtual async Task Intersect_parameter_list_over_value_converted_list()
{
List<SomeEnum> toFindList = [SomeEnum.One, SomeEnum.Three, SomeEnum.Eight];

await AssertQuery(ss => ss.Set<ArrayEntity>().Where(e => e.ValueConvertedListOfEnum.Intersect(toFindList).Any()));

AssertSql(
"""
@toFindList={ 'One'
'Three'
'Eight' } (DbType = Object)

SELECT s."Id", s."ArrayContainerEntityId", s."ArrayOfStringConvertedToDelimitedString", s."Byte", s."ByteArray", s."Bytea", s."EnumConvertedToInt", s."EnumConvertedToString", s."IList", s."IntArray", s."IntList", s."ListOfStringConvertedToDelimitedString", s."NonNullableText", s."NullableEnumConvertedToString", s."NullableEnumConvertedToStringWithNonNullableLambda", s."NullableIntArray", s."NullableIntList", s."NullableStringArray", s."NullableStringList", s."NullableText", s."StringArray", s."StringList", s."ValueConvertedArrayOfEnum", s."ValueConvertedListOfEnum", s."Varchar10", s."Varchar15"
FROM "SomeEntities" AS s
WHERE s."ValueConvertedListOfEnum" && @toFindList
""");
}

#endregion

#region Other translations

// TODO: https://github.com/dotnet/efcore/issues/30669
Expand Down
43 changes: 43 additions & 0 deletions test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Immutable;
using System.Net;
using System.Net.NetworkInformation;
using System.Text.Json;
Expand Down Expand Up @@ -263,6 +264,48 @@ public void Array_over_type_mapping_with_value_converter_by_clr_type_list()
public void Array_over_type_mapping_with_value_converter_by_store_type()
=> Array_over_type_mapping_with_value_converter(CreateTypeMappingSource().FindMapping("ltree[]"), typeof(List<LTree>));

[Theory]
[InlineData(typeof(LTree[]))]
[InlineData(typeof(List<LTree>))]
public void CreateParameter_with_value_converter_accepts_list(Type mappingType)
{
var mapping = CreateTypeMappingSource().FindMapping(mappingType)!;
Assert.NotNull(mapping.Converter);
var parameter = mapping.CreateParameter(
new NpgsqlCommand(),
"p",
new List<LTree> { new("foo"), new("bar") });
Assert.Equal(["foo", "bar"], Assert.IsType<string[]>(parameter.Value));
}

[Theory]
[InlineData(typeof(LTree[]))]
[InlineData(typeof(List<LTree>))]
public void CreateParameter_with_value_converter_accepts_immutable_list(Type mappingType)
{
var mapping = CreateTypeMappingSource().FindMapping(mappingType)!;
Assert.NotNull(mapping.Converter);
var parameter = mapping.CreateParameter(
new NpgsqlCommand(),
"p",
ImmutableList.Create(new LTree("foo"), new LTree("bar")));
Assert.Equal(["foo", "bar"], Assert.IsType<string[]>(parameter.Value));
}

[Theory]
[InlineData(typeof(LTree[]))]
[InlineData(typeof(List<LTree>))]
public void CreateParameter_with_value_converter_accepts_array(Type mappingType)
{
var mapping = CreateTypeMappingSource().FindMapping(mappingType)!;
Assert.NotNull(mapping.Converter);
var parameter = mapping.CreateParameter(
new NpgsqlCommand(),
"p",
new LTree[] { new("foo"), new("bar") });
Assert.Equal(["foo", "bar"], Assert.IsType<string[]>(parameter.Value));
}

private void Array_over_type_mapping_with_value_converter(CoreTypeMapping mapping, Type expectedType)
{
var arrayMapping = (NpgsqlArrayTypeMapping)mapping;
Expand Down