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
65 changes: 53 additions & 12 deletions src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,50 @@ private static RelationalTypeMappingParameters CreateParameters(string storeType
storeType);
}

private IEnumerable<TElement> AsElementEnumerable(object value)
=> value switch
{
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}'")
};

private static TConcreteCollection CreateInstance(int? count)
=> (count, typeof(TConcreteCollection)) switch
{
({ } c, var type) when type.GetConstructor([typeof(int)]) is { } ctorWithSize
=> (TConcreteCollection)ctorWithSize.Invoke([c]),
var (_, type) when type.GetConstructor([]) is { } ctor
=> (TConcreteCollection)ctor.Invoke(null),
var (_, type) => throw new InvalidOperationException(
$"Type {type.Name} cannot be instantiated as it does not have a public parameterless constructor")
};

private static object Materialize(IEnumerable<TElement> elements)
{
if (typeof(TConcreteCollection).IsArray)
{
return elements.ToArray();
}

var count = elements.TryGetNonEnumeratedCount(out var c) ? c : (int?)null;
var collection = CreateInstance(count);

if (collection is not ICollection<TElement> destination)
{
throw new InvalidOperationException(
$"Type {typeof(TConcreteCollection).Name} cannot be populated (no ICollection<{typeof(TElement).Name}>).");
}

foreach (var element in elements)
{
destination.Add(element);
}

return collection;
}

/// <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 @@ -233,21 +277,18 @@ 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.
// 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<T> vs T[] 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())
{
switch (value)
{
case IEnumerable<TElement> elements:
value = elements.ToList();
break;

case IEnumerable elements:
value = elements.Cast<TElement>().ToList();
break;
}
value = AsElementEnumerable(value).ToList();
}
else if (value is not null && Converter is not null && !Converter.ModelClrType.IsInstanceOfType(value))
{
value = Materialize(AsElementEnumerable(value));
}

var param = base.CreateParameter(command, name, value, nullable, direction);
Expand Down
105 changes: 105 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,110 @@ 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
""");
}

[ConditionalFact]
public virtual async Task Intersect_parameter_hash_set_over_value_converted_array()
{
HashSet<SomeEnum> toFindHashSet = [SomeEnum.One, SomeEnum.Three, SomeEnum.Eight];

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

AssertSql(
"""
@toFindHashSet={ '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" && @toFindHashSet
""");
}

[ConditionalFact]
public virtual async Task Intersect_parameter_non_collection_enumerable_over_value_converted_array()
{
var toFindEnumerable = new List<SomeEnum>
{
SomeEnum.One,
SomeEnum.Three,
SomeEnum.Eight
}.Where(_ => true);

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

AssertSql(
"""
@toFindEnumerable={ '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" && @toFindEnumerable
""");
}

#endregion

#region Other translations

[ConditionalFact]
Expand Down
105 changes: 105 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,110 @@ 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
""");
}

[ConditionalFact]
public virtual async Task Intersect_parameter_hash_set_over_value_converted_list()
{
HashSet<SomeEnum> toFindHashSet = [SomeEnum.One, SomeEnum.Three, SomeEnum.Eight];

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

AssertSql(
"""
@toFindHashSet={ '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" && @toFindHashSet
""");
}

[ConditionalFact]
public virtual async Task Intersect_parameter_non_collection_enumerable_over_value_converted_list()
{
var toFindEnumerable = new List<SomeEnum>
{
SomeEnum.One,
SomeEnum.Three,
SomeEnum.Eight
}.Where(_ => true);

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

AssertSql(
"""
@toFindEnumerable={ '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" && @toFindEnumerable
""");
}

#endregion

#region Other translations

// TODO: https://github.com/dotnet/efcore/issues/30669
Expand Down
Loading