Skip to content

Commit

Permalink
Fix containment inference of multirange mapping from non-range item (#…
Browse files Browse the repository at this point in the history
…3013)

Fixes #3012
  • Loading branch information
roji authored Dec 11, 2023
1 parent 0a795e3 commit 1f724d3
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 6 deletions.
2 changes: 0 additions & 2 deletions src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -871,8 +871,6 @@ NpgsqlMultirangeTypeMapping multirangeTypeMapping
// (e.g. IP address containment)
containerMapping = _typeMappingSource.FindContainerMapping(container.Type, containeeMapping, Dependencies.Model);

// containerMapping = _typeMappingSource.FindContainerMapping(container.Type, containeeMapping);

// Apply the inferred mapping to the container, or fall back to the default type mapping
if (containerMapping is not null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ private static RelationalTypeMappingParameters CreateParameters(string storeType
if (elementJsonReaderWriter is not null && elementJsonReaderWriter.ValueType != typeof(TElement).UnwrapNullableType())
{
throw new InvalidOperationException(
$"When '{elementJsonReaderWriter.ValueType}', '{typeof(TElement).UnwrapNullableType()}' building an array mapping, the JsonValueReaderWriter for element mapping '{elementMapping.GetType().Name}' is incorrect ('{elementMapping.JsonValueReaderWriter?.GetType().Name ?? "<null>"}').");
$"When building an array mapping over '{typeof(TElement).Name}', the JsonValueReaderWriter for element mapping '{elementMapping.GetType().Name}' is incorrect ('{elementMapping.JsonValueReaderWriter?.GetType().Name ?? "<null>"}' instead of '{typeof(TElement).UnwrapNullableType()}').");
}

// If there's no JsonValueReaderWriter on the element, we also don't set one on its array (this is for rare edge cases such as
Expand Down
20 changes: 20 additions & 0 deletions src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,26 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType)
return rangeStoreType is null ? null : FindMapping(containerClrType, rangeStoreType);
}

// If this containment of a range within a multirange, just flow down to the general collection mapping logic; multiranges are
// handled just like normal collections over ranges (since they can be unnested).
// However, we also support containment of the base type (e.g. int) directly in its multirange (e.g. int4range). A multirange
// is *not* a collection over the base type, so handle that specific case here.
if (containerClrType.IsMultirange() && !containeeTypeMapping.ClrType.IsRange())
{
var multirangeStoreType = containeeTypeMapping.StoreType switch
{
"int" or "integer" => "int4multirange",
"bigint" => "int8multirange",
"decimal" or "numeric" => "nummultirange",
"date" => "datemultirange",
"timestamp" or "timestamp without time zone" => "tsmultirange",
"timestamptz" or "timestamp with time zone" => "tstzmultirange",
_ => null
};

return !_supportsMultiranges || multirangeStoreType is null ? null : FindMapping(containerClrType, multirangeStoreType);
}

// Then, try to find the mapping with the containee mapping as the element type mapping.
// This is the standard EF lookup mechanism, and takes care of regular arrays and multiranges, which are supported as full primitive
// collections.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,32 @@ public override async Task Parameter_collection_null_Contains(bool async)
""");
}

[ConditionalTheory] // #3012
[MinimumPostgresVersion(14, 0)] // Multiranges were introduced in PostgreSQL 14
[MemberData(nameof(IsAsyncData))]
public virtual async Task Parameter_collection_of_ranges_Contains(bool async)
{
var ranges = new NpgsqlRange<int>[]
{
new(5, 15),
new(40, 50)
};

await AssertQuery(
async,
ss => ss.Set<PrimitiveCollectionsEntity>().Where(e => ranges.Contains(e.Int)),
ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => ranges.Any(p => p.LowerBound <= c.Int && p.UpperBound >= c.Int)));

AssertSql(
"""
@__ranges_0={ '[5,15]', '[40,50]' } (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."String", p."Strings"
FROM "PrimitiveCollectionsEntity" AS p
WHERE @__ranges_0 @> p."Int"
""");
}

public override async Task Column_collection_of_ints_Contains(bool async)
{
await base.Column_collection_of_ints_Contains(async);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,13 @@ private void DropCollations(NpgsqlConnection conn)
return;
}

const string getUserCollations = @"SELECT nspname, collname
const string getUserCollations =
"""
SELECT nspname, collname
FROM pg_collation coll
JOIN pg_namespace ns ON ns.oid=coll.collnamespace
JOIN pg_authid auth ON auth.oid = coll.collowner WHERE rolname <> 'postgres';
";
JOIN pg_authid auth ON auth.oid = coll.collowner WHERE nspname <> 'pg_catalog';
""";

(string Schema, string Name)[] userDefinedTypes;
using (var cmd = new NpgsqlCommand(getUserCollations, conn))
Expand Down

0 comments on commit 1f724d3

Please sign in to comment.