diff --git a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs index 6abd9031d..3fd39ac8c 100644 --- a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs +++ b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs @@ -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) { diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs index 3ae21fcec..b986d9122 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs @@ -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 ?? ""}')."); + $"When building an array mapping over '{typeof(TElement).Name}', the JsonValueReaderWriter for element mapping '{elementMapping.GetType().Name}' is incorrect ('{elementMapping.JsonValueReaderWriter?.GetType().Name ?? ""}' 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 diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index bfd022647..3c9baa486 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -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. diff --git a/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs index ef84acab7..7700cb224 100644 --- a/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs @@ -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[] + { + new(5, 15), + new(40, 50) + }; + + await AssertQuery( + async, + ss => ss.Set().Where(e => ranges.Contains(e.Int)), + ss => ss.Set().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); diff --git a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseCleaner.cs b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseCleaner.cs index a1805342a..f4f5c2071 100644 --- a/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseCleaner.cs +++ b/test/EFCore.PG.FunctionalTests/TestUtilities/NpgsqlDatabaseCleaner.cs @@ -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))