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
18 changes: 13 additions & 5 deletions src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,10 @@ private static RelationalTypeMappingParameters CreateParameters(string storeType
{
var providerElementType = elementConverter.ProviderClrType;

// Nullability has been unwrapped on the element converter's provider CLR type, so add it back here if needed
if (elementType.IsNullableValueType())
// Nullability has been unwrapped on the element converter's provider CLR type, so add it back here if needed.
// object can contain null, and a value-type provider element cannot store that null unless it is nullable.
if (elementType.IsNullableValueType()
|| (elementType.UnwrapNullableType() == typeof(object) && providerElementType.IsValueType))
{
providerElementType = providerElementType.MakeNullable();
}
Expand All @@ -132,20 +134,26 @@ private static RelationalTypeMappingParameters CreateParameters(string storeType
var comparer = typeof(TCollection).IsArray && typeof(TCollection).GetArrayRank() > 1
? null // TODO: Value comparer for multidimensional arrays
: (ValueComparer?)Activator.CreateInstance(
elementType.IsNullableValueType() || elementMapping.Comparer.Type.IsNullableValueType()
elementType.IsNullableValueType()
|| (elementType.IsValueType && elementMapping.Comparer.Type.IsNullableValueType())
? typeof(ListOfNullableValueTypesComparer<,>)
.MakeGenericType(typeof(TConcreteCollection), elementType.UnwrapNullableType())
: elementType.IsValueType
? typeof(ListOfValueTypesComparer<,>).MakeGenericType(typeof(TConcreteCollection), elementType)
: typeof(ListOfReferenceTypesComparer<,>).MakeGenericType(typeof(TConcreteCollection), elementType),
elementMapping.Comparer.ToNullableComparer(elementType)!);
// ComposeConversion adapts a value comparer to object. Typed elements keep ToNullableComparer.
elementType.UnwrapNullableType() == typeof(object)
? elementMapping.Comparer.ComposeConversion(elementType)!
: elementMapping.Comparer.ToNullableComparer(elementType)!);
#pragma warning restore EF1001

var elementJsonReaderWriter = elementMapping.JsonValueReaderWriter;

if (elementJsonReaderWriter is not null)
{
if (!typeof(TElement).UnwrapNullableType().IsAssignableTo(elementJsonReaderWriter.ValueType))
// object[] is a valid wider element type over a converted value (for example int stored as bigint).
if (typeof(TElement).UnwrapNullableType() != typeof(object)
&& !typeof(TElement).UnwrapNullableType().IsAssignableTo(elementJsonReaderWriter.ValueType))
{
throw new InvalidOperationException(
$"When building an array mapping over '{typeof(TElement).Name}', the JsonValueReaderWriter for element mapping '{elementMapping.GetType().Name}' is incorrect ('{elementJsonReaderWriter.ValueType.Name}' instead of '{typeof(TElement).UnwrapNullableType()}', the JsonValueReaderWriter is '{elementJsonReaderWriter.GetType().Name}').");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public NpgsqlArrayConverter(ValueConverter? elementConverter)

if (elementConverter is not null)
{
if (modelElementType.UnwrapNullableType() != elementConverter.ModelClrType.UnwrapNullableType())
if (!ElementTypeMatches(modelElementType, elementConverter.ModelClrType))
{
throw new ArgumentException(
$"The element's value converter model type ({elementConverter.ModelClrType}), doesn't match the array's ({modelElementType})");
Expand Down Expand Up @@ -308,6 +308,11 @@ elementConversionExpression is null
return Lambda<Func<TInput, TOutput>>(body, input);
}

// object is a valid wider element type (e.g. object[] with an int converter). Other mismatches stay rejected.
private static bool ElementTypeMatches(Type collectionElementType, Type converterClrType)
=> collectionElementType.UnwrapNullableType() == converterClrType.UnwrapNullableType()
|| collectionElementType.UnwrapNullableType() == typeof(object);

private static Expression ForLoop(
ParameterExpression loopVar,
Expression initValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,46 @@ public virtual async Task Parameter_collection_Dictionary_Values_with_value_conv
""");
}

[ConditionalTheory]
[InlineData(false)]
[InlineData(true)]
public virtual async Task Parameter_collection_of_objects_with_value_converter_Contains(bool useList)
{
if (useList)
{
List<object?> values = [null, new WrappedId(22)];

await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => values.Contains(c.WrappedId)));
await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => values.Contains(c.NullableWrappedId)));
}
else
{
object?[] values = [null, new WrappedId(22)];

await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => values.Contains(c.WrappedId)));
await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => values.Contains(c.NullableWrappedId)));
}

AssertSql(
"""
@values={ NULL
'22' } (DbType = Object)

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."WrappedId" = ANY (@values)
""",
//
"""
@values={ NULL
'22' } (DbType = Object)

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."NullableWrappedId" = ANY (@values) OR (p."NullableWrappedId" IS NULL AND array_position(@values, NULL) IS NOT NULL)
""");
}

public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int()
{
await base.Parameter_collection_ImmutableArray_of_ints_Contains_int();
Expand Down
101 changes: 101 additions & 0 deletions test/EFCore.PG.Tests/Storage/NpgsqlArrayValueConverterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,107 @@ public void Can_convert_non_nullable_lists_with_nullable_element_converter()
Assert.Equal([1, 2], converter.ConvertFromProviderExpression.Compile()(["1", "2"]));
}

[ConditionalFact]
public void Can_convert_object_arrays_through_an_element_converter()
{
var elementConverter = new ValueConverter<int, long>(v => v, v => checked((int)v));
var converter = new NpgsqlArrayConverter<object[], object[], long[]>(elementConverter);

var toProvider = (Func<object[], long[]>)converter.ConvertToProviderExpression.Compile();
Assert.Equal([1L, 2L], toProvider([1, 2]));
Assert.Null(toProvider(null));

var fromProvider = (Func<long[], object[]>)converter.ConvertFromProviderExpression.Compile();
Assert.Equal(new object[] { 1, 2 }, fromProvider([1L, 2L]));
Assert.Null(fromProvider(null));
}

[ConditionalFact]
public void Can_convert_object_lists_through_an_element_converter()
{
var elementConverter = new ValueConverter<int, long>(v => v, v => checked((int)v));
var converter = new NpgsqlArrayConverter<List<object>, List<object>, long[]>(elementConverter);

var toProvider = (Func<List<object>, long[]>)converter.ConvertToProviderExpression.Compile();
Assert.Equal([1L, 2L], toProvider([1, 2]));

var fromProvider = (Func<long[], List<object>>)converter.ConvertFromProviderExpression.Compile();
Assert.Equal(new List<object> { 1, 2 }, fromProvider([1L, 2L]));
}

[ConditionalFact]
public void Can_convert_object_arrays_through_a_string_element_converter()
{
var elementConverter = new ValueConverter<int, string>(v => v.ToString(), v => int.Parse(v));
var converter = new NpgsqlArrayConverter<object[], object[], string[]>(elementConverter);

var toProvider = (Func<object[], string[]>)converter.ConvertToProviderExpression.Compile();
Assert.Equal(new string[] { "1", null, "2" }, toProvider(new object[] { 1, null, 2 }));

var fromProvider = (Func<string[], object[]>)converter.ConvertFromProviderExpression.Compile();
Assert.Equal(new object[] { 1, null, 2 }, fromProvider(new string[] { "1", null, "2" }));
}

[ConditionalFact]
public void Can_convert_object_lists_through_a_string_element_converter()
{
var elementConverter = new ValueConverter<int, string>(v => v.ToString(), v => int.Parse(v));
var converter = new NpgsqlArrayConverter<List<object>, List<object>, string[]>(elementConverter);

var toProvider = (Func<List<object>, string[]>)converter.ConvertToProviderExpression.Compile();
Assert.Equal(new string[] { "1", null, "2" }, toProvider(new List<object> { 1, null, 2 }));

var fromProvider = (Func<string[], List<object>>)converter.ConvertFromProviderExpression.Compile();
Assert.Equal(new List<object> { 1, null, 2 }, fromProvider(new string[] { "1", null, "2" }));
}

[ConditionalFact]
public void Can_convert_object_arrays_through_a_string_to_string_converter()
{
var elementConverter = new ValueConverter<string, string>(v => v, v => v);
var converter = new NpgsqlArrayConverter<object[], object[], string[]>(elementConverter);

var toProvider = (Func<object[], string[]>)converter.ConvertToProviderExpression.Compile();
Assert.Equal(new string[] { "a", null }, toProvider(new object[] { "a", null }));
}

[ConditionalFact]
public void Can_preserve_null_elements_in_object_arrays()
{
var elementConverter = new ValueConverter<int, long>(v => v, v => checked((int)v));
var converter = new NpgsqlArrayConverter<object[], object[], long?[]>(elementConverter);

var toProvider = (Func<object[], long?[]>)converter.ConvertToProviderExpression.Compile();
Assert.Equal(new long?[] { null, 2L }, toProvider(new object[] { null, 2 }));

var fromProvider = (Func<long?[], object[]>)converter.ConvertFromProviderExpression.Compile();
Assert.Equal(new object[] { null, 2 }, fromProvider(new long?[] { null, 2L }));
}

[ConditionalFact]
public void Can_preserve_null_elements_in_object_lists()
{
var elementConverter = new ValueConverter<int, long>(v => v, v => checked((int)v));
var converter = new NpgsqlArrayConverter<List<object>, List<object>, long?[]>(elementConverter);

var toProvider = (Func<List<object>, long?[]>)converter.ConvertToProviderExpression.Compile();
Assert.Equal(new long?[] { null, 2L }, toProvider(new List<object> { null, 2 }));

var fromProvider = (Func<long?[], List<object>>)converter.ConvertFromProviderExpression.Compile();
Assert.Equal(new List<object> { null, 2 }, fromProvider(new long?[] { null, 2L }));
}

[ConditionalFact]
public void Rejects_an_element_converter_whose_type_does_not_match_the_array()
{
var elementConverter = new ValueConverter<int, long>(v => v, v => checked((int)v));

var exception = Assert.Throws<ArgumentException>(
() => new NpgsqlArrayConverter<long[], long[], long[]>(elementConverter));

Assert.Contains("doesn't match", exception.Message);
}

private enum Beatles
{
John = 7,
Expand Down